| <!doctype html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>Board ยท amem</title> | |
| <!-- the reader's theme and contrast, set before the first paint; storage is a convenience --> | |
| <script>try { const s = localStorage, r = document.documentElement; if (s.getItem('amem-theme')) r.dataset.theme = s.getItem('amem-theme'); if (s.getItem('amem-contrast')) r.dataset.contrast = s.getItem('amem-contrast'); } catch (_) { /* private window */ }</script> | |
| <link rel="stylesheet" href="tokens.css"> | |
| <style> | |
| .file fieldset { border: 0; margin: 0; padding: 0; min-width: 0; } | |
| .file-grid { display: grid; gap: var(--am-space-4); grid-template-columns: minmax(0, 1fr); } | |
| .file-foot { display: flex; align-items: center; gap: var(--am-space-3); flex-wrap: wrap; } | |
| .groups { display: grid; gap: var(--am-space-8); } | |
| .group { display: grid; gap: var(--am-space-2); } | |
| .group > .am-hint { padding: var(--am-space-3) var(--am-space-4); border: var(--am-line) dashed var(--am-rule); border-radius: var(--am-radius); } | |
| #handoff-text { min-height: 8rem; } | |
| @media (min-width: 720px) { | |
| .file-grid { grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); } | |
| .file-grid .wide { grid-column: 1 / -1; } | |
| } | |
| </style> | |
| </head> | |
| <body class="am-body"> | |
| <svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false"><defs> | |
| <symbol id="i-mark" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7.5 3.5H3.5v17h4M16.5 3.5h4v17h-4"/><path d="M8 8.5h8M8 12h8M8 15.5h5"/></symbol> | |
| <symbol id="i-running" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="8" cy="8" r="6.2"/><circle cx="8" cy="8" r="3" fill="currentColor" stroke="none"/></symbol> | |
| <symbol id="i-connecting" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="8" cy="8" r="6.2" stroke-dasharray="2.7 2.2"/><circle cx="8" cy="8" r="1.5" fill="currentColor" stroke="none"/></symbol> | |
| <symbol id="i-done" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="8" r="6.2"/><path d="M5.3 8.2l1.9 1.9 3.6-3.9"/></symbol> | |
| <symbol id="i-ok" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2.8 8.6l3.3 3.2L13.2 4.6"/></symbol> | |
| <symbol id="i-fail" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><circle cx="8" cy="8" r="6.2"/><path d="M5.8 5.8l4.4 4.4M10.2 5.8l-4.4 4.4"/></symbol> | |
| <symbol id="i-warn" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2.2l6.3 11.2H1.7Z"/><path d="M8 6.6v3M8 11.6v.05"/></symbol> | |
| <symbol id="i-absent" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><circle cx="8" cy="8" r="6.2" stroke-dasharray="0.1 2.6"/></symbol> | |
| <symbol id="i-refused" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><circle cx="8" cy="8" r="6.2"/><path d="M3.7 12.3l8.6-8.6"/></symbol> | |
| <symbol id="i-ceiling" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 2.8h11"/><path d="M8 13.6V5.8M5.2 8.5L8 5.7l2.8 2.8"/></symbol> | |
| <symbol id="i-admit" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M6 13.8V2.4h7.2v11.4"/><path d="M1.6 8.1h6.6M6 5.9l2.2 2.2L6 10.3"/></symbol> | |
| <symbol id="i-handoff" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2 9.6h3.2l1 1.7h3.6l1-1.7H14v3a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1Z"/><path d="M8 2v5.6M5.7 5.4L8 7.7l2.3-2.3"/></symbol> | |
| <symbol id="i-lease" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="8" r="6.2"/><path d="M8 4.6V8l2.4 1.5"/></symbol> | |
| <symbol id="i-queued" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M3 4.6h10M3 8h10M3 11.4h6"/></symbol> | |
| <symbol id="i-invalid" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><circle cx="8" cy="8" r="6.2"/><path d="M5 8h6"/></symbol> | |
| <symbol id="i-superseded" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2.4 8h8.4M8 5l3 3-3 3"/><path d="M13.6 3.2v9.6"/></symbol> | |
| <symbol id="i-demoted" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2.2v8.2M4.9 7.4L8 10.5l3.1-3.1"/><path d="M3 13.6h10"/></symbol> | |
| <symbol id="i-erased" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><rect x="2.4" y="2.4" width="11.2" height="11.2" rx="1.6" stroke-dasharray="2.2 2.2"/></symbol> | |
| <symbol id="i-copy" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"><rect x="5.6" y="5.6" width="8" height="8" rx="1.4"/><path d="M10.4 5.6V3.8a1.3 1.3 0 0 0-1.3-1.3H3.8a1.3 1.3 0 0 0-1.3 1.3v5.3a1.3 1.3 0 0 0 1.3 1.3h1.8"/></symbol> | |
| <symbol id="i-terminal" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="1.8" y="2.6" width="12.4" height="10.8" rx="1.6"/><path d="M4.6 6.2l2 1.8-2 1.8M8.4 10h3"/></symbol> | |
| <symbol id="i-search" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><circle cx="7" cy="7" r="4.4"/><path d="M10.3 10.3l3.5 3.5"/></symbol> | |
| <symbol id="i-entry" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2.8" y="1.8" width="10.4" height="12.4" rx="1.4"/><path d="M5.4 5.4h5.2M5.4 8h5.2M5.4 10.6h3"/></symbol> | |
| <symbol id="i-start" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"><path d="M4.6 2.9v10.2L12.8 8Z"/></symbol> | |
| <symbol id="i-label-0" viewBox="0 0 16 16" stroke="currentColor" stroke-width="1.2"><rect x="1.6" y="9.6" width="3" height="4.6" rx=".5" fill="currentColor"/><rect x="6.5" y="6.4" width="3" height="7.8" rx=".5" fill="none"/><rect x="11.4" y="2.6" width="3" height="11.6" rx=".5" fill="none"/></symbol> | |
| <symbol id="i-label-1" viewBox="0 0 16 16" stroke="currentColor" stroke-width="1.2"><rect x="1.6" y="9.6" width="3" height="4.6" rx=".5" fill="currentColor"/><rect x="6.5" y="6.4" width="3" height="7.8" rx=".5" fill="currentColor"/><rect x="11.4" y="2.6" width="3" height="11.6" rx=".5" fill="none"/></symbol> | |
| <symbol id="i-label-2" viewBox="0 0 16 16" stroke="currentColor" stroke-width="1.2"><rect x="1.6" y="9.6" width="3" height="4.6" rx=".5" fill="currentColor"/><rect x="6.5" y="6.4" width="3" height="7.8" rx=".5" fill="currentColor"/><rect x="11.4" y="2.6" width="3" height="11.6" rx=".5" fill="currentColor"/></symbol> | |
| <symbol id="i-trust-0" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4"><circle cx="8" cy="8" r="5.6"/></symbol> | |
| <symbol id="i-trust-1" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4"><circle cx="8" cy="8" r="5.6"/><path d="M8 2.4a5.6 5.6 0 0 0 0 11.2Z" fill="currentColor" stroke="none"/></symbol> | |
| <symbol id="i-trust-2" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4"><circle cx="8" cy="8" r="5.6" fill="currentColor"/></symbol> | |
| <symbol id="i-pair" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="4.8" cy="8" r="2.8"/><path d="M7.6 8h6.6M11.2 8v2.4M13.6 8v1.8"/></symbol> | |
| <symbol id="i-busy" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M8 1.8a6.2 6.2 0 1 0 6.2 6.2"/></symbol> | |
| </defs></svg> | |
| <a class="am-skip" href="#main">Skip to the page</a> | |
| <header class="am-top"> | |
| <a class="am-mark" href="index.html"><svg class="am-glyph" viewBox="0 0 24 24" aria-hidden="true"><use href="#i-mark"/></svg>amem</a> | |
| <nav class="am-nav" aria-label="Pages"> | |
| <a href="index.html">Sessions</a> | |
| <a href="board.html" aria-current="page">Board</a> | |
| <a href="reader.html">Reader</a> | |
| </nav> | |
| <div class="am-top-end" id="pairing"></div> | |
| </header> | |
| <main class="am-main" id="main"> | |
| <div class="am-head"> | |
| <h1 class="am-title">Board</h1> | |
| <p class="am-lead">The tasks on the ledger by status, each under the task it was filed from. A task addressed to a person is marked: claim it, do it, and complete it with a handoff. <span id="domain"></span></p> | |
| </div> | |
| <div id="unpaired" class="am-alert am-alert--await" role="status" hidden> | |
| <svg class="am-glyph" aria-hidden="true"><use href="#i-pair"/></svg> | |
| <p><b>Not paired.</b> <span id="unpaired-why"></span></p> | |
| <div class="am-alert-act"><div class="am-cmd"><code><span class="p">$ </span>amem</code></div></div> | |
| </div> | |
| <div id="error" class="am-alert" role="alert" hidden> | |
| <svg class="am-glyph" aria-hidden="true"><use href="#i-fail"/></svg> | |
| <p><b id="error-what"></b> <span id="error-why"></span></p> | |
| <div class="am-alert-act"><button class="am-btn am-btn--secondary" type="button" id="error-close">Dismiss</button></div> | |
| </div> | |
| <section class="am-panel file" aria-labelledby="file-h"> | |
| <h2 class="am-section-title" id="file-h">File a task</h2> | |
| <form id="file"> | |
| <fieldset id="file-fields" class="file-grid"> | |
| <div class="am-field"><label for="f-title">Title</label> | |
| <input class="am-ctrl" id="f-title" name="title" required autocomplete="off" placeholder="One line: what is to be done"> | |
| </div> | |
| <div class="am-field"><label for="f-req">Requirements</label> | |
| <input class="am-ctrl is-mono" id="f-req" name="requirements" autocomplete="off" spellcheck="false" placeholder="human, tool:github"> | |
| <span class="am-hint">Comma separated. <span class="am-mono">human</span> addresses it to a person.</span> | |
| </div> | |
| <div class="am-field wide"><label for="f-body">Body</label> | |
| <textarea class="am-ctrl" id="f-body" name="body" placeholder="What is to be done, in Markdown"></textarea> | |
| </div> | |
| <div class="file-foot wide"> | |
| <button class="am-btn am-btn--primary" type="submit" id="file-go">File task</button> | |
| <span class="am-hint" id="file-hint" aria-live="polite">It lands as proposed, waiting for admission.</span> | |
| </div> | |
| </fieldset> | |
| </form> | |
| </section> | |
| <div class="groups" id="lists" aria-busy="true"></div> | |
| </main> | |
| <dialog class="am-dialog" id="handoff" aria-labelledby="handoff-h"> | |
| <form method="dialog" id="handoff-form"> | |
| <h2 class="am-section-title" id="handoff-h">Complete with a handoff</h2> | |
| <p>Complete <b id="handoff-title"></b> with a handoff: what was done, what remains, what the next one should know.</p> | |
| <div class="am-field"><label for="handoff-text">Handoff</label> | |
| <textarea class="am-ctrl" id="handoff-text" required></textarea></div> | |
| <div class="am-dialog-foot"> | |
| <button class="am-btn am-btn--secondary" value="cancel" formnovalidate>Cancel</button> | |
| <button class="am-btn am-btn--primary" value="ok" id="handoff-ok">Complete</button> | |
| </div> | |
| </form> | |
| </dialog> | |
| <script src="amem.js"></script> | |
| <script> | |
| // ---- small DOM helpers: task text is anyone's, so it is only ever set as text ---- | |
| const $ = (id) => document.getElementById(id); | |
| const NS = 'http://www.w3.org/2000/svg'; | |
| function el(tag, cls, ...kids) { | |
| const e = document.createElement(tag); | |
| if (cls) e.className = cls; | |
| for (const k of kids) if (k !== null && k !== undefined && k !== false) e.append(k); | |
| return e; | |
| } | |
| function glyph(name, label) { | |
| const s = document.createElementNS(NS, 'svg'); | |
| s.setAttribute('class', 'am-glyph'); | |
| if (label) { s.setAttribute('role', 'img'); s.setAttribute('aria-label', label); } else s.setAttribute('aria-hidden', 'true'); | |
| const u = document.createElementNS(NS, 'use'); | |
| u.setAttribute('href', `#i-${name}`); | |
| s.append(u); | |
| return s; | |
| } | |
| const state = (tone, name, label) => el('span', `am-state am-state--${tone}`, glyph(name, label)); | |
| const id = (t) => el('span', 'am-id', t); | |
| // ---- the ledger's states, in its own words, each with its gutter glyph ---- | |
| const ORDER = ['claimed', 'admitted', 'proposed', 'completed', 'invalidated']; | |
| const WORDS = { claimed: 'Claimed, under a lease', admitted: 'Admitted, claimable', proposed: 'Proposed, waiting for admission', completed: 'Completed, with a handoff', invalidated: 'Invalidated' }; | |
| const GUTTER = { claimed: ['live', 'lease'], admitted: ['ink', 'queued'], proposed: ['await', 'admit'], completed: ['ok', 'done'], invalidated: ['ink', 'invalid'] }; | |
| const EMPTY = { | |
| claimed: ['Nothing claimed. A session claims an admitted task with ', el('code', null, 'amem work'), '; Claim here takes one for a person.'], | |
| admitted: 'Nothing claimable. Admit a proposal, or file a task above.', | |
| proposed: 'No proposals waiting.', | |
| completed: 'Nothing completed yet.', | |
| invalidated: 'Nothing invalidated.', | |
| }; | |
| function parentOf(t) { | |
| const m = /^(task|continues|retries):(.+)$/.exec(t.origin || ''); | |
| return m ? m[2] : null; | |
| } | |
| function relation(t) { | |
| const o = t.origin || ''; | |
| if (o.startsWith('task:')) return 'subagent of'; | |
| if (o.startsWith('continues:')) return 'continues'; | |
| if (o.startsWith('retries:')) return 'retries'; | |
| return o ? 'from' : ''; | |
| } | |
| // every call goes to the server through the page's key | |
| async function api(method, path, body) { | |
| const r = await amem.fetch(path, { method, headers: body ? { 'content-type': 'application/json' } : {}, body: body ? JSON.stringify(body) : undefined }); | |
| const text = await r.text(); | |
| if (r.status === 401) { paired = false; showPairing(); } | |
| if (!r.ok) throw new Error(text || r.statusText); | |
| return text ? JSON.parse(text) : null; | |
| } | |
| function fail(what, why) { | |
| $('error-what').textContent = what; | |
| $('error-why').textContent = why; | |
| $('error').hidden = false; | |
| } | |
| $('error-close').addEventListener('click', () => { $('error').hidden = true; }); | |
| // what a page says when it has no key; a hosted copy also says where amem has to run | |
| const unpairedWhy = () => { const w = amem.unpaired(); return amem.hosted && !/own machine/.test(w) ? `${w} This copy of the pages runs in your browser and talks to amem on your own machine: run amem there, and it opens this page paired.` : w; }; | |
| let paired = false; | |
| let boardDomain = ''; | |
| function showPairing() { | |
| const end = $('pairing'); | |
| if (paired) { | |
| let host = amem.server() || location.origin; | |
| try { host = new URL(host).host; } catch (_) { /* as it is */ } | |
| end.replaceChildren('Paired with ', id(host)); | |
| $('unpaired').hidden = true; | |
| return; | |
| } | |
| end.replaceChildren(state('await', 'pair'), 'Not paired'); | |
| $('unpaired-why').textContent = unpairedWhy(); | |
| $('unpaired').hidden = false; | |
| $('file-fields').disabled = true; | |
| $('file-hint').textContent = 'Pair this browser first, as the note above says.'; | |
| $('lists').removeAttribute('aria-busy'); | |
| $('lists').replaceChildren(el('div', 'am-empty', el('b', null, 'Tasks show here once this browser is paired'), el('p', null, 'Nothing on this page reaches amem until then.'))); | |
| } | |
| let all = []; | |
| async function refresh() { | |
| if (!paired) return; | |
| let data; | |
| try { data = await api('GET', '/api/tasks'); } catch (e) { if (paired) fail('The board did not load.', e.message); return; } | |
| $('domain').textContent = data.domain ? `Domain ${data.domain}.` : ''; | |
| boardDomain = data.domain || ''; | |
| all = data.tasks; | |
| const byId = new Map(all.map((t) => [t.id, t])); | |
| const depth = (t) => { let d = 0, p = parentOf(t); const seen = new Set(); while (p && byId.has(p) && !seen.has(p)) { seen.add(p); d++; p = parentOf(byId.get(p)); } return d; }; | |
| // a refresh keeps a body that was open open | |
| const open = new Set([...document.querySelectorAll('.am-task-body[open]')].map((d) => d.dataset.id)); | |
| const lists = $('lists'); | |
| lists.removeAttribute('aria-busy'); | |
| lists.replaceChildren(); | |
| for (const status of ORDER) { | |
| const tasks = all.filter((t) => t.status === status); | |
| const h = el('h2', 'am-sub-title', WORDS[status], el('small', null, String(tasks.length))); | |
| h.id = `g-${status}`; | |
| const group = el('section', 'group', h); | |
| group.setAttribute('aria-labelledby', h.id); | |
| lists.append(group); | |
| if (!tasks.length) { group.append(el('p', 'am-hint', ...[].concat(EMPTY[status]))); continue; } | |
| const list = el('div', 'am-list'); | |
| group.append(list); | |
| // parents before children, each child right after its parent where both share a status | |
| const roots = tasks.filter((t) => !parentOf(t) || !tasks.some((x) => x.id === parentOf(t))); | |
| const place = (t) => { list.append(render(t, depth(t), open)); tasks.filter((c) => parentOf(c) === t.id).forEach(place); }; | |
| roots.forEach(place); | |
| } | |
| } | |
| function render(t, d, open) { | |
| const human = (t.requirements || []).includes('human'); | |
| const row = el('div', `am-task${d ? ' is-child' : ''}`); | |
| row.style.setProperty('--depth', String(Math.min(d, 5))); | |
| const [tone, g] = GUTTER[t.status] || ['ink', 'queued']; | |
| const rel = relation(t); | |
| const parent = parentOf(t); | |
| const meta = el('span', 'am-task-meta'); | |
| if (human) meta.append(el('b', null, 'For a person')); | |
| if (rel) meta.append(el('span', null, `${rel} `, id(parent || t.origin))); | |
| if ((t.depends_on || []).length) meta.append(el('span', null, 'after ', ...t.depends_on.flatMap((x, i) => (i ? [', ', id(x)] : [id(x)])))); | |
| if ((t.requirements || []).length) meta.append(el('span', null, `needs ${t.requirements.join(', ')}`)); | |
| if (t.holder) meta.append(el('span', null, `held by ${t.holder}`)); | |
| if (t.handoff) { | |
| const a = el('a', null, id(t.handoff)); | |
| // the reader, on this domain's bundle at the owner's handling, opened at the handoff | |
| const q = new URLSearchParams({ handling: 'restricted', bundle: `/bundle/${boardDomain}/restricted`, open: t.handoff }); | |
| a.href = `reader.html?${q}`; | |
| meta.append(el('span', null, 'handoff ', a)); | |
| } | |
| if (t.reason) meta.append(el('span', null, t.reason)); | |
| const when = el('span', 'am-time', `${(t.created || '').slice(0, 16).replace('T', ' ')}Z`); | |
| meta.append(el('span', null, `filed by ${t.created_by}, `, when)); | |
| const acts = []; | |
| if (t.status === 'proposed') acts.push(['Admit', 'secondary', () => act('admit', t)]); | |
| if (t.status === 'admitted') acts.push(['Claim', 'secondary', () => act('claim', t)]); | |
| if (t.status === 'claimed') acts.push(['Complete', 'secondary', () => completeWith(t)]); | |
| if (t.status !== 'completed' && t.status !== 'invalidated') acts.push(['Invalidate', 'danger', () => act('invalidate', t, { reason: 'closed from the board' })]); | |
| if (t.status === 'invalidated' || t.status === 'completed') acts.push(['Retry', 'secondary', () => act('retry', t)]); | |
| const buttons = el('span', 'am-task-acts', ...acts.map(([word, role, fn]) => { | |
| const b = el('button', `am-btn am-btn--${role}`, word); | |
| b.type = 'button'; | |
| b.setAttribute('aria-label', `${word}: ${t.title}`); | |
| b.addEventListener('click', fn); | |
| return b; | |
| })); | |
| const body = el('details', 'am-task-body', el('summary', null, 'Body'), el('pre', null, t.body || '')); | |
| body.dataset.id = t.id; | |
| if (open.has(t.id)) body.open = true; | |
| row.append(state(tone, g, t.status), el('span', 'am-task-title', el('span', null, t.title), id(t.id)), buttons, meta, body); | |
| return row; | |
| } | |
| async function act(what, t, body) { | |
| try { await api('POST', `/api/tasks/${encodeURIComponent(t.id)}/${what}`, body); $('error').hidden = true; await refresh(); } | |
| catch (e) { fail(`Could not ${what} ${t.id}.`, e.message); } | |
| } | |
| function completeWith(t) { | |
| $('handoff-title').textContent = `${t.title} (${t.id})`; | |
| $('handoff-text').value = ''; | |
| const dlg = $('handoff'); | |
| dlg.returnValue = ''; | |
| dlg.showModal(); | |
| dlg.onclose = async () => { | |
| if (dlg.returnValue !== 'ok') return; | |
| await act('complete', t, { handoff: $('handoff-text').value }); | |
| }; | |
| } | |
| $('file').addEventListener('submit', async (e) => { | |
| e.preventDefault(); | |
| const f = new FormData($('file')); | |
| const requirements = String(f.get('requirements') || '').split(',').map((s) => s.trim()).filter(Boolean); | |
| const hint = $('file-hint'); | |
| try { | |
| const r = await api('POST', '/api/tasks', { title: f.get('title'), body: f.get('body') || '', requirements }); | |
| $('file').reset(); | |
| hint.className = 'am-hint'; | |
| hint.replaceChildren(r && r.deduplicated ? 'Already on the ledger: ' : 'Filed: ', id(r && r.task ? r.task.id : ''), r && r.deduplicated ? '.' : ', proposed and waiting for admission.'); | |
| await refresh(); | |
| } catch (err) { | |
| hint.className = 'am-err'; | |
| hint.replaceChildren(glyph('fail'), `Not filed: ${err.message}`); | |
| } | |
| }); | |
| (async () => { | |
| await amem.ready; | |
| paired = amem.paired(); | |
| showPairing(); | |
| if (!paired) return; | |
| await refresh(); | |
| setInterval(refresh, 5000); | |
| })(); | |
| </script> | |
| </body> | |
| </html> |