Files

22 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
02-ui-performance 06 execute 4
02-04
neode-ui/src/views/Server.vue
neode-ui/src/views/Home.vue
neode-ui/src/views/__tests__/serverTabCache.test.ts
neode-ui/src/views/__tests__/homeTabCache.test.ts
true
PERF-02
truths prohibitions artifacts key_links
Returning to the Server tab within the TTL issues no RPC for any of its seven load groups
Returning to the Home tab within the TTL issues no RPC for its system, update, wallet or storage-usage groups
A stale return to either tab keeps the previous content on screen while exactly one background revalidation runs per stale group, with a visible refresh indicator
The Server tab's seven loads still run concurrently — the conversion does not turn a parallel fan-out into a chain
Any Server load that genuinely consumes another load's result remains ordered, and the dependency is recorded rather than assumed away
The wallet figures on Home are revalidated on tab re-entry, so a resumed tab never presents a paused-poll balance as current
Wallet balances, transaction history and identity material from either tab are held in memory only and never written to sessionStorage (D-08)
statement verification
First entry to either tab in a fresh session may still show a loading state; only revisits are required to be instant backstop
MUST NOT present cached data as live — a money- or liveness-critical surface (wallet balance, incoming payment, mesh peer reachability, app install or health state) must never render from cache without a visible refresh signal and an in-flight revalidation
MUST NOT persist wallet balances, transaction history, credentials, DIDs, seed or identity material, or peer identity payloads to sessionStorage
MUST NOT achieve perceived speed by removing behavior or hiding state — no suppressing the refresh indicator, no dropping a fetch the surface needs, no disabling a feature to win the metric
path provides
neode-ui/src/views/__tests__/serverTabCache.test.ts Per-group call-count assertions and a concurrency assertion for the seven Server loads
path provides
neode-ui/src/views/__tests__/homeTabCache.test.ts Call-count assertions plus the wallet-freshness-on-re-entry assertion
from to via pattern
neode-ui/src/views/Server.vue neode-ui/src/composables/useCachedResource.ts each of the seven load groups becomes a keyed cached resource useCachedResource
from to via pattern
neode-ui/src/views/Home.vue neode-ui/src/composables/useCachedResource.ts system stats, update status, wallet status and storage usage become keyed cached resources useCachedResource
from to via pattern
neode-ui/src/views/Home.vue neode-ui/src/components/RefreshIndicator.vue the wallet card's refresh state drives the indicator so a resumed balance is never shown as settled RefreshIndicator
Cache the two remaining uncached-fetch main tabs: Server, whose seven independent loads re-run in full on every tab entry, and Home, whose system, update, wallet and storage figures do the same on top of two polling intervals.

Purpose: PERF-02. RESEARCH.md classifies both as uncached fetch rather than as waterfalls — their calls are already concurrent — so the work here is caching, not reordering. Home carries the phase's sharpest liveness constraint: a wallet balance is the one figure where "instant from cache" must never mean "quietly out of date".

Output: both tabs' fetches on keyed cached resources with deliberate TTLs and persist choices, still concurrent on cold load, with wallet freshness guaranteed on re-entry.

