Auth layer (Authentik 2026.8.0): - compose: 3-service stack (authentik-postgres, authentik-server, authentik-worker), postgres health-gated, bind mounts under /home/dank/authentik - Caddyfile: auth.franksdank.cloud vhost; forward_auth on the no-auth vhosts (homepage, SearXNG, Hermes, Prometheus, RomM, Copyparty); outpost path proxied first in a route block - .env.example: AUTHENTIK_PG_PASS + AUTHENTIK_SECRET_KEY - deploy.sh: render only CADDY_BASICAUTH_HASH (the homepage per-user hashes are retired; grafana + pihole keep basicauth) - tests/authentik.bats (new) + updated homepage/caddyfile/pihole seams - docs: Authentik.md + per-service pages; Caddy.md + Homepage.md updated Homepage (naked domain): - Caddyfile: dankclaw.franksdank.cloud -> franksdank.cloud www.franksdank.cloud; dankclaw + dankserver redirect to it; bare-IP catch-all updated - addn-hosts: A records for the naked domain, www, and auth (all -> 143) - tests/pihole.bats: vhost_names handles multi-address blocks; TLD check allows the naked domain All 146 tests pass.
204 lines
9.7 KiB
Markdown
204 lines
9.7 KiB
Markdown
# 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 — 2026-08-23: deployed to production
|
||
|
||
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. 146 tests pass. 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)).
|
||
|
||
The per-service `/docs/` pages, the *arr stack, and the optional `/how/` page remain separate threads.
|
||
|
||
## 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://<name>.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["dankclaw.franksdank.cloud (Caddy)"]
|
||
ROOT["/ user services<br/>(shared password)"]
|
||
ADMIN["/admin/ admin services<br/>(operator password)"]
|
||
HOW["/how/ how it is built<br/>(deferred)"]
|
||
STATUS["/status.json"]
|
||
end
|
||
TIMER["systemd timer<br/>every 3 minutes"]
|
||
CHECKER["checker script<br/>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
|
||
[*] --> WIP
|
||
Up --> Down: probe fails
|
||
Down --> Up: probe succeeds
|
||
WIP --> Up: work completes
|
||
```
|
||
|
||
- **Up** — the probe reached the backend.
|
||
- **Down** — the probe failed (connection refused or timeout).
|
||
- **WIP** — a static override. The service is intentionally not finished. 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): SearXNG, ComfyUI, OpenWebUI, Gitea, Calibre-Web, RomM, Copyparty, Plex, Jellyfin.
|
||
|
||
**Admin services** (`/admin/`): Hermes, Grafana, Prometheus, Loki, Caddy, Pi-hole, Gluetun, the LLM fleet, and the low-level backends (mariadb, redis, Postgres, node-exporter, alloy). The backends show status only. They get a link only where a UI is meaningful.
|
||
|
||
**Excluded for now:** the *arr stack (Radarr, Sonarr, Lidarr, Readarr, Prowlarr, qBittorrent). It is not started. It is a separate effort, possibly on a different host.
|
||
|
||
## Layout
|
||
|
||
```
|
||
shared/homepage/
|
||
├── index.html # root page (user services)
|
||
├── admin.html # admin + infrastructure services
|
||
├── how.html # deferred (Q6)
|
||
├── 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)
|
||
└── fonts/ # Terminus (woff2) for @font-face
|
||
```
|
||
|
||
`status.json` is generated at runtime in `/srv/homepage/`. It is not committed.
|
||
|
||
## Rendering
|
||
|
||
The page renders a **grid of TUI-style panels** — one panel per service. Each panel shows the state badge, the name, the description, and the `Open` and `Docs` links. 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",
|
||
"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.
|
||
- `url` — the "Open" link (the Caddy site).
|
||
- `docs` — the per-service docs page (`/docs/<svc>/`).
|
||
- `probe` — the backend `host:port` the checker probes.
|
||
- `method` — `tcp` or `http`. The probe type for this service (Q10).
|
||
- `wip` — when true, the badge shows WIP and the checker skips the probe.
|
||
- `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/<svc>/` (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 `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
|