> **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. ## Related Links - [PR#42](2026-09-09-PR%2342-sizing-corrections.md) — corrected the sizing numbers; this PR corrects how loudly they are said, and adds the order button this PR now gates. ## The Problem **A full quota was as red as a full disk.** The SSD group quota on the production host reached 92 %, so the advice chip turned red and the machine verdict read *overloaded*. Neither is true in the way the colour claims: a quota is raised by asking a service desk, nothing stops while it is being asked for, and the hardware behind it had well over a terabyte free. Red belongs to what cannot be asked for. **The build pages showed every repository's work to everyone.** The pages are readable without any token — that is deliberate, a build page is meant to be linkable — and they name every branch, every commit, every artifact and every log line of every served repository. For a repository that is private on the forge, that is more than a passer-by should see, and the instance is on the internet. **The order button offered to spend money.** PR#42 put a button on the System page that composes a hardware order; it appeared for anyone who opened the page, and it named the webspace and the machine while doing so. ## Non-Goals - No login, no sessions, no users. The control token is what Werkator has, and this PR only makes it readable earlier in the request. - No configuration key for visibility. The forge knows, and a second place to declare it could only drift from the truth. - Private repositories are not removed from the overview. That a repository exists, that it builds, and that a build failed stays visible; only what names the work goes. - Nothing changes for a public repository, or for an instance with no forge configured. ## The Scenarios ### Feature: how loudly an advice is shown #### Scenario#43.01: A quota to raise is yellow, a filling volume is red So that a colour says how urgent a thing is, not merely what kind of thing it is. - **Given** a disk whose peak is 92 % of a group quota - **and** whose physical volume has room to spare - **Then** the verdict stays `upgrade` and the chip is drawn in the watch colour - **and** the machine verdict counts it as pressure, not as overload - **Given** the same fill against the volume itself, or against a quota whose volume is the tighter budget - **Then** the chip is drawn in the critical colour and the machine verdict counts it as overload - **because** raising a quota whose backing is what runs out buys nothing ##### Verified by - [CapacitySizingTest](../../src/test/kotlin/de/hoennig/werkator/metrics/CapacitySizingTest.kt) - [UiViewsTest](../../src/test/kotlin/de/hoennig/werkator/server/UiViewsTest.kt) ### Feature: private repositories #### Background - The pages and the JSON API are readable without a token; only mutating endpoints have ever required one. - A repository's visibility is the forge's own fact, reachable through the Gitea repository API. #### Scenario#43.02: What a visitor sees of a private repository So that a build page can stay public without publishing the work behind it. - **Given** a repository the forge calls private - **and** a browser without the control token - **When** any page or API of that repository is requested - **Then** its name reads `hidden private repo`, greyed out, and carries no link - **and** branch, build name and commit read `n/a` - **and** the artifact key is empty, so no artifact, no log and no permanent branch link is offered - **and** no link into the forge is rendered - **and** the redaction happened on the server: the JSON the browser received never held the values - **And then** the counts, the statuses and the timestamps are untouched, and the shapes are unchanged, so the page renders the repository like any other - **Given** a public repository - **Then** nothing changes at all ##### Verified by - [RedactionTest](../../src/test/kotlin/de/hoennig/werkator/server/RedactionTest.kt) - [BuildsApiControllerTest](../../src/test/kotlin/de/hoennig/werkator/server/BuildsApiControllerTest.kt) #### Scenario#43.03: The overview keeps the shape, loses the identity - **Given** a private repository among the served ones - **Then** its entry shows `hidden private repo` and is not clickable - **and** its failed builds are listed as `n/a`, with their timestamps - **and** its branch count, running and pending counts and watcher problem stay ##### Verified by - [RedactionTest](../../src/test/kotlin/de/hoennig/werkator/server/RedactionTest.kt) #### Scenario#43.04: The forge is asked, and the fallbacks are asymmetric So that nothing has to be configured, and a failure does not publish a repository. - **Given** a repository whose forge answers `private` - **Then** it is hidden; answered `public`, it is shown - **Given** a repository with no forge configured - **Then** it is public, exactly as before this change — there is nothing to ask and nothing to hide behind - **Given** a repository whose forge is configured but does not answer - **Then** the last answer stands, and without one the repository is hidden - **And** the answer is cached, so the pages' polling never reaches the forge ##### Verified by - [RepositoryPrivacyTest](../../src/test/kotlin/de/hoennig/werkator/server/RepositoryPrivacyTest.kt) #### Scenario#43.05: The token in a cookie reveals, the token in a header acts So that a server-rendered page can decide what to show, without weakening what protects the actions. - **Given** a browser that has been given the control token - **Then** it is kept in local storage as before *and* in a cookie - **and** the cookie makes every page and every API answer unredacted - **and** restarting or deleting a build still requires the token in the request header - **Given** a foreign page that makes the browser send the cookie - **Then** it gains nothing: it can neither read the response across origins nor set the header the actions require ##### Verified by - [BuildsApiControllerTest](../../src/test/kotlin/de/hoennig/werkator/server/BuildsApiControllerTest.kt) ### Feature: the order button #### Scenario#43.06: Only the operator may order - **Given** a browser without the control token - **Then** `GET /api/system` reports no hosting, so neither the order button nor the webspace and machine names reach it - **Given** the control token - **Then** the button appears as in PR#42 ##### Verified by - [SystemApiControllerTest](../../src/test/kotlin/de/hoennig/werkator/server/SystemApiControllerTest.kt) #### Scenario#43.07: An action nobody may take is shown, but inert So that the page says what exists without inviting a click that can only fail. - **Given** a browser without a valid control token - **Then** the restart, delete and cancel buttons are rendered greyed out and disabled - **and** their tooltip says why: `Needs the control token` - **and** this holds for the server-rendered rows and for the rows the script re-renders alike - **And** the header carries a key button that asks for the token and reloads - **because** with every action disabled there would otherwise be nothing left to click that asks for it - **Given** a valid token - **Then** the buttons behave as before, and the key button offers to forget it again ##### Verified by - [UiControllerTest](../../src/test/kotlin/de/hoennig/werkator/server/UiControllerTest.kt) ## The Solution `ResourceAdvice` gains an `askable` flag and, from it, a `severity` in the page's own `ok`/`warn`/`crit` vocabulary. The chips take their colour from the severity rather than from the verdict, so the text keeps saying *upgrade* while the colour says how loudly; `SystemHealth` applies the same rule to the machine verdict. A disk is askable when its budget is a quota and the volume behind it is not the tighter budget. `GiteaClient.readVisibility` asks the forge, and `RepositoryPrivacy` caches the answer per repository with the fallbacks of Scenario#43.04. `Redaction` is one object holding what a redacted build, branch and overview look like, applied in the controllers before anything is serialized — never in the page. That is what keeps the frontend unaware: same fields, same types, one row per build, and a first paint that already agrees with the first poll. The same cookie tells a page whether it may act: `canAct` reaches the templates and, as a `data-` attribute, the script, so the disabled state is decided in one place and by the only party that can tell a valid token from a stale one. `ViewerIdentity` reads the token from the cookie, from the current request where a controller has many handlers. The cookie is written by `werkator.js` next to the local-storage copy it has always kept, including once on load for a browser that was given the token before this change. ## Open Questions - The visibility cache has no invalidation other than its fifteen minutes; making a repository public shows up within that window, not immediately. - A repository is hidden as a whole. A forge that distinguishes internal from private is treated as private. ## Additional Changes - Version 1.5.0 and its release-notes entry, since this is what gets deployed.