22 KiB
phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, requirements-note, coverage, duration, completed, status
| phase | plan | subsystem | tags | requires | provides | affects | tech-stack | key-files | key-decisions | patterns-established | requirements-completed | requirements-note | coverage | duration | completed | status | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 02-ui-performance | 09 | ui |
|
|
|
|
|
|
|
|
PERF-02 was already marked Complete in REQUIREMENTS.md by 02-07 (extending KeepAlive/useCachedResource to every main tab). This gap-closure plan doesn't add new requirement coverage — it retracts a previously-reported regression against that same requirement, restoring the record to accurate. |
|
~130min | 2026-07-31 | complete |
Phase 2 Plan 9: Server/Web5 KeepAlive Gap Closure Summary
Named a proven probe-measurement artifact (not a real defect) as the cause of Server.vue's and Web5.vue's "genuinely remounts" reading, and pinned their real round-trip survival with instance-identity regression tests — no source change needed.
Performance
- Duration: ~130 min (approximate — includes extensive device-probe iteration to isolate the measurement artifact)
- Tasks: 3 (Task 1 auto, Task 2 auto/tdd, Task 3 checkpoint:human-verify — approved)
- Files modified: 3 (
keepalive-remount-probe.spec.tscreated,keepAliveLifecycle.test.tsmodified,02-FINDINGS.mdmodified)
Accomplishments
- Retracted a previously-reported regression with direct evidence, rather than accepting it at face value. 02-08 (and 02-VERIFICATION.md's gap 1) reported that
/dashboard/server"genuinely remounts" on every tab revisit, reproduced twice via a "corrected" probe method. This plan's Task 1 built a further-instrumented, committed probe and found the opposite: Server.vue's component instance survives every round trip, exactly like every other registered tab. The "remount" reading was a measurement artifact of the probe's own generic CSS selector, not a real KeepAlive/lifecycle bug. - Discovered the same artifact affects Web5.vue too, which 02-08 had reported as surviving — its own reading carried the identical latent ambiguity, confirmed directly in this plan's device runs.
- Named the mechanism with direct, reproduced proof, not inference:
Server.vue/Web5.vue/Fleet.vueshare the fully generic.view-container [data-controller-container]/.view-containerselector that every KeepAlive-cached main tab's root carries viaDashboardRouterView.vue's fallthrough class. Once more than one tab is cached (the normal, intended KeepAlive state — that is the entire point of the feature), a naive first-DOM-match probe cannot reliably tell "the tab actually on screen" from "a different cached tab whose root happens to still be connected to the document."Settings.vue(the away tab every round trip in this plan, and inmeasure.ts's own convention, uses) compounds the ambiguity: it rendersAccountInfoSection.vue/KioskDisplaySection.vueunconditionally, both of which also carrydata-controller-container, so even Settings' own content can be mistaken for the target view mid-transition. - Confirmed with an independent, authoritative signal, not just a hypothesis: added
document.elementFromPoint()at the viewport center (real hit-testing, respects actual stacking/z-index — unlike thegetBoundingClientRect()/offsetParentheuristic both 02-08's and this plan's own first-pass "corrected" probe used) as a second, orthogonal check. Across four independent full-session device runs, this authoritative signal directly contradicted the naive method's "remounted" verdict for Server in one run and for Web5 in a separate run — and never once contradicted survival the other way. A companion diagnostic (enumerating every matching root's own stamp mark) independently found the ORIGINAL stamped Web5 root still connected and visible in the document, simply not the one the naive.find()picked first. - Eliminated every other ranked suspect by direct measurement, not by inspection alone: LRU eviction (suspect 3) ruled out via exact DOM-population tracing across each surface's own isolated round trip;
route.pathmismatch (suspect 4) ruled out via exactlocation.pathnamelogging on every hop;include-name comma-matching (suspect 5) ruled out structurally (no wrapper name contains a comma); a Server/Web5-specific runtime error (suspect 2) found no supporting evidence across four full-session console/pageerror captures, and a real jsdom reproduction using the actualServer.vue/Web5.vuecomponents across the exact ten-tabKEEP_ALIVE_PATHSsequence with real LRU pressure did not reproduce the fault either. - Landed the plan's own explicitly anticipated "no source change" branch for Task 2: since the architecture was proven to already work, no edit was made to
DashboardRouterView.vue,dashboardViewWrappers.ts,keepAliveRoutes.tsorServer.vue's KeepAlive/lifecycle wiring. Four new regression tests measure component-instance identity via Vue's ownvm.$.uid(immune to the CSS-selector ambiguity that produced the original false reading) and passed immediately against the unmodified code — that immediate pass is the pin, per the plan's own design for this outcome. - User confirmed on real hardware (Task 3 checkpoint, approved): instant Server/Web5 revisits with no spinner or blank frame, preserved scroll/expanded state across a round trip, unchanged page margins and slide/depth transitions on Server, Web5, and other spot-checked tabs, fresh (not frozen) data after leaving Network for a minute, and — per the coordinator's additional request — a working OpenWrt Gateway Connect form (WR-03 sanity check).
Task Commits
- Task 1: Name the measured cause of Server's KeepAlive miss, and commit the probe that proves it —
4d471759(fix) - Task 2: Land the targeted fix, pin it with a regression test (no-op branch), and confirm on unmodified code —
3e3159fa(test) - Task 3: checkpoint:human-verify — approved by the user ("I've checked everything you've asked, it's ok for now thanks, approve"), covering all seven verification items including the OpenWrt Connect-form (WR-03) sanity check.
Plan metadata: this commit (docs: complete 02-09 plan)
Files Created/Modified
neode-ui/e2e/perf/keepalive-remount-probe.spec.ts— new, committed, re-runnable Playwright spec covering everyKEEP_ALIVE_PATHStab; adds instance-uid capture, session-wide console/pageerror capture, DOM-population/pathname logging per hop, and an authoritativedocument.elementFromPoint()hit-test signal beyond 02-08's ad-hoc probe. Structurally asserts every registered path produces a result row (mirrorssurface-perf.spec.ts's own sole assertion); does not hard-gate on individual survival verdicts, since producing those verdicts honestly is the probe's job, not a pass/fail condition on itself.neode-ui/src/views/dashboard/__tests__/keepAliveLifecycle.test.ts— added a02-09 gap closuredescribe block with four tests (round-trip identity for Server viavm.$.uid; the same for Web5 plus a synthetic second tab;keepAliveIncludeNames()correctness;shouldKeepAlive/KEEP_ALIVE_MAXunchanged). Also broadened the module-level@/api/rpc-clientmock to a Proxy fallback (covers methods Web5.vue calls beyond the four Server.vue needs) and added avue-i18nmock — both additive, no existing test's behavior changed.keepAliveTabs.test.tsconfirmed byte-for-byte unmodified (git diff --statempty) and still green..planning/phases/02-ui-performance/02-FINDINGS.md— new## Server KeepAlive Root Cause (gap closure)section recording the full method, every eliminated suspect with its discriminating evidence, theelementFromPoint()proof, the verdict, and Task 2's no-op outcome.
Decisions Made
See key-decisions in frontmatter above — summarized: named cause is a proven probe-measurement artifact (generic shared selector, confirmed via an authoritative hit-test signal that repeatedly contradicted the naive "remounted" verdict); Task 2 took the plan's own anticipated no-source-change branch; no deploy was performed since nothing in neode-ui/src changed; Fleet was left as an open, unconfirmed-either-way question outside this plan's named scope.
Deviations from Plan
Auto-fixed Issues
1. [Rule 1 - Bug] Probe's own initial content-selector wait could resolve against the previous tab's still-visible content before navigation landed
- Found during: Task 1, first device run
- Issue: For surfaces sharing the generic
.view-container [data-controller-container]selector (Server, Web5, Fleet),page.locator(contentSelector).first().waitFor({state:'visible'})alone could resolve instantly against the PREVIOUS tab's still-visible content, before the actual navigation completed — caught directly by instrument 3'slocation.pathnamelogging (Server's ownafterVisit.pathnameread back/dashboard, not/dashboard/server). - Fix: Gated every visit/return hop on
page.waitForURL()matching the exact literal path before checking content visibility. - Files modified:
neode-ui/e2e/perf/keepalive-remount-probe.spec.ts - Verification: Re-ran against archi-dev-box;
pathname(visit/return)fields subsequently matched the expected path on every run. - Committed in:
4d471759
2. [Rule 1 - Bug] Overly narrow "exactly one visible match" settle-wait broke on contentSelectors that legitimately match multiple sibling elements
- Found during: Task 1, mid-investigation
- Issue: An interim fix attempted to wait for exactly one visible
contentSelectormatch before stamping/reading (hypothesizing a Settings-leave-transition overlap). This assumption was wrong for several surfaces — e.g..home-cardlegitimately matches 3 pairs of sibling cards simultaneously (6 elements) as a permanent, correct state, not a transient collision — causing the probe to hang/timeout on every surface. - Fix: Replaced the "exactly one" assertion with a DOM-population-stability check (raw element count identical across 5 consecutive 100ms-spaced polls), which tolerates legitimately-plural selectors while still waiting out real transition churn.
- Files modified:
neode-ui/e2e/perf/keepalive-remount-probe.spec.ts - Verification: Probe ran to completion (no hangs) against archi-dev-box across all subsequent runs.
- Committed in:
4d471759
3. [Rule 1 - Bug] Web5.vue's armWeb5Live() throws under test when its child refs are auto-stubbed
- Found during: Task 2, writing the Web5 round-trip regression test
- Issue:
Web5.vue's activation-arm function calls exposed methods directly on child component refs (connectedNodesRef.value?.loadPeers(), etc.); a barestubs: { Web5ConnectedNodes: true }auto-stub doesn't expose anything, so the call throws (loadPeers is not a function) as an unhandled rejection during mount. - Fix: Defined minimal, explicit expose-stub components for
Web5ConnectedNodes/Web5NodeVisibility/Web5Identities/Web5NostrRelays(no-op method bodies matching the real components' exposed API) instead of auto-stubbing them. - Files modified:
neode-ui/src/views/dashboard/__tests__/keepAliveLifecycle.test.ts - Verification: Test 2 passes cleanly with no unhandled-rejection warnings.
- Committed in:
3e3159fa
4. [Rule 3 - Blocking] Probe's final assertion hard-failed on Mesh's (and later, Fleet's/Chat's) known, pre-existing environmental unmeasurability
- Found during: Task 1, iterating toward a clean
exit 0run - Issue: An early version asserted every surface's
errorfield wasnull, which fails the whole spec on Mesh's device-not-reporting-connectedcondition (already documented in 02-FINDINGS.md's own## Resultsas "unmeasured", not "failed") and, in one run, on Chat's AIUI-connection timing — both pre-existing, environment-dependent blockers unrelated to this plan's KeepAlive question. - Fix: Relaxed the final assertion to structural-only (every registered path produces a result row), mirroring
surface-perf.spec.ts's own sole assertion (expect(results.length).toBe(SURFACES.length)) — an errored sample is recorded and printed, never discarded, never used to fail the harness itself. - Files modified:
neode-ui/e2e/perf/keepalive-remount-probe.spec.ts - Verification: Final run exited 0 against archi-dev-box while still printing all 10 per-path result lines, including the Mesh row's honest "unmeasured" error text.
- Committed in:
4d471759
Total deviations: 4 auto-fixed (3 probe-methodology bugs found and fixed mid-investigation, 1 test-environment stubbing fix) Impact on plan: All four were necessary to reach a trustworthy measurement or a passing regression test; none represent scope creep. The methodology fixes are themselves part of Task 1's substantive finding, not incidental cleanup.
Issues Encountered
- Playwright's browser binaries were not installed in this environment (
chrome-headless-shellmissing); resolved by runningnpx playwright install chromium(official Playwright browser download, not a new npm dependency — no package-legitimacy concern). - Several device probe runs showed different pass/fail patterns run-to-run for Fleet specifically (survived in some runs, remounted in others) — reported honestly as inconclusive/out-of-scope rather than asserted either way; this plan's named scope was Server (and, by discovery, Web5), not Fleet.
__vueParentComponent/__vnode(Vue's devtools-hook properties, used for the instance-uid instrument) read backnullon every check against the deployed production build — confirmed this is the production bundle stripping the hook (__VUE_PROD_DEVTOOLS__defaults off), not a probe bug, by cross-checking the identical code path populates the field correctly under vitest's dev-mode Vue build. Documented as an honest limitation in 02-FINDINGS.md rather than silently treated as a null-equals-false signal.
User Setup Required
None — no external service configuration required.
Next Phase Readiness
- 02-VERIFICATION.md's gap 1 (Server.vue KeepAlive remount) is closed: the underlying architecture was already correct; the reported regression is retracted with direct, reproduced evidence, and a committed, re-runnable probe plus four CSS-selector-immune regression tests guard against this class of false positive recurring.
- The committed
keepalive-remount-probe.spec.tsis available for 02-10 (or any future plan) to re-run independently againstKEEP_ALIVE_PATHSwithout needing to reconstruct this plan's instrumentation from scratch. - Fleet's own survival status remains genuinely unconfirmed by this plan (elementFromPoint never resolved a definite verdict for it across the runs performed) — flagged, not asserted, and left for whichever future plan next needs a clean answer on that specific tab.
- 02-10 is running concurrently against the same archi-dev-box deployment and owns
02-FINDINGS.mdfor its own timing-regression gap (verification gap 2); this plan made no further edits to that file after Task 2's commit, per the coordinator's file-ownership instruction.
Phase: 02-ui-performance Completed: 2026-07-31
Self-Check: PASSED
- FOUND:
neode-ui/e2e/perf/keepalive-remount-probe.spec.ts - FOUND:
neode-ui/src/views/dashboard/__tests__/keepAliveLifecycle.test.ts - FOUND:
.planning/phases/02-ui-performance/02-FINDINGS.md - FOUND: commit
4d471759 - FOUND: commit
3e3159fa