Each component builds on its own, and stops reporting when it is done

A project's components were decoupled everywhere except the one place it
showed: there was a single build slot per project, and a single card state
in the app keyed by project alone. So pressing Update on one client of a
two-client project disabled the other client's button for the length of a
build it shares nothing with, drew this one's progress bar and download
percentage under the other's row, and -- had the button been pressable --
would have been a silent no-op on the server, since a second request while
one was running returned without starting anything.

The slot is now per component. `Inner` has no `building` flag; a
component's own `ComponentRun` with an open `step` is the answer, and
`claim` writes that entry synchronously under the lock the route answers
from, so nothing can read a just-claimed component as idle -- which the
phone would take for a build that had already finished. A failure is
recorded against the component whose command it was rather than in the
project's one error slot, which two components building at once cannot
share.

A pull stays exclusive with everything, because there is one checkout and
it rewrites the files every component builds from. Releasing it and
claiming what it decided to build happen under one lock: a phone polling in
the gap would find a project neither pulling nor building and call the run
over.

The app mirrors the split -- `ProjectState` for the pull and the
project-wide Rebuild, `ComponentState` keyed by component for everything
one component is asked to do. Two hierarchies rather than one keyed by a
pair, so a download has nowhere project-wide to be stored. A component's
failure is drawn in its own row beside the Retry that acts on it, which is
also where a failed service action now reports.

And a finished component shows nothing at all: the elapsed times are gone
from both halves of the wire, and its button simply goes back to being
pressable. A bar, a count and a last line all describe something happening
now, and left up they sit there looking live next to a sibling that
genuinely is.

Verified on the emulator against test-projects/two-clients, which exists
for this: while `tablet` built, its row alone carried the bar and its
button alone was disabled, `phone` stayed pressable and silent, and both
returned to normal with no timing left behind.
This commit is contained in:
iris committed 2026-09-01 03:09:33 -04:00
1 parent 4641b9ec9b
commit 90082bd286
6 files changed
+862 -318

No files matched your search

+12 -6
View File
@@ -168,12 +168,18 @@ Pull acts on the build machine — fetch, fast-forward, then run the command
— while **Update** still means "install what's built onto this phone", so
the two never mean each other.
While it runs, the card says which step is happening, how long it has
taken, what the finished steps took, and the last line the build printed.
The command's output is read as it arrives rather than collected at the
end, so a long build is visibly moving instead of being indistinguishable
from a stuck one — and when it *is* slow, the phase timings say whether
the time went to the network or the compiler.
While it runs, each component's own row says which step is happening and
the last line that component printed. The command's output is read as it
arrives rather than collected at the end, so a long build is visibly
moving instead of being indistinguishable from a stuck one. When it
finishes, all of that goes and the button becomes pressable again: what a
row shows is work in progress, not a report on work that is over.
Components of one project build at the same time and are independent of
each other, so updating one client of a project that builds two leaves the
other's button live rather than making it wait. A pull is the exception —
there is one checkout, so it waits for everything and everything waits for
it.
Deliberate limits, because a phone is a bad place to resolve a mess: