sign-off-axi 0.4.8 → 0.4.9
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/README.md +3 -1
- package/dist/cli.js +39 -10
- package/package.json +1 -1
- package/web/index.html +15 -13
package/README.md
CHANGED
|
@@ -172,7 +172,7 @@ the new page on a phone." \
|
|
|
172
172
|
|
|
173
173
|
*Proof of a fix, with two captures. Pass, Fail, or Needs work.*
|
|
174
174
|
|
|
175
|
-
Evidence lists
|
|
175
|
+
Evidence lists every update, never in **Waiting on you**: a passed run shows a tick, and a plain update that links no run, a failed run or an unchecked run says its honest result, each with its verdicts and Dismiss on the update's own screen. **Proven today** counts every update filed today, each with its own result. Updates are grouped by the project named in the run's proof line, `verify-<project>: ...` (trimmed, compared without case); a proof line with no such prefix goes under **Other**.
|
|
176
176
|
|
|
177
177
|
### Explainer video, with chapters
|
|
178
178
|
|
|
@@ -283,6 +283,8 @@ sign-off-axi signin done \
|
|
|
283
283
|
--key npm-login
|
|
284
284
|
```
|
|
285
285
|
|
|
286
|
+
To hide a card as the Dismiss button does, or bring it back: `sign-off-axi dismiss <key>` and `sign-off-axi restore <key>`.
|
|
287
|
+
|
|
286
288
|
<picture>
|
|
287
289
|
<source media="(prefers-color-scheme: dark)" srcset="docs/media/kind-signin-dark.webp">
|
|
288
290
|
<img src="docs/media/kind-signin.webp" alt="A sign-in card on top with an Open button, what it unlocks, who waits and the time left">
|
package/dist/cli.js
CHANGED
|
@@ -28,8 +28,8 @@ const AGENTS = {
|
|
|
28
28
|
'gemini-cli': { bin: 'gemini', dir: '.agents/skills', home: join(homedir(), '.gemini', 'skills') },
|
|
29
29
|
};
|
|
30
30
|
const TOP_HELP = `usage: sign-off-axi [command] [board.json] [flags]
|
|
31
|
-
commands[
|
|
32
|
-
(none)=list, raise, post, answers, wait, poll, open, status, stop, serve, export, init, report, signin, doctor, notify, validate, help
|
|
31
|
+
commands[20]:
|
|
32
|
+
(none)=list, raise, post, answers, wait, poll, open, status, stop, serve, export, init, report, signin, dismiss, restore, doctor, notify, validate, help
|
|
33
33
|
board: the nearest .sign-off/ board (the walk's bounds and SIGN_OFF_HOME: README, "Test runs never touch your board"), else ~/.sign-off/decisions.json; a board file given always wins
|
|
34
34
|
flags: --full and --json on list, answers and wait, --events on wait, --json on status, --filer on post, raise and wait, --help on every command, -v/--version
|
|
35
35
|
examples:
|
|
@@ -43,6 +43,7 @@ examples:
|
|
|
43
43
|
sign-off-axi post .sign-off/decisions.json --type report --title "Sign-in now takes 2 steps" --about "Why the code step moved first." --view mocks/sign-in-flow/index.html --preview mocks/sign-in-flow/preview.svg
|
|
44
44
|
sign-off-axi post --type signin --key npm-login --service npm --link https://example.com/login --unlocks "Publish the release" --waits "Main" --expires 15m
|
|
45
45
|
sign-off-axi signin done --key npm-login
|
|
46
|
+
sign-off-axi dismiss ship-the-new-sign-in-screen-this-week
|
|
46
47
|
sign-off-axi init
|
|
47
48
|
sign-off-axi doctor
|
|
48
49
|
sign-off-axi validate ask.json
|
|
@@ -248,6 +249,16 @@ Closes a sign-in card that post --type signin made: the sign-in is done, so the
|
|
|
248
249
|
Run it when you see the sign-in worked. A card posted with --start also closes by itself when its command ends with exit 0. wait --key <key> also returns once it is done.
|
|
249
250
|
examples:
|
|
250
251
|
sign-off-axi signin done --key npm-login
|
|
252
|
+
`,
|
|
253
|
+
dismiss: `usage: sign-off-axi dismiss <key> [board.json]
|
|
254
|
+
Hides a card from the board, like its Dismiss button. The agent is not told, and an ask it waits on keeps waiting.
|
|
255
|
+
restore <key> [board.json] brings it back. Find the key with \`sign-off-axi list --full\`.
|
|
256
|
+
examples:
|
|
257
|
+
sign-off-axi dismiss ship-the-new-sign-in-screen-this-week
|
|
258
|
+
sign-off-axi restore ship-the-new-sign-in-screen-this-week
|
|
259
|
+
`,
|
|
260
|
+
restore: `usage: sign-off-axi restore <key> [board.json]
|
|
261
|
+
Brings back a card that dismiss hid. See \`sign-off-axi dismiss --help\`.
|
|
251
262
|
`,
|
|
252
263
|
report: `usage: sign-off-axi report init <folder>
|
|
253
264
|
Writes a report page to fill: <folder>/index.html and its card picture <folder>/preview.svg. A file already there is kept.
|
|
@@ -502,7 +513,7 @@ function filerOf(values, session = false) {
|
|
|
502
513
|
const waitFor = (on, filer, key) => (filer ? `Run \`sign-off-axi wait${on} --filer ${filer}\` to block until the user answers`
|
|
503
514
|
: `Run \`sign-off-axi wait${on}${key ? ` --key ${key}` : ''}\` to block until the user answers`);
|
|
504
515
|
/** Still waiting on the user: not answered, not dismissed, and something to answer (an item with no options and no reply is only shown). */
|
|
505
|
-
const waits = (d) => !d.dismissed && awaitsVerdict(d.type, d.answer) && (d.options.length > 0 || !!d.reply || d.type === 'signin' || d.type === 'plan' || d.type === 'design');
|
|
516
|
+
const waits = (d) => d.type !== 'evidence' && !d.dismissed && awaitsVerdict(d.type, d.answer) && (d.options.length > 0 || !!d.reply || d.type === 'signin' || d.type === 'plan' || d.type === 'design');
|
|
506
517
|
/** The board's decisions with their latest answer; a missing board file is an empty board.
|
|
507
518
|
* A dismissal hides an item from the board and is not waiting here either, checked against the same dismissed log the board reads. */
|
|
508
519
|
function board(file, say = warn) {
|
|
@@ -543,7 +554,7 @@ function list(args) {
|
|
|
543
554
|
];
|
|
544
555
|
const where = tilde(file) + (existsSync(file) ? '' : ' (no file yet; raise creates it)');
|
|
545
556
|
if (values.full) {
|
|
546
|
-
return toon({ board: where, decisions: rows.map((d) => ({ key: d.key, status: awaitsVerdict(d.type, d.answer) ? (d.dismissed ? 'dismissed' : 'waiting') : d.answer.answer, title: d.title, about: d.about ?? '', detail: d.detail ?? '',
|
|
557
|
+
return toon({ board: where, decisions: rows.map((d) => ({ key: d.key, status: awaitsVerdict(d.type, d.answer) ? (d.dismissed ? 'dismissed' : d.type === 'evidence' ? 'shown' : 'waiting') : d.answer.answer, title: d.title, about: d.about ?? '', detail: d.detail ?? '',
|
|
547
558
|
recommend: d.recommend_value ?? '', default_if_silent: d.default_if_silent ?? '', close: d.close })) }, help);
|
|
548
559
|
}
|
|
549
560
|
return toon({
|
|
@@ -972,6 +983,22 @@ function signin(args) {
|
|
|
972
983
|
return toon({ board: tilde(file), closed: [{ key: d.key, status: saved ? 'done' : 'already done', service: d.title }] }, [`Run \`sign-off-axi list${on}\` to see what still waits on the user`]);
|
|
973
984
|
});
|
|
974
985
|
}
|
|
986
|
+
/** dismiss|restore <key> [board.json]: the board's Dismiss and Undo, for an agent. The same dismissed log the board writes, for a key that is on the board. */
|
|
987
|
+
function dismiss(restoring) {
|
|
988
|
+
const cmd = restoring ? 'restore' : 'dismiss';
|
|
989
|
+
return (args) => {
|
|
990
|
+
const at = args.findIndex((a) => !a.startsWith('-') && !existsSync(a) && !a.endsWith('.json')), key = args[at];
|
|
991
|
+
if (at < 0)
|
|
992
|
+
throw fail(`${cmd} needs a card key`, 'VALIDATION_ERROR', `sign-off-axi ${cmd} <key> [board.json]`);
|
|
993
|
+
const { file, on } = parse(cmd, args.filter((_, i) => i !== at), {});
|
|
994
|
+
return locked(`${file}.lock`, () => {
|
|
995
|
+
if (!isKey(key) || !board(file).some((x) => x.key === key))
|
|
996
|
+
throw fail(`no card with key ${key} on ${tilde(file)}`, 'NOT_FOUND', `Run \`sign-off-axi list${on} --full\` to see the keys`);
|
|
997
|
+
appendDismissed(file, [key], !restoring);
|
|
998
|
+
return toon({ board: tilde(file), [restoring ? 'restored' : 'dismissed']: [key] }, [`Run \`sign-off-axi ${restoring ? 'dismiss' : 'restore'}${on} ${key}\` to undo`]);
|
|
999
|
+
});
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
975
1002
|
/** For a program that feeds the board: the --from file, checked like any post, becomes the whole board, items and top-level fields. Answers stay.
|
|
976
1003
|
* With a filer it replaces only that filer's items (and items with no filer it names by key); every other item and top-level field stays. */
|
|
977
1004
|
function replace(file, named, on, from, filer) {
|
|
@@ -1101,7 +1128,7 @@ function postMedia(file, named, on, values, type, filer, message) {
|
|
|
1101
1128
|
if (asMessage(had) !== canon({ ...message, media: sources.length ? sources.map((src) => sha256(readFileSync(src))) : undefined })) {
|
|
1102
1129
|
throw fail(`key ${own} already holds a different message on ${tilde(file)}`, 'CONFLICT', 'Post it under a new key');
|
|
1103
1130
|
}
|
|
1104
|
-
return toon({ board: tilde(file), posted: [{ key: own, number: had.number, media: sources.length, title: had.title, status: 'already posted' }] }, [`Run \`sign-off-axi open${on}\` so the user can give a verdict`]);
|
|
1131
|
+
return toon({ board: tilde(file), posted: [{ key: own, number: had.number, media: sources.length, title: had.title, status: 'already posted' }] }, explainer ? [`Run \`sign-off-axi open${on}\` so the user can give a verdict`] : []);
|
|
1105
1132
|
}
|
|
1106
1133
|
const answers = readAnswers(file);
|
|
1107
1134
|
if (message && own && Object.hasOwn(answers, own) && !awaitsVerdict(message.kind, answers[own]))
|
|
@@ -1157,7 +1184,7 @@ function postMedia(file, named, on, values, type, filer, message) {
|
|
|
1157
1184
|
notifyEntered(file, named, before);
|
|
1158
1185
|
if (explainer)
|
|
1159
1186
|
return toon({ board: tilde(file), raised: [{ key, status: 'raised', title }] }, [`Run \`sign-off-axi open${on}\` so the user can watch and answer`, waitFor(on, filer, key)]);
|
|
1160
|
-
return toon({ board: tilde(file), posted: [{ key, number, media: media.length, title, ...(message && { status: 'posted' }) }] }
|
|
1187
|
+
return toon({ board: tilde(file), posted: [{ key, number, media: media.length, title, ...(message && { status: 'posted' }) }] });
|
|
1161
1188
|
});
|
|
1162
1189
|
}
|
|
1163
1190
|
/** post --type report: the flags as one report message, posted as post --kind report posts it. A report with no page is refused before anything is read. */
|
|
@@ -1708,10 +1735,12 @@ const tokenMissing = (n) => n.muxr
|
|
|
1708
1735
|
} })()
|
|
1709
1736
|
: n.token_env && !process.env[n.token_env] ? `${n.token_env} is not set` : undefined;
|
|
1710
1737
|
const NOTIFY_SENDER = `void (async()=>{const delays=[2000,10000,60000],f=process.env.SIGN_OFF_NOTIFY_TOKEN_FILE;for(let i=0;i<4;i++){try{const t=f?require('node:fs').readFileSync(f,'utf8').trim():process.env.SIGN_OFF_NOTIFY_TOKEN;if((await fetch(process.env.SIGN_OFF_NOTIFY_URL,{method:'POST',headers:{'content-type':'application/json',...(t&&{authorization:'Bearer '+t})},body:process.env.SIGN_OFF_NOTIFY_BODY,signal:AbortSignal.timeout(10000)})).ok)return}catch{}if(i<3)await new Promise(r=>setTimeout(r,delays[i]))}require('node:fs').appendFileSync(process.env.SIGN_OFF_NOTIFY_LOG,new Date().toISOString()+' notification send failed for card '+process.env.SIGN_OFF_NOTIFY_ID+'\\n')})();`;
|
|
1711
|
-
/**
|
|
1738
|
+
/** Cards worth a notice: those waiting on the user, and a new evidence update (shown, never waiting). */
|
|
1739
|
+
const notifies = (d) => waits(d) || (d.type === 'evidence' && !d.dismissed);
|
|
1740
|
+
/** The cards that would notify before a write: one that notifies after the write but not before it is new to the user. */
|
|
1712
1741
|
function waitingKeys(file) {
|
|
1713
1742
|
try {
|
|
1714
|
-
return new Set(board(file, () => { }).filter(
|
|
1743
|
+
return new Set(board(file, () => { }).filter(notifies).map((d) => d.key));
|
|
1715
1744
|
}
|
|
1716
1745
|
catch {
|
|
1717
1746
|
return undefined;
|
|
@@ -1725,7 +1754,7 @@ function notifyEntered(file, named, before) {
|
|
|
1725
1754
|
const so = dirname(named), notify = before ? readSetup(so).notify : undefined;
|
|
1726
1755
|
if (!before || !notify || tokenMissing(notify))
|
|
1727
1756
|
return;
|
|
1728
|
-
const { items } = readItems(file), fresh = board(file, () => { }).filter((d) =>
|
|
1757
|
+
const { items } = readItems(file), fresh = board(file, () => { }).filter((d) => notifies(d) && !before.has(d.key) && !items.some((i) => i?.key === d.key && i.announced));
|
|
1729
1758
|
if (!fresh.length)
|
|
1730
1759
|
return;
|
|
1731
1760
|
const at = new Date().toISOString().replace(/\.\d+Z$/, 'Z');
|
|
@@ -2788,7 +2817,7 @@ await runAxiCli({
|
|
|
2788
2817
|
topLevelHelp: TOP_HELP,
|
|
2789
2818
|
getCommandHelp: (cmd) => HELP[cmd],
|
|
2790
2819
|
home: axi('list', list),
|
|
2791
|
-
commands: { list: axi('list', list), raise: axi('raise', (args) => post(args, 'raise')), post: axi('post', post), answers: axi('answers', answers), wait: axi('wait', wait), poll: axi('poll', poll), open: axi('open', open), status: axi('status', status), stop: axi('stop', stop), serve: axi('serve', serve), export: axi('export', exportBoard), init: axi('init', init), report: axi('report', report), signin: axi('signin', signin), doctor: axi('doctor', doctor), notify: axi('notify', notify), validate, help: ([name, ...rest]) => {
|
|
2820
|
+
commands: { list: axi('list', list), raise: axi('raise', (args) => post(args, 'raise')), post: axi('post', post), answers: axi('answers', answers), wait: axi('wait', wait), poll: axi('poll', poll), open: axi('open', open), status: axi('status', status), stop: axi('stop', stop), serve: axi('serve', serve), export: axi('export', exportBoard), init: axi('init', init), report: axi('report', report), signin: axi('signin', signin), dismiss: axi('dismiss', dismiss(false)), restore: axi('restore', dismiss(true)), doctor: axi('doctor', doctor), notify: axi('notify', notify), validate, help: ([name, ...rest]) => {
|
|
2792
2821
|
if (rest.length || (name && !HELP[name]))
|
|
2793
2822
|
throw fail(`unknown command or argument: ${[name, ...rest].join(' ')}`, 'VALIDATION_ERROR', 'Run `sign-off-axi --help` to see the commands');
|
|
2794
2823
|
return name ? HELP[name] : TOP_HELP;
|
package/package.json
CHANGED
package/web/index.html
CHANGED
|
@@ -414,6 +414,7 @@
|
|
|
414
414
|
margin-top: 1rem; padding: .75rem 0 .5rem; background: var(--bg); }
|
|
415
415
|
/* Answer takes the row on one line; when a long label leaves no room, Skip and Later move under it rather than squeeze it. */
|
|
416
416
|
.decide-bar > * { white-space: nowrap; }
|
|
417
|
+
.decide-bar > .silent-line { white-space: normal; min-width: 0; } /* the stay-quiet line wraps; nowrap pushed it past a phone's edge in a wider font */
|
|
417
418
|
.decide-bar .answer { flex: 1 1 auto; background: var(--action); color: var(--on-accent); font-weight: 400; }
|
|
418
419
|
.decide-bar .later { display: inline-flex; flex: none; align-items: center; justify-content: center; min-height: 2.75rem; padding: 0 0.75rem; color: var(--accent); background: transparent; text-decoration: none; }
|
|
419
420
|
.decide-bar .skip { flex: none; padding: 0 0.75rem; color: var(--muted); background: transparent; }
|
|
@@ -437,8 +438,9 @@
|
|
|
437
438
|
.ev-card .t { font-size: 1.1875rem; font-weight: 600; line-height: 1.32; letter-spacing: -0.012em; }
|
|
438
439
|
.ev-group h2 { font-size: 1.375rem; line-height: 1.27; font-weight: 700; letter-spacing: -0.012em; }
|
|
439
440
|
.ev-card .m { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; color: var(--muted); font-size: 0.9375rem; }
|
|
440
|
-
.ev-ok { display: inline-flex; align-items: center; gap: 0.25rem; color: var(--ok); }
|
|
441
|
-
.ev-
|
|
441
|
+
.ev-ok, .ev-bad, .ev-warn { display: inline-flex; align-items: center; gap: 0.25rem; color: var(--ok); }
|
|
442
|
+
.ev-bad { color: var(--danger); } .ev-warn { color: var(--warn); } .ev-none { color: var(--muted); }
|
|
443
|
+
.ev-ok svg, .ev-bad svg, .ev-warn svg { width: 0.9375rem; height: 0.9375rem; fill: none; stroke: currentColor; stroke-width: 2.4; stroke-linecap: round; stroke-linejoin: round; }
|
|
442
444
|
/* One update: the whole capture on its tile, what it proves in a line, what the check recorded, then its verdict. */
|
|
443
445
|
#reader .ev-scroll { flex: 1; min-height: 0; overflow: auto; padding: 1rem 1.25rem 3rem; }
|
|
444
446
|
.ev-item { display: grid; gap: 1rem; max-width: 42.5rem; margin: 0 auto; }
|
|
@@ -1180,9 +1182,10 @@
|
|
|
1180
1182
|
}
|
|
1181
1183
|
/** One update on the Evidence screen: its capture as the hero, what it proves and how it went; the whole card opens the update. */
|
|
1182
1184
|
function evCard(d) {
|
|
1185
|
+
const result = RESULT[d.evidenceCard?.result];
|
|
1183
1186
|
return el('a', { class: 'ev-card', href: `#evidence/${d.key}`, id: `item-${d.key}` }, el('span', { class: 'ev-media' }, ...hero(d)),
|
|
1184
1187
|
el('span', { class: 't', dir: 'auto' }, headline(d)),
|
|
1185
|
-
el('span', { class: 'm' }, el('span', { class:
|
|
1188
|
+
el('span', { class: 'm' }, result ? el('span', { class: `ev-${result[0]}` }, mark(result[0]), result[2]) : el('span', { class: 'ev-none' }, 'Not run'), d.filed ? el('span', {}, stamp(d.filed)) : ''));
|
|
1186
1189
|
}
|
|
1187
1190
|
/** Evidence as its own screen: a card per passed update, grouped under the project its check names, in the order each project first shows;
|
|
1188
1191
|
* proof that names no project goes under Other, last. An item put in this area that is not an update (a page, a diagram) keeps the card it always had. */
|
|
@@ -1368,13 +1371,10 @@
|
|
|
1368
1371
|
const icon = (a) => { const s = el('span', { class: `icon i-${a}`, 'aria-hidden': 'true' }); s.innerHTML = `<svg viewBox="0 0 24 24"><path d="${ICONS[a]}"/></svg>`; return s; };
|
|
1369
1372
|
// A design with only screen notes (answer 'screens') still waits for its design-wide verdict.
|
|
1370
1373
|
const isAnswered = (d) => Object.hasOwn(state.answers, d.key) && !(d.type === 'design' && state.answers[d.key].answer === 'screens');
|
|
1371
|
-
/** Each item lives in one area: answered items, then
|
|
1374
|
+
/** Each item lives in one area: answered items, then the area the item names, then evidence updates (never an ask, whatever their result), then what is only to read (explainers, pages with nothing to answer), then the open asks.
|
|
1372
1375
|
* A plan has no options but waits for its send-back. */
|
|
1373
|
-
/** An update the Evidence list shows: passed proof only; every other update (no run, a failed or unfinished check) waits on you. */
|
|
1374
|
-
const listed = (d) => d.evidenceCard?.result === 'RUN PASS';
|
|
1375
1376
|
const area = (d) => {
|
|
1376
|
-
const a = isAnswered(d) ? 'answered' : d.type === 'evidence'
|
|
1377
|
-
: state.readonly || d.type === 'explainer' || !(d.options.length || d.reply || d.type === 'signin' || d.type === 'plan' || d.kind === 'design') ? 'reports' : 'waiting');
|
|
1377
|
+
const a = isAnswered(d) ? 'answered' : d.type === 'evidence' ? 'evidence' : d.area ?? (state.readonly || d.type === 'explainer' || !(d.options.length || d.reply || d.type === 'signin' || d.type === 'plan' || d.kind === 'design') ? 'reports' : 'waiting');
|
|
1378
1378
|
// Reports lists only what explains: a page to tap through, or a narrated video. Text alone (a receipt, a status line) is a note, folded under it.
|
|
1379
1379
|
return a === 'reports' && !state.readonly && d.type !== 'explainer' && !reads(d) ? 'notes' : a;
|
|
1380
1380
|
};
|
|
@@ -1431,7 +1431,7 @@
|
|
|
1431
1431
|
const n = by.waiting.length, proven = filedToday(by.evidence).length, fresh = filedToday(by.reports).length;
|
|
1432
1432
|
const soonest = by.waiting.map((d) => d.silent_by).filter(Boolean).sort((x, y) => Date.parse(x) - Date.parse(y))[0];
|
|
1433
1433
|
const lines = [[n ? `${n} ${n === 1 ? 'decision needs' : 'decisions need'} you.` : 'Nothing needs you.', 1], [soonest && `The soonest closes at ${when(soonest)}.`],
|
|
1434
|
-
[proven && `${proven} ${proven === 1 ? '
|
|
1434
|
+
[proven && `${proven} ${proven === 1 ? 'update' : 'updates'} filed.`, 1], [fresh && `${fresh} new ${fresh === 1 ? 'report is' : 'reports are'} waiting.`]];
|
|
1435
1435
|
if (lastSeen === undefined) try { lastSeen = localStorage.getItem(seenKey()); } catch { lastSeen = null; }
|
|
1436
1436
|
return el('p', { class: 'card glance' }, lastSeen && !Number.isNaN(Date.parse(lastSeen)) ? el('span', { class: 'seen' }, `Since you last looked at ${when(lastSeen)}`) : '', ...lines.filter(([t]) => t).map(([t, strong]) => el(strong ? 'b' : 'span', {}, `${t} `)));
|
|
1437
1437
|
}
|
|
@@ -1471,9 +1471,9 @@
|
|
|
1471
1471
|
return el('div', { class: 'search', role: 'search', ...(query === null && { hidden: '' }) }, box, out);
|
|
1472
1472
|
}
|
|
1473
1473
|
|
|
1474
|
-
/** Proven today on Today: a swipeable row of today's
|
|
1474
|
+
/** Proven today on Today: a swipeable row of today's updates, each with its own result and capture; a wide screen sets them three across. */
|
|
1475
1475
|
function proven(items) {
|
|
1476
|
-
const fresh = items
|
|
1476
|
+
const fresh = filedToday(items);
|
|
1477
1477
|
return el('section', { class: 'i-evidence wide' }, el('div', { class: 'sec-head' }, el('h2', {}, 'Proven today'), items.length ? el('a', { href: '#evidence' }, 'See all') : ''),
|
|
1478
1478
|
fresh.length ? el('div', { class: 'ev-scroll-row' }, ...fresh.slice(0, 3).map((d) => withX(d, evCard(d)))) : el('p', { class: 'empty' }, 'Nothing proven today yet.'));
|
|
1479
1479
|
}
|
|
@@ -1975,7 +1975,9 @@
|
|
|
1975
1975
|
});
|
|
1976
1976
|
};
|
|
1977
1977
|
if (d.type === 'signin') {
|
|
1978
|
-
const
|
|
1978
|
+
const gone = xBtn(d, 'Dismiss');
|
|
1979
|
+
if (gone) gone.addEventListener('click', () => (location.hash = to));
|
|
1980
|
+
const signinPage = el('div', { class: 'decide' }, row(d, { x: false }), el('div', { class: 'decide-bar' }, skip || '', el('a', { class: 'later', href: '#waiting' }, 'Later'), gone), decideLists(d, queue));
|
|
1979
1981
|
if (!state.readonly) swipe(signinPage);
|
|
1980
1982
|
return signinPage;
|
|
1981
1983
|
}
|
|
@@ -2077,7 +2079,7 @@
|
|
|
2077
2079
|
? 'How the tools work, step by step. Tap one to open it.' : 'Explainers you asked for, or that a decision needs.')
|
|
2078
2080
|
: at === 'answered' && by.answered.length ? el('p', { class: 'lede' }, (() => { const n = by.answered.filter((d) => isToday(state.answers[d.key].at)).length; return `${n} ${n === 1 ? 'answer' : 'answers'} today. Each one has gone back to its agent.`; })())
|
|
2079
2081
|
: at === 'designs' && by.designs.length ? el('p', { class: 'lede' }, 'Designs to look over. Approve one, or say what should change.')
|
|
2080
|
-
: at === 'evidence' && by.evidence.length ? el('p', { class: 'lede' }, '
|
|
2082
|
+
: at === 'evidence' && by.evidence.length ? el('p', { class: 'lede' }, 'Updates that show a change works, newest first.') : '', at === 'decide' ? '' : bulkBar(bulk));
|
|
2081
2083
|
document.getElementById('board').replaceChildren(top, ...(at === 'waiting' && !state.readonly ? [filters(by.waiting), waitingPage(shown, hidden)]
|
|
2082
2084
|
: at === 'decide' ? [decideAt < 0 ? (key ? el('p', { class: 'empty' }, 'Nothing to decide for that question.') : el('div', { class: 'decide' }, decideLists(null, decideQueue)))
|
|
2083
2085
|
: decidePage(decideQueue[decideAt], decideQueue.length > 1 ? decideQueue[(decideAt + 1) % decideQueue.length] : undefined, decideQueue)]
|