<execution_context> @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/02-ui-performance/02-CONTEXT.md @.planning/phases/02-ui-performance/02-RESEARCH.md @.planning/phases/02-ui-performance/02-PATTERNS.md @.planning/phases/02-ui-performance/02-FINDINGS.md @.planning/phases/02-ui-performance/02-02-SUMMARY.md @.planning/phases/02-ui-performance/02-04-SUMMARY.md @.planning/codebase/CONVENTIONS.md @CLAUDE.md Task 1: Cache the Server tab's seven load groups neode-ui/src/views/Server.vue, neode-ui/src/views/__tests__/serverTabCache.test.ts - `neode-ui/src/views/Server.vue` — 889 lines; grep for `onMounted`, `onActivated`, `useCachedResource`, `checkTorStatus`, `loadNetworkData`, `loadInterfaces`, `loadDiskStatus`, `loadTorServices`, `loadVpnPeers` and `loadFipsSummary` first, then read the `onMounted` block at line ~831 and each loader body. Do not read the file whole. - `neode-ui/src/views/__tests__/ServerNetworkRefresh.test.ts` — an existing test for this view; follow its mocking setup and extend rather than duplicate its conventions. - `neode-ui/src/views/Cloud.vue` lines 505-530 and 945-970 — the resource-definition and keep-last-value reference. - `neode-ui/src/composables/useCachedResource.ts` — the options contract, including the `onActivated` revalidation added by plan 02-02. - `neode-ui/src/api/rpc-client.ts` — the `dedup: true` option. - `.planning/phases/02-ui-performance/02-04-SUMMARY.md` — the side-effect bucket table for `Server.vue`. - `.planning/phases/02-ui-performance/02-FINDINGS.md` — Server's measured revisit RPC count and primary cause. - `.planning/phases/02-ui-performance/02-RESEARCH.md` assumption A3 — flags that the seven loads are *assumed* independent and that the assumption is unverified. - Mounting Server, deactivating, and reactivating inside the TTL issues zero new RPCs across all seven groups - Reactivating after the TTL issues exactly one revalidation per stale group with previous content still rendered - A cold load issues the independent groups concurrently — their recorded start times overlap - A rejected refresh in one group leaves the other six unaffected and keeps that group's last known data rendered - Disk and network state carry a shorter TTL than FIPS summary state, so fast-moving figures do not sit stale Convert each of the seven loads to a keyed `useCachedResource` entry following the `Cloud.vue` pattern, with `computed` views over `entry.data` and `entry.loadState` and keep-last-value error handling that sets the view's existing error ref rather than raising a toast (D-07). Some sub-cards in this view already use the hook — reuse their keys rather than introducing a second entry for the same dataset, and say in the SUMMARY which ones you found.
Before parallelizing or caching anything, settle RESEARCH.md assumption A3: the seven
loads are *assumed* independent, and that assumption is explicitly flagged as
unverified. Read each loader body and confirm none of the seven consumes another's
result or side effect. Record the verdict per loader in the SUMMARY. If any pair does
have a real dependency, keep that pair ordered and cache them individually — do not
flatten a genuine dependency into a concurrent group to make a number look better.

Set `ttlMs` explicitly per group using the D-06 discretion: disk status, network data
and interface state move fast enough for a short TTL of around 10000 ms; Tor status,
Tor services and VPN peers sit near the 30000 ms default; the FIPS summary is
near-static and warrants a longer value. Give the reason per group in the SUMMARY.

Set `persist` explicitly per group. Anything carrying VPN peer identity, Tor onion
addresses or key material is memory-only (`persist: false`). Non-identity system
figures may persist.

Pass `dedup: true` on every underlying `rpcClient.call`.

Keep the fan-out concurrent, per D-13. `Server.vue`'s `onMounted` today issues all
seven without awaiting them in sequence, which is already correct; the conversion must
preserve that shape. Where a resource needs an explicit kick, use `immediate: false`
and call `refresh()` inside a single `Promise.allSettled` array. D-13 reserves an
aggregate endpoint for a screen needing three or more genuinely dependent calls — if
the A3 verdict turns up such a chain here, D-12 bounds the response to an additive new
handler with no refactor of existing handlers and nothing touching the orchestrator,
and the task stops for a checkpoint before any `core/` change since no backend work is
otherwise in this plan's scope.

Wire `RefreshIndicator` into the Server header, driven by whether any group is
`refreshing` — the subtle in-header signal D-05 specifies, not a stale-age badge.

