Staging: the working set#
Every editor in Wheelhouse stages. It posts the operations it wants to POST
/api/stage, they join a queue held by the agent, and the editor is finished — nothing
has reached the router. That queue is the working set: an ordered list of set
and delete operations that will be sent to the router as one array, in one commit,
when somebody presses Commit. Until then it can be read, added to, trimmed one
operation at a time, or thrown away.
Why staging exists at all#
The router's own HTTP API has no staged-diff mode. A call to its /configure
endpoint applies and commits immediately — that is recorded, with the date it was
verified, in docs/api-cookbook.md. So the staging area
is Wheelhouse's: the agent accumulates the change set in memory and submits the whole
of it as one request, which is one commit
(agent/staging.go, ToConfigurePayload).
That is the difference between "the UI applied nine settings" and "the router performed one commit". Only the second can be shown as a diff, refused as a unit, armed with commit-confirm, and undone with one rollback.
The queue belongs to the agent, not to your tab#
There is exactly one working set per agent process —
agent/main.go creates it once with staging = newStagingArea()
— and everything that stages, stages into it:
- every browser session signed in to that router;
- the reconcile loop, when it is running in its default
stagemode; wheelhouse-agent apply --agent-url …from a shell or a cron job.
The browser adopts whatever the agent already holds when the page loads, and re-reads
it whenever the tab comes back into view
(ui/src/lib/staging.tsx). This is not a nicety. An
index sent to POST /api/stage/remove names a position in the agent's queue, so a tab
that had only appended to its own local copy could un-stage somebody else's operation.
For the same reason, every mutating staging call returns the entire queue rather than
an acknowledgement.
The working set is held in memory and is not written to disk. Restarting the agent empties it. Nothing is lost from the router by that — nothing had been applied — but work you staged and did not commit is gone.
What may enter it#
Two verbs, and nothing else. set and delete are the only operations the Commit Bar
can render honestly, so anything else is refused rather than carried:
POST /api/stageanswers 400 naming the offending index for an operation whose verb is notsetordelete, one with no path, or one with an empty element in its path (agent/main.go,handleStage).StagingArea.Adddrops any other verb even if it reaches that far, andToConfigurePayloadfilters again on the way out (agent/staging.go).
An empty path element is caught here because the alternative is a commit that fails on the router with a message naming neither the field nor the request.
Staging is a write: it needs the operator role and a usable
licence. Both gates are on the route
(agent/main.go, routes()).
A worked example#
R=https://<router>:8443
T=wh_... # an API token with the operator role
curl -sk -X POST "$R/api/stage" -H "Authorization: Bearer $T" \
-d '[{"op":"set","path":["interfaces","ethernet","eth1","description","primary uplink"]},
{"op":"set","path":["interfaces","ethernet","eth1","address","203.0.113.2/24"]}]'{"staged": 2,
"ops": [{"op":"set","path":["interfaces","ethernet","eth1","description","primary uplink"]},
{"op":"set","path":["interfaces","ethernet","eth1","address","203.0.113.2/24"]}],
"commands": ["set interfaces ethernet eth1 description 'primary uplink'",
"set interfaces ethernet eth1 address 203.0.113.2/24"]}Read it back at any time, from any session:
curl -sk -H "Authorization: Bearer $T" "$R/api/staged"
# {"count":2,"commands":[…],"ops":[…]}Change your mind about the second one, then commit the rest:
curl -sk -X POST "$R/api/stage/remove" -H "Authorization: Bearer $T" -d '{"index":1}'
curl -sk -X POST "$R/api/commit" -H "Authorization: Bearer $T" -d '{"confirm_minutes":2}'The ops array rides along with commands on every one of those answers so that a
client never has to re-parse a rendered string back into a path. It cannot be done
safely: a value containing a space does not survive a join-then-split round trip.
What staging does not do#
- It does not validate. The router decides whether a command is acceptable, and it decides at commit. Staging checks the shape of an operation, not its meaning.
- It does not deduplicate. Staging the same operation twice queues it twice. Only
the reconcile paths use
AddIfAbsent, which exists because two clicks of Stage on the Reconcile page, or a loop pass racing a click, used to queue every operation twice — and the seconddeleteof the same node then failed the whole commit (agent/staging.go). - It does not reorder. Operations are sent in the order they were staged.
- It is not a transaction against other writers. Someone committing from an SSH session while your set is staged changes the configuration your commands will land on.
See also#
- The Commit Bar — the component that reads this queue and commits it.
- Every click shows its commands — what the queue renders as.
- Commit-confirm — committing with a rollback timer armed.
- Desired state — the other thing that stages into this queue.
- Agent endpoints —
/api/stage,/api/staged,/api/stage/remove,/api/discard.
Checked against#
agent/staging.go ·
agent/main.go ·
ui/src/lib/staging.tsx ·
ui/src/components/CommitBar.tsx ·
docs/api-cookbook.md ·
docs/ui.md