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 |
|
|
false | true |
|
|
|
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## Resultsand## Outstandingsections (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.mdkey-decisions— the deploy command and the probe-correction decision.
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:
- 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 receivesview-container flex-noneby attribute fallthrough fromDashboardRouterView.vue:13, so a surviving instance should keep the same element — but Server, Web5 and Fleet all share the genericcontentSelector.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. - A Server-specific runtime error during activation or deactivation tearing down the cached subtree — Server carries seven
useCachedResourcegroups plusarmVpnPoll/disarmVpnPoll(Server.vue:944-949) and a once-per-session seed inonMounted(Server.vue:963). A device-only failure would explain why Step A passes while the device does not. Instrument (2) settles this. KEEP_ALIVE_MAX = 6LRU eviction with 10 registered paths. The arithmetic argues against it for an immediate revisit — Vue re-adds the just-activated key as newest and pruneskeys.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_MAXis a measured value per 02-08's on-device heap reading).route.pathdiffering between visits, which would make:key="route.path"andwrapperFor(route.path)resolve to a different cache entry each time.DashboardSidebar.vue:148links/dashboard/serverandDashboard.vue:230pushes{ 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.include-name matching. Vue'smatches()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).
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, andnpm run buildmust all be green. The build gotcha from CLAUDE.md applies: afternpm run build, grepweb/dist/neode-uifor 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 localweb/distcopy — contains the change. - Re-run
keepalive-remount-probe.spec.tsagainst the redeployed build and confirm/dashboard/servernow reports the same instance-survival result as Home/Apps/Marketplace/Cloud/Web5/Fleet. archy-x250-devmay 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.
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.
<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> |
<success_criteria>
- Verification gap 1 is closed:
/dashboard/servereither 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.tsfails 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>