380 lines
28 KiB
Markdown
380 lines
28 KiB
Markdown
---
|
|
phase: 02-ui-performance
|
|
plan: 03
|
|
type: execute
|
|
wave: 2
|
|
depends_on: ["02-01"]
|
|
files_modified:
|
|
- neode-ui/src/stores/resources.ts
|
|
- neode-ui/src/stores/auth.ts
|
|
- neode-ui/src/stores/__tests__/resourcesClear.test.ts
|
|
- neode-ui/src/views/AppDetails.vue
|
|
- neode-ui/src/views/MarketplaceAppDetails.vue
|
|
- neode-ui/src/views/__tests__/secondaryScreenCache.test.ts
|
|
- neode-ui/src/views/CloudFolder.vue
|
|
- neode-ui/src/views/server/OpenWrtGateway.vue
|
|
autonomous: true
|
|
requirements: [PERF-03]
|
|
|
|
must_haves:
|
|
truths:
|
|
- "Opening a secondary screen for an item already opened this session paints its content immediately from cache, with no blocking full reload"
|
|
- "A repeat open of the same secondary screen inside the TTL issues no new RPC for the cached dataset"
|
|
- "A repeat open after the TTL keeps the previous content on screen while exactly one background revalidation runs"
|
|
- "Opening secondary screen for item B never renders item A's data — each per-item cache key embeds the item identifier"
|
|
- "Secondary screens are not instance-cached: their component mounts fresh on every visit (D-04)"
|
|
- "Independent loads inside a secondary screen run concurrently rather than one awaiting the next"
|
|
- "Logging out clears every cached resource from memory and from sessionStorage, so the next session starts empty"
|
|
- "Large or peer-sourced payloads (file listings, media metadata) are held in memory only and are never written to sessionStorage (D-08)"
|
|
- "A destructive action taken on a secondary screen (uninstall, stop, remove) invalidates that screen's cached entry before the screen re-renders"
|
|
- statement: "First opens of a never-before-visited secondary screen may still show a loading state; only repeat opens are required to be instant"
|
|
verification: backstop
|
|
prohibitions:
|
|
- "MUST NOT let one item's or one identity's cached data be served under another — every per-item cache key is fully qualified by the item identifier, and the whole cache is cleared on logout"
|
|
- "MUST NOT persist peer-sourced content (other nodes' file listings, media metadata) to sessionStorage"
|
|
- "MUST NOT display a stale success or health state after a destructive action — such actions invalidate their screen's cache before rendering"
|
|
artifacts:
|
|
- path: "neode-ui/src/stores/resources.ts"
|
|
provides: "clearAll() — drops every cached entry from memory and every resource: snapshot from sessionStorage"
|
|
exports: ["clearAll"]
|
|
- path: "neode-ui/src/stores/auth.ts"
|
|
provides: "logout() purges the resource cache before the session ends"
|
|
- path: "neode-ui/src/views/__tests__/secondaryScreenCache.test.ts"
|
|
provides: "Fetcher call-count assertions proving cache-on-repeat-open and per-item key isolation"
|
|
- path: "neode-ui/src/stores/__tests__/resourcesClear.test.ts"
|
|
provides: "Coverage for clearAll and the logout purge"
|
|
key_links:
|
|
- from: "neode-ui/src/stores/auth.ts"
|
|
to: "neode-ui/src/stores/resources.ts"
|
|
via: "logout() calls clearAll() so no cached payload outlives the session"
|
|
pattern: "clearAll"
|
|
- from: "neode-ui/src/views/AppDetails.vue"
|
|
to: "neode-ui/src/composables/useCachedResource.ts"
|
|
via: "per-item keyed cached resources keyed by the route's app id"
|
|
pattern: "useCachedResource"
|
|
- from: "neode-ui/src/views/MarketplaceAppDetails.vue"
|
|
to: "neode-ui/src/composables/useCachedResource.ts"
|
|
via: "per-item keyed cached resource keyed by the route's marketplace app id"
|
|
pattern: "useCachedResource"
|
|
---
|
|
|
|
<objective>
|
|
Make secondary screens — the screens reached from a tab's main page — open without a
|
|
blocking reload and paint instantly on repeat visits, using the existing
|
|
stale-while-revalidate hook keyed per item, with no component-instance caching.
|
|
|
|
Purpose: PERF-03. D-04 is explicit that secondary screens get `useCachedResource` keyed
|
|
per item but no `<KeepAlive>` — item counts are unbounded and an instance cache would
|
|
bloat. The data cache alone delivers the instant repeat open.
|
|
|
|
This plan runs in parallel with the tracer (02-02): it touches a disjoint set of files
|
|
and it consumes `useCachedResource` exactly as its eight existing callers already do, so
|
|
it does not depend on the tracer's architecture landing first. It does own the
|
|
cache-lifetime safety work — the logout purge — that every other plan's caching relies on.
|
|
|
|
Output: a purge-on-logout cache lifecycle, and the findings-named secondary screens
|
|
converted to keyed cached resources with their independent loads parallelized.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@$HOME/.claude/gsd-core/workflows/execute-plan.md
|
|
@$HOME/.claude/gsd-core/templates/summary.md
|
|
</execution_context>
|
|
|
|
<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/codebase/CONVENTIONS.md
|
|
@CLAUDE.md
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Task 1: Cache lifetime — purge every cached resource on logout</name>
|
|
<files>neode-ui/src/stores/resources.ts, neode-ui/src/stores/auth.ts, neode-ui/src/stores/__tests__/resourcesClear.test.ts</files>
|
|
<read_first>
|
|
- `neode-ui/src/stores/resources.ts` — the whole file. Note `SNAPSHOT_PREFIX` (`resource:`), the `entries` reactive Map, the `inflight`, `revalidators` and `invalidateTimers` Maps, and the existing single-key `evict(key)` at line ~156 whose shape `clearAll` mirrors. The store's return object at the end of `defineStore` is what must gain the new export.
|
|
- `neode-ui/src/stores/auth.ts` — the whole file. `logout()` is at line ~76 and calls `rpcClient.logout()`. This is the single choke point: `Dashboard.vue`'s `handleLogout`, `views/settings/AccountSection.vue`'s `handleLogout` and `Login.vue` all route through it via `stores/app.ts`'s `logout: auth.logout` re-export.
|
|
- `neode-ui/src/stores/app.ts` line ~50 — confirms the re-export, so no additional call site needs editing.
|
|
- `neode-ui/src/stores/__tests__/` — list it and read one existing store test for the Pinia `setActivePinia(createPinia())` setup convention.
|
|
</read_first>
|
|
<behavior>
|
|
- After `clearAll()`, `entries.size` is 0
|
|
- After `clearAll()`, no sessionStorage key beginning with `resource:` remains, and keys not beginning with that prefix are untouched
|
|
- `clearAll()` cancels any pending invalidate timers and drops the in-flight and revalidator maps, so a resolving fetch from the old session cannot repopulate the cache
|
|
- `clearAll()` does not throw when sessionStorage is unavailable or throws on access
|
|
- `auth.logout()` clears the cache even when the backend `auth.logout` RPC rejects
|
|
</behavior>
|
|
<action>
|
|
Add `clearAll()` to `neode-ui/src/stores/resources.ts` and include it in the store's
|
|
returned object alongside the existing `evict`. It must: clear the `entries` Map;
|
|
clear the `inflight`, `revalidators` and `invalidateTimers` Maps, calling
|
|
`clearTimeout` on each pending timer first; and remove every sessionStorage key
|
|
beginning with `SNAPSHOT_PREFIX`. Iterate the sessionStorage keys into an array
|
|
before removing, so the live index does not shift mid-loop, and wrap the whole
|
|
storage section in the same defensive try/catch the file already uses around
|
|
`sessionStorage` access.
|
|
|
|
In `neode-ui/src/stores/auth.ts`, call `useResourcesStore().clearAll()` from
|
|
`logout()`. Place the call so it runs whether or not the `rpcClient.logout()` RPC
|
|
succeeds — a failed server-side logout must still leave no cached payload behind
|
|
locally. Do not add a second purge call at any other site; `auth.logout()` is the
|
|
choke point every logout path already funnels through.
|
|
|
|
Write `neode-ui/src/stores/__tests__/resourcesClear.test.ts` covering the five
|
|
behaviors above. For the sessionStorage assertions, seed both a `resource:`-prefixed
|
|
key and an unrelated key and assert only the former is removed. For the auth test,
|
|
mock `rpcClient.logout` to reject and assert the cache is still empty afterwards.
|
|
</action>
|
|
<verify>
|
|
<automated>cd neode-ui && npm run test -- src/stores/__tests__/resourcesClear.test.ts && npm run type-check</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- `neode-ui/src/stores/resources.ts` exports `clearAll` from its store return object
|
|
- `neode-ui/src/stores/auth.ts` calls `clearAll` inside `logout()`: `grep -c "clearAll" neode-ui/src/stores/auth.ts` is at least 1
|
|
- `npm run test -- src/stores/__tests__/resourcesClear.test.ts` exits 0 with all five behaviors covered
|
|
- A test asserts that a sessionStorage key not beginning with `resource:` survives `clearAll()`
|
|
- A test asserts the cache is empty after `logout()` when the logout RPC rejects
|
|
- `npm run type-check` exits 0
|
|
</acceptance_criteria>
|
|
<done>No cached resource — in memory or in sessionStorage — outlives a logout, and the guarantee is pinned by tests.</done>
|
|
</task>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Task 2: App detail screens open instantly on repeat visits</name>
|
|
<files>neode-ui/src/views/AppDetails.vue, neode-ui/src/views/MarketplaceAppDetails.vue, neode-ui/src/views/__tests__/secondaryScreenCache.test.ts</files>
|
|
<read_first>
|
|
- `neode-ui/src/views/AppDetails.vue` — the whole file is 386 lines. Read `onMounted` at line ~204 (`loadBitcoinSync(); loadCredentials()` — already fire-and-forget, so not a waterfall), both loader bodies, how the route's `:id` param reaches the component, and the existing error handling.
|
|
- `neode-ui/src/views/MarketplaceAppDetails.vue` — 700 lines. Grep for `onMounted` (line ~525), `rpcClient`, `fetch(` and `await` first, then read only the loader region and the `onMounted` block. Do not read the whole file.
|
|
- `neode-ui/src/views/Cloud.vue` lines 505-530 and 945-970 — the in-repo reference for a cached-resource definition and for the keep-last-value error handling to mirror.
|
|
- `neode-ui/src/composables/useCachedResource.ts` — the options contract (`key`, `fetcher`, `ttlMs`, `persist`, `revalidateOnFocus`, `immediate`) and the returned `entry` / `data` / `loadState` / `error` / `refresh` / `invalidate` surface.
|
|
- `neode-ui/src/api/rpc-client.ts` — the `dedup: true` option (line ~16, applied at line ~95) to pass on every newly-parallelized call so concurrent identical calls collapse.
|
|
- `neode-ui/src/views/__tests__/CloudPeersRefresh.test.ts` — the Vitest + `@vue/test-utils` + Pinia + `vi.mock('@/api/rpc-client')` pattern to follow.
|
|
- `.planning/phases/02-ui-performance/02-FINDINGS.md` — the measured revisit RPC count and primary cause for each of these two screens; if the findings classify one as already fast, leave it alone per D-02 and record that in the SUMMARY.
|
|
</read_first>
|
|
<behavior>
|
|
- Mounting AppDetails for app id `alpha`, unmounting, and remounting for `alpha` inside the TTL calls each fetcher exactly once in total
|
|
- Mounting AppDetails for `alpha` then for `beta` calls each fetcher twice in total and renders `beta`'s data, never `alpha`'s
|
|
- Remounting for `alpha` after the TTL lapses calls the fetcher once more while the previously cached data is already rendered on the first frame
|
|
- A rejected refresh leaves the previously rendered data in the DOM and sets the view's error ref
|
|
- The independent loads in a single mount are issued concurrently, not one after the other
|
|
</behavior>
|
|
<action>
|
|
Convert both app-detail screens to keyed cached resources, one resource per logical
|
|
dataset, following the `Cloud.vue` pattern.
|
|
|
|
Cache keys embed the item identifier so two items can never collide: use
|
|
`app-details:${appId}` shaped keys — for `AppDetails.vue` that means
|
|
`app-details:bitcoin-sync:${appId}` and `app-details:credentials:${appId}`, and for
|
|
`MarketplaceAppDetails.vue` a key of the same shape built from its own route param.
|
|
The key must be computed from the current route param at hook-call time, and the
|
|
component must re-key when the param changes (these screens are not instance-cached,
|
|
so a param change normally remounts them — confirm that by reading how the route is
|
|
declared, and if the router reuses the instance across an id change, drive the
|
|
resource through a `watch` on the id that calls `refresh()` against the new key).
|
|
|
|
Set `ttlMs` explicitly per resource rather than taking the default. Credentials and
|
|
install/health state move fast enough to warrant the 30000 ms default; near-static
|
|
catalog-shaped metadata can take a longer value. Set `persist` explicitly too:
|
|
anything carrying credential material, DIDs, wallet figures or transaction history
|
|
is `persist: false` and stays memory-only, per D-08 and the privacy prohibition in
|
|
this plan's `must_haves`. Record each key's TTL and persist choice in the SUMMARY.
|
|
|
|
Pass `dedup: true` on the underlying `rpcClient.call` for each fetcher so two mounted
|
|
consumers of the same method collapse into one request.
|
|
|
|
Where a screen awaits independent loads sequentially, replace the chain with a single
|
|
`await Promise.allSettled([...])` — `allSettled` rather than `all` so one failing
|
|
load does not suppress the others, matching the existing per-loader error handling
|
|
where each loader owns its own loading ref. This is D-13's client-side fix: waterfalls
|
|
are removed by parallelizing plus rpc-client dedup, and a new aggregate endpoint is
|
|
reserved for a screen that genuinely needs three or more dependent calls. If one of
|
|
these screens turns out to need such an endpoint, D-12 bounds it: additive only, a new
|
|
handler alongside the existing ones, no refactor of an existing handler and nothing
|
|
touching the orchestrator — and it stops for a checkpoint before any `core/` change,
|
|
since no backend work is otherwise in this plan's scope. Verify independence before
|
|
parallelizing:
|
|
a load that consumes another's result stays sequential. `AppDetails.vue`'s
|
|
`onMounted` already fires both loaders without awaiting them, so it is already
|
|
effectively parallel — do not "fix" it into something slower, and say so in the
|
|
SUMMARY.
|
|
|
|
Error handling follows D-07: a failed background refresh keeps the last known value
|
|
on screen and sets the view's existing error ref for a banner. No toast.
|
|
|
|
Invalidate before re-render after a destructive action: wherever these screens
|
|
trigger an uninstall, stop, or removal, call the affected resource's `invalidate()`
|
|
(or `refresh()`) as part of the action's completion path, so the screen cannot show a
|
|
stale healthy state for something that no longer exists.
|
|
|
|
Create `neode-ui/src/views/__tests__/secondaryScreenCache.test.ts` covering the five
|
|
behaviors above with `vi.fn()` fetchers and explicit call-count assertions. Use fake
|
|
timers to cross the TTL boundary. The per-item isolation test is the important one —
|
|
assert on rendered content, not only on call counts.
|
|
</action>
|
|
<verify>
|
|
<automated>cd neode-ui && npm run test -- src/views/__tests__/secondaryScreenCache.test.ts && npm run type-check</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- `neode-ui/src/views/AppDetails.vue` and `neode-ui/src/views/MarketplaceAppDetails.vue` each import and call `useCachedResource`
|
|
- Every cache key introduced contains the route item id — a test asserts that mounting for `alpha` then `beta` produces two distinct keys and renders `beta`'s data
|
|
- `npm run test -- src/views/__tests__/secondaryScreenCache.test.ts` exits 0 with all five behaviors covered
|
|
- A repeat mount inside the TTL records exactly one total fetcher call per resource
|
|
- A repeat mount after the TTL records exactly two, with the cached data present on the first rendered frame
|
|
- Every fetcher passes `dedup: true` to `rpcClient.call`
|
|
- Every resource carrying credentials, DIDs, wallet figures or transaction history is declared `persist: false`
|
|
- `npm run test` (full suite) exits 0 and `npm run type-check` exits 0
|
|
- The SUMMARY lists every key with its TTL, its persist choice, and the reason
|
|
</acceptance_criteria>
|
|
<done>Both app-detail screens paint from cache on a repeat open, never cross item data, revalidate exactly once when stale, and hold nothing sensitive in sessionStorage.</done>
|
|
</task>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Task 3: The remaining findings-named secondary screens</name>
|
|
<files>neode-ui/src/views/CloudFolder.vue, neode-ui/src/views/server/OpenWrtGateway.vue, neode-ui/src/views/__tests__/secondaryScreenCache.test.ts</files>
|
|
<read_first>
|
|
- `.planning/phases/02-ui-performance/02-FINDINGS.md` — the authoritative list. Only screens this doc classifies as `remount storm`, `serial RPC waterfall` or `uncached fetch` are in scope; anything it classifies `already fast` is left alone per D-02.
|
|
- `.planning/phases/02-ui-performance/02-PERF-BASELINE.json` — the `revisitRpcCalls` array per screen, which shows whether calls overlapped or ran one after another.
|
|
- `neode-ui/src/views/CloudFolder.vue` — it has no `onMounted`; data arrives through `cloudStore` and two `watch` blocks (line ~188 on `cloudStore.currentPath`, line ~313 on `[useNativeUI, section, routeFolderPath]`). Read both watches and how `cloudStore` loads before deciding whether the cache belongs in the view or behind the store's loader.
|
|
- `neode-ui/src/views/server/OpenWrtGateway.vue` — 909 lines; grep for `onMounted` (line ~374, `onMounted(() => load())`) and `load(` first, then read only the `load()` implementation and the note at line ~204 about reconnecting.
|
|
- `neode-ui/src/stores/cloud.ts` — if the cache belongs behind the store loader rather than in `CloudFolder.vue`, this is where it goes; read its loading actions first.
|
|
- `neode-ui/src/views/AppDetails.vue` as left by Task 2 — the in-plan precedent for key shape, TTL and persist choices.
|
|
</read_first>
|
|
<action>
|
|
Convert the remaining secondary screens that `02-FINDINGS.md` names, in the ranked
|
|
order the findings give, applying the same treatment established in Task 2: a keyed
|
|
cached resource per logical dataset with the item identifier in the key, an explicit
|
|
TTL, an explicit `persist` decision, `dedup: true` on the underlying call, keep-last-
|
|
value error handling with no toast, and `invalidate()` on any destructive action.
|
|
|
|
Candidate set from the route table, gated on what the findings actually name:
|
|
`cloud/:folderId` (`CloudFolder.vue`), `server/openwrt`
|
|
(`views/server/OpenWrtGateway.vue`), `cloud/peers/:peerId?` (`PeerFiles.vue`),
|
|
`apps/lnd/channels` (`views/apps/LightningChannels.vue`), `goals/:goalId`
|
|
(`GoalDetail.vue`) and `app-session/:appId` (`AppSession.vue`). `PeerFiles.vue`,
|
|
`Credentials.vue`, `Federation.vue` and `Monitoring.vue` already consume
|
|
`useCachedResource`; for those, verify the key embeds the item id and that the persist
|
|
choice is right, and change nothing else.
|
|
|
|
Two payload classes are memory-only regardless of what the findings say: file
|
|
listings and media metadata (large, per D-08) and any peer-sourced content — another
|
|
node's file listing or media index must not be written to this node's sessionStorage.
|
|
Declare `persist: false` for both and note it in the SUMMARY.
|
|
|
|
For `CloudFolder.vue`, decide where the cache belongs before writing code. Its data
|
|
flows through `cloudStore` and two watches, not through a mount hook. If several
|
|
views share the same store loader, put the cached resource behind the store action so
|
|
every consumer benefits, rather than wrapping the view's own reads and leaving the
|
|
store uncached. Record the decision and its reason in the SUMMARY.
|
|
|
|
Extend `neode-ui/src/views/__tests__/secondaryScreenCache.test.ts` with a repeat-open
|
|
call-count assertion for each screen converted here.
|
|
|
|
Scope guard: if `02-FINDINGS.md` names more than the four screens this plan's
|
|
`files_modified` covers, convert them in ranked order until the plan's context budget
|
|
is reached, then stop and report the remainder to the orchestrator as an unplanned-item
|
|
gap with the surface names and their measured causes. Do not silently skip a named
|
|
screen and do not quietly narrow the findings list.
|
|
|
|
`neode-ui/src/views/ContainerAppDetails.vue` is out of scope. `02-FINDINGS.md`
|
|
`## Corrections to Prior Research` records whether it has any importer or route entry;
|
|
if it has none, it is unreachable code and converting it would deliver nothing, even
|
|
though `02-RESEARCH.md` names it as a confirmed waterfall. Do not spend effort on it,
|
|
and do not delete it in this plan.
|
|
</action>
|
|
<verify>
|
|
<automated>cd neode-ui && npm run test -- src/views/__tests__/secondaryScreenCache.test.ts && npm run test && npm run type-check</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- Every secondary screen that `02-FINDINGS.md` classifies as slow either appears in this plan's converted set or is reported to the orchestrator as an unplanned-item gap — none is silently skipped
|
|
- Each converted screen imports `useCachedResource` and its keys embed the route item id
|
|
- Each converted screen has a repeat-open call-count assertion in `secondaryScreenCache.test.ts`
|
|
- File-listing and media-metadata resources are declared `persist: false`
|
|
- No file named `ContainerAppDetails.vue` appears in this plan's diff: `git diff --name-only HEAD -- neode-ui/src/views/ContainerAppDetails.vue | wc -l` prints 0
|
|
- `npm run test` exits 0 and `npm run type-check` exits 0
|
|
- `npm run build` exits 0 and the new cache keys appear in the built bundle: `grep -rl "app-details:" web/dist/neode-ui/assets | head -1` prints a file
|
|
</acceptance_criteria>
|
|
<done>Every secondary screen the profiling pass named as slow opens from cache on a repeat visit, with per-item keys, no sensitive or peer-sourced payload in sessionStorage, and a call-count test pinning each one.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<threat_model>
|
|
## Trust Boundaries
|
|
|
|
| Boundary | Description |
|
|
|----------|-------------|
|
|
| peer node → this node's browser storage | Peer-sourced file listings and media metadata cross from another operator's node into local storage |
|
|
| authenticated session → sessionStorage | Cached per-item payloads survive navigation and reload within the browser tab |
|
|
| item A's cache entry → item B's render | A key-construction mistake serves one item's data under another's screen |
|
|
|
|
## STRIDE Threat Register
|
|
|
|
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
|
|-----------|----------|-----------|----------|-------------|-----------------|
|
|
| T-02-02 | Information Disclosure | Cached entries surviving a logout or identity switch | high | mitigate | Task 1 adds `resources.clearAll()` and calls it from `auth.logout()` on both the success and failure paths, dropping memory entries and every `resource:` sessionStorage snapshot |
|
|
| T-02-01 | Information Disclosure | `useCachedResource` default `persist: true` on credential, DID and wallet payloads | high | mitigate | Tasks 2 and 3 require an explicit per-resource `persist` decision; credential material, DIDs, wallet figures and transaction history are `persist: false` (memory-only) |
|
|
| T-02-10 | Information Disclosure | Peer-sourced content written to local sessionStorage | high | mitigate | Task 3 declares file listings and media metadata `persist: false` unconditionally, independent of the findings classification |
|
|
| T-02-11 | Spoofing | Per-item cache key collision serving item A's data under item B | medium | mitigate | Every key embeds the route item id; Task 2's per-item isolation test asserts on rendered content, not only on fetcher call counts |
|
|
| T-02-12 | Tampering | A stale cached entry masking the result of a destructive action | medium | mitigate | Tasks 2 and 3 require `invalidate()` on the completion path of every uninstall, stop or removal action on a converted screen |
|
|
| T-02-SC | Tampering | npm/pip/cargo installs | high | mitigate | No package-manager installs are in scope; `Promise.allSettled` is a language built-in and `useCachedResource` already ships in this repo. 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
|
|
|
|
Symbols and paths created by this plan — new API, not drift from the existing codebase:
|
|
|
|
- `neode-ui/src/stores/resources.ts` — new export `clearAll()`
|
|
- `neode-ui/src/stores/__tests__/resourcesClear.test.ts`
|
|
- `neode-ui/src/views/__tests__/secondaryScreenCache.test.ts`
|
|
- Cache-key family introduced: `app-details:<dataset>:<appId>` and the same shape for the other converted secondary screens
|
|
|
|
Created elsewhere in Phase 02: `neode-ui/src/views/dashboard/keepAliveRoutes.ts`
|
|
(`shouldKeepAlive`, `KEEP_ALIVE_PATHS`, `KEEP_ALIVE_MAX`),
|
|
`neode-ui/src/views/dashboard/DashboardRouterView.vue`,
|
|
`neode-ui/src/components/RefreshIndicator.vue`,
|
|
`neode-ui/src/composables/__tests__/useCachedResource.test.ts`,
|
|
`neode-ui/src/views/dashboard/__tests__/keepAliveTabs.test.ts`,
|
|
`neode-ui/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-03 edge-probe row (spec-less fallback):** returned `unclassified` / `unresolved`. FLAGGED, not auto-backstopped and not dropped. Resolved in substance by this plan's `must_haves.truths`; the first-visit-may-still-load boundary is carried as a `verification: backstop` marker because CONTEXT.md D-11 states it as an allowance rather than as an assertable check. The probe row itself stays unresolved and is surfaced here for human review.
|
|
- **FA-B (correction to `02-RESEARCH.md`):** `neode-ui/src/views/ContainerAppDetails.vue` appears to be dead code — `grep -rn "ContainerAppDetails" neode-ui/src` returns only a self-referential comment inside the file, and it has no entry in `neode-ui/src/router/index.ts` (verified 2026-07-30). RESEARCH.md names it as the phase's confirmed serial-waterfall fix target. Plan 02-01 Task 3 re-runs the grep and records the verdict; this plan excludes the file either way and sources its waterfall targets from measured `revisitRpcCalls` instead.
|
|
- **Open:** `CloudFolder.vue` loads through `cloudStore` and two watches rather than a mount hook, so whether the cache belongs in the view or behind the store action is decided during Task 3 and recorded in the SUMMARY.
|
|
- **Open:** whether the router reuses a detail component instance across an id change on these routes is not settled from the route table alone. Task 2 requires it to be confirmed by reading the route declaration, with a `watch`-driven re-key as the fallback.
|
|
</assumptions_and_flagged_items>
|
|
|
|
<verification>
|
|
- `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 and the new cache keys appear in `web/dist/neode-ui/assets`
|
|
- Every secondary screen named slow by `02-FINDINGS.md` is either converted here or reported as an unplanned-item gap
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- Repeat opens of a secondary screen paint from cache with no blocking reload and no new RPC inside the TTL
|
|
- Per-item keys prevent any cross-item data bleed, proven by a rendered-content assertion
|
|
- Secondary screens still mount fresh — nothing here instance-caches them
|
|
- Logout leaves no cached payload in memory or in sessionStorage
|
|
- No sensitive or peer-sourced payload is written to sessionStorage
|
|
</success_criteria>
|
|
|
|
<output>
|
|
Create `.planning/phases/02-ui-performance/02-03-SUMMARY.md` when done. It MUST record:
|
|
every cache key introduced with its TTL and persist choice and the reason; which screens
|
|
the findings named and which of those were converted here versus reported as a gap; the
|
|
`CloudFolder.vue` cache-placement decision; and the verdict on whether
|
|
`ContainerAppDetails.vue` is reachable.
|
|
</output>
|