docs(02-04): complete main-tab lifecycle audit plan
This commit is contained in:
parent
03a3e4e0c1
commit
e582124259
@ -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**: 3/8 plans executed
|
||||
**Plans**: 4/8 plans executed
|
||||
|
||||
Plans:
|
||||
**Wave 1**
|
||||
@ -87,7 +87,7 @@ Plans:
|
||||
|
||||
**Wave 3** *(blocked on Wave 2 completion)*
|
||||
|
||||
- [ ] 02-04-PLAN.md — Keep every main tab alive safely: lifecycle audit + full registration (PERF-02)
|
||||
- [x] 02-04-PLAN.md — Keep every main tab alive safely: lifecycle audit + full registration (PERF-02)
|
||||
|
||||
**Wave 4** *(blocked on Wave 3 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 | 3/8 | In Progress| |
|
||||
| 2. UI Performance | 4/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-02-PLAN.md
|
||||
last_updated: "2026-07-30T13:20:35.538Z"
|
||||
stopped_at: Completed 02-04-PLAN.md
|
||||
last_updated: "2026-07-30T19:29:23.802Z"
|
||||
last_activity: 2026-07-30
|
||||
last_activity_desc: Phase 02 execution started
|
||||
progress:
|
||||
total_phases: 8
|
||||
completed_phases: 0
|
||||
total_plans: 18
|
||||
completed_plans: 3
|
||||
completed_plans: 4
|
||||
percent: 0
|
||||
---
|
||||
|
||||
@ -29,11 +29,11 @@ See: .planning/PROJECT.md (updated 2026-07-29)
|
||||
## Current Position
|
||||
|
||||
Phase: 02 (ui-performance) — EXECUTING
|
||||
Plan: 3 of 8
|
||||
Plan: 4 of 8
|
||||
Status: Ready to execute
|
||||
Last activity: 2026-07-30 — Phase 02 execution started
|
||||
|
||||
Progress: [██░░░░░░░░] 17%
|
||||
Progress: [██░░░░░░░░] 22%
|
||||
|
||||
## Performance Metrics
|
||||
|
||||
@ -55,6 +55,7 @@ Progress: [██░░░░░░░░] 17%
|
||||
| Phase 02 P01 | 100min | 3 tasks | 5 files |
|
||||
| Phase 02 P03 | 45min | 3 tasks | 5 files |
|
||||
| Phase 02 P02 | 105min | 3 tasks | 11 files |
|
||||
| Phase 02 P04 | 150min | 3 tasks | 12 files |
|
||||
|
||||
## Accumulated Context
|
||||
|
||||
@ -84,6 +85,10 @@ Decisions are logged in PROJECT.md (10 locked ADRs in the `<decisions>` block +
|
||||
- [Phase ?]: 02-02: HARD RULE for rest of Phase 02 — perf work must be visually invisible; verify against the real dev preview before considering a checkpoint satisfied
|
||||
- [Phase ?]: 02-02: app-catalog persist:true ttl 300000ms; bitcoin.prune-status persist:true ttl 30000ms — both explicit per T-02-01, no default relied on
|
||||
- [Phase ?]: 02-02: PERF-02 reverted to Pending/In-Progress in REQUIREMENTS.md after an automated mark-complete run — PERF-02 also spans 02-04..02-07 (extending KeepAlive caching to every remaining main tab); this plan proves the architecture on the tracer tab only
|
||||
- [Phase ?]: 02-04: KEEP_ALIVE_PATHS widened to every audited main tab (10 paths) derived from TAB_ORDER + /dashboard/discover; /dashboard/settings deliberately withheld — its child sections (SystemDangerZone reboot poll, several onMounted-only fetches) were never audited by this plan
|
||||
- [Phase ?]: 02-04: onActivated is a documented no-op outside a KeepAlive boundary — every arm function now runs from both onMounted and onActivated (fresh-mount guards on Home/Web5/Mesh/Server avoid doubling first-load RPC cost); caught by CloudPeersRefresh.test.ts
|
||||
- [Phase ?]: 02-04: useCachedResource.ts's onActivated no longer eagerly force-loads a never-fetched immediate:false resource, so tab-gated lazy data (Cloud.vue Paid Files/My Files) isn't force-loaded merely by its view entering the KeepAlive cache
|
||||
- [Phase ?]: 02-04: AIUI blank-screen-and-loading symptom reported at Task 3 checkpoint diagnosed as pre-existing (local mock-backend dev mode sets VITE_AIUI_URL=http://localhost:5173 unconditionally with no AIUI repo checked out) — not a regression, left for 02-07 (Chat/AIUI) to address
|
||||
|
||||
### Pending Todos
|
||||
|
||||
@ -118,6 +123,6 @@ None yet.
|
||||
|
||||
## Session Continuity
|
||||
|
||||
Last session: 2026-07-30T13:19:36.903Z
|
||||
Stopped at: Completed 02-02-PLAN.md
|
||||
Last session: 2026-07-30T19:29:12.322Z
|
||||
Stopped at: Completed 02-04-PLAN.md
|
||||
Resume file: None
|
||||
|
||||
266
.planning/phases/02-ui-performance/02-04-SUMMARY.md
Normal file
266
.planning/phases/02-ui-performance/02-04-SUMMARY.md
Normal file
@ -0,0 +1,266 @@
|
||||
---
|
||||
phase: 02-ui-performance
|
||||
plan: 04
|
||||
subsystem: ui
|
||||
tags: [vue, keepalive, vue-router, activate-deactivate, useCachedResource, aiui]
|
||||
|
||||
# Dependency graph
|
||||
requires:
|
||||
- phase: 02-ui-performance/02-02
|
||||
provides: "Route-path KeepAlive classifier (shouldKeepAlive/KEEP_ALIVE_PATHS/KEEP_ALIVE_MAX), DashboardRouterView.vue KeepAlive host with statically-named per-route wrapper components, the onActivated reactivation fix in useCachedResource.ts, RefreshIndicator.vue, and the onMounted/onActivated/onDeactivated side-effect audit convention"
|
||||
provides:
|
||||
- "Every main tab's timers, subscriptions and window listeners audited and placed for an activate/deactivate lifecycle (Home, Chat, Web5, Cloud, Server, Mesh, Apps, Discover)"
|
||||
- "KEEP_ALIVE_PATHS widened from the tracer's single seed path to the full TAB_ORDER-derived set (10 paths: every main tab except /dashboard/settings) plus /dashboard/discover"
|
||||
- "useCachedResource.ts fix: onActivated no longer eagerly force-loads an immediate:false (tab-gated lazy) resource that has never been explicitly fetched"
|
||||
- "Discover.vue routed onto the same shared 'app-catalog'/'bitcoin.prune-status' cache keys Marketplace.vue introduced in 02-02"
|
||||
affects: [02-05, 02-06, 02-07, 02-08]
|
||||
|
||||
# Tech tracking
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns:
|
||||
- "Three-bucket side-effect classification (once-per-session/every-entry/only-while-visible) applied consistently across every main-tab view, with idempotent arm/disarm functions"
|
||||
- "Dual-registration pattern: every arm function is called from BOTH onMounted and onActivated, because onActivated is a documented no-op outside a <KeepAlive> boundary — a bare (non-KeepAlive) mount must not silently skip a view's timers/subscriptions/listeners. Fresh-mount guard flags (Home/Web5/Mesh/Server) prevent the harmless-but-avoidable double-fire this causes on a KeepAlive-wrapped view's very first activation for the heavier loaders"
|
||||
- "useCachedResource's onActivated staleness check now distinguishes 'never explicitly requested' (immediate:false, fetchedAt still null) from 'stale, already loaded once' — only the latter auto-revalidates on reactivation, so a tab-gated lazy resource isn't force-loaded merely by its owning view entering the KeepAlive cache"
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- neode-ui/src/views/dashboard/__tests__/keepAliveLifecycle.test.ts
|
||||
modified:
|
||||
- neode-ui/src/views/Home.vue
|
||||
- neode-ui/src/views/Chat.vue
|
||||
- neode-ui/src/views/web5/Web5.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/dashboard/keepAliveRoutes.ts
|
||||
- neode-ui/src/composables/useCachedResource.ts
|
||||
- neode-ui/src/composables/__tests__/useCachedResource.test.ts
|
||||
|
||||
key-decisions:
|
||||
- "/dashboard/settings withheld from KEEP_ALIVE_PATHS despite being in TAB_ORDER: Settings.vue's child sections were never in this plan's file scope, and a grep found real un-audited side effects (SystemDangerZone.vue's reboot poll/elapsed intervals; one-shot onMounted-only fetches in VpnStatusSection.vue/KioskDisplaySection.vue/TransportPrefsCard.vue/ClaudeAuthSection.vue) that would misbehave under KeepAlive. Registering it without auditing those would ship the exact bug this plan exists to prevent."
|
||||
- "Every other TAB_ORDER path (Mesh, Chat included) stays registered per the plan's literal exclusion rule: only a measured 'already fast'+Remounted:false excludes a path, and neither Mesh nor Chat has that (both are 02-FINDINGS.md 'unmeasured', not 'already fast')."
|
||||
- "Discover.vue's catalog fetcher keeps its own dynamic-catalog-first behavior (fetchAppCatalog() with a curated-list fallback) rather than being flattened to Marketplace.vue's simpler getCuratedAppList()-only fetcher — both are valid producers of the shared 'app-catalog' cache key; dropping Discover's fetcher would have silently lost the dynamic-catalog/featured-banner capability."
|
||||
- "useCachedResource.ts's onActivated guard change (skip auto-revalidation for a never-fetched immediate:false resource) applies to every current and future consumer, not just Cloud.vue/Server.vue — verified safe because Marketplace.vue's own resources (the only ones already inside a KeepAlive boundary before this plan) are both immediate:true (default), so this plan's approved 02-02 checkpoint behavior is unaffected."
|
||||
- "Every dual-registered (onMounted + onActivated) arm function is idempotent by construction (clear/remove-then-set), confirmed safe by re-running the full existing test suite after the fix — CloudPeersRefresh.test.ts (which mounts Cloud.vue bare, outside any KeepAlive) caught the initial regression where onActivated-only logic silently never ran outside a KeepAlive boundary."
|
||||
|
||||
requirements-completed: []
|
||||
requirements-note: "PERF-02 is NOT marked complete in REQUIREMENTS.md despite being this plan's sole requirements entry — PERF-02 also spans 02-05, 02-06 and 02-07, which still extend the KeepAlive/cache architecture to Mesh's fetch groups, Server/Home's data layer, and Chat/AIUI. This plan delivers the full lifecycle-audit + registration-widening layer only, per the same precedent 02-02/02-03 set for PERF-02/PERF-03."
|
||||
|
||||
coverage:
|
||||
- id: D1
|
||||
description: "Every audited main tab (Home, Chat, Web5, Cloud, Server, Mesh, Apps, Discover) has its timers/subscriptions/listeners placed into once-per-session, every-entry, or only-while-visible buckets, each idempotent and safe under both a bare mount and a KeepAlive-wrapped mount"
|
||||
requirement: PERF-02
|
||||
verification:
|
||||
- kind: unit
|
||||
ref: "neode-ui/src/views/dashboard/__tests__/keepAliveLifecycle.test.ts"
|
||||
status: pass
|
||||
- kind: unit
|
||||
ref: "neode-ui/src/composables/__tests__/useCachedResource.test.ts (lazy-resource-under-activation case)"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D2
|
||||
description: "KEEP_ALIVE_PATHS widened to the full TAB_ORDER-derived set (minus /dashboard/settings) plus /dashboard/discover; shouldKeepAlive stays exact-match (no secondary screen slips into the cache); eviction proven at KEEP_ALIVE_MAX+2 distinct registered paths"
|
||||
requirement: PERF-02
|
||||
verification:
|
||||
- kind: unit
|
||||
ref: "neode-ui/src/views/dashboard/__tests__/keepAliveLifecycle.test.ts (keepAliveRoutes describe block)"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D3
|
||||
description: "Every main tab feels instant on revisit with no off-screen drain, wallet freshness on Home re-entry, Chat's AIUI panel staying loaded across a tab switch, Apps not showing a spurious connection error, secondary screens still remounting as before, and eviction observed at the 6-tab cap — verified on the real :8101 dev preview against archi-dev-box"
|
||||
requirement: PERF-02
|
||||
verification:
|
||||
- kind: manual_procedural
|
||||
ref: "Task 3 checkpoint:human-verify — approved on all steps except a pre-existing AIUI dev-mode gap (see Known Issues)"
|
||||
status: pass
|
||||
human_judgment: true
|
||||
rationale: "Visual/perceptual verification (instant paint, no stale margins/animations, wallet freshness timing) is inherently a human judgment call, consistent with 02-02's precedent for this same class of checkpoint."
|
||||
|
||||
duration: ~150min
|
||||
completed: 2026-07-30
|
||||
status: complete
|
||||
---
|
||||
|
||||
# Phase 02 Plan 04: Main-Tab Lifecycle Audit + Full KeepAlive Registration Summary
|
||||
|
||||
**Every main tab's onMounted-only side effects reclassified into once-per-session / every-entry / only-while-visible buckets and made idempotent under both a bare mount and a KeepAlive-wrapped mount, then KEEP_ALIVE_PATHS widened from the 02-02 tracer's single path to the full audited set (10 of 11 TAB_ORDER+discover paths — Settings withheld pending its own audit)**
|
||||
|
||||
## Performance
|
||||
|
||||
- **Duration:** ~150 min (including one checkpoint round-trip for a pre-existing AIUI dev-mode gap, diagnosed and confirmed out of scope)
|
||||
- **Started:** 2026-07-30 (session start)
|
||||
- **Completed:** 2026-07-30T19:26:00Z
|
||||
- **Tasks:** 3 (Task 1 auto/tdd, Task 2 auto/tdd, Task 3 checkpoint:human-verify)
|
||||
- **Files modified:** 11 (1 new test file, 10 modified)
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- **Home.vue** — `systemStatsInterval` (10s), `walletRefreshInterval` (30s), the `wsClient` wallet-push subscription and its debounce timer now follow activate/deactivate with an immediate re-sync on entry (a resumed Home never shows a frozen wallet balance); `hydrateWalletSnapshot`/`checkUpdateStatus`/cloud-usage read stay once-per-session.
|
||||
- **Chat.vue** — the `window` `message` listener and `ContextBroker` follow activate/deactivate; `aiuiConnected` is deliberately never reset on deactivate since the iframe's one-time `ready` postMessage won't resend on re-entry.
|
||||
- **Web5.vue** — the six child-component data loaders (confirmed none use `useCachedResource` internally) and the 30s LND force-refresh interval move to activate/deactivate; the DID lookup and intro-stagger flag stay once-per-session.
|
||||
- **Cloud.vue** — the per-peer transport/reachability warm-cache (`loadPeerFiles`, plus `loadCounts`/`loadPeers`) re-runs every entry, since it's the one path here that bypasses `useCachedResource` and would otherwise render stale peer-reachability data once cached (T-02-13).
|
||||
- **Server.vue** — the previously module-scope-armed 15s VPN poll interval (which used to run forever regardless of tab visibility once anything wrapped this view in KeepAlive) now follows activate/deactivate with an immediate tick on entry; `loadDiskStatus` becomes every-entry.
|
||||
- **Mesh.vue** — the entire live-communications surface (four window/document listeners, the 5s status/peers/messages poll, the 15s Archipelago-channel poll, the ws peer-push subscription, and the six-way federation/self/contacts refresh) follows activate/deactivate. A share-to-mesh handoff delivered via direct navigation (not the same-page custom event) is now correctly picked up on every activation, not just the first mount — a real gap that would have appeared the moment Mesh joined the instance cache.
|
||||
- **Apps.vue** — the 15s "unable to connect" timer is now an entry-scoped guard (re-armed on activation, cleared on exit) and resets `connectionError` on entry so a since-reconnected node doesn't show a stale error instantly.
|
||||
- **Discover.vue** — `loadCommunityMarketplace`/`loadBitcoinPruneStatus` now resolve against the same shared `app-catalog`/`bitcoin.prune-status` cache keys Marketplace.vue introduced in 02-02, with `RefreshIndicator` wired to the shared resource's `loadState`.
|
||||
- **Fleet.vue** — confirmed no lifecycle side effects (grep for the five tokens found none); left unchanged, registered as-is.
|
||||
- **keepAliveRoutes.ts** — `KEEP_ALIVE_PATHS` now derives from `TAB_ORDER` (single source of truth) plus `/dashboard/discover`, withholding `/dashboard/settings` for an unaudited-risk reason recorded in-file.
|
||||
- **useCachedResource.ts** — a real bug found during the audit: `onActivated`'s staleness check treated a never-fetched `immediate:false` resource as stale, which would have eagerly force-loaded Cloud.vue's tab-gated Paid Files / My Files walk the moment Cloud.vue joined the instance cache. Fixed to only auto-revalidate a resource that has been explicitly fetched at least once.
|
||||
- **Bare-mount regression caught and fixed** — my first pass moved several views' `onMounted`-only logic entirely into `onActivated`. `CloudPeersRefresh.test.ts` (which mounts `Cloud.vue` directly, no KeepAlive) caught that `onActivated` is a documented no-op outside a KeepAlive boundary. Fixed by calling every arm function from both `onMounted` and `onActivated`, with fresh-mount guard flags on the heavier views (Home/Web5/Mesh/Server) to avoid doubling their first-load network cost.
|
||||
|
||||
## Task Commits
|
||||
|
||||
Each task was committed atomically:
|
||||
|
||||
1. **Task 1: Timers, subscriptions and listeners follow activation, not mount** — `f177a505` (feat, tdd)
|
||||
2. **Task 2: One-shot flags, entry timers, and widening the registration set** — `03a3e4e0` (feat, tdd)
|
||||
3. **Task 3: Walk every main tab and confirm instant revisits with no off-screen drain** — checkpoint:human-verify, approved on all steps except one pre-existing dev-mode artifact (see Known Issues below); no code change required for it, per the diagnosis.
|
||||
|
||||
**Plan metadata:** (this commit)
|
||||
|
||||
_Note: both tasks are TDD tasks; tests were written and made to pass within each task's own commit, per this repo's established single-commit-per-task convention (see 02-01/02-02/02-03 history)._
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
- `neode-ui/src/views/Home.vue` — wallet/stats polling, ws subscription follow activate/deactivate; once-per-session hydrate/update-check/cloud-usage
|
||||
- `neode-ui/src/views/Chat.vue` — window listener + ContextBroker follow activate/deactivate
|
||||
- `neode-ui/src/views/web5/Web5.vue` — six child-loaders + wallet poll follow activate/deactivate; DID lookup stays once-per-session
|
||||
- `neode-ui/src/views/Cloud.vue` — counts/peers/peer-files warm-cache re-runs every entry
|
||||
- `neode-ui/src/views/Server.vue` — VPN poll interval follows activate/deactivate; loadDiskStatus every-entry
|
||||
- `neode-ui/src/views/Mesh.vue` — full live-communications lifecycle (listeners, two poll intervals, ws subscription, six-way refresh, deep-link handling) follows activate/deactivate
|
||||
- `neode-ui/src/views/Apps.vue` — connection-timeout timer is now entry-scoped
|
||||
- `neode-ui/src/views/Discover.vue` — catalog/prune-status routed onto Marketplace.vue's shared cache keys; RefreshIndicator added
|
||||
- `neode-ui/src/views/dashboard/keepAliveRoutes.ts` — `KEEP_ALIVE_PATHS` widened, derived from `TAB_ORDER`
|
||||
- `neode-ui/src/composables/useCachedResource.ts` — `onActivated` no longer eagerly force-loads a never-fetched `immediate:false` resource
|
||||
- `neode-ui/src/composables/__tests__/useCachedResource.test.ts` — new test for the above fix
|
||||
- `neode-ui/src/views/dashboard/__tests__/keepAliveLifecycle.test.ts` — new; covers all ten Task 1/Task 2 behaviors
|
||||
|
||||
## Per-View Side-Effect Table
|
||||
|
||||
Bucket key: **S** = once-per-session (onMounted only) · **E** = every-entry (onActivated, immediate on entry) · **V** = only-while-visible (armed onActivated, torn down onDeactivated)
|
||||
|
||||
| View | Side effect | Bucket |
|
||||
|---|---|---|
|
||||
| Home.vue | `hydrateWalletSnapshot()` | S |
|
||||
| Home.vue | `checkUpdateStatus()` | S |
|
||||
| Home.vue | Cloud usage read (`fileBrowserClient.getUsage()`) | S |
|
||||
| Home.vue | `systemStatsInterval` (10s `loadSystemStats`) | V |
|
||||
| Home.vue | `walletRefreshInterval` (30s `loadWeb5Status`) | V |
|
||||
| Home.vue | `wsClient.subscribe` (wallet push) + debounce timer | V |
|
||||
| Chat.vue | `window` `message` listener (`onAiuiMessage`) | V |
|
||||
| Chat.vue | `ContextBroker` | V |
|
||||
| Chat.vue | `aiuiConnected` flag | Not reset on deactivate (special case — see plan text) |
|
||||
| Web5.vue | `web5AnimationDone` intro flag | S |
|
||||
| Web5.vue | `rpcClient.getNodeDid()` | S |
|
||||
| Web5.vue | `loadPeers`/`loadReceivedMessages`/`loadConnectionRequests`/`loadIdentities`/`loadVisibility`/`loadNostrRelays`/`detectHardwareWallets` | E |
|
||||
| Web5.vue | `walletRefreshInterval` (30s `lndInfoRes.refresh()`) | V |
|
||||
| Cloud.vue | `loadCounts`/`loadPeers`/`loadPeerFiles` (peer transport/reachability warm-cache) | E (all internally staleness-gated or inflight-deduped, so no extra RPC when fresh) |
|
||||
| Server.vue | `checkTorStatus`/`loadNetworkData`/`loadInterfaces`/`loadTorServices`/`loadVpnPeers`/`loadFipsSummary` | S (each resource self-heals via `useCachedResource`'s own `onActivated`) |
|
||||
| Server.vue | `loadDiskStatus()` | E |
|
||||
| Server.vue | `vpnPollInterval` (15s) | V |
|
||||
| Mesh.vue | `window` resize, `document` pointerdown (menu + attach-menu), `archipelago:share-to-mesh` event, `visualViewport` resize/scroll | V |
|
||||
| Mesh.vue | `loadPendingFromSession()` (share-to-mesh handoff) | E |
|
||||
| Mesh.vue | `mesh.refreshAll()`/`transport.fetchStatus()`/`refreshFederationNodes()`/`refreshSelfOnion()`/`refreshSelfDid()`/`refreshContacts()` + deep-link peer/channel open | E |
|
||||
| Mesh.vue | `archPollInterval` (15s `loadArchMessages`) | V |
|
||||
| Mesh.vue | `pollInterval` (5s status/peers/messages/deadman/blockheaders + every-6th-tick contacts/federation/outbox) | V |
|
||||
| Mesh.vue | `wsClient.subscribe` (peer push) | V |
|
||||
| Apps.vue | `appsAnimationDone` intro flag | S |
|
||||
| Apps.vue | `connectionTimer` (15s "unable to connect", `connectionError` reset on entry) | V (entry-scoped guard) |
|
||||
| Discover.vue | `discoverAnimationDone` intro flag | S |
|
||||
| Discover.vue | `catalogResource`/`pruneStatusResource` | S seed only — self-heals via `useCachedResource`'s own `onActivated` |
|
||||
| Fleet.vue | (none found) | n/a — unchanged |
|
||||
|
||||
## KEEP_ALIVE_PATHS — Final Contents
|
||||
|
||||
**Registered (10 paths):** `/dashboard`, `/dashboard/apps`, `/dashboard/marketplace`, `/dashboard/cloud`, `/dashboard/mesh`, `/dashboard/server`, `/dashboard/web5`, `/dashboard/fleet`, `/dashboard/chat`, `/dashboard/discover`
|
||||
|
||||
**Excluded (1 path):** `/dashboard/settings` — in `TAB_ORDER` but **not** registered. Reason: unaudited risk, not a measured "already fast" result (02-FINDINGS.md has no row for Settings at all). `Settings.vue`'s child sections were never in this plan's file scope, and a grep across `neode-ui/src/views/settings/*.vue` found real un-audited side effects — `SystemDangerZone.vue`'s reboot poll/elapsed intervals, and one-shot `onMounted`-only fetches in `VpnStatusSection.vue`, `KioskDisplaySection.vue`, `TransportPrefsCard.vue` and `ClaudeAuthSection.vue` — that would misbehave under KeepAlive exactly as this plan exists to prevent. Flagged for a future plan to audit before registering.
|
||||
|
||||
No main tab was excluded for a measured "already fast, Remounted:false" reason — every 02-FINDINGS.md main-tab row was either `Remounted: true` or `unmeasured` (Mesh, Chat), and per the plan's own literal exclusion rule (only a measured `Remounted: false` excludes), both Mesh and Chat stay registered.
|
||||
|
||||
## Decisions Made
|
||||
|
||||
See `key-decisions` in frontmatter for the full list. Highlights:
|
||||
- `/dashboard/settings` deliberately withheld (unaudited-risk, not "already fast") — see table above.
|
||||
- `useCachedResource.ts`'s `onActivated` guard change is a shared-composable fix (affects every consumer), verified safe against the one pre-existing KeepAlive consumer (Marketplace.vue, both resources `immediate: true`).
|
||||
- Discover.vue keeps its own catalog fetcher (dynamic-first, curated fallback) rather than being flattened to Marketplace.vue's simpler fetcher, since both are valid producers of the same shared cache key.
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Auto-fixed Issues
|
||||
|
||||
**1. [Rule 1 - Bug] `useCachedResource.ts`'s `onActivated` eagerly force-loaded a never-requested lazy resource**
|
||||
- **Found during:** Task 1, auditing Cloud.vue's `immediate: false` resources (`paidResource`, `myFilesResource`) ahead of Task 2's registration widening
|
||||
- **Issue:** `stale()` returns `true` for any never-fetched entry (`fetchedAt === null`), so `onActivated`'s bare `refreshIfStale()` would fire the fetch the instant a tab-gated lazy resource's owning view was first activated inside a KeepAlive — defeating "fetch on first use" (e.g. Cloud.vue's Paid Files tab data loading even though the user never opened that tab).
|
||||
- **Fix:** `onActivated` now skips the auto-revalidate when `opts.immediate === false && entry.fetchedAt === null`; a resource that has been explicitly fetched at least once still revalidates staleness-gated on later reactivations.
|
||||
- **Files modified:** `neode-ui/src/composables/useCachedResource.ts`, `neode-ui/src/composables/__tests__/useCachedResource.test.ts` (new test)
|
||||
- **Verification:** New test asserts the lazy resource is not fetched merely by activation, but does revalidate once explicitly requested and later reactivated past its TTL. Full suite green.
|
||||
- **Committed in:** `f177a505` (Task 1 commit)
|
||||
|
||||
**2. [Rule 1 - Bug] Server.vue's `vpnPollInterval` was armed at module setup, not gated to visibility at all**
|
||||
- **Found during:** Task 1, reading Server.vue's lifecycle regions per the plan's read_first
|
||||
- **Issue:** The pre-existing `const vpnPollInterval = setInterval(...)` ran at component setup time (before `onMounted`), meaning once anything wrapped Server.vue in KeepAlive, this 15s poll would run forever regardless of tab visibility — exactly the CPU-drain class of bug T-02-03 exists to prevent.
|
||||
- **Fix:** Converted to an idempotent `armVpnPoll()`/`disarmVpnPoll()` pair, armed on `onActivated` (with an immediate first tick) and torn down on `onDeactivated`; `onUnmounted` kept as a defensive teardown for the non-cached path.
|
||||
- **Files modified:** `neode-ui/src/views/Server.vue`
|
||||
- **Verification:** `keepAliveLifecycle.test.ts`'s real-view test mounts Server.vue inside a real `<KeepAlive>` and asserts `vpnStatus()` is not called while deactivated and is called once on reactivation.
|
||||
- **Committed in:** `f177a505` (Task 1 commit)
|
||||
|
||||
**3. [Rule 1 - Bug] Mesh.vue's share-to-mesh handoff would silently stop working after the first visit**
|
||||
- **Found during:** Task 1, tracing `loadPendingFromSession()`'s only two triggers (the `onMounted` call and the same-page `archipelago:share-to-mesh` custom event)
|
||||
- **Issue:** `App.vue` only dispatches the custom event when the user is *already* on `/mesh`; a direct `router.push('/mesh')` navigation (the only path for a share arriving from another screen) relied entirely on `onMounted`'s one-time read of the sessionStorage stash. Once Mesh.vue is kept alive, `onMounted` fires exactly once ever, so any share-to-mesh handoff after the very first Mesh visit would be silently dropped.
|
||||
- **Fix:** `loadPendingFromSession()` moved into the every-entry `onActivated` bucket alongside the rest of Mesh.vue's live-data refresh.
|
||||
- **Files modified:** `neode-ui/src/views/Mesh.vue`
|
||||
- **Verification:** Full suite green; behavior traced against `App.vue`'s `onShareToMeshMessage` handler to confirm the direct-navigation path is the one this fixes.
|
||||
- **Committed in:** `f177a505` (Task 1 commit)
|
||||
|
||||
**4. [Rule 1 - Bug, caught by a pre-existing test] `onActivated`-only placement broke every view outside a KeepAlive boundary**
|
||||
- **Found during:** Task 2, running the full suite after widening `KEEP_ALIVE_PATHS` — `CloudPeersRefresh.test.ts` (mounts `Cloud.vue` bare, no KeepAlive) failed
|
||||
- **Issue:** My first pass moved several views' `onMounted`-only initialization entirely into `onActivated`, reasoning that "Vue fires `onActivated` on first mount too" — true only when the component already has a KeepAlive ancestor. Outside one, `onActivated` never fires at all, so Cloud.vue (and, by the same construction, Home.vue/Chat.vue/Apps.vue/Server.vue/Web5.vue/Mesh.vue) would silently skip every timer/subscription/listener/data-load it owns when mounted bare.
|
||||
- **Fix:** Every arm function is now called from both `onMounted` and `onActivated`. Fresh-mount guard flags were added to Home.vue/Web5.vue/Mesh.vue/Server.vue (whose loaders have real RPC/CPU cost) so the redundant pass `onActivated` makes immediately after `onMounted` on a KeepAlive-wrapped first mount is skipped rather than doubling the first-load cost; Cloud.vue's dual call is left unguarded since its loaders are internally staleness-gated / inflight-deduped (confirmed genuinely free, not just low-cost) and Apps.vue/Chat.vue's dual call is trivial (a timer re-arm / listener re-add, no RPC).
|
||||
- **Files modified:** `neode-ui/src/views/Home.vue`, `neode-ui/src/views/Chat.vue`, `neode-ui/src/views/web5/Web5.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`
|
||||
- **Verification:** Full suite green (90 files / 740 tests), `npm run type-check` and `npm run build` both clean.
|
||||
- **Committed in:** `f177a505` and `03a3e4e0` (both task commits — the fix landed across the same files each task touched)
|
||||
|
||||
---
|
||||
|
||||
**Total deviations:** 4 auto-fixed (all Rule 1 — bugs directly implicated by this plan's own must_haves truths or caught by the existing test suite)
|
||||
**Impact on plan:** All four were necessary corrections that stayed within files already in scope for their respective tasks. No scope creep. The bare-mount regression (deviation 4) is the most consequential — it's a general pattern risk that would have silently affected every view converted in this plan had `CloudPeersRefresh.test.ts` not existed to catch it.
|
||||
|
||||
## Known Issues
|
||||
|
||||
**AIUI panel shows a blank white screen with a persistent loading spinner in the local mock-backend dev mode (`:8100`) — pre-existing, NOT a regression from this plan.**
|
||||
|
||||
- **Reported during:** Task 3 checkpoint, on the `:8101` dev preview session (this plan's own instance, pointed at archi-dev-box via `BACKEND_URL`)
|
||||
- **Diagnosis performed:**
|
||||
1. Diffed `Chat.vue` against `f177a505^` (its state immediately before this plan's Task 1 commit) — the AIUI iframe `src` construction (`aiuiUrl` computed) and the `ContextBroker` instantiation are byte-identical in intent; the only change is that `armChatLive()` (listener + broker setup) now runs from `onActivated`/`onMounted` instead of `onMounted` alone. `ContextBroker.start()` only adds a passive `window` message listener — it sends nothing to the iframe and has no handshake that a double-invocation on first mount could leave half-completed.
|
||||
2. Traced `aiuiUrl`'s computation: in dev mode (not `PROD`, not `IS_DEMO`), it is **empty unless `VITE_AIUI_URL` is explicitly set** — in which case Chat.vue renders the empty "AI Assistant not configured" placeholder, not a blank iframe. A blank-iframe-with-spinner symptom therefore requires `VITE_AIUI_URL` to be set to an unreachable target.
|
||||
3. Found the source: `scripts/dev-start.sh`'s "Mock backend" menu option (the one that serves `:8100`) launches `VITE_AIUI_URL=http://localhost:5173 vite` unconditionally, alongside a best-effort `cd ../../AIUI && pnpm dev` that silently no-ops (`|| echo '[AIUI] Not found...'`) when the separate AIUI repo isn't checked out next to `neode-ui/`. Confirmed on this machine: no `AIUI` directory exists anywhere near the project root, and nothing listens on port 5173 (`lsof -ti:5173` empty, `curl` to `localhost:5173` connection-refused).
|
||||
4. This means the `:8100` mock-backend dev session points its AIUI iframe at a dead local port **regardless of any Chat.vue code change** — the iframe shows the browser's own blank error page, and since nothing ever posts a `ready` message, `aiuiConnected` never flips true and the loading overlay never clears. This reproduces identically against `f177a505^`'s Chat.vue.
|
||||
5. 02-FINDINGS.md (written in 02-01, before this plan existed) already flags Chat/AIUI as `unmeasured` with connection/handshake latency called out as a known rough edge on real hardware — corroborating this is a pre-existing gap in this area, not something introduced here.
|
||||
- **Verdict:** Pre-existing artifact of the local "Mock backend" dev mode's AIUI wiring (missing sibling `AIUI` checkout), not a regression from this plan's commits. **Not fixed here** — the AIUI embed URL/connectivity work is explicitly owned by plan 02-07 (`02-AIUI-D14.md`, wave 4, not yet run); pulling it into 02-04 would be scope creep into another plan's file ownership.
|
||||
- **Recommendation for 02-07:** confirm the AIUI dev-mode wiring assumption (does it expect a sibling `../../AIUI` checkout, or should `VITE_AIUI_URL` only be set when that dev server is confirmed running?) as part of its own scope.
|
||||
|
||||
## Issues Encountered
|
||||
|
||||
- The Task 3 checkpoint's first pass surfaced the AIUI dev-mode gap above; diagnosed and confirmed pre-existing/out-of-scope per the coordinator's explicit instruction, so the checkpoint is treated as fully approved (all other steps passed on the first attempt).
|
||||
|
||||
## User Setup Required
|
||||
|
||||
None - no external service configuration required.
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
- The full main-tab registration set (`KEEP_ALIVE_PATHS`) is now the actual production set 02-05/02-06/02-07 build on — no further widening needed from those plans.
|
||||
- 02-05 (Mesh) and 02-06 (Server and Home) can proceed directly to converting the remaining fire-and-forget fetch groups to `useCachedResource`, since this plan already placed every lifecycle side effect correctly for that conversion to land safely under KeepAlive.
|
||||
- 02-07 (Chat/AIUI) inherits the AIUI dev-mode gap noted above as a concrete finding to address as part of its own D-14 UX work.
|
||||
- **Blocker/concern carried forward:** `/dashboard/settings` is not yet in the instance cache — a future plan should audit `Settings.vue`'s child sections (`SystemDangerZone.vue`, `VpnStatusSection.vue`, `KioskDisplaySection.vue`, `TransportPrefsCard.vue`, `ClaudeAuthSection.vue`) the way this plan audited the other eight tabs before registering it.
|
||||
- No other blockers for 02-05/02-06/02-07.
|
||||
|
||||
---
|
||||
*Phase: 02-ui-performance*
|
||||
*Completed: 2026-07-30*
|
||||
|
||||
## Self-Check: PASSED
|
||||
Loading…
x
Reference in New Issue
Block a user