Create `neode-ui/src/views/__tests__/serverTabCache.test.ts` covering the five
behaviors above with `vi.fn()` fetchers, `<KeepAlive>` mounting, fake timers and
recorded invocation timestamps for the concurrency assertion.
cd neode-ui && npm run test -- src/views/__tests__/serverTabCache.test.ts src/views/__tests__/ServerNetworkRefresh.test.ts && npm run test && npm run type-check - `neode-ui/src/views/Server.vue` imports `useCachedResource` and every one of the seven loads resolves through a cached entry - `npm run test -- src/views/__tests__/serverTabCache.test.ts` exits 0 with all five behaviors covered - The reactivate-inside-TTL test asserts zero additional fetcher calls across all seven groups - The concurrency test asserts the independent groups' cold-load starts overlap - The pre-existing `ServerNetworkRefresh.test.ts` still passes unmodified in intent - Groups carrying VPN peer identity, onion addresses or key material are declared `persist: false` - Every fetcher passes `dedup: true` - `neode-ui/src/views/Server.vue` renders `RefreshIndicator` - `npm run test` exits 0 and `npm run type-check` exits 0 - The SUMMARY records the A3 independence verdict per loader, plus each group's TTL, persist choice and reason All seven Server loads are cached with verified independence, deliberate TTLs and persist choices; a revisit inside the TTL issues no RPC; the cold-load fan-out is still concurrent. Task 2: Cache the Home tab and guarantee wallet freshness on re-entry neode-ui/src/views/Home.vue, neode-ui/src/views/__tests__/homeTabCache.test.ts - `neode-ui/src/views/Home.vue` lines 293 and 524-560 — the `onMounted` block calls `hydrateWalletSnapshot()`, `loadSystemStats()`, `checkUpdateStatus()`, `loadWeb5Status()` and `await fileBrowserClient.getUsage()`, and arms `systemStatsInterval` (10s), `walletRefreshInterval` (30s), a `wsClient.subscribe` and a `wsWalletDebounce`. Read each loader body too. - `.planning/phases/02-ui-performance/02-04-SUMMARY.md` — the bucket table for `Home.vue`; plan 02-04 already moved the intervals and the websocket subscription onto activate/deactivate and required an immediate loader call on re-entry. This task must build on that placement, not undo it. - `neode-ui/src/views/web5/Web5.vue` lines 140 and 293 — two existing `useCachedResource` definitions in this codebase covering wallet-adjacent data; check whether Home can share a key with them rather than creating a parallel entry for the same dataset. - `neode-ui/src/components/RefreshIndicator.vue` — as created by plan 02-02, and its `state` prop typing. - `neode-ui/src/composables/useCachedResource.ts` and `neode-ui/src/stores/resources.ts` — the `persist` option and the sessionStorage snapshot path it controls. - `.planning/phases/02-ui-performance/02-FINDINGS.md` — Home's measured revisit RPC count and primary cause. - Mounting Home, deactivating, and reactivating inside the TTL issues zero new RPCs for the system, update and storage-usage groups - Reactivating always triggers a wallet revalidation regardless of TTL, and the refresh indicator is visible while it is in flight - The wallet figure previously on screen stays rendered throughout that revalidation — the card never blanks or falls back to a skeleton - A rejected wallet refresh leaves the last known figure rendered and raises no toast - No wallet balance, transaction record or identity value is written to sessionStorage by any Home resource - The existing wallet snapshot hydration still paints last-known figures before any network round-trip Convert Home's fetches to keyed cached resources: system stats, update status, wallet or Web5 status, and cloud storage usage. Where `Web5.vue` already defines a resource for the same dataset, share its key rather than creating a second entry for the same data.
Set `ttlMs` explicitly per group: system stats are fast-moving and should carry a
short TTL matching the existing 10s poll cadence; update status is near-static and
warrants a long value; storage usage sits near the default.

