Files
archy/.planning/phases/02-ui-performance/02-05-SUMMARY.md

23 KiB

phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, requirements-note, coverage, duration, completed, status
phase plan subsystem tags requires provides affects tech-stack key-files key-decisions patterns-established requirements-completed requirements-note coverage duration completed status
02-ui-performance 05 ui
vue
keepalive
useCachedResource
leaflet
pinia
mesh
phase provides
02-ui-performance/02-04 Mesh.vue's activate/deactivate lifecycle audit (armMeshLive/teardownMeshLiveEffects, dual onMounted+onActivated registration idiom, /dashboard/mesh registered in KEEP_ALIVE_PATHS)
phase provides
02-ui-performance/02-02 useCachedResource composable (TTL/persist/onActivated revalidation), RefreshIndicator.vue, the Cloud.vue reference pattern for immediate:false + manual Promise.allSettled kick
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
02-06
02-07
02-08
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
created modified
neode-ui/src/views/__tests__/meshTabCache.test.ts
neode-ui/src/components/__tests__/meshMapLifecycle.test.ts
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
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.
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
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.
id description requirement verification human_judgment
D1 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 PERF-02
kind ref status
unit neode-ui/src/views/__tests__/meshTabCache.test.ts pass
false
id description requirement verification human_judgment
D2 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 PERF-02
kind ref status
unit neode-ui/src/views/__tests__/meshTabCache.test.ts pass
false
id description requirement verification human_judgment
D3 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) PERF-02
kind ref status
unit neode-ui/src/views/__tests__/meshTabCache.test.ts pass
false
id description requirement verification human_judgment
D4 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 PERF-02
kind ref status
unit neode-ui/src/components/__tests__/meshMapLifecycle.test.ts pass
false
id description requirement verification human_judgment
D5 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 PERF-02
kind ref status
unit npm run test (92 files / 752 tests) pass
kind ref status
other npm run type-check && npm run build; grep for mesh.transport-status/mesh.refresh-all/etc in web/dist/neode-ui/assets/Mesh-*.js pass
false
50min 2026-07-30 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 <h1>, 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.tsdedup: true on fetchStatus/fetchPeers/fetchMessages/fetchDeadmanStatus/fetchBlockHeaders's RPC calls
  • neode-ui/src/stores/transport.tsdedup: true on fetchStatus/fetchPeers's RPC calls
  • neode-ui/src/api/rpc-client.tsdedup: true on getNodeDid, getTorAddress, meshContactsList, federationListNodes
  • neode-ui/src/components/MeshMap.vuearmMapVisibility()/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 <MeshMap v-if="showMapPanel"> 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 (<MeshMap v-if="showMapPanel"> 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 <threat_model> 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