Files
werkator/docs/prs/2026-09-08-PR#33-system-page-disks.md
2026-09-08 15:17:33 +02:00

122 lines
7.5 KiB
Markdown

> **WARNING:** This document describes only the change applied in this PR.
> It may already be outdated once the next PR is merged.
> Historic PR-documentation is not maintained along with new PRs — treat it as a snapshot, not as current documentation.
## The Problem
The System page (PR#29, PR#32) measures one disk: the filesystem of the served repository's checkout.
A Hostsharing Managed Webspace has two: the SSD home directory and the HDD tree under `/home/storage/<pac>/users/<user>`, each with its own group quota.
The plan is to keep the artifacts, which grow, on the HDD and everything else on the SSD.
Whether the HDD needs booking is then a question the page cannot answer, and the mount path is no title for a gauge.
A second problem surfaces with that plan: the measurement series live in `system-metrics-state.json` in the artifact root.
Moving the artifacts to the HDD would move the history with them, onto the slow disk.
## Non-Goals
- Detecting SSD versus HDD; the labels come from the configuration.
- Moving the artifacts, the werkdock image store, or anything else; this PR only measures.
- Keeping the flat `diskTotalGib`/`diskUsedGib`/`diskFreeGib`/`diskSource` fields of `GET /api/system`; `werkator.js` was their only consumer.
## The Scenarios
### Feature: The System page shows every configured filesystem
#### Background
- The instance configuration `~/.werkator.yml` owns what is the host's, not a repository's (ADR 0009).
- A *monitored disk* has a label (the gauge title), a path on the filesystem, and a key naming its series.
- The reading of a disk is the tightest of the user quota, the group quota, and the volume of that filesystem (PR#16).
#### Scenario#33.01: `metrics.disks` is an instance section
So that a host's mounts are configured once for the instance, and a branch cannot reach them by construction.
- **Given** `~/.werkator.yml` declares `metrics.disks` with a label and a path per entry
- **When** a repository's effective configuration is loaded
- **Then** the section comes from the home file alone
- **and** a copy in a repository's own files is ignored with one warning, like `server`
- **and** the key of an entry defaults to its label lowercased (`SSD` → `ssd`), with an explicit `key` taking precedence.
##### Verified by
- [ConfigLoaderTest](../../src/test/kotlin/de/hoennig/werkator/config/ConfigLoaderTest.kt)
#### Scenario#33.02: every configured disk gets its own gauge, history, peak, health reason, and sizing chip
So that the SSD and the HDD are judged side by side, and the page says which of the two to book more of.
- **Given** two entries, `SSD` on the home directory and `HDD` on the storage tree
- **When** the collector samples
- **Then** each entry is read against its own filesystem's quota lines
- **and** its series are `disk.<key>.usedGib` and `disk.<key>.freeGib` in the state, the history, and `GET /api/system/history`
- **and** `GET /api/system` carries `disks[]` with key, label, total, source, and the aggregates, plus `capacity.disks[]` with the advice per disk
- **and** the page shows one gauge per disk in the configured order, one footer total each, and one chip each in the verdict panel
- **and** the machine verdict names the disk by its label (`HDD at 85 % (8.50 of 10.00 GiB)`).
- **Given** no `metrics.disks` section
- **Then** the page shows the checkout's filesystem as before, labelled `Disk`, under the key `disk`.
##### Verified by
- [SystemMetricsCollectorTest](../../src/test/kotlin/de/hoennig/werkator/metrics/SystemMetricsCollectorTest.kt)
- [SystemHealthTest](../../src/test/kotlin/de/hoennig/werkator/metrics/SystemHealthTest.kt)
- [CapacitySizingTest](../../src/test/kotlin/de/hoennig/werkator/metrics/CapacitySizingTest.kt)
- [SystemApiControllerTest](../../src/test/kotlin/de/hoennig/werkator/server/SystemApiControllerTest.kt)
- [UiViewsTest](../../src/test/kotlin/de/hoennig/werkator/server/UiViewsTest.kt)
#### Scenario#33.03: the history recorded for the single disk continues as the first configured disk
So that switching a running instance to two disks keeps the weeks of history the sizing advice needs.
- **Given** a state file written before this PR, with `diskUsedGib`/`diskFreeGib` series and a `diskSourceKey`
- **When** the collector loads it under a configuration whose first disk has the key `ssd`
- **Then** the series and the history columns continue as `disk.ssd.usedGib`/`disk.ssd.freeGib`
- **and** the binding-source reset of Scenario#16.05 still fires only when that disk's source changes
- **and** the other disks start their columns empty.
##### Verified by
- [SystemMetricsCollectorTest](../../src/test/kotlin/de/hoennig/werkator/metrics/SystemMetricsCollectorTest.kt)
- [MetricsHistoryTest](../../src/test/kotlin/de/hoennig/werkator/metrics/MetricsHistoryTest.kt)
#### Scenario#33.04: the measurement series stay in the instance's state home
So that moving the artifacts to a slow, large volume does not move the history with them.
- **Given** a state file still in the served repository's artifact root
- **When** the server starts with this PR
- **Then** the file is moved once to `${XDG_STATE_HOME:-~/.local/state}/werkator/system-metrics-state.json`
- **and** the series continue there, whatever `artifacts.rootDir` says.
##### Verified by
- [SystemMetricsCollectorTest](../../src/test/kotlin/de/hoennig/werkator/metrics/SystemMetricsCollectorTest.kt)
## The Solution
`metrics.disks` is a new instance-level section next to `server` and `executor`: `ConfigLoader` reads it from the home file alone and drops it from repository files with the same warning, so a branch cannot reach it.
Each entry becomes a `MonitoredDisk` (key, label, directory), resolved against the instance's home directory by `MetricsConfiguration`; without the section the served checkout is the one disk under the key `disk`.
`SystemMetrics` carries the disks as a list (`DiskMetrics`) instead of the flat fields.
The collector reads the `quota` output once per sample and matches every disk's file store against it, so two filesystems with two group quotas are two budgets, and keeps the binding source per disk key for the reset of PR#16.
The history tiers take their columns from what is sampled instead of a fixed key list, so a disk added later starts with nulls and one removed ages out.
`HistoryPeaks`, `SystemHealth`, and `CapacitySizing` iterate the disks: a reason names the disk by its label, the sizing advice is `capacity.disks[]` with key and label beside the verdict.
The page renders the gauges, the footer totals, and the chips from those lists; `werkator.js` resolves a card or chip keyed `disk.<key>` against `disks[]` of the polled snapshot and draws its chart from the `disk.<key>.usedGib` column.
A state file from before this PR is migrated on load: `diskUsedGib`/`diskFreeGib` become the first configured disk's series in the aggregates and in all three tiers, and the old `diskSourceKey` becomes that disk's entry.
The state file itself moves out of the artifact root into the instance's state home, once, because the series are the instance's and the artifacts are about to go elsewhere.
## Open Questions
- The default disk without configuration is keyed `disk`, so its series are `disk.disk.usedGib`; a nicer default key would only matter for an API consumer.
- A bare `~` in YAML is null, not a path; the documentation and the template quote it.
## Additional Changes
- The `.werkator.yml` template of `init` carries the section commented out, and `docs/configuration.md` documents it under the instance file.
## Prerequisite PRs
- PR#29 (system page with history) and PR#32 (capacity advice).