Files
archy/.planning/phases/02-ui-performance/02-09-PLAN.md
T

30 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, gap_closure, requirements, user_setup, must_haves
phase plan type wave depends_on files_modified autonomous gap_closure requirements user_setup must_haves
02-ui-performance 09 execute 6
02-08
neode-ui/e2e/perf/keepalive-remount-probe.spec.ts
neode-ui/src/views/dashboard/__tests__/keepAliveLifecycle.test.ts
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
.planning/phases/02-ui-performance/02-FINDINGS.md
false true
PERF-02
service why env_vars
archi-dev-box (the node's own UI login) The remount probe drives a real authenticated browser session against the deployed build on archi-dev-box — D-11's named on-device verification target. The password is not derivable from this environment (same gate 02-01 and 02-08 hit).
name source
ARCHY_PASSWORD Supplied out-of-band by the coordinator at runtime, exactly as in 02-08. Passed only as an environment variable to the probe command — never written to a file, never committed, never echoed into a log or a planning artifact.
truths artifacts key_links
A tab round-trip away from and back to /dashboard/server on archi-dev-box leaves the Server component instance alive: the corrected visible-ancestor remount probe reports the same instance for /dashboard/server as it does for Home, Apps, Marketplace, Cloud, Web5 and Fleet
The reason /dashboard/server behaved differently from every other registered KEEP_ALIVE_PATHS tab is named in 02-FINDINGS.md with the discriminating evidence that proves it — a measured cause, not a guess (D-10's measure-before-fix discipline applies to this gap the same way it applied to the phase)
keepAliveLifecycle.test.ts fails if /dashboard/server stops surviving a round-trip through the real DashboardRouterView — the regression is pinned, not just fixed once
The remount evidence is re-runnable by anyone: the corrected probe 02-08 ran ad hoc is a committed spec file, not a method description in a summary
Nothing visual changed anywhere in the dashboard: page margins, the slide/depth route transitions and every animation render exactly as they did before this plan (D-01's caching must stay invisible)
neode-ui/e2e/perf/keepalive-remount-probe.spec.ts — committed, re-runnable corrected remount probe covering every KEEP_ALIVE_PATHS tab
neode-ui/src/views/dashboard/__tests__/keepAliveLifecycle.test.ts — regression test pinning Server's round-trip survival through the real router + DashboardRouterView
.planning/phases/02-ui-performance/02-FINDINGS.md — a `## Server KeepAlive Root Cause (gap closure)` section naming the cause, the evidence that discriminated it, and the fix
DashboardRouterView.vue `:include=keepAliveIncludes` -> dashboardViewWrappers.ts `keepAliveIncludeNames()` -> keepAliveRoutes.ts `KEEP_ALIVE_PATHS` — the registration chain that is supposed to cover /dashboard/server and demonstrably did not deliver for it
The committed probe -> the build actually served from /opt/archipelago/web-ui on archi-dev-box — the probe must exercise the deployed bundle, not a local dev server
Close verification gap 1: `/dashboard/server` has been registered in `KEEP_ALIVE_PATHS` since 02-04, yet 02-08's corrected, twice-reproduced remount probe shows it fully remounting on every tab revisit — the only registered tab that does. 02-FINDINGS.md explicitly deferred the root cause ("Task 2's scope is measurement, not per-view debugging"). This plan names that cause and closes it.

Purpose: PERF-02's substance ("main-tab switches render immediately from cached state") is delivered for every main tab except this one. D-01 says main tabs get BOTH <KeepAlive> instance caching AND useCachedResource data caching; Server got the data half (RPC count 8 -> 2, measured) and not the instance half. D-10's discipline — name the measured cause before the fix — applies to this gap exactly as it applied to the phase.

Output: a committed re-runnable remount probe, a named root cause in 02-FINDINGS.md, a targeted fix, a regression test in keepAliveLifecycle.test.ts, a --frontend-only deploy to archi-dev-box only (D-15 — no fleet, no OTA), and a human confirmation on real hardware (D-11).

Coverage: this plan covers verification gap 1 (Server.vue remount) and contributes to PERF-02. Verification gap 2 (the six timing-regression surfaces) is covered by 02-10-PLAN.md.

Absolute constraint, inherited from CLAUDE.md and D-01: no visual or animation change of any kind. The KeepAlive machinery in DashboardRouterView.vue / dashboardViewWrappers.ts already broke the visual contract once (02-02's Task 3 checkpoint: broken page margins + missing slide transitions, caused by moving view-wrapper off the transitioning element). Those files carry long comments explaining exactly which invariants must hold. Read them before touching them, and keep keepAliveTabs.test.ts untouched and green — it is the structural pin for that contract.

<source_audit>

Multi-Source Coverage Audit (gap-closure scope, covers 02-09 + 02-10)

Source Item Covered by Status
VERIFICATION gap 1 Server.vue registered in KEEP_ALIVE_PATHS but genuinely remounts on every revisit; root cause never investigated 02-09 Tasks 1-3 COVERED
VERIFICATION gap 1 "or an explicit accepted override" alternative 02-09 Task 3 checkpoint (human decides only if the fix branch is exhausted) COVERED
VERIFICATION gap 2 Six surfaces (Discover, Server, Web5, AppDetails, OpenWrtGateway, wallet/send-flow) unimproved or regressed vs baseline 02-10 Tasks 1-2 COVERED
VERIFICATION gap 2 Unresolved environmental-noise theory (different time of day, 85% disk) flagged but never tested 02-10 Task 1 (re-run under recorded current conditions) + Task 2 (three-way comparison) COVERED
VERIFICATION human_verification 1 "Does Server show a visible spinner/blank frame on real hardware?" 02-09 Task 3 checkpoint COVERED
VERIFICATION human_verification 2 "Real regression or environmental noise?" 02-10 Tasks 1-2 COVERED
REQ PERF-02 Main-tab switches render immediately from cached state 02-09 (Server instance caching), 02-10 (Discover/Server/Web5 timing verdict) COVERED
REQ PERF-03 Secondary screens open without blocking reload, instant on repeat 02-10 (AppDetails/OpenWrtGateway/wallet-send timing verdict) COVERED
CONTEXT D-01 Main tabs use BOTH KeepAlive and useCachedResource 02-09 (delivers the KeepAlive half for Server) COVERED
CONTEXT D-10 Measured cause named before the fix lands 02-09 Task 1 gates Task 2; 02-10 Task 1 gates Task 2 COVERED
CONTEXT D-11 On-device verification on archi-dev-box; pass bar = no visible spinner/blank on revisit 02-09 Tasks 2-3; 02-10 Task 1 COVERED
CONTEXT D-15 Dev pair only, no OTA/fleet 02-09 Task 2 deploy step; 02-10 Task 1 COVERED

No MISSING rows. Explicitly out of scope for these two plans (already recorded as open follow-ups in 02-FINDINGS.md ## Outstanding and owned by no gap in 02-VERIFICATION.md): Fleet's 330->1054ms split-signal regression (not in the verifier's named six), CloudFolder's cloudStore.navigate() TTL gap, PeerFiles.vue's unconditional refetch, /dashboard/settings remaining withheld from the cache, ContainerAppDetails.vue dead-code deletion, the HealthNotifications.vue toast intercepting clicks, and the archy-x250-dev deploy. Do not pick any of these up here. </source_audit>

<execution_context> @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md </execution_context>

@.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 these source files before changing anything — they carry the invariants this plan must not break: @neode-ui/src/views/dashboard/DashboardRouterView.vue @neode-ui/src/views/dashboard/dashboardViewWrappers.ts @neode-ui/src/views/dashboard/keepAliveRoutes.ts @neode-ui/src/views/dashboard/tests/keepAliveLifecycle.test.ts

Do NOT read 02-PERF-BASELINE.json or 02-PERF-AFTER.json into context (50-60KB each). If a field is needed, extract it with node -e or jq.

Read only these parts of the phase's prior artifacts (they are large):

  • .planning/phases/02-ui-performance/02-FINDINGS.md ## Results and ## Outstanding sections (the Server row and the first Outstanding bullet are the gap under repair; the paragraph above the Results table documents the corrected probe method this plan turns into a committed spec).
  • .planning/phases/02-ui-performance/02-08-SUMMARY.md key-decisions — the deploy command and the probe-correction decision.
Task 1: Name the measured cause of Server's KeepAlive miss, and commit the probe that proves it neode-ui/e2e/perf/keepalive-remount-probe.spec.ts, .planning/phases/02-ui-performance/02-FINDINGS.md `ARCHY_PASSWORD` is exported in the environment and `curl -sS -o /dev/null -w '%{http_code}' http://archi-dev-box/` returns a 2xx/3xx — the probe drives a real authenticated UI session on the deployed build (D-11). If either is unmet, stop and report it rather than substituting a local dev server or a mock backend; a local run cannot answer this question. `neode-ui/src/views/dashboard/dashboardViewWrappers.ts` (the whole file — the `KeepWrap:` naming scheme and the memoized `wrapperFor` cache are the mechanism under suspicion), `neode-ui/src/views/dashboard/DashboardRouterView.vue` (the `:include` wiring and the four numbered structural invariants in its script comment), `neode-ui/src/views/dashboard/keepAliveRoutes.ts` (`KEEP_ALIVE_PATHS` derivation and `KEEP_ALIVE_MAX = 6`), `neode-ui/e2e/perf/measure.ts` `stampRoot`/`readRootProbe`/`NEUTRAL_SELECTOR` (the original probe and the away-hop it uses), `neode-ui/e2e/perf/surfaces.ts` (the `server`, `web5` and `fleet` rows all share `rootSelector: '.view-container'` and the `contentSelector` shape `.view-container [data-controller-container]`), and `neode-ui/src/views/Server.vue` lines 930-1000 (`onActivated`/`onDeactivated`/`onUnmounted`/`onMounted` and the seven `useCachedResource` groups' arming). Produce a named, evidence-backed cause for why `/dashboard/server` does not survive a tab round-trip while every other registered path does, and leave behind a probe anyone can re-run. Per D-10, no fix may land until this task has named the cause — a fix without a named cause is forbidden in this phase.

Work the diagnosis in this order, cheapest discriminator first, and stop as soon as one reproduces:

Step A — jsdom reproduction (no device, no password). Extend the existing pattern at the bottom of keepAliveLifecycle.test.ts (the createRouter + mount(DashboardRouterView) + per-path mount-count test that proves LRU eviction), but with the REAL Server.vue component at the REAL /dashboard/server path, a real away-hop to /dashboard/settings (the same neutral tab measure.ts's NEUTRAL_SELECTOR uses), and a return hop. Count onMounted invocations. Reuse the existing vi.mock('@/api/rpc-client') and vi.mock('@/stores/app') module-scope mocks and the QuickActionsCard/TorServicesCard/ServerModals/FipsNetworkCard stubs already in that file. If the mount count reaches 2 on the return hop, the cause is structural and reproducible locally — capture which of the suspects below explains it and skip to writing it up. This scratch reproduction becomes Task 2's regression test, so write it where it will live.

Step B — on-device instrumented probe (only if Step A does not reproduce). Create neode-ui/e2e/perf/keepalive-remount-probe.spec.ts: a standalone Playwright spec that logs into ARCHY_BASE_URL (default http://archi-dev-box) with process.env.ARCHY_PASSWORD, then for EVERY path in KEEP_ALIVE_PATHS (import it directly from @/views/dashboard/keepAliveRoutes or re-declare the list with a comment saying why, if the e2e tsconfig cannot resolve the alias) performs visit -> away to /dashboard/settings -> return, and reports per path whether the instance survived. Do NOT edit measure.ts, surfaces.ts or surface-perf.spec.ts — the 02-01 harness stays frozen so 02-10 can re-run it unmodified. Implement the corrected method 02-08 documented (02-FINDINGS.md, the paragraph above the Results table): stamp and read back the .view-container ancestor of the surface's own VISIBLE contentSelector match, using getBoundingClientRect/offsetParent to exclude KeepAlive's inactive cached instances, never document.querySelector's first DOM match. Add three instruments the ad-hoc 02-08 probe did not have, because they are what discriminate the suspects: (1) alongside the element stamp, record a monotonic per-mount counter written by the page itself so element identity and instance identity can disagree visibly in the output; (2) subscribe to page.on('pageerror') and page.on('console') for the whole round-trip and print anything captured; (3) after each hop, log document.querySelectorAll('.view-container').length and the observed location.pathname so cache population and the actual route path are visible in the transcript.

Ranked suspects, with the static evidence already gathered — test them in this order and record which the data kills:

  1. The probe measures DOM-element identity, not component-instance identity. Server's root is a single stable <div class="pb-6"> (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.

<threat_model>

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.
</threat_model>
- 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.

<success_criteria>

  • 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. </success_criteria>
Create `.planning/phases/02-ui-performance/02-09-SUMMARY.md` when done. Record the named cause, which suspects the evidence eliminated, the exact fix, the pre-fix failing-test observation, the post-deploy probe reading, archy-x250-dev's reachability, and the checkpoint outcome verbatim.