--- phase: 02-ui-performance plan: 06 subsystem: ui tags: [vue, useCachedResource, sessionstorage, pinia, wallet, rpc-dedup] # Dependency graph requires: - phase: 02-ui-performance/02-02 provides: "useCachedResource composable (TTL/persist/onActivated revalidation), RefreshIndicator.vue, the explicit-per-key persist convention" - phase: 02-ui-performance/02-04 provides: "Server.vue's vpnPollInterval and loadDiskStatus placed on activate/deactivate; Home.vue's systemStatsInterval/walletRefreshInterval/wsClient subscription placed on activate/deactivate with an immediate re-sync on entry" - phase: 02-ui-performance/02-05 provides: "The 'host useCachedResource at the component, not inside a Pinia store setup' finding (onActivated no-ops in a bare effectScope), and the side-effect-only/sentinel-timestamp resource pattern for wrapping an existing function that sets its own refs" provides: - "All seven Server.vue load groups (network summary, FIPS summary, VPN peers, interfaces, Tor services — shared by checkTorStatus/loadTorServices — and disk status) on useCachedResource with explicit per-group TTL and persist" - "Home.vue's system stats, update status and cloud storage usage on every-entry TTL-gated useCachedResource entries" - "Home.vue's wallet composite fetch on a persist:false useCachedResource that revalidates unconditionally on every activation (T-02-13)" - "RefreshIndicator wired into both Server's and Home's headers" affects: [02-08] # Tech tracking tech-stack: added: [] patterns: - "A pre-existing (pre-phase) useCachedResource conversion can lack explicit TTL/persist — auditing for that gap before assuming a plan's own conversion work is still needed" - "refreshHomeGroupIfStale/homeCachedGroups — the Home.vue instance of the refreshMeshGroupIfStale generalized staleness-gate pattern from 02-05, applied via Promise.allSettled" - "A cached resource with no ttlMs relied on for staleness gating at all, because the view always calls refresh() unconditionally on activation (the wallet-freshness exception to the default stale-while-revalidate contract)" key-files: created: - neode-ui/src/views/__tests__/serverTabCache.test.ts - neode-ui/src/views/__tests__/homeTabCache.test.ts modified: - neode-ui/src/views/Server.vue - neode-ui/src/views/Home.vue - neode-ui/src/stores/homeStatus.ts - neode-ui/src/api/rpc-client.ts - neode-ui/src/views/dashboard/__tests__/keepAliveLifecycle.test.ts key-decisions: - "RESEARCH assumption A3 settled: read all seven Server loader bodies (checkTorStatus, loadNetworkData, loadInterfaces, loadVpnPeers, loadFipsSummary, loadTorServices, loadDiskStatus) — none consumes another's result or side effect (networkRes's fetcher only reads its OWN previous value for merge purposes, which is self-referential, not cross-resource). The concurrent fan-out is correct as originally written; no aggregate endpoint or ordering was needed." - "Five of Server's seven groups (network-summary, fips-summary, vpn-peers, interfaces, tor-services) were ALREADY on useCachedResource from a pre-phase legacy commit (ea254f63, predates this milestone) but relied entirely on the composable's defaults (30s TTL, persist:true) — this task's real work was adding explicit per-group TTL/persist/dedup, not the initial conversion. Only loadDiskStatus was a genuinely uncached plain fetch." - "Home's wallet composite (lnd.getinfo + ecash/fedimint/ark balances + 3 histories) does NOT share a cache key with either of Web5.vue's two existing resources (web5.networking-profits, web5.lnd-info). web5.networking-profits is an unrelated dataset. web5.lnd-info covers only the single lnd.getinfo call and — critically — is declared with the composable's implicit default persist:true; Web5.vue is out of this plan's file scope to fix, so sharing that key would either corrupt its differently-shaped entry.data (Home's sentinel timestamp vs. its typed balance object) or silently fail to close the sessionStorage gap this task exists to close, since Web5.vue's own hook instance would keep persisting on its own independent refresh cycle regardless of what Home declares." - "homeStatus.refresh() and (in Mesh.vue's 02-05 precedent) mesh.refreshAll()/transport.fetchStatus() are both Pinia store actions wrapped by a useCachedResource hosted at the VIEW, not inside the store — defineStore(id, setup) runs in a bare effectScope where onActivated() silently no-ops (Vue dev warning only), so the wrapper must live where the real component/KeepAlive lifecycle is." - "dedup:true added directly inside vpnStatus()/dnsStatus()/diskStatus() in rpc-client.ts (parameterless convenience methods with no per-call options), following the existing in-file precedent at getNodeDid()/meshContactsList()/federationListNodes()." patterns-established: - "Before assuming a plan's list of 'uncached load groups' needs full conversion, grep for existing useCachedResource usage in the target file — legacy or prior-plan work may have already wired the hook without the TTL/persist/dedup decisions a later plan is tasked with adding." requirements-completed: [] requirements-note: "PERF-02 is NOT marked complete — 02-07 (Chat/AIUI) still extends the KeepAlive/cache architecture to the one remaining unconverted main tab, per the precedent set by 02-02/02-03/02-04/02-05's own summaries." coverage: - id: D1 description: "All seven Server.vue load groups (network summary, FIPS summary, VPN peers, interfaces, Tor services, disk status, plus checkTorStatus sharing tor-services) are cached with explicit TTL/persist; a revisit inside TTL issues zero RPC across all seven, a cold load still fires the independent groups concurrently, and a rejected group leaves the other six unaffected" requirement: PERF-02 verification: - kind: unit ref: "neode-ui/src/views/__tests__/serverTabCache.test.ts" status: pass human_judgment: false - id: D2 description: "VPN peers (npub) and Tor services (onion addresses) declare persist:false; network-summary/fips-summary/interfaces/disk-status (no identity payload) persist; every fetcher backing the seven groups passes dedup:true; RefreshIndicator renders in the Server header driven by any group refreshing" requirement: PERF-02 verification: - kind: unit ref: "neode-ui/src/views/__tests__/serverTabCache.test.ts" status: pass human_judgment: false - id: D3 description: "Home's system stats, update status and cloud storage usage are every-entry, TTL-gated cached resources; a revisit inside TTL issues zero new RPCs for those three groups" requirement: PERF-02 verification: - kind: unit ref: "neode-ui/src/views/__tests__/homeTabCache.test.ts" status: pass human_judgment: false - id: D4 description: "The wallet composite (home.wallet-status) revalidates unconditionally on every reactivation regardless of TTL, the previously rendered figure stays in the DOM throughout, persist:false (no sessionStorage key), hydrateWalletSnapshot still paints before any network round-trip, the websocket wallet-push path is intact, and RefreshIndicator binds to the wallet resource's loadState" requirement: PERF-02 verification: - kind: unit ref: "neode-ui/src/views/__tests__/homeTabCache.test.ts" status: pass human_judgment: false - id: D5 description: "No visual/animation regression — full suite (94 files / 767 tests incl. the structural keepAliveTabs.test.ts DOM-shape pin) stays green, type-check and build are clean, and the built bundle contains the new resource keys" requirement: PERF-02 verification: - kind: unit ref: "npm run test (94 files / 767 tests)" status: pass - kind: other ref: "npm run type-check && npm run build; grep for home.wallet-status/home.system-stats/home.update-status/home.cloud-usage/server.disk-status in web/dist/neode-ui/assets/{Home,Server}-*.js" status: pass human_judgment: false duration: 73min completed: 2026-07-30 status: complete --- # Phase 02 Plan 06: Server and Home Tab Cache Summary **Server's seven load groups (five already on useCachedResource from a pre-phase commit, gaining their first explicit TTL/persist/dedup here; disk status newly converted) and Home's system/update/storage groups on every-entry TTL-gated cache, with the wallet composite on an unconditional-revalidate-on-entry resource that never persists to sessionStorage** ## Performance - **Duration:** ~73 min - **Started:** 2026-07-30T20:26:48Z - **Completed:** 2026-07-30T21:39:25Z - **Tasks:** 2 (Task 1 auto/tdd, Task 2 auto/tdd) - **Files modified:** 7 (2 new test files, 5 modified) ## Accomplishments - **RESEARCH A3 settled (Server.vue):** read every one of the seven loader bodies — `checkTorStatus`, `loadNetworkData`, `loadInterfaces`, `loadVpnPeers`, `loadFipsSummary`, `loadTorServices`, `loadDiskStatus`. None consumes another's result or side effect; `networkRes`'s fetcher reads only its OWN previous cached value to merge partial RPC results, which is self-referential, not a cross-resource dependency. Verdict: independent, concurrent fan-out is correct as-is — no aggregate endpoint or ordering change was needed. - **Found: 5 of Server's 7 groups were already on `useCachedResource`** from a pre-phase legacy commit (`ea254f63`, predates this UI-performance milestone) — `networkRes`, `fipsSummaryRes`, `vpnPeersRes`, `interfacesRes`, `torServicesRes`. None had an explicit `ttlMs`/`persist` (relying on the composable's 30s/`persist:true` defaults), and several underlying calls lacked `dedup:true`. `checkTorStatus()` turned out to just call `torServicesRes.refresh()` and derive a label — it shares that resource rather than being an eighth key. Only `loadDiskStatus()` was a genuinely uncached plain fetch, forced on every activation. - **Server.vue's per-group TTL/persist table** (all explicit, none defaulted, per T-02-01): | Key | TTL | Persist | Reason | |---|---|---|---| | `server.network-summary` | 10,000ms | `true` | Fast tier (WiFi/VPN/DNS state); this node's own status/wgPubkey, not peer identity | | `server.interfaces` | 10,000ms | `true` | Fast tier; local hardware info (MAC/IP), no identity | | `server.disk-status` | 10,000ms | `true` | Fast tier; usage figures carry no identity/financial payload | | `server.vpn-peers` | 30,000ms | `false` | Near-default; carries npub (peer Nostr identity) | | `server.tor-services` | 30,000ms | `false` | Near-default; carries onion_address | | `server.fips-summary` | 60,000ms | `true` | Near-static (installed/service_active/key_present rarely change; authenticated_peer_count can drift, so not fully static); key_present is a boolean flag, not the key itself | - **Home.vue's cache keys** (all hosted in Home.vue, not inside the `homeStatus` Pinia store — a store's `defineStore(id, setup)` runs in a bare effectScope where `onActivated()` silently no-ops, the same finding 02-05 made for Mesh's store actions): | Key | Wraps | TTL | Persist | Reason | |---|---|---|---|---| | `home.system-stats` | `homeStatus.refresh(packages)` (system/bitcoin/vpn/fips/tollgate, 5 RPCs) | 10,000ms | `true` | Matches the pre-existing 10s poll cadence; aggregate status, no identity | | `home.update-status` | `checkUpdateStatus()` | 300,000ms | `true` | Near-static; an available update doesn't appear/disappear quickly | | `home.cloud-usage` | `fileBrowserClient.getUsage()` | 30,000ms | `true` | Default tier; non-sensitive | | `home.wallet-status` | `loadWeb5Status()` (7-call composite: lnd.getinfo + ecash/fedimint/ark balances + 3 histories) | n/a — never TTL-gated, always unconditional | `false` | T-02-13 exception: a money figure must never be presented as current without a visible re-check | - **Web5.vue key-sharing evaluated and declined** (read both of its resources): `web5.networking-profits` is an unrelated dataset (routing/content-sale profit totals). `web5.lnd-info` covers only the single `lnd.getinfo` call and — the reason sharing was declined — is declared with the composable's implicit default `persist:true`; Web5.vue is outside this plan's file scope to fix, so sharing that key would either corrupt its differently-shaped `entry.data` (a sentinel timestamp here vs. its typed balance object there) or silently fail to close the sessionStorage gap this task exists to close, since Web5.vue's own hook instance would keep persisting on its own independent 30s-interval refresh regardless of what Home declares. Home's wallet fetch is also a strictly broader 7-call composite, not the same single-call dataset. - **`dedup:true` added to every underlying RPC call touched by this plan**: Server's `network.diagnostics`/`router.list-forwards` (inline in `networkRes`'s fetcher) plus `vpnStatus()`/`dnsStatus()`/`diskStatus()` (parameterless convenience methods in `rpc-client.ts`, given `dedup:true` directly in their bodies, matching the existing `getNodeDid()`/`meshContactsList()` precedent); Home's 7-call wallet composite and `checkUpdateStatus()`'s two calls; `homeStatus.ts`'s `system.stats`/`bitcoin.getinfo`/`fips.status`/`openwrt.get-status` (its `vpn.status` call already picked up `dedup:true` for free via the `vpnStatus()` change). - **`RefreshIndicator` wired into both headers**: Server gained a new minimal top-of-page row (it previously had no visible page header at all — its `QuickActionsCard` header is `v-if="false"`); Home's got added inline next to the existing typed-welcome `