Files
werkator/docs/plan/08-web-ui.md
mhoennigandClaude Opus 5 35f06ec1ec Rename GitTally to Werkator
`gitTally` is the name of another product in the git space, so the
rename is a precaution; nothing about what the build system does changes.

The name follows one rule: `Werkator` where it is prose, capitalized
where it is a Kotlin type and its file, lowercase everywhere a machine
reads it — the command, packages, paths, configuration keys and values,
the Gitea check context. Environment variables keep their convention and
are uppercase throughout.

Every configuration file is still found under its pre-rename name
(`ConfigFiles`): `.gittally.yml` at the repository root, in a build
worktree and as committed on a branch, `.git/gittally/.gittally.yml` for
the machine layer. The current name wins where both exist, and the old
file is then ignored rather than merged — two files side by side are a
half-done rename, not a layering. Without the fallback an installation
that updated without renaming would not fail: a configuration that is
not found leaves every setting at its default, so it would come up
looking healthy while having forgotten its credentials and its builds.

`docs/werkator-migrationsplan.md` lists what the fallback does not
cover and has to be moved by hand — above all the state directory
`.git/werkator/`, which holds the build history, the control token and
the worktrees, and has no fallback of its own.

`docs/migration-from-legacy.md` is deleted with this: it mapped the
legacy script's environment variables, and every host it addressed has
long since moved to the YAML configuration.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 19:39:55 +02:00

