diff --git a/.planning/phases/02-ui-performance/02-11-PLAN.md b/.planning/phases/02-ui-performance/02-11-PLAN.md new file mode 100644 index 00000000..76dfc632 --- /dev/null +++ b/.planning/phases/02-ui-performance/02-11-PLAN.md @@ -0,0 +1,214 @@ +--- +phase: 02-ui-performance +plan: 11 +type: execute +wave: 8 +depends_on: ["02-10"] +files_modified: + - .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 +autonomous: true +gap_closure: true +requirements: [PERF-02, PERF-03] +user_setup: + - service: archi-dev-box (the node's own UI login) + why: "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." + env_vars: + - name: ARCHY_PASSWORD + source: "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." + +must_haves: + truths: + - "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" + artifacts: + - ".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)" + key_links: + - "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. + + + +@$HOME/.claude/gsd-core/workflows/execute-plan.md +@$HOME/.claude/gsd-core/templates/summary.md + + + +@.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. + + + + + +## 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. | + + + +- 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. + + + +- 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. + + + +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. +