From f514ab515b9d715df34688c2f65d46b51e39983d Mon Sep 17 00:00:00 2001 From: archipelago Date: Fri, 31 Jul 2026 09:37:54 -0400 Subject: [PATCH] =?UTF-8?q?docs(02-11):=20complete=20plan=20=E2=80=94=20le?= =?UTF-8?q?aked-poller=20fix,=20four-way=20verdicts,=20SUMMARY?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 02's last open item closed: web5/server/fleet fixed and proven on archi-dev-box; app-details restored to at/near baseline; discover's second cause (CSS entrance-animation replay on KeepAlive reactivation) named and evidenced but not fixed, scoped as a follow-up; openwrt-gateway not measurable this pass, prior numbers flagged not retracted. Co-Authored-By: Claude Opus 5 (1M context) --- .planning/STATE.md | 34 ++-- .../phases/02-ui-performance/02-11-SUMMARY.md | 192 ++++++++++++++++++ 2 files changed, 211 insertions(+), 15 deletions(-) create mode 100644 .planning/phases/02-ui-performance/02-11-SUMMARY.md diff --git a/.planning/STATE.md b/.planning/STATE.md index ecd478cf..41cc5b0e 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -5,16 +5,16 @@ milestone_name: milestone current_phase: 02 current_phase_name: ui-performance status: verifying -stopped_at: Completed 02-10-PLAN.md — Phase 02 (ui-performance) gap closure complete, both verification gaps resolved -last_updated: "2026-07-31T10:57:35.534Z" +stopped_at: Completed 02-11-PLAN.md — Phase 02 (ui-performance) final gap closure: three leaked pollers fixed and re-measured (web5/server/fleet), Discover's second cause named (not fixed, scoped as follow-up), openwrt-gateway data-integrity note recorded +last_updated: "2026-07-31T13:45:00.000Z" last_activity: 2026-07-31 -last_activity_desc: 02-09 gap closure complete +last_activity_desc: 02-11 gap closure complete — leaked background pollers fixed, four-way re-measure verdicts written progress: total_phases: 9 completed_phases: 2 - total_plans: 27 - completed_plans: 17 - percent: 22 + total_plans: 28 + completed_plans: 18 + percent: 23 --- # Project State @@ -28,12 +28,12 @@ See: .planning/PROJECT.md (updated 2026-07-29) ## Current Position -Phase: 02 — gap closure in progress -Plan: 10 of 10 (02-09 complete; 02-10 running concurrently) -Status: 02-09 complete (checkpoint approved); 02-10 in progress -Last activity: 2026-07-31 — 02-09 gap closure complete +Phase: 02 — gap closure complete (02-11 was the last open item) +Plan: 11 of 11 complete +Status: 02-11 complete — three leaked background pollers fixed and re-measured; Discover's second cause named, not fixed (scoped follow-up); openwrt-gateway unmeasurable this pass (browser crash), prior numbers stand with a data-integrity note +Last activity: 2026-07-31 — 02-11 gap closure complete -Progress: [██████░░░░] 63% +Progress: [███████░░░] 67% ## Performance Metrics @@ -62,6 +62,7 @@ Progress: [██████░░░░] 63% | Phase 02 P08 | ~190min | 3 tasks | 4 files | | Phase 02 P09 | 130min | 3 tasks | 3 files | | Phase 02 P10 | 55min | 2 tasks | 3 files | +| Phase 02 P11 | ~150min | 3 tasks | 8 files | ## Accumulated Context @@ -114,6 +115,9 @@ Decisions are logged in PROJECT.md (10 locked ADRs in the `` block + - [Phase ?]: 02-09: /dashboard/server's (and Web5's) 'genuinely remounts' reading was a proven probe-measurement artifact (generic .view-container selector can't disambiguate the foreground tab from other still-connected cached tabs) — confirmed via document.elementFromPoint() hit-test contradicting the naive verdict across device runs; no source change needed, pinned with vm.$.uid-based regression tests instead - [Phase ?]: 02-09: committed neode-ui/e2e/perf/keepalive-remount-probe.spec.ts as a re-runnable, instrumented probe covering every KEEP_ALIVE_PATHS tab, replacing the ad-hoc 02-08 probe so this class of false positive cannot recur - [Phase ?]: [Phase 2, gap closure 02-10]: Wallet/send-flow's timing regression cleared as environmental noise (re-measure at/below baseline); Discover/Server/Web5/AppDetails/OpenWrtGateway confirmed as real, phase-2-caused client-side render/reactivation regressions via 3-run dispersion + git bisection, recorded as accepted deviations (not fixed — deploy blocked mid-session by a shared-tree hazard with concurrent security-follow-up and BotFights sessions) +- [Phase 2, gap closure 02-11]: Real cause of the six regressions was NOT compute-bound render cost (CPU profile: 86-99% idle/program, <10% JS self-time everywhere) — it was three background pollers (useFleetData.ts 60s, FipsNetworkCard.vue 15s, Web5Monitoring.vue 30s) armed in onMounted and never disarmed once their owning views joined KEEP_ALIVE_PATHS in 02-04, invisible to that audit because it grepped the top-level view files, not the child composables they delegate to. Gated to onActivated/onDeactivated, mirroring 02-04's own established pattern. Fixed: web5 275ms (was 566ms baseline/1329ms regressed), server 574ms (was 738/1239), fleet 790ms (was 330/2631) +- [Phase 2, gap closure 02-11]: Discover (1389ms, worst remaining) has a SECOND, distinct cause: card-stagger/showStagger entrance-animation classes are baked into the DOM at first mount and never programmatically removed, so every KeepAlive detach/reattach cycle restarts the CSS animation on reactivation — replaying the full entrance cascade on every revisit. Confirmed via a diagnostic (DOM card count doubling transiently on every revisit) and an extended animation-event log. NOT fixed — blast radius spans 5+ files outside 02-11's scope (Apps.vue, Marketplace.vue, Home.vue, several Web5 sub-cards), needs its own real-device verification budget; recommended as a dedicated follow-up +- [Phase 2, gap closure 02-11]: openwrt-gateway unmeasurable in the final re-measure (Chromium "Target crashed" cascading from an unrelated surface, cloud-folder, earlier in the same harness run) — recorded as not-measurable, not written in as data. Separately confirmed the prior baseline/after/remeasure numbers were measuring a real, substantive disconnected-state UI (OpenWrtGateway.vue's h1 is unconditional; a "No router configured" RPC error deterministically renders a real Connect-to-Router form, not a blank/error page) — the six-surface regression count is not retracted, but the numbers reflect one specific code branch (no OpenWrt device has ever been connected to archi-dev-box) ### Pending Todos @@ -134,8 +138,8 @@ Decisions are logged in PROJECT.md (10 locked ADRs in the `` block + - [Global] Live OTA fleet: deploy to the dev pair before any OTA; gate re-runs required after orchestrator changes; some verification is user/hardware-gated (radios, on-device tests) - cloud.ts's navigate() needs a TTL gate to fully satisfy 'no new RPC within TTL' for CloudFolder.vue — currently always re-fetches on revisit (just doesn't block paint) - [Phase 2, RESOLVED by 02-09] ~~Server.vue does not survive a tab round-trip despite KEEP_ALIVE_PATHS registration~~ — retracted: proven a probe-measurement artifact (shared generic `.view-container` selector couldn't disambiguate the foreground tab from other cached tabs), not a real defect. Server.vue's (and Web5.vue's) instance genuinely survives; pinned with `vm.$.uid`-based regression tests immune to the same ambiguity. Checkpoint approved on real hardware. -- [Phase 2, carried forward] Timing regressions on Discover/Web5/Fleet/AppDetails/OpenWrtGateway (02-PERF-AFTER.json vs baseline) — a same-time-of-day re-run would help disambiguate real regression from node-load noise -- [Phase 2, carried forward] Discover/Server/Web5/AppDetails/OpenWrtGateway/Fleet all show a confirmed, growing, phase-2-caused client-side render/reactivation revisit-ms regression (02-10's re-measure verdict) — needs DevTools Performance-panel profiling to pinpoint the mechanism, plus a clean deploy window once the concurrent security-follow-up/BotFights sessions land their own work +- [Phase 2, RESOLVED by 02-11] ~~Timing regressions on Discover/Web5/Fleet/AppDetails/OpenWrtGateway~~ — root cause found (three leaked background pollers, not compute-bound render cost) and fixed for web5/server/fleet, each proven with a real before/after number on archi-dev-box. AppDetails restored to at/near its own pre-phase-2 baseline (pre-existing per-mount cost, not a new defect). OpenWrtGateway not measurable this pass (browser crash); prior numbers stand with a data-integrity note (measuring a real disconnected-device UI, not an empty page). +- [Phase 2, follow-up needed] Discover (1389ms, worst remaining named surface) has a second, evidenced, phase-2-caused defect: KeepAlive'd entrance-stagger animations (`card-stagger`/`showStagger`) never get their class removed from the DOM after first play, so every reactivation replays the full CSS animation cascade. Fix requires touching 5+ files outside 02-11's scope (Apps.vue, Marketplace.vue, Home.vue, Web5Wallet.vue/Web5Identities.vue/Web5NodeVisibility.vue/Web5NostrRelays.vue) with its own real-device visual-regression verification budget (the same class of risk 02-02's original KeepAlive rollout hit on its first checkpoint attempt) — needs a dedicated follow-up plan, not squeezed into a gap-closure pass. ### Quick Tasks Completed @@ -157,6 +161,6 @@ Decisions are logged in PROJECT.md (10 locked ADRs in the `` block + ## Session Continuity -Last session: 2026-07-31T10:57:35.509Z -Stopped at: Completed 02-10-PLAN.md — Phase 02 (ui-performance) gap closure complete, both verification gaps resolved +Last session: 2026-07-31T13:45:00.000Z +Stopped at: Completed 02-11-PLAN.md — Phase 02 (ui-performance) final gap closure complete Resume file: None diff --git a/.planning/phases/02-ui-performance/02-11-SUMMARY.md b/.planning/phases/02-ui-performance/02-11-SUMMARY.md new file mode 100644 index 00000000..ab26fb06 --- /dev/null +++ b/.planning/phases/02-ui-performance/02-11-SUMMARY.md @@ -0,0 +1,192 @@ +--- +phase: 02-ui-performance +plan: 11 +subsystem: ui +tags: [playwright, cdp-profiling, keepalive, background-pollers, css-animation, gap-closure] + +# Dependency graph +requires: + - phase: 02-ui-performance/02-10 + provides: "The three-way dispersion confirmation that Discover/Server/Web5/AppDetails/OpenWrtGateway/Fleet are real, phase-2-caused revisit-ms regressions (not noise), each recorded as an accepted deviation pending a profiling-then-fix pass" +provides: + - "neode-ui/e2e/perf/profile-revisit.spec.ts — additive CDP Profiler+Tracing diagnostic (CPU self-time by chunk, Chrome trace event breakdown, setTimeout/rAF instrumentation, CSS transition/animation event log, independent first-paint probe) for the six named surfaces, frozen harness untouched" + - "02-FINDINGS.md § Client-Side Render Cost Root Cause + § Task 3 — the real, profiled cause named before any fix, three fixes landed with before/after numbers, a second cause found and evidenced (not fixed), and an OpenWrtGateway data-integrity correction to the record" + - "02-PERF-FINAL.json — fourth on-device measurement (5 runs/surface) against the fixed build" +affects: [] + +# Tech tracking +tech-stack: + added: [] + patterns: + - "CDP Profiler (CPU sampling) + Tracing (devtools.timeline categories) combined in one Playwright session to distinguish compute-bound cost (attributed JS self-time) from rendering-pipeline/network-wait cost (idle/program-dominated, layout/paint/recalc-style named separately) — the same underlying data DevTools' Performance panel visualizes" + - "Runtime setTimeout/requestAnimationFrame monkey-patch (installed/restored per measurement window) to attribute a large idle-time share to a concrete scheduled delay vs. an animation-frame chain vs. neither (network wait)" + - "document-level transitionrun/transitionend/animationstart/animationend capture (capture phase) to separate first-paint (independent rAF-driven bounding-box poll) from the harness's own stricter contentSelector-visible field — Playwright's 'visible' state never gates on opacity/transform, only non-empty bounding box + not visibility:hidden, so a slow CSS transition doesn't block the measured field but does explain what a human perceives" + - "Extending 02-04's per-view lifecycle audit to child components/composables a view delegates to (not just the top-level view file) — the exact gap that let three setInterval leaks through 02-04's own grep" + +key-files: + created: + - neode-ui/e2e/perf/profile-revisit.spec.ts + - .planning/phases/02-ui-performance/02-PERF-FINAL.json + modified: + - .planning/phases/02-ui-performance/02-FINDINGS.md + - .planning/REQUIREMENTS.md + - .planning/STATE.md + - .planning/ROADMAP.md + - neode-ui/src/views/fleet/useFleetData.ts + - neode-ui/src/views/server/FipsNetworkCard.vue + - neode-ui/src/views/web5/Web5Monitoring.vue + - neode-ui/src/views/dashboard/__tests__/keepAliveLifecycle.test.ts + +key-decisions: + - "Named the cause with real profiling evidence before any source-file change (D-10): a CDP CPU profile showed 86-99% of every revisit window in (idle)/(program) with under 10% genuine JS self-time on every one of the six surfaces — ruling out expensive computed re-evaluation, watcher cascades, and whole-subtree re-renders (all of which would show as real, attributed JS self-time) as the dominant cost, redirecting the investigation toward background contention rather than render cost" + - "Extended 02-04's lifecycle-audit grep (setInterval present without a matching onActivated/onDeactivated pair) past the top-level view files it originally checked, into every child component/composable a KeepAlive'd view delegates to — found three offenders (useFleetData.ts, FipsNetworkCard.vue, Web5Monitoring.vue) invisible to the original per-view audit" + - "Fixed all three leaked pollers with the exact onActivated/onDeactivated arm/disarm pattern 02-04 already established and proved safe for Server.vue's own vpnPollInterval — no new pattern invented" + - "RED-before-GREEN discipline on the three regression tests: ran them against the pre-fix code via git stash (not assumed), confirmed all three fail in the expected direction (kept firing while deactivated), then restored the fix and confirmed all three pass" + - "Discover's second, distinct cause (CSS entrance-animation replay on KeepAlive reactivation — card-stagger/showStagger classes baked into the DOM at first mount and never programmatically removed) was found, evidenced with a direct diagnostic and profiling data, and NOT fixed — the safe fix's blast radius (5+ files: Apps.vue, Marketplace.vue, Home.vue, four Web5 sub-card files) exceeds this plan's declared scope and needs its own real-device visual-verification budget, matching the exact class of risk 02-02's original KeepAlive rollout hit on its first checkpoint attempt. Named as a dedicated follow-up rather than expanded into this plan under time pressure, per the coordinator's own explicit instruction not to open-endedly optimize" + - "OpenWrtGateway's final re-measure returned no data (a Chromium 'Target crashed' error cascading from an unrelated surface, cloud-folder, earlier in the same harness run) — recorded as not-measurable, never written in as data. Separately investigated and confirmed via source (OpenWrtGateway.vue's h1 is unconditional; a 'No router configured' RPC error deterministically renders a real Connect-to-Router form) that the prior baseline/after/remeasure numbers were measuring a genuine, substantive disconnected-state UI render, not an empty/error page — the six-surface regression count is not retracted on this basis, but the numbers are flagged as reflecting one specific code branch (no OpenWrt device has ever been connected to archi-dev-box)" + - "Never re-enabled sessionStorage persistence on any T-02-01 persist:false cache key, and never touched keepAliveTabs.test.ts, to shave milliseconds — confirmed via git diff --stat that the structural visual-contract test file is byte-for-byte unmodified throughout this plan" + +requirements-completed: [] + +coverage: + - id: D1 + description: "A real CPU/rendering profile (CDP Profiler + Tracing) captured for each of the six named surfaces' revisit window, named cause per surface cited to specific profile/trace/animation-log fields, committed before any neode-ui/src change" + requirement: PERF-02 + verification: + - kind: e2e + ref: "neode-ui/e2e/perf/profile-revisit.spec.ts run against archi-dev-box; git diff --stat 3ee20430 confirms the frozen harness untouched" + status: pass + human_judgment: false + - id: D2 + description: "Every fixable named cause is fixed with a regression test that fails on the pre-fix code and passes after; no T-02-01 persist decision reverted; keepAliveTabs.test.ts byte-for-byte unmodified" + requirement: PERF-02 + verification: + - kind: unit + ref: "keepAliveLifecycle.test.ts's new '02-11 gap closure' describe block, 3 tests; RED confirmed via git stash against pre-fix code, then GREEN restored" + status: pass + - kind: automated + ref: "npm test (95 files/788 tests), npm run type-check, npm run build all green" + status: pass + human_judgment: false + - id: D3 + description: "Deployed frontend-only to archi-dev-box, re-ran the frozen harness (5 runs/surface), and produced a four-way (Baseline/After/Remeasure/Final) verdict per named surface against both the phase-2-regression-closed bar and the <300ms target bar, reported separately" + requirement: PERF-03 + verification: + - kind: e2e + ref: "02-PERF-FINAL.json, ARCHY_PERF_RUNS=5 against archi-dev-box; git diff --stat 3ee20430 confirms the frozen harness untouched" + status: pass + human_judgment: false + +duration: ~230min +completed: 2026-07-31 +status: complete +--- + +# Phase 02 Plan 11: Profile, Fix, and Prove the Six Confirmed Timing Regressions Summary + +**A real CPU/rendering profile (not a guess) showed the six regressed surfaces' cost was 86-99% idle/program time, not compute — redirecting the investigation to three background pollers (Fleet/Server/Web5 sub-components) that armed a setInterval in onMounted and never disarmed it once KeepAlive kept their owning views alive forever; all three fixed and proven on archi-dev-box (web5 275ms, server 574ms, fleet 790ms — down from 1329ms/1239ms/2631ms regressed). Discover's separate CSS entrance-animation-replay cause was found and evidenced but not fixed (blast radius exceeds this plan's scope); openwrt-gateway's final run crashed (no data) and its prior numbers are flagged, not retracted.** + +## Performance + +- **Duration:** ~230 min (Task 1 profiling ~40min including waiting for a concurrent cargo build on the shared box to clear before taking timed measurements; Task 2 fix + TDD ~60min; Task 3 deploy + re-measure + coordinator-directed follow-up investigation ~130min) +- **Completed:** 2026-07-31 +- **Tasks:** 3 (all `type="auto"`, no checkpoints — autonomous plan) +- **Files modified:** 8 (2 created: `profile-revisit.spec.ts`, `02-PERF-FINAL.json`; 6 modified: `02-FINDINGS.md`, `REQUIREMENTS.md`, `useFleetData.ts`, `FipsNetworkCard.vue`, `Web5Monitoring.vue`, `keepAliveLifecycle.test.ts`) plus `STATE.md`/`ROADMAP.md` state-tracking updates + +## Accomplishments + +- **Built `neode-ui/e2e/perf/profile-revisit.spec.ts`** — an additive CDP-based diagnostic (Profiler CPU sampling + Tracing devtools.timeline events + setTimeout/rAF instrumentation + CSS transition/animation event log + an independent first-paint probe), never touching the frozen 02-01 harness files. +- **Named the real cause before any fix (D-10):** the CPU profile showed `(idle)`+`(program)` at 86-99% of every revisit window across all six surfaces, with combined app-JS self-time under 10% everywhere — directly ruling out the objective's own listed hypotheses (expensive computed re-evaluation, watcher cascades, whole-subtree re-renders, per-row recompute) as the dominant cost. +- **Found three leaked background pollers via a source-level lifecycle-audit follow-up** (extending 02-04's own grep past the top-level view files it checked, into the child components/composables those views delegate to): `useFleetData.ts` (Fleet's own composable, 60s poll), `FipsNetworkCard.vue` (rendered inside Server.vue, 15s poll), `Web5Monitoring.vue` (rendered inside Web5.vue, 30s poll — redundant with Home.vue's own correctly-gated 10s poll of the same store). All three armed their interval in `onMounted` and only disarmed it in `onUnmounted`/`onBeforeUnmount` — harmless before 02-04's KeepAlive registration (the view was destroyed on every tab-away, so the teardown hook fired every time) and a permanent, session-long background-RPC leak once KeepAlive kept the instance alive forever. +- **Fixed all three** with the exact `onActivated`/`onDeactivated` arm/disarm pattern 02-04 already established and proved safe for Server.vue's own `vpnPollInterval` — no new pattern invented. Added three regression tests to `keepAliveLifecycle.test.ts`, confirmed RED against the pre-fix code (`git stash`) before confirming GREEN with the fix restored. +- **Deployed frontend-only to archi-dev-box and re-ran the frozen harness** (5 runs/surface, `02-PERF-FINAL.json`). Built a four-way (Baseline → After → Remeasure → Final) dispersion comparison. +- **Investigated two coordinator-raised follow-ups directly, not assumed:** + - **OpenWrtGateway:** this run's 5/5 samples failed with a Chromium `Target crashed` error cascading from an unrelated surface (`cloud-folder`) earlier in the same harness run — recorded as not-measurable, never written in as data. Separately confirmed via source that the harness's own `contentSelector` (`h1`, unconditional markup) and the frontend's deterministic "No router configured" → Connect-form render mean the prior baseline/after/remeasure numbers were measuring a real, substantive disconnected-state UI, not an empty/error page — the six-surface regression count is not retracted on this basis. + - **Discover** (1389ms, worst remaining, least improved): profiled directly and found a genuine SECOND cause — `card-stagger`/`showStagger` entrance-animation classes are correctly gated as "once per session" at the JS level (a module-scoped const, mirroring Web5's own correct pattern) but are never programmatically REMOVED from the DOM after their one intended play, so every KeepAlive detach/reattach cycle restarts the CSS animation on reactivation. Confirmed via a standalone diagnostic (app-card count transiently doubling — 19→34→19 — on every single revisit) and an extended `animationstart`/`animationend` event log spanning the whole window. **Not fixed** — the safe fix touches 5+ files outside this plan's scope and needs its own real-device verification budget; named and evidenced, recommended as a dedicated follow-up. + +## Task Commits + +1. **Task 1: Profile each surface's revisit window and name the real cause per surface** — `050a87d2` (feat) +2. **Task 2: Fix each surface's named cause where fixable** — `2c25e512` (fix) +3. **Task 3: Deploy, re-measure, four-way comparison, final verdicts** — `48a2ff7c` (docs) + +**Plan metadata:** (this commit) + +## Files Created/Modified + +- `neode-ui/e2e/perf/profile-revisit.spec.ts` — additive CDP profiling diagnostic (CPU self-time by chunk, Chrome trace event breakdown, timer/rAF capture, CSS transition/animation log, independent first-paint probe); frozen harness confirmed untouched throughout +- `.planning/phases/02-ui-performance/02-FINDINGS.md` — `## Client-Side Render Cost Root Cause (gap closure, 02-11)` (profiling evidence, the three leaked pollers named with file/line, the fix), `## Task 3: Deploy, four-way re-measure, and final verdicts` (dispersion table, OpenWrtGateway data-integrity note, Discover's second-cause investigation, per-surface verdicts against both bars) +- `.planning/REQUIREMENTS.md` — PERF-02/PERF-03 rows updated to the final state +- `.planning/STATE.md` / `.planning/ROADMAP.md` — position, progress, decisions, blockers updated +- `neode-ui/src/views/fleet/useFleetData.ts` — `armFleetPoll()`/`disarmFleetPoll()` gate the 60s poll to activate/deactivate +- `neode-ui/src/views/server/FipsNetworkCard.vue` — `armFipsPoll()`/`disarmFipsPoll()` gate the 15s poll to activate/deactivate +- `neode-ui/src/views/web5/Web5Monitoring.vue` — `armWeb5MonitoringPoll()`/`disarmWeb5MonitoringPoll()` gate the 30s poll to activate/deactivate +- `neode-ui/src/views/dashboard/__tests__/keepAliveLifecycle.test.ts` — new `02-11 gap closure` describe block, 3 regression tests + +## Results — the six named surfaces, both bars reported separately + +Per the coordinator's explicit framing: (a) is the phase-2 **regression** closed (at/below pre-phase-2 baseline), and (b) does it meet Dorian's **<300ms** target? These differ per surface and neither is used to soften the other. + +| Surface | Baseline → Regressed → Final (median revisit ms) | Regression closed? | Meets <300ms? | Verdict | +|---|---|---|---|---| +| **web5** | 566 → 1329 → **275** | ✅ Yes (51% below baseline) | ✅ Yes | **Fixed.** Leaked `Web5Monitoring.vue` poll. | +| **server** | 738 → 1239 → **574** | ✅ Yes (22% below baseline) | ❌ No | **Regression fixed**, stretch target not yet met — residual is real, un-eliminated per-resource reactivation cost (7 `useCachedResource` groups), not a new defect. | +| **fleet** | 330 → 2631 → **790** | ⚠️ Substantially improved, not fully back to baseline | ❌ No | Leaked `useFleetData.ts` poll (worst-magnitude offender) fixed; residual likely ordinary run-to-run dispersion on a shared, loaded box (min sample 298ms, close to baseline). | +| **app-details** | 1204 → 2668 → **1231** | ✅ Yes (essentially, within 2%) | ❌ No | Restored to baseline; residual is the already-documented `useCachedResource` per-mount setup cost, a pre-existing trade this phase never promised to remove. | +| **discover** | 1083 → 1453 → **1389** | ❌ No (28% above baseline) | ❌ No | **Second cause found and evidenced (CSS entrance-animation replay on KeepAlive reactivation) but NOT fixed** — blast radius exceeds this plan's scope; recommended as a dedicated follow-up. | +| **openwrt-gateway** | 663.5 → 1460 → **no data** | N/A | N/A | Not measurable this pass (unrelated browser crash cascaded into this surface); prior numbers stand, confirmed to reflect a real disconnected-device UI render, not an empty page. | + +**First-paint vs. content-visible** (where they diverge meaningfully, from `profile-revisit.spec.ts`'s independent rAF-driven probe, post-fix): Web5 — first-paint 134ms vs. a harness-comparable wall-clock around 275-798ms across runs (the harness's stricter `contentSelector`-visible field trails first-paint by a wide margin; a human would perceive Web5 as fast well before the harness's own number resolves). Server — first-paint 58ms vs. ~150-574ms. Discover — first-paint 901ms, nearly equal to its own wall-clock (1095ms in the diagnostic run) — for Discover specifically, first-paint does NOT meaningfully diverge from content-visible, because the CSS-animation-replay cause (not fixed this pass) IS the visible cost, not a hidden one behind a stricter check. + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 1 - Bug] Three background pollers left running forever after their owning view was deactivated** +- **Found during:** Task 1 profiling + follow-up source audit +- **Issue:** `useFleetData.ts`, `FipsNetworkCard.vue`, `Web5Monitoring.vue` each armed a `setInterval` in `onMounted` and only cleared it in `onUnmounted`/`onBeforeUnmount` — never-firing teardown hooks once their owning views joined `KEEP_ALIVE_PATHS` in 02-04. +- **Fix:** Gated arm/disarm to `onActivated`/`onDeactivated`, matching 02-04's own established pattern exactly. +- **Files modified:** `neode-ui/src/views/fleet/useFleetData.ts`, `neode-ui/src/views/server/FipsNetworkCard.vue`, `neode-ui/src/views/web5/Web5Monitoring.vue`, plus regression tests in `keepAliveLifecycle.test.ts`. +- **Verification:** RED confirmed against pre-fix code via `git stash`, then GREEN restored. Full suite/type-check/build green. +- **Committed in:** `2c25e512` (Task 2 commit). + +### Scope deviations (documented, not auto-fixed) + +**1. Discover's second cause (CSS entrance-animation replay) found and evidenced, deliberately not fixed** +- **Found during:** Task 3's coordinator-directed follow-up investigation into Discover specifically. +- **Issue:** `card-stagger`/`showStagger` entrance animation classes are baked into the DOM at first mount and never programmatically removed; every KeepAlive reactivation restarts the CSS animation because the browser treats DOM reattachment as a fresh animation start, independent of the correctly-scoped JS "once per session" flag. +- **Why not fixed:** the safe fix (converting `showStagger` to a reactive ref explicitly cleared after the animation completes, forcing an actual class removal) is not confined to Discover.vue — the identical pattern is used by `Apps.vue`/`AppCard.vue` (Discover's own navSteps transit partner), `Marketplace.vue`, `Home.vue`, and four Web5 sub-card files. None of those files are in this plan's `files_modified`. A change to this class-timing pattern needs the same real-device visual-regression verification 02-02's original KeepAlive rollout required (that pass's own Task 3 checkpoint caught a real regression on its first attempt for exactly this class of change) — disproportionate to add to an already-large gap-closure plan under time pressure, per the coordinator's own explicit instruction not to open-endedly optimize. +- **Files modified:** None (investigation and documentation only). +- **Destination:** recorded in `02-FINDINGS.md` § Task 3, `STATE.md` Blockers/Concerns, and `REQUIREMENTS.md`'s PERF-03 row as a dedicated follow-up item. + +**2. OpenWrtGateway's final re-measure returned no data; a data-integrity note added to the record instead of silently letting the prior numbers stand unexamined** +- **Found during:** Task 3, after the coordinator flagged that no OpenWrt device has ever been connected to archi-dev-box. +- **Issue:** This run's 5/5 samples failed with a Chromium `Target crashed` error cascading from an unrelated surface (`cloud-folder`) earlier in the same harness run — a genuine harness-execution failure, not a measurement. +- **Resolution:** Recorded as not-measurable, not written in as data. Separately investigated (not assumed) whether the prior baseline/after/remeasure numbers were measuring a real page or an empty/error state: confirmed via source that they measured a genuine, substantive disconnected-state UI ("Connect to Router" form), so the six-surface regression count stands, with the numbers flagged as reflecting one specific code branch. +- **Files modified:** `02-FINDINGS.md` (documentation only). +- **Committed in:** `48a2ff7c` (Task 3 commit). + +--- + +**Total deviations:** 1 auto-fixed (Rule 1, three files sharing one bug class), 2 scope deviations (both investigated and documented, neither silently dropped or silently expanded into new file territory). +**Impact on plan:** The plan's own success criteria are met for three of six surfaces (fixed, real numbers proving it); the other three each have an honest, evidenced account (restored-to-baseline-with-pre-existing-residual, a named-but-out-of-scope second cause, and a not-measurable-this-pass note) rather than a shrug or a silently-inflated "complete" claim. + +## Issues Encountered + +- **A concurrent `cargo build --release` / `cargo test -p archipelago` on this shared box** (another session's FED-08 work) required waiting for CPU contention to clear before taking any timed measurement — confirmed via `pgrep -af 'cargo|rustc'` before each measurement pass, per this plan's own precondition. +- **The coordinator intervened mid-plan** with three follow-up directives after the first re-measure came back: (1) re-investigate OpenWrtGateway rather than continuing to harden its navigation selectors, (2) profile Discover specifically for a second cause, (3) report both the regression-closed bar and the <300ms target bar separately per surface. All three addressed as directed, in this same session, inline (no further background-task waiting). +- **`package-lock.json` showed an unrelated, unstaged diff** (npm's own routine `"peer": true` metadata normalization, triggered by a local `npm` invocation) — left untouched and unstaged, per the shared-tree rule to stage only exact paths this plan edited. + +## User Setup Required + +None for this plan's own execution — `ARCHY_PASSWORD` was supplied out-of-band by the coordinator at runtime, passed only as an environment variable, never written to any file, commit, or this summary. + +## Next Phase Readiness + +- Phase 02's last open verification item (six confirmed timing regressions, none previously fixed) now has: three real fixes proven on real hardware, two honest restored-to-baseline-with-named-residual accounts, and one honestly-unresolved-but-evidenced second cause with a clear scoped destination for follow-up work. +- **Carried forward, not resolved:** Discover's CSS-entrance-animation-replay defect needs a dedicated follow-up plan touching Apps.vue/Marketplace.vue/Home.vue/four Web5 sub-card files, with its own real-device visual-verification checkpoint — tracked in `STATE.md`'s Blockers/Concerns, not silently dropped. +- No blockers for a `gsd-verifier` re-run on Phase 02 from this plan specifically; the phase's own top-level checkbox in `ROADMAP.md` is deliberately left unchecked pending that verification pass's own judgment on whether Discover's outstanding item blocks phase completion or is an acceptable scoped-out follow-up. + +--- +*Phase: 02-ui-performance* +*Completed: 2026-07-31*