docs(02-03): complete secondary screen caching plan
This commit is contained in:
parent
ec89901fa9
commit
ac6852893d
@ -73,7 +73,7 @@ Plans:
|
||||
3. Secondary screens open without a blocking full reload; repeat visits are instant
|
||||
4. The fixes are verified on real node hardware (not just the dev box) — the sluggishness the user reported is gone on-device
|
||||
|
||||
**Plans**: 1/8 plans executed
|
||||
**Plans**: 2/8 plans executed
|
||||
|
||||
Plans:
|
||||
**Wave 1**
|
||||
@ -83,7 +83,7 @@ Plans:
|
||||
**Wave 2** *(blocked on Wave 1 completion)*
|
||||
|
||||
- [ ] 02-02-PLAN.md — TRACER: KeepAlive host, hook reactivation, app-store tab, refresh indicator (PERF-02)
|
||||
- [ ] 02-03-PLAN.md — Secondary screens: per-item cache, parallel loads, purge on logout (PERF-03)
|
||||
- [x] 02-03-PLAN.md — Secondary screens: per-item cache, parallel loads, purge on logout (PERF-03)
|
||||
|
||||
**Wave 3** *(blocked on Wave 2 completion)*
|
||||
|
||||
@ -196,7 +196,7 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8
|
||||
| Phase | Plans Complete | Status | Completed |
|
||||
|-------|----------------|--------|-----------|
|
||||
| 1. Federation & Mesh Hardening | 0/10 | Planned | - |
|
||||
| 2. UI Performance | 1/8 | In Progress| |
|
||||
| 2. UI Performance | 2/8 | In Progress| |
|
||||
| 3. Multinode Verification Pass | 0/TBD | Not started | - |
|
||||
| 4. Lifecycle Perfection & Quadlet Default | 0/TBD | Not started | - |
|
||||
| 5. Registry-Distributed Manifests | 0/TBD | Not started | - |
|
||||
|
||||
@ -5,15 +5,15 @@ milestone_name: milestone
|
||||
current_phase: 02
|
||||
current_phase_name: ui-performance
|
||||
status: executing
|
||||
stopped_at: Completed 02-01-PLAN.md
|
||||
last_updated: "2026-07-30T10:46:50.979Z"
|
||||
stopped_at: Completed 02-03-PLAN.md
|
||||
last_updated: "2026-07-30T12:27:28.785Z"
|
||||
last_activity: 2026-07-30
|
||||
last_activity_desc: Phase 02 execution started
|
||||
progress:
|
||||
total_phases: 8
|
||||
completed_phases: 0
|
||||
total_plans: 18
|
||||
completed_plans: 1
|
||||
completed_plans: 2
|
||||
percent: 0
|
||||
---
|
||||
|
||||
@ -33,7 +33,7 @@ Plan: 2 of 8
|
||||
Status: Ready to execute
|
||||
Last activity: 2026-07-30 — Phase 02 execution started
|
||||
|
||||
Progress: [█░░░░░░░░░] 6%
|
||||
Progress: [█░░░░░░░░░] 11%
|
||||
|
||||
## Performance Metrics
|
||||
|
||||
@ -53,6 +53,7 @@ Progress: [█░░░░░░░░░] 6%
|
||||
| Plan | Duration | Tasks | Files |
|
||||
|------|----------|-------|-------|
|
||||
| Phase 02 P01 | 100min | 3 tasks | 5 files |
|
||||
| Phase 02 P03 | 45min | 3 tasks | 5 files |
|
||||
|
||||
## Accumulated Context
|
||||
|
||||
@ -74,6 +75,10 @@ Decisions are logged in PROJECT.md (10 locked ADRs in the `<decisions>` block +
|
||||
- [Phase ?]: Marketplace is the 02-02 tracer tab (worst-measured main tab, 2033ms revisit) matching the user's own 'often app store' complaint
|
||||
- [Phase ?]: ContainerAppDetails.vue confirmed fully unreachable dead code (no importer, no route) — no serial-RPC-waterfall target exists in the measured D-09 surface set
|
||||
- [Phase ?]: archi-dev-box UI password was unknown/undiscoverable from this environment — paused at a checkpoint:human-action rather than guessing or falling back to a mock baseline silently
|
||||
- [Phase ?]: Purged the resource cache on logout via clearAll() + a generation guard, so no in-flight fetch from an ending session can repopulate memory or sessionStorage (T-02-02)
|
||||
- [Phase ?]: AppDetails/MarketplaceAppDetails/OpenWrtGateway converted to per-item (or single-key) keyed useCachedResource; CloudFolder.vue's existing store-level cache left as-is (cloud.ts TTL gate is a follow-up, out of this plan's file scope)
|
||||
- [Phase ?]: Wallet/send flow (SendBitcoinModal.vue) reported as an unplanned-item gap — named by findings as owned by 02-03 but not in files_modified; its cost is pure client-side remount, not a caching problem
|
||||
- [Phase ?]: PERF-03 reverted to Pending in REQUIREMENTS.md after an initial mark-complete was premature — its own text requires real-node-hardware verification, which is 02-08's job (also declares PERF-03); 02-03 delivers the code-level portion only
|
||||
|
||||
### Pending Todos
|
||||
|
||||
@ -86,6 +91,7 @@ None yet.
|
||||
- [Phase 5] Fleet registry flip awaits explicit user authorization + timing call
|
||||
- [Phase 6] Strengthened ADR-009 validation may reject existing catalog apps — audit manifests before enforcement lands
|
||||
- [Global] Live OTA fleet: deploy to the dev pair before any OTA; gate re-runs required after orchestrator changes; some verification is user/hardware-gated (radios, on-device tests)
|
||||
- cloud.ts's navigate() needs a TTL gate to fully satisfy 'no new RPC within TTL' for CloudFolder.vue — currently always re-fetches on revisit (just doesn't block paint)
|
||||
|
||||
### Quick Tasks Completed
|
||||
|
||||
@ -107,6 +113,6 @@ None yet.
|
||||
|
||||
## Session Continuity
|
||||
|
||||
Last session: 2026-07-30T10:46:50.963Z
|
||||
Stopped at: Completed 02-01-PLAN.md
|
||||
Last session: 2026-07-30T12:26:09.237Z
|
||||
Stopped at: Completed 02-03-PLAN.md
|
||||
Resume file: None
|
||||
|
||||
61
.planning/WINDOWS.md
Normal file
61
.planning/WINDOWS.md
Normal file
@ -0,0 +1,61 @@
|
||||
---
|
||||
schema_version: 1
|
||||
open_count: 3
|
||||
waived_count: 0
|
||||
fixed_count: 0
|
||||
total_count: 3
|
||||
last_updated: 2026-07-30T12:25:22.605Z
|
||||
---
|
||||
|
||||
# Broken Windows Ledger
|
||||
|
||||
> Cross-phase defect register. `/gsd-ship` blocks while `open_count > 0`.
|
||||
> Waive with `gsd-tools windows waive <id> "<reason>"` (reason required).
|
||||
> Mark fixed with `gsd-tools windows fixed <id>`.
|
||||
|
||||
| id | phase | kind | file | line | description | status | reason | recorded_at | resolved_at |
|
||||
|----|-------|------|------|------|-------------|--------|--------|-------------|-------------|
|
||||
| 1 | 02 | deviation | neode-ui/src/stores/cloud.ts | | CloudFolder.vue's file listing cache lacks a TTL gate in cloudStore.navigate() — always re-issues the RPC on revisit (paints from cache instantly first, but still refetches unconditionally). Needs a TTL check added to navigate() to fully satisfy 'no new RPC within TTL'. | open | | 2026-07-30T12:25:22.301Z | |
|
||||
| 2 | 02 | deviation | neode-ui/src/views/Home.vue | | Wallet/send flow (SendBitcoinModal.vue via Home.vue) named by 02-FINDINGS.md as owned by 02-03 (worst-ranked revisit, 2607ms) but not in 02-03-PLAN.md's files_modified — reported as an unplanned-item gap, not converted. Cause is pure client-side remount cost (0 RPC), not a caching problem. | open | | 2026-07-30T12:25:22.450Z | |
|
||||
| 3 | 02 | deviation | neode-ui/src/views/PeerFiles.vue | | 02-03-PLAN.md assumed PeerFiles.vue already used useCachedResource; it actually uses the raw resources store directly (correctly per-item-keyed) with no TTL gate and the same loading/refreshing conflation bug fixed in OpenWrtGateway.vue this plan. Left untouched (out of files_modified scope) — candidate for the same fix in a future plan. | open | | 2026-07-30T12:25:22.605Z | |
|
||||
|
||||
````json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"kind": "deviation",
|
||||
"phase": "02",
|
||||
"file": "neode-ui/src/stores/cloud.ts",
|
||||
"line": null,
|
||||
"description": "CloudFolder.vue's file listing cache lacks a TTL gate in cloudStore.navigate() — always re-issues the RPC on revisit (paints from cache instantly first, but still refetches unconditionally). Needs a TTL check added to navigate() to fully satisfy 'no new RPC within TTL'.",
|
||||
"status": "open",
|
||||
"reason": "",
|
||||
"recorded_at": "2026-07-30T12:25:22.301Z",
|
||||
"resolved_at": null
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"kind": "deviation",
|
||||
"phase": "02",
|
||||
"file": "neode-ui/src/views/Home.vue",
|
||||
"line": null,
|
||||
"description": "Wallet/send flow (SendBitcoinModal.vue via Home.vue) named by 02-FINDINGS.md as owned by 02-03 (worst-ranked revisit, 2607ms) but not in 02-03-PLAN.md's files_modified — reported as an unplanned-item gap, not converted. Cause is pure client-side remount cost (0 RPC), not a caching problem.",
|
||||
"status": "open",
|
||||
"reason": "",
|
||||
"recorded_at": "2026-07-30T12:25:22.450Z",
|
||||
"resolved_at": null
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"kind": "deviation",
|
||||
"phase": "02",
|
||||
"file": "neode-ui/src/views/PeerFiles.vue",
|
||||
"line": null,
|
||||
"description": "02-03-PLAN.md assumed PeerFiles.vue already used useCachedResource; it actually uses the raw resources store directly (correctly per-item-keyed) with no TTL gate and the same loading/refreshing conflation bug fixed in OpenWrtGateway.vue this plan. Left untouched (out of files_modified scope) — candidate for the same fix in a future plan.",
|
||||
"status": "open",
|
||||
"reason": "",
|
||||
"recorded_at": "2026-07-30T12:25:22.605Z",
|
||||
"resolved_at": null
|
||||
}
|
||||
]
|
||||
````
|
||||
217
.planning/phases/02-ui-performance/02-03-SUMMARY.md
Normal file
217
.planning/phases/02-ui-performance/02-03-SUMMARY.md
Normal file
@ -0,0 +1,217 @@
|
||||
---
|
||||
phase: 02-ui-performance
|
||||
plan: 03
|
||||
subsystem: ui
|
||||
tags: [vue3, pinia, stale-while-revalidate, sessionStorage, useCachedResource]
|
||||
|
||||
# Dependency graph
|
||||
requires:
|
||||
- phase: 02-ui-performance/02-01
|
||||
provides: "02-FINDINGS.md's per-surface measured causes and Ranked Fix Order; the existing useCachedResource composable and resources.ts store this plan extends"
|
||||
provides:
|
||||
- "resources.ts clearAll() — purges every cached resource (memory + sessionStorage) on logout, with a generation guard so an in-flight fetch from the ending session cannot repopulate the cache after it resolves"
|
||||
- "auth.ts logout() calls clearAll() unconditionally (success and failure paths)"
|
||||
- "AppDetails.vue and MarketplaceAppDetails.vue converted to per-item keyed useCachedResource"
|
||||
- "OpenWrtGateway.vue converted from an always-refetch raw-store read to a TTL-gated useCachedResource, fixing a loading/refreshing conflation bug that hid cached content behind a full skeleton on every mount"
|
||||
affects: [02-04-keepalive-lifecycle, 02-08-verify]
|
||||
|
||||
# Tech tracking
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns:
|
||||
- "Per-item cache key family: app-details:<dataset>:<routeId> — computed once at setup time, safe because DashboardRouterView.vue keys <component :is> by route.path, so an id change on these routes always fully remounts the component (confirmed by reading the template; no watch-driven re-key needed)"
|
||||
- "TTL-gated force-refresh wrapper: a mutable pendingParams closure variable lets an explicit force-refresh function (load()) reuse the same useCachedResource instance an onMounted staleness check gates, without re-keying or re-creating the hook"
|
||||
- "loading must exclude 'refreshing' from any state that blocks rendering the cached view — conflating them (pre-existing pattern in OpenWrtGateway.vue) hides already-rendered content behind a full loading skeleton on every background revalidation, defeating stale-while-revalidate's entire purpose"
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- neode-ui/src/stores/__tests__/resourcesClear.test.ts
|
||||
- neode-ui/src/views/__tests__/secondaryScreenCache.test.ts
|
||||
modified:
|
||||
- neode-ui/src/stores/resources.ts
|
||||
- neode-ui/src/stores/auth.ts
|
||||
- neode-ui/src/views/AppDetails.vue
|
||||
- neode-ui/src/views/MarketplaceAppDetails.vue
|
||||
- neode-ui/src/views/server/OpenWrtGateway.vue
|
||||
|
||||
key-decisions:
|
||||
- "CloudFolder.vue: left unchanged, not converted to useCachedResource. Its data flows through cloudStore's own hand-rolled per-path Map cache (cloud.ts), which already delivers instant paint-from-cache on revisit (0 RPC measured on revisit per 02-PERF-BASELINE.json) and correctly excludes file listings from sessionStorage (D-08 satisfied by construction, not by an explicit persist:false). A literal useCachedResource conversion that also adds TTL-gated no-refetch semantics requires cloudStore.navigate() itself to skip its RPC when the cached path is fresh — that change lives in cloud.ts, which is outside this plan's files_modified. Bolting a second, parallel cache onto the view without touching cloud.ts was rejected: it would either leak a subscription/focus-listener per folder visited (useCachedResource's disposal path only fires from an active effect scope, which a dynamic per-path key called from inside a watch callback doesn't reliably have) or duplicate cloud.ts's pathCache and fragment the single source of truth CloudToolbar/FileGrid/breadcrumbs already read from cloudStore. Flagged as a residual gap for a future plan to land inside cloud.ts's navigate()."
|
||||
- "OpenWrtGateway.vue's cache key has no item id — the route (server/openwrt) has no :id param and there is exactly one configured gateway per node, so a bare 'server.openwrt-status' key is correct and no per-item collision risk exists."
|
||||
- "Wallet/send flow (SendBitcoinModal.vue via Home.vue) is named by 02-FINDINGS.md as owned by 02-03 (worst-ranked revisit at 2607ms) but does not appear in this plan's files_modified. Per Task 3's scope guard, this is reported as an unplanned-item gap rather than silently dropped or force-fitted into an out-of-scope file edit — see 'Unplanned-Item Gap' below."
|
||||
- "PeerFiles.vue does NOT already consume useCachedResource as this plan's Task 3 read_first assumed — it uses the raw resources store directly (resources.entry/resources.refresh, same pattern OpenWrtGateway.vue used before this plan) with a correctly per-item key (cloud.peer-browse:<onion>). It also force-refetches unconditionally on every mount (no staleness gate) and conflates 'refreshing' with a blocking loading state, the same bug this plan just fixed in OpenWrtGateway.vue. Left untouched — out of files_modified scope — but flagged here as a correction to the plan's assumption and a candidate for the same TTL-gate + loading-state fix in a future plan."
|
||||
- "ContainerAppDetails.vue: reconfirmed fully unreachable (zero grep matches, no importer, no route entry) per 02-FINDINGS.md's 'Corrections to Prior Research' section. Untouched, as required."
|
||||
- "PERF-03 is NOT marked complete in REQUIREMENTS.md despite being this plan's sole `requirements:` entry — its own requirement text conditions completion on 'verified on real node hardware, not just the dev box', which is 02-08's on-device checkpoint:human-verify pass (02-08-PLAN.md also declares PERF-03 in its frontmatter). This plan delivers the code-level portion only; an earlier automated `requirements.mark-complete PERF-03` run was reverted after re-reading REQUIREMENTS.md's own text — do not re-mark it complete until 02-08 lands."
|
||||
|
||||
requirements-completed: []
|
||||
|
||||
coverage:
|
||||
- id: D1
|
||||
description: "Logout purges every cached resource (memory + sessionStorage), including one that was in-flight when logout ran"
|
||||
requirement: "PERF-03"
|
||||
verification:
|
||||
- kind: unit
|
||||
ref: "neode-ui/src/stores/__tests__/resourcesClear.test.ts"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D2
|
||||
description: "AppDetails.vue's bitcoin-sync and credentials data paint from a per-item cache on repeat visits, never cross items, and credentials stay memory-only"
|
||||
requirement: "PERF-03"
|
||||
verification:
|
||||
- kind: unit
|
||||
ref: "neode-ui/src/views/__tests__/secondaryScreenCache.test.ts#AppDetails.vue — per-item cached resources (bitcoin sync + credentials)"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D3
|
||||
description: "MarketplaceAppDetails.vue's catalog version data is per-item cached (120s TTL) instead of refetched on every mount"
|
||||
requirement: "PERF-03"
|
||||
verification:
|
||||
- kind: unit
|
||||
ref: "neode-ui/src/views/__tests__/secondaryScreenCache.test.ts#MarketplaceAppDetails.vue — per-item cached catalog versions"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D4
|
||||
description: "OpenWrtGateway.vue's router status paints from cache on repeat visits with no new RPC inside the TTL, and no longer hides cached content behind a loading skeleton during background revalidation"
|
||||
requirement: "PERF-03"
|
||||
verification:
|
||||
- kind: unit
|
||||
ref: "neode-ui/src/views/__tests__/secondaryScreenCache.test.ts#OpenWrtGateway.vue — cached router status (no item id: one gateway per node)"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D5
|
||||
description: "CloudFolder.vue cache-placement decision and Wallet/send-flow scope gap are documented, not silently dropped"
|
||||
verification: []
|
||||
human_judgment: true
|
||||
rationale: "This is a documentation/scope-judgment deliverable (why a screen was left as-is or reported as a gap), not a testable code behavior — a human should confirm the reasoning is sound before the next plan (02-08 verify, or a future cloud.ts follow-up) relies on it."
|
||||
|
||||
# Metrics
|
||||
duration: 45min
|
||||
completed: 2026-07-30
|
||||
status: complete
|
||||
---
|
||||
|
||||
# Phase 02 Plan 03: Secondary Screen Caching Summary
|
||||
|
||||
**Per-item keyed `useCachedResource` conversions for AppDetails, MarketplaceAppDetails, and OpenWrtGateway, plus a logout cache-purge with a generation guard against in-flight-fetch resurrection — CloudFolder's existing store-level cache is left in place with the reasoning recorded.**
|
||||
|
||||
## Performance
|
||||
|
||||
- **Duration:** ~45 min
|
||||
- **Tasks:** 3
|
||||
- **Files modified:** 5 (2 new test files, 1 new + 2 modified store files, 3 modified view files)
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- `resources.ts` gained `clearAll()`, wired into `auth.ts`'s `logout()` on both the success and failure paths, so no cached payload (memory or sessionStorage) outlives a logout — including one that was mid-flight when logout ran (closed via a generation counter, a real gap the TDD tests caught).
|
||||
- `AppDetails.vue`'s bitcoin-sync and credentials data are now keyed per app id (`app-details:bitcoin-sync:<id>`, `app-details:credentials:<id>`), each with an explicit TTL and persist choice; credentials is memory-only. Stop/restart/uninstall now invalidate the credentials cache so a stale healthy state can't outlive a destructive action.
|
||||
- `MarketplaceAppDetails.vue`'s one non-confounded RPC (catalog `package.versions`) is now a keyed, long-TTL cached resource; the other calls on this screen (`getCurrentApp()`, the bitcoin-prune `fetch()`) were confirmed not to need conversion.
|
||||
- `OpenWrtGateway.vue` converted from an unconditional every-mount refetch to a TTL-gated cache, and a real bug this exposed — `loading` treating a background `'refreshing'` revalidation the same as a blocking `'loading'` state — is fixed, so cached content now actually paints instantly instead of flashing a skeleton on every visit.
|
||||
- `CloudFolder.vue`'s cache-placement question was resolved: leave the existing `cloud.ts` per-path cache in place (already satisfies the plan's truths for this screen) rather than bolt on a redundant or leak-prone second cache confined to the view.
|
||||
|
||||
## Task Commits
|
||||
|
||||
1. **Task 1: Cache lifetime — purge every cached resource on logout** - `f44b8ac7` (feat)
|
||||
2. **Task 2: App detail screens open instantly on repeat visits** - `7c6c487a` (feat)
|
||||
3. **Task 3: The remaining findings-named secondary screens** - `ec89901f` (feat)
|
||||
|
||||
**Plan metadata:** (this commit)
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
- `neode-ui/src/stores/resources.ts` — added `clearAll()` and a `generation` counter guard against post-purge in-flight writes
|
||||
- `neode-ui/src/stores/auth.ts` — `logout()` calls `useResourcesStore().clearAll()` unconditionally
|
||||
- `neode-ui/src/stores/__tests__/resourcesClear.test.ts` — new; covers all `clearAll()`/logout behaviors
|
||||
- `neode-ui/src/views/AppDetails.vue` — bitcoin-sync + credentials converted to keyed `useCachedResource`; invalidate on stop/restart/uninstall
|
||||
- `neode-ui/src/views/MarketplaceAppDetails.vue` — catalog versions converted to a keyed `useCachedResource`
|
||||
- `neode-ui/src/views/server/OpenWrtGateway.vue` — router status converted from raw-store always-refetch to TTL-gated `useCachedResource`; fixed the `loading`/`refreshing` conflation bug
|
||||
- `neode-ui/src/views/__tests__/secondaryScreenCache.test.ts` — new; covers all four converted resources' repeat-open/TTL/keep-last-value/concurrency behaviors
|
||||
|
||||
## Cache Keys Introduced
|
||||
|
||||
| Key | File | TTL | Persist | Reason |
|
||||
|---|---|---|---|---|
|
||||
| `app-details:bitcoin-sync:<appId>` | AppDetails.vue | 30 000 ms | `true` (default) | Non-sensitive numeric health/sync state; default TTL matches plan guidance for "install/health-state-shaped data" |
|
||||
| `app-details:credentials:<appId>` | AppDetails.vue | 30 000 ms | **`false`** | Credential material (D-08 / T-02-01) — memory-only, never written to sessionStorage |
|
||||
| `app-details:versions:<appId>` | MarketplaceAppDetails.vue | 120 000 ms | `true` (default) | Near-static catalog metadata (version list, deprecation/EOL flags) — no credential/DID/wallet/tx-history content, so a longer TTL than the 30s default is appropriate per plan guidance |
|
||||
| `server.openwrt-status` | OpenWrtGateway.vue | 30 000 ms | `true` (default) | No item id — one gateway per node, fixed route with no `:id` param. Contains host/hostname/uptime/release/tollgate-config/wifi/wan status; none of it is credential/DID/wallet/tx-history material |
|
||||
|
||||
## Findings-Named Secondary Screens: Converted vs. Gap
|
||||
|
||||
Per `02-FINDINGS.md`'s Owning Plans table, five secondary/modal surfaces are named as owned by 02-03:
|
||||
|
||||
| Surface | Status | Notes |
|
||||
|---|---|---|
|
||||
| AppDetails | **Converted** (Task 2) | bitcoin-sync + credentials keyed resources |
|
||||
| MarketplaceAppDetails | **Converted** (Task 2) | catalog versions keyed resource |
|
||||
| CloudFolder | **Decision recorded, left unchanged** (Task 3) | See "CloudFolder.vue Cache-Placement Decision" below |
|
||||
| OpenWrtGateway | **Converted** (Task 3) | router status keyed resource, no item id |
|
||||
| Wallet / send flow (SendBitcoinModal.vue via Home.vue) | **Unplanned-item gap** | Named by findings (worst-ranked revisit, 2607ms) but not in this plan's `files_modified`; `Home.vue`/`SendBitcoinModal.vue` were not touched. Per the plan's Task 3 scope guard, this is reported to the orchestrator as a gap rather than silently dropped or force-fitted into an out-of-scope edit. Measured cause per findings: the send modal fully remounts on every reopen (`BaseModal`'s `v-if`) with **zero RPC either time** — the ~1.9s extra cost on reopen is pure client-side recompute (fee/balance/store re-subscription), not a data-cache problem, so this surface likely needs a different fix (client-side profiling / render-cost reduction) rather than a `useCachedResource` conversion. Recommend a follow-up task or plan scoped explicitly to `Home.vue`/`SendBitcoinModal.vue`. |
|
||||
|
||||
## CloudFolder.vue Cache-Placement Decision
|
||||
|
||||
`CloudFolder.vue`'s file-listing data flows through `cloudStore` (`src/stores/cloud.ts`), driven by two `watch()` blocks rather than a mount hook. `cloud.ts` already implements its own hand-rolled per-path stale-while-revalidate cache (`pathCache: Map<string, FileBrowserItem[]>` in `navigate()`): a revisit to a previously-viewed path paints the cached listing synchronously while a background refresh runs underneath, and the listing is never written to sessionStorage (D-08 is satisfied by construction — this is a plain in-memory `Map`, not backed by `resources.ts`).
|
||||
|
||||
Decision: **leave this mechanism in place; do not add a `useCachedResource` wrapper in `CloudFolder.vue`.**
|
||||
|
||||
Reasoning:
|
||||
1. **Single consumer.** Only `CloudFolder.vue` calls `cloudStore.navigate()`/reads `cloudStore.currentPath`/`sortedItems` for the file-listing role (`Cloud.vue` uses a separate `peersResource`/`countsResource` pair for federation peers, an unrelated dataset). The plan's guidance to "put the cached resource behind the store action" when several views share a loader doesn't apply here.
|
||||
2. **The one real gap — no TTL gate — lives in `cloud.ts`, not the view.** `cloudStore.navigate()` always re-issues its RPC on every call, regardless of freshness (it just doesn't block rendering, since the cached listing paints first). Properly closing this gap means adding a TTL check inside `navigate()` itself before firing `fileBrowserClient.listDirectory()`. `cloud.ts` is **not** in this plan's `files_modified` (only `CloudFolder.vue` is), so that change is out of scope here.
|
||||
3. **The alternatives were worse than the status quo.** Wrapping the view's own reads in a fresh `useCachedResource` call *inside* the path-change `watch()` callback (needed because the key is dynamic, one per path) would call the composable outside a reliably-active effect scope — `onScopeDispose` (which registers the `window.addEventListener('focus', ...)` cleanup) only fires when `getCurrentScope()` returns non-null, which a `watch` callback invoked via the reactivity scheduler doesn't reliably provide. That risks a `focus`-listener leak per folder visited for the life of the mount. The other alternative — duplicating `cloud.ts`'s `pathCache` logic directly inside the view — would fragment the single source of truth `CloudToolbar` (breadcrumbs) and `FileGrid` (items) already read from `cloudStore`.
|
||||
4. **Measured evidence supports leaving it.** `02-PERF-BASELINE.json`/`02-FINDINGS.md` record `CloudFolder`'s revisit RPC count as `0` with `remounted: true` — the current mechanism already delivers "paints instantly from cache, no new RPC" for the *revisit* case; the ~5s first-visit cost is a lazy route-chunk cold load, unrelated to data caching.
|
||||
|
||||
**Flagged for follow-up:** a future plan (or an extension of a `cloud.ts`-scoped plan) should add the TTL gate inside `navigate()` so a path visited within its TTL skips the RPC entirely, matching the letter of "no new RPC for the cached dataset" in addition to the spirit ("paints instantly, no blocking reload") this screen already satisfies.
|
||||
|
||||
## Decisions Made
|
||||
|
||||
- **`app-details:bitcoin-sync:<appId>` / `app-details:credentials:<appId>` keys are computed once at setup time, not re-derived via a `watch`.** Confirmed by reading `DashboardRouterView.vue`: `<component :is="Component" :key="route.path" />` — since `route.path` for `apps/:id` includes the id itself, an id change on this route always produces a different key, which Vue treats as a full unmount/remount. This was an open assumption in the plan (`02-03-PLAN.md`'s "Assumptions & Flagged Items"); it resolves to "always remounts," so no `watch`-driven re-key was needed for AppDetails or MarketplaceAppDetails.
|
||||
- **`server.openwrt-status` has no item id in its key.** The route (`server/openwrt`) has no `:id` param and there is exactly one configured router gateway per node — a bare key is correct; adding a fake per-node id would be scope creep with no isolation benefit.
|
||||
- **`ContainerAppDetails.vue` verdict reconfirmed:** fully unreachable (per `02-FINDINGS.md`'s "Corrections to Prior Research" — zero grep matches, no importer, no route entry). Untouched, as the plan requires; `git diff --name-only HEAD -- neode-ui/src/views/ContainerAppDetails.vue` prints nothing.
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Auto-fixed Issues
|
||||
|
||||
**1. [Rule 1 - Bug] `resources.ts`'s `refresh()` could repopulate memory/sessionStorage after `clearAll()`**
|
||||
- **Found during:** Task 1, writing the TDD test for "a resolving fetch from the old session cannot repopulate the cache"
|
||||
- **Issue:** `clearAll()` cleared the `entries`/`inflight`/`revalidators`/`invalidateTimers` maps, but an already-in-flight `refresh()` call captured its own reference to the (now-detached) entry object before `clearAll()` ran. When that fetch resolved afterward, its `writeSnapshot()` call still executed unconditionally, writing a fresh sessionStorage entry for a key that had just been purged.
|
||||
- **Fix:** Added a `generation` counter, incremented by `clearAll()`. `refresh()` captures `startGeneration` at call time and checks `generation !== startGeneration` before writing to the entry or to sessionStorage, both on the success and error paths.
|
||||
- **Files modified:** `neode-ui/src/stores/resources.ts`
|
||||
- **Verification:** `resourcesClear.test.ts`'s "drops in-flight bookkeeping..." test asserts no sessionStorage write survives a post-clearAll resolution.
|
||||
- **Committed in:** `f44b8ac7` (Task 1 commit)
|
||||
|
||||
**2. [Rule 1 - Bug] `OpenWrtGateway.vue`'s `loading` computed hid cached content behind a full skeleton on every background revalidation**
|
||||
- **Found during:** Task 3, writing the TTL-lapse repeat-open test — the cached hostname failed to appear on the first frame after a stale remount
|
||||
- **Issue:** `loading` treated `loadState === 'refreshing'` the same as `'loading'`, so any time a cached entry's status flipped to `'refreshing'` (background revalidate in flight), the full loading skeleton rendered instead of the already-cached status panels — directly contradicting D-07's keep-last-value requirement and this plan's must_haves truth ("previous content stays on screen while exactly one background revalidation runs"). This was previously masked because the pre-existing code force-refetched unconditionally on every mount, so the skeleton showed on literally every visit regardless of cache freshness — a UX regression that predates this plan but only became visible/fixable once mount-time force-fetching was replaced with TTL gating.
|
||||
- **Fix:** `loading` now only blocks on `routerResource.data.value === null && (loadState === 'loading' || loadState === 'idle')` — a true first-load with no data at all. A background refresh (`'refreshing'`) no longer hides the rendered status panels.
|
||||
- **Files modified:** `neode-ui/src/views/server/OpenWrtGateway.vue`
|
||||
- **Verification:** `secondaryScreenCache.test.ts`'s "a repeat mount after the TTL lapses shows cached data on the first frame..." test for OpenWrtGateway.
|
||||
- **Committed in:** `ec89901f` (Task 3 commit)
|
||||
|
||||
---
|
||||
|
||||
**Total deviations:** 2 auto-fixed (both Rule 1 — bugs directly blocking this plan's must_haves truths)
|
||||
**Impact on plan:** Both fixes were necessary for the plan's core correctness guarantee (stale-while-revalidate actually keeping cached content visible); no scope creep — both stayed within the files already being converted.
|
||||
|
||||
## Issues Encountered
|
||||
|
||||
- Initial test-writing pass for AppDetails/MarketplaceAppDetails created a **fresh Pinia instance per mount**, which silently defeated the in-memory cache for `persist: false` resources (credentials) between a mount/unmount/remount pair within one test — the production app has a single, app-lifetime Pinia instance, so this was a test-authoring bug, not a product bug. Fixed by sharing one `Pinia` instance across the mount pairs within each test (matching how a real browsing session keeps the same Pinia across secondary-screen navigation).
|
||||
- Vue's `useI18n()` (used by both `AppDetails.vue` and `MarketplaceAppDetails.vue`) throws "Need to install with `app.use` function" without a mounted i18n plugin — resolved by mocking `vue-i18n` directly (`useI18n: () => ({ t: (key) => key })`), matching the existing in-repo convention in `MarketplaceRefresh.test.ts`.
|
||||
|
||||
## User Setup Required
|
||||
|
||||
None - no external service configuration required.
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
- `resources.ts`'s `clearAll()` is available for any other plan that needs a full-cache purge (e.g. an identity-switch flow beyond logout).
|
||||
- The `app-details:*` / `server.openwrt-status` key family and the "gate `.refresh()` on `data === null || isStale`" pattern established here (rather than the composable's own `immediate` auto-refresh) is reusable for future per-item secondary-screen conversions — see `AppDetails.vue`/`OpenWrtGateway.vue` for the reference shape.
|
||||
- **Blocker/concern carried forward:** the Wallet/send-flow gap (`Home.vue`/`SendBitcoinModal.vue`, 2607ms revisit, zero RPC — pure client-side recompute cost) needs its own task or plan; it is not a data-caching problem and a `useCachedResource` conversion would not fix it.
|
||||
- **Blocker/concern carried forward:** `cloud.ts`'s `navigate()` needs a TTL gate to fully satisfy "no new RPC within the TTL" for `CloudFolder.vue`; today it always re-fetches on every call (just without blocking the paint). Not urgent — the measured revisit cost is already near-zero — but noted for whichever plan next touches `cloud.ts`.
|
||||
- **Correction carried forward:** `PeerFiles.vue` does not already use `useCachedResource` (it uses the raw `resources` store directly, correctly per-item-keyed) and shares the same "refreshing hides content" pattern this plan fixed in `OpenWrtGateway.vue`. Worth a small follow-up fix when that file is next touched.
|
||||
|
||||
---
|
||||
*Phase: 02-ui-performance*
|
||||
*Completed: 2026-07-30*
|
||||
|
||||
## Self-Check: PASSED
|
||||
|
||||
All created/modified files verified present on disk; all three task commit hashes (`f44b8ac7`, `7c6c487a`, `ec89901f`) verified in git history.
|
||||
Loading…
x
Reference in New Issue
Block a user