Files
archy/.planning/phases/02-ui-performance/02-04-SUMMARY.md

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
vue
keepalive
vue-router
activate-deactivate
useCachedResource
aiui
phase provides
02-ui-performance/02-02 Route-path KeepAlive classifier (shouldKeepAlive/KEEP_ALIVE_PATHS/KEEP_ALIVE_MAX), DashboardRouterView.vue KeepAlive host with statically-named per-route wrapper components, the onActivated reactivation fix in useCachedResource.ts, RefreshIndicator.vue, and the onMounted/onActivated/onDeactivated side-effect audit convention
Every main tab's timers, subscriptions and window listeners audited and placed for an activate/deactivate lifecycle (Home, Chat, Web5, Cloud, Server, Mesh, Apps, Discover)
KEEP_ALIVE_PATHS widened from the tracer's single seed path to the full TAB_ORDER-derived set (10 paths: every main tab except /dashboard/settings) plus /dashboard/discover
useCachedResource.ts fix: onActivated no longer eagerly force-loads an immediate:false (tab-gated lazy) resource that has never been explicitly fetched
Discover.vue routed onto the same shared 'app-catalog'/'bitcoin.prune-status' cache keys Marketplace.vue introduced in 02-02
02-05
02-06
02-07
02-08
added patterns
Three-bucket side-effect classification (once-per-session/every-entry/only-while-visible) applied consistently across every main-tab view, with idempotent arm/disarm functions
Dual-registration pattern: every arm function is called from BOTH onMounted and onActivated, because onActivated is a documented no-op outside a <KeepAlive> boundary — a bare (non-KeepAlive) mount must not silently skip a view's timers/subscriptions/listeners. Fresh-mount guard flags (Home/Web5/Mesh/Server) prevent the harmless-but-avoidable double-fire this causes on a KeepAlive-wrapped view's very first activation for the heavier loaders
useCachedResource's onActivated staleness check now distinguishes 'never explicitly requested' (immediate:false, fetchedAt still null) from 'stale, already loaded once' — only the latter auto-revalidates on reactivation, so a tab-gated lazy resource isn't force-loaded merely by its owning view entering the KeepAlive cache
created modified
neode-ui/src/views/dashboard/__tests__/keepAliveLifecycle.test.ts
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
neode-ui/src/views/Discover.vue
neode-ui/src/views/dashboard/keepAliveRoutes.ts
neode-ui/src/composables/useCachedResource.ts
neode-ui/src/composables/__tests__/useCachedResource.test.ts
/dashboard/settings withheld from KEEP_ALIVE_PATHS despite being in TAB_ORDER: Settings.vue's child sections were never in this plan's file scope, and a grep found real un-audited side effects (SystemDangerZone.vue's reboot poll/elapsed intervals; one-shot onMounted-only fetches in VpnStatusSection.vue/KioskDisplaySection.vue/TransportPrefsCard.vue/ClaudeAuthSection.vue) that would misbehave under KeepAlive. Registering it without auditing those would ship the exact bug this plan exists to prevent.
Every other TAB_ORDER path (Mesh, Chat included) stays registered per the plan's literal exclusion rule: only a measured 'already fast'+Remounted:false excludes a path, and neither Mesh nor Chat has that (both are 02-FINDINGS.md 'unmeasured', not 'already fast').
Discover.vue's catalog fetcher keeps its own dynamic-catalog-first behavior (fetchAppCatalog() with a curated-list fallback) rather than being flattened to Marketplace.vue's simpler getCuratedAppList()-only fetcher — both are valid producers of the shared 'app-catalog' cache key; dropping Discover's fetcher would have silently lost the dynamic-catalog/featured-banner capability.
useCachedResource.ts's onActivated guard change (skip auto-revalidation for a never-fetched immediate:false resource) applies to every current and future consumer, not just Cloud.vue/Server.vue — verified safe because Marketplace.vue's own resources (the only ones already inside a KeepAlive boundary before this plan) are both immediate:true (default), so this plan's approved 02-02 checkpoint behavior is unaffected.
Every dual-registered (onMounted + onActivated) arm function is idempotent by construction (clear/remove-then-set), confirmed safe by re-running the full existing test suite after the fix — CloudPeersRefresh.test.ts (which mounts Cloud.vue bare, outside any KeepAlive) caught the initial regression where onActivated-only logic silently never ran outside a KeepAlive boundary.
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.
id description requirement verification human_judgment
D1 Every audited main tab (Home, Chat, Web5, Cloud, Server, Mesh, Apps, Discover) has its timers/subscriptions/listeners placed into once-per-session, every-entry, or only-while-visible buckets, each idempotent and safe under both a bare mount and a KeepAlive-wrapped mount PERF-02
kind ref status
unit neode-ui/src/views/dashboard/__tests__/keepAliveLifecycle.test.ts pass
kind ref status
unit neode-ui/src/composables/__tests__/useCachedResource.test.ts (lazy-resource-under-activation case) pass
false
id description requirement verification human_judgment
D2 KEEP_ALIVE_PATHS widened to the full TAB_ORDER-derived set (minus /dashboard/settings) plus /dashboard/discover; shouldKeepAlive stays exact-match (no secondary screen slips into the cache); eviction proven at KEEP_ALIVE_MAX+2 distinct registered paths PERF-02
kind ref status
unit neode-ui/src/views/dashboard/__tests__/keepAliveLifecycle.test.ts (keepAliveRoutes describe block) pass
false
id description requirement verification human_judgment rationale
D3 Every main tab feels instant on revisit with no off-screen drain, wallet freshness on Home re-entry, Chat's AIUI panel staying loaded across a tab switch, Apps not showing a spurious connection error, secondary screens still remounting as before, and eviction observed at the 6-tab cap — verified on the real :8101 dev preview against archi-dev-box PERF-02
kind ref status
manual_procedural Task 3 checkpoint:human-verify — approved on all steps except a pre-existing AIUI dev-mode gap (see Known Issues) pass
true Visual/perceptual verification (instant paint, no stale margins/animations, wallet freshness timing) is inherently a human judgment call, consistent with 02-02's precedent for this same class of checkpoint.
~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.vuesystemStatsInterval (10s), walletRefreshInterval (30s), the wsClient wallet-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 window message listener and ContextBroker follow activate/deactivate; aiuiConnected is deliberately never reset on deactivate since the iframe's one-time ready postMessage won't resend on re-entry.
  • Web5.vue — the six child-component data loaders (confirmed none use useCachedResource internally) 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, plus loadCounts/loadPeers) re-runs every entry, since it's the one path here that bypasses useCachedResource and 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; loadDiskStatus becomes 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 connectionError on entry so a since-reconnected node doesn't show a stale error instantly.
  • Discover.vueloadCommunityMarketplace/loadBitcoinPruneStatus now resolve against the same shared app-catalog/bitcoin.prune-status cache keys Marketplace.vue introduced in 02-02, with RefreshIndicator wired to the shared resource's loadState.
  • Fleet.vue — confirmed no lifecycle side effects (grep for the five tokens found none); left unchanged, registered as-is.
  • keepAliveRoutes.tsKEEP_ALIVE_PATHS now derives from TAB_ORDER (single source of truth) plus /dashboard/discover, withholding /dashboard/settings for an unaudited-risk reason recorded in-file.
  • useCachedResource.ts — a real bug found during the audit: onActivated's staleness check treated a never-fetched immediate:false resource 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 into onActivated. CloudPeersRefresh.test.ts (which mounts Cloud.vue directly, no KeepAlive) caught that onActivated is a documented no-op outside a KeepAlive boundary. Fixed by calling every arm function from both onMounted and onActivated, 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:

  1. Task 1: Timers, subscriptions and listeners follow activation, not mountf177a505 (feat, tdd)
  2. Task 2: One-shot flags, entry timers, and widening the registration set03a3e4e0 (feat, tdd)
  3. 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-usage
  • neode-ui/src/views/Chat.vue — window listener + ContextBroker follow activate/deactivate
  • neode-ui/src/views/web5/Web5.vue — six child-loaders + wallet poll follow activate/deactivate; DID lookup stays once-per-session
  • neode-ui/src/views/Cloud.vue — counts/peers/peer-files warm-cache re-runs every entry
  • neode-ui/src/views/Server.vue — VPN poll interval follows activate/deactivate; loadDiskStatus every-entry
  • neode-ui/src/views/Mesh.vue — full live-communications lifecycle (listeners, two poll intervals, ws subscription, six-way refresh, deep-link handling) follows activate/deactivate
  • neode-ui/src/views/Apps.vue — connection-timeout timer is now entry-scoped
  • neode-ui/src/views/Discover.vue — catalog/prune-status routed onto Marketplace.vue's shared cache keys; RefreshIndicator added
  • neode-ui/src/views/dashboard/keepAliveRoutes.tsKEEP_ALIVE_PATHS widened, derived from TAB_ORDER
  • neode-ui/src/composables/useCachedResource.tsonActivated no longer eagerly force-loads a never-fetched immediate:false resource
  • neode-ui/src/composables/__tests__/useCachedResource.test.ts — new test for the above fix
  • neode-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/settings deliberately withheld (unaudited-risk, not "already fast") — see table above.
  • useCachedResource.ts's onActivated guard change is a shared-composable fix (affects every consumer), verified safe against the one pre-existing KeepAlive consumer (Marketplace.vue, both resources immediate: 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: false resources (paidResource, myFilesResource) ahead of Task 2's registration widening
  • Issue: stale() returns true for any never-fetched entry (fetchedAt === null), so onActivated's bare refreshIfStale() 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: onActivated now skips the auto-revalidate when opts.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 (before onMounted), 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 on onActivated (with an immediate first tick) and torn down on onDeactivated; onUnmounted kept 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 asserts vpnStatus() 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 (the onMounted call and the same-page archipelago:share-to-mesh custom event)
  • Issue: App.vue only dispatches the custom event when the user is already on /mesh; a direct router.push('/mesh') navigation (the only path for a share arriving from another screen) relied entirely on onMounted's one-time read of the sessionStorage stash. Once Mesh.vue is kept alive, onMounted fires 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-entry onActivated bucket 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's onShareToMeshMessage handler 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_PATHSCloudPeersRefresh.test.ts (mounts Cloud.vue bare, no KeepAlive) failed
  • Issue: My first pass moved several views' onMounted-only initialization entirely into onActivated, reasoning that "Vue fires onActivated on first mount too" — true only when the component already has a KeepAlive ancestor. Outside one, onActivated never 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 onMounted and onActivated. 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 pass onActivated makes immediately after onMounted on 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-check and npm run build both clean.
  • Committed in: f177a505 and 03a3e4e0 (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 :8101 dev preview session (this plan's own instance, pointed at archi-dev-box via BACKEND_URL)
  • Diagnosis performed:
    1. Diffed Chat.vue against f177a505^ (its state immediately before this plan's Task 1 commit) — the AIUI iframe src construction (aiuiUrl computed) and the ContextBroker instantiation are byte-identical in intent; the only change is that armChatLive() (listener + broker setup) now runs from onActivated/onMounted instead of onMounted alone. ContextBroker.start() only adds a passive window message listener — it sends nothing to the iframe and has no handshake that a double-invocation on first mount could leave half-completed.
    2. Traced aiuiUrl's computation: in dev mode (not PROD, not IS_DEMO), it is empty unless VITE_AIUI_URL is 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 requires VITE_AIUI_URL to be set to an unreachable target.
    3. Found the source: scripts/dev-start.sh's "Mock backend" menu option (the one that serves :8100) launches VITE_AIUI_URL=http://localhost:5173 vite unconditionally, alongside a best-effort cd ../../AIUI && pnpm dev that silently no-ops (|| echo '[AIUI] Not found...') when the separate AIUI repo isn't checked out next to neode-ui/. Confirmed on this machine: no AIUI directory exists anywhere near the project root, and nothing listens on port 5173 (lsof -ti:5173 empty, curl to localhost:5173 connection-refused).
    4. This means the :8100 mock-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 a ready message, aiuiConnected never flips true and the loading overlay never clears. This reproduces identically against f177a505^'s Chat.vue.
    5. 02-FINDINGS.md (written in 02-01, before this plan existed) already flags Chat/AIUI as unmeasured with 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.
  • Verdict: Pre-existing artifact of the local "Mock backend" dev mode's AIUI wiring (missing sibling AIUI checkout), 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 ../../AIUI checkout, or should VITE_AIUI_URL only 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/settings is not yet in the instance cache — a future plan should audit Settings.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

Self-Check: PASSED