From 8563cf4a848a3ffd6f4cac61863a463f3233fbbb Mon Sep 17 00:00:00 2001 From: archipelago Date: Thu, 30 Jul 2026 16:25:06 -0400 Subject: [PATCH] docs(02-05): complete Mesh tab cache + graphics lifecycle plan --- .planning/ROADMAP.md | 6 +- .planning/STATE.md | 18 +- .../phases/02-ui-performance/02-05-SUMMARY.md | 228 ++++++++++++++++++ 3 files changed, 242 insertions(+), 10 deletions(-) create mode 100644 .planning/phases/02-ui-performance/02-05-SUMMARY.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 49451943..cecd9826 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -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**: 4/8 plans executed +**Plans**: 5/8 plans executed Plans: **Wave 1** @@ -91,7 +91,7 @@ Plans: **Wave 4** *(blocked on Wave 3 completion)* -- [ ] 02-05-PLAN.md — Mesh: cache the six fetch groups, bound the D3 graph and Leaflet map (PERF-02) +- [x] 02-05-PLAN.md — Mesh: cache the six fetch groups, bound the D3 graph and Leaflet map (PERF-02) - [ ] 02-06-PLAN.md — Server and Home: cache the uncached fan-out, guarantee wallet freshness (PERF-02) - [ ] 02-07-PLAN.md — Chat/AIUI: stable embed URL + the two D-14 UX defaults (PERF-02) @@ -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 | 4/8 | In Progress| | +| 2. UI Performance | 5/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 | - | diff --git a/.planning/STATE.md b/.planning/STATE.md index 3bbf15e9..34047a77 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -5,15 +5,15 @@ milestone_name: milestone current_phase: 02 current_phase_name: ui-performance status: executing -stopped_at: Completed 02-04-PLAN.md -last_updated: "2026-07-30T19:29:23.802Z" +stopped_at: Completed 02-05-PLAN.md +last_updated: "2026-07-30T20:24:44.908Z" last_activity: 2026-07-30 last_activity_desc: Phase 02 execution started progress: total_phases: 8 completed_phases: 0 total_plans: 18 - completed_plans: 4 + completed_plans: 5 percent: 0 --- @@ -29,11 +29,11 @@ See: .planning/PROJECT.md (updated 2026-07-29) ## Current Position Phase: 02 (ui-performance) — EXECUTING -Plan: 4 of 8 +Plan: 5 of 8 Status: Ready to execute Last activity: 2026-07-30 — Phase 02 execution started -Progress: [██░░░░░░░░] 22% +Progress: [███░░░░░░░] 28% ## Performance Metrics @@ -56,6 +56,7 @@ Progress: [██░░░░░░░░] 22% | Phase 02 P03 | 45min | 3 tasks | 5 files | | Phase 02 P02 | 105min | 3 tasks | 11 files | | Phase 02 P04 | 150min | 3 tasks | 12 files | +| Phase 02 P05 | 50min | 2 tasks | 8 files | ## Accumulated Context @@ -89,6 +90,9 @@ Decisions are logged in PROJECT.md (10 locked ADRs in the `` block + - [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 +- [Phase ?]: 02-05: mesh.refreshAll()/transport.fetchStatus() stay uncached at the store level (other callers need guaranteed-fresh reads); the useCachedResource wrapper around each lives in Mesh.vue instead, since Pinia's defineStore(id,setup) runs in a bare effectScope where onActivated() silently no-ops +- [Phase ?]: 02-05: FLAGGED - RESEARCH.md's premise that Mesh.vue owns a D3 force simulation is incorrect for this codebase (verified via grep); only NetworkMap.vue/Federation.vue has one. Task 2's D3 truths are vacuously satisfied; only the real Leaflet map lifecycle (MeshMap.vue, added to scope) was implemented +- [Phase ?]: 02-05: per-group TTL/persist table - mesh.refresh-all/federation-nodes/self-onion/self-did/contacts all persist:false (identity payload); transport-status persists (aggregate, non-identity); reachability groups get 10s TTL, identity groups 300s ### Pending Todos @@ -123,6 +127,6 @@ None yet. ## Session Continuity -Last session: 2026-07-30T19:29:12.322Z -Stopped at: Completed 02-04-PLAN.md +Last session: 2026-07-30T20:24:44.884Z +Stopped at: Completed 02-05-PLAN.md Resume file: None diff --git a/.planning/phases/02-ui-performance/02-05-SUMMARY.md b/.planning/phases/02-ui-performance/02-05-SUMMARY.md new file mode 100644 index 00000000..dc02d6b5 --- /dev/null +++ b/.planning/phases/02-ui-performance/02-05-SUMMARY.md @@ -0,0 +1,228 @@ +--- +phase: 02-ui-performance +plan: 05 +subsystem: ui +tags: [vue, keepalive, useCachedResource, leaflet, pinia, mesh] + +# Dependency graph +requires: + - phase: 02-ui-performance/02-04 + provides: "Mesh.vue's activate/deactivate lifecycle audit (armMeshLive/teardownMeshLiveEffects, dual onMounted+onActivated registration idiom, /dashboard/mesh registered in KEEP_ALIVE_PATHS)" + - phase: 02-ui-performance/02-02 + provides: "useCachedResource composable (TTL/persist/onActivated revalidation), RefreshIndicator.vue, the Cloud.vue reference pattern for immediate:false + manual Promise.allSettled kick" +provides: + - "Mesh.vue's six previously-uncached tab-entry fetch groups (mesh.refreshAll, transport.fetchStatus, refreshFederationNodes, refreshSelfOnion, refreshSelfDid, refreshContacts) each behind a keyed useCachedResource entry with an explicit TTL and persist decision" + - "RefreshIndicator wired into the Mesh header, driven by whether any of the six groups is refreshing" + - "MeshMap.vue's Leaflet map instance quiesced/repaired across the Mesh tab's activate/deactivate cycle (armMapVisibility/disarmMapVisibility)" + - "A documented, tested finding that no D3 force simulation exists in Mesh.vue's tree — RESEARCH.md's premise was incorrect for this codebase" +affects: [02-06, 02-07, 02-08] + +# Tech tracking +tech-stack: + added: [] + patterns: + - "useCachedResource hosted at the component call site (Mesh.vue) even when it wraps a store action, when the store action has other callers needing a guaranteed-fresh (uncached) read — Pinia's defineStore(id, setup) runs in a bare effectScope, not a component instance, so the composable's internal onActivated() would silently no-op (dev warning) if called at store scope" + - "refreshMeshGroupIfStale(res) gate — mirrors Cloud.vue's loadCounts() idiom (entry.data === null || isStale.value) for immediate:false resources force-refreshed inside a single Promise.allSettled array, keeping a multi-group fan-out concurrent instead of serialized (T-02-16)" + - "Side-effect-only cached resources: fetchers that wrap an existing store/view function which already sets its own reactive refs as a side effect, resolving to a sentinel timestamp (Date.now()) rather than the real payload — used purely to gate/dedupe RPCs and expose loadState for a shared RefreshIndicator, not to hold data" + +key-files: + created: + - neode-ui/src/views/__tests__/meshTabCache.test.ts + - neode-ui/src/components/__tests__/meshMapLifecycle.test.ts + modified: + - neode-ui/src/views/Mesh.vue + - neode-ui/src/stores/mesh.ts + - neode-ui/src/stores/transport.ts + - neode-ui/src/api/rpc-client.ts + - neode-ui/src/components/MeshMap.vue + - neode-ui/src/views/mesh/mesh-styles.css + +key-decisions: + - "mesh.refreshAll()/transport.fetchStatus() themselves are left untouched (still uncached, always-fresh) because they have other callers needing a guaranteed-fresh read: clearAllMesh() (must re-read post-clear state) and Web5SendReceiveModals.vue's pre-send balance/mesh-only check (money-critical, must never read from a TTL-gated cache). The useCachedResource() wrapper around each lives in Mesh.vue and calls the store's existing action as its fetcher, rather than living inside stores/mesh.ts/transport.ts." + - "FLAGGED: RESEARCH.md's premise that Mesh.vue owns a live D3 force simulation is factually wrong for this codebase — grep for d3/forceSimulation/simulation across neode-ui/src found nothing in Mesh.vue's or MeshMap.vue's tree. The only D3 force simulation belongs to NetworkMap.vue (Federation.vue's graph, out of scope). Task 2's D3-specific truths are vacuously satisfied; only the real Leaflet map lifecycle (MeshMap.vue) was fixed." + - "MeshMap.vue was added to files_modified beyond the plan's literal list, because the Leaflet map instance and its listeners/ResizeObserver live there, not in Mesh.vue — fixing the map's activate/deactivate correctness structurally requires editing where the instance lives (Rule 3 auto-fix, minimal/in-spirit, non-architectural)." + - "meshMapLifecycle.test.ts is a separate file from meshTabCache.test.ts (not appended) because its vi.mock('@/stores/mesh')/vi.mock('leaflet') hoist file-wide and would clobber meshTabCache.test.ts's need for the real mesh/transport stores — same class of conflict 02-02 hit and resolved by splitting MarketplaceRefresh.test.ts out of keepAliveTabs.test.ts." + - "Per-group TTL/persist decisions (see table below), following D-06 discretion and the T-02-01 persist prohibition." + +patterns-established: + - "refreshMeshGroupIfStale(res) — the generalized form of Cloud.vue's loadCounts() staleness gate, applied uniformly across a set of immediate:false resources kicked from a single Promise.allSettled array" + - "Fetchers for side-effect-only cached resources resolve to Date.now() rather than null, so useCachedResource's entry.data !== null / isStale gating works correctly even when the real payload is stored elsewhere (in existing store refs), not in the resource's own entry.data" + +requirements-completed: [] +requirements-note: "PERF-02 is NOT marked complete — it also spans 02-06 and 02-07 (Server/Home data layer, Chat/AIUI), which still extend the KeepAlive/cache architecture to the remaining main tabs, per the precedent set by 02-02/02-03/02-04's own summaries." + +coverage: + - id: D1 + description: "All six Mesh tab-entry fetch groups (mesh.refresh-all, transport.status, federation-nodes, self-onion, self-did, contacts) are cached with explicit TTLs; a revisit inside TTL issues zero RPC across all six, a cold load still fires all six concurrently, and a rejected group never blocks the other five" + requirement: PERF-02 + verification: + - kind: unit + ref: "neode-ui/src/views/__tests__/meshTabCache.test.ts" + status: pass + human_judgment: false + - id: D2 + description: "Peer/reachability/transport data (10s TTL) revalidates on a stale revisit while this node's own DID/onion (300s TTL) stays cached; every group carrying peer or self identity data is persist:false, only aggregate transport status persists; every fetcher backing the six groups passes dedup:true" + requirement: PERF-02 + verification: + - kind: unit + ref: "neode-ui/src/views/__tests__/meshTabCache.test.ts" + status: pass + human_judgment: false + - id: D3 + description: "RefreshIndicator renders in the Mesh header, visible while any of the six groups revalidates (including reachability) so a resumed tab never presents a frozen reachability state as current (T-02-13)" + requirement: PERF-02 + verification: + - kind: unit + ref: "neode-ui/src/views/__tests__/meshTabCache.test.ts" + status: pass + human_judgment: false + - id: D4 + description: "MeshMap.vue's Leaflet map is quiesced/repaired across the Mesh tab's activate/deactivate cycle: exactly one map instance is constructed across repeated visits, its size is invalidated on reactivation, and its window resize listener/ResizeObserver are removed on deactivate and re-added exactly once on activate" + requirement: PERF-02 + verification: + - kind: unit + ref: "neode-ui/src/components/__tests__/meshMapLifecycle.test.ts" + status: pass + human_judgment: false + - id: D5 + description: "No visual/animation regression — full suite (752 tests incl. the structural keepAliveTabs.test.ts DOM-shape pin) stays green, type-check and build are clean, and the built bundle contains the new resource keys/strings" + requirement: PERF-02 + verification: + - kind: unit + ref: "npm run test (92 files / 752 tests)" + status: pass + - kind: other + ref: "npm run type-check && npm run build; grep for mesh.transport-status/mesh.refresh-all/etc in web/dist/neode-ui/assets/Mesh-*.js" + status: pass + human_judgment: false + +duration: 50min +completed: 2026-07-30 +status: complete +--- + +# Phase 02 Plan 05: Mesh Tab Cache + Graphics Lifecycle Summary + +**Mesh's six fetch groups (status/peers/messages/deadman/blockheaders, transport status, federation nodes, self onion, self DID, contacts) each behind a keyed `useCachedResource` with per-group TTL/persist decisions and a shared `RefreshIndicator`, plus the Leaflet map's listener/ResizeObserver lifecycle bounded across tab deactivation — no D3 simulation was found to exist in Mesh.vue's tree, contrary to RESEARCH.md's premise** + +## Performance + +- **Duration:** ~50 min +- **Completed:** 2026-07-30T20:17:44Z +- **Tasks:** 2 (Task 1 auto/tdd, Task 2 auto/tdd) +- **Files modified:** 8 (2 new test files, 6 modified) + +## Accomplishments + +- **Six fetch groups cached** behind `useCachedResource`, all hosted in `Mesh.vue` (see key-decisions for why, not in `stores/mesh.ts`/`stores/transport.ts`), each `immediate: false` and force-refreshed only when stale via `refreshMeshGroupIfStale()` inside a single `Promise.allSettled` array in `armMeshLive`: + + | Key | Fetcher wraps | TTL | Persist | Reason | + |---|---|---|---|---| + | `mesh.refresh-all` | `mesh.refreshAll()` (status/peers/messages/deadman/block-headers) | 10,000ms | `false` | Peer reachability is the liveness-critical surface T-02-13 forbids freezing; carries peer DIDs/pubkeys (identity payload) | + | `mesh.transport-status` | `transport.fetchStatus()` | 10,000ms | `true` | Live transport-availability state, but no peer identity payload — explicitly the one group the plan names as safe to persist | + | `mesh.federation-nodes` | `refreshFederationNodes()` | 30,000ms | `false` | D-06 default (not liveness-critical); carries DID/pubkey/onion | + | `mesh.self-onion` | `refreshSelfOnion()` | 300,000ms | `false` | This node's own onion address is effectively static; identity payload | + | `mesh.self-did` | `refreshSelfDid()` | 300,000ms | `false` | This node's own DID is effectively static; identity payload | + | `mesh.contacts` | `refreshContacts()` | 30,000ms | `false` | User-set aliases/contact records — identity payload per T-02-01 | + +- `armMeshLive`'s six-way `Promise.all([...])` fan-out replaced with `Promise.allSettled(meshCachedGroups.map(refreshMeshGroupIfStale))` — proven concurrent in test (all six underlying RPCs have already fired by the very next synchronous line after mount, before any `await`), and proven non-blocking (one group's store action rejecting still lets the other five complete and the post-fan-out `.then()` — `refreshOutboxCount` + deep-link matching — still runs). +- `dedup: true` added to every RPC call backing the six groups: `mesh.status`, `mesh.peers`, `mesh.messages`, `mesh.deadman-status`, `mesh.block-headers` (mesh.ts), `transport.status` (transport.ts), and the convenience methods `getNodeDid`, `getTorAddress`, `meshContactsList`, `federationListNodes` (rpc-client.ts). +- `RefreshIndicator` added to the Mesh header (new `.mesh-title-row` flex wrapper around the existing `

