> **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 - [RFC 0003: A Home Page for the Instance](../rfcs/0003-homepage.md) — the brief, with the nine decisions D1 to D9. - [RFC 0001: Web UI Instrument Panel](../rfcs/0001-web-ui-instrument-panel.md) — the visual language the page borrows its tokens, status dots and icons from. - The four accepted artboards under [`0003-homepage/drafts/`](../rfcs/0003-homepage/drafts/). ## The Problem `/` used to show the Latest Builds of the served repository — with six served repositories, one of six. Three things were missing at the one address a visitor types first. There was no view of the instance as a whole. The Repositories page answers "which repositories are there and what happened last", but neither "what is happening right now" nor "what needs looking after": running and failed builds stand in it beside everything else. The state of the machine was somewhere else entirely. Whether the host is healthy stands on `/system` and nowhere near the page a reader opens first, although it is the question one has on arrival. And the wordmark led to a subpage. `brandUrl` was either `/repos` or the served repository, so clicking the logo landed in a list — a good list, but not a place where the application says what it is. A fourth problem surfaced while writing the brief, and it is a privacy one rather than a design one. A private repository's rows were redacted, but its *name* was confirmable: `/api/repos//builds/latest` answered 200 with redacted data while an invented name answered 404. For a repository named after a customer or an unreleased product, the name is the disclosure. ## Non-Goals - No graphs for Load, Swap, CPU wait and Mem wait on the system page itself. They have none today although `SystemHealth` judges by the pressure values; that is a separate PR. - No new API. The page is fed by `GET /api/repos`, `GET /api/system` and `GET /api/system/history`, which already existed; only one field is added. - No change to what a *trusted* browser sees. Everything here is about the visitor without the control token. - No frontend build, no framework: server-rendered Thymeleaf and the one hand-written `werkator.js`, as everywhere else. ## The Scenarios ### Feature: a private repository is not confirmable by name #### Background - A *visitor* is a browser without the control token; a *trusted* browser has it (PR#43). - The **unscoped** routes (`/latest`, `/api/builds/latest`, …) permanently mean the served repository (ADR 0009) and name nothing. - The **named** routes (`/repos//…`, `/api/repos//…`) carry a repository name in the path. - RFC 0003 D8: the *content* of a private repository is secret — its name, its branch and build-definition names, commits, artifacts and what is in them. Its *metadata* are not — that a build ran, when, how long, with what outcome, and every aggregate figure of the machine. #### Scenario#54.01: A named route of a private repository answers like an unserved name So that guessing a name tells a visitor nothing, the answer must be indistinguishable from the answer for a repository this instance does not serve. - **Given** the instance serves a private repository - **When** a visitor requests a named route of it - **Then** the response is 404 - **and** its status, content type and body are the same as for a name the instance does not serve at all - **and** the check is a cached lookup, so the two do not differ in their timing either ##### Verified by - [BuildsApiControllerTest: "a named private repository answers a visitor exactly like an unserved name"](../../src/test/kotlin/de/hoennig/werkator/server/BuildsApiControllerTest.kt) #### Scenario#54.02: The unscoped route keeps serving the served repository, redacted So that a private repository remains usable as the instance's own repository, the form that names nothing is untouched. - **Given** the served repository is private - **When** a visitor requests an unscoped route - **Then** the response is 200 with redacted rows, exactly as before this PR ##### Verified by - [BuildsApiControllerTest: "the unscoped route still serves the served repository, redacted"](../../src/test/kotlin/de/hoennig/werkator/server/BuildsApiControllerTest.kt) #### Scenario#54.03: The control token opens the named route again - **Given** the instance serves a private repository - **When** a browser holding the control token requests a named route of it - **Then** the response is 200 with unredacted data ##### Verified by - [BuildsApiControllerTest: "the named route of a private repository opens for a browser holding the token"](../../src/test/kotlin/de/hoennig/werkator/server/BuildsApiControllerTest.kt) ### Feature: the bar strip of a repository #### Background - The strip is twelve bars: the height is the build's duration, the colour its outcome. - It is scaled against the *longest build of the same repository*, never against a neighbour's, whose builds may take a hundred times as long. #### Scenario#54.04: The strip holds the last twelve builds, oldest first So that the newest build is always the rightmost bar and the strip reads left to right in time. - **Given** a repository with more than twelve recorded builds - **When** the overview is derived - **Then** the strip holds exactly twelve entries - **and** the oldest of the twelve comes first ##### Verified by - [RepoOverviewsTest: "the bar strip holds the last twelve builds, oldest first"](../../src/test/kotlin/de/hoennig/werkator/server/RepoOverviewsTest.kt) #### Scenario#54.05: A private repository keeps its strip and loses its labels So that the mosaic stays an even grid and a passer-by still cannot tell what the repository is. - **Given** the instance serves a private repository - **When** a visitor receives the overview - **Then** the name, the branch and the commit are redacted - **and** the strip is served unchanged, because duration and outcome are metadata (D8) ##### Verified by - [RepoOverviewsTest: "a private repository keeps its strip but loses everything that names it"](../../src/test/kotlin/de/hoennig/werkator/server/RepoOverviewsTest.kt) ### Feature: the home page #### Scenario#54.06: `/` shows the system rail and one card per repository - **Given** the instance serves several repositories - **When** a visitor opens `/` - **Then** the rail carries the verdict, one row per gauge of the system page and the four tiles, each linking into `/system` - **and** the mosaic carries one card per repository, newest activity first, with its bar strip - **and** the page carries the watcher banner but neither the view tabs nor the repository line (D6) ##### Verified by - [UiControllerTest: "the home page shows the system rail and one card per repository"](../../src/test/kotlin/de/hoennig/werkator/server/UiControllerTest.kt) #### Scenario#54.07: A private repository is a card without a destination - **Given** the instance serves a private repository - **When** a visitor opens `/` - **Then** its card is greyed, italic and not a link - **and** it carries its bar strip, unlabelled (D4) ##### Verified by - [UiControllerTest: "a private repository is a card without a destination, strip and all"](../../src/test/kotlin/de/hoennig/werkator/server/UiControllerTest.kt) #### Scenario#54.08: One served repository gets the home page too So that every installation has the same entrance; the rail is the point of the page and does not depend on the number of repositories (D3). - **Given** the instance serves exactly one repository - **When** a visitor opens `/` - **Then** the home page appears - **and** the single card takes the whole width instead of a third of it ##### Verified by - [UiControllerTest: "with one served repository the home page still appears, its single card full width"](../../src/test/kotlin/de/hoennig/werkator/server/UiControllerTest.kt) ### Feature: light or dark, chosen by the reader #### Scenario#54.09: Every page carries the switch, and a stored choice is applied before the first paint So that the page never flashes the browser's preference before showing the chosen one. - **Given** a reader has chosen light or dark - **When** any page is loaded - **Then** the header carries the switch - **and** the choice is set on the root element by an inline script in `head`, before the body is parsed - **and** `color-scheme` follows the choice, so form controls and scrollbars do not stay on the browser's idea of it ##### Verified by - [UiControllerTest: "every page carries the light/dark switch, applied before the first paint"](../../src/test/kotlin/de/hoennig/werkator/server/UiControllerTest.kt) - [DarkModeTokensTest](../../src/test/kotlin/de/hoennig/werkator/server/DarkModeTokensTest.kt) ## The Solution **The route (D2).** `/` is the instance's home page; the repository's Latest view moved to `/latest`, reachable scoped as `/repos/` and `/repos//latest`. The `AGENTS.md` invariant that the unscoped routes permanently mean the served repository survives intact, because `/latest`, `/branches` and `/history` still do — only `/` now belongs to the instance. The wordmark leads there; `homeUrl`, which confusingly meant the repository's Latest view, is now `latestUrl`. `/index.html` still redirects to the latest builds, which is the page it always named. **The page.** `HomeViews.kt` derives everything from what the API already serves. The rail reuses `SystemMetricsView` for its gauge rows, so a reconfigured `metrics.disks` changes the rail without a change here. The mosaic is `RepoOverviewDto` with one new field: `recentBuilds`, the duration and outcome of the last twelve builds (D1) — so one call of `GET /api/repos` feeds the whole page and `GET /api/builds/durations`, which is the History page's per-definition trend over thirty days, stays untouched. The sparklines are the one thing the server does not draw. They are drawn by `werkator.js` from `GET /api/system/history?range=24h`, exactly as the system page's charts are, with the same `drawSpark` — which now takes its box from the element's own `viewBox` instead of assuming 300×120. Two implementations of one curve would be two things to keep in step; without JavaScript the rail still names every metric, its value and its link. **The privacy line (D8, D9).** `RepoRouting` replaces three copies of `repoOf` and three copies of the privacy check. A named route of a private repository throws the same `UnknownRepositoryException` an unserved name throws, so the answer passes through the same handler with the same body. `RepositoryPrivacy` caches per repository and is refreshed for every served repository on every poll of `/api/repos`, so on a running server this path makes no forge request and the two 404s do not differ in their timing either. **Light and dark.** The dark palette now stands twice in `werkator.css` — once for the browser's preference unless light was chosen, once for a chosen dark — because CSS cannot share a token list between a media query and a plain selector and this project has no frontend build. `DarkModeTokensTest` compares the two blocks, so a colour changed in one and forgotten in the other fails the build. ## Open Questions - The switch has two states, light and dark, and no way back to "follow the browser" short of clearing site data. A third state would need a three-way control; the brief asked for a button. - A card's timestamp is the full `YYYY-MM-DD HH:MM` of `UiFormats.timestamp`, as on the Repositories page, where the draft showed a relative time ("2 h ago"). A relative format would need a second implementation in `werkator.js` to keep in step, and the invariant that the two produce identical formats is worth more than the shorter string. - The residual timing signal of Scenario#54.01 rests on the privacy cache being warm. It is, on any server whose pages are being polled; on a cold cache the first named request for a private repository is one forge round trip slower than for an unserved name. ## Additional Changes - The rationale on `GET /api/builds/durations` was factually wrong: it claimed the build timings themselves were the secret, which its own neighbours (`latest`, `branches`, `history`) disprove by handing them out for every build. The empty list stays — a trend card is grouped by build definition, and with that name redacted every card carries the same label and the list says nothing — but for the real reason. - The D8 rule is now an invariant in `AGENTS.md`, so the next PR does not decide it differently again, and the architecture skill's sentence on `durations` was corrected with it. - Two sentences in the PR#53 doc were wrong: commit `bbb892f` reached main without any pull request at all, and the number 53 belongs to the documentation PR itself. - A repository's row, chip and card now lead to its Latest view (`/latest`, `/repos//latest`) instead of its "UI root". With one served repository that root was `/`, which since this PR is the home page — so the single card on it pointed at the page it sits on. - The rail's rows link to `/system` plainly. They were written with a `#` fragment, but the system page's cards carry `data-gauge`, not matching ids, so the fragment would have been a dead anchor. - `.grow` needed `min-width: 0`, or the grid track grows to the sparkline's intrinsic width and pushes the value out of the rail on a phone. ## Prerequisite PRs - PR#43 — the redaction of a private repository this PR narrows further. - PR#49 — the build-duration trend, whose endpoint this PR deliberately leaves alone. ## Follow-up PRs - Graphs for Load, Swap, CPU wait and Mem wait on the system page, which the rail's tiles link to but which have no chart there yet.