Wallet is the exception this task exists for, and it does not get a normal TTL-gated
treatment. A balance is a money figure: showing yesterday's number with no visible
signal that it is being re-checked is the failure this plan's first prohibition
forbids. So on tab re-entry the wallet resource revalidates unconditionally rather
than only when its TTL has lapsed, the previously known figure stays rendered
throughout, and `RefreshIndicator` is bound to that resource's `loadState` so the
re-check is visible. Plan 02-04 already placed an immediate loader call in
`onActivated`; wire the cached resource so that call is what revalidates it, rather
than adding a second independent call path.

Declare `persist: false` for the wallet or Web5 status resource and for anything else
carrying balances, transaction history, DIDs or identity material. The existing
`hydrateWalletSnapshot()` mechanism stays exactly as it is — it is the view's own
deliberate last-known-figures path and is not being replaced by the resource cache.
Storage usage and system stats are non-sensitive and may persist.

Pass `dedup: true` on every underlying call. Keep the existing concurrency: today's
`onMounted` fires the loaders without awaiting them in sequence except for the
`fileBrowserClient.getUsage()` await; move that into the same `Promise.allSettled`
group rather than leaving it as a trailing await.

Leave the websocket-driven wallet refresh from plan 02-04 in place. It is what makes a
zero-confirmation incoming transaction appear in seconds, and removing or debouncing
it harder to reduce request counts would be exactly the metric-gaming this plan's
third prohibition forbids.

Create `neode-ui/src/views/__tests__/homeTabCache.test.ts` covering the six behaviors
above. The sessionStorage assertion should seed the store, mount, deactivate and
reactivate, then assert no `resource:` key exists for the wallet entry.
cd neode-ui && npm run test -- src/views/__tests__/homeTabCache.test.ts && npm run test && npm run type-check && npm run build - `neode-ui/src/views/Home.vue` imports `useCachedResource` and renders `RefreshIndicator` bound to the wallet resource's `loadState` - `npm run test -- src/views/__tests__/homeTabCache.test.ts` exits 0 with all six behaviors covered - A test asserts reactivation triggers a wallet revalidation even when the TTL has not lapsed - A test asserts the previously rendered wallet figure is still in the DOM during that revalidation - A test asserts no sessionStorage key exists for the wallet resource after a mount and reactivation cycle - The wallet or Web5 status resource is declared `persist: false` - `hydrateWalletSnapshot` is still called from `onMounted` and still paints before any network call - The `wsClient.subscribe` wallet-push path from plan 02-04 is still present and still triggers a wallet refresh - `npm run test` exits 0, `npm run type-check` exits 0, `npm run build` exits 0 - The SUMMARY records each Home key with its TTL, persist choice and reason, and states which keys are shared with `Web5.vue` Home's system, update and storage figures come from cache on revisit, the wallet always re-checks visibly on re-entry without blanking or persisting, and the real-time wallet push path is intact.

<threat_model>

Trust Boundaries

Boundary Description
node RPC responses → browser cache System, network, Tor, VPN and wallet payloads now live longer in memory and possibly in sessionStorage
authenticated session → sessionStorage Anything persisted is readable by any script on the origin and survives reload
cached wallet figure → user's financial decision A balance shown from cache can drive a send decision

STRIDE Threat Register

Threat ID Category Component Severity Disposition Mitigation Plan
T-02-01 Information Disclosure Wallet balances, transaction history, VPN peer identity and Tor onion addresses written to sessionStorage by the default persist: true high mitigate Both tasks require explicit per-resource persist decisions; every identity-bearing or financial group is persist: false, asserted by a test in Task 2
T-02-13 Spoofing A cached wallet balance rendered as current after a paused poll high mitigate Task 2 revalidates the wallet unconditionally on re-entry rather than on TTL lapse, keeps the prior figure rendered, and binds RefreshIndicator to the in-flight state so the re-check is visible
T-02-17 Tampering Flattening a genuine load-order dependency among the seven Server loads to reduce measured latency medium mitigate Task 1 requires RESEARCH assumption A3 to be settled by reading each loader body, with the verdict recorded per loader and any real dependency kept ordered
T-02-16 Denial of Service Converting an already-concurrent fan-out into a serial chain of awaited refreshes medium mitigate Both tasks forbid per-group awaiting, require immediate: false plus a single Promise.allSettled, and assert overlapping start times
T-02-SC Tampering npm/pip/cargo installs high mitigate No package-manager installs are in scope. A task that finds it needs a new dependency stops and routes through the Package Legitimacy Gate with a blocking human checkpoint before installing
</threat_model>

