Co-authored-by: mhoennig <michael@hoennig.de> Reviewed-on: #54
14 KiB
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 — the brief, with the nine decisions D1 to D9.
- RFC 0001: Web UI Instrument Panel — the visual language the page borrows its tokens, status dots and icons from.
- The four accepted artboards under
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/<guessed>/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
SystemHealthjudges by the pressure values; that is a separate PR. - No new API. The page is fed by
GET /api/repos,GET /api/systemandGET /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/<name>/…,/api/repos/<name>/…) 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
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
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
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
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
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
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
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
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-schemefollows the choice, so form controls and scrollbars do not stay on the browser's idea of it
- and the choice is set on the root element by an inline script in
Verified by
- UiControllerTest: "every page carries the light/dark switch, applied before the first paint"
- DarkModeTokensTest
The Solution
The route (D2).
/ is the instance's home page; the repository's Latest view moved to /latest, reachable scoped as /repos/<name> and /repos/<name>/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:MMofUiFormats.timestamp, as on the Repositories page, where the draft showed a relative time ("2 h ago"). A relative format would need a second implementation inwerkator.jsto 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/durationswas 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 ondurationswas corrected with it. - Two sentences in the PR#53 doc were wrong: commit
bbb892freached 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/<name>/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
/systemplainly. They were written with a#<key>fragment, but the system page's cards carrydata-gauge, not matching ids, so the fragment would have been a dead anchor. .growneededmin-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.