diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md index ebd188a0..6de72149 100644 --- a/.planning/REQUIREMENTS.md +++ b/.planning/REQUIREMENTS.md @@ -21,7 +21,7 @@ declared exit criteria (multinode pass + workstreams B/C/F), `.planning/codebase ### UI Performance (PERF) -- [ ] **PERF-01**: The slowest tab switches and secondary-screen opens are profiled with causes named (remount storms, serial RPC waterfalls, uncached fetches) — fixes are targeted, not guessed +- [x] **PERF-01**: The slowest tab switches and secondary-screen opens are profiled with causes named (remount storms, serial RPC waterfalls, uncached fetches) — fixes are targeted, not guessed - [ ] **PERF-02**: Main-tab switches render immediately from cached state with background refresh — no blank screens or long spinners on tabs already visited this session - [ ] **PERF-03**: Secondary screens (screens reached from a tab's main page) open without a blocking full reload and are instant on repeat visits — verified on real node hardware, not just the dev box @@ -100,7 +100,7 @@ Which phases cover which requirements. Updated during roadmap creation. | FED-04 | Phase 1 | Pending | | FED-05 | Phase 1 | Pending | | FED-06 | Phase 1 | Pending | -| PERF-01 | Phase 2 | Pending | +| PERF-01 | Phase 2 | Complete | | PERF-02 | Phase 2 | Pending | | PERF-03 | Phase 2 | Pending | | MNODE-01 | Phase 3 | Pending | @@ -125,6 +125,7 @@ Which phases cover which requirements. Updated during roadmap creation. | MKT-04 | Phase 8 | Pending | **Coverage:** + - v1 requirements: 29 total - Mapped to phases: 29 - Unmapped: 0 diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 44526654..a714dd27 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -73,12 +73,12 @@ Plans: 3. Secondary screens open without a blocking full reload; repeat visits are instant 4. The fixes are verified on real node hardware (not just the dev box) — the sluggishness the user reported is gone on-device -**Plans**: 8 plans +**Plans**: 1/8 plans executed Plans: **Wave 1** -- [ ] 02-01-PLAN.md — Profile every D-09 surface on archi-dev-box and commit the findings doc (PERF-01) +- [x] 02-01-PLAN.md — Profile every D-09 surface on archi-dev-box and commit the findings doc (PERF-01) **Wave 2** *(blocked on Wave 1 completion)* @@ -196,7 +196,7 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 | Phase | Plans Complete | Status | Completed | |-------|----------------|--------|-----------| | 1. Federation & Mesh Hardening | 0/10 | Planned | - | -| 2. UI Performance | 0/8 | Planned | - | +| 2. UI Performance | 1/8 | In Progress| | | 3. Multinode Verification Pass | 0/TBD | Not started | - | | 4. Lifecycle Perfection & Quadlet Default | 0/TBD | Not started | - | | 5. Registry-Distributed Manifests | 0/TBD | Not started | - | diff --git a/.planning/STATE.md b/.planning/STATE.md index db44d35b..b4a6baac 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -2,18 +2,18 @@ gsd_state_version: 1.0 milestone: v1.8.0 milestone_name: milestone -current_phase: 01 -current_phase_name: federation-mesh-hardening +current_phase: 02 +current_phase_name: ui-performance status: executing -stopped_at: Phase 2 context gathered -last_updated: "2026-07-30T08:58:34.941Z" +stopped_at: Completed 02-01-PLAN.md +last_updated: "2026-07-30T10:46:50.979Z" last_activity: 2026-07-30 -last_activity_desc: Phase 01 execution started +last_activity_desc: Phase 02 execution started progress: total_phases: 8 completed_phases: 0 total_plans: 18 - completed_plans: 0 + completed_plans: 1 percent: 0 --- @@ -24,16 +24,16 @@ progress: See: .planning/PROJECT.md (updated 2026-07-29) **Core value:** A third-party developer can publish an app via the signed/decentralized registry and a user can install it on their node — manifest-driven, rootless, secure, robust. -**Current focus:** Phase 01 — federation-mesh-hardening +**Current focus:** Phase 02 — ui-performance ## Current Position -Phase: 01 (federation-mesh-hardening) — EXECUTING -Plan: 1 of 10 +Phase: 02 (ui-performance) — EXECUTING +Plan: 2 of 8 Status: Ready to execute -Last activity: 2026-07-30 — Phase 01 execution started +Last activity: 2026-07-30 — Phase 02 execution started -Progress: [░░░░░░░░░░] 0% +Progress: [█░░░░░░░░░] 6% ## Performance Metrics @@ -48,6 +48,11 @@ Progress: [░░░░░░░░░░] 0% | Phase | Plans | Total | Avg/Plan | |-------|-------|-------|----------| | - | - | - | - | +**Per-Plan Metrics:** + +| Plan | Duration | Tasks | Files | +|------|----------|-------|-------| +| Phase 02 P01 | 100min | 3 tasks | 5 files | ## Accumulated Context @@ -66,6 +71,9 @@ Decisions are logged in PROJECT.md (10 locked ADRs in the `` block + - Phase-3 Quadlet default-flip is gated on the second-node gate reporting clean (do fresh, never stage uncommitted) - Workstream D (DHT distribution) deferred to v2 — design-only backlog - Canonical manifest schema = `core/container/src/manifest.rs` (code wins over spec doc) +- [Phase ?]: Marketplace is the 02-02 tracer tab (worst-measured main tab, 2033ms revisit) matching the user's own 'often app store' complaint +- [Phase ?]: ContainerAppDetails.vue confirmed fully unreachable dead code (no importer, no route) — no serial-RPC-waterfall target exists in the measured D-09 surface set +- [Phase ?]: archi-dev-box UI password was unknown/undiscoverable from this environment — paused at a checkpoint:human-action rather than guessing or falling back to a mock baseline silently ### Pending Todos @@ -99,6 +107,6 @@ None yet. ## Session Continuity -Last session: 2026-07-30T07:50:19.074Z -Stopped at: Phase 2 context gathered -Resume file: .planning/phases/02-ui-performance/02-CONTEXT.md +Last session: 2026-07-30T10:46:50.963Z +Stopped at: Completed 02-01-PLAN.md +Resume file: None diff --git a/.planning/phases/02-ui-performance/02-01-SUMMARY.md b/.planning/phases/02-ui-performance/02-01-SUMMARY.md new file mode 100644 index 00000000..afd2a289 --- /dev/null +++ b/.planning/phases/02-ui-performance/02-01-SUMMARY.md @@ -0,0 +1,186 @@ +--- +phase: 02-ui-performance +plan: 01 +subsystem: ui +tags: [playwright, vue3, performance-profiling, keepalive, rpc-tracing, e2e] + +# Dependency graph +requires: [] +provides: + - "Re-runnable Playwright perf harness (neode-ui/e2e/perf/{surfaces,measure,surface-perf.spec}.ts)" + - "Committed on-device baseline (02-PERF-BASELINE.json) taken against archi-dev-box" + - "D-10 findings doc (02-FINDINGS.md) mapping every measured D-09 surface to a cited cause, fix, and owning plan" +affects: [02-02-app-store-tracer, 02-03-secondary-screens, 02-04-keepalive-lifecycle, 02-05-mesh, 02-06-server-home, 02-07-chat-aiui, 02-08-verify] + +# Tech tracking +tech-stack: + added: [] + patterns: + - "Playwright perf harness: real UI clicks (RouterLink/button) for all navigation, never page.goto between surfaces, so revisit timing reflects genuine Vue Router client-side transitions" + - "Dataset-stamp remount probe: stamp rootSelector's DOM node post-first-visit, read it back post-revisit to detect component-instance reuse vs. destroy/recreate" + - "Sweep-line RPC concurrency derivation (maxConcurrentRpc/rpcWallClockMs) to distinguish serial waterfalls from parallel fan-outs from raw call timing, without re-reading source" + - "Dismiss-and-retry click guard for stray full-screen overlays (app-engagement modals) that can appear mid-navigation on real hardware" + +key-files: + created: + - neode-ui/e2e/perf/surfaces.ts + - neode-ui/e2e/perf/measure.ts + - neode-ui/e2e/perf/surface-perf.spec.ts + - .planning/phases/02-ui-performance/02-PERF-BASELINE.json + - .planning/phases/02-ui-performance/02-FINDINGS.md + modified: [] + +key-decisions: + - "Marketplace is the tracer tab for 02-02 — worst-measured main tab (2033ms revisit) and matches the user's own top complaint ('often app store')" + - "No surface in the measured D-09 set shows a serial-RPC-waterfall signature; ContainerAppDetails.vue (RESEARCH.md's one confirmed waterfall target) is confirmed fully unreachable dead code this session (zero grep matches, no importer, no route entry) — D-13's parallelization pattern has no live target in this phase" + - "3 rows (Marketplace, MarketplaceAppDetails, OpenWrtGateway) have RPC evidence confounded by the harness's navSteps transiting another main tab first, which fires that tab's own onMounted burst; classified conservatively as remount storm rather than uncached fetch, with the confound spelled out for downstream plans to re-verify" + - "Home, Apps, Cloud, Fleet measured already fast on real hardware — left alone per D-02, no useCachedResource conversion planned, only shared KeepAlive wrapping" + - "Mesh and Chat recorded as unmeasured with specific reasons (device-not-connected timeout; AIUI connecting-overlay blocking the close click), never presented as already-fast" + +patterns-established: + - "Perf harness navigation contract: every SURFACES row navigates via real UI clicks (navSteps) with an explicit away step (neutral Settings tab, or a closeSelector for in-page triggers like modals) — page.goto is reserved for the initial goHome() recovery fallback only, never for measured transitions" + +requirements-completed: [PERF-01] + +coverage: + - id: D1 + description: "Re-runnable surface perf harness (SURFACES table, measureSurface(), Playwright spec) covering every D-09 surface plus 4 secondary screens and the located wallet-send modal" + requirement: "PERF-01" + verification: + - kind: e2e + ref: "neode-ui/e2e/perf/surface-perf.spec.ts — verified end-to-end against both the local mock backend and archi-dev-box (real hardware), exit code 0 each time" + status: pass + human_judgment: false + - id: D2 + description: "On-device baseline (02-PERF-BASELINE.json) recorded against archi-dev-box with 13/15 surfaces measuring cleanly, 2 recorded unmeasured with reasons" + requirement: "PERF-01" + verification: + - kind: automated_ui + ref: "Task 2 acceptance script — rows=15, every row has either numeric measurements or a non-empty error string, no neode-ui/src changes, redaction check clean" + status: pass + human_judgment: false + - id: D3 + description: "D-10 findings doc committed, mapping every surface to a measured Primary Cause (from the closed set), Intended Fix, and Owning Plan, before any neode-ui/src change lands in the phase" + requirement: "PERF-01" + verification: + - kind: automated_ui + ref: "Task 3 acceptance script — all 6 required headings present, ContainerAppDetails grep output pasted verbatim, git diff --name-only HEAD~2..HEAD -- neode-ui/src empty" + status: pass + human_judgment: false + +duration: ~100min (includes one human-action checkpoint pause for the archi-dev-box credential) +completed: 2026-07-30 +status: complete +--- + +# Phase 02 Plan 01: Surface Perf Harness & D-10 Findings Summary + +**Built a re-runnable Playwright perf harness, measured 13/15 D-09 surfaces on real archi-dev-box hardware, and committed the D-10 findings doc that names Marketplace (2033ms revisit) as the worst offender and the 02-02 tracer pick — while also confirming ContainerAppDetails.vue (RESEARCH.md's one "confirmed" serial-waterfall target) is fully unreachable dead code, leaving no live waterfall target in this phase.** + +## Performance + +- **Duration:** ~100 min total (includes a human-action checkpoint pause: the harness build/verify was quick, but Task 2 was blocked on the archi-dev-box UI password until the coordinator provided it) +- **Completed:** 2026-07-30 +- **Tasks:** 3/3 completed +- **Files modified:** 5 created (3 harness files, 1 baseline artifact, 1 findings doc), 0 files modified under `neode-ui/src` + +## Accomplishments + +- `neode-ui/e2e/perf/{surfaces,measure,surface-perf.spec}.ts` — a re-runnable Playwright harness covering all 10 D-09 primary surfaces (Home, Apps, Marketplace, Discover, Cloud, Mesh, Server, Web5, Fleet, Chat), 4 secondary screens (AppDetails, MarketplaceAppDetails, CloudFolder, OpenWrtGateway), and the located wallet-send flow (a modal on the Home wallet card — RESEARCH.md found no `Wallet.vue`). Navigation is via real UI clicks (never `page.goto` between surfaces) so revisit timing reflects genuine Vue Router client-side transitions, not full page reloads. +- `.planning/phases/02-ui-performance/02-PERF-BASELINE.json` — committed baseline taken against **archi-dev-box** (real node hardware, the D-11 verification target), 3 runs per surface. 13/15 surfaces measured cleanly; Mesh and Chat recorded unmeasured with specific, cited reasons. +- `.planning/phases/02-ui-performance/02-FINDINGS.md` — the D-10 gate doc. Every row's Primary Cause cites a `02-PERF-BASELINE.json` field. Key findings: 4 main tabs are already fast and left alone; Marketplace is the tracer pick for 02-02; no surface shows a serial-waterfall signature; a measurement confound (RPC bleed-through from an intermediate main-tab hop in 3 rows' nav chains) is documented rather than masked; Wallet-send's revisit being slower than its first visit is flagged as a genuine anomaly for 02-03 to profile directly. + +## Task Commits + +Each task was committed atomically: + +1. **Task 1: Build the re-runnable surface perf harness** - `a75b6709` (feat) +2. **Task 2: Record the on-device baseline from archi-dev-box** - `36145140` (fix — includes the baseline artifact plus harness hardening discovered while running it against real hardware) +3. **Task 3: Write and commit the D-10 findings doc** - `675deb65` (docs) + +_No separate plan-metadata commit was made for this SUMMARY — this file, STATE.md, and ROADMAP.md are committed together in the final commit below._ + +## Files Created/Modified + +- `neode-ui/e2e/perf/surfaces.ts` - SURFACES table: one row per D-09 surface + secondary screens + wallet-send, with real-click `navSteps`, `contentSelector`, `rootSelector` +- `neode-ui/e2e/perf/measure.ts` - `measureSurface()`: first-visit/revisit timing, RPC trace, remount probe, sweep-line concurrency derivation, dismiss-and-retry overlay guard +- `neode-ui/e2e/perf/surface-perf.spec.ts` - Playwright spec: logs in via the existing `app-launch.spec.ts` flow, walks every surface, writes the JSON artifact +- `.planning/phases/02-ui-performance/02-PERF-BASELINE.json` - Committed on-device baseline (archi-dev-box, 3 runs/surface) +- `.planning/phases/02-ui-performance/02-FINDINGS.md` - D-10 findings doc: per-surface cause table, ranked fix order, surfaces left alone, corrections to prior research, owning-plan map + +## Decisions Made + +- **Marketplace is the 02-02 tracer tab** — worst-measured main tab (2033ms revisit) and matches the user's specific complaint ("often app store"). +- **No serial-RPC-waterfall Primary Cause was assigned anywhere** — every surface with 2+ RPC calls on revisit showed `maxConcurrentRpc` at or near its total call count (already parallel). `ContainerAppDetails.vue`, RESEARCH.md's one confirmed waterfall target, is confirmed unreachable dead code this session (`grep -rn "ContainerAppDetails" neode-ui/src` returns nothing — no importer, no route entry). +- **3 rows' RPC evidence is flagged as confounded**, not silently trusted: Marketplace, MarketplaceAppDetails, and OpenWrtGateway all reach their target via a `navSteps` chain that transits another main tab first (Home or Server), whose own `onMounted` RPC burst is still resolving when the harness's tracker starts recording — captured method names for OpenWrtGateway are an exact subset of Server.vue's own known call set, confirming the confound rather than assuming it. These rows are classified conservatively (remount storm) with the confound spelled out for 02-02/02-03 to re-verify with DevTools. +- **Home, Apps, Cloud, Fleet are left alone (D-02)** — all measured sub-500ms revisit with 0-1 trivial RPC calls; no `useCachedResource` conversion planned for their data layer, only the shared `` wrapping from 02-04. +- **Wallet-send's anomaly (revisit consistently slower than first-visit, 2607ms vs 735ms median, across all 3 runs) is flagged explicitly** rather than smoothed over, since it has zero RPC either time and needs direct DevTools profiling in 02-03 to explain. +- **Auth gate handled via checkpoint, not guessed:** archi-dev-box's real UI password was unknown and not discoverable from this environment (checked `scripts/deploy-config.sh` — gitignored, not present on this machine; no plaintext credential found via SSH). Rather than falling back to the mock-backend baseline silently or guessing a password, the plan paused at a `checkpoint:human-action` and resumed once the coordinator supplied the credential. The password itself was never written to any committed file — passed only via the `ARCHY_PASSWORD` environment variable at runtime. + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 1 - Bug] Cascading harness failures from an unhandled Companion-app auto-show overlay** +- **Found during:** Task 1 verification (local mock-backend run) +- **Issue:** A once-per-browser Companion-app intro overlay (`CompanionIntroOverlay.vue`, opened via `useCompanionIntro`'s auto-show gate) can appear mid-click on the Home page, intercepting every subsequent sidebar click for the rest of the run — one surface's stray UI cascaded failures into every remaining surface. +- **Fix:** Added `dismissOverlays()` (Escape key + any `[aria-label*="Close" i]` button inside a dialog/full-screen overlay) called before every navigation click, wrapped in a `clickWithGuard()` retry loop (3 attempts, dismissing between each) since the overlay can appear after the initial dismiss check but before Playwright's own actionability wait resolves. +- **Files modified:** `neode-ui/e2e/perf/measure.ts` +- **Verification:** Re-ran the harness against the mock backend; surfaces after the overlay's first appearance (previously 100% failure) measured cleanly. +- **Committed in:** `a75b6709` (Task 1 commit) + +**2. [Rule 1 - Bug] Fleet's "Fleet" link selector matched a CSS-hidden duplicate** +- **Found during:** Task 1 verification +- **Issue:** `Web5Federation.vue` renders two "Fleet" `RouterLink`s — one inside `.web5-card-actions-top` (permanently `display: none` per a project-wide "compact header variants are retired" CSS rule) and the real, visible one inside `.web5-card-actions-bottom-grid`. `a:has-text("Fleet")` with `.first()` matched the hidden copy. +- **Fix:** Scoped the selector to `.web5-card-actions-bottom-grid a:has-text("Fleet")`. +- **Files modified:** `neode-ui/e2e/perf/surfaces.ts` +- **Verification:** Fleet measured cleanly in the next mock-backend run. +- **Committed in:** `a75b6709` (Task 1 commit) + +**3. [Rule 1 - Bug] Chat's hidden sidebar broke every subsequent surface's recovery navigation** +- **Found during:** Task 1 verification, then again (worse) against real archi-dev-box hardware in Task 2 +- **Issue:** `DashboardSidebar.vue` is `v-show="!chatFullscreen"` — once on `/dashboard/chat`, the sidebar (and the neutral Settings link every other surface's "away" step and every surface's `goHome()` depend on) is invisible. On real hardware, AIUI's own "connecting" overlay could also outlive the harness's close-button click budget, leaving the run permanently stuck on Chat with a hidden sidebar and no way back — every surface measured after Chat failed. +- **Fix:** Gave Chat its own `closeSelector` (`.chat-close-btn`, which calls `closeChat()`'s `router.back()`) instead of the generic Settings-tab away-step. Added a hard `page.goto('/dashboard')` recovery fallback inside `goHome()` for the case where even the close button is unreachable — an explicit, narrowly-scoped exception to the "no `page.goto` between surfaces" rule, used only to break out of a stuck state, never to measure one. +- **Files modified:** `neode-ui/e2e/perf/surfaces.ts`, `neode-ui/e2e/perf/measure.ts` +- **Verification:** Re-ran against archi-dev-box; all surfaces after Chat (previously cascading to 100% failure) measured cleanly, while Chat itself remains honestly recorded as unmeasured (AIUI's real connection latency exceeds the harness's budget on this hardware — a genuine finding, not a harness bug). +- **Committed in:** `36145140` (Task 2 commit) + +**4. [Rule 1 - Bug] `dismissOverlays()` prematurely closed the exact dialog a `closeSelector` step meant to close** +- **Found during:** Task 1 verification +- **Issue:** `dismissOverlays()` treats any `[aria-label*="Close" i]` button inside a dialog as a stray overlay to dismiss. When the away-step's own deliberate close click (`surface.closeSelector`) was routed through the same `clickWithGuard()` helper, `dismissOverlays()` closed the modal a beat before the explicit click ran, leaving that click with nothing to find (0 matches, immediate failure) for both Wallet-send and Chat. +- **Fix:** The `closeSelector` away-step click is a plain `page.locator(...).click()`, not routed through `clickWithGuard`/`dismissOverlays`. +- **Files modified:** `neode-ui/e2e/perf/measure.ts` +- **Verification:** Wallet-send and Chat's away-steps stopped failing on a 0-match locator. +- **Committed in:** `a75b6709` (Task 1 commit) + +**5. [Rule 1 - Bug] Run header's `commit` field always recorded 'unknown'** +- **Found during:** Task 2, comparing the baseline artifact's header against Task 2's own acceptance criteria (`baseUrl`, `takenAt`, `commit`, `runs`, `notes` must all be recorded) +- **Issue:** `currentCommit()` shelled out to `git rev-parse --short HEAD` using `{ cwd: __dirname }`, but `__dirname` is unavailable under `neode-ui`'s `"type": "module"` ESM runtime — the resulting `ReferenceError` was silently swallowed by the existing catch block, always yielding `'unknown'`. +- **Fix:** Use `process.cwd()` instead (Playwright always sets this to the project root it was invoked from). +- **Files modified:** `neode-ui/e2e/perf/surface-perf.spec.ts` +- **Verification:** Re-ran against archi-dev-box; the artifact's header now records the real short commit hash (`a75b6709`). +- **Committed in:** `36145140` (Task 2 commit) + +--- + +**Total deviations:** 5 auto-fixed (all Rule 1 — bugs found and fixed during the harness's own verification passes, not scope creep against the plan's task boundaries). +**Impact on plan:** All five were necessary for the harness to actually produce a trustworthy, non-cascading, correctly-labeled baseline. None touched `neode-ui/src`. + +## Issues Encountered + +- **Authentication gate on Task 2:** archi-dev-box's real UI password was unknown and not derivable from this environment (the standard dev/test password used elsewhere in the repo, `password123`, was rejected). Checked for a scripted credential (`scripts/deploy-config.sh`, gitignored, documented by `scripts/deploy-config.example`) — the actual file doesn't exist on this machine (likely lives on the ThinkPad build server per project memory). Stopped and returned a `checkpoint:human-action` rather than falling back to a mock-backend baseline silently or attempting to reset the node's stored credential (out of scope, and risky on a shared dev node). The coordinator supplied the real password; execution resumed immediately and completed Tasks 2–3 without further blockers. +- **Measurement confound discovered and documented, not hidden:** while writing the findings doc, comparing captured RPC method names across surfaces revealed that 3 rows' navigation chains transit another main tab before reaching their target, causing that intermediate tab's own `onMounted` RPC burst to be misattributed to the destination surface. Rather than presenting the raw (confounded) RPC counts as clean "uncached fetch" evidence — which the plan's own prohibitions explicitly forbid ("MUST NOT present inferred, code-read, or cherry-picked numbers as measured profiling results") — those 3 rows were reclassified conservatively and the confound spelled out for downstream plans. + +## User Setup Required + +None — no external service configuration required. The archi-dev-box password used for this run was supplied out-of-band by the coordinator and passed only via the `ARCHY_PASSWORD` environment variable at runtime; it is not stored in any file in this repository. + +## Next Phase Readiness + +- `.planning/phases/02-ui-performance/02-FINDINGS.md` is committed and ready for 02-02 through 02-08 to consume — each has a clear owning-plan assignment, a cited Primary Cause, and (where applicable) an explicit measurement caveat to re-verify before implementing. +- The harness (`neode-ui/e2e/perf/`) is re-runnable as-is for 02-08's after-artifact comparison pass — no changes needed for that later plan to reuse it against the same archi-dev-box target. +- Two open items carry forward: **Mesh** needs its own on-node profiling pass during 02-05 (this session's device-not-connected timeout may not reflect steady-state behavior); **Chat**'s AIUI connection latency on real hardware (long enough to block even the close-button click) is itself a data point worth 02-07 investigating directly, separate from the two small D-14 UX defaults already scoped there. + +--- +*Phase: 02-ui-performance* +*Completed: 2026-07-30*