370 lines
28 KiB
Markdown
370 lines
28 KiB
Markdown
---
|
|
phase: 02-ui-performance
|
|
plan: 04
|
|
type: execute
|
|
wave: 3
|
|
depends_on: ["02-02"]
|
|
files_modified:
|
|
- neode-ui/src/views/dashboard/keepAliveRoutes.ts
|
|
- neode-ui/src/views/Home.vue
|
|
- neode-ui/src/views/web5/Web5.vue
|
|
- neode-ui/src/views/Chat.vue
|
|
- neode-ui/src/views/Cloud.vue
|
|
- neode-ui/src/views/Server.vue
|
|
- neode-ui/src/views/Mesh.vue
|
|
- neode-ui/src/views/Apps.vue
|
|
- neode-ui/src/views/Discover.vue
|
|
- neode-ui/src/views/Fleet.vue
|
|
- neode-ui/src/views/dashboard/__tests__/keepAliveLifecycle.test.ts
|
|
autonomous: false
|
|
requirements: [PERF-02]
|
|
|
|
must_haves:
|
|
truths:
|
|
- "Every main tab the profiling pass showed remounting on revisit is instance-cached, and switching to it and back shows its previous content immediately"
|
|
- "A polling interval, websocket subscription or window listener started by a main tab stops while that tab is off screen and restarts when the tab is re-entered"
|
|
- "Returning to a tab that polls live data re-reads that data immediately on re-entry rather than waiting out the poll interval"
|
|
- "A one-shot intro or animation flag still fires exactly once per session and does not replay on every tab revisit"
|
|
- "A connection-timeout timer that only makes sense on a fresh entry is re-armed on re-entry, not left armed from the first visit"
|
|
- "Visiting all main tabs in sequence leaves at most KEEP_ALIVE_MAX view instances resident — the least recently used tab is evicted"
|
|
- "Main tabs classified already fast by the profiling pass, with no measured remount cost, are left unregistered and the reason is recorded (D-02)"
|
|
- "No secondary screen is instance-cached as a side effect of widening the registration set (D-04)"
|
|
- statement: "Off-screen tabs consume no measurable CPU from their own timers while deactivated"
|
|
verification: backstop
|
|
prohibitions:
|
|
- "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 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"
|
|
artifacts:
|
|
- path: "neode-ui/src/views/dashboard/keepAliveRoutes.ts"
|
|
provides: "KEEP_ALIVE_PATHS widened from the tracer's single path to the full audited main-tab set"
|
|
exports: ["shouldKeepAlive", "KEEP_ALIVE_PATHS", "KEEP_ALIVE_MAX"]
|
|
- path: "neode-ui/src/views/dashboard/__tests__/keepAliveLifecycle.test.ts"
|
|
provides: "Assertions that deactivation stops timers and subscriptions and reactivation restarts them and refreshes live data"
|
|
key_links:
|
|
- from: "neode-ui/src/views/dashboard/keepAliveRoutes.ts"
|
|
to: "neode-ui/src/views/dashboard/useRouteTransitions.ts"
|
|
via: "KEEP_ALIVE_PATHS is built from the exported TAB_ORDER main-tab list, not from isDetailRoute"
|
|
pattern: "TAB_ORDER"
|
|
- from: "neode-ui/src/views/Home.vue"
|
|
to: "vue onActivated / onDeactivated"
|
|
via: "poll intervals and the websocket subscription are torn down on deactivate and re-armed with an immediate refresh on activate"
|
|
pattern: "onDeactivated"
|
|
- from: "neode-ui/src/views/Chat.vue"
|
|
to: "vue onActivated / onDeactivated"
|
|
via: "the window message listener and ContextBroker follow activation rather than mount/unmount"
|
|
pattern: "onDeactivated"
|
|
---
|
|
|
|
<objective>
|
|
Turn instance caching on for every main tab the profiling pass showed remounting, and make
|
|
every main tab correct under that lifecycle first.
|
|
|
|
Purpose: PERF-02. The tracer (02-02) proved the architecture on one tab and deliberately
|
|
left `KEEP_ALIVE_PATHS` seeded with only that tab. Widening it is not a one-line config
|
|
change: once a view's instance survives, `onMounted` fires exactly once for the session
|
|
and `onBeforeUnmount` never fires on tab-away. Every polling interval, websocket
|
|
subscription and window listener a main tab starts would otherwise run forever for every
|
|
tab ever visited — a CPU and memory drain on the low-power fleet hardware D-03 is
|
|
explicitly protecting — and every per-visit refresh would silently stop happening. That
|
|
is why registration and the lifecycle audit ship together, in one plan, rather than
|
|
registration landing early and correctness catching up later.
|
|
|
|
Output: the full audited main-tab registration set, and every main-tab view's side
|
|
effects deliberately placed for an activate/deactivate lifecycle.
|
|
</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/phases/02-ui-performance/02-02-SUMMARY.md
|
|
@.planning/codebase/CONVENTIONS.md
|
|
@CLAUDE.md
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Task 1: Timers, subscriptions and listeners follow activation, not mount</name>
|
|
<files>neode-ui/src/views/Home.vue, neode-ui/src/views/web5/Web5.vue, neode-ui/src/views/Chat.vue, neode-ui/src/views/Cloud.vue, neode-ui/src/views/Server.vue, neode-ui/src/views/Mesh.vue, neode-ui/src/views/dashboard/__tests__/keepAliveLifecycle.test.ts</files>
|
|
<read_first>
|
|
- `.planning/phases/02-ui-performance/02-02-SUMMARY.md` — the tracer tab's recorded side-effect placement decisions; this task repeats that audit across the remaining tabs and must stay consistent with the precedent set there.
|
|
- `neode-ui/src/views/Home.vue` lines 293 and 524-560 — `onMounted` starts `systemStatsInterval` (10s `loadSystemStats`), `walletRefreshInterval` (30s `loadWeb5Status`), a `wsClient.subscribe` returning `unsubscribeWs`, a `wsWalletDebounce` timeout, and calls `hydrateWalletSnapshot()`. Read the matching `onBeforeUnmount` teardown too.
|
|
- `neode-ui/src/views/web5/Web5.vue` lines 93-96, 140, 293, 337 and 371 — two `useCachedResource` resources already exist here plus an `onMounted` and an `onUnmounted`; read all of them.
|
|
- `neode-ui/src/views/Chat.vue` lines 61-125 — `onMounted` adds a `window` `message` listener and starts a `ContextBroker`; `onBeforeUnmount` removes and stops them.
|
|
- `neode-ui/src/views/Cloud.vue` — grep for `onMounted`, `onBeforeUnmount`, `onUnmounted`, `setInterval` and `subscribe` first, then read only those regions. The file is 1029 lines; do not read it whole.
|
|
- `neode-ui/src/views/Server.vue` — grep for the same five tokens; its `onMounted` at line ~831 fires seven independent loads. Read only that region and any teardown.
|
|
- `neode-ui/src/views/Mesh.vue` — grep for the same five tokens. The file is 2651 lines; read only the lifecycle regions. Its `onMounted` already does `await Promise.all([...])` across six fetch groups and must stay parallel.
|
|
- `.planning/phases/02-ui-performance/02-RESEARCH.md` pitfall 4 and pitfall 6 — the failure modes this task exists to prevent.
|
|
</read_first>
|
|
<behavior>
|
|
- Deactivating a view that owns a polling interval clears that interval; the poll callback is not invoked again while deactivated
|
|
- Reactivating that view restarts the interval and immediately invokes its loader once, so the first frame after re-entry is not interval-stale
|
|
- Deactivating a view that holds a websocket subscription unsubscribes it; reactivating re-subscribes exactly once, never twice
|
|
- Deactivating a view that added a `window` event listener removes it; reactivating adds it back exactly once
|
|
- Unmounting a view (rather than deactivating it) still tears everything down, so a non-cached mount path is unregressed
|
|
- Two consecutive activations without an intervening deactivation do not double-arm any timer, subscription or listener
|
|
</behavior>
|
|
<action>
|
|
For each view listed in `files`, classify every side effect its lifecycle hooks start
|
|
into exactly one of three buckets and place it accordingly:
|
|
|
|
- **Once per session** — stays in `onMounted`, unchanged. Example shape: a one-time
|
|
hydration from a stored snapshot.
|
|
- **Every entry** — moves to `onActivated`, and the `onMounted` call is removed so it
|
|
is not run twice on the first visit.
|
|
- **Only while visible** — started in `onActivated` and stopped in `onDeactivated`,
|
|
with the existing `onBeforeUnmount` / `onUnmounted` teardown left in place so the
|
|
non-cached path still cleans up.
|
|
|
|
Make every start idempotent: before arming a timer, clear any existing handle; before
|
|
subscribing, drop any existing unsubscribe function; before adding a listener, remove
|
|
it. Vue fires `onActivated` on first mount as well as on every reactivation, so a
|
|
non-idempotent start would double-arm on the first visit.
|
|
|
|
Concrete placements this task must make:
|
|
|
|
`Home.vue` — `hydrateWalletSnapshot()` is once-per-session and stays in `onMounted`.
|
|
`systemStatsInterval` and `walletRefreshInterval` are only-while-visible: clear both
|
|
in `onDeactivated`, re-arm both in `onActivated`. The `wsClient.subscribe` handle
|
|
(`unsubscribeWs`) and the `wsWalletDebounce` timeout are only-while-visible too. On
|
|
re-entry, `onActivated` must call `loadSystemStats()` and `loadWeb5Status()` once
|
|
immediately rather than waiting out the 10s and 30s intervals — a wallet balance is a
|
|
liveness-critical figure and must never render from a paused poll without an
|
|
immediate revalidation behind it.
|
|
|
|
`Chat.vue` — the `window` `message` listener and the `ContextBroker` are
|
|
only-while-visible. Move both to `onActivated` / `onDeactivated`, keeping the existing
|
|
`onBeforeUnmount` teardown. Note that `aiuiConnected` is set by a `ready` message from
|
|
the iframe: once the iframe survives deactivation, that message will not be re-sent on
|
|
re-entry, so `aiuiConnected` must not be reset on deactivate.
|
|
|
|
`Web5.vue`, `Cloud.vue`, `Server.vue`, `Mesh.vue` — apply the same three-bucket
|
|
classification to whatever their greps turn up. Do not restructure their fetch
|
|
orchestration in this task: `Mesh.vue`'s `Promise.all` fan-out and `Server.vue`'s
|
|
seven fire-and-forget loads are already concurrent, and converting them to cached
|
|
resources is plans 02-05 and 02-06. This task only relocates lifecycle side effects.
|
|
|
|
Write `neode-ui/src/views/dashboard/__tests__/keepAliveLifecycle.test.ts` covering the
|
|
six behaviors above against a small consumer component built with the same
|
|
activate/deactivate idiom, plus at least one assertion against a real converted view —
|
|
mount it inside a `<KeepAlive>`, deactivate, advance fake timers past its poll
|
|
interval, and assert its loader was not called while off screen and was called once on
|
|
reactivation.
|
|
|
|
Record in the SUMMARY, per view, every side effect and the bucket it was placed in.
|
|
Plan 02-08's on-device pass reads this table when checking for CPU drain.
|
|
</action>
|
|
<verify>
|
|
<automated>cd neode-ui && npm run test -- src/views/dashboard/__tests__/keepAliveLifecycle.test.ts && npm run test && npm run type-check</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- `neode-ui/src/views/Home.vue` and `neode-ui/src/views/Chat.vue` each reference `onDeactivated`: `grep -c "onDeactivated" neode-ui/src/views/Home.vue` and the same for `Chat.vue` are each at least 1
|
|
- `neode-ui/src/views/Home.vue` calls its stats and wallet loaders from `onActivated` so re-entry does not wait out the poll interval
|
|
- `npm run test -- src/views/dashboard/__tests__/keepAliveLifecycle.test.ts` exits 0 with all six behaviors covered
|
|
- A test asserts a paused interval's callback is not invoked while the view is deactivated
|
|
- A test asserts two consecutive activations do not double-arm a timer, subscription or listener
|
|
- `npm run test` (full suite) exits 0 and `npm run type-check` exits 0
|
|
- `Mesh.vue`'s `onMounted` still awaits a `Promise.all` and `Server.vue` still issues its loads without awaiting them sequentially — neither fetch fan-out was serialized by this task
|
|
- The SUMMARY contains a per-view table of every side effect and its assigned bucket
|
|
</acceptance_criteria>
|
|
<done>Every main-tab side effect is deliberately placed for an activate/deactivate lifecycle, off-screen tabs run no timers or subscriptions, and re-entering a live-data tab refreshes it immediately.</done>
|
|
</task>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Task 2: One-shot flags, entry timers, and widening the registration set</name>
|
|
<files>neode-ui/src/views/Apps.vue, neode-ui/src/views/Discover.vue, neode-ui/src/views/Fleet.vue, neode-ui/src/views/dashboard/keepAliveRoutes.ts, neode-ui/src/views/dashboard/__tests__/keepAliveLifecycle.test.ts</files>
|
|
<read_first>
|
|
- `neode-ui/src/views/Apps.vue` lines 383 and 544-560 — `onMounted` sets `appsAnimationDone = true` and, when the store is not connected, arms a 15s `connectionTimer` that raises `connectionError`; `onBeforeUnmount` clears the timer.
|
|
- `neode-ui/src/views/Discover.vue` lines 232 and 598-605 — `onMounted` sets `discoverAnimationDone` and calls `loadCommunityMarketplace()` (guarded on an empty list) and `loadBitcoinPruneStatus()`.
|
|
- `neode-ui/src/views/Fleet.vue` — 154 lines with no lifecycle hook found by grep; confirm before changing anything.
|
|
- `neode-ui/src/views/Marketplace.vue` as left by plan 02-02 — the tracer already moved its catalog and prune-status fetches onto the shared `app-catalog` and `bitcoin.prune-status` cache keys; `Discover.vue` calls the same loader and should pick up the same entries without a second conversion.
|
|
- `neode-ui/src/views/dashboard/keepAliveRoutes.ts` — as created by 02-02, seeded with the tracer path only.
|
|
- `neode-ui/src/views/dashboard/useRouteTransitions.ts` — `TAB_ORDER` (now exported by 02-02) is the canonical main-tab path list: `/dashboard`, `/dashboard/apps`, `/dashboard/marketplace`, `/dashboard/cloud`, `/dashboard/mesh`, `/dashboard/server`, `/dashboard/web5`, `/dashboard/fleet`, `/dashboard/chat`, `/dashboard/settings`.
|
|
- `neode-ui/src/router/index.ts` — confirm `/dashboard/discover` is a real route (`name: 'discover'`, `Discover.vue`) that `TAB_ORDER` does not list, and that `/dashboard/monitoring` is reached from Web5 rather than from the tab bar.
|
|
- `.planning/phases/02-ui-performance/02-FINDINGS.md` — the per-surface `Remounted` column and primary cause, which decide which tabs get registered.
|
|
</read_first>
|
|
<behavior>
|
|
- A one-shot intro flag set on first entry is still set exactly once across three visits to the same tab
|
|
- A connection-timeout timer that guards a fresh entry is re-armed on each re-entry and cleared on each exit, so it never fires against a stale visit
|
|
- `shouldKeepAlive` returns true for every registered main-tab path and false for every detail path, including detail paths whose prefix matches a registered path
|
|
- Visiting more distinct registered tabs than `KEEP_ALIVE_MAX` leaves exactly `KEEP_ALIVE_MAX` instances resident, and the least recently used one has been unmounted
|
|
</behavior>
|
|
<action>
|
|
First, finish the lifecycle audit for the remaining main tabs.
|
|
|
|
`Apps.vue`: `appsAnimationDone` is a one-shot intro flag and stays in `onMounted`.
|
|
The 15s `connectionTimer` is an entry-scoped guard — under a surviving instance it
|
|
would be armed once on the first visit and never again, so its diagnosis of "unable to
|
|
connect" would go stale. Move arming to `onActivated` (clearing any prior handle
|
|
first) and clearing to `onDeactivated`, keeping the existing `onBeforeUnmount` clear.
|
|
|
|
`Discover.vue`: `discoverAnimationDone` is a one-shot flag and stays put. Its
|
|
`loadCommunityMarketplace()` and `loadBitcoinPruneStatus()` calls now resolve against
|
|
the shared cache keys the tracer introduced; confirm by reading `Marketplace.vue` as
|
|
the tracer left it, and route `Discover.vue` through the same cached resources rather
|
|
than duplicating the fetch. Wire the shared resource's `loadState` to
|
|
`RefreshIndicator` in this view's header the same way the tracer did — the subtle
|
|
in-header signal D-05 specifies, never a stale-age badge.
|
|
|
|
`Fleet.vue`: confirm it has no lifecycle side effects before changing anything. If the
|
|
grep finds none, change nothing and record that.
|
|
|
|
Then widen the registration set. Build `KEEP_ALIVE_PATHS` in
|
|
`neode-ui/src/views/dashboard/keepAliveRoutes.ts` from the imported `TAB_ORDER` plus
|
|
`/dashboard/discover`, minus any path that `02-FINDINGS.md` classifies `already fast`
|
|
with a `Remounted` value of false. A tab with no measured remount cost gains nothing
|
|
from an instance cache and D-02 says to leave already-fast views alone; a tab with
|
|
`Remounted: true` has a real cost to remove and is registered. Keep the source list
|
|
derived from `TAB_ORDER` rather than restating ten literal paths, so a future tab
|
|
addition does not silently miss registration. Record in the SUMMARY exactly which
|
|
paths ended up in the set and which were excluded with their measured reason.
|
|
|
|
Do not widen the match from exact-path to prefix-path. Every secondary screen in the
|
|
route table sits under a main tab's path prefix — `/dashboard/apps/:id`,
|
|
`/dashboard/marketplace/:id`, `/dashboard/cloud/:folderId`, `/dashboard/server/openwrt`,
|
|
`/dashboard/web5/credentials`, `/dashboard/settings/update` — and a prefix match would
|
|
instance-cache all of them, which D-04 rules out.
|
|
|
|
`KEEP_ALIVE_MAX` stays at 6 against roughly eleven registered paths, so the long tail
|
|
evicts. Plan 02-08 tunes it against on-device memory; do not change it here.
|
|
|
|
Extend `keepAliveLifecycle.test.ts` with the four behaviors above. The eviction test
|
|
is the important one: navigate through `KEEP_ALIVE_MAX + 2` registered paths with
|
|
mount/unmount-counting stubs and assert the least recently used stub was unmounted.
|
|
</action>
|
|
<verify>
|
|
<automated>cd neode-ui && npm run test -- src/views/dashboard/__tests__/keepAliveLifecycle.test.ts src/views/dashboard/__tests__/keepAliveTabs.test.ts && npm run test && npm run type-check && npm run build</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- `neode-ui/src/views/dashboard/keepAliveRoutes.ts` imports `TAB_ORDER` from `useRouteTransitions` and builds `KEEP_ALIVE_PATHS` from it
|
|
- `shouldKeepAlive({ path: '/dashboard/apps' })` is true and `shouldKeepAlive({ path: '/dashboard/apps/bitcoin' })` is false — asserted in the test file
|
|
- The same false assertion holds for `/dashboard/marketplace/x`, `/dashboard/cloud/x`, `/dashboard/server/openwrt`, `/dashboard/web5/credentials` and `/dashboard/settings/update`
|
|
- The eviction test navigates through more than `KEEP_ALIVE_MAX` registered paths and asserts exactly `KEEP_ALIVE_MAX` instances remain resident
|
|
- `neode-ui/src/views/Apps.vue` arms its connection timer from `onActivated` and clears it from `onDeactivated`
|
|
- `neode-ui/src/views/Discover.vue` renders `RefreshIndicator` bound to the shared catalog resource's `loadState`
|
|
- `npm run test` exits 0, `npm run type-check` exits 0, `npm run build` exits 0
|
|
- The built bundle carries the widened set: `grep -rl "KEEP_ALIVE\|shouldKeepAlive" web/dist/neode-ui/assets | head -1` prints a file
|
|
- The SUMMARY lists every registered path and every excluded path with its measured reason
|
|
</acceptance_criteria>
|
|
<done>Every main tab that measurably remounts is registered, every one-shot and entry-scoped side effect is correctly placed, no secondary screen slipped into the instance cache, and eviction is proven by test.</done>
|
|
</task>
|
|
|
|
<task type="checkpoint:human-verify" gate="blocking">
|
|
<name>Task 3: Walk every main tab and confirm instant revisits with no off-screen drain</name>
|
|
<what-built>
|
|
Instance caching extended from the single tracer tab to every main tab that the
|
|
profiling pass showed remounting, with each tab's timers, websocket subscriptions and
|
|
window listeners moved onto the activate/deactivate lifecycle so an off-screen tab
|
|
costs nothing, and re-entering a live-data tab refreshes it immediately. The instance
|
|
cache is capped at 6 with least-recently-used eviction.
|
|
</what-built>
|
|
<how-to-verify>
|
|
1. From the repo root run `./scripts/dev-start.sh` and open the :8100 dev preview
|
|
pointed at archi-dev (password `password123`).
|
|
2. Visit every main tab once in order: Home, Apps, App store, Cloud, Mesh, Server,
|
|
Web5, Fleet, Chat, Settings. Let each finish loading.
|
|
3. Now switch between them at random. Expected on every revisit: content appears
|
|
immediately, scroll position and in-page state (search text, selected sub-tab,
|
|
expanded panels) are as you left them, and no intro animation replays.
|
|
4. Home specifically: note the wallet balance, leave Home for a minute, come back.
|
|
Expected: the previous figure is on screen instantly AND it updates within a second
|
|
or two as the immediate re-entry refresh lands — it must not sit frozen waiting for
|
|
the next 30s poll.
|
|
5. Chat specifically: open Chat, wait for the AIUI panel to load, switch away, switch
|
|
back. Expected: the panel is still loaded — it does not reload from scratch.
|
|
6. Apps specifically: with the backend running, open Apps, leave, and come back.
|
|
Expected: no spurious "Unable to connect to server" message appears.
|
|
7. Open a secondary screen from any tab (an app detail page, a cloud folder, the
|
|
OpenWrt page), navigate away and back. Expected: these still behave as before —
|
|
they are deliberately not instance-cached.
|
|
8. Cycle through all ten tabs twice, then return to the first one you visited.
|
|
Expected: it may show a brief load — it was evicted by the cap. This is correct.
|
|
9. Leave the browser sitting on one tab for a few minutes after having visited all of
|
|
them. The node should be idle; if the machine's fan spins up or the UI gets
|
|
sluggish, an off-screen timer is still running — report which tab you visited last.
|
|
</how-to-verify>
|
|
<resume-signal>Type "approved", or describe what you saw: which step, which tab, what happened instead.</resume-signal>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<threat_model>
|
|
## Trust Boundaries
|
|
|
|
| Boundary | Description |
|
|
|----------|-------------|
|
|
| off-screen view instance → node resources | A deactivated but resident view can hold timers, sockets and heavy graphics contexts against a low-power fleet node |
|
|
| cached render → user's belief about liveness | A surviving instance shows figures that were true when the tab was last visible, not necessarily now |
|
|
| main-tab path prefix → secondary screen | A loose path match would sweep secondary screens into the instance cache |
|
|
|
|
## STRIDE Threat Register
|
|
|
|
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
|
|-----------|----------|-----------|----------|-------------|-----------------|
|
|
| T-02-03 | Denial of Service | Resident view instances and their timers on low-power fleet hardware | medium | mitigate | `KEEP_ALIVE_MAX` of 6 with LRU eviction (D-03); Task 1 stops every interval, subscription and listener on `onDeactivated`; Task 3 step 9 and plan 02-08 check idle CPU and memory on archi-dev-box |
|
|
| T-02-13 | Spoofing | A stale wallet balance or peer-reachability figure rendered as if current | high | mitigate | Task 1 requires `onActivated` to fire an immediate loader call for every live-data surface, so a resumed tab revalidates on the frame it returns rather than waiting out a paused poll; the `RefreshIndicator` from 02-02 makes the in-flight refresh visible |
|
|
| T-02-14 | Information Disclosure | A secondary screen accidentally instance-cached by a widened path match | medium | mitigate | Task 2 keeps exact-path matching and asserts `shouldKeepAlive` is false for six representative secondary-screen paths whose prefixes match a registered tab |
|
|
| T-02-15 | Denial of Service | A double-armed timer or duplicate subscription after repeated activations | low | mitigate | Task 1 requires every start to be idempotent and asserts that two consecutive activations do not double-arm |
|
|
| T-02-SC | Tampering | npm/pip/cargo installs | high | mitigate | No package-manager installs are in scope; `onActivated` and `onDeactivated` are Vue core. 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 or changed by this plan — new API, not drift:
|
|
|
|
- `neode-ui/src/views/dashboard/keepAliveRoutes.ts` — `KEEP_ALIVE_PATHS` widened from the tracer seed to the audited main-tab set, now derived from `TAB_ORDER`
|
|
- `neode-ui/src/views/dashboard/__tests__/keepAliveLifecycle.test.ts`
|
|
- `onActivated` / `onDeactivated` handlers added to `Home.vue`, `Chat.vue`, `Apps.vue`, and to `Web5.vue`, `Cloud.vue`, `Server.vue`, `Mesh.vue` where their greps turn up only-while-visible side effects
|
|
|
|
Created elsewhere in Phase 02: `shouldKeepAlive()`, `KEEP_ALIVE_MAX`,
|
|
`DashboardRouterView.vue`, `RefreshIndicator.vue`, `resources.clearAll()`,
|
|
`useCachedResource.test.ts`, `keepAliveTabs.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}`,
|
|
cache keys `app-catalog`, `bitcoin.prune-status`, `app-details:<dataset>:<id>`.
|
|
</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 off-screen-CPU truth carried as a `verification: backstop` marker because a unit test can prove a timer handle was cleared but not that the process draws no CPU — that half is checked on hardware in plan 02-08.
|
|
- **Open (RESEARCH pitfall 4 breadth):** RESEARCH.md names `Apps.vue`'s connection timer and `Server.vue`'s seven-call initializer as the known instances. This planner additionally found `Home.vue`'s two `setInterval` handles plus a `wsClient.subscribe`, and `Chat.vue`'s `window` message listener plus `ContextBroker`. `Cloud.vue`, `Web5.vue` and `Mesh.vue` were not exhaustively read — Task 1 greps each for `onMounted`, `onBeforeUnmount`, `onUnmounted`, `setInterval` and `subscribe` and handles whatever it finds. If a view turns out to hold a side effect none of those five tokens catch, record it in the SUMMARY rather than letting it pass.
|
|
- **Open (D-01 versus D-02 boundary):** D-01 says main tabs use both `<KeepAlive>` and `useCachedResource`; D-02 says already-fast views are left alone. This plan resolves the tension by measurement: a main tab is registered when `02-FINDINGS.md` records `Remounted: true` for it, and excluded when it is classified already fast with no remount cost. Every exclusion is recorded with its measured reason.
|
|
- **FA-D (RESEARCH assumption A2):** `KEEP_ALIVE_MAX` stays at 6 here and is tuned against real on-device memory in plan 02-08, not guessed at again in this plan.
|
|
</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 widened registration appears in `web/dist/neode-ui/assets`
|
|
- The human-verify checkpoint is approved against archi-dev on the :8100 preview, including the Home wallet-freshness step and the eviction step
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- Every main tab that measurably remounted now renders instantly from a surviving instance, with scroll and in-page state intact
|
|
- No off-screen tab runs a timer, a subscription or a listener
|
|
- Re-entering a live-data tab revalidates immediately rather than waiting out its poll
|
|
- One-shot flags fire once; entry-scoped guards re-arm per entry
|
|
- The instance cache is capped and evicts, proven by test and observed on device
|
|
- No secondary screen was instance-cached, and every excluded main tab has a recorded measured reason
|
|
</success_criteria>
|
|
|
|
<output>
|
|
Create `.planning/phases/02-ui-performance/02-04-SUMMARY.md` when done. It MUST record:
|
|
the per-view table of every side effect and the bucket it was placed in; the final
|
|
`KEEP_ALIVE_PATHS` contents; every main-tab path excluded from registration with its
|
|
measured reason; and any side effect found that the five-token grep would have missed.
|
|
Plans 02-05, 02-06, 02-07 and 02-08 read all four from this file.
|
|
</output>
|