<artifacts_this_phase_produces>

Artifacts this phase produces

Created or changed by this plan — new API, not drift:

  • neode-ui/src/views/__tests__/serverTabCache.test.ts
  • neode-ui/src/views/__tests__/homeTabCache.test.ts
  • Server cache keys for Tor status, network data, interfaces, disk status, Tor services, VPN peers and the FIPS summary
  • Home cache keys for system stats, update status, wallet/Web5 status and cloud storage usage (some shared with Web5.vue)

Created elsewhere in Phase 02: shouldKeepAlive(), KEEP_ALIVE_PATHS, KEEP_ALIVE_MAX, DashboardRouterView.vue, RefreshIndicator.vue, resources.clearAll(), useCachedResource.test.ts, keepAliveTabs.test.ts, keepAliveLifecycle.test.ts, meshTabCache.test.ts, secondaryScreenCache.test.ts, resourcesClear.test.ts, e2e/perf/{surfaces,measure,surface-perf.spec}.ts, .planning/phases/02-ui-performance/{02-FINDINGS.md,02-PERF-BASELINE.json,02-PERF-AFTER.json}. </artifacts_this_phase_produces>

<assumptions_and_flagged_items>

Assumptions & Flagged Items

  • PERF-02 edge-probe row (spec-less fallback): returned unclassified / unresolved. FLAGGED, not auto-backstopped and not dropped; surfaced here for human review. Resolved in substance by this plan's must_haves.truths, with the first-entry allowance carried as a verification: backstop marker because CONTEXT.md D-11 states it as an allowance rather than as an assertable check.
  • RESEARCH assumption A3 (carried, unresolved at plan time): the seven Server.vue loads are assumed independent with no ordering dependency. This planner did not verify it either. Task 1 makes settling it a precondition of the conversion and requires a per-loader verdict in the SUMMARY — the assumption is not permitted to pass through silently.
  • Open: whether Home can share wallet-adjacent cache keys with Web5.vue's two existing resources is decided during Task 2 by reading both, and recorded in the SUMMARY. Two entries for one dataset would double the request count this plan is reducing.
  • Note: the unconditional wallet revalidation on re-entry is a deliberate departure from the TTL-gated default. It costs one request per tab entry and buys the guarantee that a money figure is never presented as settled when it is merely cached. </assumptions_and_flagged_items>
- `cd neode-ui && npm run test` exits 0 - `cd neode-ui && npm run type-check` exits 0 - `cd neode-ui && npm run build` exits 0 - Server and Home revisit RPC counts are zero inside the TTL for their TTL-gated groups, asserted in their test files

<success_criteria>

  • Revisits to Server and Home paint from cache with no RPC for TTL-gated groups
  • The wallet always re-checks visibly on re-entry while keeping its previous figure on screen
  • No financial or identity payload from either tab is written to sessionStorage
  • Both cold-load fan-outs remain concurrent, and any genuine ordering dependency among the Server loads is preserved and documented
  • The real-time wallet push path is unchanged </success_criteria>
Create `.planning/phases/02-ui-performance/02-06-SUMMARY.md` when done. It MUST record: the RESEARCH A3 independence verdict per Server loader; every cache key introduced with its TTL, persist choice and reason; which Home keys are shared with `Web5.vue`; and confirmation that the websocket wallet-push path is intact.