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

+43 -4
View File
@@ -388,17 +388,56 @@ mutable at runtime from the phone.
the time one after the other does. The saving only appears when more than
one has work, which is what a pull produces.
The status is per component (`ComponentStatus`), so a card draws each
component's bar, timing and last line inside that component's own row;
component's bar and last line inside that component's own row;
the project's own area at the bottom keeps only what belongs to the whole
project, which is fetching and pulling. A bar under the card could only
ever say that *something* was happening, and with everything building at
once that is exactly what the reader is trying to find out.
A failure no longer stops the others -- they are already running -- so
the first failure in declaration order is the one reported, which is what
a walk in that order would have said. The phone draws every component in
A failure no longer stops the others -- they are already running -- and
it is reported against the component whose command it was, never as the
project's. The phone draws every component in
a card of its own, including a project with only one: the flat layout it
used to get meant two shapes to keep in step, and put that APK's size up
beside the card's corner controls where it read as belonging to them.
- **There is a build slot per component, not per project, and the two
halves have to agree on that.** A component being built neither blocks
another's build nor disables its controls: `Inner` has no `building`
flag, only a `ComponentRun` per component whose open `step` *is* the
answer, and `claim` writes that entry synchronously under the same lock
the route answers from -- so nothing can read a component the request
just claimed as idle, which the phone would take for "the build is
over". `RunningBuild::building` stays, but it means "anything at all is
happening here" and is only for the controls that act on the whole
checkout; anything about one component reads that component's `step`.
The app mirrors the split exactly: `ProjectState` for the pull and the
project-wide Rebuild, `ComponentState` keyed by component name for
everything a single component is asked to do. One map keyed by project
alone is what the bug was -- pressing Update on one client of a
two-client project disabled the other's button and drew this one's
download bar under it -- and two hierarchies rather than one keyed by a
pair is what stops it coming back, since a download has no
project-wide meaning to be stored with.
The exception, and it is worth keeping visible so it does not read as
more of the same: **a pull really is exclusive with everything.** There
is one checkout, and it rewrites the files every component builds from,
so `Inner::pulling` blocks any component from being claimed and waits
for any still building. Ending the pull and claiming what it decided to
build happen under one lock for the same reason `claim` is
synchronous -- a phone polling in the gap would see a project that is
neither pulling nor building and call the run finished.
- **A finished component shows nothing, and its button goes back to
normal.** Iris's call, 2026-09-01: "you shouldn't see the time it took
once it finishes, it should just go back to its normal enabled button
state." So `ComponentBuildProgress` draws only while the step is open,
and the elapsed times are gone from both halves of the wire -- a bar, a
count and a last line all describe something happening *now*, and every
one of them sits there looking live beside a sibling that genuinely is.
What says the build landed is the control becoming pressable again and
the card's own freshness. The failure is the exception, because it is an
outcome rather than residue; it is drawn by the component card next to
the Retry that acts on it, in the one place that reports that
component's failures whether they came from a build, a download or a
service action.
- **A project's own `.dev-updater.ron` is a request, never an
instruction.** It only runs once accepted from the phone, which copies
it into `config.ron`; `AppEntry::pending_declaration` is the whole gate.