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

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
vue
keepalive
vue-router
sessionstorage
useCachedResource
transitions
phase provides
02-ui-performance 02-01's D-09 profiling findings (02-FINDINGS.md Ranked Fix Order) selecting the tracer tab
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
02-04
02-05
02-06
02-07
02-08
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:<path>) 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
created modified
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
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
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:<path> 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
Per-route KeepAlive wrapper components with static names (KeepWrap:<path>), 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
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.
id description requirement verification human_judgment rationale
D1 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 PERF-02
kind ref status
unit neode-ui/src/views/dashboard/__tests__/keepAliveTabs.test.ts pass
kind ref status
manual_procedural Task 3 checkpoint:human-verify, approved on :8100 dev preview against archi-dev after fix commit 26687055 pass
true 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 description requirement verification human_judgment
D2 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 PERF-02
kind ref status
unit neode-ui/src/composables/__tests__/useCachedResource.test.ts pass
false
id description requirement verification human_judgment
D3 RefreshIndicator renders nothing for ready/idle/loading and a labeled aria-live=polite element for refreshing, with no layout shift PERF-02
kind ref status
unit neode-ui/src/views/dashboard/__tests__/keepAliveTabs.test.ts pass
false
id description requirement verification human_judgment
D4 A rejected background refresh on Marketplace.vue keeps prior content on screen and raises no toast (D-07) PERF-02
kind ref status
unit neode-ui/src/views/__tests__/MarketplaceRefresh.test.ts pass
false
id description requirement verification human_judgment rationale
D5 Route transition animations (slide/depth) and page margins are unchanged from before the KeepAlive restructure PERF-02
kind ref status
unit neode-ui/src/views/dashboard/__tests__/keepAliveTabs.test.ts (structural DOM-shape assertions) pass
kind ref status
manual_procedural Task 3 checkpoint:human-verify, approved on :8100 dev preview against archi-dev after fix commit 26687055 pass
true 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
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 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:<path> wrapper components from dashboardViewWrappers.ts behind <KeepAlive :include>, 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:<path>) 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 <DashboardRouterView>
  • 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 <KeepAlive> 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 <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 (commit 26687055) 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 file dashboardViewWrappers.ts holds a memoized per-route-path factory of statically-named wrapper components (KeepWrap:<path>) 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-catalogpersist: true, ttlMs: 300000 (non-sensitive, small, near-static catalog data per D-06 discretion). bitcoin.prune-statuspersist: 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 <Transition> 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 <Transition> 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 <Transition><KeepAlive :include> → 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