From 350c06bcafd4abd16d160c5095ac6c296348bc4a Mon Sep 17 00:00:00 2001 From: archipelago Date: Thu, 30 Jul 2026 04:10:32 -0400 Subject: [PATCH] docs(02-ui-performance): research phase domain Codebase-grounded research for KeepAlive/SWR caching, serial-waterfall fixes, and the AIUI D-14 ride-along dependency risk. Co-Authored-By: Claude Fable 5 --- .../phases/02-ui-performance/02-RESEARCH.md | 737 ++++++++++++++++++ 1 file changed, 737 insertions(+) create mode 100644 .planning/phases/02-ui-performance/02-RESEARCH.md diff --git a/.planning/phases/02-ui-performance/02-RESEARCH.md b/.planning/phases/02-ui-performance/02-RESEARCH.md new file mode 100644 index 00000000..a083f15a --- /dev/null +++ b/.planning/phases/02-ui-performance/02-RESEARCH.md @@ -0,0 +1,737 @@ +# Phase 2: UI Performance - Research + +**Researched:** 2026-07-30 +**Domain:** Vue 3 SPA rendering/data-caching performance (client-side only; no new external dependencies) +**Confidence:** HIGH (codebase-verified for architecture/pitfalls; MEDIUM for the one external Vue-core issue citation; LOW/flagged for the AIUI ride-along, D-14) + + +## User Constraints (from CONTEXT.md) + +### Locked Decisions + +**Caching Approach (PERF-02)** +- **D-01:** Main tabs use BOTH `` around the RouterView (component instances, + scroll position, and in-page state survive tab switches) AND `useCachedResource` + stale-while-revalidate for their data. Today no `` exists anywhere — every + tab switch fully remounts the view. +- **D-02:** Which views get converted is decided by profiling (D-09), not blanket + conversion — "fixes are targeted, not guessed" (PERF-01). Already-fast views are left + alone. +- **D-03:** Heavy views (Mesh with its D3 graph + Leaflet map) ARE kept alive, but + KeepAlive's `max` instance cap is set so the oldest unused views evict — bounded memory + on low-power nodes. +- **D-04:** Secondary screens (AppDetails, server sub-pages, etc.) get `useCachedResource` + keyed per item (e.g. `app-details:${appId}`) but NO KeepAlive — unbounded item counts + would bloat an instance cache. Repeat opens paint instantly from the data cache. + +**Refresh / Staleness UX** +- **D-05:** Background refresh on a cached view shows a SUBTLE indicator (small + spinner/shimmer in the header/tab area driven by `loadState === 'refreshing'`) — not + invisible, not stale-age badges. +- **D-06:** Default TTL 30s (the hook's default); per-view tuning is Claude's discretion + (shorter for fast-moving data like mesh peers/sync status, longer for near-static data + like the app catalog). +- **D-07:** Background refresh failures are silent keep-last-value — cached data stays on + screen, retry on next focus/TTL; no toast. Errors surface only on explicit user refresh. +- **D-08:** sessionStorage persistence ON (instant paint after reload) except for large + payloads (file lists, media), which stay memory-only — the hook's `persist: false`. + +**Profiling & Verification (PERF-01, PERF-03)** +- **D-09:** Profiling starting set = ALL main surfaces: Apps/app store (+ AppDetails), + Mesh, Wallet (+ send flows), Cloud/Files, Server, Network, and Web5. User confirmed all + of these feel slow, "often app store". +- **D-10:** Profiling produces a COMMITTED findings doc in the phase directory: each slow + surface → measured cause (remount storm / serial RPC waterfall / uncached fetch) → + intended fix. Written and committed BEFORE fixes land. +- **D-11:** On-device verification target is **archi-dev-box**. Pass bar: NO visible + spinner/blank on revisit of a tab or secondary screen already visited this session; + first visits may still show loading. + +**Fix Scope** +- **D-12:** Backend (`core/`) changes are ADDITIVE ONLY: new aggregate/batch RPC endpoints + and cheap response-shaping are allowed when profiling names a backend cause; NO refactors + of existing handlers and NOTHING touching the orchestrator (keeps the lifecycle gate out + of play). +- **D-13:** Serial fetch waterfalls in views are fixed client-side by parallelizing + (`Promise.all`) plus rpc-client request dedup. Aggregate endpoints (per D-12) only where + a screen genuinely needs 3+ dependent calls. +- **D-14:** Folded-in AIUI UX defaults (small, in-scope ride-alongs): (a) the AIUI chat + starts EXPANDED, (b) on mobile AIUI starts on the CHAT view, not the context view + (current start-on-context is wrong). Everything else AIUI-related is deferred. +- **D-15:** Deploy discipline: dev pair only this phase — no OTA. Fleet rollout rides the + normal release train later. + +### Claude's Discretion +- Per-view TTL values (D-06). +- KeepAlive `max` cap value and eviction tuning (D-03). +- Choice between client-side parallelization and a new aggregate endpoint per screen, + within D-12/D-13 bounds. +- Exact placement/styling of the subtle refresh indicator (D-05), consistent with the + existing design system. + +### Deferred Ideas (OUT OF SCOPE) +- AIUI permissioned node access, AIUI content deep-dive, AIUI peer media, IndeeHub + cross-node content source with payments, AIUI Nostr integration polish. (The two small + AIUI UX defaults — chat starts expanded; mobile starts on chat — were folded INTO this + phase as D-14 and are NOT deferred.) + + + +## Phase Requirements + +| ID | Description | Research Support | +|----|-------------|------------------| +| PERF-01 | Slowest tab switches/secondary-screen opens profiled, causes named (remount storm / serial RPC waterfall / uncached fetch), before fixes land | See "Concrete Findings Per Surface" below — a code-level pre-scan of every D-09 surface, with the actual `onMounted` patterns found, to seed (not replace) the required profiling pass. `## Validation Architecture` maps this to a committed findings doc, not an automated test. | +| PERF-02 | Main-tab switches render instantly from cache, refresh in background | `## Architecture Patterns` (KeepAlive + RouterView slot pattern for THIS codebase's actual nested router-view), `## Common Pitfalls` (the `onActivated` gap in `useCachedResource`, the async-component name-matching KeepAlive bug, the `:key="route.path"` trap already in `Dashboard.vue`) | +| PERF-03 | Secondary screens open without blocking reload, instant on repeat visit, verified on real hardware | `## Don't Hand-Roll` (reuse `useCachedResource` keyed per item, per D-04 — no KeepAlive), `## Environment Availability` (archi-dev-box reachability confirmed this session) | + + +## Summary + +This phase is almost entirely a **client-side Vue 3 architecture fix**, not a new-library +adoption — the caching primitive (`useCachedResource` + `resources` Pinia store) and the +dedup primitive (`rpc-client`'s `dedup: true`) already exist and are production-proven +across 8 views. The gap is structural: (1) nothing in the app tree uses `` yet, +so every tab switch fully unmounts/remounts, and (2) several of the D-09 "feels slow" +surfaces still fetch data with plain `onMounted` + raw `rpcClient.call` instead of the +cached hook, or fire genuinely serial `await` chains. + +The single most important ground-truth finding this research turned up — and the one the +planner most needs — is that **the actual KeepAlive insertion point is NOT `App.vue`'s +outer `RouterView`** (which CONTEXT.md's canonical_refs points to). `App.vue`'s +`RouterView` only ever renders `OnboardingWrapper.vue` or `Dashboard.vue` — those don't +remount on tab switch. The real per-tab mount/unmount churn happens in a **second, +nested** `` inside `Dashboard.vue` (`neode-ui/src/views/Dashboard.vue:87`), +which is also keyed by `route.path` for its `` and branches into two different +wrapper `
`s (a chat/mesh branch and a default branch). `` must wrap +`` directly inside THAT structure, in both branches, and the +current `:key="route.path"` placement needs to move so it doesn't recreate the cached +component itself. This is a bigger template restructure than "wrap RouterView" implies, +and the plan should scope a dedicated task for it. + +The second load-bearing finding is a genuine gap in `useCachedResource` itself: KeepAlive's +`onActivated`/`onDeactivated` hooks are not currently used anywhere in the hook. Because a +KeepAlive'd component is *deactivated*, not unmounted, on tab-away, `onScopeDispose` never +fires — the `window.addEventListener('focus', ...)` and store subscription stay live for +every kept-alive tab simultaneously (a minor traffic consideration), AND, more importantly, +there is currently no mechanism that re-checks staleness when a KeepAlive'd tab is +*reactivated* (switched back to) — only on window focus or on first mount. Without adding +an `onActivated(() => refreshIfStale())` call to the hook, D-01's "background refresh on +revisit" behavior will not actually trigger for KeepAlive'd main tabs. This is safe to add +unconditionally (Vue no-ops these hooks outside a `` boundary), so it benefits +both main tabs and (harmlessly) secondary screens using the same hook. + +**Primary recommendation:** Fix the Dashboard.vue nested-RouterView structure first (the +KeepAlive host) and extend `useCachedResource` with `onActivated`-driven revalidation +before converting any individual view — both are one-time, shared foundation work that +every per-surface fix in PERF-01's findings doc will depend on. Then profile the D-09 +surfaces, convert only what profiling names (D-02), and fix any serial `await` chains with +`Promise.all`/`Promise.allSettled` (a real instance already found in +`ContainerAppDetails.vue`, detailed below). + +## Architectural Responsibility Map + +| Capability | Primary Tier | Secondary Tier | Rationale | +|------------|-------------|----------------|-----------| +| Main-tab component-instance/scroll persistence (KeepAlive) | Browser/Client | — | Pure client-side SPA (no SSR); Vue's built-in `` owns instance lifecycle | +| Main-tab & secondary-screen data caching (SWR) | Browser/Client | — | `useCachedResource` + `resources` Pinia store already own this; extend, don't replace | +| In-flight request dedup | Browser/Client (rpc-client) | — | `rpc-client.ts`'s `dedup: true` option already implements this | +| Serial-waterfall fixes (client parallelization) | Browser/Client | — | `Promise.all`/`Promise.allSettled` around existing independent fetch calls | +| New aggregate/batch RPC endpoints (only for 3+ dependent calls, D-12/D-13) | API/Backend (`core/`) | Browser/Client (consumes via existing rpc-client) | Additive-only per D-12; the calling view still owns caching/dedup client-side | +| Profiling & measurement (PERF-01) | Browser/Client (DevTools Performance panel, Vue devtools timeline) | — | No backend instrumentation is required or in scope | +| AIUI chat-expanded / mobile-starts-on-chat defaults (D-14) | External app (AIUI, separate repo/container image) | Browser/Client (neode-ui passes iframe URL query params) | AIUI's own source is NOT in this checkout — see Open Questions | + +## Standard Stack + +### Core +| Library | Version | Purpose | Why Standard | +|---------|---------|---------|--------------| +| Vue 3 `` | 3.5.24 (built-in, already installed) [VERIFIED: neode-ui/package.json] | Cache main-tab component instances across route changes | Native Vue primitive purpose-built for exactly this; no reason to hand-roll | +| Vue Router `v-slot="{ Component, route }"` on `` | vue-router 4.6.3 (already installed) [VERIFIED: neode-ui/package.json] | Only supported way to interpose ``/`` between the router and the rendered view in Vue Router 4 | Directly wrapping `` with `` does not work in Vue Router 4 — the scoped-slot form is required [CITED: router.vuejs.org/guide/advanced/router-view-slot] | +| `useCachedResource` composable | project-internal, `neode-ui/src/composables/useCachedResource.ts` | SWR data layer: memory → sessionStorage → fetch, sticky-ready, keep-last-value, focus revalidation, abort-on-unmount | Already proven across 8 views (Monitoring, Cloud, Server, Federation, Credentials, Web5, FIPS cards) — the pattern to extend, per canonical_refs | +| `resources` Pinia store | project-internal, `neode-ui/src/stores/resources.ts` | Shared cache backing the hook; in-flight dedup per key, invalidate/subscribe | Backing store for the hook above — don't create a second cache layer | +| `rpc-client.ts` `dedup: true` | project-internal | Collapses concurrent identical calls (same method+params) into one request | Already implemented; use for any newly-parallelized fetch group so duplicate concurrent calls collapse | +| Native `Promise.all` / `Promise.allSettled` | JS built-in | Parallelize independent RPC calls instead of serial `await` chains | No library needed; D-13 explicitly calls for this over any queue/batching library | + +### Supporting +| Library | Version | Purpose | When to Use | +|---------|---------|---------|-------------| +| Browser Performance API (`performance.mark`/`performance.measure`) | Web platform, no install | Instrument tab-switch/secondary-screen-open timings for the PERF-01 findings doc | When profiling needs a repeatable, loggable number rather than just "felt slow in DevTools" | +| Chrome/Chromium DevTools Performance panel | Browser built-in | Visual flame-graph profiling of remount storms and RPC waterfalls | Primary profiling tool for D-10's findings doc — no new tooling to install | +| Vue devtools (browser extension) | Already used by the team per existing dev workflow | Component render/mount timeline, KeepAlive cache inspection | Useful to visually confirm `` is actually caching the intended component names | + +### Alternatives Considered +| Instead of | Could Use | Tradeoff | +|------------|-----------|----------| +| `` | Custom `v-show`-based tab persistence (render all main tabs simultaneously, toggle visibility) | Avoids the KeepAlive/async-component name-matching bug entirely, but keeps ALL main-tab DOM (D3 graph, Leaflet map, all lists) mounted and reactive permanently — worse memory/CPU on low-power nodes than a capped KeepAlive; rejected, out of step with D-03's bounded-memory requirement | +| Client-side `Promise.all` (D-13's default) | New aggregate RPC endpoint per screen | Aggregate endpoints reduce round-trips further but require backend changes (additive-only per D-12) and add a new response shape to maintain; reserve for screens with 3+ genuinely dependent calls, per D-13 | +| `useCachedResource` extension | A dedicated third-party SWR library (e.g. a Vue port of `swr`/`vue-query`) | Would duplicate functionality the project already built and tuned (sticky-ready, sessionStorage snapshot, keep-last-value) and adds an external dependency for zero net capability gain — explicitly against the phase's canonical_refs guidance to extend, not reinvent | + +**Installation:** +```bash +# No new packages required — KeepAlive is Vue 3 core, Promise.all is native JS, +# useCachedResource/resources/rpc-client already exist in the codebase. +``` + +**Version verification:** `neode-ui/package.json` pins `"vue": "^3.5.24"` and +`"vue-router": "^4.6.3"` [VERIFIED: package.json read directly]. The async-component +KeepAlive `include`/`exclude` name-matching bug (vuejs/core #7533 / #11764) was reported +fixed by extracting the component name from `__asyncResolved` rather than the wrapper +vnode; treat as fixed at 3.5.24 but confirm with a quick manual smoke test on the actual +lazy-loaded routes in this repo before relying on `include`/`exclude` filtering (see +Common Pitfalls #1) [CITED: github.com/vuejs/core/issues/11764]. + +## Package Legitimacy Audit + +**No external packages are being installed for this phase.** Every primitive needed +(``, `Promise.all`/`Promise.allSettled`, `useCachedResource`, `resources` store, +`rpc-client` dedup) is either Vue 3 core, a JS language built-in, or already present and +in production use in this codebase. The Package Legitimacy Gate protocol is not applicable +— skip the registry/postinstall checks; there is nothing new to vet. + +**Packages removed due to [SLOP] verdict:** none (n/a — no new packages). +**Packages flagged as suspicious [SUS]:** none (n/a — no new packages). + +## Architecture Patterns + +### System Architecture Diagram + +``` +User clicks a tab / router-link + │ + ▼ +Vue Router navigates (App.vue's OUTER RouterView is untouched — it only +distinguishes OnboardingWrapper vs Dashboard, both of which stay mounted) + │ + ▼ +Dashboard.vue's NESTED ◄── the + │ real + ▼ remount + point + │ (line ~87, + ▼ neode-ui/src/ + ◄── NEW views/ + │ Dashboard.vue) + ├── cache HIT (tab visited this session) ──► instance reactivated + │ │ (onActivated fires) + │ ▼ + │ useCachedResource.refreshIfStale() re-checked on activation ◄── NEW + │ │ (extension to the hook) + │ ├── fresh (< TTL) ──────────────────► render immediately, no RPC + │ └── stale (≥ TTL) ──► background refresh (loadState='refreshing') + │ │ subtle indicator (D-05) + │ ▼ + │ rpc-client.call({ dedup: true }) + │ │ + │ ▼ + │ Backend RPC (core/, additive-only, D-12) + │ + └── cache MISS (first visit this session) ──► fresh mount + │ + ▼ + useCachedResource: memory → sessionStorage snapshot → fetch + │ (paints instantly if a snapshot exists, + │ D-08, except persist:false payloads) + ▼ + Parallel fetch (Promise.all / Promise.allSettled, D-13) instead + of serial await chains — fixes the ContainerAppDetails.vue-style + waterfall found below + │ + ▼ + rpc-client.call({ dedup: true }) → Backend RPC → render + +Secondary screens (AppDetails, ContainerAppDetails, server sub-pages, ...): +same nested RouterView, but the component is OUTSIDE (or excluded from) the +KeepAlive boundary (D-04) — full mount/unmount each visit, but +useCachedResource keyed per item (e.g. `app-details:${appId}`) still paints +instantly from the data cache on repeat opens. +``` + +### Recommended Project Structure +``` +neode-ui/src/ +├── views/ +│ ├── Dashboard.vue # MODIFY: nested RouterView gets KeepAlive +│ ├── dashboard/ +│ │ └── useRouteTransitions.ts # existing TAB_ORDER / isDetailRoute — extend +│ │ # isDetailRoute if new secondary routes need +│ │ # explicit KeepAlive exclusion (see Pitfall #7) +│ ├── Apps.vue, Mesh.vue, Cloud.vue, Server.vue, ... # CONVERT per profiling (D-02) +│ ├── AppDetails.vue, ContainerAppDetails.vue, ... # keyed useCachedResource, +│ │ # no KeepAlive (D-04) +├── composables/ +│ └── useCachedResource.ts # MODIFY: add onActivated(refreshIfStale) hook +├── stores/ +│ └── resources.ts # unchanged — already supports this +└── api/ + └── rpc-client.ts # unchanged — dedup already supported +``` + +### Pattern 1: KeepAlive + RouterView scoped slot (the only correct Vue Router 4 form) +**What:** Vue Router 4 removed the ability to simply wrap `` with +``/`` — the scoped-slot form is mandatory. +**When to use:** The single nested `` in `Dashboard.vue` that renders all +tab/secondary-screen content. +**Example:** +```vue + + + + + + + + +``` +Note: `:key="route.path"` is safe to keep on `` itself here — for the fixed set +of main-tab paths (`/dashboard/apps`, `/dashboard/mesh`, ...) the path is stable per view, +so KeepAlive still matches the same cache slot on revisit. It only becomes a problem if a +secondary/detail route with a *varying* `:id` param is included in the same KeepAlive +boundary (each id would get its own cache slot) — which is exactly why D-04 excludes +secondary screens from KeepAlive entirely. + +`Dashboard.vue`'s current template (`neode-ui/src/views/Dashboard.vue:87-118`) branches +into two different wrapper `
`s depending on `route.path` (a chat/mesh branch with +different classes, and a default branch with scroll-container classes). Both branches wrap +`` — the KeepAlive needs to live inside BOTH branches (or the +branching needs to move to apply classes to a single shared wrapper so one KeepAlive +covers both cases). This is real template surgery, not a one-line wrap. + +### Pattern 2: `useCachedResource` with `onActivated` revalidation (extension needed) +**What:** Add reactivation-triggered staleness checks so KeepAlive'd tabs actually +background-refresh on revisit, not just on window focus. +**When to use:** Inside `useCachedResource` itself, so every consuming view benefits +without per-view changes. +**Example:** +```typescript +// Source: neode-ui/src/composables/useCachedResource.ts (existing) + Vue's onActivated +// (vuejs.org/api/composition-api-lifecycle.html#onactivated) — safe no-op outside +// , so this is safe to add unconditionally for every consumer. +import { onActivated } from 'vue' +// ... inside useCachedResource, alongside the existing onScopeDispose block: +if (getCurrentScope()) { + onActivated(() => refreshIfStale()) +} +``` +This mirrors the existing `if (opts.immediate ?? true) refreshIfStale()` call at setup +time — `onActivated` also fires on first mount (per Vue's lifecycle docs), so the two +calls are redundant-but-harmless on first mount (the store's `inflight` map dedupes them) +and the ONLY source of a stale-check on every subsequent reactivation. + +### Pattern 3: Parallelize a serial `onMounted` waterfall +**What:** Replace sequential `await` chains with `Promise.all`/`Promise.allSettled`. +**When to use:** Any `onMounted` (or other lifecycle hook) that awaits independent RPC +calls one after another with no data dependency between them. +**Example — the actual bug found in this codebase, `ContainerAppDetails.vue:169-172`:** +```typescript +// BEFORE (serial waterfall — 3 sequential round-trips): +onMounted(async () => { + await loadContainer() + await loadLogs() + await loadHealthStatus() +}) + +// AFTER (parallel — same 3 calls, one round-trip's worth of latency): +onMounted(async () => { + await Promise.allSettled([loadContainer(), loadLogs(), loadHealthStatus()]) +}) +``` +`loadContainer`/`loadLogs`/`loadHealthStatus` each set their own `loading` ref +independently and don't read each other's results, so this is a safe parallelization — +confirm this per-screen during profiling (D-02), don't blanket-apply. + +### Anti-Patterns to Avoid +- **Wrapping `App.vue`'s outer `` with `` and calling it done:** + this RouterView only ever swaps `OnboardingWrapper.vue` ↔ `Dashboard.vue` ↔ 404 — it + doesn't remount on tab switch. The real fix must land in `Dashboard.vue`'s nested + RouterView. Fixing the wrong RouterView would look correct in code review but produce + zero observed improvement, since `Dashboard.vue` itself was never the thing remounting. +- **Blanket `` with no `include`/`max`:** caches every route ever visited + forever — unbounded memory on low-power fleet nodes, and defeats D-04 (secondary + screens must NOT be instance-cached). +- **Hand-rolling a second cache layer per view** (a local `ref` + manual + `sessionStorage.getItem`) instead of extending `useCachedResource` — this is exactly the + duplication the canonical_refs explicitly warn against. +- **Treating `onMounted` as "runs once per view visit" once KeepAlive is added:** it does + not — `onMounted` only fires on first mount. Any per-visit logic (scroll restore, + one-shot animation flags like `Apps.vue`'s `appsAnimationDone`, `Server.vue`'s + `connectionTimer` setup) that assumed "onMounted = every view entry" must move to + `onActivated`/`onDeactivated` once that view is KeepAlive'd, or it silently stops running + on revisit. + +## Don't Hand-Roll + +| Problem | Don't Build | Use Instead | Why | +|---------|-------------|-------------|-----| +| Stale-while-revalidate data cache | A new per-view `ref` + manual `sessionStorage` read/write | `useCachedResource` (extend with `onActivated`, Pattern 2 above) | Already handles sticky-ready, keep-last-value-on-error, focus revalidation, abort-on-unmount, sessionStorage snapshotting — reinventing it per-view is exactly the "8 views already use it, main tabs just don't yet" gap this phase closes | +| Component-instance persistence across route changes | A custom `v-show`-based always-mounted tab strip, or manual component-instance caching via a `Map` | Vue's built-in `` | Native primitive with `max`/LRU eviction, `include`/`exclude` name matching, and `activated`/`deactivated` hooks purpose-built for this | +| In-flight request dedup | A custom promise-cache keyed by method+params | `rpc-client.ts`'s `dedup: true` option | Already implemented and used elsewhere (`Cloud.vue`, `Server.vue` pass `dedup: true` on several calls already) | +| Parallel-fetch orchestration | A custom fetch queue/batcher | `Promise.all`/`Promise.allSettled` | Native JS; D-13 explicitly scopes fixes to this, reserving new endpoints for 3+ dependent calls only | +| Performance measurement | Custom timing/telemetry instrumentation shipped to a backend | Browser DevTools Performance panel + `performance.mark`/`measure` (local, not shipped anywhere) | PERF-01 needs a committed findings doc, not a telemetry pipeline; out of scope per D-12 (no orchestrator/backend refactors) | + +**Key insight:** This phase has zero legitimate reasons to add a new library. The +project already built (and 8 views already prove out) the exact SWR primitive this phase +needs; the only real gaps are (1) nobody has wired `` into the actual remount +point yet, and (2) the SWR hook itself needs one small extension (`onActivated`) to work +correctly once component instances stop being destroyed on tab switch. + +## Common Pitfalls + +### Pitfall 1: KeepAlive `include`/`exclude` name-matching bug with async (lazy-loaded) route components +**What goes wrong:** ``'s name matching can fail to recognize +async components (i.e. anything loaded via `component: () => import(...)`) — either +caching everything regardless of `include`, or caching nothing. +**Why it happens:** Vue historically matched names against the async-component wrapper +vnode rather than the resolved inner component; ALL 44 routes in this codebase's +`router/index.ts` use `component: () => import(...)` [VERIFIED: grep of +neode-ui/src/router/index.ts], so this bug class applies directly here if unpatched. +**How to avoid:** The fix (extracting the name from `__asyncResolved`) is reported merged +and the project pins `vue: ^3.5.24`, which should include it [CITED: +github.com/vuejs/core/issues/11764] — but confirm with a manual smoke test (navigate to +an `include`-listed tab, confirm no refetch/remount on revisit; navigate to a +NOT-included tab, confirm it DOES remount) before trusting `include`/`exclude` filtering +in this codebase. If it misbehaves, `` with no `include`/`exclude` at +all (letting LRU eviction do the exclusion work) is documented as working correctly with +async components (only `include`/`exclude` were broken), and is a safe fallback for this +phase. +**Warning signs:** A tab listed in `include` still shows a spinner/remounts every visit +during manual verification, or a tab NOT in `include` unexpectedly retains scroll +position/state across visits. + +### Pitfall 2: The real remount point is nested inside `Dashboard.vue`, not `App.vue` +**What goes wrong:** Wrapping `App.vue`'s outer `` (line ~8, per CONTEXT.md's +canonical_refs) produces no visible improvement. +**Why it happens:** `App.vue`'s RouterView only ever swaps between top-level route +components (`OnboardingWrapper.vue`, `Dashboard.vue`, `NotFound.vue`) — Dashboard itself +stays mounted for the entire authenticated session. The actual per-tab remount churn is a +SECOND, nested `` inside `Dashboard.vue` (`neode-ui/src/views/Dashboard.vue:87`), +which ALSO keys its wrapper `
` by `route.path` for `` purposes and +branches into two different template paths (chat/mesh vs. everything else). +**How to avoid:** Scope the KeepAlive work explicitly against `Dashboard.vue`'s nested +RouterView, in both of its template branches. Update the phase's canonical reference for +future agents. +**Warning signs:** After "adding KeepAlive," tab switches still show the intro +animation/spinner every time, or profiling still shows a full component-tree teardown on +tab switch. + +### Pitfall 3: `useCachedResource` has no reactivation hook — background refresh silently stops working once KeepAlive lands +**What goes wrong:** D-01's "renders from cache immediately, refreshes in background" only +half-works: the cache-hit instant paint works (KeepAlive preserves the mounted instance +and its data), but the "refreshes in background" half depends on `refreshIfStale()` being +re-invoked on revisit — and today that only happens at setup time (once) and on window +`focus` events (which don't fire on in-SPA tab switches). +**Why it happens:** The hook was written before any component used KeepAlive, so it never +needed `onActivated`/`onDeactivated`. `onScopeDispose` (used for cleanup today) never fires +for a KeepAlive'd component on tab-away — the component is deactivated, not unmounted. +**How to avoid:** Add `onActivated(() => refreshIfStale())` to the hook (Pattern 2 above) — +safe to do unconditionally since Vue no-ops these hooks for components outside a +`` boundary. +**Warning signs:** A KeepAlive'd tab shows correct instant-paint on revisit but NEVER shows +the subtle "refreshing" indicator (D-05) even after the TTL has clearly elapsed, and data +visibly goes stale (e.g. mesh peer status frozen at whatever it was on last visit). + +### Pitfall 4: `onMounted`-based one-shot logic breaks silently once a view is KeepAlive'd +**What goes wrong:** Per-visit setup that assumed `onMounted` fires on every tab entry +(scroll restoration, connection-timeout timers, animation-completion flags) stops running +after the first visit. +**Why it happens:** `onMounted` fires exactly once per component instance; KeepAlive +reuses the instance, so it fires exactly once, ever, for that tab's lifetime in the +session. Concretely: `Apps.vue`'s `connectionTimer` (a 15s timeout that only arms once, +`onMounted`) and `Server.vue`'s 7-call `onMounted(() => { checkTorStatus(); ... +loadFipsSummary() })` initializer would both need review — the latter is fine to leave in +`onMounted` if all 7 calls should only ever run once per session (first visit), but WOULD +need to move to `onActivated` if any of them should re-run/re-check on every tab revisit. +**How to avoid:** During each view's conversion (D-02), explicitly decide per side-effect: +"once ever" stays in `onMounted`; "every visit" moves to `onActivated`. +**Warning signs:** A feature that worked on first tab visit stops updating/re-arming on +subsequent visits within the same session. + +### Pitfall 5: Serial `await` chains masquerading as "already async" +**What goes wrong:** Code that looks async-savvy (`async function`, `await` everywhere) +can still be a serial waterfall if each `await` blocks the next independent call. +**Why it happens:** Found concretely in `ContainerAppDetails.vue:169-172`: +`await loadContainer(); await loadLogs(); await loadHealthStatus()` — three independent +RPC-backed loads, each fully round-tripping before the next starts. `Server.vue`'s +`onMounted` (non-async, fire-and-forget calls) and `Mesh.vue`'s `onMounted` +(`await Promise.all([...])`) are both already correctly parallel and should NOT be +"fixed" — profiling must distinguish real waterfalls from already-parallel code before +touching it (D-02). +**How to avoid:** `Promise.all`/`Promise.allSettled` for independent loads (Pattern 3). +**Warning signs:** DevTools Network/Performance panel shows RPC calls for one screen +starting one-after-another rather than overlapping. + +### Pitfall 6: KeepAlive memory growth on low-power fleet nodes +**What goes wrong:** Every kept-alive tab holds its full component tree (refs, computed +caches, and for Mesh specifically a live D3 force-graph + Leaflet map instance) in memory +indefinitely. +**Why it happens:** `` with no `max` never evicts. Mesh.vue is 2,651 lines and +pulls in both `d3` and `@vue-leaflet/vue-leaflet`/`leaflet` [VERIFIED: +neode-ui/package.json + neode-ui/src/views/Mesh.vue] — one of the heaviest views in the +app, and one D-03 explicitly wants kept alive anyway (with eviction). +**How to avoid:** Set `max` per D-03 (Claude's discretion on the exact number — this +research recommends starting around 8, one less than the 10 entries in `TAB_ORDER` from +`useRouteTransitions.ts`, so the least-recently-used main tab evicts under memory +pressure rather than every tab staying resident forever) and verify actual memory +behavior on archi-dev-box, not just the dev workstation, per D-11. +**Warning signs:** Memory growth over a session of visiting many tabs; sluggishness +returning after extended use even though individual tab switches feel fast at first. + +### Pitfall 7: `isDetailRoute` (the existing "is this a secondary screen" helper) doesn't cover every secondary screen +**What goes wrong:** `neode-ui/src/views/dashboard/useRouteTransitions.ts`'s +`isDetailRoute()` only recognizes `/apps/:id` and `/marketplace/:id` as detail routes. +Routes like `cloud/:folderId`, `cloud/peers/:peerId?`, `server/openwrt`, `goals/:goalId`, +`app-session/:appId`, `web5/credentials`, `web5/networking-profits`, +`apps/lnd/channels` are ALSO secondary screens per the phase's own definition +("screens reached from a tab's main page") but this helper won't flag them. +**Why it happens:** The helper was written for background-image/transition purposes, not +for KeepAlive scoping, and was never meant to be an exhaustive secondary-screen registry. +**How to avoid:** Don't reuse `isDetailRoute` as the source of truth for KeepAlive +`include`/`exclude`. Build the KeepAlive `include` list explicitly from the known main-tab +component names (Home, Apps, Mesh, Cloud, Server, Web5, Marketplace/Discover, Chat, +Settings, Fleet — the `TAB_ORDER` set) rather than trying to derive "not a detail route" +generically. +**Warning signs:** A secondary screen unexpectedly gets cached (component instance +survives navigating away and back) because it slipped through an overly broad `include` +pattern. + +## Code Examples + +### Existing SWR usage to mirror (already production code — do not reinvent) +```typescript +// Source: neode-ui/src/views/Cloud.vue (existing, verified) — the pattern main tabs +// being converted should follow. +const peersResource = useCachedResource({ + key: 'cloud.peers', + fetcher: (signal) => rpcClient.call({ method: 'federation.list-nodes', signal, dedup: true }), + // ttlMs left at hook default (30s) here; tune shorter/longer per D-06 as needed +}) + +async function loadPeers() { + await peersResource.refresh() + const e = peersResource.entry + if (e.error) loadError.value = e.error // keep-last-known-value; surface error in a banner, not a toast +} +``` + +### rpc-client dedup — already available, use for any newly-parallelized group +```typescript +// Source: neode-ui/src/api/rpc-client.ts (existing) +const res = await rpcClient.call<{ interfaces: NetworkInterface[] }>({ + method: 'network.list-interfaces', + signal, + dedup: true, // collapses concurrent identical calls from multiple mounted consumers + maxRetries: 1, +}) +``` + +## State of the Art + +| Old Approach | Current Approach | When Changed | Impact | +|--------------|------------------|---------------|--------| +| Every tab switch fully unmounts/remounts the view (`fetch-on-mount` every visit) | `` + `useCachedResource` stale-while-revalidate | This phase | Instant re-paint from cached instance + data on revisit; RPC only fires when actually stale | +| Ad-hoc per-view fetch-on-mount with raw `rpcClient.call` (no cache) | `useCachedResource` keyed resource, shared `resources` Pinia store | Already the pattern for 8 views (Monitoring, Cloud, Server, Federation, Credentials, Web5, FIPS cards); this phase extends it to main tabs + remaining secondary screens | Consistent stale-while-revalidate behavior app-wide instead of two different data-loading philosophies coexisting | +| Serial `await` chains for independent loads (e.g. `ContainerAppDetails.vue`) | `Promise.all`/`Promise.allSettled` | This phase, per-surface as profiling names it (D-02/D-13) | Removes N×round-trip latency stacking into N round-trips' worth | + +**Deprecated/outdated:** N/A — no library version is being deprecated; this is closing a +gap between an already-modern pattern (used in 8 views) and the views that predate it +(the main tabs, added before `useCachedResource` existed). + +## Assumptions Log + +| # | Claim | Section | Risk if Wrong | +|---|-------|---------|---------------| +| A1 | Vue 3.5.24 (as pinned, `^3.5.24`) includes the fix for the KeepAlive `include`/`exclude` async-component name-matching bug (vuejs/core #7533/#11764) | Standard Stack, Pitfall 1 | If not actually fixed at the resolved `^3.5.24` patch version, `include`/`exclude`-based KeepAlive scoping could silently cache the wrong views or none at all; the `max`-only fallback noted in Pitfall 1 is the mitigation — verify manually before trusting `include` | +| A2 | KeepAlive `max` starting value of ~8 is a reasonable default for archi-dev-box-class hardware | Pitfall 6 | Too high risks memory growth on low-power fleet nodes (D-03's stated concern); too low risks evicting a tab the user just switched away from, defeating the "instant on revisit" goal — this is explicitly Claude's discretion per CONTEXT.md and should be tuned against real on-device memory profiling (D-11), not just picked and shipped | +| A3 | The 7 unawaited-but-concurrent calls in `Server.vue`'s `onMounted` (line 831) are all safe to run in parallel with no ordering dependency | "Concrete Findings" implied by Pitfall 5 | If any of the 7 (`checkTorStatus`, `loadNetworkData`, `loadInterfaces`, `loadDiskStatus`, `loadTorServices`, `loadVpnPeers`, `loadFipsSummary`) secretly depends on another's side effect, treating this as "already fine, don't touch" during profiling could miss a real bug — profiling (D-10) should still name Server explicitly rather than skip it on the assumption this code is already optimal | + +**AIUI ride-along (D-14) is tracked as an Open Question, not an Assumption, below** — +it's a missing-dependency finding (verified via filesystem check), not a training-data +guess. + +## Open Questions + +1. **Where does the AIUI source actually live, and does it already support the D-14 + query-param-style controls?** + - What we know: `apps/aiui/manifest.yml` only describes a prebuilt container image + (`localhost/archipelago-aiui:latest`) — no source in this checkout. `neode-ui`'s own + `package.json` `dev:mock` script and `scripts/setup-aiui-server.sh` both reference a + SIBLING repo at `../../AIUI` (i.e., `/AIUI`) as AIUI's real source, + with `dev:mock` gracefully falling back to "chat will show placeholder" when it's + absent [VERIFIED: read of both files]. A direct filesystem check on this machine + found NO `AIUI` directory next to `archy` [VERIFIED: `ls` of the parent directory]. + `Chat.vue` already constructs the iframe URL with query params + (`embedded=true&hideClose=true&mockArchy=1&seed=1`), so the mechanism for passing a + "start expanded" / "start on chat view" flag into AIUI from neode-ui plausibly exists + as a pattern, but whether AIUI's OWN code currently reads/honors such params for + expanded-state or mobile-view-default is unknown without that source. + - What's unclear: Whether AIUI's source is checked out on the machine that will + actually execute this phase's plans (the user's memory notes the ThinkPad, `.116`, as + the primary build server — AIUI may live there and not on whatever machine ran this + research), and whether it already has a query-param or postMessage hook for these two + UX defaults or needs new code added on the AIUI side. + - Recommendation: The planner should add an early checkpoint/precondition task for D-14 + confirming AIUI source location and current param support BEFORE scoping the actual + UX-default change as line-of-code work — this is a blocking dependency the phase + cannot resolve from within `archy` alone if the source truly isn't reachable from the + execution environment. + +2. **What is the precise profiling methodology/output format for the D-10 findings doc?** + - What we know: D-10 requires a COMMITTED doc, written and committed before fixes land, + mapping each slow surface → measured cause → intended fix, for the D-09 surface list. + - What's unclear: Whether "measured" means DevTools Performance-panel screenshots/traces + attached in the doc, `performance.mark`/`measure` numbers pasted in, or a narrative + description backed by code inspection (as this research itself did for several + surfaces, below). + - Recommendation: Treat this research's "Concrete Findings Per Surface" (next section) + as a starting hypothesis set, not a substitute for D-10's own profiling pass — the + plan should have the profiling task actually run DevTools/Performance-API timing on + archi-dev-box (or at minimum the dev workstation with network throttling toward + archi-dev) and record real numbers, since several "looks fine in code" surfaces + (e.g. `Mesh.vue`'s already-parallel `Promise.all`) may still be slow for reasons this + static read cannot see (render cost of the D3 graph, RPC latency to a real node vs. + the mock backend, etc.). + +## Concrete Findings Per Surface (seed data for the required PERF-01 profiling pass) + +Code-level pre-scan of each D-09 surface, to speed up (not replace) profiling: + +| Surface | Uses `useCachedResource` today? | `onMounted` fetch pattern found | Likely cause category (to CONFIRM by profiling) | +|---|---|---|---| +| Apps (`Apps.vue`) | No — reads from `useAppStore`'s WebSocket-pushed `packages` state, no per-mount RPC | `onMounted` only arms a 15s connection-timeout timer; no fetch | Likely **remount storm only** (loses scroll/search/tab-selection state and re-runs list sort/filter/animation on every visit) — data itself is already live via WebSocket, not refetched | +| Marketplace/Discover (the "app store" the user called out specifically) | Not checked this session | Not checked this session | **Needs direct profiling** — user said "often app store," but Apps.vue itself looks WebSocket-backed; the actual store/discover views need their own pass, not inherited from Apps.vue's analysis | +| Mesh (`Mesh.vue`, 2,651 lines, D3 + Leaflet) | No | `onMounted(async () => { await Promise.all([mesh.refreshAll(), transport.fetchStatus(), refreshFederationNodes(), refreshSelfOnion(), refreshSelfDid(), refreshContacts()]) })` — already parallelized | **Uncached fetch + remount storm** (6 RPC groups already run in parallel, so NOT a waterfall — but nothing is cached, so all 6 re-run on every tab revisit; plus a D3/Leaflet re-render cost on every remount) | +| Cloud (`Cloud.vue`) | Yes — `peersResource`/`countsResource` already on `useCachedResource` | `onMounted(async () => { loadCounts(); await loadPeers(); void loadPeerFiles() })` — mostly cache-gated already | Likely **already close to fixed**; profiling may show this needs little to no work, or only KeepAlive (component-instance persistence), not new caching | +| Server (`Server.vue`, 889 lines) | Partially (some sub-cards use the hook per grep) | `onMounted(() => { checkTorStatus(); loadNetworkData(); loadInterfaces(); loadDiskStatus(); loadTorServices(); loadVpnPeers(); loadFipsSummary() })` — 7 independent fire-and-forget calls, already effectively parallel (not `await`ed serially) | **Uncached fetch** (7 RPC calls fire fresh on every visit; already parallel, so not a waterfall) | +| ContainerAppDetails (secondary screen for installed apps) | No | `onMounted(async () => { await loadContainer(); await loadLogs(); await loadHealthStatus() })` | **Confirmed serial RPC waterfall** — concrete Pattern 3 fix candidate | +| AppDetails (secondary screen) | No | `onMounted(() => { loadBitcoinSync(); loadCredentials() })` — both fire-and-forget, not awaited sequentially | Likely fine as-is (already parallel); candidate for `useCachedResource` conversion per D-04 mainly for the instant-repeat-visit requirement (PERF-03), not a waterfall fix | +| Wallet / send flows | Not located/checked this session (no `Wallet.vue` found under `views/`; likely spread across `AppDetails.vue`'s Bitcoin-app-specific code and modal components) | Not checked | **Needs direct profiling** — locate the actual wallet/send-flow components first | +| Web5 (`web5/Web5.vue`) | Yes (in the useCachedResource grep list) | Not inspected in detail this session | Likely **already close to fixed**; confirm via profiling rather than assuming | + +## Environment Availability + +| Dependency | Required By | Available | Version | Fallback | +|------------|------------|-----------|---------|----------| +| Node.js | Frontend build/dev | ✓ | v24.14.1 [VERIFIED: `node --version`] | — | +| npm | Frontend build/dev | ✓ | 11.11.0 [VERIFIED: `npm --version`] | — | +| Vitest | Unit tests for the `onActivated` hook extension | ✓ (already configured, `neode-ui/vitest.config.ts`) | 3.1.1 [VERIFIED: package.json] | — | +| Playwright | Optional e2e smoke of tab-switch behavior | ✓ (`neode-ui/e2e/`) | 1.58.2 [VERIFIED: package.json] | Manual verification on archi-dev-box is the phase's actual pass bar (D-11) regardless | +| archi-dev-box (on-device verification target, D-11) | PERF-01/02/03 sign-off | ✓ — resolvable from this environment (multiple addresses returned) [VERIFIED: `getent hosts archi-dev-box`] | — | — | +| AIUI source repo (`../../AIUI`, sibling to `archy`) | D-14 UX defaults | ✗ — not present on this machine [VERIFIED: `ls` of parent directory] | — | Must be located (possibly on the ThinkPad `.116` build server per project memory) before D-14 can be implemented as code — see Open Question 1 | + +**Missing dependencies with no fallback:** +- AIUI source repo for D-14 — there is no in-repo fallback; the two small UX defaults + cannot be implemented without either the AIUI source or a confirmed existing + neode-ui-side hook (iframe query param) that AIUI already honors. + +**Missing dependencies with fallback:** +- None of the frontend-perf-specific work (KeepAlive, useCachedResource extension, + Promise.all fixes) has any missing dependency — everything needed is already installed. + +## Validation Architecture + +### Test Framework +| Property | Value | +|----------|-------| +| Framework | Vitest 3.1.1 [VERIFIED: package.json], jsdom environment, globals enabled | +| Config file | `neode-ui/vitest.config.ts` | +| Quick run command | `npm run test -- .test.ts` (run from `neode-ui/`) | +| Full suite command | `npm run test` (from `neode-ui/`); `npm run test:watch` for iteration | + +### Phase Requirements → Test Map +| Req ID | Behavior | Test Type | Automated Command | File Exists? | +|--------|----------|-----------|-------------------|-------------| +| PERF-01 | Slow surfaces profiled, causes named before fixes land | manual (committed findings doc, D-10) | N/A — not an automatable assertion; verified by the doc's existence + review | ❌ Wave 0 — this is a doc deliverable, not a test | +| PERF-02 | Main-tab component instance NOT recreated on revisit; cached data renders synchronously | unit (`@vue/test-utils` + Vitest, mounting `Dashboard.vue`'s router-view structure or an isolated harness around ``) | `npm run test -- src/views/dashboard/__tests__/keepAliveTabs.test.ts` (NEW) | ❌ Wave 0 — needs a new test file | +| PERF-02 | `useCachedResource`'s `onActivated` extension actually calls `refreshIfStale()` on reactivation, and is a no-op when not inside `` | unit | `npm run test -- src/composables/__tests__/useCachedResource.test.ts` (NEW — no existing test file for this composable was found) | ❌ Wave 0 | +| PERF-03 | Repeat opens of a secondary screen (e.g. AppDetails) resolve from cache without re-invoking the fetcher | unit (spy call count assertion on the fetcher passed to `useCachedResource`) | `npm run test -- src/views/__tests__/AppDetails.test.ts` (NEW, or extend an existing view test if one exists) | ❌ Wave 0 | +| PERF-03 (on-device pass bar, D-11) | No visible spinner/blank on revisit of any tab/secondary screen already visited this session, on archi-dev-box | manual only | N/A — inherently a live/manual on-device check per the phase's own success criteria | ❌ Wave 0 — manual-only by design, not a gap to fill with automation | + +### Sampling Rate +- **Per task commit:** `npm run test -- ` (quick, targeted) +- **Per wave merge:** `npm run test` (full Vitest suite) + a manual archi-dev-box + tab-switch/secondary-screen walkthrough for whatever surfaces that wave touched +- **Phase gate:** Full Vitest suite green, PLUS the committed D-10 findings doc exists and + is referenced, PLUS a full manual pass-bar walkthrough on archi-dev-box per D-11 before + `/gsd-verify-work` + +### Wave 0 Gaps +- [ ] `neode-ui/src/composables/__tests__/useCachedResource.test.ts` — no existing test + file for this composable; needed to cover the new `onActivated` behavior and confirm + existing sticky-ready/keep-last-value/dedup semantics aren't regressed by the change +- [ ] `neode-ui/src/views/dashboard/__tests__/keepAliveTabs.test.ts` — new test harness for + the KeepAlive-wrapped nested router-view (component identity survives a simulated route + change to an `include`-listed path; does NOT survive for a non-included path) +- [ ] A committed profiling findings doc at + `.planning/phases/02-ui-performance/` (per D-10) — this is a deliverable, not a test + file, but Wave 0 of the plan should treat "produce this doc" as a hard prerequisite + gating all per-surface fix waves, per D-02/D-10 + +## Security Domain + +`security_enforcement` is absent from `.planning/config.json` → treated as enabled. + +### Applicable ASVS Categories + +| ASVS Category | Applies | Standard Control | +|---------------|---------|-----------------| +| V2 Authentication | No | This phase touches no auth/session code paths | +| V3 Session Management | No | No session logic changes | +| V4 Access Control | Conditional — only if D-12/D-13 leads to a NEW aggregate RPC endpoint | Any new endpoint must sit behind whatever RBAC/session middleware existing sibling `core/` RPC handlers already use — additive-only per D-12 means copying the existing auth-check pattern, not designing a new one | +| V5 Input Validation | Conditional — same trigger as V4 | Any new aggregate endpoint's parameters must be validated the same way existing handlers validate theirs (existing project convention, not a new library) | +| V6 Cryptography | No | Nothing in this phase touches crypto/secrets | + +### Known Threat Patterns for this stack + +| Pattern | STRIDE | Standard Mitigation | +|---------|--------|---------------------| +| A new aggregate RPC endpoint accidentally exposing more fields than the requesting view needs (over-fetch through a "convenience" batch endpoint) | Information Disclosure | Shape the aggregate response to exactly what the calling screen renders — mirror the discipline already visible in existing typed RPC response interfaces in `rpc-client.ts` | +| sessionStorage snapshot (D-08) persisting sensitive per-user data longer than intended | Information Disclosure | Already mitigated by the existing `persist: false` opt-out for large/sensitive payloads (D-08); when converting a new view, explicitly decide persist:true/false rather than defaulting blindly | + +No new authentication, authorization, or cryptographic surfaces are introduced by this +phase — the security review here is narrow by design, matching the phase's actual scope. + +## Sources + +### Primary (HIGH confidence) +- `neode-ui/src/composables/useCachedResource.ts`, `neode-ui/src/stores/resources.ts`, + `neode-ui/src/api/rpc-client.ts`, `neode-ui/src/App.vue`, `neode-ui/src/views/Dashboard.vue`, + `neode-ui/src/views/dashboard/useRouteTransitions.ts`, `neode-ui/src/router/index.ts`, + `neode-ui/src/views/{Apps,Mesh,Cloud,Server,AppDetails,ContainerAppDetails}.vue`, + `neode-ui/package.json`, `neode-ui/vitest.config.ts`, `apps/aiui/manifest.yml`, + `scripts/setup-aiui-server.sh` — all read directly this session. +- [RouterView slot | Vue Router](https://router.vuejs.org/guide/advanced/router-view-slot) — + confirms the scoped-slot KeepAlive/Transition pattern is the only supported form in + Vue Router 4. +- [KeepAlive | Vue.js](https://vuejs.org/guide/built-ins/keep-alive) — `include`/`exclude` + name matching, `max` LRU eviction, `activated`/`deactivated` hook semantics. + +### Secondary (MEDIUM confidence) +- [KeepAlive include/exclude parameters do not work as expected for asynchronously loaded router views · Issue #11764 · vuejs/core](https://github.com/vuejs/core/issues/11764) — + WebFetch-summarized; describes the bug and states it was fixed by extracting the name + from `__asyncResolved` — the exact patch-version boundary was not independently + cross-verified against a changelog this session (see Assumption A1). +- WebSearch results on `onActivated`/`onDeactivated` being a safe no-op outside + `` (multiple Vue lifecycle-hook explainer articles, cross-checked against the + official Vue lifecycle-hooks API reference). + +### Tertiary (LOW confidence) +- None — no unverified WebSearch-only claims were used to make a prescriptive + recommendation in this document; the one external claim with residual uncertainty (A1) + is explicitly flagged with a manual-verification mitigation rather than presented as fact. + +## Metadata + +**Confidence breakdown:** +- Standard stack: HIGH — no new libraries; every primitive cited was read directly from + the codebase. +- Architecture: HIGH — the KeepAlive insertion point, the `onActivated` gap, and the + serial-waterfall example were all found by direct code inspection, not inferred. +- Pitfalls: HIGH for pitfalls 1–7 (code-grounded); MEDIUM for the exact Vue patch-version + boundary in Pitfall 1 (external GitHub issue, not independently changelog-verified). +- AIUI ride-along (D-14): LOW — genuinely blocked pending source-location confirmation; + flagged as Open Question 1, not glossed over. + +**Research date:** 2026-07-30 +**Valid until:** 2026-08-13 (30 days is generous for a fast-moving frontend area; if the +AIUI repo location or the Vue KeepAlive patch status changes, re-verify before relying on +this doc)