# 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`](../../src/main/kotlin/de/javagil/werkator/metrics/DiskQuota.kt) 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/` | yes, by the watcher's stale-worktree pruning | | Build environments | `.git/werkator/buildenv//rootfs` | yes, per `envKey` | | Artifacts and logs | the artifact store under `XDG_STATE_HOME/werkator/artifacts/` | 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 ![the drafts, light](0002-disk-usage-and-physical-headroom/drafts-light.png) - **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: `SystemHealth` gains 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: 1. **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. 2. **The five categories** of the table above, "other" among them and named as not Werkator's. 3. **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. 4. **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 - `SystemMetricsCollector` gains a per-category tally in the walk it already does, and `DiskMetrics` a `breakdown`; 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 `` element, not a hand-rolled overlay.