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

20 KiB

phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, requirements-note, coverage, duration, completed, status
phase plan subsystem tags requires provides affects tech-stack key-files key-decisions patterns-established requirements-completed requirements-note coverage duration completed status
02-ui-performance 06 ui
vue
useCachedResource
sessionstorage
pinia
wallet
rpc-dedup
phase provides
02-ui-performance/02-02 useCachedResource composable (TTL/persist/onActivated revalidation), RefreshIndicator.vue, the explicit-per-key persist convention
phase provides
02-ui-performance/02-04 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 provides
02-ui-performance/02-05 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
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
02-08
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)
created modified
neode-ui/src/views/__tests__/serverTabCache.test.ts
neode-ui/src/views/__tests__/homeTabCache.test.ts
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
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().
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.
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.
id description requirement verification human_judgment
D1 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 PERF-02
kind ref status
unit neode-ui/src/views/__tests__/serverTabCache.test.ts pass
false
id description requirement verification human_judgment
D2 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 PERF-02
kind ref status
unit neode-ui/src/views/__tests__/serverTabCache.test.ts pass
false
id description requirement verification human_judgment
D3 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 PERF-02
kind ref status
unit neode-ui/src/views/__tests__/homeTabCache.test.ts pass
false
id description requirement verification human_judgment
D4 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 PERF-02
kind ref status
unit neode-ui/src/views/__tests__/homeTabCache.test.ts pass
false
id description requirement verification human_judgment
D5 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 PERF-02
kind ref status
unit npm run test (94 files / 767 tests) pass
kind ref status
other 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 pass
false
73min 2026-07-30 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 <h1>, both driven by a loadState-derived computed (serverRefreshIndicatorState = any of six groups refreshing; homeRefreshIndicatorState = the wallet resource's own loadState, directly — the wallet is the one group whose refresh visibility matters most, per T-02-13).

  • Server's loadDiskStatus() converted to the seventh useCachedResource entry (server.disk-status); armServerEntryEffects() no longer force-calls it on every activation — the resource's own internal onActivated (added in 02-02) now self-heals it staleness-gated, exactly like the other six.

  • Home's armLiveDataPolling() restructured around homeCachedGroups/refreshHomeGroupIfStale (the 02-05 refreshMeshGroupIfStale pattern), kicked via a single Promise.allSettled — the plan's required move of fileBrowserClient.getUsage()'s trailing await into the same concurrent fan-out as the other loaders.

Task Commits

Each task was committed atomically:

  1. Task 1: Cache the Server tab's seven load groups - e6ed5536 (feat, tdd)
  2. Task 2: Cache the Home tab and guarantee wallet freshness on re-entry - 926fa606 (feat, tdd)

Plan metadata: (this commit) - docs(02-06): complete Server and Home tab cache plan

Note: both tasks are TDD tasks; tests were written and made to pass within each task's own commit, per this repo's established single-commit-per-task convention (see 02-01 through 02-05 history).

Files Created/Modified

  • neode-ui/src/views/Server.vue — explicit TTL/persist added to the five pre-existing cached resources; loadDiskStatus converted to a sixth-and-seventh (server.disk-status) cached resource; dedup:true/signal threaded through networkRes's two inline calls; RefreshIndicator added to a new minimal header row; armServerEntryEffects/onMounted comments updated to reflect the settled A3 verdict
  • neode-ui/src/views/Home.vuesystemStatsRes/updateStatusRes/cloudUsageRes/walletStatusRes added; armLiveDataPolling restructured around homeCachedGroups/refreshHomeGroupIfStale + unconditional wallet refresh; onMounted no longer async/no trailing await; dedup:true added to the wallet composite and checkUpdateStatus's calls; RefreshIndicator added next to the header <h1>; new defineExpose block (loadWeb5Status, homeRefreshIndicatorState) for test access
  • neode-ui/src/stores/homeStatus.tsdedup:true added to system.stats/bitcoin.getinfo/fips.status/openwrt.get-status
  • neode-ui/src/api/rpc-client.tsdedup:true added inside vpnStatus()/dnsStatus()/diskStatus()
  • neode-ui/src/views/__tests__/serverTabCache.test.ts — new; 7 tests covering Task 1's five required behaviors plus persist/dedup/indicator-wiring assertions
  • neode-ui/src/views/__tests__/homeTabCache.test.ts — new; 8 tests covering Task 2's six required behaviors plus dedup/websocket-path assertions
  • neode-ui/src/views/dashboard/__tests__/keepAliveLifecycle.test.ts — one assertion fixed (see Deviations): the pre-existing Server.vue vpnPollInterval test's reactivation count was invalidated by server.network-summary's new explicit 10s TTL

Decisions Made

See key-decisions in frontmatter for the full list. Highlights:

  • RESEARCH A3 settled (independent, no ordering dependency) — see Accomplishments.
  • Five of Server's seven groups were already converted by a pre-phase legacy commit; this task's real work was explicit TTL/persist/dedup, not the initial useCachedResource wiring.
  • Web5.vue key-sharing evaluated and declined for a documented, safety-driven reason (would either corrupt Web5's typed entry or fail to close the sessionStorage gap).
  • homeStatus.refresh() wrapped at the view (Home.vue), not inside the store, per the 02-05 Pinia-effectScope finding.

Deviations from Plan

Auto-fixed Issues

1. [Rule 1 - Bug, caught by the existing test suite] keepAliveLifecycle.test.ts's Server.vue vpnPollInterval reactivation count invalidated by the new explicit TTL

  • Found during: Task 1, full-suite run after adding networkRes's explicit 10s TTL
  • Issue: The pre-existing test deactivated Server for 20s (chosen, per its own comment, to be "comfortably under networkRes's [then-default] 30s TTL"). Once server.network-summary got an explicit 10s TTL, that 20s deactivation window now makes networkRes itself stale, so reactivation triggers a SECOND vpnStatus() call (via networkRes's own onActivated revalidation) in addition to armVpnPoll's immediate tick — the test's +1 assertion needed to become +2.
  • Fix: Updated the assertion and its comment to explain both contributing calls.
  • Files modified: neode-ui/src/views/dashboard/__tests__/keepAliveLifecycle.test.ts
  • Verification: Full suite green (94 files / 767 tests) after the fix.
  • Committed in: e6ed5536 (Task 1 commit)

Total deviations: 1 auto-fixed (Rule 1 — a test assertion directly invalidated by this task's own required TTL change) Impact on plan: Necessary correction within a file this task's change directly affected. No scope creep — the fix only updates an assertion and its explanatory comment.

Known Stubs

None — no stub data, placeholder text, or unwired data sources were introduced. Every cached group's fetcher performs a real RPC round-trip (or wraps an existing store/view function that does).

Threat Flags

None beyond what the plan's own <threat_model> already anticipated (T-02-01, T-02-13, T-02-16, T-02-17) — no new network endpoints, auth paths, or trust-boundary-crossing surface was introduced by this plan.

Issues Encountered

  • The keepAliveLifecycle.test.ts assertion invalidation above — resolved by updating the test to match the new, plan-mandated TTL behavior; no production-code bug.
  • flushPromises() (a single macrotask boundary) needed to be called twice in homeTabCache.test.ts to fully settle loadWeb5Status()'s deeply nested Promise.allSettled chains under fake timers before the wallet resource's loadState reliably reached 'ready' — a test-authoring detail (settle() helper), not a production bug.
  • A leaking permanent mockImplementation() override (used by one test to simulate a never-resolving RPC) was caught before commit — beforeEach now unconditionally restores the default mock implementation alongside mockClear().

User Setup Required

None - no external service configuration required.

Next Phase Readiness

  • Every main tab in KEEP_ALIVE_PATHS except Chat/AIUI now has its data layer on useCachedResource with explicit TTL/persist decisions (Marketplace/Discover 02-02, secondary screens 02-03, Mesh 02-05, Server/Home this plan) — 02-07 (Chat/AIUI) is the one remaining conversion.
  • The "host useCachedResource at the component, not inside a Pinia store setup" finding (02-05, reconfirmed here for homeStatus.ts) is now established across two independent stores — a strong signal for 02-08's on-device pass to treat as settled architecture, not a per-case judgment call.
  • No blockers for 02-07/02-08.

Phase: 02-ui-performance Completed: 2026-07-30

Self-Check: PASSED