PageSourceSearch

https://gettheminutes.com/v2-assets/release-20260930-realtime/automations.mjs

js gettheminutes.com collected 2026-10-02 01:30:31 UTC 15,785 bytes, 177 lines download raw bytes

1import {createMenu} from './menu.mjs';
2import {icon} from './icons.mjs';
3import {scopeTag} from './ui.mjs';
4
5export const WEBHOOK_EVENTS = [
6  ['meeting.transcript_ready', 'Final transcript is available'],
7  ['output.created', 'An action’s result was saved (minutes or another)'],
8  ['action_items.created', 'Action items were extracted'],
9];
10const el = (tag, text, cls) => {
11  const node = document.createElement(tag);
12  if (text !== undefined) node.textContent = text;
13  if (cls) node.className = cls;
14  return node;
15};
16const BUTTON_CLASSES = {primary: 'btn btn--primary', danger: 'btn btn--danger'};
17const button = (text, action, cls) => {
18  const node = el('button', text, BUTTON_CLASSES[cls] || ['btn', cls].filter(Boolean).join(' ')); node.type = 'button'; node.onclick = action; return node;
19};
20
21/** Workspace-scoped management. The service is authoritative for role and delivery state.
22 * Settings › Automations supplies the page title and navigation; the webhooks card names its
23 * scope. */
24export async function openAutomations({api, host, identity}) {
25  let disposed = false, revision = 0, loading=false, snapshot=null;
26  let menus=[];const testRequests=new Map();
27  const draftKey=identity.user_id&&identity.workspace_id?`minutes:webhook-draft:${identity.user_id}:${identity.workspace_id}`:null;
28  let pendingCreate=null;
29  try{const draft=JSON.parse(sessionStorage.getItem(draftKey)||'null');if(draft&&typeof draft.operation_id==='string'&&typeof draft.url==='string'&&typeof draft.name==='string'&&Array.isArray(draft.events))pendingCreate=draft;}catch{}
30  const keepDraft=value=>{pendingCreate=value;try{if(draftKey){if(value)sessionStorage.setItem(draftKey,JSON.stringify(value));else sessionStorage.removeItem(draftKey);}}catch{}add.textContent=value?'Finish webhook setup':'Add webhook';if(!value)add.prepend(icon('plus'));};
31  const page = el('section', undefined, 'automations-page');
32  const card = el('section', undefined, 'automations-card');
33  const header = el('div', undefined, 'automations-card-heading');
34  const intro = el('div'); intro.append(el('h2', 'Webhooks'), el('p', 'A signed POST when these events happen. Failures retry 5 times over 24 hours, then pause.'));
35  const status = el('p', '', 'automation-status'); status.setAttribute('role', 'status');
36  const list = el('div', undefined, 'automation-list');
37  const columns=el('div',undefined,'automation-columns');for(const label of ['Endpoint · HTTPS','Events','Status'])columns.append(el('span',label));
38  const add = button(pendingCreate?'Finish webhook setup':'Add webhook', () => edit(), 'primary'); add.disabled = true;
39  if(!pendingCreate)add.prepend(icon('plus'));
40  const refresh=button("Refresh status",()=>load(),"btn--quiet btn--sm automation-refresh");refresh.prepend(icon('refresh-cw'));
41  const title=el('div',undefined,'automations-card-title');title.append(intro.firstElementChild,scopeTag(identity.workspace_kind==='team'?'team':'you'));intro.prepend(title);
42  header.append(intro, add); card.append(header, status, columns, list);
43  const footer=el('div',undefined,'automations-card-footer');footer.append(refresh);
44  const shortcuts=el('section',undefined,'automations-shortcuts');
45  const shortcutIcon=el('span',undefined,'automations-shortcuts-icon');shortcutIcon.append(icon('zap'));
46  const shortcutCopy=el('div');shortcutCopy.append(el('h2','Shortcuts on iPhone'),el('p','Members can use Minutes actions in Shortcuts: Get meeting minutes, Get action items, Start recording. They run on that person’s iPhone. Read actions return saved outputs; Shortcuts can’t start on our events, so use webhooks for that.'));
47  shortcuts.append(shortcutIcon,shortcutCopy);
48  page.append(shortcuts, card, footer, el('p', 'Using Zapier or Make? Create a “catch webhook” step there and paste its URL here — no separate integration needed.', 'muted'));
49  host.replaceChildren(page);
50  const current = () => !disposed && page.isConnected;
51  const errorText = 'Webhooks could not be loaded. Try again.';
52  async function load({quiet=false}={}) {
53    const stamp = ++revision;loading=true;
54    if(!quiet)status.replaceChildren(el('span', 'Loading webhooks…'));
55    try {
56      const result = await api('/api/v2/webhooks');
57      if (!current() || stamp !== revision) return;
58      add.disabled = !result.can_manage;
59      status.textContent = result.can_manage ? '' : 'Only workspace admins can manage webhooks.';
60      const next=JSON.stringify(result);
61      if(next===snapshot)return;
62      if(quiet && (page.querySelector('dialog[open]') || list.contains(document.activeElement)))return;
63      snapshot=next;
64      menus.forEach(menu=>menu.close());menus=[];list.replaceChildren();
65      if (!result.webhooks.length) list.append(el('p', 'No webhooks yet. Add one to send meeting events to your tools.'));
66      for (const hook of result.webhooks) {
67        const row = el('article', undefined, 'automation-row');
68        const endpoint = el('div'); endpoint.append(el('code', hook.endpoint_display), el('p', hook.name || 'Webhook'));
69        const events = el('div', undefined, 'automation-events'); for (const event of hook.events) events.append(el('code', event));
70        const state = el('div'); state.append(el('span', {active:hook.last_attempt_at?'Delivering':'Waiting for events',retrying:'Failing · retrying',paused:'Paused'}[hook.status] || 'Waiting', `automation-badge ${hook.status}`));
71        state.firstElementChild.prepend(icon({active:hook.last_attempt_at?'check':'clock',retrying:'alert-triangle',paused:'clock'}[hook.status]||'clock'));
72        if (hook.last_attempt_at) state.append(el('p', `Last: ${new Date(hook.last_attempt_at).toLocaleString(undefined,{month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'})}${hook.last_status ? ` · ${hook.last_status}` : ''}`));
73        if (hook.next_attempt_at) state.append(el('p', `Next try: ${new Date(hook.next_attempt_at).toLocaleTimeString(undefined,{hour:'2-digit',minute:'2-digit'})}`));
74        if (result.can_manage) {
75          const controls = el('div', undefined, 'automation-controls');
76          const mutate = async (suffix) => {
77            controls.querySelectorAll('button').forEach(n => n.disabled = true);
78            try {
79              const body=suffix==='test'?{operation_id:testRequests.get(hook.id)||crypto.randomUUID()}:{};
80              if(suffix==='test')testRequests.set(hook.id,body.operation_id);
81              await api(`/api/v2/webhooks/${encodeURIComponent(hook.id)}/${suffix}`, {method:'POST',body:JSON.stringify(body)});
82              if(suffix==='test')testRequests.delete(hook.id);
83              if (current()){await load();if(suffix==='test')status.textContent='Test event queued. The delivery status will update after it is sent.';}
84            }
85            catch { if(current()) status.textContent='The webhook could not be updated. Try again.'; }
86            finally {controls.querySelectorAll('button').forEach(n=>n.disabled=false);}
87          };
88          const more=button('',()=>{},'btn--quiet btn--sm btn--icon');more.append(icon('more-horizontal'));more.setAttribute('aria-label',`Options for ${hook.name||hook.endpoint_display}`);
89          const menu=createMenu(more);menus.push(menu);more.onclick=()=>menu.open([
90            {label:hook.status==='paused'?'Resume':'Pause',action:()=>mutate(hook.status==='paused'?'resume':'pause')},
91            ...(hook.status==='paused'?[]:[{label:'Send test event',action:()=>mutate('test')}]),
92            {separator:true},
93            {label:'Delete webhook…',icon:'trash-2',danger:true,action:()=>remove(hook)}
94          ]);controls.append(more);
95          if (hook.status === 'retrying') controls.append(button('Try again now', () => mutate('retry')));
96          state.append(controls);
97        }
98        row.append(endpoint, events, state);list.append(row);
99      }
100    } catch { if (current() && stamp === revision) status.replaceChildren(el('span', errorText), button('Try again', load)); } finally {if(stamp===revision)loading=false;}
101  }
102  function containFocus(dialog){
103    dialog.addEventListener('keydown',event=>{
104      if(event.key!=='Tab')return;
105      const controls=[...dialog.querySelectorAll('button,input,select,[tabindex="0"]')].filter(n=>!n.disabled&&n.getClientRects().length);
106      const first=controls[0],last=controls.at(-1);
107      if(!first){event.preventDefault();return;}
108      if(event.shiftKey&&document.activeElement===first){event.preventDefault();last.focus();}
109      else if(!event.shiftKey&&document.activeElement===last){event.preventDefault();first.focus();}
110    });
111  }
112  function remove(hook){
113    const dialog=el('dialog',undefined,'webhook-delete');const title=el('h2',`Delete “${hook.name||hook.endpoint_display}”?`);title.id='webhook-delete-title';dialog.setAttribute('aria-labelledby',title.id);
114    const error=el('p');error.setAttribute('role','alert');let busy=false;
115    const cancel=button('Cancel',()=>dialog.close());const confirm=button('Delete webhook',async()=>{
116      if(busy)return;busy=true;cancel.disabled=confirm.disabled=true;
117      try{await api(`/api/v2/webhooks/${encodeURIComponent(hook.id)}`,{method:'DELETE'});dialog.close();if(current())await load();}
118      catch{error.textContent='Deletion was not confirmed. Try again.';}
119      finally{busy=false;cancel.disabled=confirm.disabled=false;}
120    },'danger');
121    dialog.append(title,el('p','This stops future events and removes queued deliveries. A request already in progress may still arrive. Meetings are unchanged.'),error,cancel,confirm);
122    dialog.addEventListener('cancel',e=>{if(busy)e.preventDefault();});dialog.addEventListener('close',()=>dialog.remove());page.append(dialog);containFocus(dialog);dialog.showModal();cancel.focus();
123  }
124  function edit() {
125    const dialog = el('dialog', undefined, 'webhook-editor');
126    const form = el('form'); const title = el('h2','Add webhook');title.id='webhook-editor-title';dialog.setAttribute('aria-labelledby',title.id);
127    const close = button('',()=>dialog.close(),'btn--quiet btn--icon');close.append(icon('x'));close.setAttribute('aria-label','Close');
128    const header=el('header');header.append(title,close);
129    const content=el('div',undefined,'webhook-editor-body');
130    const urlLabel=el('label','Endpoint URL');const url=el('input');url.type='url';url.required=true;url.placeholder='https://your-tool.example/hooks/minutes';urlLabel.append(url);
131    const nameLabel=el('label','Name · optional');const name=el('input');name.maxLength=120;nameLabel.append(name);
132    const fields=el('fieldset');fields.append(el('legend','Events'));
133    for(const [value,description] of WEBHOOK_EVENTS){const label=el('label',undefined,'webhook-event');const check=el('input');check.type='checkbox';check.value=value;check.name='event';const copy=el('span');copy.append(el('code',value),el('small',description));label.append(check,copy);fields.append(label);}
134    const error=el('p','','automation-status');error.setAttribute('role','alert');
135    const example=el('pre',JSON.stringify({event:'output.created',event_id:'example-event',meeting_id:'example-meeting',output_id:'example-output',sent_at:'2026-09-24T11:40:00Z'},null,2),'webhook-example');
136    content.append(urlLabel,el('p','Must be HTTPS. We never follow redirects.','muted'),nameLabel,fields,el('h3','Signing secret'),el('p','Your signing secret appears after you add the webhook. Save it then to verify the Minutes-Signature header (HMAC-SHA256).','muted'),el('h3','Example payload'),example,el('p','Payloads carry IDs, not meeting content. Fetch content with an API key.','muted'),error);
137    const footer=el('footer');const cancel=button('Cancel',()=>dialog.close());const save=el('button','Add webhook','btn btn--primary');save.type='submit';footer.append(cancel,save);form.append(header,content,footer);dialog.append(form);page.append(dialog);
138    let busy=false;const operationID=pendingCreate?.operation_id||crypto.randomUUID();let submitted=pendingCreate;
139    if(submitted){url.value=submitted.url;name.value=submitted.name;fields.querySelectorAll('input').forEach(n=>n.checked=submitted.events.includes(n.value));form.querySelectorAll('input').forEach(n=>n.disabled=true);save.textContent='Check and finish';error.textContent='Finish the previous request before adding another webhook. Its signing secret has not been acknowledged.';}
140    dialog.addEventListener('cancel',event=>{if(busy)event.preventDefault();});dialog.addEventListener('close',()=>dialog.remove());
141    form.onsubmit=async event=>{
142      event.preventDefault();if(busy)return;
143      const events=[...fields.querySelectorAll('input:checked')].map(n=>n.value);
144      let parsed;try{parsed=new URL(url.value);}catch{}
145      if(!parsed || parsed.protocol!=='https:' || parsed.username || parsed.password || parsed.hash){error.textContent='Enter an HTTPS endpoint without a username, password or fragment.';url.focus();return;}
146      if(!events.length){error.textContent='Choose at least one event.';fields.querySelector('input').focus();return;}
147      submitted??={operation_id:operationID,url:parsed.href,name:name.value.trim(),events};
148      keepDraft(submitted);
149      busy=true;error.textContent='Adding webhook…';form.querySelectorAll('button,input').forEach(n=>n.disabled=true);
150      try{
151        const result=await api('/api/v2/webhooks',{method:'POST',body:JSON.stringify(submitted)});
152        if(!current() || !dialog.isConnected)return;
153        content.replaceChildren(el('h3','Webhook added'),el('p','Save this signing secret, then choose Done to finish setup.'),el('code',result.signing_secret,'webhook-secret'),el('p','Check the Minutes-Signature header (HMAC-SHA256) on each request.'));
154        const copy=button('Copy signing secret',async()=>{try{await navigator.clipboard.writeText(result.signing_secret);copy.textContent='Copied';}catch{copy.textContent='Select and copy the secret above';}});
155        const testStatus=el('p');testStatus.setAttribute('role','status');let testID=crypto.randomUUID();
156        const testButton=button('Send test event',async()=>{
157          testButton.disabled=true;testStatus.textContent='Queuing test event…';
158          try{await api(`/api/v2/webhooks/${encodeURIComponent(result.id)}/test`,{method:'POST',body:JSON.stringify({operation_id:testID})});testStatus.textContent='Test event queued. Check its delivery status in the list.';testID=crypto.randomUUID();}
159          catch{testStatus.textContent='The test event was not confirmed. Try again.';}
160          finally{testButton.disabled=false;}
161        });
162        content.append(copy,testStatus);footer.replaceChildren(testButton,button('Done',()=>{keepDraft(null);dialog.close();},'primary'));close.disabled=false;copy.focus();await load();
163       }catch(failure){if(current()){
164        if([400,403,422,429].includes(failure.status)){
165          submitted=null;keepDraft(null);form.querySelectorAll('button,input').forEach(n=>n.disabled=false);
166          error.textContent=failure.status===403?'Only workspace admins can add webhooks.':'The webhook could not be added. Check the HTTPS endpoint and selected events.';
167        }else error.textContent='We could not confirm the webhook was added. Try again with the same details.';
168        save.disabled=false;save.textContent='Try again';cancel.disabled=false;close.disabled=false;
169      }}
170      finally{busy=false;}
171    };
172    containFocus(dialog);dialog.showModal();url.focus();
173  }
174  await load();
175  const poll=setInterval(()=>{if(current()&&!loading&&!document.hidden&&!page.querySelector('dialog[open]')&&!list.contains(document.activeElement))load({quiet:true});},5000);
176  return () => { disposed=true;clearInterval(poll); menus.forEach(menu=>menu.close()); ++revision; page.querySelectorAll('dialog').forEach(dialog=>dialog.close()); };
177}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.