Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/web.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@ on:
branches: [main]
paths:
- "web/**"
# The page's copy of the result schema is tested against the server's.
- "server/schemas/**"
- ".github/workflows/web.yml"
pull_request:
paths:
- "web/**"
# The page's copy of the result schema is tested against the server's.
- "server/schemas/**"
- ".github/workflows/web.yml"

permissions:
Expand Down
34 changes: 21 additions & 13 deletions web/README.md
Original file line number Diff line number Diff line change
@@ -1,32 +1,40 @@
# web

Browser app for zero-install capture experiments. Right now it holds a placeholder page and `src/lib/units.ts`, which converts meters (what ARKit reports) to feet and inches for display.
The placement review page: pick a sample or drop a scan, and see where the Base Power battery goes and why. It calls the placement server's `POST /v1/placements` (see `server/README.md`) and shows the decision, the spot and cable run, the site plan, every check with its reason, and the views still missing. The site plan comes from `POST /v1/placements/site-plan.svg`, asked for only after an answer arrives, so a scene the server refuses is sent once.

## Setup
## Run

Requires Node 24 or later (`.node-version`) and the pnpm version pinned in `package.json` `packageManager`. Corepack and pnpm's own version switching both pick up that pin.
Requires Node 24 or later (`.node-version`) and the pnpm pinned in `package.json`.

```sh
pnpm install
pnpm dev # http://localhost:5173
```

## Commands
The page talks to the server at `VITE_PLACEMENT_API`, default `/api`. In `pnpm dev` and `pnpm preview`, `/api` is proxied to `PLACEMENT_SERVER` (default `http://localhost:8000`), so start the server first: `cd server && uv run uvicorn api:app --port 8000`. A reviewer can point the page at another server with **Change**; the choice is kept in the browser. `?sample=<id>` opens a sample directly, so a link shares an answer.

Vercel previews of this page are built with `VITE_PLACEMENT_API=https://house-scanning-server.vercel.app`, the hosted server (public rules only; see `server/README.md`). Its host refuses request bodies over 4.5 MB without the headers a browser needs to read the refusal, so the page refuses larger files itself; send `scene.json` or a zip without the photos. A page served over https can't call an http server; the page says so when one is entered.

Without a reachable server, the bundled samples can still show the answer the server gave them when it was recorded; the page says so ("Saved answer").

| Command | What it does |
| ---------------- | ------------------------------------------------------ |
| `pnpm dev` | Vite dev server |
| `pnpm build` | `tsc -b`, then `vite build` into `dist/` |
| `pnpm typecheck` | `tsc -b` (no emit) |
| `pnpm lint` | `biome ci .`: lint and format check, no writes |
| `pnpm format` | `biome format --write .` |
| `pnpm test` | `vitest run` |
| `pnpm check` | lint, typecheck, test, build; stops at first failure |
| `pnpm preview` | Serve `dist/` with the same `/api` proxy |
| `pnpm check` | lint, typecheck, test, build; what CI runs |
| `pnpm contract` | Regenerate the result types from the server's schema |

## The result contract

`src/contract/result.schema.json` is a copy of `server/schemas/result.schema.json`, and `src/contract/result.ts` is generated from it by `pnpm contract`. Answers are validated against the schema at runtime, so a server that breaks the contract shows a specific error instead of a broken page. `scripts/contract.test.ts` fails when the generated files don't match the copy or the copy doesn't match the server's schema; CI runs it when either side changes. After the server's schema changes, run `pnpm contract` and commit the results.

The page accepts answers with fields it doesn't know, as long as `schema_version` has the same major version (`1.x`), so a server that adds a field doesn't break a deployed page. The copy stays identical to the server's; only the generated types and validator are loosened (`tolerantSchema` in `scripts/contract-lib.ts`).

CI (`.github/workflows/web.yml`, job "Web checks") runs `pnpm install --frozen-lockfile` and then `pnpm run check`.
The samples in `src/samples/` are synthetic. `scripts/record_samples.py` builds their scenes and records the server's answers under the public rules, as the hosted server gives them (`cd server && PYTHONPATH=. uv run python ../web/scripts/record_samples.py`); rerun it when the solver changes. Generated and recorded files are excluded from Biome so they stay byte-identical to what produced them.

## Conventions

- Lengths are stored in meters. Convert to feet and inches only for display, using `src/lib/units.ts`.
- TypeScript runs in strict mode with `noUncheckedIndexedAccess`.
- Tests live next to their source files as `*.test.ts`.
- The server reports feet; `src/lib/format.ts` says them the way people do ("2 ft 9 in right of the meter"). `src/lib/units.ts` converts ARKit's meters.
- TypeScript runs in strict mode with `noUncheckedIndexedAccess`. Tests sit next to their source as `*.test.ts`.
- Use synthetic data only. The repository is public, so never commit real home photos, addresses or meter numbers.
3 changes: 2 additions & 1 deletion web/biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"useIgnoreFile": true
},
"files": {
"ignoreUnknown": false
"ignoreUnknown": false,
"includes": ["**", "!src/contract", "!src/samples"]
},
"formatter": {
"enabled": true,
Expand Down
9 changes: 8 additions & 1 deletion web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,14 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>House Scan: web capture</title>
<title>Placement review · House Scan</title>
<meta name="theme-color" content="#f2f4f3" media="(prefers-color-scheme: light)" />
<meta name="theme-color" content="#141a1d" media="(prefers-color-scheme: dark)" />
<meta name="description" content="Where a Base Power battery can go, and why." />
<link
rel="icon"
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect x='3' y='9' width='26' height='14' rx='2' fill='none' stroke='%237b8a90' stroke-width='3' transform='rotate(-8 16 16)'/%3E%3C/svg%3E"
/>
</head>
<body>
<div id="root"></div>
Expand Down
9 changes: 8 additions & 1 deletion web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,14 @@
"lint": "biome ci .",
"format": "biome format --write .",
"test": "vitest run",
"check": "pnpm run lint && pnpm run typecheck && pnpm run test && pnpm run build"
"check": "pnpm run lint && pnpm run typecheck && pnpm run test && pnpm run build",
"contract": "node scripts/contract.ts",
"preview": "vite preview"
},
"dependencies": {
"@fontsource-variable/public-sans": "^5.3.0",
"@fontsource/barlow-semi-condensed": "^5.3.0",
"@fontsource/ibm-plex-mono": "^5.3.0",
"react": "^19.3.0",
"react-dom": "^19.3.0"
},
Expand All @@ -26,6 +31,8 @@
"@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0",
"@vitejs/plugin-react": "^6.1.1",
"ajv": "^8.20.0",
"json-schema-to-typescript": "^16.0.0",
"typescript": "~6.0.3",
"vite": "^8.3.1",
"vitest": "^5.0.0"
Expand Down
Loading