---
name: verify-frontend
description: How to verify Massing web/viewer UI changes LIVE — full verification works; two historic "stalls" are fixed and neither was the geometry loader. Invoke when you changed apps/web and need to prove it works. Covers typecheck/lint/vitest/build, driving the real app, and honest flagging of flows you genuinely couldn't exercise.
---

# Verify a Massing frontend change

> Standing directions for this repo: [docs/roadmap-directions.md](../../../docs/roadmap-directions.md). Read those first.

**The "preview stall" was diagnosed and FIXED in v0.3.703 — it was never the geometry loader.** Five
SSE endpoints polled the database inside `async def gen()`, blocking the event loop for every other
client; two open tabs left the server wedged and *every* request timed out, including the app's boot
`api.health()`. That left `projectId` unset, so the portal showed "No project open" while a model sat
rendered on screen. Fixed with `run_in_threadpool`; measured >8s timeouts → ~20 ms. **Full live
verification now works** — start both servers and drive the real app.

**A SECOND "stall" was diagnosed and fixed 2026-08-02, and it was not the loader either — the canvas
had width ZERO.** Container 830x572, canvas 0x493, four meshes and 230 triangles built and *visible*,
`.frag` 200, worker fine, console clean. The renderer sizes itself once at construction; if the
container is not at final width yet, that first size sticks for ever. A `ResizeObserver` in
`createViewer` now tracks it (`apps/web/src/viewer/canvasResize.test.ts`).

**Twice now, "the geometry loader stalls" has meant something else entirely** — once a blocked event
loop, once a zero-width canvas. Before writing that sentence a third time, ask the page:

```js
const c = document.querySelector('canvas'), k = document.querySelector('#container');
({ canvas: c.width+'x'+c.height, container: k.clientWidth+'x'+k.clientHeight })   // canvas ≈ container × devicePixelRatio
```

and count what is actually in the scene (`v.viewer.world._scene.three`, not `v.world`). A caveat
nobody re-tests becomes folklore: this skill asserted the stall in its own frontmatter for weeks
*while its body said it was fixed*, and the contradiction propagated into eight changelog entries.

## Always
```
cd apps/web && export PATH="/c/Program Files/nodejs:$PATH"
npm run typecheck && npm run lint      # Node 24 (Node 18 breaks the build)
npx vitest run <path/if/covered>
npm run build                          # strongest compile check; ~1 min
```

## Force the tools rail to build (verifies rail UI live)
The preview server is on :5173. In the running preview (via the browser tools), navigate to a project with a source IFC, then:
```js
window.dispatchEvent(new CustomEvent('aec:persona', { detail: 'all' }));  // forces buildToolsPanel + buildClashPanel
// then read #panel-tools — the tool-group sections, buttons, inputs all render
```
This verifies: new rail tools/buttons/inputs, the ribbon tabs, the Library palette, the node-canvas launcher, the KEYS shortcut layer (dispatch KeyboardEvents), the Ask/analytical boxes. Exercise handlers by `.click()`ing buttons and reading the result-overlay / DOM.

## What you CAN'T verify live (flag it honestly)
- (historic) `panel-tree` was unverifiable behind the stall. Both stalls are fixed — verify it live.
- Nothing, if both servers are current. **Start them yourself**: `.claude/launch.json` has `api`
  (:8093) and `web` (:5173) configs — use `preview_start` with `{name: "api"}` / `{name: "web"}`. A
  previous note here said a stale :8093 process was "the user's call"; there was usually **no process
  at all**, and the config to start one has always been in the repo. Check before assuming.
- Anything geometry-coupled (placing an element, section cuts, camera).

## Report
State exactly what was verified live vs. by typecheck/build vs. not exercised. Never claim an interactive viewer flow "verified" if the stall prevented it.

See memory: `tools-panel-verify-technique`, `web-build-needs-node-20`, `web-eslint-node-pin`, `dev-api-port`.
