Captures a long infra session: fleet *.internal DNS (git-sourced, 42 names, three resolvers including a new colo one), waterland studio containerised on irv-ml1, Homepage cleaned up and themed with Australis Skyfall over an Arbo-generated background, and four unmanaged stacks adopted into stacks/. Four detail files added. Auto-archived 4 entries to archival-memory.md (Recent decisions 2, Tried and abandoned 2); 5 held back by the open-deferred guard rather than moved. Also records three operator-owned open items: the colo DNS repoint, the static-v6 convention, and the deliberately belayed AI-tab Dormant regrouping.
7.5 KiB
[2026-08-19] Homepage cleaned up, then themed with Australis Skyfall + an Arbo-generated background
Commits 9d92c4b, c3de7db, 45c1995, f38cf69, df68dd2.
The cleanup (three real defects)
- UltraSeedbox rendered on all four tabs. The bookmark group had no entry in
settings.yaml'slayout:block at all, and Homepage's documented behaviour is that a group with notab:is shown on every tab. Pinned to Main. ⚠️ This will happen again to the next group added without atab:— the rule is now written at the top of the layout block. - Uptime Kuma rendered twice — a manual
services.yamlentry under Monitoring andhomepage.group=Appson the container. Exactly the "never list a labelled container manually" failure the stack README warns about; it survived the previous day's audit because a duplicate reads as two plausible cards rather than as an error. Manual block deleted, label moved toMonitoring,homepage.siteMonitoradded. - Column counts were fiction — several groups declared more columns than
they had members, so the last row of each was dead space (Notes: 1 card in a
4-wide row). Columns now track member counts;
GET /api/servicesprints the live per-group counts and is the check.
Later, on operator instruction, the AI tab was reordered by clickability:
Gateways & Chat → Image & Media → Audio Tools on top, then the vLLM /docs
seats and TTS endpoints. Reasoning written into the config so it survives:
order by "would I click this?", not by how central the service is.
⚠️ The expensive red herring — the tab bar after a recreate
After a recreate the client render comes up with no tab bar, no wallpaper and
no i18n (search box shows the raw key search.search), groups falling back to
side-by-side columns. It restores itself with no intervention.
Timing, measured rather than assumed: a fresh container was still tab-less at
4m30s, twice; it was healthy again after roughly an hour. docker ps
reporting healthy says nothing about it — the container is serving, the page
is just wrong.
An hour went into ruling out four causes that were never the cause:
- Not the config — restoring
settings.yamlandservices.yamlto their committed versions reproduces it, as does the pre-adoption backup in/opt/docker-bu/conf/homepage/. - Not the v2.0.0 release — a throwaway container on
v1.13.2shows identical symptoms, and the image never changed anyway (working and broken both reportv2.0.0/ rev17456f2). - Not
PUID/PGID, and not Docker discovery — tested both, and with the socket unmounted entirely. - Not server-side — the server-rendered HTML still contains the tab
markup, the background URL and
useEqualHeights;GET /api/validatereturns[]. The loss is client-side, with no page error, no failed chunk and no non-200.
Every throwaway container in that list was judged within ~30s of starting, so they were all inside the same window — and that consistency read as a reproduction when it was the same measurement mistake five times over.
Operative rule: recreate, walk away, re-check later. Do not chase it.
⚠️ The iteration loop that would have prevented the overcook
custom.css is served per request from /api/config/custom.css, so a CSS
change needs a browser reload — not a container recreate, and it never owed
the layout warm-up above. Conflating the two costs ~10 operator-visible minutes
per attempt (operator called this out directly).
Faster still, and how the final pass was done: inject candidate CSS into the
running page and screenshot it —
await p.addStyleTag({content: css}) in Playwright against the live
dashboard. Seconds per iteration, no deploy. Build + deploy only once the
render looks right.
The theme — Australis Skyfall
Operator supplied a Claude Design handoff bundle via the Booth (26-copper).
Skyfall is a dual-theme OKLCH system: one lightness law across every chromatic
family (deep 0.48 / base 0.66 / bright 0.80), all hues cooler than neutral, a
Sea neutral ramp drifting ice-blue→ocean-green as it brightens, and a
"calm depth" language of hairline + two-layer shadow on every elevated
surface, never one without the other.
theme/colors.css layout.css typography.css vendored VERBATIM from the bundle
theme/fonts/Supreme-{400,500,700}.woff2 the body/UI face
theme/skyfall.css.in the Homepage bindings (ours)
theme/build.py → conf/custom.css (generated — do not hand-edit)
The build step exists for one reason: Homepage serves only custom.css and
custom.js out of its config dir, with no static route beside them, so a
@font-face pointing at a vendored .woff2 would 404 — the face must arrive
as a data URI. The background image takes the other road, because
/app/public/images is a real static route (mounted read-only in
compose.yaml).
Only Supreme is embedded: a link dashboard has no display type, and Victor Mono ships as 2.4 MB TTF statics per cut — 30x the whole stylesheet for a handful of latency figures.
The background is generated, not stock
Arbo as an image-gen engine (the operator's actual ask, which I first
misread as "use Arbo's palette" and had to redo). Arbo's t2i-ui-background
workflow is purpose-built: "abstract full-bleed backgrounds, no subject".
Job 13f0891f4e42, seed 26, flux2-klein-9b, 2048×1152, 1.6 MB PNG → 22 KB
WebP (smooth gradients compress absurdly well).
⚠️ Arbo API gotcha: prompt is a discriminated union, not a string — a
bare string 422s. {"kind":"raw","text":…,"negative":…} is the shape.
Two documented deviations from the design system
- Skyfall forbids this background. Its rule is "flat semantic surfaces; no
photography, no textures", with one permitted motif — a subtle aurora
gradient on hero/empty-state areas only, "never behind body text blocks".
A dashboard is a body-text block. Present on the operator's explicit
instruction, mitigated rather than excused: abstract, no subject, strictly
cool temperature, held at
opacity: 30. That number is load-bearing — at 14 the aurora was invisible, and turning it up makes the cards fight the ribbon. - Service icons stay full-colour vendor logos. Desaturating them from CSS only makes them illegible.
Overcorrection, and the colour pass
First stat-well pass went from font-thin 13px straight to bold 22px in
heading white — operator: "went from subtle to BASH YOU OVER THE HEAD."
The principle missed: a stat only has to out-rank its own label, not the
service name above it. Now --text-md medium in cyan.
Colour was then lifted from inside the system: Skyfall names Aurora (blue, cyan, green) the primary families, "used generously, in that order", while Dawn (amber/red/violet) is semantic-only. So group markers cycle blue→cyan→green down the page (icons full strength, names at 0.72), service icons take a single cool wash, latency tags move to the info family so "how fast" stops looking like "is it alive". No Dawn colour is used decoratively anywhere.
Two DOM findings that made it possible:
- Homepage renders mdi icons as a gradient behind an SVG mask — recolour
via
background, notcolor. - Homepage emits
docker-status-<state>, notstatus-<state>. The original selectors matched nothing, so every green pill up to that point was stock colouring rather than the theme. Both forms are now matched.