17 KiB
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 <RouterView> structure.
Current structure to modify (neode-ui/src/views/Dashboard.vue:87-118):
<RouterView v-slot="{ Component, route }">
<Transition :name="getTransitionName(route)">
<div :key="route.path" class="view-wrapper">
<div v-if="route.path === '/dashboard/chat' || route.path === '/dashboard/mesh'" ...>
<component :is="Component" />
</div>
<div v-else ...>
<component :is="Component" class="view-container flex-none" />
<div class="shrink-0 h-6 md:h-12" aria-hidden="true"></div>
</div>
</div>
</Transition>
</RouterView>
Two branches both wrap <component :is="Component" /> — <KeepAlive> must be inserted around <component :is="Component" /> in BOTH branches (or the two wrapper <div>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 <div>, not on <component> itself — leave it there (it only drives the <Transition>, not KeepAlive's cache identity) per RESEARCH.md's note that keying <component> 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):
<router-view v-slot="{ Component, route }">
<transition :name="transitionName">
<keep-alive :include="mainTabComponentNames" :max="8">
<component :is="Component" :key="route.path" />
</keep-alive>
</transition>
</router-view>
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):
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):
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:
import { onActivated } from 'vue'
// ...
if (getCurrentScope()) {
onActivated(() => refreshIfStale())
}
Safe unconditionally — Vue no-ops onActivated outside a <KeepAlive> 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):
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:
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 <KeepAlive :include>.
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):
import { computed, ref, watch, onMounted } from 'vue'
import { useCachedResource } from '../composables/useCachedResource'
Core SWR resource-definition pattern (Cloud.vue:510-524):
// Federation peers — cached so the Folders tab's peer cards paint instantly
// on revisit while the list revalidates behind them.
const peersResource = useCachedResource<PeerNode[]>({
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):
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:
onMountedalready doesawait Promise.all([...6 calls...])— NOT a waterfall; convert each of the 6 fetches into auseCachedResourcekey (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 touseCachedResourceif 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):
onMounted(async () => {
await loadContainer()
await loadLogs()
await loadHealthStatus()
})
Independent load functions confirming no cross-dependency (lines 175-202):
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:
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):
const detailsResource = useCachedResource<AppDetails>({
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 <KeepAlive> 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):
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):
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)
const resource = useCachedResource<T>({
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.
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.
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/<findings-doc>.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 <KeepAlive> 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