Archipelago — open-source initial import

This commit is contained in:
Archipelago
2026-08-12 10:55:50 +00:00
commit e9c69062fe
1936 changed files with 443175 additions and 0 deletions
@@ -0,0 +1,322 @@
# 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`):
```vue
<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):
```vue
<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):
```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 `<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):
```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 `<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`):
```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<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):
```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<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):
```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<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.
```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/<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