--- type: reference description: "A password-protected web site on Dankclaw lists the services that are up, with a short description of each and a link to both the service and its usage documentation." tags: - infrastructure - homepage timestamp: 2026-09-21 --- # Homepage A password-protected web site on Dankclaw lists the services that are up, with a short description of each and a link to both the service and its usage documentation. **Served by:** Caddy, `file_server` + `forward_auth` (Authentik SSO) — **Docs:** this page ## Status — deployed to production (2026-08-23), on the naked domain since 2026-08-24 The homepage is live on Dankclaw. `./scripts/deploy.sh dankclaw` copies the static assets to `/srv/homepage/`, installs the status checker (systemd timer, every 3 min, writes `status.json`), writes `deployed.js` (the last master deployment datetime), renders the Caddyfile (the shared basicauth hash from the host `.env`), reloads Caddy, and checks health. The Caddyfile auth uses a `route` block with `forward_auth` (see [Auth](#auth)); the caddy container mounts the host's `/srv/homepage` (not the repo source) so the generated files serve. **Moved to the naked domain (2026-08-24):** the homepage now serves `franksdank.cloud` and `www.franksdank.cloud`. The old names `dankclaw.franksdank.cloud` and `dankserver.franksdank.cloud` redirect to it. Authentication moved from per-page basicauth to Authentik `forward_auth` (see [Authentik](Authentik.md)). **Docs page shipped (2026-09-17):** `/docs` serves a static user documentation page (`shared/homepage/docs.html`) — access walkthrough, the running-model guide, media how-tos, and troubleshooting. Per-service `/docs//` pages remain a separate thread. The three LLM cards (`frank-large`, `frank-medium`, `frank-secondary`) moved from admin-only to the **user** page with live model descriptions and a `docs` link to `/docs#models`. **Sections + WIP split (2026-09-21):** both pages now group their panels under section headings rendered by shared `renderSection()`/`renderPanel()` components in `app.js`. The user page orders **AI, Web Apps, Media Library Apps**; the admin page orders **Media Library Apps, Observability, AI, Infrastructure**. The WIP badge split into **WIP: OPEN** (work in progress) and **WIP: CLOSED** (work finished by decision — e.g. a card that is deliberately unprobeable) via the `wip` value (`"open"` / `"closed"`). Tautulli (Plex statistics, `192.168.0.235:8181`) joined the admin page's Media Library Apps section. **Amber CRT theme + API docs (2026-10-07):** the palette moved from green phosphor to amber CRT-AMBIOS (`--fg #ffb000` on `--bg #0a0600`), and the font stack now leads with **VT323** loaded from the Google Fonts CDN (the deploy also ships `shared/homepage/img/` beside the page, so `/img/*` serves the docs screenshots — the post-auth listener serves files only through the `@data` path allowlist, so new top-level assets must be added there (the `@home` catch-all answers anything else with `index.html`, i.e. a PNG arrives as `200 text/html` and renders broken; pinned in `homepage_caddy.bats` — this exact bug bit the screenshots on 2026-10-08). The docs page gained a full (`/docs#api-access`): per-user OpenWebUI API keys (User menu → Settings → Account → API keys), the base URL `https://ai.franksdank.cloud/api/v1`, and copy-paste configs for **Opencode** (`~/.config/opencode/opencode.json`) and **Pi** (`~/.pi/agent/models.json`) pointing at `frank-secondary`. Shipped together with the OpenWebUI side: `features.api_keys=true` for regular users and the restored `frank-secondary` model row + public `read` access grant without which the picker showed "No results found" for every family account (see [[OpenWebUI]]). ## Design decision — 2026-08-22 The live up/down check runs server-side. A checker on Dankclaw probes each backend and writes a same-origin `status.json`. The page fetches only that file and renders the badges. The browser never calls a backend directly. The checker runs as a systemd timer every 3 minutes. The site has two pages. The root lists user-facing services only, with no host labels. An `/admin/` subpage lists the admin and infrastructure services behind a separate password for the operator. Gitea is a user service. Hermes is an admin service. A "how it is built" page for visitors is deferred (Q6). The client-side probe design is blocked in this topology. The page origin is `https://franksdank.cloud`. The service URLs are `https://.franksdank.cloud`. That is cross-origin and auth-gated. Both fail a browser `fetch`. Intended direction: move to client-side probing. Serve every service as a same-origin `https` path under `franksdank.cloud`. Add a health endpoint and CORS headers to each backend. Then the page probes each service directly. Keep this note until that shift is complete. ## Structure The site has two pages. The root page lists the user services. The `/admin/` page lists the admin and infrastructure services. The optional `/how/` page is deferred (Q6). ```mermaid graph TD subgraph site["franksdank.cloud + www (Caddy)"] ROOT["/ user services
(shared password)"] ADMIN["/admin/ admin services
(operator password)"] HOW["/how/ how it is built
(deferred)"] STATUS["/status.json"] end TIMER["systemd timer
every 3 minutes"] CHECKER["checker script
probes each backend"] BACKENDS["backends on both hosts"] TIMER --> CHECKER CHECKER -->|writes| STATUS STATUS -->|polled| ROOT STATUS -->|polled| ADMIN CHECKER -->|probes| BACKENDS ``` ## Status model Each service shows one of three states. ```mermaid stateDiagram-v2 [*] --> Up [*] --> Down [*] --> WipOpen: wip = "open" [*] --> WipClosed: wip = "closed" Up --> Down: probe fails Down --> Up: probe succeeds WipOpen --> Up: work completes WipClosed --> Up: card becomes probeable ``` - **Up** — the probe reached the backend. - **Down** — the probe failed (connection refused or timeout). - **WIP: Open** — work is in progress. The checker does not probe it. - **WIP: Closed** — the work is closed by decision (for example a card that is deliberately unprobeable, with the reason in its description). The checker does not probe it. ## Checker The checker runs on Dankclaw as a systemd timer. Every run probes each backend directly, then writes `status.json`. It probes the backend `host:port`, not the Caddy site. A Caddy `502` or `401` cannot tell up from down. The probe method is per service (`tcp` or `http`). The timeout is 5 s. The page reads `status.json` once on load. It does not auto-refresh. ```mermaid flowchart TD A[timer fires] --> B[read service config] B --> C{for each service} C -->|wip| D[mark WIP] C -->|probe| E[probe: tcp or http] E -->|connected| F[mark Up] E -->|refused / timeout| G[mark Down] D --> H[write status.json] F --> H G --> H ``` ## Service classification **User services** (root page), in section order: - **AI** — OpenWebUI, and the three LLM fleet cards (frank-large, frank-primary, frank-secondary — status + docs link, no Open link). - **Web Apps** — SearXNG, Gitea, Copyparty, Marreta. - **Media Library Apps** — Calibre-Web, RomM, Plex, Jellyfin, Seerr. **Admin services** (`/admin/`), in section order: - **Media Library Apps** — the *arr stack (Prowlarr, Sonarr, Radarr, Lidarr, Readarr), qBittorrent, and Tautulli — live on FrankBook-Pro and Dankserver, operator-only: no vhosts (Tautulli included), direct IP:port access only. See [[FrankBook-Pro]]. - **Observability** — Grafana, Prometheus, Loki, node-exporter, alloy, alertmanager. - **AI** — Hermes, ComfyUI. - **Infrastructure** — Caddy, dnsmasq, Gluetun, and the low-level backends (mariadb, valkey, Postgres). The backends show status only. They get a link only where a UI is meaningful. ## Layout ``` shared/homepage/ ├── index.html # root page (user services) ├── admin.html # admin + infrastructure services ├── docs.html # user documentation page, served at /docs (2026-09-17) ├── style.css # full theme; a toggle strips to bare HTML ├── app.js # loads status.json once, renders the panels ├── services.json # service config (user + admin) ├── img/ # screenshots served at /img (docs page; deployed with the page) └── fonts/ # optional self-hosted fonts (deployed with the page; VT323 comes from the Google Fonts CDN, this dir is absent) ``` `status.json` is generated at runtime in `/srv/homepage/`. It is not committed. ## Rendering The page renders **sections of TUI-style panels** — one panel per service, grouped under a section heading. Each panel shows the state badge, the name, the description, and the `Open` and `Docs` links. Section and panel are shared components (`renderSection()` / `renderPanel()` in `app.js`); each page's section order is fixed by `SECTION_ORDER`. A summary line at the top counts the states. The footer shows the last master deployment datetime (Q43). The page reads `status.json` **once on load**. It does not auto-refresh (Q41). Styling is **toggleable** (Q27). The default is the full terminal theme. A toggle strips the page to bare, unstyled HTML (motherfuckingwebsite.com style). The page does not alter the user's cursor (Q28). ## Auth Caddy wraps the site with `forward_auth` (Authentik SSO). A single Authentik session covers the whole site: the user page, the `/admin/` page, and the shared data + assets. The browser's Authentik cookie is reused for the data fetches, so no per-page credential is needed. See [Authentik](Authentik.md) for the provider, the outpost, and the first-time setup. The Caddyfile pattern (one of the forward_auth vhosts): ``` franksdank.cloud www.franksdank.cloud { root * /srv/homepage route { # 1. The outpost path is proxied to Authentik (login UI + session). reverse_proxy /outpost.goauthentik.io/* authentik-server:9000 # 2. forward_auth checks the session before serving the page. forward_auth authentik-server:9000 { uri /outpost.goauthentik.io/auth/caddy copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Email X-Authentik-Name trusted_proxies private_ranges } # 3. The pages, in file order. 'handle' preserves order; a top-level # 'rewrite' would be sorted first and break the path matchers. handle /admin* { try_files /admin.html file_server } @data path /services.json /status.json /app.js /style.css /deployed.js handle @data { file_server } handle { try_files /index.html file_server } } } ``` A `handle` inline matcher is a single token; the multi-path `@data` match is a named matcher. The caddy container mounts the host's `/srv/homepage` (where the deploy writes the static files and the checker writes `status.json`), not the repo's `shared/homepage`. A compose change needs a container recreate (`podman compose up -d caddy`), not a reload. The Caddyfile syntax is documented in the agent knowledge base (`~/.pi/agent/docs/caddy/caddyfile.md`). ## Service entry format ```json { "name": "SearXNG", "category": "user", "section": "Web Apps", "description": "Privacy-respecting metasearch engine.", "url": "http://search.franksdank.cloud", "docs": "/docs/searxng/", "probe": "127.0.0.1:8081", "method": "http", "wip": false, "showLink": true } ``` - `category` — `user` or `admin`. Selects the page. - `section` — the heading the panel renders under. Must be one of the page's `SECTION_ORDER` entries (user: AI, Web Apps, Media Library Apps; admin: Media Library Apps, Observability, AI, Infrastructure), or the panel is silently dropped from the page. - `url` — the "Open" link (the Caddy site). - `docs` — the per-service docs page (`/docs//`). - `probe` — the backend `host:port` the checker probes. - `method` — `tcp` or `http`. The probe type for this service (Q10). - `wip` — `false`, or `"open"` / `"closed"`. When set, the badge shows **WIP: OPEN** or **WIP: CLOSED** and the checker skips the probe. **A card must be probeable from the monitor's host, or it must be `wip`.** The checker runs on Dankclaw, so its reach is: that host's own loopback, its own container names, and LAN addresses that are actually *published*. A card whose probe is a container-internal port of another host can never turn green — it reports a red dot for a healthy service, which is how the board stops being worth reading (the same lesson as the Prometheus jobs that scrape HTML pages). Three cards were exactly that until 2026-09-16 — `mariadb` (`192.168.0.235:3306`), `valkey` and `Postgres` (`127.0.0.1:6379`/`:5432`) — all probing unpublished database ports. Publishing a database on the LAN to make a badge green is not a trade this fleet makes, so they are `wip` with the reason in their description, and the app that fronts each database carries the status (Glutun, a WireGuard UDP service, was the pre-existing example of the same call). - `showLink` — when false, the panel shows status only (no "Open" link). ## Deployment and testing The deploy is **git-driven**. Master always reflects the currently deployed state (Q33). `deploy.sh` copies the assets to `/srv/homepage/`, installs the checker timer, renders the Caddyfile (both hashes), and reloads Caddy. It stamps the deployment datetime the footer shows (Q43). The build is **full TDD** (Q36–Q38). Tests exist before the code: the checker (a fake up port, a down port, a WIP service, and the JSON schema), the pages and config (the `services.json` schema, `caddy validate`, the `status.json` reference), and the auth check (the forward_auth block and the renderable basicauth placeholder). ## Per-service docs (separate workstream) Each service gets a docs page at `/docs//` (Q30). The content is a **how-to for a regular user** — no maintenance details (Q29). The pages are password-protected by the same auth as the homepage section they belong to (Q31). They are built as each service goes active (Q32). The `docs` link on a service points to its page. The existing reference docs in `serverAppConfigs-docs/` are not linked from the homepage. They may contain secrets (Q22). ## See Also - [[Homepage Design Grilling]] — the decision log - [[Caddy]] — the proxy that serves it - [[Dankclaw Stack]], [[Dankserver Stack]] — the services it lists