Files
archy/.planning/phases/02-ui-performance/02-11-PLAN.md
T

25 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, gap_closure, requirements, user_setup, must_haves
phase plan type wave depends_on files_modified autonomous gap_closure requirements user_setup must_haves
02-ui-performance 11 execute 8
02-10
.planning/phases/02-ui-performance/02-FINDINGS.md
.planning/phases/02-ui-performance/02-PERF-FINAL.json
neode-ui/e2e/perf/profile-revisit.spec.ts
neode-ui/src/views/web5/Web5.vue
neode-ui/src/views/Server.vue
neode-ui/src/views/Discover.vue
neode-ui/src/views/AppDetails.vue
neode-ui/src/views/server/OpenWrtGateway.vue
neode-ui/src/views/Fleet.vue
neode-ui/src/composables/useCachedResource.ts
neode-ui/src/views/dashboard/dashboardViewWrappers.ts
neode-ui/src/views/dashboard/DashboardRouterView.vue
neode-ui/src/composables/useControllerNav.ts
true true
PERF-02
PERF-03
service why env_vars
archi-dev-box (the node's own UI login) Both the diagnostic profiling pass and the final re-measure drive a real authenticated browser session against the deployed build on archi-dev-box — the same D-11 on-device target every prior perf plan in this phase used.
name source
ARCHY_PASSWORD Supplied out-of-band by the coordinator at runtime. Passed only as an environment variable on the harness/profiling command line — never written to a file, commit, or planning artifact. Redact as [supplied] in all committed prose.
truths artifacts key_links
Before any source file changes, a real CPU/rendering profile (CDP Profiler + Tracing domains — the same underlying data DevTools' Performance panel visualizes) has been captured for each of the six surfaces' revisit window, and 02-FINDINGS.md names, per surface, WHERE the milliseconds actually go, cited to specific profile fields — not inferred from source reading alone
Fleet ends with an explicit verdict and a re-measured number, not just a root-cause guess — it is the worst specimen (330ms baseline -> 2631ms) and this plan's clearest test of whether the named cause and the fix are correct
AppDetails ends with an explicit verdict and a re-measured number
Web5 ends with an explicit verdict and a re-measured number
OpenWrtGateway ends with an explicit verdict and a re-measured number
Server ends with an explicit verdict and a re-measured number
Discover ends with an explicit verdict and a re-measured number
Every fix applied preserves the security decisions this phase already made (T-02-01 persist:false on wallet/identity keys stays false; no re-enabling sessionStorage persistence to shave milliseconds) and the visual/animation contract keepAliveTabs.test.ts pins (structural DOM-shape assertions stay green, byte-for-byte unmodified test file)
The re-measure uses the frozen 02-01 harness unmodified (git diff --stat against the harness's three core files is empty) so 02-PERF-FINAL.json stays directly comparable to BASELINE/AFTER/REMEASURE
First-paint (when a human would perceive content) is reported separately from contentSelector-visible (the harness's stricter measured field) for every surface where they diverge meaningfully, so the report doesn't overstate how bad a fixed/irreducible cost feels in practice
.planning/phases/02-ui-performance/02-FINDINGS.md — new `## Client-Side Render Cost Root Cause (gap closure)` section: profiling method, per-surface evidence, named cause, fix (or irreducible-cost justification), before/after numbers
.planning/phases/02-ui-performance/02-PERF-FINAL.json — fourth on-device measurement, 5 runs/surface, against the fixed build
neode-ui/e2e/perf/profile-revisit.spec.ts — the additive CDP-profiling diagnostic script (does not touch the frozen harness files)
02-PERF-FINAL.json -> 02-PERF-BASELINE.json / 02-PERF-AFTER.json / 02-PERF-REMEASURE.json — joined by surface `id`, four-way comparison on `revisitMs`
Each named cause in 02-FINDINGS.md -> the specific CPU-profile/trace-event/animation-log field that supports it
Phase 02's last open item: six surfaces are measurably SLOWER on revisit than the pre-phase-2 baseline, confirmed real (not noise) by 02-10's three-way dispersion analysis — Fleet (330ms -> 2631ms, worst, ~8x), AppDetails (1204ms -> 2668ms), Web5 (566ms -> 1329ms), OpenWrtGateway (664ms -> 1460ms), Server (738ms -> 1239ms), Discover (1083ms -> 1453ms). RPC counts are flat-or-improved on all six (confirmed in 02-FINDINGS.md's three-way table) and, for Server/Web5, the component instance is independently proven (02-09) to genuinely survive the round-trip — so this is NOT network and NOT a remount. It is client-side render/reactivity cost that KeepAlive caching made visible by removing the network wait that used to hide it.

D-10 discipline applies to this plan as it did to every plan before it: name the measured cause — with real profiling evidence, not a plausible-sounding hypothesis — before touching any source file. 02-10 named a class ("split-signal render/reactivation cost") but did not pinpoint a mechanism per surface; that is this plan's Task 1.

The target is NOT "restore the old baseline" — those baselines were already poor. A revisit to a KeepAlive-cached surface shows an existing component instance with data already in memory: no mount, no fetch, no layout from scratch. Pass bar: revisit ms under 300ms. Target: under 150ms. Where a surface genuinely cannot reach that, the plan must say so with profiling evidence naming the irreducible cost, not quietly settle at baseline and call it fixed.

Hard constraints carried into every task below: security decisions (T-02-01 persist:false on wallet/identity cache keys) are non-negotiable and stay exactly as they are regardless of any timing cost they impose. No visual or animation change — keepAliveTabs.test.ts structurally pins the dashboard DOM shape and must stay green and byte-for-byte unmodified. Money surfaces never present cached data as live (D-05/T-02-01, unaffected by this plan's scope). Frontend-only deploy to archi-dev-box, never the fleet/OTA path.

<execution_context> @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @CLAUDE.md

@.planning/phases/02-ui-performance/02-VERIFICATION.md

Read only these parts of the large phase artifacts:

  • .planning/phases/02-ui-performance/02-FINDINGS.md## Method, the ## Re-measurement (gap closure) section (three-way table, per-surface verdicts, Accepted Deviations table) — this is the exact gap this plan closes.
  • .planning/phases/02-ui-performance/02-REVIEW.md — the CR-01 follow-up (persist-schema-migration mechanism) and the "persist is now required, not defaulted" call-site table — every fix in this plan must preserve every persist:false decision recorded there.
  • .planning/phases/02-ui-performance/02-02-SUMMARY.md and 02-04-SUMMARY.md — the KeepAlive wrapper architecture (DashboardRouterView.vue/dashboardViewWrappers.ts) and the lifecycle-audit bucket convention (once-per-session / every-entry / only-while-visible), so any fix follows the established pattern rather than inventing a new one.
  • neode-ui/src/composables/useCachedResource.ts, neode-ui/src/stores/resources.ts — the shared caching layer every affected surface uses.

Do not re-read 02-PERF-BASELINE.json / 02-PERF-AFTER.json / 02-PERF-REMEASURE.json in full — extract fields with node -e/jq as needed.

Task 1: Profile each surface's revisit window and name the real cause per surface neode-ui/e2e/perf/profile-revisit.spec.ts, .planning/phases/02-ui-performance/02-FINDINGS.md `ARCHY_PASSWORD` is exported in the environment and `curl -sS -o /dev/null -w '%{http_code}' http://archi-dev-box/` returns 2xx/3xx. If a `cargo build`/`rustc` process is active on this shared box (`pgrep -af 'cargo|rustc'`), wait for it to finish before taking ANY timed measurement — a concurrent full workspace rebuild saturates this machine's CPU and would corrupt the numbers exactly the way 02-10's three-way analysis worked to rule out. `neode-ui/e2e/perf/surfaces.ts` (read-only, for `navSteps`/`contentSelector`/`rootSelector` — do not modify) and `neode-ui/e2e/perf/keepalive-remount-probe.spec.ts` (the 02-09 precedent for an additive, frozen-harness-respecting diagnostic spec: login helper, click-with-guard retry pattern). Build (or extend, if already present from an earlier session in this plan) a standalone, additive Playwright spec — `neode-ui/e2e/perf/profile-revisit.spec.ts` — that does NOT edit `surfaces.ts`/`measure.ts`/`surface-perf.spec.ts` (confirm via `git diff --stat` against those three files before and after this task).

For each of the six named surfaces (Web5, Server, Discover, AppDetails, OpenWrtGateway, Fleet), reproduce the harness's own first-visit -> away -> revisit structure, then during the revisit window ONLY, capture:

  1. A CDP Profiler (CPU) sample profile (Profiler.start/Profiler.stop), aggregated by self-time per function AND per deployed chunk (vendor/index/per-route chunk — production has no sourcemaps, so bucket by chunk name, not node_modules path).
  2. A CDP Tracing capture (categories including disabled-by-default-devtools.timeline) — the same data DevTools' Performance panel renders — aggregated by event name (Layout, RecalculateStyles, Paint, CompositeLayers, RunTask, TimerFire, ...) on the renderer main thread, so rendering-pipeline cost (which a bare JS CPU profile can only see as generic "(program)"/"(idle)") is visible by name.
  3. Every setTimeout/requestAnimationFrame call scheduled during the window (delay value, count) via a runtime monkey-patch, restored after read-back.
  4. Every RPC call's wall-clock start/duration during the window (method name + timing only, no bodies — T-02-06 convention).
  5. Every CSS transitionrun/transitionend/animationstart/animationend event on document during the window (type, property/animation name, target tag+class, elapsed time) — decisive evidence for whether a CSS transition duration (which does NOT block Playwright's visible check — only non-empty bounding box + not visibility:hidden — but DOES affect what a human perceives) is what dominates the wall clock.
  6. A first-paint timestamp independent of Playwright's own waitFor({state:'visible'}): a requestAnimationFrame-driven poll recording the instant contentSelector first gets a non-empty bounding box, so first-paint and content-visible can be reported as two distinct numbers.

Run it against archi-dev-box and capture the full output. For each surface, read the category/function/trace breakdown and name the DOMINANT real cost — do not guess from source reading alone; if the profile is ambiguous, add more instrumentation (e.g. per-component console.time/performance.mark if trace categories aren't decisive) rather than asserting a hypothesis the data doesn't support.

Write a new ## Client-Side Render Cost Root Cause (gap closure) section into 02-FINDINGS.md: one subsection per surface, citing the specific profile/trace/timer/animation-log evidence, naming the mechanism (e.g. "N ms in Layout/RecalculateStyles scaling with subtree size," "N chained rAF calls before contentSelector's bounding box is non-empty," "a debounced/every-entry useCachedResource onActivated cascade re-triggering M resources," "the navSteps chain transits another KeepAlive'd main tab whose own reactivation cost bleeds into this surface's measured window"). Commit this findings section BEFORE starting Task 2's source edits (D-10 gate — verified by commit-order, same discipline 02-01/02-09 used). git diff --stat 3ee20430 -- neode-ui/e2e/perf/surfaces.ts neode-ui/e2e/perf/measure.ts neode-ui/e2e/perf/surface-perf.spec.ts (must print nothing — the instrument stays frozen) grep -q "## Client-Side Render Cost Root Cause (gap closure)" .planning/phases/02-ui-performance/02-FINDINGS.md && for s in Web5 Server Discover AppDetails OpenWrtGateway Fleet; do grep -q "$s" .planning/phases/02-ui-performance/02-FINDINGS.md && echo "present: $s"; done 02-FINDINGS.md names, with cited profiling/trace evidence, the real dominant cost for each of the six surfaces' revisit-ms cost — committed before any neode-ui/src file changes in this plan.

Task 2: Fix each surface's named cause where fixable; record irreducible-cost justification where not neode-ui/src/views/web5/Web5.vue, neode-ui/src/views/Server.vue, neode-ui/src/views/Discover.vue, neode-ui/src/views/AppDetails.vue, neode-ui/src/views/server/OpenWrtGateway.vue, neode-ui/src/views/Fleet.vue, neode-ui/src/composables/useCachedResource.ts, neode-ui/src/views/dashboard/dashboardViewWrappers.ts, neode-ui/src/views/dashboard/DashboardRouterView.vue, neode-ui/src/composables/useControllerNav.ts, .planning/phases/02-ui-performance/02-FINDINGS.md For each surface whose Task 1 evidence names a fixable client-side cost (expensive computed re-evaluation, watcher cascade, whole-subtree re-render instead of re-attach, per-row recompute, unbounded rAF/timer chain, a navSteps transit-confound that is itself measuring another tab's real cost) apply the smallest change that removes the named cost, guarded by a test that fails before the fix and passes after. Where the named cost is genuinely irreducible (e.g., the security-mandated absence of sessionStorage persistence for wallet/identity keys forcing a real re-fetch every session-boot; or a navSteps chain that legitimately must transit another main tab because Fleet has no direct sidebar entry) do not force a fix — document the irreducible cost with its profiling evidence in 02-FINDINGS.md instead, and say so plainly rather than leaving the surface unaddressed. Work surface by surface, smallest diff first. Do not touch a file this task doesn't need to for a given surface's fix.

Guardrails that apply to every fix in this task, no exceptions:

  • Never re-enable persist: true/sessionStorage writes on any key T-02-01/CR-01 decided must be persist: false (web5.lnd-info, web5.networking-profits, server.fips-summary, and every other key in 02-REVIEW.md's persist-decision table) — if a fix's instinct is "cache this in sessionStorage to skip the re-fetch," that instinct is wrong for these keys; find a different mechanism (e.g. in-memory-only staleness widening, avoiding redundant re-render, not adding storage).
  • Never change keepAliveTabs.test.ts and never change any visible layout, spacing, animation timing/easing, or transition name — run this file's existing assertions after every change in this task and treat any diff in its output as a hard stop, not a thing to update.
  • If a fix requires touching useCachedResource.ts, dashboardViewWrappers.ts, or DashboardRouterView.vue (shared by every KeepAlive'd surface, not just the six named here), verify the change against the FULL existing keepAliveLifecycle.test.ts + keepAliveTabs.test.ts suite, not just the affected surface's own tests — a shared-composable/host change with an unseen side effect on an unrelated tab is exactly the class of regression 02-04's own bare-mount bug demonstrated this phase.
  • If a navSteps transit-confound is Task 1's named cause for a surface (e.g. Fleet transiting Web5, OpenWrtGateway transiting Server, Discover/AppDetails transiting Apps), the fix target is the TRANSITED tab's own reactivation cost, not the destination surface — fixing Web5's reactivation cost should show up as an improvement in Fleet's measured number too; say so explicitly if that's what happens, rather than fixing the same mechanism twice under two different surface names.
  • Every fix gets a regression test in the relevant __tests__ file (or a new one, following the CloudPeersRefresh.test.ts/MarketplaceRefresh.test.ts precedent when an existing test file's vi.mock('vue-router') would conflict) that fails on the pre-fix code and passes after.

After each surface's fix (or irreducible-cost decision) lands, append its outcome to the ## Client-Side Render Cost Root Cause (gap closure) section in 02-FINDINGS.md: what changed, why, the regression test that pins it, and — once Task 3's re-measure exists — the before/after number.

Run the full suite, npm run type-check, and npm run build (grep the built bundle for a new string introduced by at least one fix, per CLAUDE.md's silent-no-op-build caution) after all six surfaces are addressed, before moving to Task 3. cd neode-ui && npm test 2>&1 | tail -15 cd neode-ui && npm run type-check git diff --stat -- neode-ui/src/views/dashboard/tests/keepAliveTabs.test.ts (must print nothing — structural visual contract untouched) Every one of the six surfaces has either a landed, test-covered fix or an explicit, evidence-backed irreducible-cost note in 02-FINDINGS.md. Full suite, type-check and build are green. keepAliveTabs.test.ts is byte-for-byte unmodified and still passing. No persist:false decision was reverted.

Task 3: Deploy, re-measure with the frozen harness, and write the four-way verdict .planning/phases/02-ui-performance/02-PERF-FINAL.json, .planning/phases/02-ui-performance/02-FINDINGS.md, .planning/REQUIREMENTS.md No `cargo`/`rustc` process is active on this shared box (`pgrep -af 'cargo|rustc'`) — re-check immediately before both the deploy and the harness run, not just once at the start of this task, since a build can start mid-task on a shared machine. Confirm the working tree's `neode-ui/src` changes from Task 2 are the ONLY uncommitted/unpushed changes about to ship — `git status --short -- neode-ui/src` should show only this plan's own files. If another session's uncommitted work is present in `neode-ui/src` at deploy time (the exact hazard 02-10 hit), stop and report it rather than deploying it alongside this plan's fix, per the deploy-safety rule carried in this plan's context.

Deploy frontend-only from the MAIN checkout (never a worktree — this box IS archi-dev-box over loopback, so a worktree-sourced deploy is the exact hazard that previously destroyed ~1810 tracked files): ARCHIPELAGO_TARGET=archipelago@archi-dev-box scripts/deploy-to-target.sh --frontend-only. Confirm the post-deploy health check and scripts/deploy-history.log's new entry (dirty=false, the commit hash matching Task 2's last commit).

Re-run the frozen harness exactly as 02-10 did: cd neode-ui && ARCHY_BASE_URL=http://archi-dev-box ARCHY_PERF_RUNS=5 ARCHY_PERF_OUT=../.planning/phases/02-ui-performance/02-PERF-FINAL.json npx playwright test e2e/perf/surface-perf.spec.ts --project=chromium --reporter=line, with ARCHY_PASSWORD exported. Confirm git diff --stat against the three frozen harness files is still empty.

Build a four-way comparison (BASELINE -> AFTER -> REMEASURE -> FINAL) for all six named surfaces, dispersion (min/median/max) not bare medians, the same discipline 02-10 established. For each surface, write an explicit verdict into 02-FINDINGS.md:

  • Fixed: revisit ms at or below the pass bar (300ms) or the stretch target (150ms), with the number and the mechanism that closed the gap.
  • Improved but not yet under the pass bar: report the number honestly, explain the remaining gap against the profiling evidence (do not round up to "fixed").
  • Irreducible/justified: the profiling evidence from Task 1/2 naming exactly why (e.g. a security-mandated re-fetch cost with no persistence to shortcut it), with the number recorded as the accepted floor.

Report first-paint (from Task 1's independent probe, re-run once against the final build if useful for the write-up) separately from contentSelector-visible for any surface where they diverge meaningfully, so the report states plainly whether the fix also improved the human-perceived experience, not just the harness's stricter metric.

Update .planning/REQUIREMENTS.md's PERF-02/PERF-03 rows to point at this final verdict. Commit and push (git add by path, Co-Authored-By trailer, git push gitea-ai main). test -f .planning/phases/02-ui-performance/02-PERF-FINAL.json && node -e "const r=require('./.planning/phases/02-ui-performance/02-PERF-FINAL.json'); console.log('rows:', (r.surfaces||r.results||r).length||Object.keys(r).length, 'baseUrl:', r.baseUrl, 'runs:', r.runs)" git diff --stat 3ee20430 -- neode-ui/e2e/perf/surfaces.ts neode-ui/e2e/perf/measure.ts neode-ui/e2e/perf/surface-perf.spec.ts (must print nothing) for s in Web5 Server Discover AppDetails OpenWrtGateway Fleet; do grep -A2 "^**$s" .planning/phases/02-ui-performance/02-FINDINGS.md | head -3; done 02-PERF-FINAL.json exists (5 runs/surface, archi-dev-box, frozen harness). Every one of the six named surfaces (Web5, Server, Discover, AppDetails, OpenWrtGateway, Fleet) has an explicit fixed/improved/irreducible verdict with a real number in 02-FINDINGS.md. REQUIREMENTS.md reflects the final state. Work is committed and pushed.

<threat_model>

Trust Boundaries

Boundary Description
operator workstation -> archi-dev-box UI login The real node password crosses this boundary at runtime to drive both the profiling script and the final re-measure
profiling script -> local console output CPU profiles / trace events / RPC method names are printed to the terminal transcript, never written to a committed artifact verbatim
fixed build -> /opt/archipelago/web-ui on a real node Task 3's frontend-only deploy

STRIDE Threat Register

Threat ID Category Component Severity Disposition Mitigation Plan
T-02-11-01 Information Disclosure ARCHY_PASSWORD high mitigate Environment variable only, never inlined into a committed command, never echoed into 02-FINDINGS.md or the SUMMARY.
T-02-11-02 Information Disclosure profile-revisit.spec.ts output medium mitigate RPC tracking captures method name + timing only, no bodies (matches the frozen harness's own T-02-06 mitigation exactly).
T-02-11-03 Tampering any persist:false cache-key decision (T-02-01/CR-01) critical mitigate Task 2's guardrails explicitly forbid re-enabling sessionStorage persistence on any wallet/identity key to shave milliseconds; every fix is checked against 02-REVIEW.md's persist-decision table before landing.
T-02-11-04 Tampering keepAliveTabs.test.ts (visual/structural contract) high mitigate Task 2 explicitly gates every fix against this file's unmodified assertions passing; git diff --stat on this specific file is a Task 2 verify step.
T-02-11-05 Tampering frozen harness (surfaces.ts/measure.ts/surface-perf.spec.ts) medium mitigate git diff --stat gate in Task 1 and Task 3, identical to every prior gap-closure plan this phase.
T-02-11-06 Tampering scripts/deploy-to-target.sh --frontend-only medium mitigate Main-checkout only (never a worktree — this box is archi-dev-box itself), archi-dev-box only, no OTA/fleet path, post-deploy health check and deploy-history log entry confirmed.
T-02-11-SC Tampering npm/pip/cargo installs high accept No new package-manager dependency is needed for a CDP-profiling script (Playwright is already a devDependency) or for any of the named fix classes. If one becomes necessary, halt and route through the Package Legitimacy Gate.
</threat_model>
- 02-FINDINGS.md names the real, profiled cause for each of the six surfaces before any source-file change (commit-order verified). - Every one of the six surfaces ends with an explicit verdict: fixed (with numbers under the pass bar), improved-but-honest (with numbers and the remaining gap explained), or irreducible/justified (with the security or architectural reason named). - `02-PERF-FINAL.json` exists against a fresh archi-dev-box deploy, 5 runs/surface, frozen harness confirmed via `git diff --stat`. - Full vitest suite, type-check and build green; `keepAliveTabs.test.ts` byte-for-byte unmodified and passing. - No `persist:false` (T-02-01) decision reverted. - First-paint reported separately from contentSelector-visible wherever they diverge meaningfully.

<success_criteria>

  • The phase's last open item (six confirmed-real timing regressions, none previously fixed) is closed with real fixes where fixable and honest, evidenced irreducible-cost notes where not.
  • No surface is left at "accepted deviation, no further action" without at least one real attempt at profiling + fixing, per the objective's explicit reframing that the old baseline is not the target.
  • The visual/animation contract and every T-02-01 security decision this phase already made are provably unchanged. </success_criteria>
Create `.planning/phases/02-ui-performance/02-11-SUMMARY.md` when done: per-surface before/after numbers across all four artifacts, first-paint vs content-visible where they diverge, what changed and why, what was deliberately left as an irreducible/justified cost, and confirmation the visual/security constraints held.