The board was on the Australis TERMINAL palette, which is dark-only by design
("Always dark first. No light mode in this system"). Skyfall is the dual-theme
web derivative of the same science, and its bundle turned out to be sitting in
this repo's own git history: a predecessor vendored it on 2026-08-19 and a
later commit deleted it. `git show 45c1995:...` returns colors.css with both
`:root` (dark) and `[data-theme="light"]` (Skyfall Day) intact, plus the
calm-depth layout tokens, the typography scale and Supreme 400/500/700. So the
light ramp is canonical rather than derived, which was the entire objection to
building one.
The visual language moves with the palette. Depth is now the recipe and not a
choice — every elevated surface carries a 1px hairline AND a two-layer shadow,
never one without the other. Radii move to Skyfall's scale, cards at
--radius-lg. Widget stat values move from the display face to mono, because
Skyfall is explicit that numbers and telemetry are always --font-mono. The
full-width aurora ribbon under the tab bar is gone: Skyfall sanctions exactly
two accent expressions, the active rail and hero-only glows, and a decorative
gradient across the chrome is neither — so the colour it carried now lands on
the active tab as a 2px accent bar plus an --accent-soft fill, which is the
rail. Every binding is written against the semantic layer; there are no raw
family tokens and no colour literals left in our own file.
build.py now guards the vendoring instead of advising it. The three token files
are hashed and a mismatch FAILS the build — a vendored file is either
byte-identical to the bundle or it is a fork wearing the bundle's name, and the
theme this one replaces had to be torn out twice for exactly that.
⚠ Homepage's own theme toggle is unreachable, and reaching for it breaks the
dashboard. It renders only when settings.yaml leaves `theme:` unpinned, and
with the key absent the page's data loader throws and its catch branch serves
`initialSettings: {}` — no tab bar, no layout, no i18n. Six force-recreates
over seven minutes all came up empty; restoring `theme: dark` rendered
correctly on the next recreate in 12 seconds, while /api/services returned 200
with fully correct content the whole time. That is the first confirmed cause of
the long-running "tab bar goes missing after a recreate" symptom, and it also
retires the homepage.log-size lead recorded earlier today: rolling the log
aside did nothing during this episode, so that coincidence was intermittency.
So the toggle is ours. conf/custom.js renders it and stores the choice;
build.py re-emits each vendored light block twice, once for an explicit
`data-theme` and once inside a prefers-color-scheme media query scoped to
`html:not([data-theme="dark"]):not([data-theme="light"])` — that :not() pair is
what lets a stored dark choice survive a light-mode OS. Verified against both
OS preferences: load, click, click back, reload, all four correct. `data-theme`
is the control surface; Homepage's own `dark` class stays on <html> and does
not fight, because our rules carry !important on the surfaces Tailwind's
`dark:` variants would otherwise claim.
Two font substitutions, both documented rather than silent: Space Grotesk for
Bespoke Sans and JetBrains Mono for Victor Mono. Only Supreme was ever vendored
here and Skyfall's own notes call Victor Mono user-supplied, so this is a
two-line swap when the real faces arrive.
Dark and light, all four tabs: http://10.100.10.50:8090/b/homepage-skyfall/
309 lines
16 KiB
Markdown
309 lines
16 KiB
Markdown
# `[2026-08-24]` Homepage: remote-label consumption re-verified, then the board relaid out on a uniform grid
|
|
|
|
Prompted by the operator: *"Homepage on esh-vm-docker lists remote dockers and
|
|
can absolutely consume their labels, please verify again. I am still
|
|
unsatisfied with the layout and aesthetics."*
|
|
|
|
## The verification — the operator was right, and the record now says so
|
|
|
|
**Homepage on `esh-docker-vm` discovers services by container label from all
|
|
five Docker engines in `conf/docker.yaml`, not just its own.** This is not an
|
|
inference; `GET /api/services` returns every card's `server` field, and the
|
|
2026-08-24 snapshot resolves to:
|
|
|
|
| `server` | host | label-discovered services |
|
|
|---|---|---|
|
|
| `ana-pfi-docker` | 10.250.50.70 | 30 |
|
|
| `irv-ml1-docker` | 10.100.79.3 (over WireGuard) | 15 |
|
|
| `ana-ml2-docker` | 10.250.50.54 | 14 |
|
|
| `esh-vm-docker` | 10.0.50.45 (the dashboard's own host) | 13 |
|
|
| `nh3-pfi-docker` | 10.100.50.40 | 2 |
|
|
|
|
**74 of 107 cards are label-discovered, and only 13 of those come from the
|
|
dashboard's own engine** — the other 61 are read off four remote hosts,
|
|
including irv-ml1 across the WireGuard tunnel. The remaining 33 carry
|
|
`server: null`: those are the manual `services.yaml` entries — hardware, BMCs,
|
|
hypervisors, printers, and user-level systemd services that have no container
|
|
to label in the first place. **That null is the only thing "not label-driven"
|
|
about this dashboard**, and it is a property of the entry, not of the host it
|
|
points at.
|
|
|
|
⚠ If a future session doubts this again, the check is one command and takes two
|
|
seconds — do not reason about it from the docs:
|
|
|
|
```bash
|
|
curl -s http://10.0.50.45:5100/api/services \
|
|
| jq -r '.[].services[] | .server' | sort | uniq -c
|
|
```
|
|
|
|
## What was actually wrong with the layout
|
|
|
|
Measured with Playwright against the live board (per-group `card=` width, card
|
|
height spread, and a geometric title-vs-status overlap test), not judged by
|
|
eye:
|
|
|
|
- **Card width changed at every group boundary.** `columns:` is not a density
|
|
dial — it sets `lg:grid-cols-N` for one group, so it fixes that group's card
|
|
width. Notes rendered a single **1464px** card; News and Media **728px**;
|
|
Eval & Retrieval **286px**; everything else 360px. Scrolling the page, the
|
|
grid resized five times.
|
|
- **Long names printed underneath their own status pill.** Measured by
|
|
re-injecting the old rule and testing the title text node's box against the
|
|
status cluster's box: **6 cards, all on the AI tab** — 3 in Inference, 2 in
|
|
Dormant, 1 in Eval & Retrieval; zero on the other three tabs, which is why
|
|
it survived earlier passes. Root cause is a genuinely counter-intuitive one:
|
|
the rule reserved a
|
|
78px gutter with `padding-right` and relied on `overflow: hidden` to hold it,
|
|
but **overflow clips at the padding box, not the content box** — so the
|
|
reserved gutter was spill room the title rendered straight through. The
|
|
intended `text-overflow: ellipsis` never fired either, because the ellipsis
|
|
is painted by whichever block's own line overflows, and here that is the
|
|
anonymous box around the bare title text node, which does not carry
|
|
`overflow`.
|
|
- **`AI Systems` / Scriberr was on all four tabs** — the 2026-08-18 UltraSeedbox
|
|
bug recurring, this time arriving from a container label rather than from
|
|
`settings.yaml`.
|
|
- **Icons were grey smudges.** Homepage masks every glyph over
|
|
`--color-logo-start/stop`; stock slate-400 → slate-700 sinks the bottom half
|
|
of each icon into the card fill.
|
|
- Bookmark groups and Jellyfin's trailing stream rows were the two components
|
|
the theme had never reached.
|
|
|
|
## The fixes
|
|
|
|
`stacks/homepage/conf/settings.yaml` — **all 20 groups to `columns: 4`.**
|
|
`stacks/homepage/theme/australis.css.in` → rebuilt → `conf/custom.css`:
|
|
gutter held by wrapping, description clamped to 3 lines (floor still 2), icon
|
|
ramp overridden, bookmark + trailing-widget components themed, group gap
|
|
10px → 22px. `stacks/scriberr/compose.yaml` — `homepage.group` → `AI - Audio
|
|
Tools`, container recreated on ana-ml2.
|
|
|
|
After: **every group renders at card=360**, and the collision count is zero.
|
|
|
|
Before/after, all four tabs: `http://10.100.10.50:8090/b/homepage-relayout/`
|
|
(24h TTL; also on the standing link board).
|
|
|
|
## ⚠ Three traps worth carrying forward
|
|
|
|
1. **"Columns = member count" is RETIRED** (it was the 2026-08-18 rule). It was
|
|
avoiding dead cells in a short last row and bought a worse defect. A short
|
|
last row is what a grid looks like; a card wider than its neighbours is what
|
|
a mistake looks like.
|
|
2. **A `:root` override of a Homepage theme variable is silently ignored.**
|
|
Homepage sets `--color-logo-*` on `.theme-slate`, and that class is on the
|
|
`<html>` element — the same element `:root` matches. `.theme-slate` (0,1,0)
|
|
beats `:root` (0,0,1), so the override does nothing and looks like the
|
|
variable is not the one in play. `html[class]` (0,1,1) wins, and does not
|
|
hard-code which `theme-*` class is active. Specificity alone is not enough
|
|
either: a custom property resolves from the *nearest* ancestor that sets it,
|
|
so the override has to land on `<html>`, not on `<body>`.
|
|
3. **The post-recreate tab-bar loss is INTERMITTENT, not guaranteed.** The
|
|
2026-08-19 note reads as though every recreate costs up to an hour of broken
|
|
render. This recreate came up correct within 10 seconds — fresh payload on
|
|
the first poll, all four tabs clickable a minute later. Recreate, *check*,
|
|
and only then walk away if it is actually in the broken state.
|
|
|
|
Also re-confirmed, since the change depended on it: **a `settings.yaml` edit
|
|
needs a container recreate, not a restart.** `docker restart homepage` left the
|
|
old `"columns":1` payload embedded in the served HTML with the correct file
|
|
mounted and readable inside the container; `compose up -d --force-recreate`
|
|
cleared it immediately.
|
|
|
|
## Deliberately not done — operator's call
|
|
|
|
The Main tab still opens on three sparse bands: **Notes** (1 member) and
|
|
**Games** (1) each burn a full 4-wide row, and **News** has 2. Merging Notes +
|
|
News, or folding Games into Apps, would tighten the top of the page — but that
|
|
is information architecture, not layout, and the group names are the operator's.
|
|
Surfaced rather than done.
|
|
|
|
→ **Resolved in pass 2 below**, where the operator delegated the naming
|
|
("re-categorize however you want"). Notes + News became `Daily`, Games folded
|
|
into `Apps`, and the `AI - Audio Tools` placement in this pass was superseded
|
|
(Scriberr moved on to `AI - Studios`).
|
|
|
|
---
|
|
|
|
# `[2026-08-24, pass 2]` Recategorised on "do I open this?", API groups collapsed
|
|
|
|
Operator, after seeing pass 1: *"You can re-categorize however you want.
|
|
service networking tab is uneven, you can split out the adguard cards, etc.
|
|
most of the issues are that tools I use and have a UI are interspersed with API
|
|
endpoints which are largely informational only. They might even go in their own
|
|
cards or start collapsed."*
|
|
|
|
## The axis
|
|
|
|
Every group is now either **tools** (expanded, top of tab) or **endpoints** (an
|
|
API, a broker, an agent — `initiallyCollapsed: true`, bottom of tab). A
|
|
collapsed group still renders its eyebrow and rule, so presence costs one line
|
|
instead of two rows.
|
|
|
|
Second, quieter rule that fell out of the same pass: **a group's members should
|
|
all carry a widget or none should.** A stat strip adds ~50px, so one widget card
|
|
in a row of plain ones opens a void under the plain ones — which is most of what
|
|
made the 13-member `Service Networking` band look broken.
|
|
|
|
## Shape
|
|
|
|
- **Main** — `Daily` (Memos, Miniflux, Nevermore, SearXNG — replaces the
|
|
1-card Notes and 2-card News bands), `Monitoring`, `Apps` (12; absorbed the
|
|
1-card `Games` band), `Media`, `UltraSeedbox`.
|
|
- **AI** — `AI - Gateways & Chat` (8) and `AI - Studios` (6) expanded; then
|
|
`AI - Inference` (7), `AI - Eval & Retrieval` (4), `AI - Speech (TTS)` (4),
|
|
`AI - Audio Tools` (2), `AI - Dormant` (6) all collapsed.
|
|
- **Toolchain** — `DNS & Filtering` (3), `Reverse Proxies` (2),
|
|
`Compose Consoles` (5), `Toolchain` (3), `Agents (no UI)` (6, collapsed).
|
|
- **Infrastructure** — unchanged; every card there is already a console.
|
|
|
|
Measured after: every group `card=360`, and `DNS & Filtering` and
|
|
`Reverse Proxies` both `h=134..134` — dead flush.
|
|
|
|
## ⚠ The move that made it affordable
|
|
|
|
**The sixteen GPU-backed model seats were NOT relabelled.** `homepage.group` is
|
|
read at container **creation**, so renaming `AI - Inference` to something
|
|
clearer would have meant recreating six vLLM seats plus four eval seats plus
|
|
four TTS engines — multi-minute model reloads on endpoints peers reach through
|
|
the gateway. Order plus `initiallyCollapsed` buys the same separation for free,
|
|
so the names stay ugly on purpose. **Do not spend that recreate on a label.**
|
|
|
|
28 containers *were* relabelled — all cheap web services — via five rerunnable
|
|
elway playbooks, `playbooks/homepage-regroup-<host>.yaml`. The canonical
|
|
`stacks/` tree was synced to match afterwards, so intent and reality agree.
|
|
|
|
`initiallyCollapsed: true` is a per-group key in `layout:`; confirmed present in
|
|
this build (`defaultOpen: !(group?.initiallyCollapsed ?? global)` in
|
|
`/app/.next/server/pages/index.js`).
|
|
|
|
## AdGuard (ANA) gained its widget, and the credential is fleet-wide
|
|
|
|
It was the only AdGuard without a query/blocked/latency strip, so it sat short
|
|
beside two tall siblings. **One `infra-ops` AdGuard login authenticates against
|
|
all three instances** (ANA `:8053`, NH3 `:8080`, ESH `:8080` — all returned 200
|
|
on `POST /control/login`, verified 2026-08-24). Vaulted at
|
|
`secret get nh3-dev/adguard-infra-ops-password`; written to
|
|
`/opt/docker/compose/adguard-ana/.env` (0600, root) and never into git. Its icon
|
|
was also the odd one out (`mdi-dns` against two `si-adguard`).
|
|
|
|
## ⚠⚠ `initialSettings":{}` — the tab-bar mystery is a SWALLOWED EXCEPTION
|
|
|
|
The biggest durable finding of the day, and it cost ~25 minutes. Full write-up
|
|
in `stacks/homepage/README.md`; the short version:
|
|
|
|
`initialSettings":{}` in the served HTML is **the catch branch** of the page's
|
|
data loader, not a warm-up and not a cache. And the error can vanish without
|
|
trace: the logger is assigned as the first statement *inside* the same `try`,
|
|
and the `catch` only logs `if (logger)`. If the logger is what threw, nothing is
|
|
written anywhere — which is exactly what was observed.
|
|
|
|
Ruled out by measurement, do not re-run: `/api/services`, `/api/bookmarks`,
|
|
`/api/widgets` and `/api/hash` all return **200 with correct content** while the
|
|
page serves `{}`; restoring the previous known-good `settings.yaml` reproduces
|
|
it identically; `/api/validate` returns `[]`; disk and permissions are fine.
|
|
|
|
**One-command test:**
|
|
`curl -s http://10.0.50.45:5100/ | grep -o 'initialSettings":[^,]\{0,20\}'`
|
|
|
|
**What broke the streak:** three consecutive recreates came up empty, then
|
|
rolling the 8.6 MB `conf/homepage/logs/homepage.log` aside and recreating healed
|
|
it within 15 seconds. That is one observation, not proof — but it is a coherent
|
|
mechanism (oversized log → logger init throws → silent catch) and it is the
|
|
cheapest thing to try first next time.
|
|
|
|
---
|
|
|
|
# `[2026-08-24, pass 3]` Rebuilt on Australis Skyfall — dual theme, light shipped
|
|
|
|
Operator supplied the Skyfall design-system README and said "Go full with
|
|
skyfall."
|
|
|
|
## The bundle was already in this repo's git history
|
|
|
|
**The Skyfall tokens did not need to be hunted down.** A predecessor vendored
|
|
them on 2026-08-19 and a later commit deleted them; git kept everything:
|
|
|
|
```bash
|
|
git show 45c1995:stacks/homepage/theme/colors.css # 208 lines, BOTH themes
|
|
git show 45c1995:stacks/homepage/theme/layout.css # calm-depth tokens
|
|
git show 45c1995:stacks/homepage/theme/typography.css
|
|
git show 45c1995:stacks/homepage/theme/fonts/Supreme-{400,500,700}.woff2
|
|
```
|
|
|
|
`colors.css` carries `:root` (dark) **and** `[data-theme="light"]` (Skyfall
|
|
Day) — so the light ramp is canonical, not derived. That killed the entire
|
|
objection from the previous answer, which was correct only about the
|
|
`australis-design` skill ("Always dark first. No light mode in this system").
|
|
**Skyfall is the dual-theme derivative; australis-design is the terminal
|
|
theme. They are different systems and only one of them has a light mode.**
|
|
|
|
## ⚠⚠ REMOVING `theme:` FROM settings.yaml BREAKS THE DASHBOARD
|
|
|
|
The documented way to get Homepage's own light/dark toggle is to leave `theme:`
|
|
unpinned. **Do not.** With the key absent, the page's data loader throws and its
|
|
catch branch serves `initialSettings: {}` — no tab bar, no layout, no i18n.
|
|
|
|
Measured, not inferred: six force-recreates over seven minutes all came up
|
|
empty with the key removed; restoring `theme: dark` rendered correctly on the
|
|
next recreate in **12 seconds**. `/api/services` stays 200 and fully correct
|
|
throughout, which is exactly why this reads as a caching or warm-up problem and
|
|
is not one.
|
|
|
|
This is the first *confirmed* trigger for the long-running "tab bar goes
|
|
missing" mystery. It does not explain every occurrence (the symptom has
|
|
appeared with `theme:` present), but it means **the first diagnostic step is
|
|
now `git log -p -- stacks/homepage/conf/settings.yaml`**, not container
|
|
archaeology. Also retires an earlier lead from this same session: rolling the
|
|
8.6 MB `homepage.log` aside once coincided with a recovery, but did nothing
|
|
during the `theme:`-key episode — coincidence, not cause.
|
|
|
|
## So the toggle is ours
|
|
|
|
`conf/custom.js` renders it (was an empty placeholder). Precedence:
|
|
|
|
1. explicit choice — `localStorage['skyfall-theme']`, written by the toggle;
|
|
2. OS preference — `@media (prefers-color-scheme: light)`;
|
|
3. dark — Skyfall's default.
|
|
|
|
`theme/build.py` re-emits each vendored `[data-theme="light"]` block twice: as
|
|
`[data-theme="light"], html.light`, and inside the media query scoped to
|
|
`html:not([data-theme="dark"]):not([data-theme="light"])`. **That `:not()` pair
|
|
is what lets a stored *dark* choice survive a light-mode OS.** Verified across
|
|
both OS preferences: load, click, click again, reload — all four correct.
|
|
|
|
⚠ Homepage keeps its own `class="dark scheme-dark theme-slate"` on `<html>`
|
|
regardless, because `theme:` is pinned. That is fine and was checked
|
|
explicitly: with the dark class present AND `data-theme="light"`, every themed
|
|
surface resolves to Skyfall Day, because our rules carry `!important` on the
|
|
surfaces Tailwind's `dark:` variants would otherwise claim. **`data-theme` is
|
|
the control surface; the class is not.**
|
|
|
|
## The anti-fork guard is now mechanical
|
|
|
|
`build.py` records the SHA-256 of each vendored file and **fails the build** on
|
|
a mismatch, rather than warning. A vendored file is either byte-identical to
|
|
the bundle or it is a fork wearing the bundle's name. Overrides go in
|
|
`skyfall.css.in`, which is written entirely against the semantic layer
|
|
(`--surface-*`, `--text-*`, `--border-*`, `--success/--danger/--warning`) — no
|
|
raw family tokens, no colour literals.
|
|
|
|
The one place a literal is unavoidable: Homepage consumes
|
|
`--color-logo-start/stop` as `rgb(var(--x))`, which cannot take an `oklch()`.
|
|
Those four values are exact sRGB conversions of real tokens (`--sea-80`,
|
|
`--blue-base` for dark; `--sea-40`, `--blue-deep` for light), computed rather
|
|
than eyeballed, with the conversion recorded in the file.
|
|
|
|
## Deviations, all deliberate and all written down
|
|
|
|
- **The aurora ribbon under the tab bar is gone.** Skyfall sanctions exactly two
|
|
accent expressions — the active rail and hero-only glows — and a decorative
|
|
gradient across the chrome is neither. The colour moved to a 2px accent bar
|
|
plus `--accent-soft` fill on the active tab, which *is* the rail.
|
|
- **Widget stat values moved from the display face to mono**, per Skyfall's
|
|
"numbers and telemetry are always `--font-mono`".
|
|
- **Two font substitutions**: Space Grotesk for Bespoke Sans, JetBrains Mono
|
|
for Victor Mono. Only Supreme was ever vendored, and Skyfall's own notes call
|
|
Victor Mono "user-supplied". Two-line swap when the real faces arrive.
|
|
|
|
Dark + light, all four tabs: `http://10.100.10.50:8090/b/homepage-skyfall/`
|