Projects
A project is one board and its history. The home page is the ledger of every project on this machine — and, since 17 Sep 2026, it is a plane rather than a page: the same shell the workbench has, with the ledger where the stage would be. The ledger on the left is every board, one row each, with the command line at its head — a plate the shape of a row, where a new board is typed and where its row will land; the disc on the right is the selected board's record and its actions, the full height of the column; the foot is what this machine can do and what it is doing. Nothing scrolls but the ledger. Selection is the one thing that changes the plane: click a row (or move with J / K or the arrows), and the disc follows.
Before that it was a document — a hero paragraph, a 600px section for picking the microcontroller with a second Build button of its own, an examples grid and a card grid with the same three numbers on every card, 2,600px of it — and nothing on it said whether the routers were there or what was building.
The ledger
One row a board, a plate with a gap to the next: the name with its flags (example, archived), the current version's one-sentence summary, the current build, the version, the qty-1 cost and when it was last touched. A row's edge is how it lights — a hairline at rest, brighter under the pointer, the accent when selected, as a hard line with a tight bloom and lit corner marks rather than a soft halo.
The current build is one box: the verdict as a word and a number in the state's colour — passes (the build's own check said so), checks pass (no gaps, no ERC errors, not judged), not orderable · 1 gap, not orderable · 2 erc, fails check, from a file · unchecked, no build yet — and, while a job runs on that board, its phase and the seconds with the square blinking. Hover it for the whole sentence.
Above the rows, facets: All · Passes · Not orderable · From a file ·
Examples, each with its count, one lit at a time, and Archived at the
far end as a toggle (archived boards are hidden unless it is lit); then the
filter — a field with a glass in it, matching name, summary, description
and tags — and the sort (recent, name, cost, versions). Examples are rows in the ledger with an example flag and
their own facet, not a gallery of their own; on the disc an example's first
action is Fork & edit, because editing a board that already builds asks
far less than describing one from nothing, and the fork is your own board
and never inherits the flag. Flag one with
PATCH /api/projects/<slug> {"example": true} — and only a board you have
checked yourself.
While a build is under way, on any project and from any device, its row's
box is the phase it is on and the seconds; a job whose board is not a
visible row (filtered out, archived) is a live row above the ledger
instead. When it finishes the ledger re-reads itself. The page asks GET /api/app
every 3 s while a job is running and every 30 s otherwise (not at all in a
hidden tab), and reloads the list when the set of active jobs changes.
The disc
The selected board's record. The board first, as a skeleton: the
current version's board.svg fetched and redrawn in one colour — the
outline, every pad, the copper, the silk and the pour's edge as lines on
the ground, the back layer dimmer, the ratsnest dashed where nothing is
routed yet. The picture is render.py's, whose SVG carries no classes, so
projects.js matches its literal colours (RENDER) and tests/test_app.py
holds the two palettes equal. In the frame's corners: the current build
box (the same one the ledger shows — passes, not orderable · 1 gap,
from a file · unchecked, or the running job's phase), the version, the
date and the part count.
Under it the name, large, in the label face; the version of how many and
when it was touched; the actions — Open (or, for an example, Fork &
edit first), Fork, Export .zip, Rename (also edits the
description), Archive / Unarchive, Delete (to projects/.trash/;
nothing is destroyed until the trash is emptied from the foot's trash
chip, which can also restore one); and then two columns: the readout
(parts, ERC, gaps, the build's own verdict from workspace.verdict(), the
rule the evaluator scores with, and the cost) beside the versions,
newest first, each a link into the workbench at that version with its
verdict's square and its cost or gap count — the board's history lives
here, drawn from trail in GET /api/projects (the verdict's inputs for
every version and nothing bulkier, so the list stays a small payload). In a
narrow disc the columns stack.
The row's box, the frame's box and each version's square come from one
rule (stateOf() in projects.js), so they cannot disagree: a board read
from a file (erc_errors: null) is file; then the build's passed;
then gaps and ERC errors; else good.
The verdict is the build's, not a fresh DRC
passed is what the build's own check said when the version was saved.
A version built before there was a verdict, or read off a file, has
passed: null and reads not judged; the row then falls back to gaps
and ERC, the two things a summary can see. KiCad DRC runs during the
build and is not persisted into board.json, so nothing here re-runs
it.
Keys: Enter opens the selected board, F forks it, Slash or two taps of the space bar go to the command line, Escape leaves it.
On a phone the disc is a sheet along the bottom: a grip with the name and the verdict, tap to open, tap a row to open it on that board.
The foot
The machine, as a row of chips with a rule under each in its state's
colour: freerouting (version), A* (the second router, tried first
since 13 Sep; until 17 Sep the app never asked astar.ready(), so a machine
without it read as ready), kicad-cli (the DRC judge and the renderer),
model (the backend and model that will design boards, or none and
why), catalogue (parts on disk, their source and the day it was built —
read off the manifest file, never the network); then running (jobs under
way), boards and trash. Hover a chip for what it means or why it is
not green. All of it is GET /api/app.
Making one
From a sentence. Type on the command line and press Enter (Shift+Enter for a new line). The project is created immediately, named from the first words of the prompt, and you land in its workbench with the build already running. When the first board arrives the project takes the board's own name from the model.
From a design.json. Import design.json takes a file in the design format — for instance one saved by the command line, or one you edited by hand — and builds it without calling the model. Useful when there is no API key, or to reproduce a board exactly.
From a finished .kicad_pcb. Open a file — or drop the file anywhere on the page — reads a board that already exists and shows it. The parts, where they sit and what is wired to what all come across; the traces already on it do not. Nothing is designed, resolved against stock, priced or routed, so it answers in about a fifth of a second: a 47-part split keyboard out of ergogen opens in 0.21 s.
What did not happen is on the board rather than implied by its absence. The readout says ERC not run and Route not run, the cost reads not priced, and the gaps say the parts are this file's footprints rather than orderable parts. Every decision the reader made on the way in is a note: parts brought up from the back, rotations snapped to a quarter turn, footprints dropped for having no copper, a pour that has nowhere to go here. A board this pipeline cannot build — more than two copper layers, over 150 mm, over 120 footprints, a ball grid array — is refused with the reason rather than half-read.
From a project .zip. A project exported from another machine — Export .zip on the disc, or the History tab's Export project — comes back whole through the same Open a file button or drop: every version, the draft, the conversation. Nothing is built. See Exports and files.
Blank. A name with no prompt makes an empty project; the first message in its workbench builds v001.
Fork. From the disc, F on a selected row, or any version in the workbench's History tab. The fork starts at v001 with a copy of the chosen version's files and remembers where it came from (parent: <project>/<version>).
Pinning the microcontroller
Type @ on the command line (or press @ MCU) and the roster drops down:
the five modules, each with its family, LCSC code, live price and stock.
What you type after the @ filters it; Up / Down move, Enter
pins, Escape closes. The pinned module sits in front of your words as
a token, the line under it says what the token resolves to (@pico-2w →
Raspberry Pi Pico 2 W · LCSC C42394205 · soldered down · looked up, not
searched for), and the disc shows the module while you choose: the real
vendor mesh, drag to turn; size, price and stock off the live path; the
input and what its rails can supply; the antenna keepout and whether it was
declared or inferred; the notes; and the mount — soldered down on its
own pads, or socketed on headers where the module has that choice. Click
the token to see it again; its × unpins.
It is a list and not a search, on purpose. Every other part is found by
resolve() searching JLCPCB for what the model asked for, which is right
for a jellybean: there are forty 10 k 0603 resistors and any of them will
do. It is wrong for the MCU. A module is the one part whose exact identity
decides the pinout, the footprint, the power rails and half the layout — and
it is the part the search loses most often. An unresolved 10 k costs a
resistor; an unresolved ESP32 costs the board. Pinning it means the LCSC
code is known before the model writes a word, and resolution for the most
important part on the board stops being a search and becomes a lookup. On
Build, the mount's prompt prefix goes in front of your words — the same
prefix the old picker sent, from platforms.prompt_prefix().
soldermask/platforms.py declares only what no CAD data carries: which
symbol has the pin names, how the module may be mounted, what its rails can
actually supply, and where copper may not go. Geometry, price, stock and
the 3D model are not there — they come from the live path by LCSC code,
the same one every other part uses, because a registry that copied them
would be a second source of truth that goes stale.
The roster stays off until GET /api/platforms answers: it is worth nothing
without its data. three.js and the mesh decoder (685 KB) are fetched the
first time the disc shows a platform, not with the page.
Filter and sort
The filter matches name, summary, description and tags. Sort by recent, name, cost or number of versions. Archived projects are hidden unless the Archived facet is lit.
The note under the command line
It names the backend and model that will design boards (gemini · gemini-3.8-flash), or says why none is available. Without a model you
can still open a design.json. If freerouting is not ready it says so
too: boards will save unrouted. The foot carries the same facts as
chips, with the rest of the machine beside them.