instar 1.3.814 → 1.3.815
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/dashboard/glance.js
CHANGED
|
@@ -41,21 +41,22 @@ export const GLANCE_MAX_TOKEN_LEN = 40; // a longer token is a glued-word budget
|
|
|
41
41
|
// (same discipline as the F3 purpose-line exempt list). The completeness test
|
|
42
42
|
// asserts adopted ∪ grandfathered == every TAB_REGISTRY id, so a NEW tab in NEITHER
|
|
43
43
|
// set fails the build; the monotonicity test asserts the grandfather size ≤ ceiling.
|
|
44
|
-
export const GLANCE_ADOPTED_TABS = ['commitments'];
|
|
44
|
+
export const GLANCE_ADOPTED_TABS = ['commitments', 'blockers'];
|
|
45
45
|
|
|
46
46
|
export const GLANCE_GRANDFATHERED = [
|
|
47
47
|
'insights', 'sessions', 'files', 'dropzone', 'jobs', 'features', 'systems',
|
|
48
48
|
'integrated-being', 'pr-pipeline', 'projects', 'initiatives', 'tokens',
|
|
49
49
|
'resources', 'llm-activity', 'routing-map', 'spend', 'threadline', 'evidence',
|
|
50
50
|
'process-health', 'subscriptions', 'preferences-learning', 'machines', 'mandates',
|
|
51
|
-
'
|
|
52
|
-
];
|
|
51
|
+
'secrets',
|
|
52
|
+
]; // 'blockers' left the list this PR (Phase 2) — it now builds through this component.
|
|
53
53
|
|
|
54
54
|
// The committed ceiling on grandfathered-tab count. Only ever LOWER this (each
|
|
55
55
|
// lowering marks a tab retrofitted onto the floor). Never raise it without an
|
|
56
56
|
// operator-signed justification — raising it is how a NEW tab would silently ship
|
|
57
57
|
// below the floor, the exact regression the ratchet exists to prevent.
|
|
58
|
-
|
|
58
|
+
// Lowered 25 → 24 (Phase 2): Blockers retrofitted onto the glance floor.
|
|
59
|
+
export const GLANCE_GRANDFATHERED_CEILING = 24;
|
|
59
60
|
|
|
60
61
|
// ── F10 — insider-vocab detection ────────────────────────────────────────────
|
|
61
62
|
// A readability floor, NOT a secret-redaction boundary (secret handling stays at
|
|
@@ -425,25 +426,40 @@ export function commitmentsOpenPopulation(commitments) {
|
|
|
425
426
|
|
|
426
427
|
export function buildCommitmentsGlance(commitments, now = Date.now()) {
|
|
427
428
|
const open = commitmentsOpenPopulation(commitments);
|
|
428
|
-
|
|
429
|
+
// OVERDUE TAKES PRECEDENCE over due-soon (issue #1435 §3): a promise whose HARD
|
|
430
|
+
// deadline is already in the past is OVERDUE, never merely "due soon". The old
|
|
431
|
+
// build classified due-soon purely from atRisk, so a stale beacon record a month
|
|
432
|
+
// past its hard deadline showed as "due soon" — the reported defect. Classify
|
|
433
|
+
// overdue FIRST, then take due-soon over the REMAINDER (atRisk but not overdue).
|
|
434
|
+
const overdue = open.filter((c) => c.hardDeadlineAt && Date.parse(c.hardDeadlineAt) < now);
|
|
435
|
+
const overdueSet = new Set(overdue);
|
|
436
|
+
const dueSoon = open.filter((c) => c.atRisk === true && !overdueSet.has(c));
|
|
429
437
|
const waiting = open.filter((c) => c.blockedOn === 'user-input' || c.blockedOn === 'user-authorization');
|
|
430
438
|
const quiet = open.filter((c) => c.beaconSuppressed === true);
|
|
431
|
-
const overdue = open.filter((c) => c.hardDeadlineAt && Date.parse(c.hardDeadlineAt) < now);
|
|
432
439
|
|
|
433
440
|
// Component-authored, jargon-free headline — honest to the one population.
|
|
441
|
+
// Count-aware verb agreement (#1435 §2): "1 needs" / "2 need", "1 is" / "2 are".
|
|
434
442
|
let headline;
|
|
435
443
|
if (open.length === 0) {
|
|
436
444
|
headline = "You have no open promises right now.";
|
|
437
445
|
} else {
|
|
438
|
-
const soonClause = dueSoon.length
|
|
439
|
-
|
|
446
|
+
const soonClause = dueSoon.length === 0 ? 'none need attention soon'
|
|
447
|
+
: dueSoon.length === 1 ? '1 needs attention soon'
|
|
448
|
+
: `${dueSoon.length} need attention soon`;
|
|
449
|
+
const overdueClause = overdue.length === 0 ? 'none are overdue'
|
|
450
|
+
: overdue.length === 1 ? '1 is overdue'
|
|
451
|
+
: `${overdue.length} are overdue`;
|
|
440
452
|
const noun = open.length === 1 ? 'open promise' : 'open promises';
|
|
441
453
|
headline = `I'm carrying ${open.length} ${noun}; ${soonClause}, ${overdueClause}.`;
|
|
442
454
|
}
|
|
443
455
|
|
|
456
|
+
// Every number the headline states now has a tile to drill into (#1435 §1): the
|
|
457
|
+
// "overdue" count gets its own OVERDUE tile — the most actionable state, so we add
|
|
458
|
+
// the tile rather than dropping the clause. Five tiles = the F10 max.
|
|
444
459
|
const tiles = [
|
|
445
460
|
{ key: 'open', label: 'Open', value: String(open.length), tone: 'neutral', rows: open },
|
|
446
461
|
{ key: 'due-soon', label: 'Due soon', value: String(dueSoon.length), tone: dueSoon.length ? 'warn' : 'neutral', rows: dueSoon },
|
|
462
|
+
{ key: 'overdue', label: 'Overdue', value: String(overdue.length), tone: overdue.length ? 'warn' : 'neutral', rows: overdue },
|
|
447
463
|
{ key: 'waiting', label: 'Waiting on you', value: String(waiting.length), tone: waiting.length ? 'warn' : 'neutral', rows: waiting },
|
|
448
464
|
{ key: 'quiet', label: 'Quiet', value: String(quiet.length), tone: 'muted', rows: quiet },
|
|
449
465
|
];
|
|
@@ -531,3 +547,140 @@ export function commitmentsGlanceSpec(doc, commitments, opts = {}) {
|
|
|
531
547
|
}));
|
|
532
548
|
return { headline: base.headline, tiles, population: base.population };
|
|
533
549
|
}
|
|
550
|
+
|
|
551
|
+
// ── Blockers reference builder (the Phase-2 rebuild) ──────────────────────────
|
|
552
|
+
// Turns the /blockers ledger entries into a glance spec. A blocker is a DECAYING
|
|
553
|
+
// HYPOTHESIS, not a settled wall (docs/specs/dashboard-ux-standard.md): each entry
|
|
554
|
+
// moves through a pipeline (candidate → authority-checked → access-requested →
|
|
555
|
+
// dry-run → live-run) and terminates as either RESOLVED (it turned out not to be a
|
|
556
|
+
// wall) or a TRUE-BLOCKER (the best current understanding, with a recheck date —
|
|
557
|
+
// never "stop trying"). Every tile maps to a state predicate over ONE population
|
|
558
|
+
// (the non-archived ledger entries GET /blockers returns), so the counts are honest
|
|
559
|
+
// and each headline number has exactly one tile to drill into. The full record
|
|
560
|
+
// (id, state, timestamps, terminal detail) lives at Layer 3, one click below the
|
|
561
|
+
// plain-language row — the ~7,000-word raw table is gone from the front page but
|
|
562
|
+
// nothing is lost, only moved down a layer.
|
|
563
|
+
|
|
564
|
+
const BLOCKER_NON_TERMINAL = new Set([
|
|
565
|
+
'candidate', 'authority-checked', 'access-requested', 'dry-run', 'live-run',
|
|
566
|
+
]);
|
|
567
|
+
|
|
568
|
+
// Plain-word state names for the Layer-3 record (the raw state token stays honest to
|
|
569
|
+
// the ledger but reads as everyday language, never insider jargon at the glance).
|
|
570
|
+
const BLOCKER_STATE_WORD = {
|
|
571
|
+
'candidate': 'Just spotted',
|
|
572
|
+
'authority-checked': 'Checked who can clear it',
|
|
573
|
+
'access-requested': 'Asked you for access',
|
|
574
|
+
'dry-run': 'Trying it safely first',
|
|
575
|
+
'live-run': 'Attempting it for real',
|
|
576
|
+
'resolved': 'Resolved — not a wall after all',
|
|
577
|
+
'true-blocker': 'Truly stuck for now (recheck scheduled)',
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
/** The single population: the non-archived ledger entries GET /blockers returns. */
|
|
581
|
+
export function blockersPopulation(entries) {
|
|
582
|
+
return (Array.isArray(entries) ? entries : []).filter((e) => e && typeof e.state === 'string');
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
export function buildBlockersGlance(entries) {
|
|
586
|
+
const all = blockersPopulation(entries);
|
|
587
|
+
const working = all.filter((e) => BLOCKER_NON_TERMINAL.has(e.state));
|
|
588
|
+
const stuck = all.filter((e) => e.state === 'true-blocker');
|
|
589
|
+
const resolved = all.filter((e) => e.state === 'resolved');
|
|
590
|
+
|
|
591
|
+
// Component-authored, jargon-free headline — honest to the one population, with
|
|
592
|
+
// count-aware verb agreement.
|
|
593
|
+
let headline;
|
|
594
|
+
if (all.length === 0) {
|
|
595
|
+
headline = 'No blockers are being tracked right now.';
|
|
596
|
+
} else {
|
|
597
|
+
const stuckClause = stuck.length === 0 ? 'Nothing is truly stuck right now'
|
|
598
|
+
: stuck.length === 1 ? '1 thing is truly stuck right now'
|
|
599
|
+
: `${stuck.length} things are truly stuck right now`;
|
|
600
|
+
const workClause = working.length === 0 ? 'none are being worked'
|
|
601
|
+
: working.length === 1 ? '1 is being worked'
|
|
602
|
+
: `${working.length} are being worked`;
|
|
603
|
+
headline = `${stuckClause}; ${workClause}.`;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const tiles = [
|
|
607
|
+
{ key: 'stuck', label: 'Truly stuck', value: String(stuck.length), tone: stuck.length ? 'warn' : 'neutral', rows: stuck },
|
|
608
|
+
{ key: 'working', label: 'Being worked', value: String(working.length), tone: 'neutral', rows: working },
|
|
609
|
+
{ key: 'resolved', label: 'Resolved', value: String(resolved.length), tone: 'muted', rows: resolved },
|
|
610
|
+
];
|
|
611
|
+
|
|
612
|
+
return { headline, tiles, population: all };
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/** One plain-word Layer-2 row for a blocker — the plain-language framing that opened
|
|
616
|
+
* it (its detectedText). IDs/timestamps/state machinery are Layer 3, not here. */
|
|
617
|
+
export function blockerRowText(e) {
|
|
618
|
+
return sanitizeForDisplay(e.detectedText || e.origin || 'A blocker', 'summary');
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Layer-3 full record for a blocker — every existing column plus terminal detail,
|
|
623
|
+
* one click below the plain Layer-2 row. All values via textContent (XSS-safe):
|
|
624
|
+
* detectedText / origin / terminal free text are UNTRUSTED and are displayed, never
|
|
625
|
+
* interpreted. This is where the raw state token, id, and timestamps legitimately
|
|
626
|
+
* live (they used to be dumped on the front page).
|
|
627
|
+
*/
|
|
628
|
+
export function blockerRecordNode(doc, e, opts = {}) {
|
|
629
|
+
const fmtTs = opts.fmtTs || defaultFmtTs;
|
|
630
|
+
const wrap = el(doc, 'div', 'glance-record-fields');
|
|
631
|
+
const rows = [
|
|
632
|
+
['What looked stuck', e.detectedText || '—'],
|
|
633
|
+
['state', BLOCKER_STATE_WORD[e.state] || e.state || '—'],
|
|
634
|
+
['id', e.id || '—'],
|
|
635
|
+
['opened by', e.origin || '—'],
|
|
636
|
+
['first seen', fmtTs(e.createdAt)],
|
|
637
|
+
['last update', fmtTs(e.updatedAt)],
|
|
638
|
+
];
|
|
639
|
+
const t = e.terminal;
|
|
640
|
+
if (t && t.kind === 'resolved') {
|
|
641
|
+
rows.push(['outcome', 'Resolved — it turned out not to be a wall']);
|
|
642
|
+
if (t.playbookPath) rows.push(['playbook', t.playbookPath]);
|
|
643
|
+
} else if (t && t.kind === 'true-blocker') {
|
|
644
|
+
rows.push(['outcome', `Best current understanding (${t.reasonKind || 'reason unknown'}) — not "give up"`]);
|
|
645
|
+
if (t.recheckAfter) rows.push(['recheck after', fmtTs(t.recheckAfter)]);
|
|
646
|
+
}
|
|
647
|
+
for (const [k, v] of rows) {
|
|
648
|
+
const row = el(doc, 'div', 'glance-record-row');
|
|
649
|
+
row.appendChild(el(doc, 'span', 'glance-record-key', String(k)));
|
|
650
|
+
row.appendChild(el(doc, 'span', 'glance-record-val', String(v)));
|
|
651
|
+
wrap.appendChild(row);
|
|
652
|
+
}
|
|
653
|
+
return wrap;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Build the FULL Blockers glance spec with drill wiring — importable by index.html
|
|
658
|
+
* AND the three test tiers. Each tile's onActivate renders the filtered ledger
|
|
659
|
+
* entries as plain Layer-2 rows; each row opens the Layer-3 record. Population +
|
|
660
|
+
* counts come from buildBlockersGlance (one denominator, honest counts).
|
|
661
|
+
*/
|
|
662
|
+
export function blockersGlanceSpec(doc, entries, opts = {}) {
|
|
663
|
+
const base = buildBlockersGlance(entries);
|
|
664
|
+
const tiles = base.tiles.map((t) => ({
|
|
665
|
+
key: t.key,
|
|
666
|
+
label: t.label,
|
|
667
|
+
value: t.value,
|
|
668
|
+
tone: t.tone,
|
|
669
|
+
onActivate: ({ doc: d, drilldown, openRecord }) => {
|
|
670
|
+
const rows = t.rows || [];
|
|
671
|
+
if (rows.length === 0) return; // component renders the honest F6 empty-state
|
|
672
|
+
const list = el(d, 'div', 'glance-list');
|
|
673
|
+
for (const e of rows) {
|
|
674
|
+
const row = d.createElement('button');
|
|
675
|
+
row.type = 'button';
|
|
676
|
+
row.className = 'glance-list-row';
|
|
677
|
+
row.setAttribute('aria-label', 'Open the full record');
|
|
678
|
+
row.appendChild(el(d, 'span', 'glance-list-summary', blockerRowText(e)));
|
|
679
|
+
row.addEventListener('click', () => openRecord(blockerRecordNode(d, e, { fmtTs: opts.fmtTs })));
|
|
680
|
+
list.appendChild(row);
|
|
681
|
+
}
|
|
682
|
+
drilldown.appendChild(list);
|
|
683
|
+
},
|
|
684
|
+
}));
|
|
685
|
+
return { headline: base.headline, tiles, population: base.population };
|
|
686
|
+
}
|
package/dashboard/index.html
CHANGED
|
@@ -3455,25 +3455,27 @@
|
|
|
3455
3455
|
</div>
|
|
3456
3456
|
</div>
|
|
3457
3457
|
|
|
3458
|
-
<!-- Blockers tab — Blocker Ledger read surface
|
|
3458
|
+
<!-- Blockers tab — Blocker Ledger read surface, rebuilt on the glance template
|
|
3459
|
+
(Dashboard UX Standard F10/F11, topic 29836 Phase 2).
|
|
3459
3460
|
A blocker is a DECAYING HYPOTHESIS, not a settled wall: each entry moves through
|
|
3460
3461
|
candidate → authority-checked → access-requested → dry-run → live-run → resolved /
|
|
3461
3462
|
true-blocker. A true-blocker is "best current understanding — recheck after <date>",
|
|
3462
3463
|
never "stop trying". Read-only here; data from GET /blockers. detectedText and all
|
|
3463
|
-
free-text are UNTRUSTED
|
|
3464
|
+
free-text are UNTRUSTED — the shared glance component renders every value through
|
|
3465
|
+
sanitizeForDisplay + textContent (never innerHTML). 503 = feature off. -->
|
|
3464
3466
|
<div id="blockersPanel" class="tab-panel" style="display:none;flex-direction:column;padding:20px;gap:16px;overflow-y:auto">
|
|
3465
3467
|
<div style="display:flex;justify-content:space-between;align-items:center">
|
|
3466
3468
|
<h2 style="margin:0">Blockers</h2>
|
|
3467
3469
|
<button onclick="loadBlockers()" style="padding:6px 12px">Refresh</button>
|
|
3468
3470
|
</div>
|
|
3469
3471
|
<div class="tab-purpose">
|
|
3470
|
-
Things that looked like they stopped progress, tracked as <b>decaying hypotheses</b> rather than settled walls
|
|
3471
|
-
|
|
3472
|
-
never a permanent "give up".
|
|
3473
|
-
</div>
|
|
3474
|
-
<div id="blockersBody" style="border:1px solid var(--border);border-radius:8px;padding:16px;font-size:13px">
|
|
3475
|
-
<div style="color:var(--text-dim)">Loading...</div>
|
|
3472
|
+
Things that looked like they stopped progress, tracked as <b>decaying hypotheses</b> rather than settled walls —
|
|
3473
|
+
the headline says where things stand; tap a tile to see which ones, tap one for the full record.
|
|
3474
|
+
A "truly stuck" blocker is the best current understanding with a recheck date, never a permanent "give up".
|
|
3476
3475
|
</div>
|
|
3476
|
+
<!-- Glance floors F10/F11 (topic 29836): the shared component renders the
|
|
3477
|
+
headline + tiles + drill-down here. -->
|
|
3478
|
+
<div id="blockersGlance" class="glance-root"></div>
|
|
3477
3479
|
</div>
|
|
3478
3480
|
|
|
3479
3481
|
<!-- LLM Activity tab — Observable Intelligence (docs/specs/observable-intelligence.md).
|
|
@@ -6305,79 +6307,43 @@
|
|
|
6305
6307
|
}
|
|
6306
6308
|
}
|
|
6307
6309
|
|
|
6308
|
-
// ── Blockers Tab (
|
|
6309
|
-
// A blocker is a decaying hypothesis, not a settled wall.
|
|
6310
|
-
//
|
|
6311
|
-
|
|
6312
|
-
|
|
6313
|
-
|
|
6314
|
-
|
|
6315
|
-
|
|
6316
|
-
'dry-run': '#d6a700',
|
|
6317
|
-
'live-run': '#d6a700',
|
|
6318
|
-
'resolved': 'var(--accent)',
|
|
6319
|
-
'true-blocker': 'var(--red)',
|
|
6320
|
-
};
|
|
6321
|
-
const bg = colors[state] || 'var(--text-dim)';
|
|
6322
|
-
return '<span style="display:inline-block;padding:2px 8px;border-radius:10px;font-size:11px;' +
|
|
6323
|
-
'font-weight:600;color:#fff;white-space:nowrap;background:' + bg + '">' +
|
|
6324
|
-
escapeHtml(String(state || 'unknown')) + '</span>';
|
|
6325
|
-
}
|
|
6326
|
-
function blockerTerminalLine(entry) {
|
|
6327
|
-
const t = entry && entry.terminal;
|
|
6328
|
-
if (!t) return '';
|
|
6329
|
-
if (t.kind === 'resolved') {
|
|
6330
|
-
const path = t.playbookPath ? ' · playbook: ' + escapeHtml(String(t.playbookPath)) : '';
|
|
6331
|
-
return '<div style="font-size:12px;color:var(--accent);margin-top:4px">Resolved' + path + '</div>';
|
|
6332
|
-
}
|
|
6333
|
-
if (t.kind === 'true-blocker') {
|
|
6334
|
-
// NEVER frame a true-blocker as settled / "stop trying" — it's a decaying hypothesis.
|
|
6335
|
-
const reason = t.reasonKind ? escapeHtml(String(t.reasonKind)) : 'unknown reason';
|
|
6336
|
-
const recheck = t.recheckAfter ? fmtRelTime(t.recheckAfter) : 'unscheduled';
|
|
6337
|
-
return '<div style="font-size:12px;color:var(--red);margin-top:4px">Current hypothesis (' + reason +
|
|
6338
|
-
') — recheck after ' + escapeHtml(recheck) + '</div>';
|
|
6339
|
-
}
|
|
6340
|
-
return '';
|
|
6341
|
-
}
|
|
6310
|
+
// ── Blockers Tab — glance floors F10/F11 (topic 29836, Phase 2) ──
|
|
6311
|
+
// A blocker is a decaying hypothesis, not a settled wall. Rebuilt on the shared
|
|
6312
|
+
// glance component: a plain-English headline + tiles (Layer 1); each tile drills
|
|
6313
|
+
// into the filtered ledger entries as plain sentences (Layer 2); each row opens
|
|
6314
|
+
// the full record with state/id/timestamps/terminal detail (Layer 3). detectedText
|
|
6315
|
+
// + all free text are UNTRUSTED → the component renders every value through
|
|
6316
|
+
// sanitizeForDisplay + textContent (never innerHTML), so this is XSS-safe by
|
|
6317
|
+
// construction (an improvement over the old escapeHtml-into-innerHTML table).
|
|
6342
6318
|
async function loadBlockers() {
|
|
6343
|
-
const
|
|
6319
|
+
const glanceRoot = document.getElementById('blockersGlance');
|
|
6320
|
+
if (!glanceRoot) return;
|
|
6321
|
+
|
|
6322
|
+
const glance = await loadGlanceModule();
|
|
6323
|
+
if (!glance) {
|
|
6324
|
+
glanceRoot.textContent = 'Loading the glance view failed — refresh to retry.';
|
|
6325
|
+
return;
|
|
6326
|
+
}
|
|
6327
|
+
|
|
6328
|
+
let resp = null;
|
|
6344
6329
|
try {
|
|
6345
|
-
|
|
6346
|
-
const entries = (resp && resp.entries) || [];
|
|
6347
|
-
if (entries.length === 0) {
|
|
6348
|
-
bodyEl.innerHTML = '<div style="color:var(--text-dim)">No blockers recorded' +
|
|
6349
|
-
((resp && resp.total != null) ? ' (total ' + escapeHtml(String(resp.total)) + ')' : '') + '.</div>';
|
|
6350
|
-
return;
|
|
6351
|
-
}
|
|
6352
|
-
bodyEl.innerHTML =
|
|
6353
|
-
'<table style="width:100%;border-collapse:collapse;font-size:13px">' +
|
|
6354
|
-
'<thead><tr style="text-align:left;color:var(--text-dim)">' +
|
|
6355
|
-
'<th style="padding:6px 8px">State</th>' +
|
|
6356
|
-
'<th style="padding:6px 8px">ID</th>' +
|
|
6357
|
-
'<th style="padding:6px 8px">Detected</th>' +
|
|
6358
|
-
'<th style="padding:6px 8px">Origin</th>' +
|
|
6359
|
-
'<th style="padding:6px 8px;text-align:right">Updated</th>' +
|
|
6360
|
-
'</tr></thead><tbody>' +
|
|
6361
|
-
entries.map(e =>
|
|
6362
|
-
'<tr style="border-top:1px solid var(--border);vertical-align:top">' +
|
|
6363
|
-
'<td style="padding:6px 8px">' + blockerStateBadge(e.state) + '</td>' +
|
|
6364
|
-
'<td style="padding:6px 8px;font-family:monospace;color:var(--text-dim)">' + escapeHtml(String(e.id || '—')) + '</td>' +
|
|
6365
|
-
'<td style="padding:6px 8px">' + escapeHtml(String(e.detectedText || '')) + blockerTerminalLine(e) + '</td>' +
|
|
6366
|
-
'<td style="padding:6px 8px">' + escapeHtml(String(e.origin || '—')) + '</td>' +
|
|
6367
|
-
'<td style="padding:6px 8px;text-align:right;white-space:nowrap">' + escapeHtml(fmtRelTime(e.updatedAt || e.createdAt)) + '</td>' +
|
|
6368
|
-
'</tr>'
|
|
6369
|
-
).join('') +
|
|
6370
|
-
'</tbody></table>';
|
|
6330
|
+
resp = await apiFetch('/blockers');
|
|
6371
6331
|
} catch (err) {
|
|
6372
6332
|
const m = err && err.message ? String(err.message) : String(err);
|
|
6333
|
+
glanceRoot.textContent = '';
|
|
6334
|
+
const note = document.createElement('div');
|
|
6335
|
+
note.className = 'glance-empty';
|
|
6373
6336
|
// 503 → the Blocker Ledger feature is off (monitoring.blockerLedger.enabled false, the default).
|
|
6374
|
-
|
|
6375
|
-
|
|
6376
|
-
|
|
6377
|
-
|
|
6378
|
-
|
|
6379
|
-
}
|
|
6337
|
+
note.textContent = /503|unavailable|disabled|not initialized/i.test(m)
|
|
6338
|
+
? 'Blocker tracking isn’t turned on for this agent yet.'
|
|
6339
|
+
: 'Could not load blockers right now — refresh to retry.';
|
|
6340
|
+
glanceRoot.appendChild(note);
|
|
6341
|
+
return;
|
|
6380
6342
|
}
|
|
6343
|
+
|
|
6344
|
+
const entries = (resp && Array.isArray(resp.entries)) ? resp.entries : [];
|
|
6345
|
+
const spec = glance.blockersGlanceSpec(document, entries, {});
|
|
6346
|
+
glance.renderGlance(document, glanceRoot, spec);
|
|
6381
6347
|
}
|
|
6382
6348
|
|
|
6383
6349
|
// ── Threadline → Telegram bridge settings ──────────────────
|
|
@@ -522,7 +522,16 @@ export function buildMatrixModel(poolScope, pendingScope, transient = {}) {
|
|
|
522
522
|
for (const m of machineList) {
|
|
523
523
|
const key = `${acct.accountId}::${m.machineId}`;
|
|
524
524
|
const t = transient[key] || null;
|
|
525
|
-
|
|
525
|
+
// OPTIMISTIC CANCEL (issue #1428): a confirmed cancel (2xx) drops a `cancelled`
|
|
526
|
+
// transient so the cell resets AT CLICK TIME instead of showing the stale
|
|
527
|
+
// in-flight flow for one poll cycle (~40s). It SUPPRESSES the cached
|
|
528
|
+
// pending-login (which the next poll hasn't dropped yet), letting the cell fall
|
|
529
|
+
// through to its true underlying state ("Sign in" / "Set up"). The transient is
|
|
530
|
+
// cleared on the very next poll (purgeTransients), so if the cancel actually
|
|
531
|
+
// FAILED server-side the fresh pending-login re-shows the flow — the poll stays
|
|
532
|
+
// the authority.
|
|
533
|
+
const cancelled = t && t.state === 'cancelled';
|
|
534
|
+
const pendingLogin = cancelled ? null : (inProgress.get(key) || null);
|
|
526
535
|
let state;
|
|
527
536
|
if (m.offline) state = 'offline'; // whole column offline (FD6)
|
|
528
537
|
else if (t && t.state === 'held') state = 'held';
|
|
@@ -849,6 +858,11 @@ export function createController(opts) {
|
|
|
849
858
|
if (!entry || typeof entry.at !== 'number') continue;
|
|
850
859
|
if (entry.state === 'just-verified' && t - entry.at > JUST_VERIFIED_TTL_MS) delete state.matrixTransient[key];
|
|
851
860
|
if ((entry.state === 'expired' || entry.state === 'broken') && t - entry.at > EXPIRED_TTL_MS) delete state.matrixTransient[key];
|
|
861
|
+
// The optimistic `cancelled` bridge (issue #1428) lives for exactly one poll:
|
|
862
|
+
// purgeTransients runs inside render() with the FRESH server bodies in hand, so
|
|
863
|
+
// clearing it here hands authority back to the poll — a still-pending login
|
|
864
|
+
// (cancel actually failed) re-derives 'in-progress' on this same render.
|
|
865
|
+
if (entry.state === 'cancelled') delete state.matrixTransient[key];
|
|
852
866
|
}
|
|
853
867
|
state.recentOutcomes = state.recentOutcomes
|
|
854
868
|
.filter((o) => o && typeof o.at === 'number' && now() - o.at <= OUTCOME_TTL_MS)
|
|
@@ -1328,13 +1342,19 @@ export function createController(opts) {
|
|
|
1328
1342
|
try {
|
|
1329
1343
|
const r = await postJson(URLS.cancel, { machineId, id: accountId });
|
|
1330
1344
|
if (r.ok && r.json && (r.json.cancelled || r.json.alreadyTerminal)) {
|
|
1331
|
-
// TERMINAL (cancelled): the episode is over
|
|
1332
|
-
//
|
|
1345
|
+
// TERMINAL (cancelled): the episode is over. OPTIMISTIC RESET (issue #1428):
|
|
1346
|
+
// drop a `cancelled` transient + release the hold, then rebuild the cell NOW
|
|
1347
|
+
// from cache so the operator sees the in-flight flow disappear immediately —
|
|
1348
|
+
// not after the next ~40s poll. The transient suppresses the stale cached
|
|
1349
|
+
// pending-login until the next real poll reconciles (and stays authority if
|
|
1350
|
+
// the cancel actually failed). Keep a status line as the fallback for the
|
|
1351
|
+
// rare case where another open interaction defers the full rebuild.
|
|
1333
1352
|
const key = `${accountId}::${machineId}`;
|
|
1334
|
-
delete state.matrixTransient[key];
|
|
1335
1353
|
delete state.matrixEpisodes[key];
|
|
1354
|
+
state.matrixTransient[key] = { state: 'cancelled', at: now() };
|
|
1336
1355
|
cell.removeAttribute('data-interaction-open');
|
|
1337
1356
|
setCellStatus(cell, 'Cancelled — you can set this up again.');
|
|
1357
|
+
rerenderMatrixFromCache();
|
|
1338
1358
|
} else {
|
|
1339
1359
|
const msg = (r.json && (r.json.error || r.json.reason)) ? (r.json.error || r.json.reason) : `failed (${r.status})`;
|
|
1340
1360
|
setCellStatus(cell, `Couldn’t cancel: ${msg}`);
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./builtin-manifest.schema.json",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
|
-
"generatedAt": "2026-07-11T04:
|
|
5
|
-
"instarVersion": "1.3.
|
|
4
|
+
"generatedAt": "2026-07-11T04:54:52.556Z",
|
|
5
|
+
"instarVersion": "1.3.815",
|
|
6
6
|
"entryCount": 202,
|
|
7
7
|
"entries": {
|
|
8
8
|
"hook:session-start": {
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
Phase 2 of the operator-approved four-phase glance rollout (topic 29836). The two worst
|
|
9
|
+
"wall of raw records" tabs are rebuilt on the shared glance component (F10/F11), and two
|
|
10
|
+
filed issues fold in:
|
|
11
|
+
|
|
12
|
+
- **Commitments — full rebuild + issue #1435.** The headline now has a matching tile for
|
|
13
|
+
every number it states: an **Overdue** tile joins Open / Due soon / Waiting on you /
|
|
14
|
+
Quiet. Grammar is count-aware ("1 needs attention soon", "1 is overdue"). And the
|
|
15
|
+
classification is fixed: a promise whose **hard deadline has already passed is Overdue,
|
|
16
|
+
never "Due soon"** — a stale record a month past its deadline is no longer mislabelled.
|
|
17
|
+
- **Blockers — rebuilt on the template.** The old ~7,000-word raw table that filled the
|
|
18
|
+
whole page is replaced by a one-sentence headline ("N things are truly stuck; K being
|
|
19
|
+
worked") over three tiles — **Truly stuck · Being worked · Resolved**. Tap a tile to see
|
|
20
|
+
which blockers (in plain sentences); tap one for the full record (state, id, timestamps,
|
|
21
|
+
recheck date). Nothing is lost — every column moved down a layer. The "decaying
|
|
22
|
+
hypothesis, not a wall" framing is preserved: a truly-stuck blocker is "the best current
|
|
23
|
+
understanding with a recheck date", never "give up".
|
|
24
|
+
- **Subscriptions — issue #1428.** When you cancel an in-progress sign-in, the cell now
|
|
25
|
+
clears **immediately** instead of showing the stale sign-in flow for ~40 seconds until
|
|
26
|
+
the next refresh. If the cancel actually failed server-side, the next poll still corrects
|
|
27
|
+
it (the poll stays the authority).
|
|
28
|
+
- Both rebuilt tabs left the grandfathered list (the ratchet ceiling dropped 25 → 24), so
|
|
29
|
+
they are now held to the same glance floor as every new view.
|
|
30
|
+
|
|
31
|
+
## What to Tell Your User
|
|
32
|
+
|
|
33
|
+
Two of your busiest dashboard tabs are now readable at a glance. **Commitments** leads with
|
|
34
|
+
a plain sentence about where your promises stand, with a big tile for each state — including
|
|
35
|
+
a new **Overdue** tile — and correctly separates "overdue" from "due soon" (a promise past
|
|
36
|
+
its deadline is now shown as overdue). **Blockers** no longer dumps a giant table on the
|
|
37
|
+
page: you get a one-line summary and a few tiles (Truly stuck / Being worked / Resolved), and
|
|
38
|
+
you tap to drill into the details. And when you cancel a subscription sign-in, the tile
|
|
39
|
+
resets right away instead of lingering for half a minute. Tap any tile to see which items are
|
|
40
|
+
behind that number, and tap one for its full record — nothing was removed, it just moved a
|
|
41
|
+
tap or two down.
|
|
42
|
+
|
|
43
|
+
## Summary of New Capabilities
|
|
44
|
+
|
|
45
|
+
- The **Commitments** dashboard tab now has an Overdue tile, count-aware grammar, and a
|
|
46
|
+
corrected overdue-vs-due-soon classification (issue #1435).
|
|
47
|
+
- The **Blockers** dashboard tab is rebuilt as a glance (headline + Truly stuck / Being
|
|
48
|
+
worked / Resolved tiles → filtered rows → full record), replacing the old raw table.
|
|
49
|
+
- A cancelled subscription sign-in now resets its cell immediately (issue #1428).
|
|
50
|
+
|
|
51
|
+
## Evidence
|
|
52
|
+
|
|
53
|
+
- Unit: `tests/unit/dashboard-glance-word-budget.test.ts` (43), `tests/unit/dashboard-glance-drilldown.test.ts` (13), `tests/unit/subscriptions-render.test.ts`, `tests/unit/follow-me-controller-wiring.test.ts`.
|
|
54
|
+
- Integration: `tests/integration/glance-blockers-tab.test.ts` (real `/blockers` + BlockerLedger), `tests/integration/glance-commitments-tab.test.ts`.
|
|
55
|
+
- E2E: `tests/e2e/glance-blockers-tab-lifecycle.test.ts`, `tests/e2e/glance-commitments-tab-lifecycle.test.ts` (feature-alive: 200 with the feature on, honest empty/503 with it off, no XSS survives).
|
|
56
|
+
- Live browser render (Playwright): both tabs rendered end-to-end against realistic payloads — Commitments "I'm carrying 5 open promises; 1 needs attention soon, 1 is overdue." with the Overdue tile drilling into a past-deadline promise's record; Blockers "1 thing is truly stuck right now; 2 are being worked." drilling into the true-blocker's full record.
|
|
57
|
+
- Spec: `docs/specs/dashboard-ux-standard.md` (F10/F11, conformance table updated — Commitments + Blockers now on the floor).
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Side-Effects Review — Dashboard glance Phase 2: Commitments rebuild + Blockers + #1428
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `glance-p2-commitments-blockers`
|
|
4
|
+
**Date:** `2026-07-10`
|
|
5
|
+
**Author:** `echo (instar-dev agent)`
|
|
6
|
+
**Second-pass reviewer:** `not required` (no block/allow, session-lifecycle, gate/sentinel/watchdog, or compaction surface — a client-side dashboard render change)
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Phase 2 of the operator-approved glance rollout (topic 29836, spec `docs/specs/dashboard-ux-standard.md`, F10/F11). Three view-layer changes, all in `dashboard/*.js` + `dashboard/index.html`, plus tests — **no `src/*.ts` runtime code is touched**:
|
|
11
|
+
|
|
12
|
+
1. **Commitments full rebuild** — `buildCommitmentsGlance`/`commitmentsGlanceSpec` in `dashboard/glance.js` now fold issue #1435: an **Overdue tile** (so every headline number drills down), **count-aware pluralization** ("1 needs" / "2 need", "1 is overdue"), and a **classification fix** — a promise whose HARD deadline is past is **overdue, never "due soon"** (overdue is computed first; due-soon is taken over the remainder, so a stale beacon record a month past its deadline is no longer double-counted). Five tiles now: Open · Due soon · Overdue · Waiting on you · Quiet.
|
|
13
|
+
2. **Blockers rebuild** — the old ~7,000-word raw table (built with `escapeHtml`-into-`innerHTML`) is replaced by the shared glance component. New pure builders `buildBlockersGlance` / `blockerRowText` / `blockerRecordNode` / `blockersGlanceSpec`. Headline ("N things are truly stuck; K being worked") + three tiles (Truly stuck / Being worked / Resolved) that partition the ledger population; each tile drills to plain-sentence rows; each row opens the full record (state, id, origin, timestamps, terminal detail) at Layer 3. `loadBlockers()` in `index.html` rewired onto it.
|
|
14
|
+
3. **Subscriptions optimistic cancel (issue #1428)** — a confirmed cancel (2xx) now drops a short-lived `cancelled` transient that suppresses the still-cached pending-login and rebuilds the cell AT CLICK TIME (no ~40s stale window). `purgeTransients()` clears it on the very next poll, so the poll stays authoritative if the cancel actually failed.
|
|
15
|
+
4. **Conformance ratchet** — `blockers` moved from `GLANCE_GRANDFATHERED` to `GLANCE_ADOPTED_TABS`; `GLANCE_GRANDFATHERED_CEILING` lowered 25 → 24. The ratchet only shrinks.
|
|
16
|
+
|
|
17
|
+
## Decision-point inventory
|
|
18
|
+
|
|
19
|
+
- **Commitments overdue-vs-due-soon classification** (`buildCommitmentsGlance`) — *modify* — overdue now takes precedence; pure derivation from existing server fields, no new authority.
|
|
20
|
+
- **Blockers state → tile bucketing** (`buildBlockersGlance`) — *add* — pure classification of the `/blockers` ledger population into working / stuck / resolved; no new authority, no new endpoint.
|
|
21
|
+
- **Subscriptions cell state derivation** (`buildMatrixModel`) — *modify* — a `cancelled` transient suppresses a stale cached pending-login; a display-only override cleared each poll.
|
|
22
|
+
- No block/allow, message-filter, or dispatch decision point is touched.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 1. Over-block
|
|
27
|
+
|
|
28
|
+
No block/allow surface — over-block not applicable. The classification changes decide which *tile* a record appears under (display grouping), never whether a record is admitted or an action allowed. A commitment/blocker is never dropped: the Commitments population is unchanged (`beaconEnabled && status==='pending'`), and the Blockers tiles partition the full `/blockers` population (a test asserts the tile counts sum to the population length — nothing is filtered out).
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 2. Under-block
|
|
33
|
+
|
|
34
|
+
No block/allow surface — under-block not applicable. Worst case for the #1428 optimistic reset is a *display* lag, not a missed block: if a cancel POST returns 2xx but the server actually failed to cancel, the `cancelled` transient hides the flow for at most one poll cycle, then `purgeTransients` clears it and the fresh pending-login re-renders the in-flight flow (poll is authority). A pasted code during that window still hits the submit route's existing pane-liveness guard (the dangerous half was already closed per #1428) — this change does not touch that guard.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 3. Level-of-abstraction fit
|
|
39
|
+
|
|
40
|
+
Correct layer. All logic is a **stateless client-side renderer** deriving display from data the tab already fetches — the lowest-risk layer for a UX change. It reuses the shared `dashboard/glance.js` component (built in Phase 1) rather than re-implementing per-tab markup, and reuses the shipped `sanitizeForDisplay` + `hasOpenInteraction` primitives. No server route, config, or gate is added or changed; the Blockers glance drills into the existing `GET /blockers`, the Commitments glance into the existing `GET /commitments`.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 4. Signal vs authority compliance
|
|
45
|
+
|
|
46
|
+
**Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
47
|
+
|
|
48
|
+
- [x] No — this change has no block/allow surface.
|
|
49
|
+
|
|
50
|
+
It is pure presentation: it renders records into a headline + tiles + drill-downs. It holds no authority over any action, message, or session. The `cancelled` transient is a display hint that self-clears on the next authoritative poll — it never decides an outcome.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## 5. Interactions
|
|
55
|
+
|
|
56
|
+
- **Shadowing:** The Commitments/Blockers glances replace the tabs' own prior renderers (the old blockers table + its now-removed `blockerStateBadge`/`blockerTerminalLine` helpers were deleted; no other caller referenced them — verified by grep). No server-side check is shadowed.
|
|
57
|
+
- **Double-fire:** None. `loadBlockers`/`loadCommitments` are idempotent renders triggered by tab activation + Refresh; `renderGlance` replaces (never appends) the DOM, so repeated renders can't leak detached nodes/listeners.
|
|
58
|
+
- **Races:** The `cancelled` transient shares `state.matrixTransient` with the existing poll loop. It is set after the cancel POST resolves (server has processed the cancel), read by `buildMatrixModel`, and cleared in `purgeTransients` — which runs inside `render()` only after a *fresh* `/pending-logins` fetch succeeds (a fetch failure early-returns before `render()`), so clearing it always hands authority to real server state. The F9 hold (`data-interaction-open`) is respected: the optimistic rebuild removes the cell's own hold first, and `rerenderMatrixFromCache` still skips while any OTHER interaction is open (the status line is the fallback there, and the next poll catches up).
|
|
59
|
+
- **Feedback loops:** None.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## 6. External surfaces
|
|
64
|
+
|
|
65
|
+
- **Other agents / other users:** None — a client-side render change shipped in the package `dashboard/` directory (served via `express.static`), reaching deployed agents on the normal update path (no `PostUpdateMigrator` entry needed, same as Phase 1).
|
|
66
|
+
- **External systems:** None.
|
|
67
|
+
- **Persistent state:** None — the glance persists nothing; `matrixTransient` is in-memory dashboard state only.
|
|
68
|
+
- **Operator surface (Mobile-Complete):** The Commitments "Mark delivered" action is preserved and phone-completable at Layer 3. The Blockers tab is read-only (as before). The Subscriptions cancel/sign-in flow is unchanged except that a confirmed cancel now resets the cell faster. No new operator action is introduced without a surface.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## 6b. Operator-surface quality (Operator-Surface Quality standard)
|
|
73
|
+
|
|
74
|
+
Touches `dashboard/glance.js`, `dashboard/index.html`, `dashboard/subscriptions.js` — REQUIRED, and it is the whole point of the change.
|
|
75
|
+
|
|
76
|
+
1. **Leads with the primary action?** Yes. Both tabs now open on a one-sentence plain-English headline + big labeled tiles — the answer ("where do my promises / blockers stand?") is the first thing rendered, not a wall of records. The old Blockers page-one was a ~7,000-word raw table; it is gone from Layer 1.
|
|
77
|
+
2. **Zero raw internals as primary content?** Yes, and enforced. The F10 validator scans the concatenated headline + every tile label + value and refuses to build a glance carrying internal IDs, cadences, config keys, or insider terms. Raw internals (id `BLK-004`, `cadence 1800s`, state slugs, recheck timestamps) live only at Layer 3, one or two taps down. Verified live in-browser: the Commitments headline reads "I'm carrying 5 open promises; 1 needs attention soon, 1 is overdue."; the Blockers headline reads "1 thing is truly stuck right now; 2 are being worked."
|
|
78
|
+
3. **Destructive actions de-emphasized?** No destructive action is added. "Mark delivered" (Commitments) is a constructive Layer-3 action; the Blockers tab is read-only. The Subscriptions Cancel affordance is unchanged in prominence.
|
|
79
|
+
4. **Plain language + phone width?** Labels read the way a non-engineer speaks ("Truly stuck", "Being worked", "Due soon", "Overdue"). The glance uses the shared responsive `.glance-*` CSS shipped + browser-verified in Phase 1 (flex tiles, `overflow-x` contained). No new bespoke inline styles. State words at Layer 3 are humanized ("Truly stuck for now (recheck scheduled)", never a raw `true-blocker` slug at the glance). The decaying-hypothesis framing is preserved — a true-blocker is "best current understanding … not 'give up'", never "stop trying".
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## 7. Multi-machine posture (Cross-Machine Coherence)
|
|
84
|
+
|
|
85
|
+
**Machine-local BY DESIGN — pure client-side renderer, no machine-divergent state.** `dashboard/glance.js` persists nothing, reads no config, and holds no server state; it renders whatever data the adopting tab already fetches and inherits that endpoint's posture. The Blockers glance drills into `GET /blockers` (a per-machine ledger read, unchanged by this PR); the Commitments glance into `GET /commitments` (posture unchanged). It emits no user-facing notices (no one-voice concern), holds no durable state (nothing to strand on topic transfer), and generates no URLs. The Subscriptions matrix already reads pool-scope (`?scope=pool`) so a login started on another machine surfaces here; the `cancelled` transient is per-dashboard-session in-memory display state that self-clears on the next poll — it introduces no new cross-machine surface.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## 8. Rollback cost
|
|
90
|
+
|
|
91
|
+
Pure client-side code change — revert the `dashboard/*` files and ship a patch. No persistent state, no data migration, no agent-state repair. During the rollback window a user would see the previous glance/table render; no functional regression (the underlying routes are untouched). The conformance-ratchet constants revert with the file.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Conclusion
|
|
96
|
+
|
|
97
|
+
The review produced no design changes and flags no concerns. The change is confined to the presentation layer, reuses the Phase-1 component and its safety contract (`sanitizeForDisplay` + `textContent`, no `innerHTML`) — which is a security *improvement* for Blockers over the old `escapeHtml`-into-`innerHTML` table — and is fully enforced by the F10 word-budget and F11 drill-down ratchets now covering both rebuilt tabs across all three test tiers, plus a live in-browser render of both tabs. Clear to ship.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Second-pass review (if required)
|
|
102
|
+
|
|
103
|
+
Not required — no block/allow, session-lifecycle, gate/sentinel/watchdog, coherence, or compaction surface is touched. (This is a client-side dashboard render change; the qualifying triggers in `/instar-dev` Phase 5 do not apply.)
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Evidence pointers
|
|
108
|
+
|
|
109
|
+
- Unit: `tests/unit/dashboard-glance-word-budget.test.ts` (43), `tests/unit/dashboard-glance-drilldown.test.ts` (13), `tests/unit/subscriptions-render.test.ts` (+2 for #1428), `tests/unit/follow-me-controller-wiring.test.ts` (+2 for #1428 both sides of the boundary).
|
|
110
|
+
- Integration: `tests/integration/glance-blockers-tab.test.ts` (real `/blockers` route + BlockerLedger), `tests/integration/glance-commitments-tab.test.ts`.
|
|
111
|
+
- E2E: `tests/e2e/glance-blockers-tab-lifecycle.test.ts` (feature ON 200 / dark 503 / shipped-file check), `tests/e2e/glance-commitments-tab-lifecycle.test.ts`.
|
|
112
|
+
- Live browser render (Playwright, stubbed-route harness): Commitments headline "I'm carrying 5 open promises; 1 needs attention soon, 1 is overdue." with an Overdue tile drilling to CMT-101 (past hard deadline, classified overdue not due-soon) → Layer-3 record with cadence 1800s + Mark-delivered; Blockers headline "1 thing is truly stuck right now; 2 are being worked." with Truly-stuck drilling to BLK-004 → Layer-3 record showing "recheck after" and "not 'give up'".
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Class-Closure Declaration (display-only mirror)
|
|
117
|
+
|
|
118
|
+
No agent-authored-artifact defect — not applicable. The #1435 (overdue misclassification / missing tile / grammar) and #1428 (stale cancel window) fixes are defects in runtime client-side dashboard code, not in an LLM prompt, hook, config, skill, or standards text. This change adds no self-triggered controller (the `cancelled` transient is a display hint cleared each poll, not a loop/monitor/sentinel/reaper/scheduler/recovery path that fires a restart/swap/respawn/spawn/notify/retry/kill). The recurrence of an over-budget or jargon-carrying or dead-end glance for these two tabs is now structurally refused by `validateGlanceSpec` + the F10/F11 ratchets (`tests/unit/dashboard-glance-word-budget.test.ts`, `tests/unit/dashboard-glance-drilldown.test.ts`), which render both rebuilt tabs' real builders under adversarial fixtures.
|