`, no change to any pre-existing selector/animation), driven by `meshRefreshIndicatorState` — `'refreshing'` whenever any of the six groups is in `loadState === 'refreshing'`. +- **MeshMap.vue's Leaflet lifecycle**: `onMounted`'s window-resize-listener + `ResizeObserver` setup refactored into idempotent `armMapVisibility()`/`disarmMapVisibility()`, dual-registered on `onMounted`+`onActivated` (with a fresh-mount guard) and torn down on `onDeactivated`, mirroring Mesh.vue's own `armMeshLive`/`teardownMeshLiveEffects` idiom from 02-04. The Leaflet instance itself is never destroyed/recreated by this (`initMap()`'s own guard already makes construction idempotent); reactivation calls `map.invalidateSize()` via `nextTick` so a map laid out off screen re-tiles at its real size. +- **Flagged premise mismatch, documented and tested**: RESEARCH.md's Task 2 premise ("Mesh is... a live D3 force simulation and a Leaflet map") does not hold — a full grep across `neode-ui/src` for `d3`/`forceSimulation`/`simulation` found zero hits in `Mesh.vue`'s or `MeshMap.vue`'s component tree. The only D3 force simulation in the codebase belongs to `NetworkMap.vue` (used by `Federation.vue`, a different view entirely, out of this plan's scope). Task 2's D3-specific `must_haves` truths ("the D3 force simulation stops...", "restarts only if data changed...", "cancels any pending animation-frame callback...") are therefore vacuously true (there is nothing to leak) — only the real Leaflet-map-specific truths were implemented and tested. + +## Task Commits + +Each task was committed atomically: + +1. **Task 1: Cache the six Mesh fetch groups without serializing them** - `31389bcc` (feat, tdd) +2. **Task 2: Bound the D3 simulation and Leaflet map across deactivation** - `abdfa07a` (feat, tdd — D3 portion vacuous per the flagged finding above; Leaflet portion real) + +**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/02-04 history)._ + +## Files Created/Modified + +- `neode-ui/src/views/Mesh.vue` — six `useCachedResource` entries, `meshCachedGroups`/`refreshMeshGroupIfStale`/`meshRefreshIndicatorState`, `armMeshLive`'s fan-out converted to `Promise.allSettled`, `RefreshIndicator` added to the header +- `neode-ui/src/stores/mesh.ts` — `dedup: true` on `fetchStatus`/`fetchPeers`/`fetchMessages`/`fetchDeadmanStatus`/`fetchBlockHeaders`'s RPC calls +- `neode-ui/src/stores/transport.ts` — `dedup: true` on `fetchStatus`/`fetchPeers`'s RPC calls +- `neode-ui/src/api/rpc-client.ts` — `dedup: true` on `getNodeDid`, `getTorAddress`, `meshContactsList`, `federationListNodes` +- `neode-ui/src/components/MeshMap.vue` — `armMapVisibility()`/`disarmMapVisibility()` idempotent pair, dual-registered `onMounted`/`onActivated`, `onDeactivated` teardown, `nextTick`-scheduled `invalidateSize()` on reactivation +- `neode-ui/src/views/mesh/mesh-styles.css` — new `.mesh-title-row` rule (flex wrapper for the title + RefreshIndicator; no existing selector touched) +- `neode-ui/src/views/__tests__/meshTabCache.test.ts` — new; 8 tests covering Task 1's five behaviors plus dedup/persist/indicator-wiring assertions +- `neode-ui/src/components/__tests__/meshMapLifecycle.test.ts` — new; 4 tests covering Task 2's real (Leaflet-only) behaviors + +## Decisions Made + +See `key-decisions` in frontmatter for the full list. Highlights: + +- **Cache placement (mesh.refreshAll/transport.fetchStatus)**: the `useCachedResource()` call itself lives in `Mesh.vue`, not inside `stores/mesh.ts`/`stores/transport.ts`, even though it wraps those stores' own actions. Reason, verified against Vue's source (`node_modules/@vue/runtime-core/dist/runtime-core.cjs.js`'s `injectHook`): Pinia's `defineStore(id, setup)` runs its setup function inside a bare `effectScope()`, not a real component instance (`currentInstance` is `null`), so `onActivated()` called from inside a Pinia store setup is a documented Vue no-op (dev warning only, never throws) — it would compile but would never actually revalidate anything on tab reactivation. Mesh.vue is the one call site that legitimately owns the KeepAlive/component lifecycle. The fetchers still literally re-invoke `mesh.refreshAll()`/`transport.fetchStatus()` unchanged, so those store actions' other callers (`clearAllMesh()`, and `Web5SendReceiveModals.vue`'s pre-send mesh-only check, which must never read a TTL-gated cache before moving money) keep their existing guaranteed-fresh behavior untouched. +- **MeshMap.vue added to files_modified** beyond the plan's literal list (`Mesh.vue`, `mesh.ts`, `transport.ts`, `meshTabCache.test.ts`) — the Leaflet map instance, its window listener, and its `ResizeObserver` all live in `MeshMap.vue`, a child component `` inside Mesh.vue's template. Fixing the map's activate/deactivate correctness structurally requires editing where the instance lives; this is a minimal, in-spirit, non-architectural addition (Rule 3 auto-fix — same class of judgment call 02-02 made when it added `dashboardViewWrappers.ts` outside its own original file list). +- **Test file split**: `meshMapLifecycle.test.ts` is a new, separate file rather than appended to `meshTabCache.test.ts`, because its `vi.mock('@/stores/mesh', ...)` (a minimal plain-object stub) and `vi.mock('leaflet', ...)` are hoisted to the top of whichever file they're declared in by vitest/esbuild, and would clobber `meshTabCache.test.ts`'s need for the **real** `mesh`/`transport`/`resources` Pinia stores (needed so the six-group cache/dedup/persist logic under test is genuinely exercised, not stubbed away). This mirrors the exact precedent 02-02 set with `MarketplaceRefresh.test.ts` for the same class of `vi.mock`-hoisting conflict. +- **Persist/TTL table**: see Accomplishments above — every group carrying this node's own DID/onion or any peer identity payload (peers, federation nodes, contacts/aliases) is `persist: false`; only `transport.status` (an aggregate, no identity fields) persists, matching the plan's own explicit carve-out. +- **FLAGGED (not auto-backstopped)**: the D3 force-simulation premise from RESEARCH.md is factually incorrect for the current codebase state. This was verified by grepping the entire `neode-ui/src` tree (not just `Mesh.vue`) for `d3`, `forceSimulation`, and `simulation` — the only hits belong to `src/components/federation/NetworkMap.vue`, imported exclusively by `Federation.vue`. Mesh.vue's only graphics context is the Leaflet map (`MeshMap.vue`). This is surfaced here for human review per the assumptions_and_flagged_items convention rather than silently reinterpreting the task; the real, testable Leaflet-lifecycle work (which the plan also required) was implemented and covered in full. + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 3 - Blocking] MeshMap.vue added to files_modified to fix the Leaflet map's lifecycle** +- **Found during:** Task 2, tracing where the Leaflet instance/listener/observer actually live +- **Issue:** The plan's `files_modified` for this task only lists `Mesh.vue` and `meshTabCache.test.ts`, but the Leaflet map, its window resize listener, and its `ResizeObserver` all live in the child component `MeshMap.vue` (`` in Mesh.vue's template) — Mesh.vue itself has no direct graphics-context code to edit. +- **Fix:** Extended scope to `neode-ui/src/components/MeshMap.vue` (added `armMapVisibility`/`disarmMapVisibility`, dual `onMounted`/`onActivated` registration, `onDeactivated` teardown) and a new companion test file `neode-ui/src/components/__tests__/meshMapLifecycle.test.ts`. +- **Files modified:** `neode-ui/src/components/MeshMap.vue`, `neode-ui/src/components/__tests__/meshMapLifecycle.test.ts` (new) +- **Verification:** 4 new tests pass (one map instance across 3 cycles, invalidateSize on activate, listener add/remove counts, ResizeObserver connect/disconnect counts); full suite, type-check, build all green. +- **Committed in:** `abdfa07a` (Task 2 commit) + +**2. [Rule 1 - Bug in own test authoring, caught before commit] meshMapLifecycle.test.ts's listener-count test needed a mount-time baseline** +- **Found during:** Task 2, first test run +- **Issue:** `armMapVisibility()`'s idempotent remove-then-add idiom means `window.removeEventListener('resize', ...)` is also called once during the very first mount (not just on deactivate) — the test's naive assertion (`expect(removeSpy...).toBe(1)` after the first deactivate) was off by one. +- **Fix:** Baseline both `addSpy`/`removeSpy` counts immediately after mount, then assert relative increments across the deactivate/activate cycle. +- **Files modified:** `neode-ui/src/components/__tests__/meshMapLifecycle.test.ts` +- **Verification:** Test passes; the underlying implementation was correct all along, only the test's assumption was wrong. +- **Committed in:** `abdfa07a` (Task 2 commit) + +**3. [Rule 1 - Bug in own test authoring, caught before commit] meshTabCache.test.ts's dedup:true assertion needed to scope to the six groups' own methods** +- **Found during:** Task 1, first test run +- **Issue:** `refreshOutboxCount()` (called from the fan-out's `.then()`, not one of the six cached groups) issues an `rpcClient.call({method: 'mesh.outbox'})` without `dedup: true` — a blanket "every captured rpcClient.call has dedup:true" assertion incorrectly failed on this unrelated, out-of-scope call. +- **Fix:** Restricted the assertion to the six groups' own RPC methods (`mesh.status`, `mesh.peers`, `mesh.messages`, `mesh.deadman-status`, `mesh.block-headers`, `transport.status`). +- **Files modified:** `neode-ui/src/views/__tests__/meshTabCache.test.ts` +- **Verification:** Test passes; confirms all six groups' fetchers (and no others) are asserted against dedup:true. +- **Committed in:** `31389bcc` (Task 1 commit) + +--- + +**Total deviations:** 3 (1 Rule 3 scope extension necessary to fulfill the task's literal requirement; 2 Rule 1 fixes to the test file's own assertions, caught and corrected before either commit landed — no production-code bugs found) +**Impact on plan:** The MeshMap.vue extension is the only deviation with lasting scope impact, and it is narrowly targeted (lifecycle hooks only, no restructuring, no visual change) and fully test-covered. No scope creep beyond what Task 2's literal must_haves required. + +## Known Stubs + +None — no stub data, placeholder text, or unwired data sources were introduced. Every cached group's fetcher performs a real RPC round-trip through the existing store/view functions; nothing renders hardcoded empty/mock data. + +## Threat Flags + +None beyond what the plan's own `` already anticipated (T-02-01, T-02-03, T-02-13, T-02-16) — no new network endpoints, auth paths, or trust-boundary-crossing surface was introduced by this plan. + +## Issues Encountered + +- The D3-force-simulation premise mismatch (see Decisions Made) — resolved by verifying via grep and treating the affected truths as vacuously satisfied, with the finding surfaced prominently here for human review rather than silently reinterpreting the task's scope. +- Two test-authoring bugs in the new test files themselves (both fixed before either commit — see Deviations 2 and 3 above); no production-code bugs were found during this plan. + +## User Setup Required + +None - no external service configuration required. + +## Next Phase Readiness + +- Mesh's six fetch groups and its Leaflet map lifecycle are now on the same cached/activate-deactivate architecture as every other main tab (02-02/02-04's tracer + lifecycle-audit foundation extended to the heaviest remaining tab). +- The `refreshMeshGroupIfStale`/`Promise.allSettled` pattern (and the "useCachedResource must be hosted at a real component, not inside a Pinia store setup" finding) is available for 02-06 (Server/Home) if either store owns a fetch action with other callers needing a guaranteed-fresh read. +- **For 02-08 (on-device verification)**: heap-growth across many Mesh tab cycles is a property no jsdom unit test can settle — `meshMapLifecycle.test.ts` proves exactly one Leaflet map instance is constructed across repeated activate/deactivate cycles in a synthetic harness, but real browser memory tooling on archi-dev-box is where the D-03 bounded-memory claim gets its final check, per the plan's own `verification: backstop` marker. +- **Flag carried forward**: if a future audit finds Mesh.vue (or any other main tab) genuinely does need a D3-based visualization (e.g., if `HopVizModal.vue`'s message-hop graphic is later rebuilt with D3), re-open this finding — the current absence was verified for the codebase state as of this plan's execution, not asserted as a permanent architectural constraint. +- No blockers for 02-06/02-07. + +--- +*Phase: 02-ui-performance* +*Completed: 2026-07-30* + +## Self-Check: PASSED