` (`Server.vue:2`) that receives `view-container flex-none` by attribute fallthrough from `DashboardRouterView.vue:13`, so a surviving instance should keep the same element — but Server, Web5 and Fleet all share the generic `contentSelector` `.view-container [data-controller-container]`, so a mid-transition moment where two cached views are simultaneously laid out can still let the visible-ancestor filter pick a different view's root. Instrument (1) settles this outright.
+2. A Server-specific runtime error during activation or deactivation tearing down the cached subtree — Server carries seven `useCachedResource` groups plus `armVpnPoll`/`disarmVpnPoll` (`Server.vue:944-949`) and a once-per-session seed in `onMounted` (`Server.vue:963`). A device-only failure would explain why Step A passes while the device does not. Instrument (2) settles this.
+3. `KEEP_ALIVE_MAX = 6` LRU eviction with 10 registered paths. The arithmetic argues against it for an immediate revisit — Vue re-adds the just-activated key as newest and prunes `keys.values().next().value` — but the probe's session visits every registered tab, so confirm rather than assume: instrument (3) plus a run with the cap temporarily raised (do not commit a raised cap; `KEEP_ALIVE_MAX` is a measured value per 02-08's on-device heap reading).
+4. `route.path` differing between visits, which would make `:key="route.path"` and `wrapperFor(route.path)` resolve to a different cache entry each time. `DashboardSidebar.vue:148` links `/dashboard/server` and `Dashboard.vue:230` pushes `{ path: '/dashboard/server', query: {} }` for the mobile swipe target, so this should hold — instrument (3) confirms it from the device instead of from the source.
+5. `include`-name matching. Vue's `matches()` splits each array entry on `,` (`p.split(',').includes(name)`); the wrapper names contain `:` and `/` but no comma, so this should hold identically for all ten paths — nearly free to assert in Step A, and if it fails it fails for every tab, not just Server.
+
+Then write a `## Server KeepAlive Root Cause (gap closure)` section into `02-FINDINGS.md` recording: the named cause in one sentence, the specific observation that proves it, which suspects the data killed and how, and the intended fix. Keep the phase's established candor — if the evidence proves the earlier reading was a probe artifact and Server's instance was surviving all along, say exactly that and show the instance-identity data that proves it. Absence of a reproduction is not proof of survival; only a positive instance-identity reading across the round-trip on the deployed build counts.
+
+Commit and push this task on its own (`git add` by path, Co-Authored-By trailer, `git push gitea-ai main`) before starting Task 2 — the findings-before-fix ordering must be visible in `git log`, exactly as 02-01's gate was.
+
+
+ cd neode-ui && ARCHY_BASE_URL=http://archi-dev-box npx playwright test e2e/perf/keepalive-remount-probe.spec.ts --project=chromium --reporter=line (requires ARCHY_PASSWORD in env; must exit 0 and print a per-path survival line for all 10 KEEP_ALIVE_PATHS entries, including /dashboard/server). If Step A reproduced the fault in jsdom and Step B was therefore not needed for diagnosis, the probe spec is still created and this command still runs — it is Task 2's proof-of-fix instrument and 02-VERIFICATION's re-runnable evidence.
+ grep -q "## Server KeepAlive Root Cause (gap closure)" .planning/phases/02-ui-performance/02-FINDINGS.md && git log --oneline -1 -- .planning/phases/02-ui-performance/02-FINDINGS.md
+
+
02-FINDINGS.md names one measured cause for `/dashboard/server`'s remount, with the observation that proves it and the suspects the data eliminated; `keepalive-remount-probe.spec.ts` exists, runs green against archi-dev-box, and reports instance survival per registered path; the findings commit is pushed and precedes any source change in `git log` (D-10).
+
+
+
+ Task 2: Land the targeted fix, pin it with a regression test, and deploy to archi-dev-box
+ neode-ui/src/views/dashboard/__tests__/keepAliveLifecycle.test.ts, plus exactly the file Task 1's named cause identifies (one of neode-ui/src/views/dashboard/dashboardViewWrappers.ts, neode-ui/src/views/dashboard/keepAliveRoutes.ts, neode-ui/src/views/dashboard/DashboardRouterView.vue, neode-ui/src/views/Server.vue)
+ The fix is confined to the dashboard KeepAlive wiring or to Server.vue; a single `git revert` restores the current behavior, and the deploy is a `--frontend-only` push to one dev node with no schema, no migration and no fleet/OTA exposure (D-15).
+
+ - Test 1 (the gap): routing the real `DashboardRouterView` to `/dashboard/server`, away to `/dashboard/settings`, then back to `/dashboard/server` mounts the real `Server.vue` exactly once — the second arrival reactivates rather than remounts. This test must fail against the pre-fix code (run it before the fix and record that it did).
+ - Test 2 (no collateral damage): the same round-trip through at least two other registered paths keeps their mount counts at 1, so the fix did not trade Server's survival for another tab's.
+ - Test 3 (registration is really the include list): every name in `keepAliveIncludeNames()` is matched by Vue's own `include` semantics for the path it was derived from, and `/dashboard/server`'s wrapper name is among them.
+ - Test 4 (the bound stays bound): the existing LRU-eviction test still passes unchanged — `KEEP_ALIVE_MAX` still evicts, so the fix did not buy instance survival by disabling the memory cap (D-03).
+
+
+Write the tests first, watch Test 1 fail against current code, then implement the smallest change that makes it pass.
+
+Scope the fix to the cause Task 1 named. Do not "harden" adjacent code, do not widen `KEEP_ALIVE_PATHS`, do not raise `KEEP_ALIVE_MAX` (02-08 measured it at 6 from an on-device heap reading), and do not register `/dashboard/settings` (deliberately withheld pending an audit its child sections have not had). If the named cause turns out to sit in `Server.vue` itself, keep the change to the lifecycle/reactivation wiring — the seven `useCachedResource` groups and their TTLs are 02-06's measured tuning and stay as they are.
+
+The visual contract is non-negotiable. `dashboardViewWrappers.ts` and `DashboardRouterView.vue` both carry comments explaining the exact invariants: the transitioning keyed element must be `div.view-wrapper`, it must be the direct child of `.perspective-container`, the per-route padded/full-bleed shapes live inside it, and KeepAlive must never be keyed, `v-if`-toggled or nested under a per-route element. `keepAliveTabs.test.ts` pins those structurally — leave that file untouched and green. If the fix appears to require changing rendered DOM shape, stop and raise it at Task 3's checkpoint instead of shipping a layout change.
+
+Then verify locally and ship it to one node:
+- `npm test` (full vitest suite), `npm run type-check`, and `npm run build` must all be green. The build gotcha from CLAUDE.md applies: after `npm run build`, grep `web/dist/neode-ui` for a string introduced by this change to confirm the build was not a silent no-op.
+- Deploy frontend-only to archi-dev-box and nowhere else, using the same path 02-08 used: `ARCHIPELAGO_TARGET=archipelago@archi-dev-box scripts/deploy-to-target.sh --frontend-only`. No fleet, no OTA, no signed-catalog work (D-15). Confirm the served bundle at `/opt/archipelago/web-ui` — not just the local `web/dist` copy — contains the change.
+- Re-run `keepalive-remount-probe.spec.ts` against the redeployed build and confirm `/dashboard/server` now reports the same instance-survival result as Home/Apps/Marketplace/Cloud/Web5/Fleet.
+- `archy-x250-dev` may still be offline. Check it once, and if it is unreachable record that honestly in the SUMMARY (as 02-08 did) rather than blocking; archi-dev-box is D-11's named target and single-node measurement is acceptable here.
+
+If Task 1's evidence positively proved that Server's instance was surviving all along and the earlier reading was a probe artifact, then there is no source change to make: land Tests 1-4 (they will pass immediately, which is itself the pin), skip the build/deploy step, and record in the SUMMARY exactly which instance-identity observation justified the no-change branch. This branch is only available on positive proof of survival on the deployed build — never on a failure to reproduce.
+
+Append the outcome to the `## Server KeepAlive Root Cause (gap closure)` section: what changed, the pre-fix failing-test observation, and the post-deploy probe reading. Commit and push (Co-Authored-By trailer, `git push gitea-ai main`).
+
+
+ cd neode-ui && npm test 2>&1 | tail -20 && npm run type-check && npm run build
+ cd neode-ui && npx vitest run src/views/dashboard/__tests__/keepAliveLifecycle.test.ts src/views/dashboard/__tests__/keepAliveTabs.test.ts --reporter=verbose
+ cd neode-ui && ARCHY_BASE_URL=http://archi-dev-box npx playwright test e2e/perf/keepalive-remount-probe.spec.ts --project=chromium --reporter=line (ARCHY_PASSWORD in env; /dashboard/server must report instance survival)
+
+ The round-trip test for `/dashboard/server` failed before the change and passes after it; the whole vitest suite, `type-check` and `build` are green; `keepAliveTabs.test.ts` is unmodified and passing; the deployed bundle on archi-dev-box contains the change and the committed probe reports `/dashboard/server` surviving a round-trip like every other registered tab; the work is committed and pushed.
+
+
+
+ Task 3: Confirm on archi-dev-box that Server revisits are instant and nothing visual moved
+
+ Server's KeepAlive instance-caching gap is closed: Task 1 named the measured cause and committed a re-runnable remount probe; Task 2 landed the targeted fix, pinned it with a regression test, and deployed frontend-only to archi-dev-box. This checkpoint is the D-11 pass bar for this one tab, and it also closes the two human-verification items 02-VERIFICATION.md raised for gap 1.
+
+
+ On archi-dev-box (the deployed UI, not the :8100 dev preview), in a fresh browser session:
+
+ 1. Log in and click through to the Network tab (`/dashboard/server`). Let it finish painting.
+ 2. Switch away to Settings, then back to Network. Do this four or five times, watching the Network tab specifically each time it returns.
+ - Expected: content is there the instant the tab returns. No spinner, no blank frame, no visible re-layout. This is D-11's literal pass bar.
+ 3. Before switching away, scroll the Network page down and note where you are, or open one of its expandable cards. Switch away and back.
+ - Expected: your scroll position and the open card are still as you left them — that is what instance survival buys, and it is the observable difference from the previous behavior.
+ 4. Visual invisibility check (this is the regression this machinery caused once before): on Network and on three or four other tabs, confirm page margins/padding look normal (content not pinned to the window edges) and that the slide/depth transition between tabs still animates as it always has.
+ - Expected: indistinguishable from before this plan. Any change here is a failure even if the caching works.
+ 5. Spot-check that Server's data is still fresh, not frozen: leave Network for a minute, come back, and confirm the network/VPN figures update rather than showing stale values forever.
+
+ Type "approved", or describe exactly what you saw (which tab, which step, spinner vs blank vs wrong margins). If step 2 still shows a spinner or blank frame, say so — that means the fix did not land the user-visible outcome even if the probe went green. If you would rather accept the current behavior than take further changes to this machinery, say "accept as-is" and the deviation will be recorded with your rationale for the verifier.
+
+
+
+
+
+## Trust Boundaries
+
+| Boundary | Description |
+|----------|-------------|
+| operator workstation -> archi-dev-box UI login | The real node password crosses this boundary at runtime to drive the probe |
+| build artifact -> `/opt/archipelago/web-ui` on a real node | A frontend bundle is written to a running node's served directory |
+| browser client -> node `/rpc/v1` | Existing boundary; unchanged by this plan (no RPC surface is added or modified) |
+
+## STRIDE Threat Register
+
+| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
+|-----------|----------|-----------|----------|-------------|-----------------|
+| T-02-09-01 | Information Disclosure | `keepalive-remount-probe.spec.ts` login step | high | mitigate | Password read only from `process.env.ARCHY_PASSWORD`; never a default value, never a literal, never written to a file or a planning artifact, never printed by the probe's own logging. The probe's console/pageerror capture prints page-origin messages only — review the transcript before pasting it anywhere. |
+| T-02-09-02 | Information Disclosure | probe transcript / committed artifacts | medium | mitigate | The probe records paths, instance-survival booleans and error text only — no RPC bodies, no tokens, no cookies. Same rule the 02-01 harness already follows. |
+| T-02-09-03 | Tampering | `scripts/deploy-to-target.sh --frontend-only` to archi-dev-box | medium | mitigate | Deploy is frontend-only, to the single named dev node, with no OTA and no fleet/catalog path (D-15). The served bundle is grepped post-deploy to confirm it is the intended build and not a stale or partial copy. |
+| T-02-09-04 | Denial of Service | `KEEP_ALIVE_MAX` / instance cache growth | medium | mitigate | The fix may not buy instance survival by raising or removing the cap; behavior Test 4 keeps the existing LRU-eviction assertion green, so bounded memory on low-power fleet nodes (D-03) is preserved. |
+| T-02-09-05 | Denial of Service | a cached Server instance keeping timers/polls running while off-screen | low | mitigate | `keepAliveLifecycle.test.ts`'s existing assertion that Server's `vpnPollInterval` does not fire while deactivated stays green; any change to Server's lifecycle wiring must keep it so. |
+| T-02-09-SC | Tampering | npm/pip/cargo installs | high | accept | No package-manager install is planned in this plan — no new dependency is required to diagnose or fix a KeepAlive registration issue. If one becomes necessary, halt and route through the Package Legitimacy Gate before installing anything. |
+
+
+
+- The committed probe run against the deployed archi-dev-box build reports instance survival for `/dashboard/server` alongside Home, Apps, Marketplace, Cloud, Web5 and Fleet.
+- `02-FINDINGS.md` contains a named cause with the discriminating evidence, committed before the source change (visible in `git log` ordering).
+- Full vitest suite, `npm run type-check` and `npm run build` are green; `keepAliveTabs.test.ts` is byte-for-byte unmodified.
+- The human checkpoint returned "approved" (or an explicitly recorded "accept as-is" with rationale).
+- Only archi-dev-box received a build; no fleet, no OTA (D-15). archy-x250-dev's reachability is recorded honestly either way.
+
+
+
+- Verification gap 1 is closed: `/dashboard/server` either survives a tab round-trip on real hardware, or is proven by positive instance-identity evidence to have been surviving all along — with the cause named either way.
+- A regression test in `keepAliveLifecycle.test.ts` fails if Server stops surviving a round-trip.
+- The remount evidence is re-runnable from a committed spec instead of an ad-hoc session.
+- Nothing visual changed: margins, transitions and animations are indistinguishable from before, confirmed by both the untouched structural tests and the human pass.
+
+
+
+
+
diff --git a/.planning/phases/02-ui-performance/02-10-PLAN.md b/.planning/phases/02-ui-performance/02-10-PLAN.md
new file mode 100644
index 00000000..f74663ea
--- /dev/null
+++ b/.planning/phases/02-ui-performance/02-10-PLAN.md
@@ -0,0 +1,178 @@
+---
+phase: 02-ui-performance
+plan: 10
+type: execute
+wave: 7
+depends_on: ["02-09"]
+files_modified:
+ - .planning/phases/02-ui-performance/02-PERF-REMEASURE.json
+ - .planning/phases/02-ui-performance/02-FINDINGS.md
+autonomous: true
+gap_closure: true
+requirements: [PERF-02, PERF-03]
+user_setup:
+ - service: archi-dev-box (the node's own UI login)
+ why: "The perf harness drives a real authenticated browser session against the deployed build on archi-dev-box — D-11's named on-device verification target, and the same target both prior measurement runs used. The password is not derivable from this environment."
+ env_vars:
+ - name: ARCHY_PASSWORD
+ source: "Supplied out-of-band by the coordinator at runtime, exactly as in 02-01 and 02-08. Passed only as an environment variable on the harness command line — never written to a file, never committed, never echoed into a log or a planning artifact."
+
+must_haves:
+ truths:
+ - "Each of the six surfaces 02-VERIFICATION.md named (Discover, Server, Web5, AppDetails, OpenWrtGateway, wallet/send-flow) carries a written verdict backed by a three-artifact comparison — either cleared as environmental noise with the dispersion data that clears it, or confirmed as a real regression with its cause named"
+ - "The environmental-noise theory 02-FINDINGS.md flagged but never tested is settled with data, not left as a caveat: the run conditions (clock time, disk usage, system load, whether the box was building) are recorded next to the numbers for the new run and stated for the two prior runs"
+ - "No surface is left with an unexplained regression: each confirmed regression is either fixed (only when its cause traces to a phase-2 change and the fix is small) or carries an explicit accepted deviation with a rationale written for the verifier to act on"
+ - "The three measurement artifacts stay directly comparable because the 02-01 harness is byte-for-byte unmodified across this re-run (D-10's instrument stays frozen)"
+ artifacts:
+ - ".planning/phases/02-ui-performance/02-PERF-REMEASURE.json — third measurement of all 15 surfaces against archi-dev-box under recorded current conditions"
+ - ".planning/phases/02-ui-performance/02-FINDINGS.md — a `## Re-measurement (gap closure)` section with the three-way table, per-surface verdicts, and the accepted-deviation list"
+ key_links:
+ - "02-PERF-REMEASURE.json -> 02-PERF-BASELINE.json / 02-PERF-AFTER.json — joined by surface `id`, compared on `revisitMs`, `revisitRpcCount` and per-run `samples` spread"
+ - "Each verdict in 02-FINDINGS.md -> the specific artifact field that supports it — the citation discipline 02-01 established for this doc"
+---
+
+
+Close verification gap 2: six surfaces measured unimproved or regressed on revisit time in 02-PERF-AFTER.json — Discover (1083 -> 1257ms), Server (738 -> 849ms), Web5 (566 -> 709ms), AppDetails (1204 -> 1510ms), OpenWrtGateway (663.5 -> 1148ms) and wallet/send-flow (2607 -> 2556ms, essentially unchanged) — contradicting 02-08's own must-have. 02-FINDINGS.md flagged an environmental-noise theory for exactly these rows (baseline taken 10:30 local, the after-run at 01:27 the next day on the same multi-service node that was at 85% disk and doubles as the build server) but explicitly did not resolve it. Conditions have since changed materially: roughly 118G was freed on this box, so the disk-pressure component of that theory is no longer present in the same form.
+
+Purpose: PERF-02 and PERF-03 are both marked partial in 02-VERIFICATION.md solely because these rows have no verdict. A measurement with an untested confound is not evidence either way — this plan produces the third data point that turns "regressed or noisy, unresolved" into a per-surface answer. D-10's discipline is unchanged: name the cause from measurement before touching code.
+
+Output: a third harness run (02-PERF-REMEASURE.json) taken with the instrument unmodified and the run conditions recorded, a three-way comparison that reports dispersion rather than bare medians, and a written verdict per named surface — cleared, fixed, or explicitly accepted with rationale.
+
+Coverage: this plan covers verification gap 2 and both human-verification item 2 and the timing half of PERF-02/PERF-03. Gap 1 (Server's remount) is covered by 02-09-PLAN.md; the full multi-source coverage audit for this gap-closure set lives in 02-09-PLAN.md's `` section.
+
+Constraints: the 02-01 harness (`neode-ui/e2e/perf/{surfaces,measure,surface-perf.spec}.ts`) is frozen — changing it would break comparability with both committed artifacts and invalidate the whole exercise. Deploy only to archi-dev-box, frontend-only, and only if a fix actually lands (D-15 — no fleet, no OTA). No new features. No visual or animation changes.
+
+
+
+@$HOME/.claude/gsd-core/workflows/execute-plan.md
+@$HOME/.claude/gsd-core/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/STATE.md
+@CLAUDE.md
+
+@.planning/phases/02-ui-performance/02-CONTEXT.md
+@.planning/phases/02-ui-performance/02-VERIFICATION.md
+
+Read only these parts of the large phase artifacts:
+- `.planning/phases/02-ui-performance/02-FINDINGS.md` — `## Method` (the exact run command, sample semantics and field meanings), the `## Results` table, and `## Outstanding` (the timing-variance caveat is the last-but-one bullet). Skip the `## Addendum`, which is about a different, already-closed issue.
+- `.planning/phases/02-ui-performance/02-09-SUMMARY.md` — what changed in the build this run measures.
+- `neode-ui/e2e/perf/surfaces.ts` — surface ids, `navSteps` and the transit-confound notes for `openwrt-gateway`, `marketplace` and `marketplace-app-details`.
+
+**Do not read `02-PERF-BASELINE.json`, `02-PERF-AFTER.json` or `02-PERF-REMEASURE.json` into context** — they are 50-60KB each and reading all three would consume most of this plan's budget for no benefit. Extract the fields you need with `node -e` or `jq` and work from the extracted table.
+
+
+
+
+
+ Task 1: Re-run the frozen harness against archi-dev-box under recorded conditions
+ .planning/phases/02-ui-performance/02-PERF-REMEASURE.json
+ `ARCHY_PASSWORD` is exported in the environment, `curl -sS -o /dev/null -w '%{http_code}' http://archi-dev-box/` returns a 2xx/3xx, and the bundle served from `/opt/archipelago/web-ui` is the one 02-09 deployed. If the password is unset, stop and report it — do not substitute a local dev server, the mock backend or the `:8100` preview, none of which can answer a question about real-node timing.
+
+ `.planning/phases/02-ui-performance/02-FINDINGS.md` `## Method` — the exact command, the `runs`/median/`samples` semantics, and the `firstVisitMs`/`revisitMs`/`revisitRpcCount`/`maxConcurrentRpc`/`remounted` field definitions this run must reproduce identically. `neode-ui/e2e/perf/surface-perf.spec.ts` and `measure.ts` — read to confirm the env-var contract (`ARCHY_BASE_URL`, `ARCHY_PERF_OUT`, `ARCHY_PERF_RUNS`), not to change them.
+
+
+Produce the third measurement artifact with the instrument untouched.
+
+Before running, confirm the harness is unmodified since the after-run: `git diff --stat 3ee20430 -- neode-ui/e2e/perf/` must be empty. If it is not, stop — a modified instrument makes the three artifacts incomparable and this entire plan pointless. (`keepalive-remount-probe.spec.ts`, added by 02-09, is a separate spec and does not count as a modification of the three harness files; confirm the diff is limited to that new file if anything shows.)
+
+Record the run conditions BEFORE and AFTER the run, into the SUMMARY and into Task 2's findings section — these are the variables the noise theory turns on, and 02-08 recorded none of them: wall-clock local time at start and end, `df -h /` (disk usage — roughly 118G was freed since the after-run, so this number should differ materially from the 85% recorded then), `uptime` load averages, and whether any build, test run or container churn was active on the box during the window. The point is that this run's conditions are stated rather than inferred later.
+
+Prefer a start time close to the baseline's 10:30 local window if the schedule allows — that is the controlled comparison 02-VERIFICATION.md's human-verification item 2 asks for. If the run must happen at a different hour, do not skip it and do not pretend the hour matches: record the actual time and compensate for the uncontrolled variable by sampling more, per the next paragraph.
+
+Run with `ARCHY_PERF_RUNS=5` rather than the default 3. This changes no harness code — it is the harness's own env knob — and the reported `revisitMs`/`firstVisitMs` stay medians, so they remain directly comparable to both prior artifacts. The extra samples exist so Task 2 can compute a real spread per surface instead of comparing three bare medians, which is the only way to separate a genuine regression from run-to-run variance.
+
+Command shape (password supplied from the environment, never inline in a committed file or a transcript): `cd neode-ui && ARCHY_BASE_URL=http://archi-dev-box ARCHY_PERF_RUNS=5 ARCHY_PERF_OUT=../.planning/phases/02-ui-performance/02-PERF-REMEASURE.json npx playwright test e2e/perf/surface-perf.spec.ts --project=chromium --reporter=line`, with `ARCHY_PASSWORD` already exported.
+
+Expect 15 rows. Mesh and Chat may again come back `unmeasured` — record whichever reason applies rather than presenting them as anything else, exactly as both prior runs did. Note one changed condition worth checking: the after-run's Chat block was the disk-usage toast from `HealthNotifications.vue` intercepting the close button, which was a symptom of the 85% disk; with the space freed, that block may simply be gone. If Chat measures this time, record it as a bonus data point and say plainly that it has no baseline-comparable counterpart.
+
+If a surface errors or the run aborts, re-run once and keep both transcripts; do not hand-edit the artifact. Commit and push the artifact plus the recorded conditions (`git add` by path, Co-Authored-By trailer, `git push gitea-ai main`).
+
+
+ test -f .planning/phases/02-ui-performance/02-PERF-REMEASURE.json && node -e "const r=require('./.planning/phases/02-ui-performance/02-PERF-REMEASURE.json'); const rows=r.surfaces||r.results||r; console.log('rows:', Array.isArray(rows)?rows.length:Object.keys(rows).length); console.log('baseUrl:', r.baseUrl, 'runs:', r.runs, 'commit:', r.commit)"
+ git diff --stat 3ee20430 -- neode-ui/e2e/perf/surfaces.ts neode-ui/e2e/perf/measure.ts neode-ui/e2e/perf/surface-perf.spec.ts (must print nothing — the instrument is frozen)
+
+ `02-PERF-REMEASURE.json` exists with 15 rows, a `baseUrl` of `http://archi-dev-box` and `runs: 5`; the three harness files are provably unmodified since the after-run commit; the run's clock time, disk usage, load and concurrent-activity state are recorded; the artifact is committed and pushed.
+
+
+
+ Task 2: Three-way comparison, a verdict per named surface, and the resulting action
+ .planning/phases/02-ui-performance/02-FINDINGS.md
+
+Turn three artifacts into one answer per surface.
+
+Build the comparison table with a script, not by reading JSON into context. Extract, for every surface id present in all three artifacts, from each of `02-PERF-BASELINE.json`, `02-PERF-AFTER.json` and `02-PERF-REMEASURE.json`: `firstVisitMs`, `revisitMs`, `revisitRpcCount`, `remounted`, and — this is the part 02-08 never used — the per-run values inside `samples`, reduced to min/median/max. The baseline and after artifacts already contain their own `samples` arrays (02-FINDINGS.md `## Method`: "`samples` holds every individual run so no data is discarded"), so dispersion is computable for all three runs from what is already committed, with no re-run of history required. A one-off `node -e` script that prints a markdown table is the right tool; keep it in the transcript rather than committing a script file.
+
+Then decide each of the six surfaces 02-VERIFICATION.md named — Discover, Server, Web5, AppDetails, OpenWrtGateway, wallet/send-flow — against this rule, and state which branch the data put it in:
+
+- **Cleared as environmental noise** when the baseline, after and re-measure sample ranges overlap materially, or when the re-measure returns to or below the baseline median. Cite the actual ranges. "Overlapping spread" is a claim the numbers must show, not an assertion.
+- **Confirmed regression** when the re-measure reproduces the after-run's elevated revisit time outside the baseline's sample spread. Then name the cause: identify the responsible mechanism the way 02-08 root-caused the Cloud connection-pool starvation — direct instrumentation on the device, `git log` bisection against the pre-phase-2 baseline commit `a75b6709`, and the surface's own RPC/remount fields — not by inspecting code and guessing. State plainly whether the cause is a phase-2 change or pre-existing.
+
+Apply the confounds already documented rather than re-deriving them: OpenWrtGateway's and Discover's RPC counts bleed through from the tab their `navSteps` transit (Server and Apps respectively), so treat their RPC columns as unreliable and reason from `revisitMs` and `remounted`; wallet/send-flow is a modal that remounts by design via `BaseModal`'s `v-if`, was never in any plan's `files_modified`, and its anomaly (revisit slower than first visit, zero RPC either way) is pure client-side cost — if it is confirmed as a real, still-present cost, name the mechanism.
+
+Act on each verdict, in bounds:
+- Cleared -> no code change. Write the verdict and the data.
+- Confirmed AND caused by a phase-2 change AND fixable small -> fix it. "Small" means: confined to one file, covered by a vitest assertion that fails without it, and followed by `npm test` + `npm run type-check` + `npm run build` green, an `ARCHIPELAGO_TARGET=archipelago@archi-dev-box scripts/deploy-to-target.sh --frontend-only` deploy (D-15, archi-dev-box only), and a targeted re-run of the harness for the affected surface(s) proving the number moved. No visual or animation change is permitted as part of any such fix.
+- Confirmed but caused by something pre-existing, or fixable only by a change that fails any of those bounds -> record an explicit accepted deviation. Write it for the verifier to act on: which surface, the measured numbers across all three runs, the named cause, why it is out of this phase's bounds, and where it belongs (a specific requirement such as UIFIX-06, or a named later phase). An accepted deviation with a named cause and a destination is a resolution; a shrug is not.
+
+Write all of it into a `## Re-measurement (gap closure)` section in `02-FINDINGS.md`: the run header and recorded conditions from Task 1, the three-way table with dispersion, a verdict line per named surface, and a short `### Accepted deviations` subsection listing anything not fixed. Keep the doc's citation discipline — every verdict points at the field that supports it. Keep the doc's candor: if the re-measure shows the after-run was mostly environmental and the architecture is fine, say so and show it; if it shows real regressions the phase shipped, say that just as plainly.
+
+Finally, reconcile the record: if the verdicts change what PERF-02/PERF-03 can honestly claim, update the status note for those rows in `.planning/REQUIREMENTS.md`'s coverage table to point at this section, so the next verification pass reads the resolved state rather than the old partial one. Commit and push (Co-Authored-By trailer, `git push gitea-ai main`).
+
+
+ grep -q "## Re-measurement (gap closure)" .planning/phases/02-ui-performance/02-FINDINGS.md && for s in Discover Server Web5 AppDetails OpenWrtGateway "send flow"; do grep -q "$s" .planning/phases/02-ui-performance/02-FINDINGS.md && echo "present: $s"; done
+ cd neode-ui && npm test 2>&1 | tail -10 && npm run type-check (required whether or not a fix landed — the suite must stay green)
+
+ `02-FINDINGS.md` has a `## Re-measurement (gap closure)` section containing the recorded run conditions, a three-way table with per-run spread, and one verdict per named surface — each either cleared with the overlapping-range data that clears it, fixed with the change and its proof, or listed under `### Accepted deviations` with a named cause and a destination. The vitest suite and type-check are green. Any fix that landed was deployed to archi-dev-box only and re-measured. Work is committed and pushed.
+
+
+
+
+
+## Trust Boundaries
+
+| Boundary | Description |
+|----------|-------------|
+| operator workstation -> archi-dev-box UI login | The real node password crosses this boundary at runtime to drive the harness |
+| harness -> committed measurement artifact | Observed traffic metadata is written into a repository file |
+| build artifact -> `/opt/archipelago/web-ui` on a real node | Only if a confirmed-regression fix lands |
+
+## STRIDE Threat Register
+
+| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
+|-----------|----------|-----------|----------|-------------|-----------------|
+| T-02-10-01 | Information Disclosure | harness login (`ARCHY_PASSWORD`) | high | mitigate | Password read only from the environment at runtime; never inlined into a command that gets pasted into a committed file, never written to `02-PERF-REMEASURE.json`, the findings doc or the SUMMARY. The same handling 02-01 and 02-08 used. |
+| T-02-10-02 | Information Disclosure | `02-PERF-REMEASURE.json` contents | medium | mitigate | The harness records RPC method names and timings only, never request or response bodies — its original threat mitigation, preserved because the harness is unmodified. Spot-check the artifact for anything resembling a token, credential or personal path before committing. |
+| T-02-10-03 | Tampering | frozen harness files | medium | mitigate | Task 1 gates on `git diff --stat` over the three harness files being empty; a modified instrument silently invalidates every comparison in this plan, so it is checked rather than assumed. |
+| T-02-10-04 | Tampering | `scripts/deploy-to-target.sh --frontend-only` (conditional) | medium | mitigate | Only triggered by a confirmed, phase-2-caused, small fix; frontend-only, archi-dev-box only, no OTA and no fleet/catalog path (D-15); served bundle grepped post-deploy per CLAUDE.md's silent-no-op-build gotcha. |
+| T-02-10-05 | Repudiation | measurement provenance | low | mitigate | The artifact header carries `baseUrl`, `commit` and `runs`, and Task 1 additionally records clock time, disk usage and load — so a later reader can tell which build and which machine state produced each number instead of trusting a verdict's summary of them. |
+| T-02-10-SC | Tampering | npm/pip/cargo installs | high | accept | No package-manager install is planned — re-running an existing harness and comparing committed JSON needs no new dependency. If one becomes necessary, halt and route through the Package Legitimacy Gate before installing anything. |
+
+
+
+- `02-PERF-REMEASURE.json` exists, 15 rows, `baseUrl: http://archi-dev-box`, `runs: 5`, produced by a provably unmodified harness.
+- Every one of the six surfaces named in 02-VERIFICATION.md gap 2 has a verdict in `02-FINDINGS.md` backed by cited artifact fields including per-run spread.
+- The environmental-noise theory is either supported or refuted by stated data; the run conditions for the new run are recorded and the prior two runs' known conditions are restated for comparison.
+- Anything not fixed appears under `### Accepted deviations` with a named cause and a destination requirement or phase.
+- Full vitest suite and type-check green; any fix deployed to archi-dev-box only (D-15).
+
+
+If a fix landed as part of Task 2, the D-11 pass bar applies to the affected surface: on archi-dev-box, revisit that surface after having visited it once in the session and confirm content paints immediately with no spinner or blank frame, and that nothing about its layout, margins or transition animation changed.
+
+
+
+
+- Verification gap 2 is closed: no surface is left in the unexplained "regressed, cause unknown, confound untested" state that blocked the phase's clean pass.
+- The noise-versus-regression question is answered with dispersion data from three runs, not asserted from three medians.
+- Real regressions traceable to phase 2 and cheap to fix are fixed and re-measured; everything else is an explicit, actionable accepted deviation.
+- The instrument stayed frozen, so all three artifacts remain a comparable series for any future run.
+
+
+
+
+