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 |
|
|
true |
|
|
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.
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.
<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.tsneode-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'smust_haves.truths, with the first-entry allowance carried as averification: backstopmarker 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.vueloads 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>
<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>