instar 1.3.495 → 1.3.496
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/index.html
CHANGED
|
@@ -217,6 +217,20 @@
|
|
|
217
217
|
already names the machine, so a slightly dimmed row is enough hint. */
|
|
218
218
|
.session-item.remote { opacity: 0.9; cursor: pointer; }
|
|
219
219
|
.session-item.remote:hover { opacity: 1; }
|
|
220
|
+
/* WS4.2: explicit per-machine state when a pool machine has no session
|
|
221
|
+
tiles — an idle machine must never look identical to a broken one.
|
|
222
|
+
Plain flex row; inherits the sidebar width (mobile-safe). */
|
|
223
|
+
.machine-status-row {
|
|
224
|
+
display: flex;
|
|
225
|
+
align-items: center;
|
|
226
|
+
gap: 8px;
|
|
227
|
+
padding: 8px 12px;
|
|
228
|
+
font-size: 11px;
|
|
229
|
+
color: var(--text-dim, #8b8fa3);
|
|
230
|
+
border-top: 1px dashed rgba(139, 143, 163, 0.25);
|
|
231
|
+
}
|
|
232
|
+
.machine-status-row.offline .machine-status-text { color: #f87171; }
|
|
233
|
+
.machine-status-row.online .machine-status-text { opacity: 0.85; }
|
|
220
234
|
|
|
221
235
|
.type-badge {
|
|
222
236
|
padding: 1px 6px;
|
|
@@ -3703,6 +3717,10 @@
|
|
|
3703
3717
|
// Kept separate from `sessions` because the WebSocket replaces that array
|
|
3704
3718
|
// with LOCAL sessions on every broadcast — merging would be wiped each tick.
|
|
3705
3719
|
let remoteSessions = [];
|
|
3720
|
+
// WS4.2 (multi-machine-seamlessness spec, F7): pool machine inventory for the
|
|
3721
|
+
// per-machine empty-state strip. Populated only when the pool is enabled with
|
|
3722
|
+
// 2+ machines — single-machine agents never see the strip (strict no-op).
|
|
3723
|
+
let poolMachinesView = [];
|
|
3706
3724
|
let selfMachineNickname = null;
|
|
3707
3725
|
let activeSession = null;
|
|
3708
3726
|
let activeMachineId = null; // non-null when the active session streams from a peer (§2.2)
|
|
@@ -3964,6 +3982,9 @@
|
|
|
3964
3982
|
empty.style.display = 'flex';
|
|
3965
3983
|
// Clear any existing session items
|
|
3966
3984
|
list.querySelectorAll('.session-item').forEach(el => el.remove());
|
|
3985
|
+
// WS4.2: even with zero sessions anywhere, each pool machine still gets
|
|
3986
|
+
// an explicit state row — an idle machine must never look broken.
|
|
3987
|
+
renderMachineStatusStrip(list, all);
|
|
3967
3988
|
return;
|
|
3968
3989
|
}
|
|
3969
3990
|
|
|
@@ -4051,6 +4072,37 @@
|
|
|
4051
4072
|
${telemetryHtml}
|
|
4052
4073
|
`;
|
|
4053
4074
|
}
|
|
4075
|
+
|
|
4076
|
+
// WS4.2: per-machine state strip after the tiles.
|
|
4077
|
+
renderMachineStatusStrip(list, all);
|
|
4078
|
+
}
|
|
4079
|
+
|
|
4080
|
+
// WS4.2 (multi-machine-seamlessness spec, F7 — 2026-06-12 live incident):
|
|
4081
|
+
// a machine with zero running sessions used to render as NOTHING, identical
|
|
4082
|
+
// to a broken or unreachable machine. Render an explicit state row per pool
|
|
4083
|
+
// machine that has no session tiles: "online — no active sessions" when its
|
|
4084
|
+
// heartbeat is live, "not reachable — last seen <t>" when it is not. Pool
|
|
4085
|
+
// disabled or single-machine → poolMachinesView is empty → zero rows, zero
|
|
4086
|
+
// behavior change (single-machine strict no-op, spec invariant 6).
|
|
4087
|
+
function machineStatusInfo(m) {
|
|
4088
|
+
if (m.online) return { cls: 'online', text: 'online — no active sessions' };
|
|
4089
|
+
const seen = m.selfReportedLastSeen ? new Date(m.selfReportedLastSeen).getTime() : null;
|
|
4090
|
+
const rel = seen ? fmtRelTime(seen) : 'unknown';
|
|
4091
|
+
return { cls: 'offline', text: `not reachable — last seen ${rel}` };
|
|
4092
|
+
}
|
|
4093
|
+
function renderMachineStatusStrip(list, allSessions) {
|
|
4094
|
+
list.querySelectorAll('.machine-status-row').forEach(el => el.remove());
|
|
4095
|
+
if (!poolMachinesView.length) return;
|
|
4096
|
+
const busy = new Set(allSessions.map(s => s.machineNickname || s.machineId).filter(Boolean));
|
|
4097
|
+
for (const m of poolMachinesView) {
|
|
4098
|
+
const label = m.nickname || m.machineId || 'unnamed machine';
|
|
4099
|
+
if (busy.has(m.nickname) || busy.has(m.machineId)) continue; // has tiles — badge already names it
|
|
4100
|
+
const info = machineStatusInfo(m);
|
|
4101
|
+
const row = document.createElement('div');
|
|
4102
|
+
row.className = `machine-status-row ${info.cls}`;
|
|
4103
|
+
row.innerHTML = `<span class="machine-badge">${escapeHtml(label)}</span><span class="machine-status-text">${escapeHtml(info.text)}</span>`;
|
|
4104
|
+
list.appendChild(row);
|
|
4105
|
+
}
|
|
4054
4106
|
}
|
|
4055
4107
|
|
|
4056
4108
|
// ── Terminal ──────────────────────────────────────────────
|
|
@@ -4606,6 +4658,15 @@
|
|
|
4606
4658
|
remoteSessions = (j.sessions || []).filter(s => s.remote === true && (s.status === 'running' || s.status === 'starting'));
|
|
4607
4659
|
renderSessionList();
|
|
4608
4660
|
} catch { /* best-effort — peers may be offline; next tick retries */ }
|
|
4661
|
+
// WS4.2: refresh the machine inventory on the same cadence so the sessions
|
|
4662
|
+
// view can say "online — no active sessions" vs "not reachable" per machine.
|
|
4663
|
+
try {
|
|
4664
|
+
const pool = await apiFetch('/pool');
|
|
4665
|
+
poolMachinesView = (pool && pool.enabled && Array.isArray(pool.machines) && pool.machines.length >= 2)
|
|
4666
|
+
? pool.machines
|
|
4667
|
+
: [];
|
|
4668
|
+
renderSessionList();
|
|
4669
|
+
} catch { /* best-effort — strip simply stays absent until the next tick */ }
|
|
4609
4670
|
}
|
|
4610
4671
|
function startPoolSessionsPolling() {
|
|
4611
4672
|
if (poolPollTimer) return;
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./builtin-manifest.schema.json",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
|
-
"generatedAt": "2026-06-12T21:
|
|
5
|
-
"instarVersion": "1.3.
|
|
4
|
+
"generatedAt": "2026-06-12T21:27:13.599Z",
|
|
5
|
+
"instarVersion": "1.3.496",
|
|
6
6
|
"entryCount": 201,
|
|
7
7
|
"entries": {
|
|
8
8
|
"hook:session-start": {
|
|
@@ -9,10 +9,31 @@ The PIN-gated user→agent floor-grant route (`POST /mandate/:id/grants`) finall
|
|
|
9
9
|
|
|
10
10
|
Agent awareness ships both ways: the CLAUDE.md template gains a "User floor-action grants are phone-first" bullet (send the operator the dashboard link, NEVER a terminal command), and a `PostUpdateMigrator` migration inserts the same bullet into existing agents' Coordination Mandate section (anchored insertion, append fallback, idempotent). `CapabilityIndex` entries updated for both routes.
|
|
11
11
|
|
|
12
|
+
The dashboard sessions view rendered NOTHING for a pool machine with zero running
|
|
13
|
+
sessions — visually identical to a machine that is offline or unreachable. Live
|
|
14
|
+
incident 2026-06-12 (topic 13481): minutes after the pool-tile status filter shipped,
|
|
15
|
+
an idle-but-healthy Mac Mini disappeared from the sessions view entirely and the
|
|
16
|
+
operator reasonably read it as a regression.
|
|
17
|
+
|
|
18
|
+
The sessions view now renders an explicit state row for every pool machine that has
|
|
19
|
+
no session tiles: **"online — no active sessions"** when its heartbeat is live, or
|
|
20
|
+
**"not reachable — last seen <t>"** when it is not. Data comes from the existing
|
|
21
|
+
`GET /pool` machine inventory on the existing 15-second pool poll — no new routes, no
|
|
22
|
+
server changes. Machines that have session tiles get no redundant row (their tiles
|
|
23
|
+
already carry the machine badge). Pool disabled or single-machine: the strip is never
|
|
24
|
+
rendered — strict no-op, locked by test. This is WS4.2 of the converged
|
|
25
|
+
multi-machine-seamlessness spec (`docs/specs/MULTI-MACHINE-SEAMLESSNESS-SPEC.md`,
|
|
26
|
+
3-round convergence, 91 findings addressed), whose audit and ELI16 companion ship in
|
|
27
|
+
this same PR.
|
|
28
|
+
|
|
12
29
|
## What to Tell Your User
|
|
13
30
|
|
|
14
31
|
- "You can now grant a teammate a time-boxed floor action (like a 1-hour prod-deploy) straight from the dashboard on your phone — pick the person, the action, and the duration, type your PIN, tap Grant. No terminal, no laptop."
|
|
15
32
|
|
|
33
|
+
- "Your dashboard's session list now tells you the state of every machine, even the
|
|
34
|
+
quiet ones — an idle machine says 'online — no active sessions' instead of showing
|
|
35
|
+
nothing, so you can always tell a resting machine from a broken one."
|
|
36
|
+
|
|
16
37
|
## Summary of New Capabilities
|
|
17
38
|
|
|
18
39
|
| Capability | How to Use |
|
|
@@ -20,6 +41,10 @@ Agent awareness ships both ways: the CLAUDE.md template gains a "User floor-acti
|
|
|
20
41
|
| Phone-first floor-action grants | Dashboard → Mandates tab → any active mandate → "Grant a user a floor action" |
|
|
21
42
|
| Registered-user person picker | Automatic (`GET /permissions/users` feeds it) |
|
|
22
43
|
|
|
44
|
+
| Capability | How to Use |
|
|
45
|
+
|-----------|-----------|
|
|
46
|
+
| Per-machine empty-state in the sessions view | Automatic — open the dashboard sessions list on any multi-machine pool |
|
|
47
|
+
|
|
23
48
|
## Evidence
|
|
24
49
|
|
|
25
50
|
- Unit: `tests/unit/dashboard-mandateGrantForm.test.ts` (12 tests — pick-don't-type rendering, PIN discipline incl. never-retained, expiry clamp, enum-drift pin, XSS, registry-failure degradation) + `tests/unit/PostUpdateMigrator-floorGrantPhoneFirst.test.ts` (4 tests — fresh install, anchored insertion, hand-edited fallback, idempotency).
|
|
@@ -27,3 +52,15 @@ Agent awareness ships both ways: the CLAUDE.md template gains a "User floor-acti
|
|
|
27
52
|
- E2E: `tests/e2e/coordination-mandate-lifecycle.test.ts` (+3 — route alive + Bearer-gated on the production boot path; the form's EXACT payload signs a grant end-to-end; Bearer-without-PIN structurally refused).
|
|
28
53
|
- All 24 pre-existing Mandates-tab tests + the full mandate-area suite (109 tests) green.
|
|
29
54
|
- Side-effects artifact: `upgrades/side-effects/mandate-grant-form.md`.
|
|
55
|
+
|
|
56
|
+
- New test file `tests/unit/dashboard-machineEmptyState.test.ts` (6 tests, at-rest
|
|
57
|
+
HTML/JS assertions following the established dashboard test pattern) — proven
|
|
58
|
+
failing 6/6 against the pre-change file, all pass on the fix. Covers: the
|
|
59
|
+
single-machine no-op gate, both honest state strings, the zero-sessions branch,
|
|
60
|
+
no-duplicate-rows reconciliation, no redundant rows for busy machines, and HTML
|
|
61
|
+
escaping of machine-provided strings.
|
|
62
|
+
- `tests/integration/pool-routes.test.ts` extended to lock the `/pool` contract field
|
|
63
|
+
(`selfReportedLastSeen`) the strip consumes.
|
|
64
|
+
- Side-effects artifact: `upgrades/side-effects/multi-machine-seamlessness-ws42.md`.
|
|
65
|
+
- Spec: `docs/specs/MULTI-MACHINE-SEAMLESSNESS-SPEC.md` (converged + approved) with
|
|
66
|
+
convergence report `docs/specs/reports/multi-machine-seamlessness-convergence.md`.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Side-Effects Review — WS4.2 per-machine empty-state strip (dashboard sessions view)
|
|
2
|
+
|
|
3
|
+
**Spec:** docs/specs/MULTI-MACHINE-SEAMLESSNESS-SPEC.md (converged 2026-06-12, 3 iterations; approved)
|
|
4
|
+
**Change:** dashboard/index.html (+ tests). The sessions view renders an explicit state
|
|
5
|
+
row for every pool machine that has no session tiles: "online — no active sessions"
|
|
6
|
+
when its heartbeat is live, "not reachable — last seen <t>" when not. Data: the
|
|
7
|
+
existing `GET /pool` machines array, fetched on the existing 15s pool poll cadence.
|
|
8
|
+
Closes audit finding F7 (2026-06-12 live incident: idle Mini rendered as nothing and
|
|
9
|
+
read as a regression).
|
|
10
|
+
|
|
11
|
+
## 1. Over-block
|
|
12
|
+
No issue identified — the change blocks nothing. It is a read-only presentation
|
|
13
|
+
addition; no input is rejected anywhere.
|
|
14
|
+
|
|
15
|
+
## 2. Under-block
|
|
16
|
+
No issue identified — there is no blocking surface. Honesty boundary worth naming:
|
|
17
|
+
`online:false` cannot distinguish "deliberately shut down" from "network-unreachable",
|
|
18
|
+
so the row says "not reachable — last seen <t>" (data-honest) rather than claiming to
|
|
19
|
+
know which. The spec's three-state wording maps onto the two states the data supports;
|
|
20
|
+
this is recorded as the deliberate build decision the converged spec's round-3
|
|
21
|
+
adversarial pass classified as adequately constrained.
|
|
22
|
+
|
|
23
|
+
## 3. Level-of-abstraction fit
|
|
24
|
+
Right layer. The dashboard already consumes `/pool` (Machines tab) and
|
|
25
|
+
`/sessions?scope=pool` (tiles); the strip is pure client-side composition of data both
|
|
26
|
+
endpoints already serve. No new route, no server change. A server-side "empty
|
|
27
|
+
machines" field would duplicate client-known state at a worse layer.
|
|
28
|
+
|
|
29
|
+
## 4. Signal vs authority compliance
|
|
30
|
+
Compliant by vacuity: the change introduces NO decision point — it gates no flow,
|
|
31
|
+
filters no message, constrains no behavior. (Phase-1 principle check recorded the
|
|
32
|
+
same conclusion in the build transcript.)
|
|
33
|
+
|
|
34
|
+
## 5. Interactions
|
|
35
|
+
- The strip renders AFTER session tiles and clears its own rows per render — no
|
|
36
|
+
interference with tile add/remove reconciliation (verified by test: no duplicate
|
|
37
|
+
accumulation).
|
|
38
|
+
- The zero-sessions early-return branch now also renders the strip; the existing
|
|
39
|
+
#emptyState banner still shows alongside it (intended: "no sessions" + per-machine
|
|
40
|
+
states are complementary, not contradictory).
|
|
41
|
+
- A machine WITH tiles gets no row (its tiles' machine badge already names it) —
|
|
42
|
+
prevents double-labeling.
|
|
43
|
+
- The extra `GET /pool` per 15s poll tick matches the Machines tab's existing call
|
|
44
|
+
pattern and is auth'd identically (apiFetch). Failure is swallowed best-effort: the
|
|
45
|
+
strip simply stays absent until the next tick — degraded view, never an error loop.
|
|
46
|
+
|
|
47
|
+
## 6. External surfaces
|
|
48
|
+
None beyond the operator's own PIN-gated dashboard. No new data is exposed: nickname,
|
|
49
|
+
online, lastSeen are already rendered on the Machines tab. Machine-provided strings
|
|
50
|
+
are escaped before DOM injection (tested). Single-machine agents: `poolMachinesView`
|
|
51
|
+
is populated only when the pool is enabled with 2+ machines — strict no-op, tested.
|
|
52
|
+
|
|
53
|
+
## 7. Rollback cost
|
|
54
|
+
Trivial: revert the dashboard/index.html hunk (static asset; no data, no migration,
|
|
55
|
+
no state). Ships with the next release; rollback is a one-commit revert and re-release.
|
|
56
|
+
|
|
57
|
+
## Second-pass review
|
|
58
|
+
Not required — no block/allow decisions, no session lifecycle, no
|
|
59
|
+
gate/sentinel/watchdog surface. (Phase-5 trigger list consulted; none match.)
|