91 lines
8.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Step 08: Web UI
Prerequisites: step 07.
Read `README.md` and `00-legacy-analysis.md` first.
## Goal
The browsable UI: build overview, history, current build with live log, per-build artifact index.
This step fixes the legacy stuck-loading-animation defect.
## Design
Server-rendered Thymeleaf templates plus a small, hand-written JavaScript file polling the step 07 JSON API.
No SPA framework, no build pipeline for the frontend.
Views (ported from legacy, see analysis for columns and behavior):
- `/` (Latest): latest build per branch — status badge, branch and commit with Gitea links and copy buttons, times, duration, artifact link, restart/delete actions.
- `/history`: all builds.
- `/current`: running builds with live log view (incremental fetch via the step 07 current-builds API), a cancel button per build, and a clear "no build running" state.
Remember there can be several running builds (one per branch, up to `builds.maxConcurrent`); the view must list all of them, not assume a single one.
- `/builds/{artifactKey}`: artifact index — build command, logs, links into archived report directories (rendered from the artifact store, not pre-generated HTML).
- Shared layout: view toggle nav, footer with version and optional impressum link, `prefers-color-scheme` support (port the legacy CSS look loosely, keep it simple).
Robust live updates (the actual bug fix):
- Poll JSON endpoints on an interval (1015s) and re-render table bodies from data; never re-fetch and diff whole HTML pages.
- Every async fetch has a timeout and renders an explicit error/"unknown" badge on failure — no permanent spinners by construction.
- Pause polling when the tab is hidden (`visibilitychange`), resume and refresh immediately when visible.
- Running durations tick client-side from a `data-started-at` attribute.
## Out of Scope
- System metrics page (step 09).
- WebSocket/SSE push; plain polling first, extend later if wanted.
## Tests
- MockMvc view tests: templates render for empty state, running build, mixed history.
- Escaping test: branch names with HTML/JS metacharacters render safely (legacy had injection risks).
- JavaScript logic that is non-trivial (duration formatting, poll scheduling) should live in small pure functions; test via MockMvc-rendered attributes or keep trivially simple.
## Acceptance Criteria
- `./gradlew ktlintFormat` then `./gradlew build` is green.
- Manual smoke test with a real build: status flips pending → running → success in the open browser tab without a manual reload, and a killed server results in error badges, not spinners.
Document the result in this file.
## Implementation Notes (2026-07-07)
Implemented as designed: Thymeleaf templates (`fragments`, `builds`, `current`, `artifact`) rendered by `UiController`, one hand-written `static/werkator.js`, one `static/werkator.css` (loosely ported legacy look incl. dark mode and the mobile card layout), and the legacy favicon.
Pages render the full state server-side and work without JavaScript; the script polls the JSON API (tables 10s, current builds and log tails 3s) and re-renders table bodies from data.
Every fetch runs with an 8s timeout; a failure flips the nav-row indicator to an explicit `error` badge and dims the stale table — there is no loading state at all, so no spinner can get stuck.
Polling pauses on `visibilitychange` and refreshes immediately when the tab becomes visible; running durations tick client-side from `data-started-at`.
Deviations and decisions:
- The tables show one `Started` column instead of the legacy `Commit Time` + `Status Time` pair, and client-side column sorting was not ported; the API delivers newest-first.
- Status badges show the repository status; the per-row Gitea lookup (`/control/status` per commit) was deliberately not ported — that fan-out caused the legacy stuck spinners. `GET /api/status/{commit}` remains available.
- The control token is embedded as a `<meta>` tag in every rendered page (legacy embedded its cancel token in the cancel form the same way); `werkator.js` sends it via `X-werkator-Token` for restart/cancel/delete.
- The restart endpoint moved from `POST /api/builds/{branch}/restart` to `POST /api/builds/restart?branch=…` so branch names with slashes work (resolves the step 07 deviation note).
- Artifact links render whenever a result has an artifact key; the artifact index page itself explains a pruned/missing artifact directory instead of a per-row existence check.
- `/builds/{artifactKey}` renders logs (top-level files) and the topmost `reports/**/index.html` pages from the artifact store; nested index pages below an already-listed one are skipped like legacy. Raw directory browsing is not offered.
- The build command shown on the artifact page is the currently configured one — the command effective at build time is not persisted.
- On `/current`, a build that leaves the running list keeps its card, marked `finished` with a link to its result page; its initial server render shows an empty log (the script fetches from offset 0).
- JS builds all DOM via `createElement`/`textContent`, so re-rendered data cannot inject markup; server-side escaping is covered by a MockMvc test with a hostile branch name.
- New config key `server.impressumUrl` (empty hides the footer link); the footer version comes from Spring Boot `buildInfo()` (`BuildProperties`, build time excluded for repeatability) with a `dev` fallback.
- `UiFormats` (Kotlin) and `werkator.js` intentionally produce the same timestamp/duration display formats.
Manual smoke test (2026-07-07): scratch repository with a bare origin, `pollInterval: 5s`, and a 25s build command; server on port 18982, observed through a real browser tab.
After `git push`, the open Latest tab showed the new build without reload and its badge flipped `running``success` live (`pending` was too short to sample; the row itself appeared via polling).
`/current` showed the build card with streaming live log, ticking duration, and cancel button; on completion the card flipped to `finished` with a working link to the artifact page (status badge, build command, three log links, `reports/demo/index.html` served with no-cache headers).
Killing the server flipped the indicator to a red `error` badge (fetch failure in the tooltip) and dimmed the stale table — zero spinners (verified through a TCP proxy so the tab outlived the process); restarting the server brought `live` back.
The 375px viewport stacked rows as labeled cards, and the History view correctly offers delete but no restart.
Addendum (2026-07-07, after step 12): the legacy Branches view had not been ported; it was added later on request.
`/branches` (nav: Latest | Branches | History | Current | System) lists every branch with its latest build, or an `unknown` row when never built — main/master first, then flat names, then hierarchical names, like legacy.
Deviation: the listing enumerates origin branches instead of legacy's local branches, because the new watcher's branch universe is origin (local refs never move and origin-only branches do get built).
`POST /api/builds/restart` falls back to the branch's origin head when it has no recorded build, so the Branches view can trigger first builds like legacy.
Addendum (2026-07-07): the legacy per-page reload button (`⟳`, top right) was also re-added on request, next to the live indicator.
On polling pages it triggers an immediate data refresh via the page's poller; pages without a poller (artifact index) reload fully.
Addendum (2026-07-07): all links that leave the Werkator UI open in a new tab (`target="_blank" rel="noopener noreferrer"`).
This already held for Gitea branch/commit links and the footer; it was added for the artifact page's log and report links, whose targets have no navigation.
Links between Werkator pages (nav, artifact index) stay in the same tab.
Addendum (2026-08-10): the artifacts column carries the whole build-reachability logic, and the nav lost its `Current` entry.
The permanent `🔗` link is rendered on the build it resolves to — the branch's latest green build — on every build table, instead of on each row of a branch with any green build.
A `📡` link to `/current` appears while a build runs; `/current` itself stays a routable page, it just has no tab of its own anymore.