@lovek4d/kickflip 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTEXT.md +151 -115
- package/README.md +23 -14
- package/SKILL.md +37 -37
- package/bin/doctor.mjs +4 -4
- package/bin/install.mjs +3 -3
- package/bin/kickflip.mjs +34 -26
- package/bin/wait-events.mjs +2 -2
- package/board/arch-review.mjs +1 -1
- package/board/auto-review.mjs +2 -0
- package/board/bin/board.mjs +186 -69
- package/board/domain.mjs +8 -5
- package/board/finish.mjs +3 -0
- package/board/headless.mjs +3 -0
- package/board/on-host.mjs +1 -1
- package/board/permissions.mjs +2 -0
- package/board/public/app-arch.js +2 -2
- package/board/public/app-asks.js +57 -38
- package/board/public/app-banner.js +2 -2
- package/board/public/app-cards.js +156 -72
- package/board/public/app-core.js +2 -4
- package/board/public/app-header.js +12 -5
- package/board/public/app-lanes.js +201 -47
- package/board/public/app-needs.js +2 -1
- package/board/public/app-snapshot.js +2 -2
- package/board/public/app-threads.js +11 -14
- package/board/public/app.html +2 -1
- package/board/public/app.js +53 -83
- package/board/public/attach.js +39 -3
- package/board/public/dialogs.js +59 -14
- package/board/public/hub-kit.js +9 -6
- package/board/public/hub.css +5 -18
- package/board/public/linear-key.js +6 -4
- package/board/public/log.html +48 -0
- package/board/public/prompts.js +9 -8
- package/board/public/prototype-kit.js +6 -6
- package/board/public/prototype-switcher.js +13 -5
- package/board/public/prototype.html +1 -0
- package/board/public/router.js +8 -7
- package/board/public/rules-editor.js +3 -4
- package/board/public/shell.js +20 -6
- package/board/public/sidebar.css +21 -1
- package/board/public/sidebar.js +64 -31
- package/board/public/style.css +155 -62
- package/board/public/toasts.js +2 -2
- package/board/public/ui-cards.js +176 -71
- package/board/public/ui-enums.js +5 -4
- package/board/public/ui-header.js +10 -10
- package/board/public/ui-hub.js +4 -4
- package/board/public/ui-markdown.js +1 -1
- package/board/public/ui-order.js +9 -6
- package/board/public/ui-page-header.js +10 -7
- package/board/public/ui-patch.js +53 -0
- package/board/public/ui-phase-bars.js +17 -0
- package/board/public/ui-prompts.js +8 -8
- package/board/public/ui-questions.js +26 -23
- package/board/public/ui-sidebar.js +46 -22
- package/board/public/ui-snapshot.js +20 -9
- package/board/public/ui-threads.js +17 -11
- package/board/public/ui.js +80 -12
- package/board/public/view-frame.js +1 -1
- package/board/public/view-hub-settings.js +25 -18
- package/board/public/view-hub.js +53 -12
- package/board/public/view-notes.js +1 -1
- package/board/public/view-questions.js +101 -50
- package/board/public/view-settings.js +167 -49
- package/board/review.mjs +1 -1
- package/board/server.mjs +18 -12
- package/board/store.mjs +13 -12
- package/board/tickets.mjs +3 -3
- package/board/worktree.mjs +20 -14
- package/docs/hub-session.md +4 -4
- package/hooks-plugin/hooks/codex-share-auto.mjs +1 -1
- package/hooks-plugin/hooks/watcher-stop.mjs +2 -2
- package/hub/board-repo.mjs +1 -1
- package/hub/boards.mjs +4 -4
- package/hub/close-board.mjs +1 -1
- package/hub/github.mjs +4 -4
- package/hub/home.mjs +1 -1
- package/hub/hub.mjs +12 -12
- package/hub/incoming.mjs +2 -0
- package/hub/linear.mjs +2 -2
- package/hub/live.mjs +2 -2
- package/hub/new-board.mjs +2 -2
- package/hub/permission-denied-hook.mjs +2 -0
- package/hub/pr-report.mjs +3 -1
- package/hub/presence.mjs +1 -1
- package/hub/prompts.mjs +1 -1
- package/hub/push.mjs +2 -2
- package/hub/readiness.mjs +2 -2
- package/hub/repos.mjs +3 -3
- package/hub/restart-board.mjs +1 -1
- package/hub/schedule.mjs +1 -1
- package/hub/sessions.mjs +2 -2
- package/hub/sw.js +1 -1
- package/hub/try.mjs +8 -4
- package/hub/usage.mjs +3 -3
- package/hub/views.mjs +1 -1
- package/package.json +5 -1
- package/shared/card-prs.mjs +1 -0
- package/shared/colors.css +5 -4
- package/shared/fake-gh.mjs +59 -8
- package/shared/repos.mjs +1 -1
- package/shared/usage-gate.mjs +1 -1
- package/board/public/view-polling.js +0 -87
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
// opens the topic whose pill was to its left before the Send, else to its right, and with no topic left that has an
|
|
5
5
|
// open question goes back to the board, where the Sent line shows as a toast (BoardUI.sentNotice). An open question
|
|
6
6
|
// with `urgent: true` in owner.json shows alone first, and its Send follows the same rule for its topic. A question
|
|
7
|
-
// whose options are prototype takes frames each take with ?take= (docs/adr/
|
|
7
|
+
// whose options are prototype takes frames each take with ?take= (docs/adr/2026-10-29-1639-a-questions-takes-show-inline.md).
|
|
8
8
|
// Each question's eye sends `prototype-request {key?, question, note, choice}` for that question instead of an answer,
|
|
9
|
-
// with its note box as the note and its pick so far as the choice (docs/adr/
|
|
9
|
+
// with its note box as the note and its pick so far as the choice (docs/adr/2026-11-02-1822-each-question-has-its-own-prototype-eye.md).
|
|
10
|
+
// The board server resolves the question with it, and once it is recorded the page moves on as a Send does
|
|
11
|
+
// (docs/adr/2026-11-03-1803-a-question-asked-for-a-prototype-is-resolved.md).
|
|
10
12
|
// fits: each framed take's measured size, kept across repaints and visits so a take redraws at its size at once.
|
|
11
13
|
(globalThis.AppViews ||= {}).questions = ((fits) => ({
|
|
12
14
|
className: 'view-questions',
|
|
@@ -18,7 +20,7 @@
|
|
|
18
20
|
|
|
19
21
|
create: (root, siteRoot, ctx) => BoardView.page(root, siteRoot, (B) => {
|
|
20
22
|
const { $, site } = B;
|
|
21
|
-
const { esc, choiceLabel, questionCard, roundChip, takes, linkContext, badge, topicName, topicRow, nextQuestion, sentNotice, fetchJSON, restartNotice } = BoardUI;
|
|
23
|
+
const { esc, choiceLabel, questionCard, roundChip, takes, linkContext, badge, topicName, cardTopics, topicRow, nextQuestion, sentNotice, fetchJSON, restartNotice } = BoardUI;
|
|
22
24
|
// prototype-switcher.js reads a draft's note and clears the draft once it sends that question's answer.
|
|
23
25
|
const draftKey = (id) => 'qdraft:' + id;
|
|
24
26
|
const getDraft = (id) => { try { return JSON.parse(localStorage.getItem(draftKey(id)) || '{}'); } catch { return {}; } };
|
|
@@ -26,12 +28,21 @@
|
|
|
26
28
|
const hashId = () => { const h = location.hash.slice(1); try { return decodeURIComponent(h); } catch { return h; } };
|
|
27
29
|
|
|
28
30
|
let all = [];
|
|
31
|
+
// The shown topic's resolved questions, which /api/board leaves out: GET /api/questions answers them as the topic
|
|
32
|
+
// shows, and again once its resolved count changes or after a failed load, at its next draw (docs/adr/2026-11-04-0128-the-board-download-leaves-out-resolved-questions.md).
|
|
33
|
+
let resolved = { topic: null, n: 0, questions: [] };
|
|
34
|
+
// The board last landed, whose version the next load and each tap name, so an unchanged board downloads nothing
|
|
35
|
+
// and a changed one comes as a patch (ui-patch.js).
|
|
36
|
+
let board = null;
|
|
29
37
|
let items = [];
|
|
30
38
|
let topics = [];
|
|
31
39
|
let topicId = null;
|
|
32
40
|
let soloId = null;
|
|
33
41
|
let shown = '';
|
|
34
42
|
let sentLine = '';
|
|
43
|
+
// The footer after a prototype request: the blue spinner, then these words.
|
|
44
|
+
const PROTOTYPE_REQUESTED = 'prototype requested';
|
|
45
|
+
const statusHtml = (line) => (line.startsWith(PROTOTYPE_REQUESTED) ? '<span class="try-spin" aria-hidden="true"></span>' : '') + esc(line);
|
|
35
46
|
let scrollTake = null;
|
|
36
47
|
const takeResources = new Map();
|
|
37
48
|
const clearTakeResources = (within = root) => {
|
|
@@ -41,13 +52,14 @@
|
|
|
41
52
|
};
|
|
42
53
|
B.atUnmount(() => clearTakeResources());
|
|
43
54
|
|
|
44
|
-
const
|
|
55
|
+
const send = (kind, data) => site.send(kind, { ...data, ...(board ? { since: board.version } : {}) });
|
|
56
|
+
const byId = (id) => all.find((q) => q.id === id) || resolved.questions.find((q) => q.id === id);
|
|
45
57
|
const topicOf = (qid) => topics.find((t) => t.questions.includes(qid)) || null;
|
|
46
58
|
const openTopics = () => topics.filter((t) => t.state !== 'resolved');
|
|
47
59
|
const links = (q) => linkContext(items, q.topic?.startsWith('item:') ? q.topic.slice(5) : null, { base: './' });
|
|
48
60
|
const urgentOpen = () => all.find((q) => q.urgent && q.state === 'open');
|
|
49
61
|
|
|
50
|
-
// docs/adr/
|
|
62
|
+
// docs/adr/2026-10-29-1639-waiting-on-you-leads-with-what-needs-the-owner.md
|
|
51
63
|
function topicBar() {
|
|
52
64
|
const list = topicRow(topics, topicId).pills;
|
|
53
65
|
if (list.length < 2) return '';
|
|
@@ -61,7 +73,6 @@
|
|
|
61
73
|
const uploading = (qs) => qs.some((q) => BoardAttach.busy('q:' + q.id, site.root));
|
|
62
74
|
function pending(qs) {
|
|
63
75
|
return qs.filter((q) => {
|
|
64
|
-
if (q.state === 'requested') return false;
|
|
65
76
|
const d = getDraft(q.id);
|
|
66
77
|
if (filesOf(q).length) return true;
|
|
67
78
|
if (!('choice' in d) && !d.note) return false;
|
|
@@ -76,8 +87,21 @@
|
|
|
76
87
|
return t ? t.questions.map(byId).filter(Boolean) : [];
|
|
77
88
|
}
|
|
78
89
|
|
|
90
|
+
function loadResolved() {
|
|
91
|
+
const t = soloId ? null : topics.find((x) => x.id === topicId);
|
|
92
|
+
if (!t?.resolved || (resolved.topic === t.id && resolved.n === t.resolved)) return;
|
|
93
|
+
resolved = { topic: t.id, n: t.resolved, questions: [] };
|
|
94
|
+
const asked = resolved;
|
|
95
|
+
fetchJSON(site.url('api/questions?topic=' + encodeURIComponent(t.id)), { sleep: B.sleep }).then(({ body }) => {
|
|
96
|
+
if (!B.alive() || resolved !== asked) return;
|
|
97
|
+
asked.questions = body.questions || [];
|
|
98
|
+
repaint(false);
|
|
99
|
+
}, () => { if (resolved === asked) resolved = { topic: null, n: 0, questions: [] }; });
|
|
100
|
+
}
|
|
101
|
+
|
|
79
102
|
function render() {
|
|
80
103
|
clearTakeResources();
|
|
104
|
+
loadResolved();
|
|
81
105
|
const el = $('#page');
|
|
82
106
|
const qs = visible();
|
|
83
107
|
if (!qs.length) {
|
|
@@ -94,7 +118,7 @@
|
|
|
94
118
|
el.innerHTML = `${head}<form id="qform">
|
|
95
119
|
${live.map((q, i) => questionCard(q, { n: soloId ? 0 : i + 1, draft: getDraft(q.id), links: links(q), fit: fitOf(q) })).join('')}
|
|
96
120
|
${done.length ? `<details class="done"><summary>Resolved · ${done.length}</summary>${done.map((q) => questionCard(q, { draft: getDraft(q.id), links: links(q), fit: fitOf(q) })).join('')}</details>` : ''}
|
|
97
|
-
<div class="bar"><span class="muted small${sentLine ? ' sent' : ''}" id="qstatus" role="status">${
|
|
121
|
+
<div class="bar"><span class="muted small${sentLine ? ' sent' : ''}" id="qstatus" role="status">${statusHtml(sentLine)}</span><span class="send-hint" aria-hidden="true">${/Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent) ? '⌘↵' : 'Ctrl+↵'}</span><button class="primary" type="submit" id="send">Send</button></div>
|
|
98
122
|
</form>${soloId ? '' : waitingTopics() + resolvedTopics()}`;
|
|
99
123
|
BoardAttach.paintAll();
|
|
100
124
|
el.querySelectorAll('.take-shot').forEach(fitTake);
|
|
@@ -104,7 +128,7 @@
|
|
|
104
128
|
// A width sets its page's height, so a size measured at another width is measured again.
|
|
105
129
|
const fitKey = (qid, oid) => `${site.root}\n${qid}\n${oid}`;
|
|
106
130
|
const fitOf = (q) => (oid) => { const f = fits.get(fitKey(q.id, oid)); return f && f.w === window.innerWidth ? f : null; };
|
|
107
|
-
// A phone's header is a bar at the bottom (docs/adr/
|
|
131
|
+
// A phone's header is a bar at the bottom (docs/adr/2026-10-31-1532-phone-headers-sit-at-the-bottom.md), so only a header
|
|
108
132
|
// at the top covers the frame's top.
|
|
109
133
|
const frameTop = (shot) => {
|
|
110
134
|
const head = $('header.top')?.getBoundingClientRect();
|
|
@@ -128,7 +152,7 @@
|
|
|
128
152
|
// A framed take is sized to its page once it loads, since the frame can't scroll. A page that fills its frame at
|
|
129
153
|
// any height is a full-screen take and gets screenH. A full-screen take, or a grid take past 80% of the screen's
|
|
130
154
|
// height, turns its question into one full-width take under a choice row
|
|
131
|
-
// (docs/adr/
|
|
155
|
+
// (docs/adr/2026-10-29-1639-a-questions-takes-show-inline.md).
|
|
132
156
|
function fitTake(shot) {
|
|
133
157
|
const frame = shot.querySelector('iframe');
|
|
134
158
|
const sec = shot.closest('section.q');
|
|
@@ -202,7 +226,7 @@
|
|
|
202
226
|
function waitingTopics() {
|
|
203
227
|
const list = topicRow(topics, topicId).waiting;
|
|
204
228
|
return list.length ? `<details class="done"><summary>Waiting on Claude · ${list.length}</summary>${list.map((t) =>
|
|
205
|
-
`<div class="trow"><button class="ghost" data-topic="${esc(t.id)}">${esc(t.title)}</button><span class="muted small">${t.
|
|
229
|
+
`<div class="trow"><button class="ghost" data-topic="${esc(t.id)}">${esc(t.title)}</button><span class="muted small">${t.answered ? 'answered' : 'Claude replied'}</span></div>`).join('')}</details>` : '';
|
|
206
230
|
}
|
|
207
231
|
|
|
208
232
|
function resolvedTopics() {
|
|
@@ -211,25 +235,41 @@
|
|
|
211
235
|
`<div><button class="ghost" data-topic="${esc(t.id)}">${esc(t.title)}</button></div>`).join('')}</details>` : '';
|
|
212
236
|
}
|
|
213
237
|
|
|
214
|
-
|
|
238
|
+
// The topic a tap on this page acts for, the pills before it and its name, which moveOn reads once it is recorded.
|
|
239
|
+
function leaving() {
|
|
240
|
+
const from = soloId ? topicOf(soloId)?.id : topicId;
|
|
241
|
+
return { from, pills: topicRow(topics, from).pills, name: topicName(topics.find((t) => t.id === from) || { title: '' }) };
|
|
242
|
+
}
|
|
243
|
+
// After a recorded tap: a load that started before it went out would read its question as still open.
|
|
244
|
+
// `footer` is a line or, for a Send, a function of the reloaded board giving the whole line.
|
|
245
|
+
// The tap's POST answers with the board as it left it, which lands here in place of a reload.
|
|
246
|
+
async function moveOnOnceLoaded({ from, pills }, what, out, footer = what) {
|
|
247
|
+
await loading;
|
|
248
|
+
if (!B.alive()) return;
|
|
249
|
+
if (!land(out.board, false)) await load(false);
|
|
250
|
+
moveOn(from, pills, what, typeof footer === 'function' ? footer() : footer, typeof footer !== 'function');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// The tapped Prototype button waits (ui.js waitOn) while the request is sent.
|
|
254
|
+
async function askPrototype(q, button) {
|
|
215
255
|
const st = $('#qstatus');
|
|
216
256
|
const key = q.topic?.startsWith('item:') ? q.topic.slice(5) : null;
|
|
217
|
-
|
|
257
|
+
const at = leaving();
|
|
258
|
+
st.textContent = '';
|
|
218
259
|
const d = getDraft(q.id);
|
|
219
260
|
const last = q.answers[q.answers.length - 1];
|
|
220
|
-
|
|
221
|
-
|
|
261
|
+
let out;
|
|
262
|
+
try { out = await BoardUI.waitOn(button, send('prototype-request', { ...(key ? { key } : {}), question: q.id, note: (d.note || '').trim(), choice: d.choice ?? (last ? last.choice : null) })); }
|
|
263
|
+
catch (e) { st.textContent = e.reason ? `Nothing was sent: ${e.reason}` : 'Could not send. Is the host awake?'; return; }
|
|
222
264
|
setDraft(q.id, {});
|
|
223
|
-
|
|
224
|
-
await loading;
|
|
225
|
-
if (await load(false)) { shown = sigNow(); render(); }
|
|
265
|
+
await moveOnOnceLoaded(at, `Prototype requested${at.name ? ` for ${at.name}` : ''}.`, out, PROTOTYPE_REQUESTED);
|
|
226
266
|
}
|
|
227
267
|
|
|
228
268
|
function updateBar() {
|
|
229
269
|
const qs = visible();
|
|
230
270
|
const n = pending(qs).length;
|
|
231
271
|
const b = $('#send');
|
|
232
|
-
if (b) { b.disabled = !n || uploading(qs); b.textContent = uploading(qs) ? 'Uploading…' :
|
|
272
|
+
if (b) { b.disabled = !n || uploading(qs); b.textContent = uploading(qs) ? 'Uploading…' : 'Send'; }
|
|
233
273
|
const live = qs.filter((q) => q.state !== 'resolved');
|
|
234
274
|
const answered = live.filter((q) => q.state !== 'open').length;
|
|
235
275
|
$('#progress').textContent = live.length > 1 ? `${answered}/${live.length} answered` : '';
|
|
@@ -252,45 +292,58 @@
|
|
|
252
292
|
async function fetchBoard(first) {
|
|
253
293
|
const page = $('#page');
|
|
254
294
|
try {
|
|
255
|
-
const { body: d } = await fetchJSON(site.url('api/board'), { onWait: () => { if (first) page.innerHTML = restartNotice(); }, sleep: B.sleep });
|
|
295
|
+
const { body: d } = await fetchJSON(site.url('api/board' + (board ? '?since=' + encodeURIComponent(board.version) : '')), { tag: board?.version, onWait: () => { if (first) page.innerHTML = restartNotice(); }, sleep: B.sleep });
|
|
256
296
|
if (!B.alive()) return false;
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
topics = d.ownerTopics || [];
|
|
260
|
-
site.syncBadge();
|
|
261
|
-
if (first) pick(hashId());
|
|
262
|
-
else if (soloId && byId(soloId)?.state !== 'open') pick('');
|
|
263
|
-
else if (!topics.some((t) => t.id === topicId)) pick('');
|
|
264
|
-
loaded = true;
|
|
265
|
-
const sig = sigNow();
|
|
266
|
-
const active = document.activeElement;
|
|
267
|
-
if (sig !== shown && !(active && active.tagName === 'TEXTAREA')) {
|
|
268
|
-
shown = sig;
|
|
269
|
-
const y = scrollY;
|
|
270
|
-
render();
|
|
271
|
-
scrollTo(0, first ? 0 : y);
|
|
272
|
-
}
|
|
297
|
+
if (d) land(d, first);
|
|
298
|
+
repaint(first);
|
|
273
299
|
return true;
|
|
274
300
|
} catch (e) {
|
|
275
301
|
if (first) page.innerHTML = `<p class="muted">Could not load the questions: ${esc(e.message)}</p>`;
|
|
276
302
|
return false;
|
|
277
303
|
}
|
|
278
304
|
}
|
|
305
|
+
// A board, or a patch from the board held, from a load or a tap's POST. → false for a patch from a board no longer
|
|
306
|
+
// held, which leaves the page as it is.
|
|
307
|
+
function land(d, first) {
|
|
308
|
+
const next = d.since === undefined ? d : BoardUI.applyPatch(board, d);
|
|
309
|
+
if (!next) return false;
|
|
310
|
+
board = next;
|
|
311
|
+
all = board.owner;
|
|
312
|
+
items = board.items;
|
|
313
|
+
topics = cardTopics(board.ownerTopics, items);
|
|
314
|
+
site.syncBadge();
|
|
315
|
+
if (first) pick(hashId());
|
|
316
|
+
else if (soloId && byId(soloId)?.state !== 'open') pick('');
|
|
317
|
+
else if (!topics.some((t) => t.id === topicId)) pick('');
|
|
318
|
+
loaded = true;
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
// Draws what changed, unless the owner is typing a note.
|
|
322
|
+
function repaint(first) {
|
|
323
|
+
const sig = sigNow();
|
|
324
|
+
const active = document.activeElement;
|
|
325
|
+
if (sig !== shown && !(active && active.tagName === 'TEXTAREA')) {
|
|
326
|
+
shown = sig;
|
|
327
|
+
const y = scrollY;
|
|
328
|
+
render();
|
|
329
|
+
scrollTo(0, first ? 0 : y);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
279
332
|
|
|
280
|
-
function moveOn(from, pills, what) {
|
|
333
|
+
function moveOn(from, pills, what, footer = what, countOpen = true) {
|
|
281
334
|
soloId = null;
|
|
282
335
|
const next = nextQuestion(all, pills, { topic: from });
|
|
283
336
|
const left = topics.find((t) => t.id === from)?.open || 0;
|
|
284
337
|
if (next?.topic === from) {
|
|
285
338
|
topicId = from;
|
|
286
|
-
sentLine = `${
|
|
339
|
+
sentLine = countOpen ? `${footer.replace(/\.?$/, '.')} ${left} still open.` : footer;
|
|
287
340
|
shown = sigNow();
|
|
288
341
|
const y = scrollY;
|
|
289
342
|
render();
|
|
290
343
|
scrollTo(0, y);
|
|
291
344
|
return;
|
|
292
345
|
}
|
|
293
|
-
sentLine =
|
|
346
|
+
sentLine = footer;
|
|
294
347
|
if (!next) {
|
|
295
348
|
sentNotice.put(B.route, what);
|
|
296
349
|
if (ctx.up?.()) return;
|
|
@@ -308,7 +361,7 @@
|
|
|
308
361
|
B.on(root, 'click', (ev) => {
|
|
309
362
|
if (ev.target.id === 'showAll') { soloId = null; sentLine = ''; shown = ''; render(); return; }
|
|
310
363
|
const eye = ev.target.closest && ev.target.closest('[data-proto]');
|
|
311
|
-
if (eye) {
|
|
364
|
+
if (eye) { askPrototype(byId(eye.closest('section.q').dataset.q), eye); return; }
|
|
312
365
|
const b = ev.target.closest && ev.target.closest('[data-topic]');
|
|
313
366
|
if (b) { soloId = null; topicId = b.dataset.topic; sentLine = ''; shown = ''; render(); scrollTo(0, 0); }
|
|
314
367
|
});
|
|
@@ -359,11 +412,9 @@
|
|
|
359
412
|
const st = $('#qstatus');
|
|
360
413
|
if (uploading(visible())) return;
|
|
361
414
|
if (!list.length) { st.textContent = 'Pick an option or write a note first.'; return; }
|
|
362
|
-
st.textContent = '
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
const name = topicName(topics.find((t) => t.id === from) || { title: '' });
|
|
366
|
-
// docs/adr/0163-send-records-every-ready-answer-in-one-request.md: one request, recorded all or none.
|
|
415
|
+
st.textContent = '';
|
|
416
|
+
const at = leaving();
|
|
417
|
+
// docs/adr/2026-11-03-1519-send-records-every-ready-answer-in-one-request.md: one request, recorded all or none. Send waits meanwhile.
|
|
367
418
|
const answers = list.map((q) => {
|
|
368
419
|
const d = getDraft(q.id);
|
|
369
420
|
const last = q.answers[q.answers.length - 1];
|
|
@@ -371,17 +422,17 @@
|
|
|
371
422
|
const files = filesOf(q);
|
|
372
423
|
return { id: q.id, question: q.question, choice, label: choiceLabel(q, choice), note: (d.note || '').trim(), ...(files.length ? { files } : {}) };
|
|
373
424
|
});
|
|
374
|
-
|
|
425
|
+
let out;
|
|
426
|
+
try { out = await BoardUI.waitOn($('#send'), send('owner-answers', { answers })); }
|
|
375
427
|
catch (e) { st.textContent = e.reason ? `Nothing was sent: ${e.reason}` : 'Could not send. Is the host awake?'; return; }
|
|
376
428
|
for (const q of list) {
|
|
377
429
|
setDraft(q.id, {});
|
|
378
430
|
BoardAttach.clear('q:' + q.id, site.root);
|
|
379
431
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
else if (B.alive()) { sentLine = what; $('#qstatus').textContent = what; $('#qstatus').classList.add('sent'); }
|
|
432
|
+
await moveOnOnceLoaded(at, `Sent ${list.length} answer${list.length === 1 ? '' : 's'}${at.name ? ` to ${at.name}` : ''}.`, out, () => {
|
|
433
|
+
const open = all.filter((q) => q.state === 'open').length;
|
|
434
|
+
return `sent ${list.length} answer${list.length === 1 ? '' : 's'}${open ? ` · ${open} still open` : ''}`;
|
|
435
|
+
});
|
|
385
436
|
});
|
|
386
437
|
// Each composer painted raises a bubbling attach-change (attach.js).
|
|
387
438
|
B.on(root, 'attach-change', updateBar);
|