From 5f3e192f35bef54fd195680ca94187e03c511040 Mon Sep 17 00:00:00 2001 From: Hugh Francis Date: Sun, 15 Nov 2026 01:29:05 +0000 Subject: [PATCH 01/10] kit-206: spec Co-Authored-By: Claude Opus 5.5 --- .../kit-206-viewer-reload-hint/spec.md | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 docs/devteam-dev/tickets/kit-206-viewer-reload-hint/spec.md diff --git a/docs/devteam-dev/tickets/kit-206-viewer-reload-hint/spec.md b/docs/devteam-dev/tickets/kit-206-viewer-reload-hint/spec.md new file mode 100644 index 00000000..9f15fc9c --- /dev/null +++ b/docs/devteam-dev/tickets/kit-206-viewer-reload-hint/spec.md @@ -0,0 +1,57 @@ +# kit-206-viewer-reload-hint: spec + +The artifact viewer's reload button shows an orange outline when the artifact changed since it was loaded. + +## Why + +The owner's ask (supervisor's terminal, 2026-11-15 01:25): until drafts reload live (fast-threads kit-106), the reload button should say when a reload is needed. The owner iterates on a draft by looking at it; today nothing tells them the draft on screen is behind the one on disk. When kit-106 lands, its live reload supersedes the outline (the supervisor told fast-threads so in m-20261115-012648683-supervisor). + +## How it works today + +- The stage (`#artifact-view`) shows an artifact in `#av-frame` through `showArtifact(slug, at, { fp, mainFp })`, which records the fingerprint of what it shows in `TS.fp` (`index.html` ~3559). `#av-reload` reloads the frame by clearing and resetting its `src` (~3669). +- Two hosts: + - **The dashboard** (`#artifact/main/`, `#artifact/wt//`): `openArtifact` takes `fp` from `DATA.library.artifacts` (the `/api/data` poll, every 10 s, `load()`). Nothing updates `TS.fp` afterwards. + - **A viewer tab** (`/view/artifact/?at=…`): `viewerArtifact` takes `fp` from `/api/view` (which runs `artifact describe --json`), once. The tab polls `/api/threads` every 5 s and `/api/hello` every 30 s, never `/api/data`. +- The Library's draft rows (`draftArtifacts`) list a draft only when its fingerprint differs from main's. +- `--amber` is the warning colour token, defined for light and for dark (`prefers-color-scheme: dark`). + +## The change + +1. **What the frame loaded.** `TS.fp` stays the fingerprint the open frame loaded. A new `TS.fpNow` holds the latest fingerprint seen for the same `slug`/`at`. +2. **Checking, on the normal poll.** + - Dashboard: after each `/api/data` read is drawn, if the stage shows an artifact at `main` or a draft, `fpNow` is the fp of the Library row with that slug and `at`. + - Viewer tab: every 10 s while the tab is visible, if the stage shows an artifact at `main` or a draft, it asks `/api/view` (the same ask it boots with) and takes its `fp`. A 404, an error or no answer leaves `fpNow` as it was. + - A `tree:` version never changes: never checked. A preview: never checked. +3. **The hint.** When `TS.fp` and `TS.fpNow` are both known and differ, `#av-reload` gets the class `stale`: a 2 px `var(--amber)` outline (light and dark by the token), and its title becomes "A newer version is ready: reload to see it". Otherwise the class is off and the title is "Load the page again". +4. **Reload.** Pressing `#av-reload` on an artifact reloads the frame as today, then sets `TS.fp = TS.fpNow` (and `TS.mainFp` too when `at` is main) and clears the hint. Nothing reloads by itself. +5. **Switching.** Opening another artifact, version or a preview resets `fpNow` (`tsSet`), so the hint goes. + +## Acceptance checks (the done-whens) + +| # | Done-when | Check | +|---|---|---| +| 1 | While the viewer is open, the dashboard checks the artifact's fingerprint (as /api/view or describe reports it, for main or the draft shown) on its normal poll; on a difference from what the frame loaded, reload gets an orange outline (`--amber`, light and dark) and a title saying a newer version is ready | browser test: viewer tab and dashboard stage on a draft, file changed, `#av-reload` has `stale`, its computed `outline-color` is the `--amber` value, its title says a newer version is ready; before the change it has none | +| 2 | Pressing reload loads the new version and clears the outline; nothing reloads by itself | browser test: before the press the frame's lens still reports the old title; after it, the new one, and the class is off | +| 3 | A dashboard test: open a draft, change its file, outline within one poll; press it, outline goes, frame shows the change | the test in `test/dashboard.test.mjs`, waiting at most one poll (10 s) plus the read's time | +| 4 | test.yml passes on every job; no file is added | CI at the head; `git diff --name-only --diff-filter=A origin/main` lists only this ticket's records | + +"The frame shows the change": the frame is opaque-origin, so the test reads what the artifact's lens tells the stage (`ready` carries the page's `document.title`); the draft's change is to its title. + +## Contracts + +None provided or changed. Consumed: `/api/view` (artifact kind) and `/api/data`'s `library.artifacts[].fp`, unchanged. + +## Edge cases + +- A draft whose fingerprint equals main's is absent from the Library rows: the dashboard has no `fpNow` for it and shows no hint (the viewer tab, which asks `/api/view`, does). A draft opened from the Library is listed, so the usual case works. +- A draft removed while open: the dashboard finds no row, the viewer tab gets a 404; neither changes the hint. +- The fingerprint taken at open can be a poll older than the file the frame fetched; the hint then shows once though nothing is newer, and one reload clears it. Accepted. +- `/api/view`'s comment in `devteam.mjs` says the ask is "never polled". The viewer tab now asks it every 10 s while visible (one `artifact describe` child per ask, shared by concurrent asks). That file is outside touches; the comment goes stale until kit-106's `/api/artifact-fp` replaces this poll. Told to the supervisor in the review request. + +## Out of scope + +Live reload (kit-106), keeping the scroll position across reload, a cheaper fingerprint endpoint, any change to `devteam.mjs`. + +## Rulings + +None yet. From 9d399cedb60788ceae2ec9540221f4395f3830c1 Mon Sep 17 00:00:00 2001 From: Hugh Francis Date: Sun, 15 Nov 2026 01:34:00 +0000 Subject: [PATCH 02/10] kit-206: spec review 1 (changes), fixes Co-Authored-By: Claude Opus 5.5 --- .../reviews/spec-1.md | 39 +++++++++++++++++++ .../kit-206-viewer-reload-hint/spec.md | 23 +++++------ 2 files changed, 51 insertions(+), 11 deletions(-) create mode 100644 docs/devteam-dev/tickets/kit-206-viewer-reload-hint/reviews/spec-1.md diff --git a/docs/devteam-dev/tickets/kit-206-viewer-reload-hint/reviews/spec-1.md b/docs/devteam-dev/tickets/kit-206-viewer-reload-hint/reviews/spec-1.md new file mode 100644 index 00000000..6bc7fee0 --- /dev/null +++ b/docs/devteam-dev/tickets/kit-206-viewer-reload-hint/reviews/spec-1.md @@ -0,0 +1,39 @@ +# Spec review, pass 1 + +- Reviewer: spec-reviewer (fresh subagent, claude-opus) +- Reviewed: 5f3e192f35bef54fd195680ca94187e03c511040 (spec.md) + +Sources read: the spec, `ticket show kit-206-viewer-reload-hint` (four done-whens; touches are `index.html` and `test/dashboard.test.mjs`), and quality.md §3. In `index.html`: the tokens (~30-45), the `#av-reload` markup (~779) and handler (~3669), `TS`/`tsSet`/`showArtifact` (~3543-3575), `openArtifact` (~3638), `tArtFpNow` (~4622) and its "changed since" use (~4699), `tsSubject` (~5090), the frame `load` and lens `ready` handlers (~5171, ~5191), `holding`/`drawOrHold`/`load`/`loadNow` (~5220-5320), and `viewerArtifact`/`viewerBoot` (~5644-5695). In `devteam.mjs`: `/api/data` (~14377), `/api/view` (~14415), `describing` (~13903), `artifact describe` (~7533), `artifactSource`/`artifactAt`, `diskOid`/`fingerprint` (~7184-7196), `describeArtifact`, `draftArtifacts`, the `/artifact/` response headers (`no-store`, ~14580), and ARTIFACT_LENS `hello`→`ready` (~6396). Also the viewer tests in test/dashboard.test.mjs (~1834-2020). + +Every done-when is restated faithfully, and the commit adds only the spec. The approach holds up against the code: +- **`TS.fp` is what the frame loaded.** Nothing updates it after open on either host. `tsSubject` stamps new threads with it, and `tArtFpNow` compares a thread's fp with it, so keeping it fixed until reload is right. Moving it on reload makes older threads read "changed since", which is correct. +- **The dashboard has the draft's fp on every poll.** `/api/data` runs `dashboard-data` fresh on each read, and `draftArtifacts` hashes the working tree from disk (oid cache keyed by path, size and mtime), so an uncommitted edit moves the fp. +- **Reload fetches the new bytes.** `/artifact/` is served `no-store`. +- **Trees, previews and the framed wrapper need no polling.** The wrapper's gateway grants only published trees, so "never check a tree" means the wrapper never polls. Previews are reset by `tsSet('preview')`. +- **Polling `/api/view` every 10 s is acceptable as stated.** It runs only in visible tabs, the server shares concurrent asks per slug|at, and kit-106 supersedes it. The stale comment in `devteam.mjs` is named and handed to the supervisor. + +Values and refusals: +- P1 (harness): not involved. Passes. +- P2 (nothing project-specific): nothing project-specific. Passes. +- P3 (decide and report): the open-time race is decided and accepted. Passes. +- P4 (owner-only asks): none needed. Passes. +- P5 (authority): not touched. N/A. +- P6 (keep it simple): the design is the simple one. Passes. +- R1 (no self-widening of authority): no authority change. Passes. +- R2 (plain owner-facing text): the new title is plain words. Passes. + +The blocking defect is F1: reload copies `fpNow` into `TS.fp` without checking that `fpNow` is known, so a press before the first check erases `TS.fp`; the stage then never shows the hint again, and threads drawn on it lose their fingerprint. + +| Id | Severity | Rule | Where | Finding | Disposition | +|---|---|---|---|---|---| +| F1 | Important | done-when 1 and 2; spec "The change" 1, 4 and 5 | spec "The change" 4; code `tsSet` ~3554, `tsSubject` ~5090, `tArtFpNow` ~4622 | Reload "sets `TS.fp = TS.fpNow`", but `fpNow` starts null (`tsSet` resets it, nothing sets it to `fp` at open) and stays null until the first check (up to 10 s), and after a 404 or error. A reload in that window, likely right after the owner opens a draft they just edited, sets `TS.fp = null`: the hint never shows for this open, `tsSubject` stops putting `fp` on new threads, and `tArtFpNow` returns null. Set `fpNow = fp` at open, or have reload copy only a known `fpNow`. Add a check: reload before the first poll, then change the file, and the hint still comes. | fixed: `fpNow`/`mainFpNow` start equal to `fp`/`mainFp` at open, reload copies only a known one, and the check is added | +| F2 | Minor | spec "The change" 2, viewer tab ("the same ask it boots with") | `viewerArtifact` ~5655-5663 | Boot asks `VIEWER.at` first and, after an unframed 404, shows main (`TS.at = 'main'`). "The same ask" read as `VIEWER.at` 404s every poll and never checks the main page shown. Say the poll asks `TS.slug` and `TS.at`, and drops a reply for a stage it no longer shows. | fixed: the poll asks `TS.slug`/`TS.at`; a reply for another stage is dropped | +| F3 | Minor | done-when 1 ("in light and dark"); quality.md §3 | Acceptance check 1 | The check reads `outline-color` in one scheme only, and `getComputedStyle` gives `rgb(...)` while the token is hex. Compare against a probe element's computed `color: var(--amber)`, and repeat under `Emulation.setEmulatedMedia` with `prefers-color-scheme: dark`. | fixed: probe element comparison, in light and under emulated dark | +| F4 | Minor | done-when 3 ("the frame shows the change") | Acceptance checks 2 and 3; lens `hello` ~6396, stage `ready` handler ~5191 | The lens reports `document.title` (the page's ``), not `artifact.json`'s title; and the stage keeps nothing of `ready.title` for an artifact, so the test needs its own `message` listener. Say both, so no production `TS.title` is added just for the test. | fixed: the draft's `index.html` `<title>`; the test listens for `ready` itself | +| F5 | Minor | done-when 3 ("within one poll"); quality.md §3 (a test that can flake) | Acceptance check 3 | A `dashboard-data` run in flight when the file changes returns the old fp, and `load()` skips a tick while a read is in flight, so the hint can take longer than "10 s plus the read's time". Give the wait a margin (two poll periods plus the read timeout) and say the done-when is met by the first read that starts after the change. | fixed: wait up to 2 × 10 s + 12 s; met by the first read after the change | +| F6 | Minor | spec "The change" 4; Edge cases | spec "The change" 4 | The open-time race also happens at reload: the frame can fetch a file newer than the last `fpNow`, so new threads carry an older fp and the next poll shows a spurious hint. Accept it in Edge cases, or re-check right after a reload. | accepted in Edge cases, beside the open-time race | +| F7 | Minor | spec "The change" 2, viewer tab | `viewerBoot` ~5694; `/api/view` describe timeout 12 s | An ask can take up to 12 s, longer than the 10 s period, so two asks can overlap and an older reply land last. One ask in flight per tab. | fixed: one ask in flight per tab, a tick skipped while one is pending | +| F8 | Suggestion | existing test "it polls the threads and hello" (~1880); `viewerBoot` comment (~5666) | `viewerBoot` | The comment "Nothing else: a dozen viewer tabs cost a dozen cheap reads" becomes false and must be rewritten; and the existing test forbids `route(`, `render(`, `load(`, `loadNow(`, `loadSchemaDocs(` in `viewerBoot`'s text, so the new poll must not reuse those names. | fixed: both are in the spec | +| F9 | Suggestion | none (taste) | spec "The change" 2 and 4 | Both checks also get main's fp; updating `TS.mainFp` on reload would keep a tree thread's "changed since" right on a draft stage. The viewer bar's title also stays the boot-time one after a reload. | taken in part: `mainFp` follows on reload; the bar title refresh is out of scope | + +Verdict: CHANGES 5f3e192f35bef54fd195680ca94187e03c511040 diff --git a/docs/devteam-dev/tickets/kit-206-viewer-reload-hint/spec.md b/docs/devteam-dev/tickets/kit-206-viewer-reload-hint/spec.md index 9f15fc9c..a40be559 100644 --- a/docs/devteam-dev/tickets/kit-206-viewer-reload-hint/spec.md +++ b/docs/devteam-dev/tickets/kit-206-viewer-reload-hint/spec.md @@ -17,25 +17,26 @@ The owner's ask (supervisor's terminal, 2026-11-15 01:25): until drafts reload l ## The change -1. **What the frame loaded.** `TS.fp` stays the fingerprint the open frame loaded. A new `TS.fpNow` holds the latest fingerprint seen for the same `slug`/`at`. +1. **What the frame loaded.** `TS.fp` (and `TS.mainFp`) stay the fingerprints of what the open frame loaded. New `TS.fpNow` and `TS.mainFpNow` hold the latest seen for the stage's `slug`/`at`; `showArtifact` starts them equal to `fp`/`mainFp` when it sets the stage up (`tsSet`), so they are never behind what was loaded (spec-1 F1). 2. **Checking, on the normal poll.** - - Dashboard: after each `/api/data` read is drawn, if the stage shows an artifact at `main` or a draft, `fpNow` is the fp of the Library row with that slug and `at`. - - Viewer tab: every 10 s while the tab is visible, if the stage shows an artifact at `main` or a draft, it asks `/api/view` (the same ask it boots with) and takes its `fp`. A 404, an error or no answer leaves `fpNow` as it was. - - A `tree:` version never changes: never checked. A preview: never checked. + - Dashboard: after each `/api/data` read is drawn, if the stage shows an artifact at `main` or a draft, `fpNow` is the fp of the Library row with that slug and `at`, and `mainFpNow` the fp of its main row. A row that is absent changes nothing. + - Viewer tab: every 10 s while the tab is visible, if the stage shows an artifact at `main` or a draft, it asks `/api/view` for `TS.slug` and `TS.at` (what the stage shows, which after a fallback is main, not the address's `at`) and takes `fp` and `main_fp`. One ask in flight per tab: a tick while one is pending is skipped. A reply for a stage no longer shown (another slug or `at`) is dropped. A 404, an error or no answer changes nothing (spec-1 F2, F7). + - A `tree:` version never changes: never checked (so the owner wrapper, which shows only trees, never polls). A preview: never checked. + - `viewerBoot`'s comment ("Nothing else: a dozen viewer tabs cost a dozen cheap reads") is rewritten to say a tab showing main or a draft also asks `/api/view` every 10 s while seen: one `artifact describe` per ask. The new poll's names avoid `route`, `render`, `load`, `loadNow` and `loadSchemaDocs`, which the existing viewer test forbids in `viewerBoot` (spec-1 F8). 3. **The hint.** When `TS.fp` and `TS.fpNow` are both known and differ, `#av-reload` gets the class `stale`: a 2 px `var(--amber)` outline (light and dark by the token), and its title becomes "A newer version is ready: reload to see it". Otherwise the class is off and the title is "Load the page again". -4. **Reload.** Pressing `#av-reload` on an artifact reloads the frame as today, then sets `TS.fp = TS.fpNow` (and `TS.mainFp` too when `at` is main) and clears the hint. Nothing reloads by itself. -5. **Switching.** Opening another artifact, version or a preview resets `fpNow` (`tsSet`), so the hint goes. +4. **Reload.** Pressing `#av-reload` on an artifact reloads the frame as today, then sets `TS.fp = TS.fpNow` and `TS.mainFp = TS.mainFpNow`, each only when the newer one is known, and clears the hint. Nothing reloads by itself. +5. **Switching.** Opening another artifact, version or a preview resets both (`tsSet`), so the hint goes. ## Acceptance checks (the done-whens) | # | Done-when | Check | |---|---|---| -| 1 | While the viewer is open, the dashboard checks the artifact's fingerprint (as /api/view or describe reports it, for main or the draft shown) on its normal poll; on a difference from what the frame loaded, reload gets an orange outline (`--amber`, light and dark) and a title saying a newer version is ready | browser test: viewer tab and dashboard stage on a draft, file changed, `#av-reload` has `stale`, its computed `outline-color` is the `--amber` value, its title says a newer version is ready; before the change it has none | +| 1 | While the viewer is open, the dashboard checks the artifact's fingerprint (as /api/view or describe reports it, for main or the draft shown) on its normal poll; on a difference from what the frame loaded, reload gets an orange outline (`--amber`, light and dark) and a title saying a newer version is ready | browser test: viewer tab and dashboard stage on a draft, file changed, `#av-reload` has `stale`, its title says a newer version is ready, and its computed `outline-color` equals the computed `color` of a probe element styled `color: var(--amber)`, checked in light and again under `Emulation.setEmulatedMedia` with `prefers-color-scheme: dark`; before the change it has no `stale` (spec-1 F3) | | 2 | Pressing reload loads the new version and clears the outline; nothing reloads by itself | browser test: before the press the frame's lens still reports the old title; after it, the new one, and the class is off | -| 3 | A dashboard test: open a draft, change its file, outline within one poll; press it, outline goes, frame shows the change | the test in `test/dashboard.test.mjs`, waiting at most one poll (10 s) plus the read's time | +| 3 | A dashboard test: open a draft, change its file, outline within one poll; press it, outline goes, frame shows the change | the test in `test/dashboard.test.mjs`. "Within one poll" is met by the first read that starts after the change; a read already in flight may carry the old fp, so the test waits at most two poll periods plus the read's timeout (2 × 10 s + 12 s) (spec-1 F5). It also presses reload before the first check, then changes the file, and the hint still comes (spec-1 F1) | | 4 | test.yml passes on every job; no file is added | CI at the head; `git diff --name-only --diff-filter=A origin/main` lists only this ticket's records | -"The frame shows the change": the frame is opaque-origin, so the test reads what the artifact's lens tells the stage (`ready` carries the page's `document.title`); the draft's change is to its title. +"The frame shows the change": the frame is opaque-origin, so the test reads what the artifact's lens tells the stage. Its `ready` message carries the page's `document.title`, the `<title>` in the draft's `index.html` (not `artifact.json`'s title), and the stage keeps none of it for an artifact, so the test adds its own `message` listener on the stage page and waits for a `ready` with the new `<title>` after the press. No production state is added for the test (spec-1 F4). ## Contracts @@ -45,12 +46,12 @@ None provided or changed. Consumed: `/api/view` (artifact kind) and `/api/data`' - A draft whose fingerprint equals main's is absent from the Library rows: the dashboard has no `fpNow` for it and shows no hint (the viewer tab, which asks `/api/view`, does). A draft opened from the Library is listed, so the usual case works. - A draft removed while open: the dashboard finds no row, the viewer tab gets a 404; neither changes the hint. -- The fingerprint taken at open can be a poll older than the file the frame fetched; the hint then shows once though nothing is newer, and one reload clears it. Accepted. +- The fingerprint taken at open can be a poll older than the file the frame fetched; the hint then shows once though nothing is newer, and one reload clears it. The same holds at reload: a file changed after the last check and before the press is on screen while `TS.fp` names the version before it, so threads drawn then carry that older fp and the next check shows the hint once more. Accepted for both, until kit-106 (spec-1 F6). - `/api/view`'s comment in `devteam.mjs` says the ask is "never polled". The viewer tab now asks it every 10 s while visible (one `artifact describe` child per ask, shared by concurrent asks). That file is outside touches; the comment goes stale until kit-106's `/api/artifact-fp` replaces this poll. Told to the supervisor in the review request. ## Out of scope -Live reload (kit-106), keeping the scroll position across reload, a cheaper fingerprint endpoint, any change to `devteam.mjs`. +Live reload (kit-106), refreshing the viewer bar's title after a reload, keeping the scroll position across reload, a cheaper fingerprint endpoint, any change to `devteam.mjs`. ## Rulings From 34ee7d3a4a92f707d3ba4ae86c8a869537d6bd50 Mon Sep 17 00:00:00 2001 From: Hugh Francis <hugh@sanctuary.computer> Date: Sun, 15 Nov 2026 01:36:52 +0000 Subject: [PATCH 03/10] kit-206: spec review 2 (approve 9d399ce), plan Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../kit-206-viewer-reload-hint/plan.md | 87 +++++++++++++++++++ .../reviews/spec-2.md | 31 +++++++ 2 files changed, 118 insertions(+) create mode 100644 docs/devteam-dev/tickets/kit-206-viewer-reload-hint/plan.md create mode 100644 docs/devteam-dev/tickets/kit-206-viewer-reload-hint/reviews/spec-2.md diff --git a/docs/devteam-dev/tickets/kit-206-viewer-reload-hint/plan.md b/docs/devteam-dev/tickets/kit-206-viewer-reload-hint/plan.md new file mode 100644 index 00000000..8aeea0ad --- /dev/null +++ b/docs/devteam-dev/tickets/kit-206-viewer-reload-hint/plan.md @@ -0,0 +1,87 @@ +# kit-206 viewer reload hint: implementation plan + +> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:executing-plans (inline, with test-driven-development). Steps use checkbox (`- [ ]`) syntax. + +## TL;DR + +- `#av-reload` gets the class `stale` (2 px `var(--amber)` outline, title "A newer version is ready: reload to see it") when the stage's artifact has a newer fingerprint than the one its frame loaded. +- `TS` gains `fpNow` and `mainFpNow`, started equal to `fp`/`mainFp` in `showArtifact`; `tsSet` resets them. +- Dashboard: after each `/api/data` read (`loadNow`, the raw `d`, not the possibly held `DATA`), the Library rows update them. +- Viewer tab: every 10 s while visible, one `/api/view` ask in flight for `TS.slug`/`TS.at` (main or a draft only). +- Reload copies the known newer fingerprints into `fp`/`mainFp` and clears the hint; nothing reloads by itself. +- One new browser test in `test/dashboard.test.mjs`, two subtests (dashboard stage, viewer tab), light and dark. +- Two tasks; only `index.html` and `test/dashboard.test.mjs` change. + +**Goal:** the reload button says when the artifact on the stage is behind the one on disk. + +**Architecture:** one hint function (`avHint()`) reads `TS` and sets the button; two feeders (the dashboard's data read, the viewer tab's poll) update `TS.fpNow`/`TS.mainFpNow` and call it; the reload handler moves the loaded fingerprints forward and calls it. + +**Tech stack:** the dashboard's single-file vanilla JS; `node:test` with the kit's CDP Chrome driver (`test/lib/chrome.mjs`). + +**Spec:** `docs/devteam-dev/tickets/kit-206-viewer-reload-hint/spec.md` (read it; this plan argues from it). + +## Global constraints + +- Files: `docs/devteam/dashboard/index.html`, `test/dashboard.test.mjs`, and this ticket's folder. No file added outside the ticket folder. +- Colour: `var(--amber)`, the warning token (light `#f5a623`, dark `#ffb224`). No new token. +- Titles, exact: stale "A newer version is ready: reload to see it"; otherwise "Load the page again" (today's). +- Viewer poll: 10 s, only while `document.visibilityState === 'visible'`, only `TS.kind === 'artifact'` with `TS.at` not `tree:`, one ask in flight. Its code in `viewerBoot` uses none of the names `route(`, `render(`, `load(`, `loadNow(`, `loadSchemaDocs(`. +- Never reload the frame except on the button's press. +- The new test never uses `POLL_FAST`: it waits on the real 10 s polls (spec-2 N1). Waits: dashboard up to 45 000 ms (2 × 10 s + `DATA_WAIT_MS` 20 s + 5 s), viewer tab up to 37 000 ms (2 × 10 s + describe's 12 s + 5 s); met by the first `dashboard-data` run, or `/api/view` ask, that starts after the change. +- Browser tests run here with Node 22: `/usr/local/bin/node` (the default Node 18 has no WebSocket and skips them). + +## Review focus + +1. A reload before any check (fpNow still the open-time fp): the hint still comes on a later change. Pinned in Task 1's test. +2. A held dashboard draw (the owner typing): the check must use the read, not `DATA`. Covered by using `d` in `loadNow`; reviewer reads it. +3. A switch of stage (another artifact, a preview, close) while a viewer ask is in flight: its reply is dropped. Pinned by the slug/at guard; reviewer reads it. +4. A tree stage and a preview stage never poll and never show the hint. Pinned in Task 2's test (the existing "nothing else" asks list stays clean for a tree). +5. Dark mode: outline colour is the dark token. Pinned in Task 1's test. + +--- + +### Task 1: the hint, the reload, and the dashboard's check + +**Files:** +- Modify: `docs/devteam/dashboard/index.html`: CSS near `.av-bar` (~516-535); `TS` (~3543) and `tsSet` (~3554); `showArtifact` (~3569-3570); `openArtifact` (~3647); the `#av-reload` handler (~3669); `loadNow` after `drawOrHold(d)` (~5325). +- Test: `test/dashboard.test.mjs`, a new top-level test after the viewer test (~2021). + +**Interfaces (produced):** +- `TS.fpNow`, `TS.mainFpNow`: string | null. +- `avHint(): void`: sets `#av-reload`'s `stale` class and title from `TS` (stale iff `TS.kind === 'artifact' && TS.fp && TS.fpNow && TS.fp !== TS.fpNow`). +- `avFpSeen(fp: string|null, mainFp: string|null, slug: string, at: string): void`: if the stage shows that `slug`/`at` (and `at` isn't `tree:`), sets each given non-null value on `TS.fpNow`/`TS.mainFpNow`, then `avHint()`. + +- [ ] **Step 1: write the failing test** `the viewer's reload button says when a newer version is ready, and the press loads it`, `{ skip: canDrive ? false : 'needs Chrome and a Node with WebSocket', timeout: 180000 }`, using `threadsFixture('reload-hint')`, `landArtifact(F, 'hint-mock', mock('main'))` and the draft written at `F.old/.worktrees/demo-01-draft/docs/devteam/project/artifacts/hint-mock` as `mock('v1')`, where `mock(t)` is `{ 'artifact.json': …title 'Hint mock'…, 'index.html': `<!doctype html><title>${t}

