21 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 | 02 | ui |
|
|
|
|
|
|
|
|
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. |
|
105min | 2026-07-30 | 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 in02-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_PATHSseeded with only the tracer tab's path,KEEP_ALIVE_MAX=6) deliberately avoids KeepAliveinclude/excludename-matching, since every route is an async component with no inferable name (RESEARCH A1). DashboardRouterView.vueextracted fromDashboard.vue's inline nested RouterView as the shared KeepAlive host every later plan builds on.onActivated(() => refreshIfStale())added touseCachedResource.ts, closing the reactivation gap for all 9 consumers (8 pre-existing + Marketplace).RefreshIndicator.vue: presentational,state-driven, renders nothing forready/idle/loading, anaria-live="polite"-labeled element forrefreshing, reserved-space so it never shifts layout.- Marketplace's catalog and Bitcoin prune-status fetches moved onto keyed
useCachedResourceentries (app-catalog,bitcoin.prune-status), each with an explicitpersistdecision. - 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:
- Task 1: One main tab survives a tab round-trip and revalidates on return -
385c9d86(feat, tdd) - Task 2: Subtle refresh indicator and correct per-visit behavior on the tracer tab -
a9a20039(feat, tdd) - Fix (post-checkpoint-failure): restore page margins and slide transitions broken by the restructure -
26687055(fix) - 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-routeKeepWrap:<path>wrapper components fromdashboardViewWrappers.tsbehind<KeepAlive :include>, keyed at the wrapper rootneode-ui/src/views/dashboard/dashboardViewWrappers.ts- New in the fix commit. Memoized per-route-path factory of statically-named wrapper components (KeepWrap:<path>) whose render emits the pre-restructureview-wrappermarkup byte-for-byte (full-bleed chat/mesh shape or padded default shape + spacer), so:includecan name-match without relying on async-component name inferenceneode-ui/src/composables/useCachedResource.ts- AddedonActivated(() => refreshIfStale())beside the existingonScopeDisposeregistrationneode-ui/src/views/dashboard/useRouteTransitions.ts-TAB_ORDERpromoted from module-privateconstto an export, for 02-04 to widenKEEP_ALIVE_PATHSfromneode-ui/src/views/Dashboard.vue- Inline nested RouterView block replaced with<DashboardRouterView>neode-ui/src/views/Marketplace.vue-loadCommunityMarketplace()/loadBitcoinPruneStatus()moved ontouseCachedResource(app-catalog,bitcoin.prune-status);RefreshIndicatorwired to a resourceloadStateneode-ui/src/components/RefreshIndicator.vue- Presentational background-refresh affordance,state: ResourceLoadState,label?: stringneode-ui/src/composables/__tests__/useCachedResource.test.ts- Reactivation revalidation, TTL-gated refetch, rejected-refresh keep-last-value coverageneode-ui/src/views/dashboard/__tests__/keepAliveTabs.test.ts- Round-trip mount/activation counts, detail-route exclusion, structural DOM-shape (view-wrapper/animation) pinningneode-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 inkeepAliveTabs.test.ts
Decisions Made
- Tracer tab: Marketplace.vue — top of
02-FINDINGS.mdRanked Fix Order (worst-measured revisit, 2033ms), matching the user's own "app store is slow" report. - Final
DashboardRouterView.vuetemplate shape (post-fix — differs from the plan's original design): The plan's original structure (a stable outer wrapper div +view-wrapperclass pushed onto each component root via fallthrough attrs, with<KeepAlive>nested inside that stable wrapper) broke page margins and killed every slide/depth animation on the real dev preview. Root cause:dashboard-styles.cssscopes every transition as a compound selector —.{transitionName}-enter-active.view-wrapper— and.view-wrapperitself isposition: absolute; inset: 0, both of which require.view-wrapperto be the keyed, direct child element that<Transition>toggles, not a class merged onto an arbitrary descendant. Splitting navigation across two sibling<Transition>branches behind a shared stable div broke that contract. Fix (commit26687055) restores the pre-restructure rendered DOM exactly: single<Transition :name="getTransitionName(route)">→<KeepAlive :max="KEEP_ALIVE_MAX" :include="[wrapper names]">→ a keyed per-route wrapper component (:key="route.path"). The new filedashboardViewWrappers.tsholds a memoized per-route-path factory of statically-named wrapper components (KeepWrap:<path>) whose render emits the old markup byte-for-byte — a keyeddiv.view-wrapperroot containing either the full-bleed chat/mesh shape or the padded-default shape plus the trailing spacer div. Caching is gated by:includename-matching against wrapper names derived fromKEEP_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 toKEEP_ALIVE_PATHSonly; the wrapper names and:includelist derive from it automatically. The Task-1 per-route scroll-retentionMapwas 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.tsnow includes a structural assertion pinning the padded-wrapper-inside-view-wrapperDOM shape as a regression backstop. - Side-effect audit (Marketplace.vue):
marketplaceAnimationDone(the one-shot intro flag) stays inonMounted— it is genuinely once-per-session. The catalog load and the prune-status load needed no per-viewonMounted/onActivated/onDeactivatedhooks of their own:useCachedResource's internalonActivated(added in Task 1) already revalidates them, staleness-gated, on every kept-alive reactivation. This view has no intervals, subscriptions, or window listeners, so noonDeactivatedteardown 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-verifywas approved by the user on the:8100dev preview against archi-dev, on the second attempt — after the margin/animation fix in26687055— 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.tswith 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.tsas a separate file, mirroring the existingCloudPeersRefresh.test.tsconvention 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 manualAbortSignal.timeout(8000)on its fetch. Once the call moved behinduseCachedResource, that manual timeout duplicated/conflicted with the composable's built-in abort-on-unmount signal. - Fix: Removed the manual timeout, matching the existing
Cloud.vueconvention for cached fetches in this codebase. - Files modified: neode-ui/src/views/Marketplace.vue
- Verification:
npm run testandnpm run type-checkgreen; behavior matches the in-repoCloud.vuereference 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-wrapperfallthrough onto each component root, two sibling<Transition>branches) broke outer page margins entirely and killed every up/down main-tab slide animation, becausedashboard-styles.cssrequires.view-wrapperto be the keyed, direct child that<Transition>toggles (compound selectors like.{name}-enter-active.view-wrapper;.view-wrapperisposition:absolute;inset:0). - Fix: Restored the pre-restructure rendered DOM exactly via a single
<Transition>→<KeepAlive :include>→ keyed statically-named per-route wrapper component (newdashboardViewWrappers.ts), as detailed in Decisions Made above. Deleted the now-unnecessary manual scroll-retentionMap. - 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
:8100dev 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.vuehost,dashboardViewWrappers.tsfactory,onActivatedhook fix,RefreshIndicator.vue) is proven end-to-end on one tab and ready for 02-04 to widenKEEP_ALIVE_PATHS(a one-line change; wrapper names and:includederive 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