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

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: 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):

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