${t}

` }`. A page with `COUNT_FETCH`, and on each new document a listener that pushes `e.data.title` of `{ lens: 1, type: 'ready' }` messages from `#av-frame` into `window.__titles`. Helper `amber()` = computed `color` of a probe `` appended to `#artifact-view`. Subtest **dashboard**: + - open `${B}/#artifact/wt/demo-01-draft/hint-mock`; wait for `__titles` to include `'v1'`; assert `#av-reload` has no `stale` and its title is `Load the page again`. + - press `#av-reload` (before any change); wait for a second `'v1'`. + - write `index.html` as `mock('v2')`; wait up to 45 000 ms for `stale`; assert title `A newer version is ready: reload to see it`, `getComputedStyle(#av-reload).outlineColor === amber()`, outline width `2px`, and `__titles` has no `'v2'` (nothing reloaded by itself). + - `Emulation.setEmulatedMedia` `{ features: [{ name: 'prefers-color-scheme', value: 'dark' }] }`: outline colour still equals `amber()`, and `amber()` differs from its light value; set it back to light. + - press `#av-reload`: wait for `'v2'` in `__titles`; assert no `stale`, title `Load the page again`, and `TS.fp === TS.fpNow`. +- [ ] **Step 2: run it, see it fail** — `/usr/local/bin/node --test --test-name-pattern "reload button says" test/dashboard.test.mjs`; expected: times out waiting for `stale`. +- [ ] **Step 3: implement.** CSS `#av-reload.stale { outline: 2px solid var(--amber); outline-offset: 1px; }`. Add `fpNow: null, mainFpNow: null` to `TS` and to `tsSet`'s reset. In `showArtifact`, pass `fpNow: fp, mainFpNow: mainFp` to `tsSet` (new stage), and on the same stage leave `fpNow`/`mainFpNow` as they are; call `avHint()` at its end. `avHint()` and `avFpSeen()` beside `showArtifact`. The reload handler: after resetting `src`, if `TS.kind === 'artifact'` copy `fpNow`→`fp` and `mainFpNow`→`mainFp` where non-null, then `avHint()`. `tsSet` also calls `avHint()` so a preview or close clears it. In `openArtifact`, a draft whose own Library row is absent while main's is present opens with main's fp as its `fp` (absence means equal to main; spec-2 N2). In `loadNow` after `drawOrHold(d)` (the raw `d`, so a held draw still feeds the check; spec-2 N4): if `TS.kind === 'artifact'`, `avFpSeen(row(TS.at)?.fp ?? null, row('main')?.fp ?? null, TS.slug, TS.at)` over `d.library?.artifacts`. Comments in the file's voice. +- [ ] **Step 4: run it, see it pass**, then the whole file: `/usr/local/bin/node --test test/dashboard.test.mjs` through `devteam.mjs gate test`. +- [ ] **Step 5: commit** `kit-206: the reload button's hint, and the dashboard's check`. + +### Task 2: the viewer tab's poll + +**Files:** +- Modify: `docs/devteam/dashboard/index.html`: `viewerBoot` (~5664-5695) and its comment. +- Test: `test/dashboard.test.mjs`, a second subtest in Task 1's test. + +**Interfaces (consumed):** `avFpSeen`, `TS.fpNow`, from Task 1. + +- [ ] **Step 1: write the failing subtest** **viewer tab**: write the draft back to `mock('v1')`; go to `${B}/view/artifact/hint-mock?at=demo-01-draft`; wait for `'v1'` in a fresh `__titles`; no `stale`. Write `mock('v3')`; wait up to 37 000 ms for `stale` and the stale title; `__titles` has no `'v3'`. Press: `'v3'` arrives, `stale` off. Then go to `${B}/view/artifact/hint-mock?at=tree:` (T from `landArtifact`), wait 11 s, and assert `__asked` has exactly one `/api/view?` (boot's) and `#av-reload` has no `stale`. +- [ ] **Step 2: run it, see it fail** (no `stale` in the viewer tab). +- [ ] **Step 3: implement** in `viewerBoot`, for an artifact: a `let asking = false` and a 10 s `setInterval` that returns unless visible, `TS.kind === 'artifact'`, `TS.at` not `tree:`, and not `asking`; it fetches `/api/view?kind=artifact&slug=&at=` for the `TS.slug`/`TS.at` taken at the ask, and on a 200 calls `avFpSeen(j.fp, j.main_fp, slug, at)`; anything else changes nothing; the fetch aborts after 15 s (`AbortController`; spec-2 N3); `asking` cleared in `finally`. Rewrite the boot comment to name this read (one `artifact describe` per ask, every 10 s while seen, main or a draft only). +- [ ] **Step 4: run it, see it pass**, then the whole file through the gate, and `/usr/local/bin/node --test test/dashboard.test.mjs` with the existing viewer test's "nothing else" check still green. +- [ ] **Step 5: commit** `kit-206: the viewer tab checks its artifact's fingerprint`. + +After both: verification.md (a row per done-when), then the finish (worker.md §8). + +## Rulings + +None yet. + +## From the spec reviews + +spec-2 (APPROVE 9d399ce) left N1-N4; they are taken here, not in spec.md, so its approval holds: the test's waits and no `POLL_FAST` (N1), main's fp for an unlisted draft at open (N2), a 15 s abort on the viewer's ask (N3), the check on every read, held or drawn (N4). diff --git a/docs/devteam-dev/tickets/kit-206-viewer-reload-hint/reviews/spec-2.md b/docs/devteam-dev/tickets/kit-206-viewer-reload-hint/reviews/spec-2.md new file mode 100644 index 00000000..e210a2ef --- /dev/null +++ b/docs/devteam-dev/tickets/kit-206-viewer-reload-hint/reviews/spec-2.md @@ -0,0 +1,31 @@ +# Spec review, pass 2 + +- Reviewer: spec-reviewer 2 (fresh subagent, claude-opus) +- Reviewed: 9d399cedb60788ceae2ec9540221f4395f3830c1 (spec.md) + +Sources read: `ticket show kit-206-viewer-reload-hint`, the spec and `reviews/spec-1.md` at 9d399ce, and quality.md §3. In `index.html`: the tokens (~34, ~42), `#av-reload` (~779, ~3669), `TS`/`tsSet`/`showArtifact` (~3543-3575), `openArtifact` (~3638), `route` (~3685), `tArtFpNow` (~4622), `tsSubject` (~5090), the frame `load` and lens `ready` handlers (~5171-5200), `holding`/`drawOrHold`/`load`/`loadNow` (~5232-5320), `viewerArtifact`/`viewerBoot` (~5644-5695) and the boot line (~5711). In `devteam.mjs`: `draftArtifacts` (~7382), `artifact describe` (~7544, which emits `fp` and `main_fp`), the shared `dashboardData` run (~14244), `/api/data` with `DATA_WAIT_MS` (~14377, ~11994) and `/api/view` (~14415). In `test/dashboard.test.mjs`: `POLL_FAST` (~325) and the viewer tests (~1833-2020). + +Every pass-1 fix holds in the spec text: +- **F1 holds.** `fpNow`/`mainFpNow` start equal to `fp`/`mainFp` through `tsSet`, reload copies only a known value, and acceptance check 3 includes the press-before-first-check case. +- **F2 holds.** The tab asks for `TS.slug`/`TS.at`, so after a fallback it asks for main, and a reply for a stage no longer shown is dropped. +- **F3 holds.** The colour check compares against a probe element styled `color: var(--amber)`, in light and again under emulated dark. +- **F4 holds.** The test reads the draft's `index.html` `` through its own `message` listener, so no production state is added. +- **F6 holds.** The reload-time race is accepted in Edge cases. +- **F7 holds.** One ask in flight per tab. +- **F8 holds.** The `viewerBoot` comment is rewritten, and the forbidden names are avoided (`\bload\(` does not match a name like `reload(`). +- **F9 holds.** `mainFp` follows on reload. + +Existing tests the change touches: +- The existing viewer test's "nothing else" assertion already allows `/api/view?`, so the new ask does not break it. +- The 500 and network-error fault tests inject the fault on every `/api/view` call, so the poll leaves them unchanged. + +No Critical or Important issue found: touches are respected; the `tree:` and preview exclusions keep the owner wrapper from polling; the title change is plain words, so V and R checks are unchanged from pass 1. What remains is two Minors (N1, the timing bound F5's fix added uses the wrong constant; N2, one edge case described incompletely) and two robustness suggestions (N3, N4). + +| Id | Severity | Rule | Where | Finding | Disposition | +|---|---|---|---|---|---| +| N1 | Minor | done-when 3 ("within one poll"); quality.md §3 (a test that can flake) | spec, Acceptance check 3; `devteam.mjs` `dashboardData` ~14248 (`dataRun ??=`), `/api/data` ~14380 (`DATA_WAIT_MS`, 20 s) | On the dashboard, "met by the first read that starts after the change" is not true: `/api/data` shares a `dashboard-data` run already in flight, and a run slower than `DATA_WAIT_MS` (20 s) is answered stale with the old fp. "12 s" is `/api/view`'s describe timeout, not anything on the `/api/data` path. Restate as "the first `dashboard-data` run that starts after the change", size the dashboard's wait from `DATA_WAIT_MS` and `READ_GIVE_UP_MS`, and say whether the test uses `POLL_FAST` (which would also make the viewer tab ask `/api/view` every 300 ms). | taken in the plan: the test does not use `POLL_FAST` (real 10 s polls); the dashboard waits up to 2 × 10 s + `DATA_WAIT_MS` (20 s) + 5 s, the viewer tab 2 × 10 s + 12 s + 5 s; met by the first `dashboard-data` run (or `/api/view` ask) that starts after the change | +| N2 | Minor | done-when 1; spec Edge cases, first bullet | spec Edge cases; `openArtifact` ~3647; `draftArtifacts` ~7395 | A draft equal to main opened on the dashboard keeps `TS.fp` null, so after an edit (now listed, `fpNow` known) the "both known" rule never shows the hint for that open. Say it plainly as accepted, or at open take main's fp for a draft whose row is absent while main's is present. The test should open a draft that already differs from main. | taken in the plan: `openArtifact` takes main's row's fp for a draft whose own row is absent while main's is present (absence means equal to main); the test's draft differs from main at open | +| N3 | Suggestion | none (robustness) | spec "The change" 2, viewer tab | The one-in-flight ask has no client-side timeout; a fetch that never settles stops all later checks in that tab. Abort a little past 12 s. | taken in the plan: the ask aborts after 15 s | +| N4 | Suggestion | none (taste) | spec "The change" 2, dashboard; `drawOrHold` ~5253 | "After each `/api/data` read is drawn" skips reads `holding()` holds back; take `fpNow` from each read as it arrives in `loadNow`. | taken in the plan: `loadNow` reads the raw `d`, held or drawn | + +Verdict: APPROVE 9d399cedb60788ceae2ec9540221f4395f3830c1 From dd5aad7f6ba02460be9c8c624223804b7538e93b Mon Sep 17 00:00:00 2001 From: Hugh Francis <hugh@sanctuary.computer> Date: Sun, 15 Nov 2026 01:41:08 +0000 Subject: [PATCH 04/10] kit-206: plan review 1 (changes), fixes, the spec ruling Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../kit-206-viewer-reload-hint/plan.md | 14 +++++----- .../reviews/plan-1.md | 27 +++++++++++++++++++ 2 files changed, 34 insertions(+), 7 deletions(-) create mode 100644 docs/devteam-dev/tickets/kit-206-viewer-reload-hint/reviews/plan-1.md diff --git a/docs/devteam-dev/tickets/kit-206-viewer-reload-hint/plan.md b/docs/devteam-dev/tickets/kit-206-viewer-reload-hint/plan.md index 8aeea0ad..49006f20 100644 --- a/docs/devteam-dev/tickets/kit-206-viewer-reload-hint/plan.md +++ b/docs/devteam-dev/tickets/kit-206-viewer-reload-hint/plan.md @@ -10,7 +10,7 @@ - Viewer tab: every 10 s while visible, one `/api/view` ask in flight for `TS.slug`/`TS.at` (main or a draft only). - Reload copies the known newer fingerprints into `fp`/`mainFp` and clears the hint; nothing reloads by itself. - One new browser test in `test/dashboard.test.mjs`, two subtests (dashboard stage, viewer tab), light and dark. -- Two tasks; only `index.html` and `test/dashboard.test.mjs` change. +- Risk: one-reload races at open and at reload are accepted (a spurious hint once); each visible viewer tab on main or a draft costs one `artifact describe` per 10 s; the test waits on real polls (up to 45 s). Two tasks, only `index.html` and the test change (plan-1 P3). **Goal:** the reload button says when the artifact on the stage is behind the one on disk. @@ -51,14 +51,14 @@ - `avHint(): void`: sets `#av-reload`'s `stale` class and title from `TS` (stale iff `TS.kind === 'artifact' && TS.fp && TS.fpNow && TS.fp !== TS.fpNow`). - `avFpSeen(fp: string|null, mainFp: string|null, slug: string, at: string): void`: if the stage shows that `slug`/`at` (and `at` isn't `tree:`), sets each given non-null value on `TS.fpNow`/`TS.mainFpNow`, then `avHint()`. -- [ ] **Step 1: write the failing test** `the viewer's reload button says when a newer version is ready, and the press loads it`, `{ skip: canDrive ? false : 'needs Chrome and a Node with WebSocket', timeout: 180000 }`, using `threadsFixture('reload-hint')`, `landArtifact(F, 'hint-mock', mock('main'))` and the draft written at `F.old/.worktrees/demo-01-draft/docs/devteam/project/artifacts/hint-mock` as `mock('v1')`, where `mock(t)` is `{ 'artifact.json': …title 'Hint mock'…, 'index.html': `<!doctype html><title>${t}

${t}

` }`. A page with `COUNT_FETCH`, and on each new document a listener that pushes `e.data.title` of `{ lens: 1, type: 'ready' }` messages from `#av-frame` into `window.__titles`. Helper `amber()` = computed `color` of a probe `` appended to `#artifact-view`. Subtest **dashboard**: +- [ ] **Step 1: write the failing test** `the viewer's reload button says when a newer version is ready, and the press loads it`, `{ skip: canDrive ? false : 'needs Chrome and a Node with WebSocket', timeout: 180000 }`, using `threadsFixture('reload-hint')`, `landArtifact(F, 'hint-mock', mock('main'))` and the draft written at `F.old/.worktrees/demo-01-draft/docs/devteam/project/artifacts/hint-mock` as `mock('v1')`, where `mock(t)` is `{ 'artifact.json': …title 'Hint mock'…, 'index.html': `${t}

${t}

` }`. The draft `mock('v1')` is written before the dashboard starts, so its first read already lists it. A page with `COUNT_FETCH`, and on each new document a listener, installed only where `document.getElementById('av-frame')` exists (the script also runs in the artifact's own frame), that pushes `e.data.title` of `{ lens: 1, type: 'ready' }` messages whose `e.source` is `#av-frame`'s `contentWindow` into `window.__titles`. The long waits use `soonIn(p, 45000)` and `soonIn(p, 37000)` (the default is 30 s) (plan-1 P5). Helper `amber()` = computed `color` of a probe `` appended to `#artifact-view`. Subtest **dashboard**: - open `${B}/#artifact/wt/demo-01-draft/hint-mock`; wait for `__titles` to include `'v1'`; assert `#av-reload` has no `stale` and its title is `Load the page again`. - press `#av-reload` (before any change); wait for a second `'v1'`. - write `index.html` as `mock('v2')`; wait up to 45 000 ms for `stale`; assert title `A newer version is ready: reload to see it`, `getComputedStyle(#av-reload).outlineColor === amber()`, outline width `2px`, and `__titles` has no `'v2'` (nothing reloaded by itself). - `Emulation.setEmulatedMedia` `{ features: [{ name: 'prefers-color-scheme', value: 'dark' }] }`: outline colour still equals `amber()`, and `amber()` differs from its light value; set it back to light. - press `#av-reload`: wait for `'v2'` in `__titles`; assert no `stale`, title `Load the page again`, and `TS.fp === TS.fpNow`. - [ ] **Step 2: run it, see it fail** — `/usr/local/bin/node --test --test-name-pattern "reload button says" test/dashboard.test.mjs`; expected: times out waiting for `stale`. -- [ ] **Step 3: implement.** CSS `#av-reload.stale { outline: 2px solid var(--amber); outline-offset: 1px; }`. Add `fpNow: null, mainFpNow: null` to `TS` and to `tsSet`'s reset. In `showArtifact`, pass `fpNow: fp, mainFpNow: mainFp` to `tsSet` (new stage), and on the same stage leave `fpNow`/`mainFpNow` as they are; call `avHint()` at its end. `avHint()` and `avFpSeen()` beside `showArtifact`. The reload handler: after resetting `src`, if `TS.kind === 'artifact'` copy `fpNow`→`fp` and `mainFpNow`→`mainFp` where non-null, then `avHint()`. `tsSet` also calls `avHint()` so a preview or close clears it. In `openArtifact`, a draft whose own Library row is absent while main's is present opens with main's fp as its `fp` (absence means equal to main; spec-2 N2). In `loadNow` after `drawOrHold(d)` (the raw `d`, so a held draw still feeds the check; spec-2 N4): if `TS.kind === 'artifact'`, `avFpSeen(row(TS.at)?.fp ?? null, row('main')?.fp ?? null, TS.slug, TS.at)` over `d.library?.artifacts`. Comments in the file's voice. +- [ ] **Step 3: implement.** CSS `#av-reload.stale:not(:focus-visible) { outline: 2px solid var(--amber); outline-offset: 1px; }`, so a keyboard focus ring still shows (plan-1 P6). Add `fpNow: null, mainFpNow: null` to `TS` and to `tsSet`'s reset. In `showArtifact`, pass `fpNow: fp, mainFpNow: mainFp` to `tsSet` (new stage), and on the same stage (a re-show, frame not reloaded) keep `fp`/`mainFp` as they are and take the given values only into `fpNow`/`mainFpNow` when non-null (plan-1 P2); call `avHint()` at its end. `avHint()` and `avFpSeen()` beside `showArtifact`. The reload handler: after resetting `src`, if `TS.kind === 'artifact'` copy `fpNow`→`fp` and `mainFpNow`→`mainFp` where non-null, then `avHint()`. `tsSet` also calls `avHint()` so a preview or close clears it. In `openArtifact`, a draft whose own Library row is absent while main's is present opens with main's fp as its `fp` (absence means equal to main; spec-2 N2). In `loadNow` after `drawOrHold(d)` (the raw `d`, so a held draw still feeds the check; spec-2 N4): if `TS.kind === 'artifact'`, `avFpSeen(row(TS.at)?.fp ?? null, row('main')?.fp ?? null, TS.slug, TS.at)` over `d.library?.artifacts`. Comments in the file's voice. - [ ] **Step 4: run it, see it pass**, then the whole file: `/usr/local/bin/node --test test/dashboard.test.mjs` through `devteam.mjs gate test`. - [ ] **Step 5: commit** `kit-206: the reload button's hint, and the dashboard's check`. @@ -70,17 +70,17 @@ **Interfaces (consumed):** `avFpSeen`, `TS.fpNow`, from Task 1. -- [ ] **Step 1: write the failing subtest** **viewer tab**: write the draft back to `mock('v1')`; go to `${B}/view/artifact/hint-mock?at=demo-01-draft`; wait for `'v1'` in a fresh `__titles`; no `stale`. Write `mock('v3')`; wait up to 37 000 ms for `stale` and the stale title; `__titles` has no `'v3'`. Press: `'v3'` arrives, `stale` off. Then go to `${B}/view/artifact/hint-mock?at=tree:` (T from `landArtifact`), wait 11 s, and assert `__asked` has exactly one `/api/view?` (boot's) and `#av-reload` has no `stale`. +- [ ] **Step 1: write the failing subtest** **viewer tab**: write the draft back to `mock('v1')`; go to `${B}/view/artifact/hint-mock?at=demo-01-draft`; wait for `'v1'` in a fresh `__titles`; no `stale`. Write `mock('v3')`; wait up to 37 000 ms for `stale` and the stale title; `__titles` has no `'v3'`. Press: `'v3'` arrives, `stale` off. Then go to `${B}/view/artifact/hint-mock?at=tree:` (T from `landArtifact`), wait (`soonIn(p, 37000)`) until `__asked` holds at least four `/api/threads` (boot's read and three 5 s ticks: over 15 s after both intervals started, so one 10 s tick has passed), and assert `__asked` has exactly one `/api/view?` (boot's) and `#av-reload` has no `stale` (plan-1 P1). - [ ] **Step 2: run it, see it fail** (no `stale` in the viewer tab). -- [ ] **Step 3: implement** in `viewerBoot`, for an artifact: a `let asking = false` and a 10 s `setInterval` that returns unless visible, `TS.kind === 'artifact'`, `TS.at` not `tree:`, and not `asking`; it fetches `/api/view?kind=artifact&slug=&at=` for the `TS.slug`/`TS.at` taken at the ask, and on a 200 calls `avFpSeen(j.fp, j.main_fp, slug, at)`; anything else changes nothing; the fetch aborts after 15 s (`AbortController`; spec-2 N3); `asking` cleared in `finally`. Rewrite the boot comment to name this read (one `artifact describe` per ask, every 10 s while seen, main or a draft only). +- [ ] **Step 3: implement** in `viewerBoot`, beside the 5 s threads interval (after `await tlRefresh()`, so the test's threads count is the poll's clock), for an artifact: a `let asking = false` and a 10 s `setInterval` that returns unless visible, `TS.kind === 'artifact'`, `TS.at` not `tree:`, and not `asking`; it fetches `/api/view?kind=artifact&slug=&at=` for the `TS.slug`/`TS.at` taken at the ask, and on a 200 calls `avFpSeen(j.fp, j.main_fp, slug, at)`; anything else changes nothing; the fetch aborts after 15 s (`AbortController`; spec-2 N3); `asking` cleared in `finally`. Rewrite the boot comment to name this read (one `artifact describe` per ask, every 10 s while seen, main or a draft only). - [ ] **Step 4: run it, see it pass**, then the whole file through the gate, and `/usr/local/bin/node --test test/dashboard.test.mjs` with the existing viewer test's "nothing else" check still green. - [ ] **Step 5: commit** `kit-206: the viewer tab checks its artifact's fingerprint`. -After both: verification.md (a row per done-when), then the finish (worker.md §8). +After both: verification.md (a row per done-when), then the finish (worker.md §8). The stale `/api/view` comment was told to the supervisor in m-20261115-013705816 and ruled on (below) (plan-1 P4). ## Rulings -None yet. +- m-20261115-013720113-supervisor (re m-20261115-013705816): spec approved; leaving `devteam.mjs`'s stale "never polled" comment on `/api/view` for kit-106 is fine; the supervisor tells fast-threads. ## From the spec reviews diff --git a/docs/devteam-dev/tickets/kit-206-viewer-reload-hint/reviews/plan-1.md b/docs/devteam-dev/tickets/kit-206-viewer-reload-hint/reviews/plan-1.md new file mode 100644 index 00000000..dc2005ac --- /dev/null +++ b/docs/devteam-dev/tickets/kit-206-viewer-reload-hint/reviews/plan-1.md @@ -0,0 +1,27 @@ +# Plan review, pass 1 + +- Reviewer: plan-reviewer (fresh subagent, claude-opus) +- Reviewed: 34ee7d3a4a92f707d3ba4ae86c8a869537d6bd50 (plan.md) + +**What I read.** At 34ee7d3: the plan, the spec, spec-1 and spec-2, the ticket, and quality.md §3 and §6; in `index.html` the tokens (~30-45), the viewer CSS (~516-540), `#av-reload` (~779, ~3669), `TS`/`tsSet`/`showArtifact` (~3543-3575), `openArtifact`/`route` (~3638, ~3685), the frame `load` and lens `message` handlers (~5171-5200), `drawOrHold`/`load`/`loadNow` (~5253-5330) and `viewerArtifact`/`viewerBoot` (~5644-5695); in `devteam.mjs` `ARTIFACT_LENS` (`ready` only in answer to `hello`, carrying `document.title`), `draftWorktree`/`draftArtifacts` (a plain directory under `.worktrees/` is a draft), `/api/view` (a 12 s describe; `ARTIFACT_AT` takes worktree names), the artifact route (`no-store`) and `dashboardData`/`DATA_WAIT_MS`; in the tests `soonIn`, `POLL_FAST`, `landArtifact`, `COUNT_FETCH`, the existing viewer test, `threadsFixture`, `chrome.mjs` (`send` is generic, so `Emulation.setEmulatedMedia` works) and `test/browser/dev-links.mjs`, which already clicks `#av-reload` and listens to lens messages from the parent page. + +**Coverage.** Every done-when and spec points 1-5; N1-N4 taken in as spec-2 says. + +**Interfaces.** `avHint`, `avFpSeen(fp, mainFp, slug, at)` and `fpNow`/`mainFpNow` are used the same way in both tasks. + +**Feasibility.** The lens `ready` listener on the parent works (messages from an opaque-origin frame reach the parent; `e.source` compares with `#av-frame.contentWindow`); the `#artifact/wt//` route and a draft folder under `F.old/.worktrees/` work; the computed `outline-color` against a probe and the emulated dark scheme work; the real 10 s polls fit in the 180 s timeout. + +**Existing tests.** They keep passing: the viewer test's "nothing else" list allows `/api/view?`; the `viewerBoot` checks (forbidden names, `postMessage`, `location.*`) are not tripped; the fault tests are unaffected. + +**What blocks.** P1: the tree check waits a fixed 11 s from navigation, but the poll's interval only starts after boot's awaits, so the check would usually pass even with the `tree:` guard broken. + +| Id | Severity | Rule | Where | Finding | Disposition | +|---|---|---|---|---|---| +| P1 | Important | spec "The change" 2 (a `tree:` version is never checked); plan Review focus 4; quality.md §3 (missing test with a plausible failure path) | plan Task 2 Step 1; `index.html` `viewerBoot` ~5664-5695 | The 10 s interval is created after `await viewerArtifact()` and, beside the threads interval, after `await tlRefresh()`. `go()` returns at the start of boot, so the interval starts 1-3 s later and its first tick lands after an 11 s assertion: a missing `tree:` guard would still pass. Create the view poll beside the 5 s threads interval and wait for at least four `/api/threads` in `__asked` before asserting one `/api/view?`, or another wait that provably covers one tick. | fixed: the poll sits beside the threads interval; the test waits for four `/api/threads` | +| P2 | Minor | spec "The change" 1 | plan Task 1 Step 3; `index.html` ~3570 `else Object.assign(TS, { fp, mainFp })` | On a re-show of the same stage the frame is not reloaded, but `TS.fp` is overwritten with `DATA`'s newer fp, so the hint clears while the frame still shows the old page. Keep `fp`/`mainFp` there, or accept it. | fixed: a re-show keeps `fp`/`mainFp`; the given values go only to `fpNow`/`mainFpNow` | +| P3 | Minor | quality.md §6 (the TL;DR says what could go wrong) | plan TL;DR | No risk bullet. Swap one bullet for the main risk. | fixed: a risk bullet | +| P4 | Minor | spec Edge cases, last bullet | plan, the "After both" line | No step tells the supervisor about the stale `/api/view` comment. | fixed: told in m-20261115-013705816, ruled in m-20261115-013720113; both in the plan | +| P5 | Suggestion | none (robustness) | plan Task 1 Step 1 | (a) `addScriptToEvaluateOnNewDocument` also runs in the artifact's frame: guard the listener on `#av-frame`. (b) Write the v1 draft before the dashboard starts. (c) The long waits need `soonIn(p, 45000)`/`soonIn(p, 37000)`; the default is 30 s. | fixed: all three in Task 1 Step 1 | +| P6 | Suggestion | none (taste, accessibility) | plan Task 1 Step 3 CSS | `#av-reload.stale` overrides the `:focus-visible` ring. Consider `#av-reload.stale:not(:focus-visible)`. | fixed: `:not(:focus-visible)` | + +Verdict: CHANGES 34ee7d3a4a92f707d3ba4ae86c8a869537d6bd50 From 0d103390bdb5a307e2dc48596c52c53ddc7c7d5e Mon Sep 17 00:00:00 2001 From: Hugh Francis Date: Sun, 15 Nov 2026 01:42:52 +0000 Subject: [PATCH 05/10] kit-206: plan review 2 (approve dd5aad7) Co-Authored-By: Claude Opus 5.5 --- .../reviews/plan-2.md | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 docs/devteam-dev/tickets/kit-206-viewer-reload-hint/reviews/plan-2.md diff --git a/docs/devteam-dev/tickets/kit-206-viewer-reload-hint/reviews/plan-2.md b/docs/devteam-dev/tickets/kit-206-viewer-reload-hint/reviews/plan-2.md new file mode 100644 index 00000000..f063fea0 --- /dev/null +++ b/docs/devteam-dev/tickets/kit-206-viewer-reload-hint/reviews/plan-2.md @@ -0,0 +1,22 @@ +# Plan review, pass 2 + +- Reviewer: plan-reviewer 2 (fresh subagent, claude-opus) +- Reviewed: dd5aad7f6ba02460be9c8c624223804b7538e93b (plan.md) + +**What I read.** At dd5aad7: the plan, the spec (approved at 9d399ce), plan-1, the ticket, and quality.md §3 and §6. In `index.html`: `TS`, `tsSet` and `showArtifact` (~3543-3575), `openArtifact`, `closeArtifact` and the `#av-reload` handler (~3638-3670), `route` (~3685) and its callers, `drawOrHold` and `loadNow` (~5253-5325), `tlRefresh` and every place that calls it, and `viewerArtifact` and `viewerBoot` (~5644-5695). In the tests, `COUNT_FETCH`. In the log, m-20261115-013720113-supervisor, which answers m-20261115-013705816-kit-206-viewer-reload-hint. + +**Plan-1 dispositions.** All six hold. +- **P1 holds.** Task 2 Step 3 puts the view poll after `await tlRefresh()`, next to the 5 s threads interval, so both timers start together; Step 1 waits for four `/api/threads` asks before asserting a single `/api/view?`. In a viewer tab, `/api/threads` is asked only by boot and the 5 s tick; every other caller of `tlRefresh` (4763, 4831, 4874, 4942, 5065, 5276) runs only when a card is open, an answer is sent or the dashboard draws, none of which happens in this subtest. So the fourth ask (the 15 s tick) comes after the 10 s view tick, and a missing `tree:` guard would put a second `/api/view?` in the list. +- **P2 holds.** On a re-show, `fp`/`mainFp` are kept and the new values go only into `fpNow`/`mainFpNow` (see S1). +- **P3 holds.** There is a risk bullet, and the TL;DR has exactly seven bullets. +- **P4 holds.** The ruling is in the log and quoted in the plan. +- **P5 holds.** (a), (b) and (c) are all in Task 1 Step 1. +- **P6 holds.** The CSS uses `:not(:focus-visible)`. + +**New issues from the fixes.** Only S1. The re-show branch in `openArtifact` can almost never run: `route` calls `openArtifact` only on the first draw (while `DATA` is null) and on `hashchange`, which lands on the same `slug`/`at` only through an equivalent but differently encoded hash; the viewer calls `showArtifact` once. + +| Id | Severity | Rule | Where | Finding | Disposition | +|---|---|---|---|---|---| +| S1 | Suggestion | spec Edge cases ("A draft removed while open: the dashboard finds no row … neither changes the hint") | plan Task 1 Step 3 (P2 re-show and N2 fallback together); `index.html` `openArtifact` ~3647 | On a re-show, `openArtifact`'s N2 fallback gives main's fp for a draft whose row is missing, and P2 writes it into `fpNow`; for a draft removed while open the hint would then come on, though the spec says a missing row changes nothing. Practically unreachable, so acceptable as written; or apply the fallback only when the stage is new. | taken in the build: the fallback applies only when the stage is new (no change to the plan's interfaces) | + +Verdict: APPROVE dd5aad7f6ba02460be9c8c624223804b7538e93b From 21ee88e246c0c2d1b08243cfbc17683adee0ad7d Mon Sep 17 00:00:00 2001 From: Hugh Francis Date: Sun, 15 Nov 2026 01:48:26 +0000 Subject: [PATCH 06/10] kit-206: the reload button's hint, and the dashboard's check Co-Authored-By: Claude Opus 5.5 --- docs/devteam/dashboard/index.html | 52 +++++++++++++++++++++--- test/dashboard.test.mjs | 67 +++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 6 deletions(-) diff --git a/docs/devteam/dashboard/index.html b/docs/devteam/dashboard/index.html index 0fc71f1e..a8ac6bea 100644 --- a/docs/devteam/dashboard/index.html +++ b/docs/devteam/dashboard/index.html @@ -521,6 +521,8 @@ .av-bar button, .av-bar a.av-btn { flex: none; margin: 0; display: inline-flex; align-items: center; gap: 6px; } .av-bar a.av-btn { height: 28px; padding: 0 10px; border-radius: 6px; font-size: 12px; font-weight: 500; background: var(--surface); color: var(--fg); border: 1px solid var(--line-2); } .av-bar a.av-btn:hover { background: var(--hover); } + /* the artifact changed since the frame loaded it; a keyboard's focus ring still shows over it */ + #av-reload.stale:not(:focus-visible) { outline: 2px solid var(--amber); outline-offset: 1px; } #av-frame, #pv-frame { flex: 1 1 auto; min-height: 0; width: 100%; border: 0; background: #fff; display: block; } #av-frame[hidden], #pv-frame[hidden] { display: none; } @media (max-width: 900px) { #artifact-view:not([hidden]) { inset: 8px; } } @@ -3540,7 +3542,7 @@

Fingerprints

${inc.fingerprints.map((f) => feed.has(f. // pointer events, the last reply, and where the lens says each pinned anchor is now (keyed by a // thread's id). A preview's: its name, the lens's origin, and its app's route, viewport and last // errors as the lens said them; the session that offered it and whether it answers, for the bar. -const TS = { kind: null, slug: null, at: null, fp: null, mainFp: null, path: null, name: null, route: null, viewport: null, errors: [], origin: null, session: null, up: null, target: '*', ready: false, raised: false, seq: 0, minted: new Map(), hit: null, waiters: new Map(), rects: new Map(), keyOf: new Map(), watching: new Set(), watch: '', last: null, rafPending: false, via: null, waitMs: 150, drawPending: false, pinsPending: false }; +const TS = { kind: null, slug: null, at: null, fp: null, mainFp: null, fpNow: null, mainFpNow: null, path: null, name: null, route: null, viewport: null, errors: [], origin: null, session: null, up: null, target: '*', ready: false, raised: false, seq: 0, minted: new Map(), hit: null, waiters: new Map(), rects: new Map(), keyOf: new Map(), watching: new Set(), watch: '', last: null, rafPending: false, via: null, waitMs: 150, drawPending: false, pinsPending: false }; // What the lens was asked and has not answered, let go: a waiting click hears it is gone (the page it // pointed at is no longer there, so it starts nothing), and no reply to a question from before is taken. function tsForget() { @@ -3551,8 +3553,9 @@

Fingerprints

${inc.fingerprints.map((f) => feed.has(f. } function tsSet(kind, info = {}) { tsForget(); - Object.assign(TS, { kind, slug: null, at: null, fp: null, mainFp: null, path: null, name: null, route: null, viewport: null, errors: [], origin: null, session: null, up: null, target: '*', ready: false, watch: '', last: null }, info); + Object.assign(TS, { kind, slug: null, at: null, fp: null, mainFp: null, fpNow: null, mainFpNow: null, path: null, name: null, route: null, viewport: null, errors: [], origin: null, session: null, up: null, target: '*', ready: false, watch: '', last: null }, info); for (const m of [TS.rects, TS.keyOf, TS.watching]) m.clear(); + avHint(); } // The stage, in the dashboard or as a viewer tab: the page in its frame, the bar, and the lens's // state for it. title: the viewer's (from /api/view); the dashboard's comes from its data. @@ -3566,13 +3569,33 @@

Fingerprints

${inc.fingerprints.map((f) => feed.has(f. $('av-note').hidden = !note; $('av-note').textContent = note ?? ''; $('av-tab').href = artViewer(slug, at); - if (TS.kind !== 'artifact' || TS.slug !== slug || TS.at !== at) tsSet('artifact', { slug, at, fp, mainFp }); - else Object.assign(TS, { fp, mainFp }); + // fp and mainFp are what the frame loads now; on the stage already shown its frame isn't loaded + // again, so what it loaded stays, and what is given is only the latest seen + if (TS.kind !== 'artifact' || TS.slug !== slug || TS.at !== at) tsSet('artifact', { slug, at, fp, mainFp, fpNow: fp, mainFpNow: mainFp }); + else Object.assign(TS, { fpNow: fp ?? TS.fpNow, mainFpNow: mainFp ?? TS.mainFpNow }); $('pv-frame').hidden = true; $('pv-frame').removeAttribute('src'); $('av-frame').hidden = false; if ($('av-frame').getAttribute('src') !== src) $('av-frame').src = src; $('artifact-view').hidden = false; + avHint(); +} +// The reload button says when the artifact on the stage changed since its frame loaded it (the +// latest fingerprint seen is another), until a press loads it. Never a reload by itself: kit-106's +// live reload of drafts is to take over from this. +function avHint() { + const stale = TS.kind === 'artifact' && !!TS.fp && !!TS.fpNow && TS.fp !== TS.fpNow; + const b = $('av-reload'); + b.classList.toggle('stale', stale); + b.title = stale ? 'A newer version is ready: reload to see it' : 'Load the page again'; +} +// The fingerprints just seen for an artifact there (null: not known), taken only while the stage +// shows it; a published tree never changes, so it is never checked. +function avFpSeen(fp, mainFp, slug, at) { + if (TS.kind !== 'artifact' || TS.slug !== slug || TS.at !== at || String(at).startsWith('tree:')) return; + if (fp) TS.fpNow = fp; + if (mainFp) TS.mainFpNow = mainFp; + avHint(); } // A preview's viewer, in a tab of its own. const pvViewer = (name) => `/view/preview/${encodeURIComponent(name)}`; @@ -3644,7 +3667,12 @@

Fingerprints

${inc.fingerprints.map((f) => feed.has(f. if (!at || !/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(slug ?? '')) return void (location.hash = '#library'); if (!AV) { AV_FROM = ROUTED; avFocus = document.activeElement; } const arts = DATA?.library?.artifacts ?? []; - showArtifact(slug, at, { note, fp: arts.find((x) => x.slug === slug && x.at === at)?.fp ?? null, mainFp: arts.find((x) => x.slug === slug && x.at === 'main')?.fp ?? null }); + const mainFp = arts.find((x) => x.slug === slug && x.at === 'main')?.fp ?? null; + // a draft the Library doesn't list while main's is listed is the same as main's; only for a new + // stage, as one gone while it was shown is listed nowhere either + const fresh = TS.kind !== 'artifact' || TS.slug !== slug || TS.at !== at; + const fp = arts.find((x) => x.slug === slug && x.at === at)?.fp ?? (fresh && at !== 'main' && !String(at).startsWith('tree:') ? mainFp : null); + showArtifact(slug, at, { note, fp, mainFp }); $('scrim').hidden = false; document.body.style.overflow = 'hidden'; $('av-close').focus(); @@ -3666,7 +3694,14 @@

Fingerprints

${inc.fingerprints.map((f) => feed.has(f. // a viewer tab has nothing to go back to: it never closes function leaveArtifact() { if (VIEWER) return; if (AV_FROM) history.back(); else location.hash = '#library'; } $('av-close').addEventListener('click', leaveArtifact); -$('av-reload').addEventListener('click', () => { const f = tsFrame(), src = f.getAttribute('src'); if (!src) return; f.removeAttribute('src'); f.src = src; }); +$('av-reload').addEventListener('click', () => { + const f = tsFrame(), src = f.getAttribute('src'); + if (!src) return; + f.removeAttribute('src'); + f.src = src; + // the frame now loads the latest seen + if (TS.kind === 'artifact') { if (TS.fpNow) TS.fp = TS.fpNow; if (TS.mainFpNow) TS.mainFp = TS.mainFpNow; avHint(); } +}); // a question's link to a changed artifact says so in the viewer's header document.addEventListener('click', (e) => { const a = e.target.closest?.('a[data-av-note]'); if (a) AV_NOTE = a.dataset.avNote || null; }); @@ -5323,6 +5358,11 @@

${k.plain ? esc(k.plain) : linkify(k.statement)}

document.querySelector('main').classList.remove('waiting'); drawOrHold(d); staleNote(); + // the stage's artifact as this read lists it, held from the page or drawn (a missing row says nothing) + if (TS.kind === 'artifact') { + const row = (at) => (d.library?.artifacts ?? []).find((a) => a.slug === TS.slug && a.at === at)?.fp ?? null; + avFpSeen(row(TS.at), row('main'), TS.slug, TS.at); + } } // The server answers a slow read with the last data it had, marked stale: say so where it shows on // any screen, with the time of what is drawn (not of a read held while the owner types), until a diff --git a/test/dashboard.test.mjs b/test/dashboard.test.mjs index 27878140..71644038 100644 --- a/test/dashboard.test.mjs +++ b/test/dashboard.test.mjs @@ -2021,6 +2021,73 @@ test('the viewer: one artifact on a stage the size of the tab, the threads and h assert.deepEqual(await p.ev(`[document.getElementById('artifact-view').hidden, location.pathname, location.hash, window.__esc]`), [false, '/view/artifact/viewer-mock', '', true]); }); }); +// The reload button says when the artifact on the stage changed since its frame loaded, on the +// page's real polls (never POLL_FAST: "within one poll" is the 10 s one), and only a press reloads. +test("the viewer's reload button says when a newer version is ready, and the press loads it", { skip: canDrive ? false : 'needs Chrome and a Node with WebSocket', timeout: 240000 }, async (t) => { + const undo = []; + t.after(async () => { let err = null; for (const f of undo.reverse()) { try { await f(); } catch (e) { err ??= e; } } if (err) throw err; }); + const F = threadsFixture('reload-hint'); + undo.push(() => F.done()); + // the page's is what its lens says when it is ready: how the test sees what the frame shows + const mock = (title) => ({ 'artifact.json': JSON.stringify({ v: 1, title: 'Hint mock', summary: 'A page that changes', by: 'supervisor', for: [] }), 'index.html': `<!doctype html><html><head><title>${title}

${title}

` }); + const T = landArtifact(F, 'hint-mock', mock('main')); + const draft = path.join(F.old, '.worktrees/demo-01-draft/docs/devteam/project/artifacts/hint-mock'); + const edit = (title) => { for (const [rel, text] of Object.entries(mock(title))) write(path.join(draft, rel), text); }; + // written before the dashboard starts: its first read already lists the draft + edit('v1'); + const { dash, port } = await dashboardOnFreePort(F.old, F.env({})); + const B = `http://127.0.0.1:${port}`; + undo.push(() => dash.stop()); + const chrome = await openChrome(); + undo.push(() => chrome.close()); + const p = await chrome.newPage(); + undo.push(() => p.close()); + await p.send('Page.enable'); + await p.send('Runtime.enable'); + await p.send('Emulation.setDeviceMetricsOverride', { width: 1200, height: 800, deviceScaleFactor: 1, mobile: false }); + await p.send('Page.addScriptToEvaluateOnNewDocument', { source: COUNT_FETCH }); + // the titles the stage's frame says when ready; the script runs in the artifact's own frame too, which has no stage + await p.send('Page.addScriptToEvaluateOnNewDocument', { source: `(() => { window.__titles = []; addEventListener('message', (e) => { const f = document.getElementById('av-frame'); if (f && e.source === f.contentWindow && e.data?.lens === 1 && e.data.type === 'ready') window.__titles.push(e.data.title); }); })();` }); + const soon = soonIn(p); + // two polls, and a read that may have started before the change: the dashboard's can wait DATA_WAIT_MS (20 s), the viewer's describe 12 s + const soonData = soonIn(p, 45000); + const soonView = soonIn(p, 37000); + const said = (title) => p.ev(`window.__titles.filter((x) => x === ${JSON.stringify(title)}).length`); + const button = () => p.ev(`(() => { const b = document.getElementById('av-reload'), s = getComputedStyle(b); return { stale: b.classList.contains('stale'), title: b.title, color: s.outlineColor, width: s.outlineWidth, style: s.outlineStyle }; })()`); + // the warning token as this scheme resolves it + const amber = () => p.ev(`(() => { const e = document.createElement('span'); e.style.color = 'var(--amber)'; document.getElementById('artifact-view').append(e); const c = getComputedStyle(e).color; e.remove(); return c; })()`); + const scheme = (value) => p.send('Emulation.setEmulatedMedia', { features: [{ name: 'prefers-color-scheme', value }] }); + const FRESH = 'Load the page again'; + const NEWER = 'A newer version is ready: reload to see it'; + + await t.test('on the dashboard: a draft changed on disk outlines reload in the warning colour, light and dark; the press loads it', async () => { + await p.send('Page.navigate', { url: `${B}/#artifact/wt/demo-01-draft/hint-mock` }); + await soon(`typeof __titles === 'object' && __titles.includes('v1')`, 'the draft in the frame'); + assert.deepEqual(await p.ev(`[TS.kind, TS.at, /^[0-9a-f]{64}$/.test(TS.fp ?? '')]`), ['artifact', 'demo-01-draft', true], 'opened with its fingerprint'); + let b = await button(); + assert.deepEqual([b.stale, b.title], [false, FRESH], 'nothing newer at open'); + // a press before any check: what it loaded is still known, so a later change still says so + await p.click('#av-reload'); + await soon(`__titles.filter((x) => x === 'v1').length === 2`, 'the draft again'); + assert.equal((await button()).stale, false); + edit('v2'); + await soonData(`document.getElementById('av-reload').classList.contains('stale')`, 'the outline, within a poll'); + b = await button(); + const light = await amber(); + assert.deepEqual([b.title, b.color, b.width, b.style], [NEWER, light, '2px', 'solid'], 'the warning colour, light'); + assert.equal(await said('v2'), 0, 'nothing reloaded by itself'); + await scheme('dark'); + const dark = await amber(); + assert.notEqual(dark, light, 'the dark token is another colour'); + assert.equal((await button()).color, dark, 'the warning colour, dark'); + await scheme('light'); + await p.click('#av-reload'); + await soon(`__titles.includes('v2')`, 'the new version in the frame'); + b = await button(); + assert.deepEqual([b.stale, b.title], [false, FRESH], 'the outline goes'); + assert.equal(await p.ev('TS.fp === TS.fpNow'), true); + }); +}); test('Shift over an artifact: the overlay, what the lens says is under the pointer, a thread on that spot', { skip: canDrive ? false : 'needs Chrome and a Node with WebSocket', timeout: 240000 }, async (t) => { const undo = []; t.after(async () => { let err = null; for (const f of undo.reverse()) { try { await f(); } catch (e) { err ??= e; } } if (err) throw err; }); From 7f62d296b5cb8f837d8e49828ec14fd719bd10c5 Mon Sep 17 00:00:00 2001 From: Hugh Francis Date: Sun, 15 Nov 2026 01:59:19 +0000 Subject: [PATCH 07/10] kit-206: the viewer tab checks its artifact's fingerprint; task 1 review (changes), fixes Co-Authored-By: Claude Opus 5.5 --- .../reviews/task-1.md | 26 +++++++++++++++ docs/devteam/dashboard/index.html | 19 +++++++++-- test/dashboard.test.mjs | 33 ++++++++++++++++++- 3 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 docs/devteam-dev/tickets/kit-206-viewer-reload-hint/reviews/task-1.md diff --git a/docs/devteam-dev/tickets/kit-206-viewer-reload-hint/reviews/task-1.md b/docs/devteam-dev/tickets/kit-206-viewer-reload-hint/reviews/task-1.md new file mode 100644 index 00000000..c59764ba --- /dev/null +++ b/docs/devteam-dev/tickets/kit-206-viewer-reload-hint/reviews/task-1.md @@ -0,0 +1,26 @@ +# Task 1 review + +- Reviewer: task-reviewer (fresh subagent, claude-opus) +- Reviewed: 21ee88e246c0c2d1b08243cfbc17683adee0ad7d (task 1) + +**Summary.** I read the diff against plan Task 1, the spec, plan-2 S1, and the code around it: `TS` and `tsSet`, `showArtifact`, `showPreview`, `openArtifact`, `closeArtifact`, the `#av-reload` handler, and `drawOrHold`, `load` and `loadNow`. Only `docs/devteam/dashboard/index.html` and `test/dashboard.test.mjs` changed. The new test passes at the head (2/2, under Node 22). + +The production code looks correct: +- **`tsSet` calling `avHint()` is safe.** Its three callers (`showArtifact`, `showPreview`, `closeArtifact`) all run after the DOM exists (the script at line 814 comes after `#av-reload` at line 781), and `avHint` is a hoisted declaration. On a preview or after a close it gives the class off and today's title. +- **The re-show branch** keeps `fp`/`mainFp` and only moves `fpNow`/`mainFpNow` forward when a value is given (P2). +- **`openArtifact`'s fallback** to main's fp applies only on a new stage, as S1's disposition says. Its `tree:` guard can never fire (the hash regex never yields `tree:`), but it does no harm. +- **The reload handler** does nothing for a preview and copies only fingerprints it knows. +- **The `loadNow` hook** uses the raw `d`, so a held draw still feeds the check; it runs only on the dashboard (`load()` returns early in a viewer tab), and on the first read after `route()` → `openArtifact`. +- **Comments** match the file's voice and density. + +Two mutations in a scratch copy (a `git archive` of 21ee88e) passed the test, so two properties it claims are not pinned: +1. **No open-time init, unguarded copy** (the spec-1 F1 failure mode): the press "before any check" in fact comes after the first `/api/data` read's `loadNow` hook has set `fpNow`. +2. **A delayed self-reload** (the frame's `src` reset 1.5 s after the hint): `said('v2') === 0` is checked the instant `stale` appears and never again. + +| Id | Severity | Rule | Where | Finding | Disposition | +|---|---|---|---|---|---| +| T1-1 | Important | spec acceptance check 3 (spec-1 F1); plan Review focus 1 | test/dashboard.test.mjs, the dashboard subtest, "a press before any check" | On the dashboard the first read sets `fpNow` before the press, so mutation 1 passes. Pin it where a press can come before any check (Task 2's viewer tab: press right after `'v1'`, then change the file and expect `stale`), and fix or drop the inaccurate comment here. | fixed in Task 2's commit: the viewer-tab subtest presses before its first check (asserting only boot's `/api/view?` so far), then expects the hint; the dashboard comment now says what that press shows. Mutation 1 now fails the test | +| T1-2 | Important | spec The change §4; acceptance check 2 | test/dashboard.test.mjs, `said('v2') === 0` | Checked the instant `stale` appears; mutation 2 passes. Check again right before the press, after a set wait of a few seconds. | fixed: a 3 s wait and a second check before the press, in both subtests | +| T1-3 | Suggestion | plan Task 1 Step 1 (`timeout: 180000`) | the new test's options | 240000, not the plan's 180000: harmless, matches neighbours, leaves room for Task 2's subtest. | kept at 240000: two subtests with real polls (noted in verification.md) | + +Verdict: CHANGES 21ee88e246c0c2d1b08243cfbc17683adee0ad7d diff --git a/docs/devteam/dashboard/index.html b/docs/devteam/dashboard/index.html index a8ac6bea..9cfb4d8e 100644 --- a/docs/devteam/dashboard/index.html +++ b/docs/devteam/dashboard/index.html @@ -5702,8 +5702,10 @@

${k.plain ? esc(k.plain) : linkify(k.statement)}

const d = got.d; showArtifact(VIEWER.name, at, { title: d?.title || VIEWER.name, note: held ?? (d ? note : got.status === 404 ? 'This artifact is not here' : 'This artifact could not be read'), fp: d?.fp ?? null, mainFp: d?.main_fp ?? null }); } -// The viewer's own start: the stage, the layer's threads every 5 s while the tab is seen, and the -// instance's hello (its name) every 30 s. Nothing else: a dozen viewer tabs cost a dozen cheap reads. +// The viewer's own start: the stage, the layer's threads every 5 s while the tab is seen, the +// instance's hello (its name) every 30 s, and for an artifact on main or a draft its fingerprint +// every 10 s while seen (/api/view: one artifact describe per ask, for the reload button's hint). +// Nothing else. async function viewerBoot() { document.documentElement.classList.add('viewer'); if (VIEWER.kind === 'preview') { if (showPreview(VIEWER.name, await previewInfo(VIEWER.name)) === 'reload') return; } @@ -5730,6 +5732,19 @@

${k.plain ? esc(k.plain) : linkify(k.statement)}

viewerTalk(); addEventListener('hashchange', viewerTalk); setInterval(() => { if (document.visibilityState === 'visible') tlRefresh(); }, 5000); + // one ask at a time (a describe can take 12 s), for the stage as it is when asked; a tree never changes + let asking = false; + setInterval(async () => { + if (asking || document.visibilityState !== 'visible' || TS.kind !== 'artifact' || String(TS.at).startsWith('tree:')) return; + const { slug, at } = TS; + asking = true; + const stop = new AbortController(); + const late = setTimeout(() => stop.abort(), 15000); + try { + const r = await fetch(`/api/view?${new URLSearchParams({ kind: 'artifact', slug, at })}`, { signal: stop.signal }); + if (r.ok) { const j = await r.json(); avFpSeen(j.fp ?? null, j.main_fp ?? null, slug, at); } + } catch {} finally { clearTimeout(late); asking = false; } + }, 10000); } // #talk/ in a viewer tab, and nothing else of the dashboard's routes. function viewerTalk() { diff --git a/test/dashboard.test.mjs b/test/dashboard.test.mjs index 71644038..02c7ed16 100644 --- a/test/dashboard.test.mjs +++ b/test/dashboard.test.mjs @@ -2066,7 +2066,7 @@ test("the viewer's reload button says when a newer version is ready, and the pre assert.deepEqual(await p.ev(`[TS.kind, TS.at, /^[0-9a-f]{64}$/.test(TS.fp ?? '')]`), ['artifact', 'demo-01-draft', true], 'opened with its fingerprint'); let b = await button(); assert.deepEqual([b.stale, b.title], [false, FRESH], 'nothing newer at open'); - // a press before any check: what it loaded is still known, so a later change still says so + // a press on the version shown changes nothing (a press before any check is the viewer tab's, below) await p.click('#av-reload'); await soon(`__titles.filter((x) => x === 'v1').length === 2`, 'the draft again'); assert.equal((await button()).stale, false); @@ -2081,12 +2081,43 @@ test("the viewer's reload button says when a newer version is ready, and the pre assert.notEqual(dark, light, 'the dark token is another colour'); assert.equal((await button()).color, dark, 'the warning colour, dark'); await scheme('light'); + // and some seconds on, still the old version: only the press loads it + await new Promise((r) => setTimeout(r, 3000)); + assert.equal(await said('v2'), 0, 'still nothing reloaded by itself'); await p.click('#av-reload'); await soon(`__titles.includes('v2')`, 'the new version in the frame'); b = await button(); assert.deepEqual([b.stale, b.title], [false, FRESH], 'the outline goes'); assert.equal(await p.ev('TS.fp === TS.fpNow'), true); }); + + await t.test('in a viewer tab: its own check of the draft, every 10 s; a press before any check still leaves the hint to come; a tree is never checked', async () => { + edit('v1'); + await p.send('Page.navigate', { url: `${B}/view/artifact/hint-mock?at=demo-01-draft` }); + await soon(`document.documentElement.classList.contains('viewer') && typeof __titles === 'object' && __titles.includes('v1')`, 'the draft in the viewer'); + // pressed before the tab's first check: only boot's ask so far + await p.click('#av-reload'); + await soon(`__titles.filter((x) => x === 'v1').length === 2`, 'the draft again'); + assert.equal(await p.ev(`__asked.filter((u) => u.startsWith('/api/view?')).length`), 1, 'no check yet when pressed'); + let b = await button(); + assert.deepEqual([b.stale, b.title], [false, FRESH]); + edit('v3'); + await soonView(`document.getElementById('av-reload').classList.contains('stale')`, 'the outline, within a poll'); + b = await button(); + assert.deepEqual([b.title, b.color, b.width], [NEWER, await amber(), '2px']); + await new Promise((r) => setTimeout(r, 3000)); + assert.equal(await said('v3'), 0, 'nothing reloaded by itself'); + await p.click('#av-reload'); + await soon(`__titles.includes('v3')`, 'the new version in the frame'); + assert.deepEqual([(await button()).stale, (await button()).title], [false, FRESH], 'the outline goes'); + // a published tree: after one 10 s tick has passed (four reads of the threads: boot's and three + // 5 s ticks, started beside it), still only boot's ask + await p.send('Page.navigate', { url: `${B}/view/artifact/hint-mock?at=tree:${T}` }); + await soon(`document.documentElement.classList.contains('viewer') && typeof TS === 'object' && TS.at === 'tree:${T}'`, 'the tree in the viewer'); + await soonView(`__asked.filter((u) => u === '/api/threads').length >= 4`, 'a 10 s tick gone by'); + assert.equal(await p.ev(`__asked.filter((u) => u.startsWith('/api/view?')).length`), 1, 'a tree is never checked'); + assert.equal((await button()).stale, false); + }); }); test('Shift over an artifact: the overlay, what the lens says is under the pointer, a thread on that spot', { skip: canDrive ? false : 'needs Chrome and a Node with WebSocket', timeout: 240000 }, async (t) => { const undo = []; From 225ebfba7ba9388c901e8f1d5644aaeb7c2c4f40 Mon Sep 17 00:00:00 2001 From: Hugh Francis Date: Sun, 15 Nov 2026 02:13:57 +0000 Subject: [PATCH 08/10] kit-206: task reviews 1-2 and 2 (approve 7f62d29), screenshots Co-Authored-By: Claude Opus 5.5 --- .../reviews/task-1-2.md | 19 ++++++++++++ .../reviews/task-2.md | 27 ++++++++++++++++++ .../screenshots/reload-hint-dark.png | Bin 0 -> 33911 bytes .../screenshots/reload-hint-light.png | Bin 0 -> 33471 bytes 4 files changed, 46 insertions(+) create mode 100644 docs/devteam-dev/tickets/kit-206-viewer-reload-hint/reviews/task-1-2.md create mode 100644 docs/devteam-dev/tickets/kit-206-viewer-reload-hint/reviews/task-2.md create mode 100644 docs/devteam-dev/tickets/kit-206-viewer-reload-hint/screenshots/reload-hint-dark.png create mode 100644 docs/devteam-dev/tickets/kit-206-viewer-reload-hint/screenshots/reload-hint-light.png diff --git a/docs/devteam-dev/tickets/kit-206-viewer-reload-hint/reviews/task-1-2.md b/docs/devteam-dev/tickets/kit-206-viewer-reload-hint/reviews/task-1-2.md new file mode 100644 index 00000000..01f54e3a --- /dev/null +++ b/docs/devteam-dev/tickets/kit-206-viewer-reload-hint/reviews/task-1-2.md @@ -0,0 +1,19 @@ +# Task 1 review, pass 2 + +- Reviewer: task-reviewer 2 (fresh subagent, claude-opus) +- Reviewed: 7f62d296b5cb8f837d8e49828ec14fd719bd10c5 (task 1 fixes) + +**Summary.** A cold, diff-only check of `git diff 21ee88e 7f62d29` against T1-1..T1-3 in `reviews/task-1.md`. In a `git archive` copy of 7f62d29 the new test passes (3/3, about 46 s). Pass 1's two mutations, re-run in scratch copies: + +- **T1-1 holds.** Mutation 1 (no open-time `fpNow: fp, mainFpNow: mainFp`, and an unguarded copy of `fpNow` into `fp` on reload) now fails the viewer-tab subtest ("timed out: the outline, within a poll"). That subtest presses reload after `'v1'` arrives a second time and asserts only boot's `/api/view?` has been asked, so the press is shown to come before the tab's first check; a check that fired early would fail the test, not let it pass. The dashboard comment now says what that press there really shows, and it is accurate. +- **T1-2 holds.** Mutation 2 (the frame reloads itself 1.5 s after the hint) now fails both subtests on the 3 s wait and second "nothing reloaded by itself" check (`1 !== 0`). +- **T1-3** (240000 timeout) was a Suggestion; keeping it, noted in verification.md, is acceptable. + +Nothing else in the diff touches task 1's code. + +| Id | Severity | Rule | Where | Finding | Disposition | +|---|---|---|---|---|---| + +None. + +Verdict: APPROVE 7f62d296b5cb8f837d8e49828ec14fd719bd10c5 diff --git a/docs/devteam-dev/tickets/kit-206-viewer-reload-hint/reviews/task-2.md b/docs/devteam-dev/tickets/kit-206-viewer-reload-hint/reviews/task-2.md new file mode 100644 index 00000000..ef3366f4 --- /dev/null +++ b/docs/devteam-dev/tickets/kit-206-viewer-reload-hint/reviews/task-2.md @@ -0,0 +1,27 @@ +# Task 2 review + +- Reviewer: task-reviewer 2 (fresh subagent, claude-opus) +- Reviewed: 7f62d296b5cb8f837d8e49828ec14fd719bd10c5 (task 2) + +**Summary.** I read the `viewerBoot` change (index.html ~5705-5748), `avFpSeen`/`avHint` (~3586-3599), `showArtifact`, the reload handler, and the new viewer-tab subtest, against plan Task 2, the global constraints and spec-2 N3. + +- **One ask in flight.** `asking` is set before the fetch and cleared in `finally`; a tick returns early while it is set. +- **Abort at 15 s.** An `AbortController` and a 15 s `setTimeout` cover the fetch and `r.json()`; the timer is cleared in `finally`. An abort or error lands in `catch {}` and changes nothing. +- **Stage captured at ask.** `const { slug, at } = TS` is read before the await; `avFpSeen` drops a reply whose `slug`/`at` no longer matches. A viewer tab never re-stages after boot, so the guard is defensive. +- **Tree and preview never polled.** The tick returns for `TS.kind !== 'artifact'` and for `TS.at` starting with `tree:`, before any fetch. +- **Visibility.** The tick returns unless `document.visibilityState === 'visible'`. The poll starts after `await tlRefresh()`, as planned. +- **Forbidden names.** The new code calls only `setInterval(`, `AbortController`, `setTimeout(`, `fetch(`, `r.json(`, `avFpSeen(` and `clearTimeout(`; none match `/\b(route|render|load|loadNow|loadSchemaDocs)\(/`. The "nothing else" allow-list already admits `/api/view?`; the full file passed 155/155 at this head. +- **The comment.** The boot comment names the new read (main or a draft, every 10 s while seen, one describe per ask); the inline comment gives the reason for one-at-a-time and the tree rule. Both match the file's voice and density. + +**Could the subtest pass vacuously?** +- The `stale` wait can only be met by the poll: in the viewer tab nothing else sets `fpNow`, and the frame is shown never to reload by itself. +- The tree check waits for at least four `/api/threads` reads; the threads interval and the poll start in the same task, so a 10 s tick has passed. `__asked` is fresh per document, and `TS.at === 'tree:T'` rules out a fallback to main. Mutation 3 (the poll's `tree:` guard removed) fails with `2 !== 1`. +- One-in-flight, the abort and visibility cannot be driven without a slow describe or a hidden tab; reading the code confirms them, and the plan does not ask for tests of them. +- "No check yet when pressed" depends on frame load plus a press within about 10 s of boot; if it went wrong it would fail (a flake), not pass falsely. It held here under four parallel runs. + +| Id | Severity | Rule | Where | Finding | Disposition | +|---|---|---|---|---|---| + +None. + +Verdict: APPROVE 7f62d296b5cb8f837d8e49828ec14fd719bd10c5 diff --git a/docs/devteam-dev/tickets/kit-206-viewer-reload-hint/screenshots/reload-hint-dark.png b/docs/devteam-dev/tickets/kit-206-viewer-reload-hint/screenshots/reload-hint-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..1e8dad1bc32352b20024d4284e1ec5f342fb92ed GIT binary patch literal 33911 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 literal 0 HcmV?d00001 diff --git a/docs/devteam-dev/tickets/kit-206-viewer-reload-hint/screenshots/reload-hint-light.png b/docs/devteam-dev/tickets/kit-206-viewer-reload-hint/screenshots/reload-hint-light.png new file mode 100644 index 0000000000000000000000000000000000000000..3839539ed56fc0b0eec7fe3703e4548feccc9b1e GIT binary patch literal 33471 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 Date: Sun, 15 Nov 2026 02:14:21 +0000 Subject: [PATCH 09/10] kit-206: verification Co-Authored-By: Claude Opus 5.5 --- .../verification.md | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 docs/devteam-dev/tickets/kit-206-viewer-reload-hint/verification.md diff --git a/docs/devteam-dev/tickets/kit-206-viewer-reload-hint/verification.md b/docs/devteam-dev/tickets/kit-206-viewer-reload-hint/verification.md new file mode 100644 index 00000000..34158f88 --- /dev/null +++ b/docs/devteam-dev/tickets/kit-206-viewer-reload-hint/verification.md @@ -0,0 +1,20 @@ +# kit-206 verification + +At 225ebfba7ba9388c901e8f1d5644aaeb7c2c4f40 (code as at 7f62d29; later commits add records only). `origin/main` merged: already up to date. Browser tests run with Node 22 (`/usr/local/bin/node`); the machine's default Node 18 has no WebSocket and skips them, as CI's Node 18 job does. + +| # | Done-when | Command | Result | +|---|---|---|---| +| 1 | While the viewer is open, the dashboard checks the artifact's fingerprint on its normal poll; on a difference from what the frame loaded, reload gets an orange outline (warning token, light and dark) and a title saying a newer version is ready | `/usr/local/bin/node --test --test-name-pattern "reload button says" test/dashboard.test.mjs` | pass, 3/3 (the test, its dashboard subtest and its viewer-tab subtest): `stale` within one poll after the draft changes, title "A newer version is ready: reload to see it", `outline-color` equal to a probe's computed `var(--amber)` in light and under emulated dark (the two differ), 2px solid. Screenshots: `screenshots/reload-hint-light.png`, `screenshots/reload-hint-dark.png` | +| 2 | Pressing reload loads the new version and clears the outline; nothing reloads by itself | same | pass: the frame's lens still says the old `` when the hint appears and again 3 s later; after the press it says the new one, `stale` is off, title "Load the page again". Mutations (a delayed self-reload; no open-time fpNow with an unguarded copy; no poll; no tree guard) each fail the test (task-1.md, task-1-2.md, task-2.md) | +| 3 | A dashboard test: open a draft, change its file, outline within one poll; press it, outline goes, frame shows the change | same; whole file through the gate: `devteam.mjs gate test --junit reports/junit.xml -- /usr/local/bin/node --test … test/dashboard.test.mjs` | pass; gate passed, 155/155 at 7f62d29 (262 s) | +| 4 | test.yml passes on every job; no file is added | CI (test.yml) at the PR head; `git diff --name-only --diff-filter=A origin/main` | no-file-added: pass, only this ticket's records (spec, plan, reviews, verification, two screenshots); modified: `docs/devteam/dashboard/index.html`, `test/dashboard.test.mjs`. CI: deferred to the PR's checks, every job green at the head before the merge request | + +## Notes + +- The new test's timeout is 240000 ms, not the plan's 180000: two subtests on real 10 s polls (task-1 T1-3). +- `reports/junit.xml` (the gate's output) is untracked and not committed. + +## Rulings + +- m-20261115-013720113-supervisor: spec approved; the stale "never polled" comment on `/api/view` in `devteam.mjs` is left for kit-106 (also in plan.md). +- m-20261115-014304247-supervisor: plan approved; the final review is one fresh subagent (risk normal); a screenshot of the outline in light and dark goes to the supervisor before the merge request. From 8b9d2d4a77b68a7a591f04623d3445f8f047e072 Mon Sep 17 00:00:00 2001 From: Hugh Francis <hugh@sanctuary.computer> Date: Sun, 15 Nov 2026 02:16:37 +0000 Subject: [PATCH 10/10] kit-206: final review 1 (approve 2e02abc) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../reviews/final-1.md | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 docs/devteam-dev/tickets/kit-206-viewer-reload-hint/reviews/final-1.md diff --git a/docs/devteam-dev/tickets/kit-206-viewer-reload-hint/reviews/final-1.md b/docs/devteam-dev/tickets/kit-206-viewer-reload-hint/reviews/final-1.md new file mode 100644 index 00000000..2baa9c70 --- /dev/null +++ b/docs/devteam-dev/tickets/kit-206-viewer-reload-hint/reviews/final-1.md @@ -0,0 +1,25 @@ +# Final review, pass 1 + +- Reviewer: final-reviewer (fresh subagent, claude-opus) +- Reviewed: 2e02abc6542e52b2e1ae3bae370f2a9ee6b26076 (PR #242) + +**Summary.** I read the ticket, spec, plan record, verification, task reviews and PR body, viewed both screenshots, and read the full diff of `docs/devteam/dashboard/index.html` and `test/dashboard.test.mjs` with the code around it: `TS`/`tsSet`, `showArtifact`, `avHint`/`avFpSeen`, `openArtifact`, `closeArtifact`, the `#av-reload` handler, `route`, `loadNow`, `viewerArtifact`/`viewerBoot`, `tArtFpNow` and `tsSubject`. `/usr/local/bin/node --test --test-name-pattern "reload button says" test/dashboard.test.mjs` at this head passed 3/3 (the test and both subtests). + +Done-whens: +1. **The check and the outline: met.** The dashboard takes the fingerprint from the Library rows after each `/api/data` read (the `loadNow` hook). A viewer tab asks `/api/view` every 10 s while visible, for main or a draft only, one ask at a time, aborting after 15 s, dropping a reply when the stage has moved on. The test checks the outline's computed colour equals a probe's `var(--amber)` in light and in emulated dark (the two differ), the 2px width and the title. The screenshots show the outline in both schemes. +2. **Press clears it, nothing reloads by itself: met.** The frame still shows the old `<title>` when the hint appears and again 3 s later; after the press it shows the new one, `stale` is off and the title is back to "Load the page again". The reload handler copies only fingerprints it knows. +3. **The dashboard test: met.** Both hosts, a press before the first check, and a tree never asked about. Per the task reviews, the T1-1 and T1-2 mutations and the removed tree guard each fail it. +4. **CI and no file added: partly evidenced.** No file is added outside the ticket folder; changes are limited to index.html and the test plus records. All 8 test.yml jobs were still pending when I checked; the merge gate covers it. + +Behaviour in context: +- **Thread "changed since" (`tArtFpNow`) and new threads (`tsSubject`).** `TS.fp` stays the fingerprint the frame loaded; before this change it was set only at open (`route()` runs only on the first read and on `hashchange`). "Changed since" works as before, and new threads carry the fingerprint of what is on screen. Reload moves `fp` and `mainFp` forward, matching the frame after the press. +- **Previews, close, switching stages.** Each goes through `tsSet`, which clears both new fields and calls `avHint`. The reload handler leaves previews alone. +- **The owner's framed wrapper.** It shows only trees, so it never polls. In the framed case where a draft is gone (a 404, shown "held"), the tab polls and gets 404s, which change nothing. +- **Code style.** Comments and names match the file. The new poll avoids the names the existing viewer test forbids; that test passed in the whole-file gate run at 7f62d29 and the code is unchanged since. Nothing project-specific. + +| Id | Severity | Rule | Where | Finding | Disposition | +|---|---|---|---|---|---| +| F1 | Minor | spec acceptance check 3 (flake risk) | test/dashboard.test.mjs, viewer-tab subtest, "no check yet when pressed" | The assertion that only boot's `/api/view?` has been asked depends on the frame loading and the press landing within the first 10 s tick after `tlRefresh`. On a very slow runner it could fail: a flake, not a false pass. | accepted: the load and press take about 1 s against a 10 s window, and a failure is loud; if CI shows it, the fix is to press on the first `ready` | +| F2 | Minor | spec Edge cases (ruling m-20261115-013720113) | docs/devteam/scripts/devteam.mjs, the `/api/view` comment | The comment still says `/api/view` is "never polled"; the supervisor ruled to leave it for kit-106, outside this ticket's touches. Noted so it is tracked. | ruled (m-20261115-013720113): left for kit-106; the supervisor tells fast-threads | + +Verdict: APPROVE 2e02abc6542e52b2e1ae3bae370f2a9ee6b26076