Skip to content

feat(ui): reload prompt when a new server version is deployed - #615

Merged
dborup merged 2 commits into
masterfrom
feat-614-new-version-reload-prompt
Oct 26, 2026
Merged

dborup merged 2 commits into
masterfrom
feat-614-new-version-reload-prompt

Conversation

@dborup-agent

Copy link
Copy Markdown
Collaborator

Relates to #614

Plan / what changed

A tab that stays open across a deploy keeps running old JavaScript. This adds a small, non-blocking "New version available – reload" notice.

  • public/nav-drawer.js: the single /api/health reader is extracted as window.CoreHealth.read and reused by the drawer version footer. There is no second fetch site.
  • public/version-notice.js (new): pure decide(state, health) and shouldCheck(now, last, gap), plus the wiring.
    • The baseline commit is read once at load, so nothing is shown on first load.
    • Checks run on visibilitychange (visible only), on WebSocket open and on a 5 minute interval for visible tabs. All are throttled to at least 30 s apart.
    • Null, 5xx, commit-less and unknown health responses are ignored (restart window).
    • The notice shows once per new commit. Dismiss is respected. It never auto-reloads.
  • public/app.js: dispatches corescope:ws-open when the shared socket opens.
  • public/style.css: banner uses CSS variables only.
  • Banner is built with createElement/textContent, has role="status", and its buttons are keyboard reachable.

Performance

At most one small /api/health request per 30 s, only from existing events or a 5 minute timer. No per-packet or per-render work.

Customizer (rule 8)

MIN_GAP_MS (30 s) and INTERVAL_MS (5 min) are hardcoded. They are candidates for the customizer later.

Tests

  • test-issue-614-version-notice.js (vm.createContext): decision logic, throttle, wiring, banner and Reload/Dismiss. Added to test-all.sh.
  • test-issue-614-version-notice-e2e.js (Playwright): stubs /api/health, fires visibilitychange, and asserts the banner, Reload via keyboard, and Dismiss. Runs in light, dark and 390px. Registered in scripts/e2e-shards.json.
  • Existing test-issue-111-drawer-version.js is unchanged and passes.

Not touched: cmd/ (server stays read-only, no new map[string]interface{}), workflows (fork guards unchanged).

🤖 Generated with Claude Code

dborup and others added 2 commits October 26, 2026 04:54
Relates to #614. An open tab remembers the server commit read from
/api/health at load and shows a non-blocking role=status banner with
Reload / Dismiss when a later reading reports a different commit.

- nav-drawer.js: extract the single /api/health reader as
  window.CoreHealth.read, reused by the drawer version footer
- version-notice.js: pure decide/shouldCheck plus wiring; checks on
  visibilitychange, WebSocket open and a 5 minute interval, throttled
- app.js: dispatch corescope:ws-open when the shared socket opens
- failed/5xx/commit-less health is ignored; once per new commit; never
  auto-reloads

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…n notice

Relates to #614. The version notice reads /api/health at load as its
baseline; the drawer footer now reuses that first reading (readOnce)
instead of issuing its own request, so opening the drawer still adds no
request. Update the #111 drawer E2E to allow the single shared
page-load request.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@dborup-agent

Copy link
Copy Markdown
Collaborator Author

Rapport — CS-pve-agent2 PR#615 #614 — head 69107c6

Status: Draft PR, all CI jobs green on the final head. Browser-checked in light, dark and 390px; no remaining open items.

Requirements, tests and mutants

Requirement Test (red before, green after) Mutant (caught)
Notice only when the commit changes; none on first load; same commit quiet unit decide cases [T] drop the notified check; baseline never set (both caught) [T]
Once per new commit; later commit notifies again; a second commit does not stack banners unit + wiring [T], E2E "one banner", "later commit" [T] remove the banner-exists guard (caught) [T]
Failed / 5xx / commit-less / unknown health ignored unit (null, undefined, {}, unknown, blank, non-string) + wiring + E2E 503 [T] remove the unknown/empty guard (caught) [T]
Checks only on visibilitychange (visible), WS open, throttled interval unit wiring + shouldCheck [T] always-true throttle; fire while hidden; event name changed; 1 s interval (all caught) [T]
Never auto-reloads; Reload reloads; Dismiss hides unit + E2E (no navigation, Enter on Reload navigates, Dismiss) [T] no-op reload; banner not removed on dismiss (caught) [T]
Reuse the /api/health reading, no duplicate fetch unit asserts one fetch site and zero in version-notice.js; readOnce shares one request [T] readOnce without memo (caught) [T]
Accessible, CSS variables only, no HTML strings role=status, focusable buttons, zero innerHTML writes [T]; XSS gate clean [T] remove role=status (caught) [T]
Light, dark, 390px Playwright in all three plus screenshots inspected [T][A] n/a

