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 |
|
true |
|
|
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.
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.
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.
<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.ts—SURFACES,type Surfaceneode-ui/e2e/perf/measure.ts—measureSurface(),type SurfaceMeasurement,type RpcCallneode-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 inplaywright.config.ts)
Created elsewhere in Phase 02 (for cross-plan reference):
neode-ui/src/views/dashboard/keepAliveRoutes.ts—shouldKeepAlive(),KEEP_ALIVE_PATHSneode-ui/src/components/RefreshIndicator.vueneode-ui/src/composables/__tests__/useCachedResource.test.tsneode-ui/src/views/dashboard/__tests__/keepAliveTabs.test.tsRouteMeta.keepAlive—vue-routermodule 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-phaseand 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> |
<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/srcchange in this phase - The harness can be re-run later to produce a directly comparable after-artifact </success_criteria>