28 KiB
phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, key-decisions, requirements-completed, requirements-note, coverage, duration, completed, status
| phase | plan | subsystem | tags | requires | provides | affects | tech-stack | key-files | key-decisions | requirements-completed | requirements-note | coverage | duration | completed | status | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 02-ui-performance | 04 | ui |
|
|
|
|
|
|
|
PERF-02 is NOT marked complete in REQUIREMENTS.md despite being this plan's sole requirements entry — PERF-02 also spans 02-05, 02-06 and 02-07, which still extend the KeepAlive/cache architecture to Mesh's fetch groups, Server/Home's data layer, and Chat/AIUI. This plan delivers the full lifecycle-audit + registration-widening layer only, per the same precedent 02-02/02-03 set for PERF-02/PERF-03. |
|
~150min | 2026-07-30 | complete |
Phase 02 Plan 04: Main-Tab Lifecycle Audit + Full KeepAlive Registration Summary
Every main tab's onMounted-only side effects reclassified into once-per-session / every-entry / only-while-visible buckets and made idempotent under both a bare mount and a KeepAlive-wrapped mount, then KEEP_ALIVE_PATHS widened from the 02-02 tracer's single path to the full audited set (10 of 11 TAB_ORDER+discover paths — Settings withheld pending its own audit)
Performance
- Duration: ~150 min (including one checkpoint round-trip for a pre-existing AIUI dev-mode gap, diagnosed and confirmed out of scope)
- Started: 2026-07-30 (session start)
- Completed: 2026-07-30T19:26:00Z
- Tasks: 3 (Task 1 auto/tdd, Task 2 auto/tdd, Task 3 checkpoint:human-verify)
- Files modified: 11 (1 new test file, 10 modified)
Accomplishments
- Home.vue —
systemStatsInterval(10s),walletRefreshInterval(30s), thewsClientwallet-push subscription and its debounce timer now follow activate/deactivate with an immediate re-sync on entry (a resumed Home never shows a frozen wallet balance);hydrateWalletSnapshot/checkUpdateStatus/cloud-usage read stay once-per-session. - Chat.vue — the
windowmessagelistener andContextBrokerfollow activate/deactivate;aiuiConnectedis deliberately never reset on deactivate since the iframe's one-timereadypostMessage won't resend on re-entry. - Web5.vue — the six child-component data loaders (confirmed none use
useCachedResourceinternally) and the 30s LND force-refresh interval move to activate/deactivate; the DID lookup and intro-stagger flag stay once-per-session. - Cloud.vue — the per-peer transport/reachability warm-cache (
loadPeerFiles, plusloadCounts/loadPeers) re-runs every entry, since it's the one path here that bypassesuseCachedResourceand would otherwise render stale peer-reachability data once cached (T-02-13). - Server.vue — the previously module-scope-armed 15s VPN poll interval (which used to run forever regardless of tab visibility once anything wrapped this view in KeepAlive) now follows activate/deactivate with an immediate tick on entry;
loadDiskStatusbecomes every-entry. - Mesh.vue — the entire live-communications surface (four window/document listeners, the 5s status/peers/messages poll, the 15s Archipelago-channel poll, the ws peer-push subscription, and the six-way federation/self/contacts refresh) follows activate/deactivate. A share-to-mesh handoff delivered via direct navigation (not the same-page custom event) is now correctly picked up on every activation, not just the first mount — a real gap that would have appeared the moment Mesh joined the instance cache.
- Apps.vue — the 15s "unable to connect" timer is now an entry-scoped guard (re-armed on activation, cleared on exit) and resets
connectionErroron entry so a since-reconnected node doesn't show a stale error instantly. - Discover.vue —
loadCommunityMarketplace/loadBitcoinPruneStatusnow resolve against the same sharedapp-catalog/bitcoin.prune-statuscache keys Marketplace.vue introduced in 02-02, withRefreshIndicatorwired to the shared resource'sloadState. - Fleet.vue — confirmed no lifecycle side effects (grep for the five tokens found none); left unchanged, registered as-is.
- keepAliveRoutes.ts —
KEEP_ALIVE_PATHSnow derives fromTAB_ORDER(single source of truth) plus/dashboard/discover, withholding/dashboard/settingsfor an unaudited-risk reason recorded in-file. - useCachedResource.ts — a real bug found during the audit:
onActivated's staleness check treated a never-fetchedimmediate:falseresource as stale, which would have eagerly force-loaded Cloud.vue's tab-gated Paid Files / My Files walk the moment Cloud.vue joined the instance cache. Fixed to only auto-revalidate a resource that has been explicitly fetched at least once. - Bare-mount regression caught and fixed — my first pass moved several views'
onMounted-only logic entirely intoonActivated.CloudPeersRefresh.test.ts(which mountsCloud.vuedirectly, no KeepAlive) caught thatonActivatedis a documented no-op outside a KeepAlive boundary. Fixed by calling every arm function from bothonMountedandonActivated, with fresh-mount guard flags on the heavier views (Home/Web5/Mesh/Server) to avoid doubling their first-load network cost.
Task Commits
Each task was committed atomically:
- Task 1: Timers, subscriptions and listeners follow activation, not mount —
f177a505(feat, tdd) - Task 2: One-shot flags, entry timers, and widening the registration set —
03a3e4e0(feat, tdd) - Task 3: Walk every main tab and confirm instant revisits with no off-screen drain — checkpoint:human-verify, approved on all steps except one pre-existing dev-mode artifact (see Known Issues below); no code change required for it, per the diagnosis.
Plan metadata: (this commit)
Note: both tasks are TDD tasks; tests were written and made to pass within each task's own commit, per this repo's established single-commit-per-task convention (see 02-01/02-02/02-03 history).
Files Created/Modified
neode-ui/src/views/Home.vue— wallet/stats polling, ws subscription follow activate/deactivate; once-per-session hydrate/update-check/cloud-usageneode-ui/src/views/Chat.vue— window listener + ContextBroker follow activate/deactivateneode-ui/src/views/web5/Web5.vue— six child-loaders + wallet poll follow activate/deactivate; DID lookup stays once-per-sessionneode-ui/src/views/Cloud.vue— counts/peers/peer-files warm-cache re-runs every entryneode-ui/src/views/Server.vue— VPN poll interval follows activate/deactivate; loadDiskStatus every-entryneode-ui/src/views/Mesh.vue— full live-communications lifecycle (listeners, two poll intervals, ws subscription, six-way refresh, deep-link handling) follows activate/deactivateneode-ui/src/views/Apps.vue— connection-timeout timer is now entry-scopedneode-ui/src/views/Discover.vue— catalog/prune-status routed onto Marketplace.vue's shared cache keys; RefreshIndicator addedneode-ui/src/views/dashboard/keepAliveRoutes.ts—KEEP_ALIVE_PATHSwidened, derived fromTAB_ORDERneode-ui/src/composables/useCachedResource.ts—onActivatedno longer eagerly force-loads a never-fetchedimmediate:falseresourceneode-ui/src/composables/__tests__/useCachedResource.test.ts— new test for the above fixneode-ui/src/views/dashboard/__tests__/keepAliveLifecycle.test.ts— new; covers all ten Task 1/Task 2 behaviors
Per-View Side-Effect Table
Bucket key: S = once-per-session (onMounted only) · E = every-entry (onActivated, immediate on entry) · V = only-while-visible (armed onActivated, torn down onDeactivated)
| View | Side effect | Bucket |
|---|---|---|
| Home.vue | hydrateWalletSnapshot() |
S |
| Home.vue | checkUpdateStatus() |
S |
| Home.vue | Cloud usage read (fileBrowserClient.getUsage()) |
S |
| Home.vue | systemStatsInterval (10s loadSystemStats) |
V |
| Home.vue | walletRefreshInterval (30s loadWeb5Status) |
V |
| Home.vue | wsClient.subscribe (wallet push) + debounce timer |
V |
| Chat.vue | window message listener (onAiuiMessage) |
V |
| Chat.vue | ContextBroker |
V |
| Chat.vue | aiuiConnected flag |
Not reset on deactivate (special case — see plan text) |
| Web5.vue | web5AnimationDone intro flag |
S |
| Web5.vue | rpcClient.getNodeDid() |
S |
| Web5.vue | loadPeers/loadReceivedMessages/loadConnectionRequests/loadIdentities/loadVisibility/loadNostrRelays/detectHardwareWallets |
E |
| Web5.vue | walletRefreshInterval (30s lndInfoRes.refresh()) |
V |
| Cloud.vue | loadCounts/loadPeers/loadPeerFiles (peer transport/reachability warm-cache) |
E (all internally staleness-gated or inflight-deduped, so no extra RPC when fresh) |
| Server.vue | checkTorStatus/loadNetworkData/loadInterfaces/loadTorServices/loadVpnPeers/loadFipsSummary |
S (each resource self-heals via useCachedResource's own onActivated) |
| Server.vue | loadDiskStatus() |
E |
| Server.vue | vpnPollInterval (15s) |
V |
| Mesh.vue | window resize, document pointerdown (menu + attach-menu), archipelago:share-to-mesh event, visualViewport resize/scroll |
V |
| Mesh.vue | loadPendingFromSession() (share-to-mesh handoff) |
E |
| Mesh.vue | mesh.refreshAll()/transport.fetchStatus()/refreshFederationNodes()/refreshSelfOnion()/refreshSelfDid()/refreshContacts() + deep-link peer/channel open |
E |
| Mesh.vue | archPollInterval (15s loadArchMessages) |
V |
| Mesh.vue | pollInterval (5s status/peers/messages/deadman/blockheaders + every-6th-tick contacts/federation/outbox) |
V |
| Mesh.vue | wsClient.subscribe (peer push) |
V |
| Apps.vue | appsAnimationDone intro flag |
S |
| Apps.vue | connectionTimer (15s "unable to connect", connectionError reset on entry) |
V (entry-scoped guard) |
| Discover.vue | discoverAnimationDone intro flag |
S |
| Discover.vue | catalogResource/pruneStatusResource |
S seed only — self-heals via useCachedResource's own onActivated |
| Fleet.vue | (none found) | n/a — unchanged |
KEEP_ALIVE_PATHS — Final Contents
Registered (10 paths): /dashboard, /dashboard/apps, /dashboard/marketplace, /dashboard/cloud, /dashboard/mesh, /dashboard/server, /dashboard/web5, /dashboard/fleet, /dashboard/chat, /dashboard/discover
Excluded (1 path): /dashboard/settings — in TAB_ORDER but not registered. Reason: unaudited risk, not a measured "already fast" result (02-FINDINGS.md has no row for Settings at all). Settings.vue's child sections were never in this plan's file scope, and a grep across neode-ui/src/views/settings/*.vue found real un-audited side effects — SystemDangerZone.vue's reboot poll/elapsed intervals, and one-shot onMounted-only fetches in VpnStatusSection.vue, KioskDisplaySection.vue, TransportPrefsCard.vue and ClaudeAuthSection.vue — that would misbehave under KeepAlive exactly as this plan exists to prevent. Flagged for a future plan to audit before registering.
No main tab was excluded for a measured "already fast, Remounted:false" reason — every 02-FINDINGS.md main-tab row was either Remounted: true or unmeasured (Mesh, Chat), and per the plan's own literal exclusion rule (only a measured Remounted: false excludes), both Mesh and Chat stay registered.
Decisions Made
See key-decisions in frontmatter for the full list. Highlights:
/dashboard/settingsdeliberately withheld (unaudited-risk, not "already fast") — see table above.useCachedResource.ts'sonActivatedguard change is a shared-composable fix (affects every consumer), verified safe against the one pre-existing KeepAlive consumer (Marketplace.vue, both resourcesimmediate: true).- Discover.vue keeps its own catalog fetcher (dynamic-first, curated fallback) rather than being flattened to Marketplace.vue's simpler fetcher, since both are valid producers of the same shared cache key.
Deviations from Plan
Auto-fixed Issues
1. [Rule 1 - Bug] useCachedResource.ts's onActivated eagerly force-loaded a never-requested lazy resource
- Found during: Task 1, auditing Cloud.vue's
immediate: falseresources (paidResource,myFilesResource) ahead of Task 2's registration widening - Issue:
stale()returnstruefor any never-fetched entry (fetchedAt === null), soonActivated's barerefreshIfStale()would fire the fetch the instant a tab-gated lazy resource's owning view was first activated inside a KeepAlive — defeating "fetch on first use" (e.g. Cloud.vue's Paid Files tab data loading even though the user never opened that tab). - Fix:
onActivatednow skips the auto-revalidate whenopts.immediate === false && entry.fetchedAt === null; a resource that has been explicitly fetched at least once still revalidates staleness-gated on later reactivations. - Files modified:
neode-ui/src/composables/useCachedResource.ts,neode-ui/src/composables/__tests__/useCachedResource.test.ts(new test) - Verification: New test asserts the lazy resource is not fetched merely by activation, but does revalidate once explicitly requested and later reactivated past its TTL. Full suite green.
- Committed in:
f177a505(Task 1 commit)
2. [Rule 1 - Bug] Server.vue's vpnPollInterval was armed at module setup, not gated to visibility at all
- Found during: Task 1, reading Server.vue's lifecycle regions per the plan's read_first
- Issue: The pre-existing
const vpnPollInterval = setInterval(...)ran at component setup time (beforeonMounted), meaning once anything wrapped Server.vue in KeepAlive, this 15s poll would run forever regardless of tab visibility — exactly the CPU-drain class of bug T-02-03 exists to prevent. - Fix: Converted to an idempotent
armVpnPoll()/disarmVpnPoll()pair, armed ononActivated(with an immediate first tick) and torn down ononDeactivated;onUnmountedkept as a defensive teardown for the non-cached path. - Files modified:
neode-ui/src/views/Server.vue - Verification:
keepAliveLifecycle.test.ts's real-view test mounts Server.vue inside a real<KeepAlive>and assertsvpnStatus()is not called while deactivated and is called once on reactivation. - Committed in:
f177a505(Task 1 commit)
3. [Rule 1 - Bug] Mesh.vue's share-to-mesh handoff would silently stop working after the first visit
- Found during: Task 1, tracing
loadPendingFromSession()'s only two triggers (theonMountedcall and the same-pagearchipelago:share-to-meshcustom event) - Issue:
App.vueonly dispatches the custom event when the user is already on/mesh; a directrouter.push('/mesh')navigation (the only path for a share arriving from another screen) relied entirely ononMounted's one-time read of the sessionStorage stash. Once Mesh.vue is kept alive,onMountedfires exactly once ever, so any share-to-mesh handoff after the very first Mesh visit would be silently dropped. - Fix:
loadPendingFromSession()moved into the every-entryonActivatedbucket alongside the rest of Mesh.vue's live-data refresh. - Files modified:
neode-ui/src/views/Mesh.vue - Verification: Full suite green; behavior traced against
App.vue'sonShareToMeshMessagehandler to confirm the direct-navigation path is the one this fixes. - Committed in:
f177a505(Task 1 commit)
4. [Rule 1 - Bug, caught by a pre-existing test] onActivated-only placement broke every view outside a KeepAlive boundary
- Found during: Task 2, running the full suite after widening
KEEP_ALIVE_PATHS—CloudPeersRefresh.test.ts(mountsCloud.vuebare, no KeepAlive) failed - Issue: My first pass moved several views'
onMounted-only initialization entirely intoonActivated, reasoning that "Vue firesonActivatedon first mount too" — true only when the component already has a KeepAlive ancestor. Outside one,onActivatednever fires at all, so Cloud.vue (and, by the same construction, Home.vue/Chat.vue/Apps.vue/Server.vue/Web5.vue/Mesh.vue) would silently skip every timer/subscription/listener/data-load it owns when mounted bare. - Fix: Every arm function is now called from both
onMountedandonActivated. Fresh-mount guard flags were added to Home.vue/Web5.vue/Mesh.vue/Server.vue (whose loaders have real RPC/CPU cost) so the redundant passonActivatedmakes immediately afteronMountedon a KeepAlive-wrapped first mount is skipped rather than doubling the first-load cost; Cloud.vue's dual call is left unguarded since its loaders are internally staleness-gated / inflight-deduped (confirmed genuinely free, not just low-cost) and Apps.vue/Chat.vue's dual call is trivial (a timer re-arm / listener re-add, no RPC). - Files modified:
neode-ui/src/views/Home.vue,neode-ui/src/views/Chat.vue,neode-ui/src/views/web5/Web5.vue,neode-ui/src/views/Cloud.vue,neode-ui/src/views/Server.vue,neode-ui/src/views/Mesh.vue,neode-ui/src/views/Apps.vue - Verification: Full suite green (90 files / 740 tests),
npm run type-checkandnpm run buildboth clean. - Committed in:
f177a505and03a3e4e0(both task commits — the fix landed across the same files each task touched)
Total deviations: 4 auto-fixed (all Rule 1 — bugs directly implicated by this plan's own must_haves truths or caught by the existing test suite)
Impact on plan: All four were necessary corrections that stayed within files already in scope for their respective tasks. No scope creep. The bare-mount regression (deviation 4) is the most consequential — it's a general pattern risk that would have silently affected every view converted in this plan had CloudPeersRefresh.test.ts not existed to catch it.
Known Issues
AIUI panel shows a blank white screen with a persistent loading spinner in the local mock-backend dev mode (:8100) — pre-existing, NOT a regression from this plan.
- Reported during: Task 3 checkpoint, on the
:8101dev preview session (this plan's own instance, pointed at archi-dev-box viaBACKEND_URL) - Diagnosis performed:
- Diffed
Chat.vueagainstf177a505^(its state immediately before this plan's Task 1 commit) — the AIUI iframesrcconstruction (aiuiUrlcomputed) and theContextBrokerinstantiation are byte-identical in intent; the only change is thatarmChatLive()(listener + broker setup) now runs fromonActivated/onMountedinstead ofonMountedalone.ContextBroker.start()only adds a passivewindowmessage listener — it sends nothing to the iframe and has no handshake that a double-invocation on first mount could leave half-completed. - Traced
aiuiUrl's computation: in dev mode (notPROD, notIS_DEMO), it is empty unlessVITE_AIUI_URLis explicitly set — in which case Chat.vue renders the empty "AI Assistant not configured" placeholder, not a blank iframe. A blank-iframe-with-spinner symptom therefore requiresVITE_AIUI_URLto be set to an unreachable target. - Found the source:
scripts/dev-start.sh's "Mock backend" menu option (the one that serves:8100) launchesVITE_AIUI_URL=http://localhost:5173 viteunconditionally, alongside a best-effortcd ../../AIUI && pnpm devthat silently no-ops (|| echo '[AIUI] Not found...') when the separate AIUI repo isn't checked out next toneode-ui/. Confirmed on this machine: noAIUIdirectory exists anywhere near the project root, and nothing listens on port 5173 (lsof -ti:5173empty,curltolocalhost:5173connection-refused). - This means the
:8100mock-backend dev session points its AIUI iframe at a dead local port regardless of any Chat.vue code change — the iframe shows the browser's own blank error page, and since nothing ever posts areadymessage,aiuiConnectednever flips true and the loading overlay never clears. This reproduces identically againstf177a505^'s Chat.vue. - 02-FINDINGS.md (written in 02-01, before this plan existed) already flags Chat/AIUI as
unmeasuredwith connection/handshake latency called out as a known rough edge on real hardware — corroborating this is a pre-existing gap in this area, not something introduced here.
- Diffed
- Verdict: Pre-existing artifact of the local "Mock backend" dev mode's AIUI wiring (missing sibling
AIUIcheckout), not a regression from this plan's commits. Not fixed here — the AIUI embed URL/connectivity work is explicitly owned by plan 02-07 (02-AIUI-D14.md, wave 4, not yet run); pulling it into 02-04 would be scope creep into another plan's file ownership. - Recommendation for 02-07: confirm the AIUI dev-mode wiring assumption (does it expect a sibling
../../AIUIcheckout, or shouldVITE_AIUI_URLonly be set when that dev server is confirmed running?) as part of its own scope.
Issues Encountered
- The Task 3 checkpoint's first pass surfaced the AIUI dev-mode gap above; diagnosed and confirmed pre-existing/out-of-scope per the coordinator's explicit instruction, so the checkpoint is treated as fully approved (all other steps passed on the first attempt).
User Setup Required
None - no external service configuration required.
Next Phase Readiness
- The full main-tab registration set (
KEEP_ALIVE_PATHS) is now the actual production set 02-05/02-06/02-07 build on — no further widening needed from those plans. - 02-05 (Mesh) and 02-06 (Server and Home) can proceed directly to converting the remaining fire-and-forget fetch groups to
useCachedResource, since this plan already placed every lifecycle side effect correctly for that conversion to land safely under KeepAlive. - 02-07 (Chat/AIUI) inherits the AIUI dev-mode gap noted above as a concrete finding to address as part of its own D-14 UX work.
- Blocker/concern carried forward:
/dashboard/settingsis not yet in the instance cache — a future plan should auditSettings.vue's child sections (SystemDangerZone.vue,VpnStatusSection.vue,KioskDisplaySection.vue,TransportPrefsCard.vue,ClaudeAuthSection.vue) the way this plan audited the other eight tabs before registering it. - No other blockers for 02-05/02-06/02-07.
Phase: 02-ui-performance Completed: 2026-07-30