The E2E is also red against master's public/ (it times out waiting for the feature) [T].

Regression found and fixed

The first push failed the #111 drawer E2E, which required zero /api/health requests before the drawer opens. The drawer now reuses the single page-load reading (readOnce), and that E2E allows the one shared request. It is not a flaky test [T].

CI (head 69107c6)

All jobs pass: Go Build & Test, Go Ingestor, Go Libraries & CLI, Go Server 1/3–3/3, JS Unit & Lint, Script & Container Unit Checks, Playwright 1/3–3/3, Verify E2E Shards & Coverage, Verify Go Test Assignments, Validate Docker Image, Build & Publish Docker Image. Release, Deploy Staging and Badges are skipped as usual. The earlier failure was on the previous head (the #111 E2E above). The known flaky #271 did not occur [T].

Local runs [T]

Guards [T]

  • No changes under cmd/ or .github/, so cmd/server stays read-only, there are no new map[string]interface{}, and the fork guards are unchanged.
  • scripts/check-xss-sinks.sh --diff origin/master is clean.
  • No hardcoded colors; the banner uses theme variables, including --shadow.

Remaining

  • The check gap (30 s) and interval (5 min) are hardcoded and noted as customizer candidates [K].
  • The 390px banner wraps to two lines; it is readable and inside the viewport [A].

Evidence: [T] test or CI output, [A] visual assertion or inspection, [K] known limitation or decision.

@adminopenclaw8-sketch

Copy link
Copy Markdown
Collaborator

Review — CS-Macmini PR#615 — head 69107c6

Dom: APPROVE med nits

Findings

# Severity File:Line Summary
N1 nit public/version-notice.js:157-158,177-179 A single 30s floor (MIN_GAP_MS) throttles every trigger — visibilitychange and WS-open included, not only the periodic timer — which is stricter than the issue's wording ("no extra polling load... at most once every few minutes otherwise") but still satisfies "no polling storm"; a deliberate, defensible choice, not a defect.
N2 nit public/version-notice.js:224 The check(baseline) parameter name doubles as "is this the baseline call" boolean and reads confusingly next to state.baseline. Cosmetic only.
N3 known limitation public/version-notice.js:157-158 MIN_GAP_MS/INTERVAL_MS are hardcoded, as the issue itself allows ("can be hardcoded at first... note for the customizer"). [K]

No correctness, security or scope findings survived review.

Answers to the review points

  • Reuses /api/health, no duplicate fetch: nav-drawer.js now exposes window.CoreHealth = {read, readOnce, field}; readHealthOnce() memoizes the first fetch('/api/health') promise (failures included) and is used both for the notice's baseline and the drawer footer. version-notice.js itself contains zero fetch( calls — verified by grep and by the shipped unit test. [T]
  • Checks only on visibilitychange / WS reconnect / bounded interval: start() wires exactly three triggers — document.visibilitychange (only acts when !document.hidden), window.corescope:ws-open (dispatched from app.js's sock.onopen, which now fires on every connect/reconnect), and setInterval(..., 5 min) (skips hidden tabs). All three funnel through check(false), gated by a 30s shouldCheck floor. No per-render or per-packet hook exists anywhere in the diff. [T][A]
  • Never auto-reloads; once per new commit; ignores failed/5xx/commit-less health; no notice on first load: all encoded in the pure decide(state, health) function and independently covered by 16 unit cases plus the E2E (503-during-restart, dismiss/renotify, no-navigation assertions). Verified green on the merged tree and red under two of my own mutants (below). [T]
  • CSS variables only, accessible, light/dark/390px: .version-notice* rules in style.css use only var(--surface|--text|--border|--shadow|--accent-strong|--text-on-accent|--z-popover|--bottom-nav-reserve), all defined in both the light and dark blocks — confirmed by grep. role="status" is set, buttons are real <button type="button"> elements (native keyboard/focus support, :focus-visible outline added), and nothing is built via innerHTML/HTML strings — confirmed by the unit test's "no HTML writes" assertion and by a repo-wide check-xss-sinks.sh --diff run (clean). Visually confirmed with my own screenshots at light/1280, dark/1280 and light/390 — banner renders correctly, themed, not clipped, and sits above the bottom-nav bar; at 390px the message and the two buttons wrap onto a second row rather than overlapping anything. [T][A]
  • Pure function with vm.createContext unit tests, meaningful Playwright test: test-issue-614-version-notice.js loads the real public/version-notice.js (and nav-drawer.js, for the shared reader) via vm.runInContext — real shipped code, not a reimplementation. test-issue-614-version-notice-e2e.js stubs /api/health via Playwright route interception, drives visibilitychange, and asserts banner appearance/role/no-auto-navigation/Reload-navigates/Dismiss, across light, dark and 390px. Both ran clean against a freshly built local server. [T]

Tests and mutants

All run locally against the merge of 69107c67 onto current origin/master (3cb770e6), in isolated worktrees/archives that did not touch any other agent's checkout:

  • go test ./... in cmd/server — pass (71s). go test ./... in cmd/ingestor — pass (114s). No Go source is touched by this PR; both ran for regression coverage only. [T]
  • sh test-all.sh — 274/274 pass. [T]
  • node test-frontend-helpers.js — 731/731 pass. [T]
  • scripts/check-xss-sinks.sh --diff origin/master run against the real PR branch (in an isolated detached worktree, not the shared checkout) — clean, exit 0. [T]
  • Built corescope-server/corescope-migrate from the PR head, prepared test-fixtures/e2e-fixture.db exactly as CI does (freshen → Packets page collapse button in the left column of the table opens the dialog. Kpa-clawbot/CoreScope#1486/"Group Data" message type missing from packet view window's "message type" filter Kpa-clawbot/CoreScope#1791 seed SQL → corescope-migrate), served it on a free local port (13810/13811, never used by other agents):
    • test-issue-614-version-notice.js (vm-based unit/wiring suite) — 31/31 pass.
    • test-issue-614-version-notice-e2e.js — 33/33 pass across light/dark/390px.
    • test-issue-111-drawer-version-e2e.js (affected by the shared-reader refactor) — 7/7 pass, including the updated "≤1 request" assertions.
  • My own mutants, applied to public/version-notice.js in the isolated worktree and reverted after (file verified byte-identical afterward):
    1. Dropped the commit.toLowerCase() === 'unknown' guard → 2 of the vm-unit tests failed ("health an unknown commit…" cases). Caught.
    2. Made shouldCheck always return true (disabled the throttle) → 2 tests failed, including "checks inside the throttle gap are skipped". Caught.
  • Confirmed no cmd/ or .github/ files are touched, no new map[string]interface{} anywhere in the diff, and TestForkGuardCount (part of the cmd/server suite above) passed unchanged. Commit authorship on both commits (22a299cc, 69107c67) is dborup <kontakt@meshview.dk>. No closing keywords in either commit message. git ls-remote on the head ref was identical before and after this review (69107c67...). [T]

Not verified

  • A real server restart / WebSocket-drop-and-reconnect cycle end-to-end (I verified the corescope:ws-open wiring at the unit level and that app.js dispatches it from sock.onopen, but did not kill-and-restart a live server mid-session to watch an actual reconnect trigger the check).
  • Screen-reader behavior of role="status" (only structural/attribute checks, not an actual AT pass).
  • Manual mutation testing beyond the two mutants above (I did not run the author's own mutant set, only cross-checked their claims against the actual test output).
  • Server-side code (none changed by this PR).

@dborup
dborup marked this pull request as ready for review October 26, 2026 06:08
@dborup
dborup merged commit 400d364 into master Oct 26, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants