--- phase: 02-ui-performance plan: 02 subsystem: ui tags: [vue, keepalive, vue-router, sessionstorage, useCachedResource, transitions] # Dependency graph requires: - phase: 02-ui-performance provides: "02-01's D-09 profiling findings (02-FINDINGS.md Ranked Fix Order) selecting the tracer tab" provides: - "Route-path KeepAlive classifier (shouldKeepAlive/KEEP_ALIVE_PATHS/KEEP_ALIVE_MAX) every later main-tab plan registers into" - "DashboardRouterView.vue KeepAlive host with statically-named per-route wrapper components (dashboardViewWrappers.ts) preserving pre-restructure visuals/transitions exactly" - "onActivated reactivation fix in useCachedResource.ts, live for all 9 consumers" - "RefreshIndicator.vue subtle background-refresh affordance (state-driven, no layout shift)" - "Marketplace.vue converted to cached resources (app-catalog, bitcoin.prune-status) as the reference pattern for onMounted/onActivated/onDeactivated side-effect audits" affects: [02-04, 02-05, 02-06, 02-07, 02-08] # Tech tracking tech-stack: added: [] patterns: - "Route-path exact-match KeepAlive classification instead of name-based include/exclude (async components have no inferable name)" - "Statically-named per-route KeepAlive wrapper components (KeepWrap:) as the byte-for-byte-preserving bridge between :include name matching and the pre-existing view-wrapper DOM/animation contract" - "onActivated(() => refreshIfStale()) as the standard reactivation hook alongside onScopeDispose in useCachedResource" - "persist decided explicitly per cache key (never defaulted) per T-02-01" key-files: created: - neode-ui/src/views/dashboard/keepAliveRoutes.ts - neode-ui/src/views/dashboard/DashboardRouterView.vue - neode-ui/src/views/dashboard/dashboardViewWrappers.ts - neode-ui/src/components/RefreshIndicator.vue - neode-ui/src/composables/__tests__/useCachedResource.test.ts - neode-ui/src/views/dashboard/__tests__/keepAliveTabs.test.ts - neode-ui/src/views/__tests__/MarketplaceRefresh.test.ts modified: - neode-ui/src/composables/useCachedResource.ts - neode-ui/src/views/Dashboard.vue - neode-ui/src/views/Marketplace.vue - neode-ui/src/views/dashboard/useRouteTransitions.ts key-decisions: - "Tracer tab = Marketplace.vue (app store), the top-ranked entry in 02-FINDINGS.md Ranked Fix Order (worst-measured revisit, 2033ms) and the surface the user explicitly called out as slow" - "DashboardRouterView.vue's final shape restores the pre-restructure rendered DOM exactly: Transition > KeepAlive(:include=[wrapper names]) > keyed per-route KeepWrap: wrapper whose render emits the old view-wrapper markup byte-for-byte, because dashboard-styles.css scopes transitions as .{name}-enter-active.view-wrapper compound selectors" - "Task-1 scroll-retention Map was deleted — kept-alive tabs keep scroll for free via their cached subtree; non-kept routes reset-to-top exactly as before the phase" - "app-catalog persists (ttl 300000ms, non-sensitive/near-static, D-06); bitcoin.prune-status persists (ttl 30000ms, non-sensitive/small) — both explicit decisions per T-02-01, no default relied on" - "HARD RULE for all remaining Phase 02 plans: perf work must be visually invisible — never change existing visuals/animations. keepAliveTabs.test.ts now pins the padded-wrapper-inside-view-wrapper DOM shape as a structural regression test" patterns-established: - "Per-route KeepAlive wrapper components with static names (KeepWrap:), memoized in a factory keyed by route path, so :include can name-match without depending on async-component name inference (RESEARCH A1 sidestepped)" - "Cache-key persist is always an explicit per-resource decision, never left at the composable's default" - "onMounted/onActivated/onDeactivated side-effect audit convention: once-per-session setup stays in onMounted; cache-gated fetches need no per-view hook because useCachedResource's own onActivated revalidates them" requirements-completed: [] requirements-note: "PERF-02 is NOT marked complete in REQUIREMENTS.md despite being this plan's sole `requirements:` entry — PERF-02 also appears in 02-04, 02-05, 02-06 and 02-07's frontmatter, which extend the KeepAlive/cache architecture proven here to every remaining main tab. This plan delivers the tracer (one tab) only; an automated `requirements.mark-complete PERF-02` run was reverted after cross-checking ROADMAP.md's plan list, mirroring the PERF-03 precedent set in 02-03-SUMMARY.md. Do not re-mark PERF-02 complete until 02-07 lands." coverage: - id: D1 description: "One main tab (Marketplace) renders instantly from a surviving component instance on tab round-trip, with no spinner/blank frame, scroll and search/category state preserved" requirement: PERF-02 verification: - kind: unit ref: "neode-ui/src/views/dashboard/__tests__/keepAliveTabs.test.ts" status: pass - kind: manual_procedural ref: "Task 3 checkpoint:human-verify, approved on :8100 dev preview against archi-dev after fix commit 26687055" status: pass human_judgment: true rationale: "Visual/perceptual parity (no blank frame, animation identical to pre-change, margins intact) is a judgment call unit tests cannot fully prove; this is exactly what the checkpoint caught on first attempt" - id: D2 description: "useCachedResource revalidates in the background exactly once on reactivation past TTL, and not at all within TTL; loadState transitions ready -> refreshing, never back to loading" requirement: PERF-02 verification: - kind: unit ref: "neode-ui/src/composables/__tests__/useCachedResource.test.ts" status: pass human_judgment: false - id: D3 description: "RefreshIndicator renders nothing for ready/idle/loading and a labeled aria-live=polite element for refreshing, with no layout shift" requirement: PERF-02 verification: - kind: unit ref: "neode-ui/src/views/dashboard/__tests__/keepAliveTabs.test.ts" status: pass human_judgment: false - id: D4 description: "A rejected background refresh on Marketplace.vue keeps prior content on screen and raises no toast (D-07)" requirement: PERF-02 verification: - kind: unit ref: "neode-ui/src/views/__tests__/MarketplaceRefresh.test.ts" status: pass human_judgment: false - id: D5 description: "Route transition animations (slide/depth) and page margins are unchanged from before the KeepAlive restructure" requirement: PERF-02 verification: - kind: unit ref: "neode-ui/src/views/dashboard/__tests__/keepAliveTabs.test.ts (structural DOM-shape assertions)" status: pass - kind: manual_procedural ref: "Task 3 checkpoint:human-verify, approved on :8100 dev preview against archi-dev after fix commit 26687055" status: pass human_judgment: true rationale: "First checkpoint attempt failed on exactly this criterion (broken margins, dead slide animations); only a human eyeballing the real preview caught and confirmed the fix — this is inherently a visual judgment, not something a unit test alone can close out" duration: 105min completed: 2026-07-30 status: complete --- # Phase 02 Plan 02: Tracer Tab — KeepAlive Host, Hook Reactivation, Refresh Indicator Summary **Marketplace.vue survives tab round-trips via a route-path-classified KeepAlive host with statically-named per-route wrappers, a stale-while-revalidate `onActivated` fix in `useCachedResource`, and a subtle non-shifting refresh indicator — with the original page margins and slide/depth animations restored byte-for-byte after a checkpoint-caught regression** ## Performance - **Duration:** 105 min - **Started:** 2026-07-30T07:15:17-04:00 - **Completed:** 2026-07-30T08:40:27-04:00 (fix commit; checkpoint approved shortly after on dev preview) - **Tasks:** 3 (Task 1 tracer, Task 2 auto, Task 3 checkpoint:human-verify) - **Files modified:** 11 (7 created, 4 modified — see Files Created/Modified) ## Accomplishments - **Tracer tab chosen: Marketplace** (`neode-ui/src/views/Marketplace.vue`) — the top entry in `02-FINDINGS.md`'s Ranked Fix Order (worst-measured revisit at 2033ms) and the exact surface the user called out as slow ("often app store"). - Route-path classifier (`keepAliveRoutes.ts`: `shouldKeepAlive`, `KEEP_ALIVE_PATHS` seeded with only the tracer tab's path, `KEEP_ALIVE_MAX=6`) deliberately avoids KeepAlive `include`/`exclude` name-matching, since every route is an async component with no inferable name (RESEARCH A1). - `DashboardRouterView.vue` extracted from `Dashboard.vue`'s inline nested RouterView as the shared KeepAlive host every later plan builds on. - `onActivated(() => refreshIfStale())` added to `useCachedResource.ts`, closing the reactivation gap for all 9 consumers (8 pre-existing + Marketplace). - `RefreshIndicator.vue`: presentational, `state`-driven, renders nothing for `ready`/`idle`/`loading`, an `aria-live="polite"`-labeled element for `refreshing`, reserved-space so it never shifts layout. - Marketplace's catalog and Bitcoin prune-status fetches moved onto keyed `useCachedResource` entries (`app-catalog`, `bitcoin.prune-status`), each with an explicit `persist` decision. - **Checkpoint-caught regression and fix:** the first Task 3 verification on the real dev preview failed — outer page margins broke and the up/down slide animations for main-tab switches stopped playing. Root cause and fix are recorded in detail below and in commit `26687055`. Re-verified and approved by the user on the second pass. ## Task Commits Each task was committed atomically: 1. **Task 1: One main tab survives a tab round-trip and revalidates on return** - `385c9d86` (feat, tdd) 2. **Task 2: Subtle refresh indicator and correct per-visit behavior on the tracer tab** - `a9a20039` (feat, tdd) 3. **Fix (post-checkpoint-failure): restore page margins and slide transitions broken by the restructure** - `26687055` (fix) 4. **Task 3: Confirm the tracer tab feels instant on the dev preview against archi-dev** - checkpoint:human-verify, approved on the :8100 dev preview against archi-dev after the fix above (no code commit — verification-only task) **Plan metadata:** (this commit) - `docs(02-02): complete tracer tab plan` _Note: Tasks 1 and 2 are TDD tasks; tests were written and made to pass within the same task commit per the repo's existing single-commit-per-task convention (see prior 02-01/02-03 history) rather than split into separate test/feat commits._ ## Files Created/Modified - `neode-ui/src/views/dashboard/keepAliveRoutes.ts` - Exact-match route classifier: `shouldKeepAlive`, `KEEP_ALIVE_PATHS` (tracer tab's path only), `KEEP_ALIVE_MAX` (6) - `neode-ui/src/views/dashboard/DashboardRouterView.vue` - Extracted KeepAlive host; renders per-route `KeepWrap:` wrapper components from `dashboardViewWrappers.ts` behind ``, keyed at the wrapper root - `neode-ui/src/views/dashboard/dashboardViewWrappers.ts` - **New in the fix commit.** Memoized per-route-path factory of statically-named wrapper components (`KeepWrap:`) whose render emits the pre-restructure `view-wrapper` markup byte-for-byte (full-bleed chat/mesh shape or padded default shape + spacer), so `:include` can name-match without relying on async-component name inference - `neode-ui/src/composables/useCachedResource.ts` - Added `onActivated(() => refreshIfStale())` beside the existing `onScopeDispose` registration - `neode-ui/src/views/dashboard/useRouteTransitions.ts` - `TAB_ORDER` promoted from module-private `const` to an export, for 02-04 to widen `KEEP_ALIVE_PATHS` from - `neode-ui/src/views/Dashboard.vue` - Inline nested RouterView block replaced with `` - `neode-ui/src/views/Marketplace.vue` - `loadCommunityMarketplace()`/`loadBitcoinPruneStatus()` moved onto `useCachedResource` (`app-catalog`, `bitcoin.prune-status`); `RefreshIndicator` wired to a resource `loadState` - `neode-ui/src/components/RefreshIndicator.vue` - Presentational background-refresh affordance, `state: ResourceLoadState`, `label?: string` - `neode-ui/src/composables/__tests__/useCachedResource.test.ts` - Reactivation revalidation, TTL-gated refetch, rejected-refresh keep-last-value coverage - `neode-ui/src/views/dashboard/__tests__/keepAliveTabs.test.ts` - Round-trip mount/activation counts, detail-route exclusion, structural DOM-shape (view-wrapper/animation) pinning - `neode-ui/src/views/__tests__/MarketplaceRefresh.test.ts` - **New file (deviation, see below).** Rejected-background-refresh-keeps-content/no-toast coverage for Marketplace, isolated from the real-router tests in `keepAliveTabs.test.ts` ## Decisions Made - **Tracer tab: Marketplace.vue** — top of `02-FINDINGS.md` Ranked Fix Order (worst-measured revisit, 2033ms), matching the user's own "app store is slow" report. - **Final `DashboardRouterView.vue` template shape (post-fix — differs from the plan's original design):** The plan's original structure (a stable outer wrapper div + `view-wrapper` class pushed onto each component root via fallthrough attrs, with `` nested inside that stable wrapper) broke page margins and killed every slide/depth animation on the real dev preview. Root cause: `dashboard-styles.css` scopes every transition as a compound selector — `.{transitionName}-enter-active.view-wrapper` — and `.view-wrapper` itself is `position: absolute; inset: 0`, both of which require `.view-wrapper` to be the *keyed, direct* child element that `` toggles, not a class merged onto an arbitrary descendant. Splitting navigation across two sibling `` branches behind a shared stable div broke that contract. Fix (commit `26687055`) restores the pre-restructure rendered DOM exactly: single `` → `` → a keyed per-route wrapper component (`:key="route.path"`). The new file `dashboardViewWrappers.ts` holds a memoized per-route-path factory of statically-named wrapper components (`KeepWrap:`) whose render emits the old markup byte-for-byte — a keyed `div.view-wrapper` root containing either the full-bleed chat/mesh shape or the padded-default shape plus the trailing spacer div. Caching is gated by `:include` name-matching against wrapper names derived from `KEEP_ALIVE_PATHS`; because those names are static (not inferred from the wrapped async component), the RESEARCH A1 name-inference problem does not apply. **Consequence for 02-04:** widening the instance cache is a one-line change — add paths to `KEEP_ALIVE_PATHS` only; the wrapper names and `:include` list derive from it automatically. The Task-1 per-route scroll-retention `Map` was **deleted** in the fix: kept-alive tabs now retain scroll for free via their cached subtree (the DOM literally never unmounts), and non-kept routes reset-to-top exactly as they did before this phase — no explicit tracking code needed. - **HARD RULE for all remaining Phase 02 plans (user directive, given after the checkpoint failure):** never change existing visuals or animations — performance work must be visually invisible. `keepAliveTabs.test.ts` now includes a structural assertion pinning the padded-wrapper-inside-`view-wrapper` DOM shape as a regression backstop. - **Side-effect audit (Marketplace.vue):** `marketplaceAnimationDone` (the one-shot intro flag) stays in `onMounted` — it is genuinely once-per-session. The catalog load and the prune-status load needed **no** per-view `onMounted`/`onActivated`/`onDeactivated` hooks of their own: `useCachedResource`'s internal `onActivated` (added in Task 1) already revalidates them, staleness-gated, on every kept-alive reactivation. This view has no intervals, subscriptions, or window listeners, so no `onDeactivated` teardown was required. (Precedent recorded here for 02-04's lifecycle audit across the remaining tabs.) - **Persist decisions (T-02-01, explicit per key, no default relied on):** `app-catalog` → `persist: true`, `ttlMs: 300000` (non-sensitive, small, near-static catalog data per D-06 discretion). `bitcoin.prune-status` → `persist: true`, `ttlMs: 30000` (non-sensitive, small; default TTL). - **Checkpoint:** Task 3's `checkpoint:human-verify` was approved by the user on the `:8100` dev preview against archi-dev, on the second attempt — after the margin/animation fix in `26687055` — confirming instant round-trip render, TTL-gated background revalidation with the subtle indicator, unaffected secondary-screen behavior, correct transition animation, and silent failure handling with the backend stopped. ## Deviations from Plan ### Auto-fixed Issues **1. [Rule 1 - Bug] Rejected-refresh test moved to a new dedicated file** - **Found during:** Task 2 (indicator + per-visit behavior correction) - **Issue:** Plan's acceptance criteria implied extending `keepAliveTabs.test.ts` with the rejected-background-refresh assertion, but that file uses a real Vue Router instance (`createRouter`/`createMemoryHistory`) for its round-trip mount-count tests; `vi.mock('vue-router')`, needed to isolate Marketplace's refresh behavior, hoists to the top of the file and would clobber those real-router tests. - **Fix:** Created `neode-ui/src/views/__tests__/MarketplaceRefresh.test.ts` as a separate file, mirroring the existing `CloudPeersRefresh.test.ts` convention already in this codebase for the same class of problem. - **Files modified:** neode-ui/src/views/__tests__/MarketplaceRefresh.test.ts (new) - **Verification:** Rejected-refresh test passes in isolation and alongside the full suite; `keepAliveTabs.test.ts`'s real-router tests remain unaffected. - **Committed in:** a9a20039 (Task 2 commit) **2. [Rule 1 - Bug] Dropped the ad-hoc `AbortSignal.timeout(8000)` in favor of the composable's own abort-on-unmount** - **Found during:** Task 2 (Marketplace.vue conversion to cached resources) - **Issue:** The pre-conversion `loadBitcoinPruneStatus()` used a manual `AbortSignal.timeout(8000)` on its fetch. Once the call moved behind `useCachedResource`, that manual timeout duplicated/conflicted with the composable's built-in abort-on-unmount signal. - **Fix:** Removed the manual timeout, matching the existing `Cloud.vue` convention for cached fetches in this codebase. - **Files modified:** neode-ui/src/views/Marketplace.vue - **Verification:** `npm run test` and `npm run type-check` green; behavior matches the in-repo `Cloud.vue` reference pattern the plan named. - **Committed in:** a9a20039 (Task 2 commit) **3. [Rule 1 - Bug, caught by checkpoint] Restructured KeepAlive host broke page margins and slide/depth transitions** - **Found during:** Task 3 (first checkpoint:human-verify attempt) - **Issue:** The Task-1-built `DashboardRouterView.vue` (stable outer wrapper + `view-wrapper` fallthrough onto each component root, two sibling `` branches) broke outer page margins entirely and killed every up/down main-tab slide animation, because `dashboard-styles.css` requires `.view-wrapper` to be the keyed, direct child that `` toggles (compound selectors like `.{name}-enter-active.view-wrapper`; `.view-wrapper` is `position:absolute;inset:0`). - **Fix:** Restored the pre-restructure rendered DOM exactly via a single `` → `` → keyed statically-named per-route wrapper component (new `dashboardViewWrappers.ts`), as detailed in Decisions Made above. Deleted the now-unnecessary manual scroll-retention `Map`. - **Files modified:** neode-ui/src/views/dashboard/DashboardRouterView.vue, neode-ui/src/views/dashboard/dashboardViewWrappers.ts (new), neode-ui/src/views/dashboard/__tests__/keepAliveTabs.test.ts (added structural DOM-shape assertions) - **Verification:** Full test suite green; re-verified on the `:8100` dev preview against archi-dev — margins and slide/depth animations confirmed identical to pre-change; checkpoint approved. - **Committed in:** 26687055 --- **Total deviations:** 3 auto-fixed (2 Rule 1 test/implementation-detail bugs in Task 2, 1 Rule 1 bug caught by the Task 3 checkpoint and fixed before re-verification) **Impact on plan:** All three were necessary corrections within the plan's own stated files/scope. The Task 3 fix is the most consequential — it establishes the wrapper-component pattern (`dashboardViewWrappers.ts`) that 02-04 through 02-07 must reuse rather than re-deriving the original Task-1 design, and it establishes the hard "no visual change" rule for the rest of the phase. No scope creep. ## Issues Encountered - First Task 3 verification attempt failed on the real dev preview (broken margins, dead slide animations) — see Deviation 3 above. Resolved by restoring the pre-restructure DOM shape via statically-named wrapper components; re-verified and approved on the second attempt. ## User Setup Required None - no external service configuration required. ## Next Phase Readiness - The shared architecture (route classifier, `DashboardRouterView.vue` host, `dashboardViewWrappers.ts` factory, `onActivated` hook fix, `RefreshIndicator.vue`) is proven end-to-end on one tab and ready for 02-04 to widen `KEEP_ALIVE_PATHS` (a one-line change; wrapper names and `:include` derive from it automatically). - The onMounted/onActivated/onDeactivated side-effect audit precedent (Marketplace.vue: nothing needed beyond the composable's own reactivation) gives 02-04 a concrete template for auditing the remaining tabs. - HARD RULE for the rest of Phase 02: perf work must be visually invisible — verify against the real dev preview, not just unit tests, before considering a plan's checkpoint satisfied. - No blockers for 02-04. --- *Phase: 02-ui-performance* *Completed: 2026-07-30* ## Self-Check: PASSED