VayuPress Docs

Admin UI — the VayuOS console

Last reviewed: 2026-10-17, against VayuPress 3.18.7

The operator console lives at /os. It is server-rendered Go HTML with HTMX fragment swaps, Alpine.js islands where a page needs local state (ADR-0136), and one same-origin script per app. There is no client framework and no build step.

The earlier consoles are gone. /admin, /admin/v2[/…] and /admin/v3[/…] answer with a permanent redirect to their /os equivalent (cmd/vayupress/admin_legacy.go). The JSON API (/api/v1/*) and the operator sub-pages such as /admin/modes and /admin/faults have their own lifecycle and are not redirected.


Design constraints

Every console page is served under the strict policy built by render.BuildCSP (internal/render/csp.go):

default-src 'self'; font-src 'self'; style-src 'self'; style-src-attr 'unsafe-inline';
script-src 'self' 'nonce-<nonce>' <theme-toggle hash> <Halcyon prefs hash>;
img-src 'self' data: https:; connect-src 'self'; frame-ancestors 'none';
base-uri 'self'; form-action 'self'; report-uri /csp-report

Violations are reported to /csp-report and logged. The only relaxation in the codebase, BuildCSPAllowingEval, is an opt-in for one hosted static site and is refused on the console, the API and any path that carries a session.

What that means in practice:

  • No CDN, no eval, no new Function. HTMX 2 is vendored as static/js/htmx.min.js, and Alpine as its eval-free CSP build (alpine-csp.min.js, with the island registry vayu-islands.js). Everything else is first-party.
  • Every inline <script> carries the per-request nonce (render.CSPNonce). Event handlers are attached from script, never with onclick=/onerror= attributes.
  • Stylesheets are 'self' only. Inline style="…" attributes are allowed (style-src-attr); <style> elements are not.
  • Fonts are self-hosted woff2 under /static/fonts/: Space Grotesk (headings), Inter (body), JetBrains Mono (code). Newsreader beside them is Halcyon's, for the public site.

A page that breaks one of these does not degrade gracefully — the browser blocks it. Test in a real browser against a built binary, not only with Go tests.


Assets

Console assets are compiled into the binary. At boot they are written to STATIC_DIR, and serveAdminOSAsset serves the disk copy, falling back to the embedded one when the directory is missing or read-only. URLs carry ?v=<assetVer> so a release never serves a stale script.

File Role
static/css/vayuos.css The console stylesheet, Still Air: tokens, shell, components
static/js/admin-os.js Shell bootstrap: theme, toasts, command palette, list keyboard layer, shared helpers
static/js/admin-os-<app>.js One script per app (twenty: mail, talk, members, editor, website, storage, …), loaded only by that app's page
static/js/htmx.min.js Vendored HTMX
static/js/alpine-csp.min.js, vayu-islands.js Alpine's CSP build and the islands that use it

admin-os.js is one closure of init blocks. Anything that uses its helpers ($, $$, on) must sit inside it; the list keyboard layer once sat after the closing line and threw on every keypress. tests/e2e/console.spec.js runs the shell in Chromium and fails on any page error.


Shared client helpers

Use these rather than writing another copy:

Helper Where Use
vpToast(msg, kind) admin-os.js Non-blocking feedback. Kinds: ok, error, warn, info (success/danger/warning are mapped)
vpConfirm(opts, onYes) inline shell script, admin_os_ui.go The console's confirmation modal
vpPrompt(opts, onDone) inline shell script, admin_os_ui.go The console's single-field input modal
vpPost(url, body, onok, onerr) admin-os.js CSRF-protected JSON POST that reports every failure
vpCsrf() admin-os.js The double-submit token for hand-rolled requests (X-CSRF-Token)
vpRefresh(), vpRelTime(t) admin-os.js Redraw the current page's live regions; "3 min ago"

The command palette's actions are not a client registry. The server builds them (cmdAction in admin_os_ui.go), each gated on the operator's access to the page it acts for, so the palette never offers what the role cannot do.

Native alert/confirm/prompt are forbidden in console scripts; TestConsoleScriptsUseTheConsolesOwnDialogs scans every file. HTMX's own hx-confirm is routed through vpConfirm by the shell.


Design tokens

Tokens are CSS custom properties on .vp-os[data-ui="still-air"], redefined for the light theme and for prefers-color-scheme when the theme is auto. Hex values appear only in those token blocks; a rule never names a colour. The authoritative values are in static/css/vayuos.css; the ones most code needs:

Token Role
--color-canvas, --surface-1, --surface-2, --surface-float Page and layered surfaces
--text-1, --text-2, --text-3 Primary, secondary and helper copy
--accent, --on-accent Primary actions and the text on them
--ok, --warn, --danger (each with -soft) Status colours and their tinted backgrounds
--border-1, --border-2 Hairlines
--space-1 … --space-10, --radius-*, --type-* Space, shape and type

The classic names (--brand, --bg-surface, --sp-4, …) are aliases onto these, kept for the rules carried from the classic sheet; new rules use the Still Air names.

Contrast is enforced, not hoped for: TestStillAirPaletteClearsAA fails when a reading pairing drops below its WCAG bar in either theme. The console also carries its own prefers-contrast and forced-colors rules.

Theme. light / dark / auto, toggled from the top bar and saved as the admin.theme setting. The console mirrors the choice into localStorage['vp-os-theme'], which the sign-in pages read (os-theme.js) because nobody is signed in there to read the setting.


Media storage

Uploads are written to MEDIA_DIR (default /var/lib/vayupress/media) and served same-origin from /media/{file}.