174 lines
9.2 KiB
Markdown
174 lines
9.2 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.
|
|
|
|
## 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.
|