See docs/prs/2026-10-15-PR#163-package-de-javagil.md 🤖 Generated with [Claude Code](https://claude.com/claude-code) Reviewed-on: #163 Reviewed-by: mi <1+mi@noreply.git.javagil.de> Co-authored-by: claude-code <claude-code@javagil.de>
8.1 KiB
RFC 0002: What Fills a Disk, and When the Volume Runs Out Behind the Quota
Status:
- proposed: 2026-09-09
- accepted: 2026-09-09 (A1 and B1; see Decisions)
- rejected: -
Proposal: The System page answers two questions it cannot answer today. First, what fills a monitored disk — as a breakdown of the categories Werkator itself owns, plus one honest "other" slice for everything it does not, shown in a dialog opened from the disk card. Second, that a disk can be physically running out while its quota still has room — by keeping the managed budget as the gauge and drawing the volume's ceiling into it, plus a machine-verdict reason. Both were asked for with "this has to be evaluated first", so this RFC is the evaluation; the drafts are meant to be lifted from, not merely looked at.
Context and Problem Statement
Since PR#16 the disk gauge shows the tightest of three candidate budgets: the user quota, the group quota, and the volume itself (DiskQuota.bindingDiskSpace takes the one with the least free space).
Since PR#33 there may be several monitored disks, each with its own gauge and its own history series.
That is enough to say how full a disk is, and nothing else.
Two gaps follow from it.
A — the gauge says how full, never why.
When the SSD gauge turns yellow the only lever available is guesswork: build workspaces, artifacts, unpacked build environments and the checkouts themselves all live under directories Werkator created, and none of them is broken out.
The one existing number, repoSizeGib, is the sum of the primary checkouts and is shown as a tile, so it neither adds up to the gauge nor separates what can be pruned from what cannot.
B — the tightest budget wins silently.
Picking the minimum is right for the number, and wrong for the explanation.
On a Hostsharing Managed Webspace the quota is normally the binding budget, so the gauge shows the quota; if the underlying volume then fills up — through other users of that volume, nothing Werkator did — the volume becomes the tighter candidate and the gauge switches to it.
Three things happen at once, none of them announced: the total jumps from a few hundred GiB to a couple of TiB, the percentage drops although the situation got worse, and the disk's min/max/avg aggregates are reset on purpose (PersistedMetricsState.diskSourceKey) so the old ceiling does not survive.
The reading is correct at every moment and unreadable over time.
A — What fills a disk
What can be measured, and what cannot
Werkator can attribute exactly what it created, per repository, all of it below directories it already knows:
| Category | Where | Prunable |
|---|---|---|
| Build workspaces | .git/werkator/worktrees/<branchKey> |
yes, by the watcher's stale-worktree pruning |
| Build environments | .git/werkator/buildenv/<envKey>/rootfs |
yes, per envKey |
| Artifacts and logs | the artifact store under XDG_STATE_HOME/werkator/artifacts/<repo> |
yes, by retention |
| Repositories and history | the checkout and its .git, minus the two above |
no |
| Other | the budget's used space minus the sum of the above | not by Werkator |
"Other" is the honest part: on a user quota it is the rest of that user's home, on a volume it is every other user of the volume. It must be shown, and it must be named as not-Werkator's, otherwise the pie claims an authority it does not have.
Two things are deliberately not attributable and should not be faked: a Docker image or volume (owned by the daemon, possibly on another filesystem), and a Gradle cache inside a container.
Cost
A breakdown is a directory walk, and a walk is the one expensive thing this collector does.
It is affordable because the walk already happens: readRepoSizeThrottled walks every repository and throttles itself to one probe per REPO_SIZE_PROBE_EVERY_SAMPLES samples.
Tallying that same walk into five buckets instead of one sum costs nothing extra; only the artifact store and the buildenv directory are new subtrees, and both are shallow.
On the development machine the full walk of a Werkator checkout with seven worktrees takes well under a tenth of a second warm; on a webspace over network storage it is slower, which is exactly why it must stay on the existing throttle and must never be triggered by opening the dialog.
Drafts
- A1 — a dialog from the disk card. A donut plus a legend with absolute sizes and shares, the header naming the budget and when it was measured. Costs a button in the card and a dialog element; shows enough numbers to act on.
- A2 — a stacked bar in the card, no dialog. Nothing to open, but it takes height in every disk card on a page that already has six gauges, and a 4 % slice is a sliver with no label.
Recommendation
A1, with the numbers coming from the existing throttled walk and served as part of GET /api/system (one breakdown per disk), so the dialog is a rendering of data the page already polls rather than a request of its own.
A donut over a pie: the hole carries the fill percentage, which is the number the card already shows, so the dialog and the card agree at a glance.
B — Quota free, volume full
What the reading should be
The three candidates do not compete for one truth; they are three different statements:
- the quota is what this installation is allowed to use — the budget to manage, and the one a capacity decision is about,
- the volume is what the hardware can still absorb — a constraint, not a budget,
- the binding budget is whichever of them will stop a build first.
Today only the third is shown. The fix is to keep showing the budget that is managed, and to show the constraint in it.
Drafts
- B1 — the quota keeps the gauge, the volume marks its ceiling. The bar gains a marker where the volume's free space runs out and a hatched tail for the part of the quota that is no longer backed by free space; the note line names both numbers. One card, one history, no reset.
- B2 — two gauges, quota and volume. Symmetric and unambiguous, but it doubles the disk cards on every host where the volume is never the limit, which is all of them most of the time.
- B3 — a reason in the verdict panel only. No change to the cards:
SystemHealthgains a rule and the panel says it. Cheapest, and the one place a reader scrolls past first.
Recommendation
B1 (decided; B3 was offered alongside and rejected): the card carries the marker and the note.
bindingDiskSpace keeps deciding what the level is — a volume about to fill still has to turn the gauge red — but the scale stops moving: the gauge stays on the budget that was binding when the series started, which also retires the aggregate reset.
Decisions
Taken on 2026-09-09:
- A1 — the breakdown is a dialog opened from the disk card, with the donut and the legend. A2 is rejected: the disk cards may not grow.
- The five categories of the table above, "other" among them and named as not Werkator's.
- B1 alone — the card carries the marker, the hatched tail and the note line. B3 is rejected for now: the verdict panel keeps the rules it has, and the statement stays where the numbers are. B2 is rejected as a doubling of the cards.
- Per disk, as the drafts show, because that is what the button sits on. A per-repository split of the same walk answers a different question ("which repository should be pruned") and can be added later inside the same dialog.
Consequences
SystemMetricsCollectorgains a per-category tally in the walk it already does, andDiskMetricsabreakdown; nothing new is sampled every 15 seconds.- The disk gauge stops resetting its aggregates when the binding source changes, which is a behaviour change of PR#16 and needs its scenario updated.
- The System page gains its first dialog. It must work without JavaScript to the extent the rest of the page does: the button is rendered only when the data is there, and the dialog is a
<dialog>element, not a hand-rolled overlay.
