# Phase 2: UI Performance - Pattern Map **Mapped:** 2026-07-30 **Files analyzed:** 9 (new/modified) **Analogs found:** 9 / 9 (all analogs are in-repo; several files ARE their own analog — modify-in-place) ## File Classification | New/Modified File | Role | Data Flow | Closest Analog | Match Quality | |--------------------|------|-----------|-----------------|----------------| | `neode-ui/src/views/Dashboard.vue` (nested RouterView, ~line 87) | route/container (KeepAlive host) | request-response (render orchestration) | itself (template restructure) — pattern source: Vue Router 4 `v-slot` docs example | exact-pattern, no in-repo KeepAlive precedent | | `neode-ui/src/composables/useCachedResource.ts` | composable (SWR hook) | CRUD (cache read/refresh) | itself — extend with `onActivated` | exact (self-modify) | | `neode-ui/src/views/dashboard/useRouteTransitions.ts` | utility (route classification) | transform | itself — `isDetailRoute`/`TAB_ORDER` extended into an explicit main-tab-name list | exact (self-modify) | | `.planning/phases/02-ui-performance/02-FINDINGS.md` (new, D-10 deliverable) | doc/config | batch (one-time profiling report) | no code analog — doc-only deliverable | n/a | | `neode-ui/src/views/Apps.vue`, `Mesh.vue`, `Cloud.vue`, `Server.vue`, `web5/Web5.vue`, marketplace/discover view (candidates, D-02-gated) | component (main-tab view) | request-response + CRUD (fetch-on-mount → converted to SWR) | `neode-ui/src/views/Cloud.vue` (already partially on `useCachedResource`) | role-match, best-in-class | | `neode-ui/src/views/ContainerAppDetails.vue` (`onMounted`, lines 169-172) | component (secondary screen) | request-response (serial waterfall → parallel) | itself — Pattern 3 fix in place | exact (self-modify) | | `neode-ui/src/views/AppDetails.vue` and other secondary screens (D-04 candidates) | component (secondary screen) | CRUD (keyed cache, no KeepAlive) | `neode-ui/src/views/Cloud.vue`'s `peersResource`/`countsResource` usage | role-match | | `neode-ui/src/views/Chat.vue` (`aiuiUrl` computed, lines 74-82) | component (iframe URL builder) | transform (query-param construction) | itself — extend query string for D-14 | exact (self-modify) | | `neode-ui/src/stores/resources.ts` | store | CRUD (cache backing store) | itself — unchanged, referenced only | n/a (no changes expected) | ## Pattern Assignments ### `neode-ui/src/views/Dashboard.vue` (route/container, KeepAlive host) **Analog:** No in-repo KeepAlive precedent exists (`grep -r "KeepAlive" neode-ui/src` returns nothing) — the pattern to introduce is a Vue Router 4 official pattern, applied to this file's own existing nested `` structure. **Current structure to modify** (`neode-ui/src/views/Dashboard.vue:87-118`): ```vue
``` Two branches both wrap `` — `` must be inserted around `` in BOTH branches (or the two wrapper `
`s must be unified behind a single KeepAlive so classes/padding still differ per route). The `:key="route.path"` is currently on the outer wrapper `
`, not on `` itself — leave it there (it only drives the ``, not KeepAlive's cache identity) per RESEARCH.md's note that keying `` by `route.path` is only a footgun for *varying-param* detail routes, which are excluded from KeepAlive entirely (D-04). **Target pattern** (Vue Router 4 official form, RESEARCH.md Pattern 1): ```vue ``` `mainTabComponentNames` should be built explicitly from the `TAB_ORDER` main-tab set in `useRouteTransitions.ts` (Home, Apps, Mesh, Cloud, Server, Web5, Marketplace/Discover, Chat, Settings, Fleet) — NOT derived from `isDetailRoute()` (Pitfall 7: that helper under-covers secondary screens like `cloud/:folderId`, `server/openwrt`, `web5/credentials`, etc.). **Fallback if `include`/`exclude` name-matching misbehaves** (Pitfall 1 — confirm via manual smoke test first): drop `include` entirely and rely on `:max="8"` alone with LRU eviction (documented as working correctly with async components even when `include`/`exclude` name-matching does not). --- ### `neode-ui/src/composables/useCachedResource.ts` (composable, self-modify) **Analog:** itself (full file already read, 107 lines — no re-read needed) **Current imports** (lines 25-26): ```typescript import { computed, getCurrentScope, onScopeDispose, type ComputedRef } from 'vue' import { useResourcesStore, type ResourceEntry, type ResourceLoadState } from '@/stores/resources' ``` **Existing lifecycle-scope pattern to mirror** (lines 84-94): ```typescript if (getCurrentScope()) { onScopeDispose(() => { unsubscribe() window.removeEventListener('focus', onFocus) aborter.abort() }) } if (opts.immediate ?? true) refreshIfStale() ``` **Required addition** (Pattern 2, RESEARCH.md) — add alongside the `onScopeDispose` block: ```typescript import { onActivated } from 'vue' // ... if (getCurrentScope()) { onActivated(() => refreshIfStale()) } ``` Safe unconditionally — Vue no-ops `onActivated` outside a `` boundary, so this benefits main tabs (real fix) and secondary screens (harmless no-op) with one shared change. `refreshIfStale()` (lines 71-74) and `stale()` are already defined and reusable as-is — no new staleness logic needed. --- ### `neode-ui/src/views/dashboard/useRouteTransitions.ts` (utility, self-modify) **Analog:** itself (full 183 lines read) **Existing (insufficient) classification helper** (lines 38-41): ```typescript export function isDetailRoute(path: string): boolean { return (path.includes('/apps/') && !path.endsWith('/apps')) || (path.includes('/marketplace/') && !path.endsWith('/marketplace')) } ``` Do NOT reuse this as the KeepAlive `include` source (Pitfall 7 — misses `cloud/:folderId`, `server/openwrt`, `web5/credentials`, `goals/:goalId`, `app-session/:appId`, `web5/networking-profits`, `apps/lnd/channels`). Instead build the KeepAlive include list from `TAB_ORDER` (lines 4-15), which already enumerates exactly the main-tab paths: ```typescript const TAB_ORDER = [ '/dashboard', '/dashboard/apps', '/dashboard/marketplace', '/dashboard/cloud', '/dashboard/mesh', '/dashboard/server', '/dashboard/web5', '/dashboard/fleet', '/dashboard/chat', '/dashboard/settings' ] ``` Map each path to its route component's registered `name` (check `router/index.ts` route defs) to produce `mainTabComponentNames` for Dashboard.vue's ``. --- ### Main-tab view conversions (Apps.vue, Mesh.vue, Server.vue, Web5.vue, marketplace/discover — D-02-gated) **Analog:** `neode-ui/src/views/Cloud.vue` (already the most SWR-converted main-tab-adjacent view; 1029 lines, use Grep-then-targeted-Read for any further detail, not a full read) **Imports pattern** (`Cloud.vue:405,410`): ```typescript import { computed, ref, watch, onMounted } from 'vue' import { useCachedResource } from '../composables/useCachedResource' ``` **Core SWR resource-definition pattern** (`Cloud.vue:510-524`): ```typescript // Federation peers — cached so the Folders tab's peer cards paint instantly // on revisit while the list revalidates behind them. const peersResource = useCachedResource({ key: 'cloud.peer-nodes', fetcher: async (signal) => { const result = await rpcClient.federationListNodes() void signal return result?.nodes ?? [] }, ttlMs: 30_000, immediate: false, // kicked from onMounted (keeps the legacy load order) }) const peerNodes = computed(() => peersResource.entry.data ?? []) const peersLoading = computed(() => peersResource.entry.loadState === 'loading') const peersRefreshing = computed(() => peersResource.entry.loadState === 'refreshing') ``` **`onMounted` orchestration + error-keep-last-value pattern** (`Cloud.vue:949-968`, excerpted from grep hits at those lines): ```typescript onMounted(async () => { // ... prior setup ... 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 }) ``` This mirrors D-07 exactly: silent keep-last-value on background failure, explicit-refresh-only error surfacing. **Per-view conversion checklist derived from RESEARCH.md's Concrete Findings table** (use during D-02 profiling, not blanket): - Mesh.vue: `onMounted` already does `await Promise.all([...6 calls...])` — NOT a waterfall; convert each of the 6 fetches into a `useCachedResource` key (nothing cached today) rather than touching the parallelization. - Server.vue (889 lines): 7 fire-and-forget calls in `onMounted` (line ~831) — already parallel; convert each to `useCachedResource` if profiling confirms uncached-refetch is the cause. - Apps.vue: WebSocket-pushed store data, no per-mount RPC — likely KeepAlive-only fix (remount storm), not a caching conversion. - Marketplace/Discover: not yet code-inspected — needs its own profiling pass before assigning a fix category. --- ### `neode-ui/src/views/ContainerAppDetails.vue` (secondary screen, serial-waterfall fix) **Analog:** itself — the exact anti-pattern to fix in place (Pattern 3, RESEARCH.md) **Current serial waterfall** (lines 169-173): ```typescript onMounted(async () => { await loadContainer() await loadLogs() await loadHealthStatus() }) ``` **Independent load functions confirming no cross-dependency** (lines 175-202): ```typescript async function loadContainer() { loading.value = true error.value = null try { const status = await store.getContainerStatus(appId.value) container.value = status } catch (e) { error.value = e instanceof Error ? e.message : t('common.error') } finally { loading.value = false } } async function loadLogs() { logsLoading.value = true try { logs.value = await store.getContainerLogs(appId.value, 100) } catch (e) { if (import.meta.env.DEV) console.error('Failed to load logs:', e) } finally { logsLoading.value = false } } async function loadHealthStatus() { await store.fetchHealthStatus() healthStatus.value = store.getHealthStatus(appId.value) } ``` Each sets its own `loading`/`logsLoading` ref independently and reads no shared intermediate result — safe to parallelize: ```typescript onMounted(async () => { await Promise.allSettled([loadContainer(), loadLogs(), loadHealthStatus()]) }) ``` Note other call sites in the same file (lines ~205-240) also `await loadContainer(); await loadHealthStatus()` sequentially in refresh/action handlers — check each for the same independence property before parallelizing (don't blanket-apply beyond the `onMounted` case without verifying). --- ### Secondary screens converting to keyed `useCachedResource` (AppDetails.vue and similar, D-04) **Analog:** `neode-ui/src/views/Cloud.vue`'s resource pattern above, keyed per item instead of globally. **Key-naming convention to follow** (per CONTEXT.md D-04 and RESEARCH.md's Don't-Hand-Roll table): ```typescript const detailsResource = useCachedResource({ key: `app-details:${appId.value}`, fetcher: (signal) => rpcClient.call({ method: 'apps.get-details', params: { appId: appId.value }, signal, dedup: true }), ttlMs: 30_000, // tune per D-06; shorter for fast-moving sub-resources }) ``` No `` for these components (D-04) — component instance is NOT persisted, only the data cache is, so repeat opens still fully mount/unmount but paint instantly from `entry.data` before any refetch resolves. --- ### `neode-ui/src/views/Chat.vue` (AIUI iframe URL builder, D-14 UX defaults) **Analog:** itself — extend the existing query-string builder pattern. **Current URL-builder pattern** (lines 74-82): ```typescript const aiuiUrl = computed(() => { // Demo: ?mockArchy makes AIUI use its built-in mock node data (apps, system, // network, wallet, bitcoin, files) and &seed pre-loads the example chats. const demo = IS_DEMO ? '&mockArchy=1&seed=1' : '' const envUrl = import.meta.env.VITE_AIUI_URL if (envUrl) return `${envUrl}?embedded=true&hideClose=true${demo}` if (import.meta.env.PROD || IS_DEMO) return `/aiui/?embedded=true&hideClose=true${demo}` return '' }) ``` D-14's two defaults (chat starts expanded; mobile starts on chat view not context view) would append additional query params here (e.g. `&expanded=1`, `&mobileView=chat`) following the exact same string-concatenation convention — IF AIUI's own source (sibling repo, NOT in this checkout per RESEARCH.md's Open Question 1) already reads such params. **Blocking dependency**: confirm AIUI source location/support before scoping this as line-of-code work — this file's pattern is ready, but the receiving side is unverified. **Origin-validated postMessage listener pattern** (lines 92-103, useful if D-14 needs a postMessage handshake instead of/in addition to query params): ```typescript function onAiuiMessage(event: MessageEvent) { if (!aiuiUrl.value) return // Validate origin — only accept messages from AIUI try { const expected = new URL(aiuiUrl.value, window.location.origin).origin if (event.origin !== expected) return } catch { return } if (event.data?.type === 'ready') { aiuiConnected.value = true } } ``` --- ## Shared Patterns ### Stale-while-revalidate data cache **Source:** `neode-ui/src/composables/useCachedResource.ts` (full file, 107 lines) **Apply to:** Every main-tab conversion (D-02) and every secondary-screen conversion (D-04) ```typescript const resource = useCachedResource({ key: 'some.unique.key', // or `templated:${id}` for per-item secondary screens fetcher: (signal) => rpcClient.call({ method: '...', signal, dedup: true }), ttlMs: 30_000, // default; tune per D-06 }) ``` Do not hand-roll a second `ref` + manual `sessionStorage` cache per view — this is the exact duplication RESEARCH.md's canonical_refs warns against. ### Request dedup for parallelized fetch groups **Source:** `neode-ui/src/api/rpc-client.ts` (`dedup: true` option, already used in `Cloud.vue`/`Server.vue`) **Apply to:** Any newly-parallelized `Promise.all`/`Promise.allSettled` group (Pattern 3 fixes) so concurrent identical calls from multiple mounted consumers collapse into one request. ```typescript const res = await rpcClient.call<{ interfaces: NetworkInterface[] }>({ method: 'network.list-interfaces', signal, dedup: true, maxRetries: 1, }) ``` ### Background-refresh error handling (silent keep-last-value, D-07) **Source:** `neode-ui/src/views/Cloud.vue:966-968` **Apply to:** All `useCachedResource` consumers — never toast on background refresh failure; only surface errors via an explicit-refresh path. ```typescript const e = peersResource.entry if (e.error) loadError.value = e.error // banner, not a toast ``` ### KeepAlive reactivation revalidation (new shared extension) **Source:** `neode-ui/src/composables/useCachedResource.ts` (extend, see above) **Apply to:** All views, automatically, once the hook is extended — no per-view code changes needed to get D-01's "background refresh on revisit" behavior. ## No Analog Found | File | Role | Data Flow | Reason | |------|------|-----------|--------| | `.planning/phases/02-ui-performance/.md` (D-10 deliverable) | doc/config | batch | Documentation deliverable, not code — no code analog applicable; format is Claude's discretion per RESEARCH.md Open Question 2 (narrative + DevTools/performance.mark numbers recommended) | | KeepAlive template wiring in `Dashboard.vue` | route/container | request-response | No existing `` usage anywhere in the codebase (`grep -r "KeepAlive" neode-ui/src` = 0 hits) — pattern comes from Vue Router 4 official docs, not an in-repo analog | | AIUI-side D-14 param handling | external app | event-driven | AIUI source not present in this checkout (sibling repo, unconfirmed location) — cannot pattern-map code that isn't accessible; planner must add a precondition/checkpoint task per RESEARCH.md Open Question 1 | ## Metadata **Analog search scope:** `neode-ui/src/{views,composables,stores,api,views/dashboard}` **Files scanned:** Dashboard.vue, useCachedResource.ts, useRouteTransitions.ts, rpc-client.ts, resources.ts, Cloud.vue, ContainerAppDetails.vue, Chat.vue, App.vue (partial), plus grep sweeps across `views/*.vue` for `onMounted`/`useCachedResource`/`KeepAlive` usage (per RESEARCH.md's own prior codebase pass, cross-checked) **Pattern extraction date:** 2026-07-30