# Phase 02 UI Performance — Findings (D-10) **Status:** Gate doc — committed before any `neode-ui/src` change lands in this phase (D-10). Every claim below cites a field in `02-PERF-BASELINE.json`; nothing here is inferred from code reading alone. ## Method **Target:** archi-dev-box (the on-device verification target named by D-11), resolved to `http://archi-dev-box` from the executing workstation. This is a real Archipelago node, not the local mock backend or the `:8100` dev preview — `02-PERF-BASELINE.json`'s run header records `baseUrl: "http://archi-dev-box"` and `commit: "a75b6709"` (the harness commit the run was taken against). **Harness:** `neode-ui/e2e/perf/{surfaces,measure,surface-perf.spec}.ts` (built and committed in this plan's Task 1). Run command: ```bash cd neode-ui ARCHY_BASE_URL=http://archi-dev-box \ ARCHY_PASSWORD= \ ARCHY_PERF_OUT=../.planning/phases/02-ui-performance/02-PERF-BASELINE.json \ npx playwright test e2e/perf/surface-perf.spec.ts --project=chromium --reporter=line ``` **Sample count:** 3 runs per surface (`runs: 3` in the artifact header), each run measuring a fresh first-visit (navigate from dashboard Home to the surface) and a revisit (navigate to the neutral Settings tab — or close the surface's own trigger, for the wallet-send modal — then return via the same UI-click path) in the same authenticated session. `02-PERF-BASELINE.json`'s `firstVisitMs`/`revisitMs`/`revisitRpcCount` top-level fields are the **median** across successful runs; `samples` holds every individual run (including any per-run errors) so no data is discarded. **Field meanings:** - `firstVisitMs` / `revisitMs` — wall-clock ms from the click that starts navigation to the surface's `contentSelector` becoming visible. - `revisitRpcCount` — number of POSTs to `/rpc/v1` observed during the revisit window (method name + timing only, never bodies — see the harness's threat mitigation). - `maxConcurrentRpc` — peak number of RPC calls in flight simultaneously during the revisit, derived by sweep-line over each call's `[start, start+duration]` interval. A waterfall shows `maxConcurrentRpc: 1` with 2+ calls; a parallel fan-out shows `maxConcurrentRpc` close to the total call count. - `remounted` — read back from a dataset-stamp on the view's root element after the away/back round-trip. `true` means the component instance was destroyed and recreated (expected everywhere right now — `grep -r "KeepAlive" neode-ui/src` returns 0 hits, confirmed again this session). **All 15 SURFACES rows were run against archi-dev-box.** 13 of 15 produced clean first-visit/revisit/RPC/remount numbers. 2 are recorded as `unmeasured` with the specific reason (never as "already fast" — see the Mesh and Chat rows below). **A methodological caveat that materially affects three rows' RPC evidence:** several `navSteps` chains reach their target surface by first clicking through another main tab (Marketplace and its own AppDetails go through Home's "Browse Store" link; OpenWrtGateway goes through Server's "OpenWrt Gateway" link). Home.vue and Server.vue both fire a burst of RPCs from their own `onMounted` the instant they're briefly mounted mid-chain (`system.stats`, `fips.status`, `vpn.status`/`vpn.list-peers`, `wallet.*`, `network.list-interfaces`, `tor.list-services`, etc.). Those in-flight requests are still resolving when the chain lands on the destination surface, and the harness's RPC tracker — which starts recording at the top of the revisit click-chain, not at the destination surface's own mount — captures them as if they belonged to the destination. This was confirmed by comparing captured method names directly: OpenWrtGateway's captured set (`vpn.list-peers`, `network.list-interfaces`, `tor.list-services`, `fips.status`) is an exact subset of Server.vue's own 8-call set measured cleanly on the `server` row (whose `navSteps` do **not** transit another tab). Where this confound applies, the affected row's Primary Cause is classified conservatively (`remount storm`, the reading the elevated `revisitMs` alone supports) rather than `uncached fetch`, and the confound is called out explicitly in that row's Intended Fix column so 02-02/02-03 re-verify with DevTools before assuming the RPC count is real. Rows whose `navSteps` reach their target directly (Apps, Cloud, Mesh, Server, Web5, Chat, AppDetails, CloudFolder) have no such confound — their RPC counts are trustworthy as measured. ## Per-Surface Findings | Surface | First visit (ms, median) | Revisit (ms, median) | Revisit RPC count | Max concurrent RPC | Remounted | Primary cause | Intended fix | Owning plan | |---|---|---|---|---|---|---|---|---| | Home (wallet figures) | 354 | 390 | 1 | 1 | true | already fast | Left alone (D-02) — still gets wrapped in `` per D-01 for scroll/animation-state persistence, no data-cache conversion needed | 02-04 (KeepAlive wrapping only) | | Apps (My Apps) | 359 | 267 | 1 | 1 | true | already fast | Left alone (D-02) — KeepAlive wrapping only | 02-04 (KeepAlive wrapping only) | | Marketplace (App Store) | 1228 | 2033 | 13 | 12 | true | remount storm | KeepAlive + convert to `useCachedResource`; **RPC count is confounded by the Home-transit artifact above — do not treat 13 calls as Marketplace's own fetch pattern without re-verifying via DevTools** | 02-02 (tracer/app store) | | Discover (App Store tab) | 1117 | 1083 | 1 | 1 | true | remount storm | Convert to `useCachedResource` alongside Marketplace (same app-store surface family, D-09) | 02-02 (tracer/app store) | | Cloud / Files | 463 | 247 | 0 | 0 | true | already fast | Left alone (D-02) — already on `useCachedResource` per code read; still gets KeepAlive wrapping | 02-04 (KeepAlive wrapping only) | | Mesh | — | — | — | — | — | **unmeasured** | `.mesh-status-grid` never left the hidden/loading branch within the 20s wait window across all 3 runs — the device did not report `connected` on this node in the window measured. Re-run 02-05's own on-node pass once implementing; do not assume already-fast | 02-05 (Mesh) | | Server (Network) | 969 | 738 | 8 | 7 | true | uncached fetch | Convert Server's 7–8 independent fire-and-forget `onMounted` calls to `useCachedResource` (clean measurement — `navSteps` reach Server directly, no transit confound) | 02-06 (Server and Home) | | Web5 | 307 | 566 | 0 | 0 | true | remount storm | KeepAlive wrapping — 0 RPC on revisit confirms the sub-cards' existing `useCachedResource` usage already prevents refetch; remaining cost is render/mount of the multi-card tree | 02-04 (KeepAlive wrapping only) | | Fleet | 376 | 330 | 0 | 0 | true | already fast | Left alone (D-02) — still gets KeepAlive wrapping | 02-04 (KeepAlive wrapping only) | | Chat (AIUI) | — | — | — | — | — | **unmeasured** | AIUI's own "connecting" overlay stayed up long enough on this node that the harness's close-button click never resolved within its retry budget (`.chat-close-btn` timeout, 20s) in run 0; runs 1–2 timed out even earlier at 6.6s once the harness's stuck-state recovery kicked in. This is itself a data point worth carrying into 02-07: AIUI's connection/handshake latency on real hardware is materially longer than on the dev workstation | 02-07 (Chat/AIUI) | | AppDetails (secondary) | 1040 | 1204 | 2 | 1 | true | uncached fetch | Convert `loadBitcoinSync()`/`loadCredentials()` to keyed `useCachedResource` (clean measurement — `navSteps` transit Apps, not Home, no confound) | 02-03 (secondary screens) | | MarketplaceAppDetails (secondary) | 1978 | 1793 | 14 | 12 | true | remount storm | Convert to keyed `useCachedResource`; **13 of the 14 captured calls match the same Home-transit confound as Marketplace — only `package.versions` (the 14th call, firing later at +1459ms, well after the Home-mount burst settles) is trustworthy as this view's own fetch** | 02-03 (secondary screens) | | CloudFolder (secondary) | 5116 | 1212 | 0 | 0 | true | remount storm | First visit is consistently ~5s (lazy route-chunk cold load, confirmed across runs); revisit drops to ~1.2s once the chunk is warm client-side but stays remount-bound (0 RPC either way) — convert to keyed `useCachedResource` for the folder listing regardless, per D-04's secondary-screen pattern | 02-03 (secondary screens) | | OpenWrtGateway (secondary) | 3213 | 663.5 | 4 | 4 | true | remount storm | **RPC count is confounded by the Server-transit artifact above — the captured methods are an exact subset of Server.vue's own call set, not evidence of OpenWrtGateway's own fetch pattern.** Re-verify with DevTools before deciding whether this page needs its own `useCachedResource` conversion | 02-03 (secondary screens) | | Wallet / send flow (Home wallet card → SendBitcoinModal) | 735 | 2607 | 0 | 0 | true | remount storm | **Anomaly worth flagging, not hiding:** revisit (2607ms median) is consistently *slower* than first visit (735ms median) across all 3 runs (2562/2619/2607 vs 857/735/607) — the modal always fully remounts (`BaseModal`'s `v-if`) with zero RPC either time, so the extra ~1.9s on reopen is pure client-side cost (fee/balance recompute, store re-subscription, or similar) that should be profiled directly with DevTools during 02-03, not assumed away | 02-03 (secondary screens) | ## Ranked Fix Order Worst-revisit-first, main tabs and secondary screens together (medians from the table above; `—` rows excluded, listed separately below): 1. **Wallet-send — 2607ms** (secondary/modal; anomalous revisit > first-visit, see note above) 2. **Marketplace — 2033ms** (main tab; RPC evidence confounded, see caveat) 3. **MarketplaceAppDetails — 1793ms** (secondary; RPC evidence confounded, see caveat) 4. **CloudFolder — 1212ms** (secondary; first-visit-dominant, ~5s cold chunk load) 5. **AppDetails — 1204ms** (secondary; clean uncached-fetch evidence) 6. **Discover — 1083ms** (secondary; remount storm) 7. **Server — 738ms** (main tab; clean uncached-fetch evidence) 8. **OpenWrtGateway — 663.5ms** (secondary; RPC evidence confounded) 9. **Web5 — 566ms** (main tab; remount storm, 0 RPC) 10. **Home — 390ms** (main tab; already fast) 11. **Fleet — 330ms** (main tab; already fast) 12. **Apps — 267ms** (main tab; already fast) 13. **Cloud — 247ms** (main tab; already fast) Unmeasured: **Mesh**, **Chat** — see their rows above for why, and do not slot them into this ranking as if they were fast. **Tracer tab for plan 02-02:** among *main tabs* specifically (02-02's actual scope — wallet-send and the secondary screens are not tabs), **Marketplace is the worst-performing surface (2033ms revisit)** and is also the surface the user named first ("often app store" — D-09). It is the tracer pick for 02-02, consistent with the plan set's existing "02-02 tracer/app store" naming. ## Surfaces Left Alone (D-02) These measured `already fast` on real hardware — no `useCachedResource` conversion is planned for their data layer. Per D-01 they still get wrapped in `` as part of 02-04's shared Dashboard.vue restructure (scroll/animation-state persistence is free and harmless even for an already-fast view), but that is template plumbing, not a per-view data-caching change, so they are listed here rather than under any per-surface fix plan. | Surface | First visit (ms) | Revisit (ms) | Revisit RPC | Why left alone | |---|---|---|---|---| | Home | 354 | 390 | 1 | Sub-400ms revisit with a single trivial `system.get-hostname` ping (not view-specific wallet data); wallet figures render from already-reactive store state | | Apps | 359 | 267 | 1 | Sub-300ms revisit, single trivial ping; confirms RESEARCH.md's prediction that Apps.vue's data is WebSocket-pushed, not per-mount-fetched | | Cloud | 463 | 247 | 0 | Sub-250ms revisit, zero RPC — confirms Cloud.vue's existing `useCachedResource` usage (`peersResource`/`countsResource`) is already preventing refetch | | Fleet | 376 | 330 | 0 | Sub-350ms revisit, zero RPC | ## Corrections to Prior Research **`ContainerAppDetails.vue` dead-code claim — CONFIRMED, stronger than RESEARCH.md stated.** RESEARCH.md's Assumptions & Flagged Items (FA-B) reported the planner's grep found "only a self-referential comment inside the file itself." Re-running the identical grep this session found **zero matches at all**, including no self-reference: ``` $ grep -rn "ContainerAppDetails" neode-ui/src (no output) ``` The file exists at `neode-ui/src/views/ContainerAppDetails.vue` (351 lines) but has no importer anywhere in `neode-ui/src` and no route entry in `neode-ui/src/router/index.ts` (verified directly against the full route table read for this plan's Task 1). It is unreachable dead code. **Impact:** RESEARCH.md named `ContainerAppDetails.vue:169-172`'s `await loadContainer(); await loadLogs(); await loadHealthStatus()` as the phase's one *confirmed* serial-RPC-waterfall fix target (Pattern 3). Since the file is unreachable, **no plan in this phase should spend effort parallelizing it** — doing so would touch dead code for zero user-visible benefit. More importantly: **no other surface in the measured D-09 set shows a serial-waterfall signature.** Every surface in this findings doc with 2+ RPC calls on revisit (Marketplace, Server, AppDetails, MarketplaceAppDetails, OpenWrtGateway) shows `maxConcurrentRpc` at or near its total call count (7 of 8, 12 of 13, 12 of 14, 1 of 1-2, 4 of 4) — i.e. already parallel, not serial. **`serial RPC waterfall` is not used as a Primary Cause anywhere in the Per-Surface Findings table above** because the measured evidence does not support it for any reachable surface. This is the single most consequential correction this profiling pass makes to RESEARCH.md: D-13's `Promise.all`/`Promise.allSettled` parallelization pattern has no live target to apply to in this phase's D-09 surface set. ## Owning Plans | Surface | Owning plan | |---|---| | Home | 02-04 (KeepAlive wrapping only — see Surfaces Left Alone) / 02-06 names Home in its title but the profiling shows no data-layer work is needed there | | Apps | 02-04 (KeepAlive wrapping only) | | Marketplace | 02-02 (tracer/app store) | | Discover | 02-02 (tracer/app store) | | Cloud | 02-04 (KeepAlive wrapping only) | | Mesh | 02-05 (Mesh) | | Server | 02-06 (Server and Home) | | Web5 | 02-04 (KeepAlive wrapping only) | | Fleet | 02-04 (KeepAlive wrapping only) | | Chat | 02-07 (Chat/AIUI) | | AppDetails | 02-03 (secondary screens) | | MarketplaceAppDetails | 02-03 (secondary screens) | | CloudFolder | 02-03 (secondary screens) | | OpenWrtGateway | 02-03 (secondary screens) | | Wallet / send flow | 02-03 (secondary screens) | The shared foundation both 02-02 and every other per-surface plan depend on — the Dashboard.vue nested-`` KeepAlive restructure and the `useCachedResource` `onActivated` extension — is owned by **02-04**, per RESEARCH.md's own recommendation to land that shared work before converting individual views. ## Results **Method:** Re-ran `neode-ui/e2e/perf/{surfaces,measure,surface-perf.spec}.ts` **unmodified** (no harness file was edited to produce these numbers) against `archi-dev-box` — the same target, same `runs: 3`, same `ARCHY_PERF_OUT` contract as the baseline. Run header: `baseUrl: "http://archi-dev-box"`, `commit: "3ee20430"`, `runs: 3`, taken `2026-07-31T01:27:30.384Z` (baseline was taken `2026-07-30T10:30:40.048Z` — a different time of day on the same physical node; see the timing-variance note under Outstanding). 15/15 rows present (same count as baseline); Mesh and Chat are again `unmeasured`, for reasons recorded per-row below. **A new methodology finding, discovered and independently cross-verified this run (not present at baseline time because no `` existed then):** `SURFACES.rootSelector` for every main tab is the generic class `.view-container`, which `DashboardRouterView.vue` falls through onto *every* routed view's own root (`` — see `dashboardViewWrappers.ts`/`DashboardRouterView.vue`). Before this phase, only one view was ever mounted at a time, so `.view-container` was unambiguous. Now that multiple main tabs are simultaneously KeepAlive-cached (up to `KEEP_ALIVE_MAX`), **more than one `.view-container`-classed element can coexist in the document at once**, and `measure.ts`'s `stampRoot`/`readRootProbe` (`document.querySelector`, first DOM match) can silently stamp/read a *different* cached instance than the one actually being tested — confirmed directly by DOM inspection (`document.querySelectorAll('.view-container').length` returning 2 on a fresh visit to Marketplace, and the same call's *return order* changing between the stamp step and the read-back step as Vue relocates deactivated subtrees into its internal KeepAlive storage). This was not something I could fix by editing the harness (out of this task's `files_modified`, and re-running the historical baseline commit to keep the pair comparable would be invasive for a field — `remounted` — whose baseline value is trivially `true` everywhere pre-KeepAlive regardless of selector precision). Instead, for every main-tab row I independently re-verified `remounted` with a corrected, harness-independent method: stamp/read the specific `.view-container` ancestor of the surface's own **visible** `contentSelector` match (`getBoundingClientRect`/`offsetParent`, since KeepAlive's inactive cached instances are not laid out on screen) rather than the first DOM match. Reproduced twice for Server specifically to rule out a fluke. The **"Remounted (verified)"** column below is this corrected reading; the **"Remounted (raw)"** column is the harness artifact's own (sometimes confounded) field, kept visible rather than silently replaced. | Surface | Kind | Baseline revisit ms | After revisit ms | Baseline RPC | After RPC | Remounted (raw, baseline→after) | Remounted (verified) | Verdict | |---|---|---|---|---|---|---|---|---| | Home | main-tab | 390 | 395 | 1 | 1 | true → true | **false (survives)** | improved — remount fixed; ms was already fast (D-02) and unaffected | | Apps | main-tab | 267 | 184 | 1 | 1 | true → true | **false (survives)** | improved — faster ms and remount fixed | | Marketplace | main-tab | 2033 | 683 | 13 | 8 | true → true | **false (survives)** | improved — 66% faster and remount fixed; RPC count still confounded by the Home-transit artifact 02-01 documented, unchanged this run | | Discover | secondary (cache-registered) | 1083 | 1257 | 1 | 1 | true → true | not independently re-verified | regressed — revisit got slower; raw remounted unchanged | | Cloud | main-tab | 247 | 129 | 0 | 0 | true → true | **false (survives)** | improved — faster ms and remount fixed | | Mesh | main-tab | — | — | — | — | — | — | **unmeasured** — `.mesh-status-grid` stayed hidden 20s across all 3 runs; device not reporting `connected` in this window, identical reason to baseline | | Server | main-tab | 738 | 849 | 8 | 2 | true → true | **true (genuinely remounts)** | **regressed** — slower ms AND the KeepAlive promise is not delivered for this tab, confirmed by two independent reproductions of the corrected probe. RPC count did drop (8→2, real `useCachedResource` benefit), but the surface still fully remounts on revisit | | Web5 | main-tab | 566 | 709 | 0 | 0 | true → true | **false (survives)** | regressed on ms despite remount being fixed — a split signal (0 RPC either time, so the added ~140ms is pure client-side re-render/re-activation cost on an instance that isn't actually being destroyed); flagged for direct profiling, not fixed here | | Fleet | main-tab | 330 | 1054 | 0 | 0 | true → true | **false (survives)** | **regressed, prominently** — 3.2× slower on revisit despite the instance surviving and zero RPC either time; the same split-signal pattern as Web5 but much larger. Reached via a 2-hop chain through Web5 first, so some of this may be transition-chain noise, but the raw number is recorded as measured, not adjusted | | Chat | main-tab | — | — | — | — | — | — | **unmeasured** — but for a *different* specific reason than baseline: baseline's block was the AIUI connecting-overlay outliving the close-button click budget; this run's block is the disk-usage `HealthNotifications.vue` toast (see below) intercepting `.chat-close-btn` for the same reason it now interferes elsewhere. Recorded as unmeasured either way, per the plan's own rule that an unmeasured surface is never presented as improved | | AppDetails | secondary | 1204 | 1510 | 2 | 1 | true → true | not independently re-verified (expected to remount; D-04 doesn't cache secondary screens) | regressed — revisit got slower despite one fewer RPC call | | MarketplaceAppDetails | secondary | 1793 | 1532 | 14 | 8 | true → **false** | not independently re-verified | improved on ms; the raw `remounted: false` reading is very likely the same rootSelector-ambiguity artifact described above (its `navSteps` transit a KeepAlive-cached Marketplace instance immediately before reaching the detail page) rather than evidence this secondary screen is now incorrectly instance-cached — flagged, not asserted either way. RPC count improvement is still confounded per 02-01's original note (13 of 14 baseline calls were Home-transit bleed-through; only `package.versions` was trustworthy) | | CloudFolder | secondary | 1212 | 985 | 0 | 0 | true → true | not independently re-verified | improved — faster ms, 0 RPC both times as before | | OpenWrtGateway | secondary | 663.5 | 1148 | 4 | 2 | true → true | not independently re-verified | regressed — revisit got slower; RPC count still confounded per 02-01's original note (captured methods are a subset of Server's own call set) | | Wallet / send flow | secondary (modal) | 2607 | 2556 | 0 | 0 | true → true | not independently re-verified (modal always remounts via `v-if`, by design) | unchanged — the anomaly 02-01 flagged (revisit slower than first-visit, pure client-side cost, 0 RPC) persists; no plan's `files_modified` touched `SendBitcoinModal.vue` (see Outstanding) | ## Outstanding - **Server.vue does not survive a tab round-trip on real archi-dev-box hardware**, despite being registered in `KEEP_ALIVE_PATHS` since 02-04 and 02-04's own Task 3 checkpoint reporting it as approved. Confirmed genuinely (not a measurement artifact) via two independent reproductions of a corrected, harness-independent remount probe. This directly violates this plan's own must-have truth ("every main-tab path registered in `KEEP_ALIVE_PATHS` has `After remounted` false"). Root cause not investigated here — Task 2's scope is measurement, not per-view debugging, and this is a pre-existing 02-04 behavior, not something this plan's own files touched. **Needs a follow-up plan or a targeted fix to Server.vue/its KeepAlive wrapper registration before PERF-02's per-tab claim can be considered fully verified for this specific tab.** - **Web5 and (especially) Fleet revisit ms regressed significantly despite their component instances now genuinely surviving the round-trip** (confirmed via the corrected probe) and zero RPC calls either visit. This is a split signal: the KeepAlive architecture is working (no remount, no refetch) but something client-side is adding real wall-clock cost on reactivation — Fleet's 330ms→1054ms (3.2×) is the more severe of the two. Not profiled or fixed here; flagged for direct DevTools/Performance-panel investigation, the same class of work 02-01 already flagged for Wallet-send's anomaly and never resolved. - **Discover, AppDetails and OpenWrtGateway all regressed on revisit ms** without an independent remount re-verification (not main tabs subject to the plan's KeepAlive must-have truth, so not re-probed under time constraints); OpenWrtGateway's RPC count is still confounded per 02-01's original note (its captured methods are a subset of Server's own call set from the Server-transit in its `navSteps`) and needs DevTools re-verification before treating the RPC drop as this screen's own fix rather than confound noise shifting. - **MarketplaceAppDetails's raw `remounted: false` reading is very likely a rootSelector- ambiguity artifact**, not evidence this secondary screen picked up incidental instance caching — flagged, not independently re-verified given time constraints. If a future plan needs to confirm D-04 (secondary screens are NOT instance-cached) still holds for this screen specifically, re-run the corrected probe method documented above against it directly. - **Wallet-send's anomaly (revisit slower than first-visit, 0 RPC either time) is still unaddressed** — 02-03 reported it as an unplanned-item gap (owned by 02-03 per this findings doc, but `SendBitcoinModal.vue` never appeared in any plan's `files_modified`). Carried forward unresolved. - **CloudFolder.vue's residual TTL gap**: `cloudStore.navigate()` (`cloud.ts`) still always re-fetches on revisit — it just doesn't block paint. 02-03 explicitly left this as a gap (outside its `files_modified`) rather than bolting on a second, parallel cache. Still open. - **PeerFiles.vue** was found by 02-03 to not actually use `useCachedResource` (contrary to that plan's own `read_first` assumption) and force-refetches unconditionally on every mount with the same loading/refreshing conflation bug 02-03 fixed elsewhere. Flagged by 02-03, not fixed by any plan since. Still open. - **`/dashboard/settings` remains withheld from `KEEP_ALIVE_PATHS`** (02-04's own deliberate exclusion, unaudited risk — `SystemDangerZone.vue`'s reboot poll, several `onMounted`-only fetches in its child sections). Not part of this phase's scope to resolve; carried forward as a known gap for whichever future plan audits Settings. - **`ContainerAppDetails.vue`** reconfirmed fully unreachable dead code (02-01, re-confirmed 02-03). A candidate for a follow-up cleanup deletion, not part of this phase. - **archy-x250-dev (the dev pair's second node) was offline** for both this plan's deploy (Task 1) and this re-measurement — `tailscale status` showed it "offline, last seen 2h ago" at Task 1 and unchanged when re-checked before Task 2. Only `archi-dev-box` received this phase's build and was measured. D-11's verification target is `archi-dev-box` specifically, so this does not block the plan, but the second dev-pair node has not received this phase's changes at all and should get the same `--frontend-only` deploy once it's back online. - **Chat/Mesh remain unmeasured**, same as baseline for Mesh (device not reporting `connected`); Chat's specific blocking cause changed from an AIUI-connection timeout (baseline) to a disk-usage toast intercepting the close button (this run) — both are environmental/UI-affordance issues rather than something either run's numbers can speak to. The disk-usage toast (`HealthNotifications.vue`, no `pointer-events: none` on its `.fixed.right-4.z-[200]` wrapper) intercepting clicks elsewhere on the page is itself worth a follow-up fix independent of this phase — the node's disk is genuinely at 85% right now, so the toast is a real, current, recurring condition, not a one-off. - **Baseline vs. after timing-variance caveat**: the baseline was taken at 10:30 local time; this after-run at 01:27 the next day — different system load on the same physical, multi-service node (85% disk, live containers, this same box also serving as the build machine) may contribute to some of the regressed rows above independent of any code change. This is recorded as an interpretive note, not used to soften any verdict above — every regressed row is reported as regressed with its measured numbers. A same-time-of-day re-run would help disambiguate real regression from environmental noise for Discover/Server/Web5/ Fleet/AppDetails/OpenWrtGateway. - **Assumptions & Flagged Items from the plan set not yet settled by this run**: FA-D (`KEEP_ALIVE_MAX`) was settled in Task 1 (see its SUMMARY — confirmed at 6 via an on-device memory reading). FA-B (`ContainerAppDetails.vue`) — see the dead-code note above. FA-E (AIUI source) — settled by 02-07 (source located, D-14 landed upstream). ## Addendum: Task 3 Checkpoint Follow-Up (2026-07-31, post-first-pass) The user's first Task 3 pass reported "otherwise it's getting much better" plus four specific issues. Each was investigated on archi-dev-box directly (not guessed), root-caused via git history, and — where it traced to a phase-2 commit — fixed. No new plan was created per standing direction; this addendum records the investigation and its outcome. ### Real-node phase-2 regression: first visit to Cloud, no folder opened on click **Confirmed, root-caused, and fixed — verified 5/5 fresh sessions against the redeployed build.** Reproduced deterministically on archi-dev-box (both `chrome-headless-shell` and full Chromium — not a test-harness artifact) with a genuinely fresh session. - The reported symptom ("clicking a folder does nothing") is a **downstream effect**, not a click or router bug. Direct instrumentation proved: the click event fires (confirmed via a raw DOM listener attached to the exact clicked node), `router.push({name:'cloud-folder',...})` is called (confirmed by patching the live `$router` instance), but the returned promise never settles — because Vue Router awaits the target route's async component, and that dynamic `import()` itself never resolves. - Confirmed via manual `import()` calls from the page console: after visiting Cloud once, importing **any** lazy route chunk (Fleet, CloudFolder, AppDetails — unrelated views) hangs identically; before ever visiting Cloud, the same imports resolve in ~300ms. Not chunk-specific. - Traced to exactly one permanently-pending network request: `GET /app/filebrowser/api/resources/Photos`, fired from `Cloud.vue`'s `fetchCounts()` on mount. Confirmed hung via request-lifecycle tracking (`request` fires, neither `requestfinished` nor `requestfailed` ever fires, still pending after 10s). The **identical** request, issued manually with a fresh token completely outside of Cloud.vue, returns in 29ms — ruling out File Browser or the network path itself. Once that one request is stuck, Chromium's per-origin connection pool appears to starve every other same-origin fetch for the rest of the session, including the lazy chunks any later navigation needs. - **Fix #1 applied** (`fix(02-08)`, commit `e1a3f31a`, `neode-ui/src/views/Cloud.vue`): gave `Cloud.vue`'s `syncOnEntry()` the same fresh-mount guard already used in `Home.vue`/`Web5.vue`/`Mesh.vue`/`Server.vue` (02-04 exempted Cloud on the reasoning each individual resource is staleness/inflight-deduped — true per-resource, but the two back-to-back `onMounted`+`onActivated` passes still doubled `loadPeerFiles()`'s full per-peer `content.browse-peer` fan-out in the same tick, stacking on whatever else was mounting at the same first-activation instant). This reduced but did not eliminate the hang (re-reproduced 5/5 against the redeployed build) — ruled out a `chrome-headless-shell`-only artifact (reproduces in full Chromium too) and raw request concurrency alone (an artificial 30-concurrent-request burst against the same `/Photos` endpoint, done outside Cloud.vue, completed in 179ms, no hang). - **Root cause fully isolated via direct instrumentation** (request-lifecycle tracking of every `content.browse-peer` call during a fresh Cloud mount): **13 of 14** concurrent `content.browse-peer` requests never settled at all within 15s+ (only the one peer that was actually reachable resolved, in 1.4s). `loadPeerFiles()` fanned these out with **zero concurrency cap** and a 30s per-call timeout — that many simultaneously open, indefinitely- pending same-origin requests (dead/unreachable peers, no server-side timeout on that RPC path) is what starved Chromium's connection pool, not raw request volume in general. - **Fix #2 applied** (`fix(02-08)`, commit `8fe6217b`, `neode-ui/src/views/Cloud.vue`): capped the `content.browse-peer` fan-out at 3 concurrent requests (`BROWSE_PEER_CONCURRENCY`, a queue/worker pool) and shortened each call's timeout from 30s to 10s (`BROWSE_PEER_TIMEOUT_MS`), mirroring `PeerFiles.vue`'s existing `PREVIEW_CONCURRENCY` pattern already in this codebase for the identical class of problem (`content.preview-peer`'s own fan-out). A timed-out/failed peer already resolved silently through `resources.ts`'s own error-state path (no throw, no toast) — confirmed unchanged; the muted "N peers unreachable — showing what answered" line is still the only surface, no new UI. Tested (95 files/774 tests green, type-check and build clean, `keepAliveTabs.test.ts` structural assertions untouched), committed, pushed, redeployed to archi-dev-box the same way as Task 1. - **Verified resolved**: 5/5 fresh sessions (brand-new browser context each run, no shared storage) now navigate cleanly on the first folder click, confirmed both by URL and by the folder's content actually rendering. Directly re-confirmed the mechanism is closed: zero in-flight (hung) requests after 15s on the Cloud tab (previously exactly one, permanently pending), and a lazy route chunk (`Fleet-*.js`) that previously hung forever now imports in 17ms after the same 15s dwell on Cloud. - **Closed.** Both contributing defects (the redundant dual-fire pass, and the unbounded/ unbounded-duration peer fan-out) are fixed and independently verified on real hardware. ### Classification of the other three reported issues — all pre-existing, none phase-2 All three traced via `git log`/`git merge-base --is-ancestor` against `a75b6709` (the commit the 02-01 baseline was taken at, i.e. the last commit before any phase-2 UI change landed). None of the implicated files or functions were touched by any phase-2 commit; no fix was implemented for any of these three, per standing direction (captured separately as phase-1/general UX work). 1. **Paid Files: clicking a picture opens it in a browser tab instead of the app's lightbox.** `neode-ui/src/views/Cloud.vue` `viewPaidItem()` (~line 470-493): for any non-audio purchased item it does `window.open(url, '_blank', 'noopener')` on a blob URL — it never calls `MediaLightbox.vue` (`neode-ui/src/components/cloud/MediaLightbox.vue`) at all, unlike the My Files / Peer Files rows' `handlePreview()` paths, which do route into the lightbox. Introduced in `f3393581` ("fix(content): double-pay is now impossible + purchases auto-file + Paid Files tab", 2026-07-22 — confirmed an ancestor of `a75b6709` via `git merge-base --is-ancestor`). **Pre-existing, not phase 2.** 2. **Picture-in-picture on a video doesn't close the lightbox; the transition should be fluid.** `neode-ui/src/utils/pip.ts`'s `togglePip()` only calls `video.requestPictureInPicture()`/`exitPictureInPicture()` — it never emits a close event or touches `MediaLightbox.vue`'s (`neode-ui/src/components/cloud/MediaLightbox.vue`, PiP button at ~line 21-31, `close()` at ~line 233) own visibility state, so the lightbox backdrop simply stays open behind the PiP window with no reaction and no transition at all. Introduced in `f72d4b92` ("feat(content): seller-picked payment methods + music always in the bottom bar + video PiP", 2026-07-23 — zero commits touch `MediaLightbox.vue` or `pip.ts` between `a75b6709` and `HEAD`, confirmed via `git log a75b6709..HEAD --`). **Pre-existing, not phase 2.** 3. **Some surfaces lack loader states where an open genuinely takes time.** Spot-checked rather than exhaustively swept (a full sweep of every secondary-screen/modal open across the app is its own piece of work). The clearest concrete instance found: `neode-ui/src/views/Cloud.vue` `viewPaidItem()` (same function as #1 above) makes a `content.owned-get` RPC call with a 60-second timeout and shows **no loading indicator at all** while it resolves — `paidLoading` (line 466) only covers the list fetch (`content.owned-list`), not the per-item open. Clicking a Paid Files row shows nothing until the new tab appears (or fails silently). Same commit (`f3393581`, 2026-07-22) as #1 — **pre- existing, not phase 2**. Other candidates were not individually verified in the time available; `CloudFolder.vue`'s native file-grid path does already show a loading skeleton (`neode-ui/src/components/cloud/FileGrid.vue` line 4), so that one specific secondary screen is not part of this finding. 4. **Picture-in-picture must survive tab changes or video buffering pauses; doesn't seem to.** Two parts, classified separately: - **Tab-change part, checked against phase 2's lifecycle changes specifically — not implicated.** Neither `Cloud.vue`, `CloudFolder.vue`, nor `MediaLightbox.vue` (`neode-ui/src/components/cloud/MediaLightbox.vue`) defines an `onDeactivated` hook at all (zero matches for `onDeactivated` in any of the three) — 02-04 never touched this file and added no teardown that pauses, resets, or unmounts video/lightbox state on tab deactivation. `MediaLightbox` is a `` child rendered from inside `Cloud.vue`'s template (`v-if="lightboxIndex !== null"`), so when `Cloud.vue` (KeepAlive- cached since 02-04) deactivates on a tab switch, Vue's own KeepAlive machinery recursively deactivates that whole subtree, including the teleported lightbox/video, moving its DOM into KeepAlive's internal (not document-attached) storage container. Whether Vue's exact move semantics disconnect the `