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

25 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
02-ui-performance 01 execute 1
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
true
PERF-01
truths prohibitions artifacts key_links
Every surface in the D-09 starting set (Apps, Marketplace/Discover app store, Mesh, Wallet/send flows, Cloud/Files, Server, Network, Web5, plus AppDetails) has a recorded first-visit and revisit measurement in 02-PERF-BASELINE.json
For each measured surface the findings doc names exactly one primary cause drawn from the closed set: remount storm, serial RPC waterfall, uncached fetch, or already-fast
Each named cause is backed by a number in 02-PERF-BASELINE.json (revisit ms, revisit RPC count, or remount-probe result) — not by code reading alone
02-FINDINGS.md is committed to git before any production source file under neode-ui/src is modified by this phase (D-10)
The harness re-runs on demand and produces a comparable JSON artifact, so the same measurements can be taken again after the fixes land
A surface that could not be measured is recorded in the findings doc as unmeasured with the reason, and is never recorded as already-fast
statement verification
Running the harness twice against an unchanged build yields the same primary-cause classification for every surface backstop
MUST NOT present inferred, code-read, or cherry-picked numbers as measured profiling results, and MUST NOT omit a D-09 surface from the findings doc because it was hard to measure — an unmeasured surface is recorded as unmeasured, never as already-fast
MUST NOT ship performance instrumentation that transmits, logs, or persists node or user activity off-device — profiling stays local to the developer's browser and harness run
MUST NOT commit node-identifying or account-identifying material (onion addresses, DIDs, pubkeys, wallet balances, file names, peer hostnames) into the profiling artifacts
path provides exports
neode-ui/e2e/perf/surfaces.ts SURFACES table — one row per D-09 surface with route path, content selector and view-root selector
SURFACES
type Surface
path provides exports
neode-ui/e2e/perf/measure.ts measureSurface() — first-visit vs revisit timing, RPC request trace, remount probe
measureSurface
type SurfaceMeasurement
type RpcCall
path provides
neode-ui/e2e/perf/surface-perf.spec.ts Playwright spec that walks every SURFACES row and writes the JSON artifact
path provides
.planning/phases/02-ui-performance/02-PERF-BASELINE.json Recorded pre-fix measurements for every D-09 surface
path provides
.planning/phases/02-ui-performance/02-FINDINGS.md D-10 deliverable — surface, measured cause, intended fix, owning plan
from to via pattern
neode-ui/e2e/perf/surface-perf.spec.ts neode-ui/e2e/perf/measure.ts imports measureSurface and calls it once per SURFACES row measureSurface
from to via pattern
neode-ui/e2e/perf/measure.ts browser network layer page.on('request') / page.on('response') captures RPC POSTs so revisit RPC count is observed, not assumed page.on(['"]request
from to via pattern
.planning/phases/02-ui-performance/02-FINDINGS.md .planning/phases/02-ui-performance/02-PERF-BASELINE.json every named cause cites the baseline row it came from 02-PERF-BASELINE
Measure the D-09 surfaces on real node hardware, name each one's primary cause from observed numbers, and commit the findings doc that gates every fix in this phase.

Purpose: PERF-01 and D-10 require that fixes are targeted, not guessed. Nothing under neode-ui/src/ changes in this plan — the harness lives entirely in neode-ui/e2e/perf/ so the "measure before you fix" ordering is structurally guaranteed rather than merely promised.

Output: a re-runnable Playwright perf harness, a committed baseline JSON, and 02-FINDINGS.md mapping each surface to a measured cause and the plan that fixes it.

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

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/02-ui-performance/02-CONTEXT.md @.planning/phases/02-ui-performance/02-RESEARCH.md @.planning/phases/02-ui-performance/02-PATTERNS.md @CLAUDE.md Task 1: Build the re-runnable surface perf harness neode-ui/e2e/perf/surfaces.ts, neode-ui/e2e/perf/measure.ts, neode-ui/e2e/perf/surface-perf.spec.ts - neode-ui/playwright.config.ts — testDir is `./e2e`, baseURL comes from `ARCHY_BASE_URL` (default `http://192.168.1.228`), single `chromium` project, 60s timeout - neode-ui/e2e/app-launch.spec.ts — the existing login/navigation flow to reuse verbatim; do not invent a second auth path - neode-ui/e2e/intro-experience.spec.ts — how the existing specs get past the intro/splash gating so a measurement does not accidentally time the intro animation - neode-ui/src/router/index.ts — the authoritative route table; every `SURFACES` path must exist here - neode-ui/src/views/dashboard/useRouteTransitions.ts — `TAB_ORDER` is the canonical main-tab path list - neode-ui/src/api/rpc-client.ts — the RPC transport, so the request filter matches the real endpoint shape rather than a guess Create `neode-ui/e2e/perf/surfaces.ts` exporting `type Surface` and a `SURFACES` array. One row per D-09 surface, each row carrying: `id`, `label`, `path`, `kind` (`main-tab` or `secondary`), `contentSelector` (a selector that is only present once real content has painted, not a skeleton or spinner), and `rootSelector` (the stable outermost element of the view, used by the remount probe). Rows required by D-09, using the real route paths from `router/index.ts`: `/dashboard` (home/wallet figures), `/dashboard/apps`, `/dashboard/marketplace`, `/dashboard/discover`, `/dashboard/cloud`, `/dashboard/mesh`, `/dashboard/server`, `/dashboard/web5`, `/dashboard/fleet`, `/dashboard/chat`, plus secondary rows `/dashboard/apps/:id` (AppDetails), `/dashboard/marketplace/:id` (MarketplaceAppDetails), `/dashboard/cloud/:folderId` (CloudFolder) and `/dashboard/server/openwrt` (OpenWrtGateway). Derive each `contentSelector` and `rootSelector` by reading the corresponding view file; prefer an existing stable class or a `data-` attribute already present over adding markup to `src/`.
D-09 also names "Wallet / send flows". RESEARCH.md could not locate a `Wallet.vue`.
Locate the real wallet surface first (grep for `SendBitcoinModal`, `loadWeb5Status`,
and wallet balance rendering under `neode-ui/src/views` and
`neode-ui/src/components`), then add a row for wherever wallet figures and the send
entry point actually live. If the wallet surface turns out to be a modal rather than
a route, add a row with a `trigger` field naming the selector that opens it, and
measure open-to-content instead of navigate-to-content.

Create `neode-ui/e2e/perf/measure.ts` exporting `measureSurface(page, surface,
opts)` returning a `SurfaceMeasurement`. It must record, per surface:
`firstVisitMs` (navigate from the dashboard home to the surface, wait for
`contentSelector`), `revisitMs` (navigate away to a fixed neutral tab, then back,
wait for `contentSelector`), `firstVisitRpcCount` and `revisitRpcCount` (POSTs
captured via `page.on('request')`, filtered to the RPC endpoint the rpc-client uses),
`revisitRpcCalls` (an ordered array of `{ method, startedAtMs, durationMs }` so
overlapping vs. sequential call timing is visible in the artifact), `remounted`
(see below), and `error` (a string when the surface could not be measured, with
the other numeric fields left null). Take `runs` samples per surface (default 3)
and record every sample plus the median — never only the best one.

Implement the remount probe without touching `src/`: on the first visit, in the
page context, stamp the element matched by `rootSelector` with a unique value on a
dataset key (for example `perfProbe`); after the away-and-back navigation, read the
same key back. A surviving value means the component instance was reused; a missing
value means the view remounted. Record the raw before/after values in the
measurement so the conclusion is auditable.

Derive the serial-vs-parallel signal from `revisitRpcCalls`: expose a computed
`maxConcurrentRpc` and `rpcWallClockMs` on the measurement so a waterfall (calls
starting one after another, `maxConcurrentRpc` of 1) is distinguishable from an
already-parallel fan-out without re-reading the code.

Create `neode-ui/e2e/perf/surface-perf.spec.ts`: a single Playwright test that logs
in using the flow from `app-launch.spec.ts`, iterates `SURFACES`, calls
`measureSurface` for each, and writes the full result array plus a run header
(`baseUrl`, `takenAt`, `commit` from `git rev-parse --short HEAD`, `runs`) to the
path given by the `ARCHY_PERF_OUT` environment variable, defaulting to
`e2e/test-results/surface-perf.json`. A surface that throws is caught, recorded
with its `error` string, and does not abort the remaining surfaces.

Redaction is part of the harness, not a later cleanup step: the measurement must
record RPC method names and timings only. Do not capture request bodies, response
bodies, page text, or screenshots into the JSON artifact.
cd neode-ui && npx tsc --noEmit -p tsconfig.json 2>&1 | grep -v 'e2e/test-results' ; npx playwright test e2e/perf/surface-perf.spec.ts --project=chromium --reporter=line - `neode-ui/e2e/perf/surfaces.ts` exports `SURFACES` and every entry's `path` appears in `neode-ui/src/router/index.ts` - `SURFACES` contains at least one row for each of: apps, marketplace, discover, cloud, mesh, server, web5, chat, home, fleet, and the located wallet surface - `neode-ui/e2e/perf/measure.ts` exports `measureSurface` and the file contains `page.on('request'` - `SurfaceMeasurement` carries all of `firstVisitMs`, `revisitMs`, `firstVisitRpcCount`, `revisitRpcCount`, `revisitRpcCalls`, `maxConcurrentRpc`, `remounted`, `samples`, `error` - `npx playwright test e2e/perf/surface-perf.spec.ts --project=chromium` exits 0 and writes a JSON file whose top-level array length equals `SURFACES.length` - The written JSON contains no request or response body text: `node -e "const r=require('./e2e/test-results/surface-perf.json');process.exit(JSON.stringify(r).match(/onion|did:|xpub|npub/i)?1:0)"` exits 0 - `npx tsc --noEmit` reports no errors originating in `e2e/perf/` The harness runs end to end against a reachable Archipelago node and emits a complete, body-free measurement artifact for every D-09 surface. Task 2: Record the on-device baseline from archi-dev-box archi-dev-box resolves and its web UI answers over HTTP from this machine (`getent hosts archi-dev-box` returns at least one address and the base URL returns a 200/302 for `/`) .planning/phases/02-ui-performance/02-PERF-BASELINE.json - neode-ui/e2e/perf/surface-perf.spec.ts — the harness written in Task 1, for its env-var contract - neode-ui/playwright.config.ts — `ARCHY_BASE_URL` is the target override - scripts/dev-start.sh — how the :8100 dev preview is started, in case the node's own UI is not directly reachable and the preview must proxy to it - .planning/phases/02-ui-performance/02-CONTEXT.md — D-11 names archi-dev-box as the verification target and states the pass bar Run the Task 1 harness against archi-dev-box with `ARCHY_BASE_URL` pointed at that node and `ARCHY_PERF_OUT` set to `../.planning/phases/02-ui-performance/02-PERF-BASELINE.json`, with `runs` at 3. archi-dev-box resolves to IPv6 addresses in this environment; if the bare hostname does not connect, try the reachable address form and record the exact base URL used in the artifact's run header.
If the node's UI cannot be driven directly, fall back to the :8100 dev preview
pointed at archi-dev per the phase's existing dev discipline, and record in the run
header that the measurement was taken through the preview rather than against the
node's own served bundle. Do not silently substitute the local mock backend — a
mock-backend run is not an on-device baseline and must be labelled as such in the
header if it is the only run that succeeds.

Add a `notes` field to the run header recording: the target actually used, the
browser build, whether the node was otherwise idle, and any surface that had to be
skipped and why. Commit the artifact.

Do not edit any file under `neode-ui/src/` in this task. If a surface cannot be
measured because its `contentSelector` never appears, fix the selector in
`neode-ui/e2e/perf/surfaces.ts` and re-run rather than dropping the surface.
node -e "const r=require('/home/archipelago/Projects/archy/.planning/phases/02-ui-performance/02-PERF-BASELINE.json'); const rows=r.results??r; if(!Array.isArray(rows)||rows.length===0)process.exit(1); const bad=rows.filter(x=>x.error==null&&(x.revisitMs==null||x.revisitRpcCount==null)); if(bad.length){console.error('incomplete rows',bad.map(b=>b.id));process.exit(1)} console.log('rows',rows.length)" - `.planning/phases/02-ui-performance/02-PERF-BASELINE.json` exists and parses as JSON - Its run header records `baseUrl`, `takenAt`, `commit`, `runs` and a `notes` string naming the actual target - Every `SURFACES` row appears in the results, each with either numeric measurements or a non-empty `error` string - No file under `neode-ui/src/` is modified by this task: `git diff --name-only HEAD -- neode-ui/src | wc -l` prints 0 - The artifact is committed (`git log -1 --name-only` lists `02-PERF-BASELINE.json`) A committed on-device baseline exists covering every D-09 surface, with the measurement target explicitly recorded. Task 3: Write and commit the D-10 findings doc .planning/phases/02-ui-performance/02-FINDINGS.md - .planning/phases/02-ui-performance/02-PERF-BASELINE.json — the numbers every claim must cite - .planning/phases/02-ui-performance/02-RESEARCH.md — the "Concrete Findings Per Surface" table is a hypothesis set to confirm or overturn, not a substitute for the measurement - .planning/phases/02-ui-performance/02-CONTEXT.md — D-02 (targeted not blanket), D-10 (doc before fixes), D-12/D-13 (fix scope) - .planning/phases/02-ui-performance/02-PATTERNS.md — the per-view conversion checklist the intended fixes should align with Write `.planning/phases/02-ui-performance/02-FINDINGS.md` with these sections:
1. `## Method` — the target used, the harness path, the run command with its env
   vars, the sample count, and what each recorded field means. State plainly which
   surfaces were measured on archi-dev-box and which (if any) were not.
2. `## Per-Surface Findings` — a table with columns: Surface, First visit (ms,
   median), Revisit (ms, median), Revisit RPC count, Max concurrent RPC, Remounted,
   Primary cause, Intended fix, Owning plan. `Primary cause` takes exactly one value
   from `remount storm`, `serial RPC waterfall`, `uncached fetch`, `already fast`,
   or `unmeasured`. Every non-`unmeasured` row cites the baseline field that
   justifies the cause (a remount storm needs `remounted: true`; an uncached fetch
   needs a non-zero `revisitRpcCount`; a waterfall needs `maxConcurrentRpc` of 1 with
   two or more sequential calls).
3. `## Ranked Fix Order` — the surfaces ordered worst-revisit-first. This ranking
   selects the tracer tab for plan 02-02.
4. `## Surfaces Left Alone (D-02)` — every surface classified `already fast`, with
   its numbers, so the decision not to convert it is auditable.
5. `## Corrections to Prior Research` — record, with evidence, any place the
   measurement overturns `02-RESEARCH.md`. At minimum confirm or refute this
   planner's finding that `neode-ui/src/views/ContainerAppDetails.vue` has no
   importer and no route entry (run `grep -rn "ContainerAppDetails" neode-ui/src` and
   paste the result) — RESEARCH.md names it as the confirmed serial-waterfall fix
   target, and if it is unreachable then no plan should spend effort on it and the
   real waterfalls must come from the measured `revisitRpcCalls` instead.
6. `## Owning Plans` — map each surface to the plan number that fixes it, using the
   plan set for this phase (02-02 tracer/app store, 02-03 secondary screens, 02-04
   keep-alive lifecycle, 02-05 Mesh, 02-06 Server and Home, 02-07 Chat/AIUI).

Redact before committing: replace any onion address, DID, pubkey, hostname other
than `archi-dev-box`, wallet figure, or file name that appears in a method name or
note with a short placeholder. Method names and timings stay.

Commit this doc as its own commit, and make that commit the last one in this plan —
it is the gate the rest of the phase depends on.
test -f /home/archipelago/Projects/archy/.planning/phases/02-ui-performance/02-FINDINGS.md && for s in '## Method' '## Per-Surface Findings' '## Ranked Fix Order' '## Surfaces Left Alone' '## Corrections to Prior Research' '## Owning Plans'; do grep -qF "$s" /home/archipelago/Projects/archy/.planning/phases/02-ui-performance/02-FINDINGS.md || { echo "missing: $s"; exit 1; }; done; echo OK - `02-FINDINGS.md` contains all six required headings - Every surface present in `02-PERF-BASELINE.json` appears as a row in the Per-Surface Findings table - Every row's Primary cause is one of the five allowed values - The doc references `02-PERF-BASELINE.json` at least once - `## Corrections to Prior Research` contains the literal output of the `ContainerAppDetails` grep - `git log --oneline -1 -- .planning/phases/02-ui-performance/02-FINDINGS.md` returns a commit - `git diff --name-only HEAD~3..HEAD -- neode-ui/src | wc -l` prints 0 — no production source changed in this plan The findings doc is committed, every claim in it traces to a measured number, and the fix order for the rest of the phase is fixed in writing.

<threat_model>

Trust Boundaries

Boundary Description
archi-dev-box node → developer workstation Real node data (RPC method names, timings, and anything else the harness might capture) crosses onto the workstation and into a git-tracked artifact
git-tracked planning artifacts → repository history Anything written into 02-PERF-BASELINE.json / 02-FINDINGS.md is permanent and pushed

STRIDE Threat Register

Threat ID Category Component Severity Disposition Mitigation Plan
T-02-06 Information Disclosure e2e/perf/measure.ts artifact writer medium mitigate Harness records RPC method names and timings only — no request bodies, response bodies, page text, or screenshots (Task 1); Task 3 adds a redaction pass before commit
T-02-07 Information Disclosure 02-FINDINGS.md committed to a pushed repo medium mitigate Explicit redaction step in Task 3 for onion addresses, DIDs, pubkeys, wallet figures, peer hostnames and file names
T-02-08 Spoofing Playwright login flow reusing dev credentials low accept The harness reuses the existing e2e/app-launch.spec.ts auth flow against a developer-owned dev node; no new credential surface is introduced and none are written to the artifact
T-02-SC Tampering npm/pip/cargo installs high mitigate No package-manager installs are in scope: RESEARCH.md's Package Legitimacy Audit records zero new packages, and every primitive used is Vue core, a JS built-in, or already present. If any task finds it needs a new dependency it stops and routes through the Package Legitimacy Gate with a blocking human checkpoint before installing
</threat_model>

<artifacts_this_phase_produces>

Artifacts this phase produces

New symbols and paths created by Phase 02. Newly-created names below are not pre-existing API and must not be treated as drift from the current codebase.

Created by this plan (02-01):

  • neode-ui/e2e/perf/surfaces.tsSURFACES, type Surface
  • neode-ui/e2e/perf/measure.tsmeasureSurface(), type SurfaceMeasurement, type RpcCall
  • 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
  • Environment variables consumed: ARCHY_PERF_OUT (new), ARCHY_BASE_URL (pre-existing in playwright.config.ts)

Created elsewhere in Phase 02 (for cross-plan reference):

  • neode-ui/src/views/dashboard/keepAliveRoutes.tsshouldKeepAlive(), KEEP_ALIVE_PATHS
  • neode-ui/src/components/RefreshIndicator.vue
  • neode-ui/src/composables/__tests__/useCachedResource.test.ts
  • neode-ui/src/views/dashboard/__tests__/keepAliveTabs.test.ts
  • RouteMeta.keepAlivevue-router module augmentation
  • .planning/phases/02-ui-performance/02-PERF-AFTER.json </artifacts_this_phase_produces>

<assumptions_and_flagged_items>

Assumptions & Flagged Items

Nothing below is silently dropped. Each row is an explicit flagged assumption carried into execution.

Edge-coverage probe rows (spec-less fallback — all three came back unclassified/unresolved)

Requirement Probe status Disposition here
PERF-01 unclassified / unresolved — probe could not classify FLAGGED. Not auto-backstopped. Resolved in substance by the must_haves.truths written above (measured-cause coverage, closed cause set, doc-before-fixes ordering, unmeasured-is-not-fast), plus one verification: backstop truth for run-to-run classification stability. The probe row itself remains unresolved and is surfaced here for human review.
PERF-02 unclassified / unresolved FLAGGED and carried in plans 02-02, 02-04, 02-05, 02-06, 02-07, 02-08.
PERF-03 unclassified / unresolved FLAGGED and carried in plans 02-03 and 02-08.

Prohibition-probe canon referrals (breadcrumbed, deliberately not minted)

  • Injection / XSS via cached-then-rendered payloads is canon — covered by /gsd-secure-phase and eslint security plugins; not minted here.
  • Generic GDPR / data-retention rules for browser storage are canon — covered by /gsd-secure-phase; the privacy prohibitions minted in this phase are product-specific payload classes (wallet, credential, peer-content), not the generic rule.

Corrections to prior phase artifacts

ID Item Evidence Impact
FA-B neode-ui/src/views/ContainerAppDetails.vue appears to be dead code — grep -rn "ContainerAppDetails" neode-ui/src returns only a self-referential comment inside the file itself, and the file has no entry in neode-ui/src/router/index.ts Verified 2026-07-30 by this planner RESEARCH.md names it as the confirmed serial-waterfall fix target. If dead, no plan should spend effort on it. Task 3 re-runs the grep and records the verdict.
FA-F CONTEXT.md canonical_refs names neode-ui/src/App.vue as the KeepAlive insertion point RESEARCH.md Pitfall 2, confirmed by this planner reading Dashboard.vue:87-118 Superseded — the real per-tab remount point is the nested RouterView in Dashboard.vue. Carried into plan 02-02.
</assumptions_and_flagged_items>
- `npx playwright test e2e/perf/surface-perf.spec.ts --project=chromium` exits 0 - `.planning/phases/02-ui-performance/02-PERF-BASELINE.json` covers every `SURFACES` row - `.planning/phases/02-ui-performance/02-FINDINGS.md` has all six required headings and is committed - `git diff --name-only ..HEAD -- neode-ui/src` is empty — the D-10 ordering held

<success_criteria>

  • Every D-09 surface has a measured first-visit and revisit number taken against archi-dev-box (or an explicitly labelled fallback target)
  • Every surface has exactly one primary cause from the closed set, each backed by a cited baseline field
  • The findings doc is committed before any neode-ui/src change in this phase
  • The harness can be re-run later to produce a directly comparable after-artifact </success_criteria>
Create `.planning/phases/02-ui-performance/02-01-SUMMARY.md` when done. Include the per-surface cause table and the ranked fix order verbatim — downstream plans read the tracer-tab selection from it.