Homepage: settle design (grilling Q1-Q5), log open questions Q6-Q43, progress report

- Homepage.md: rewritten to the settled design (single site, user + /admin/
  pages, server-side status.json via a 3-min systemd-timer checker, terminal
  TUI theme). Added mermaid diagrams: site structure, status states, checker
  flow. Recorded the classification (user vs admin; *arr excluded; LLM admin;
  backends status-only) and the documented future shift to client-side probing.
- Homepage Design Grilling.md (new): the decision log — settled Q1-Q5 plus 38
  open questions (Q6-Q43) across 10 categories, each with a recommendation.
- docs/index.md: link the grilling page (satisfies the index-links-every-page test).
- report/homepage-grilling-2026-08-22.html: self-contained terminal-TUI progress
  report (settled decisions, verified stack baseline, overnight processing, work
  threads, the open questions, and how to continue).

Baseline recorded: Dankclaw fully up (10/10); the Dankserver media stack is down
(only Calibre-Web + the LLM are up; no .env on the target) — a separate thread.
No homepage code built yet; the design is pending the operator's answers to Q6-Q43.
This commit is contained in:
Frank
2026-08-23 02:59:48 -07:00
parent 6a9edd4ee0
commit cdae928f37
4 changed files with 499 additions and 21 deletions
@@ -0,0 +1,167 @@
# Homepage Design Grilling
The decision log for the homepage design. It records what is settled and the open questions that remain. Answer the open questions in the morning report; the design is built only after they are settled.
## Decided
| # | Decision | Value |
|---|----------|-------|
| Q1 | How the page learns up/down | **Server-side** checker writes `status.json`; the page reads only that file. (Eventual target: client-side probing.) |
| Q2 | How the checker runs | **systemd timer**, rewrites `status.json` every **3 minutes**. |
| Q3 | Page model | **Single site** on Dankclaw. Root = user services (no host labels). `/admin/` = admin services, separate password. Optional `/how/` tourist page. |
| Q4 | (superseded by Q3 restructure) | — |
| Q5 | Service split | User: SearXNG, ComfyUI, OpenWebUI, Gitea, Calibre-Web, RomM, Copyparty, Plex, Jellyfin. Admin: Hermes, Grafana, Prometheus, Loki, Caddy, dnsmasq, Gluetun, LLM fleet, low-level backends (status-only). The *arr stack is **excluded** (separate effort, not started). |
Styling is settled in principle: terminal TUI, Terminus (monospace fallback), green-on-black, 1995 phosphor palette (green `#33ff66`, red `#ff5555`, amber `#ffcc33`, cyan `#33ccff`, bg `#050805`).
## Open questions
Each question carries a recommendation. Answer them in the morning report.
### A. Architecture and structure
**Q6 — The optional `/how/` "tourist" page: build now or defer?**
Recommendation: **defer** to a later effort. When built, make it **public** (no auth) so a visitor can see the architecture. It can render the mermaid diagrams from this repo.
**Q7 — One `status.json` or two (`user-status.json` + `admin-status.json`)?**
Recommendation: **one** `status.json`. Each service carries a `category` field. Each page filters client-side. One file, one checker output, one source of truth.
**Q8 — One `services.json` or split by page?**
Recommendation: **one** `services.json` with all services and a `category` field. The checker reads one file. Simpler to maintain.
### B. The checker
**Q9 — What language for the checker script?**
Recommendation: **python3** (available on Dankclaw, no dependencies, emits valid JSON cleanly). A bash + `nc` variant works too, but JSON by hand is error-prone.
**Q10 — Probe method: TCP connect or HTTP GET?**
Recommendation: **TCP connect** to `host:port`. Uniform, works for non-HTTP backends, and "port listening = up" matches the verified ground truth.
**Q11 — Probe timeout per service?**
Recommendation: **3 seconds**. A dead port refuses fast; a black-holed one times out at 3 s. With ~25 services the worst-case run is well under the 3-minute interval.
**Q12 — LLM "loading" state (health returns 503 during model load)?**
Recommendation: keep **three** states. A listening port shows **Up**. Optionally probe `/health` and treat 503 as Up (the process is alive). A dedicated "loading" state is a later refinement.
**Q13 — Where does the checker live in the repo?**
Recommendation: `scripts/homepage-status.py` plus `scripts/homepage-status.service` and `scripts/homepage-status.timer`, all tracked in git like the other units.
**Q14 — How does the checker reach the Dankserver backends?**
Recommendation: the `probe` field carries a full `host:port`. Dankclaw services use `127.0.0.1:PORT`. Dankserver services use `192.168.0.235:PORT`. The checker (on Dankclaw) connects to each over the LAN. Verified reachable.
### C. The status model
**Q15 — How is a service marked WIP?**
Recommendation: a static `"wip": true` field. The checker skips the probe and the page shows WIP. Drop the old self-referential "Homepage" entry (a page does not list itself).
**Q16 — Staleness: what if the timer fails and `status.json` is old?**
Recommendation: write a `generated_at` timestamp into `status.json`. The page shows a "status may be stale" warning when it is older than 10 minutes.
**Q17 — Atomic writes?**
Recommendation: the checker writes to a temp file, then renames over `status.json`. A crash never leaves a half-written file. The last good status survives.
### D. Auth
**Q18 — The admin password: a second `.env` variable?**
Recommendation: yes. `CADDY_ADMIN_HASH` in the host `.env` (git-ignored), rendered into the Caddyfile at deploy, operator-only. The root page keeps `CADDY_BASICAUTH_HASH`.
**Q19 — Per-path basicauth in Caddy v2?**
Recommendation: a `@admin path /admin/*` matcher with a second `basicauth @admin` block. Confirm the exact syntax against the Caddy docs during implementation.
### E. The pages and content
**Q20 — Where do the one-line descriptions come from?**
Recommendation: hand-written one-liners in `services.json`, drafted from the existing reference docs.
**Q21 — The "Open" link target: the Caddy site or the direct backend?**
Recommendation: the **Caddy site** URL (what a user actually opens). Status-only backends set `showLink: false` (no Open link).
**Q22 — The "docs" link: usage page or the existing reference doc?**
Recommendation: link to the **existing** reference doc (`docs/.../X.md`) as a placeholder. Swap to the per-service usage page when that workstream is built.
**Q23 — Page layout: cards or a terminal-style list?**
Recommendation: a **terminal-style list** (one service per line, like `systemctl list-units`). Columns: `STATUS | NAME | description | (open) (docs)`. Fits the TUI aesthetic better than cards.
**Q24 — A summary line at the top?**
Recommendation: yes. One line, e.g. `UP 9 DOWN 2 WIP 1`, per page.
### F. Styling
**Q25 — Terminus: bundle the font or use a fallback?**
Recommendation: **bundle** Terminus (woff2) via `@font-face`, with a monospace fallback stack. Confirm the license (Terminus is freely distributable) and drop the files in `fonts/`.
**Q26 — Confirm the palette?**
Recommendation: as proposed (green `#33ff66`, red `#ff5555`, amber `#ffcc33`, cyan `#33ccff`, bg `#050805`). Tune for contrast and readability.
**Q27 — CRT effects (scanlines + glow)?**
Recommendation: subtle scanlines + a soft green `text-shadow`. Make it toggleable (a `?crt=0` param) for those who find it much.
**Q28 — Blinking cursor / animation?**
Recommendation: a subtle CSS-only blinking block cursor on the header. Respect `prefers-reduced-motion`.
### G. The usage-docs workstream
**Q29 — Scope of the per-service usage pages?**
Recommendation: concise **how-to** guides (what it is, how to open it, the common tasks). Distinct from the deep reference docs in `docs/`.
**Q30 — Where do the usage pages live?**
Recommendation: served from the homepage site, same origin, at `/usage/<svc>/`. Source in the repo under `shared/homepage/usage/<svc>/`. Same origin keeps the "docs" link working.
**Q31 — Are the usage pages password-protected?**
Recommendation: yes. User usage pages behind the shared password, admin usage pages behind the operator password. Consistent with the pages they extend.
**Q32 — Is the usage-docs workstream a separate effort?**
Recommendation: yes, after the core homepage is live. For now the "docs" link points to the existing reference doc (Q22).
### H. Deployment and operations
**Q33 — How is the homepage deployed?**
Recommendation: extend `deploy.sh` (Dankclaw path): copy `shared/homepage/` to `/srv/homepage/`, install the timer + service, render the Caddyfile (both hashes), reload Caddy.
**Q34 — Who creates `/srv/homepage/`?**
Recommendation: `deploy.sh` creates it and copies the assets. The checker writes `status.json` there at runtime.
**Q35 — Is the timer enabled at boot?**
Recommendation: yes. A systemd timer + service, enabled, so it survives reboots and runs every 3 minutes.
### I. Testing
**Q36 — Tests for the checker?**
Recommendation: yes. A bats test runs the checker against a fake up port, a fake down port, and a WIP service, then validates the `status.json` schema. TDD, per repo rules.
**Q37 — Tests for the pages and config?**
Recommendation: yes. Validate the `services.json` schema (required fields, valid categories), run `caddy validate` on the Caddyfile, and check the pages reference `status.json`.
**Q38 — A security test for the two passwords?**
Recommendation: yes. Assert the Caddyfile carries two distinct basicauth hashes (root vs `/admin/`).
### J. Edge cases and robustness
**Q39 — A backend moves hosts (e.g. the *arr stack to a new box)?**
Recommendation: no code change. The `probe` field is a full `host:port`. A move is a config edit.
**Q40 — A slow LAN hop to a Dankserver backend?**
Recommendation: the 3 s timeout handles it. A slow-but-alive backend connects (Up); a dead one times out (Down).
**Q41 — Client-side auto-refresh?**
Recommendation: the page polls `status.json` every **60 s** (the file updates every 3 min; 60 s is responsive without hammering).
**Q42 — Mobile responsiveness?**
Recommendation: yes. Stack the columns and shrink the font on narrow screens.
**Q43 — A "last checked" timestamp on the page?**
Recommendation: yes. The footer shows `last checked HH:MM:SS` from `generated_at`.
## How to continue
Answer the open questions (Q6–Q43) in the morning report. Group the answers by category. Once settled, the build order is:
```mermaid
flowchart LR
A[settle Q6-Q43] --> B[write services.json + checker]
B --> C[write the pages + theme]
C --> D[extend deploy.sh + Caddyfile]
D --> E[tests green]
E --> F[deploy to Dankclaw]
F --> G[verify in browser]
```
+112 -21
View File
@@ -1,56 +1,147 @@
# Homepage
A password-protected homepage on each host lists the services that are up, with a short description of each and a link to both the service and its documentation.
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` + `basicauth` — **Docs:** n/a (static page in this repo)
**Served by:** Caddy, `file_server` + `basicauth` — **Docs:** this page
## Purpose
## Status — 2026-08-22: work in progress
- One landing page per host instead of a bookmark folder.
- Shows at a glance which services are currently up and active.
- Answers "what is this?" for every service with a one-line description.
- Links to the service and to this repo's docs page for that service.
The homepage is not finished. The static assets exist in `shared/homepage/`. They are not yet copied to `/srv/homepage/` on the target. Caddy serves the `basicauth` challenge (HTTP 401) and the `:80` redirect (HTTP 302). The page content is absent.
Do not treat the missing page as an outage. The Caddy site block and the redirect are live. Only the content and the deploy step remain.
## 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 optional and undecided.
The client-side probe design is blocked in this topology. The page origin is `https://dankclaw.franksdank.cloud`. The service URLs are `http://<name>.franksdank.cloud`. That is cross-origin, mixed content, and auth-gated (HTTP 401). All three fail a browser `fetch`.
Intended direction: move to client-side probing. Serve every service as a same-origin `https` path under `dankclaw.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 three pages. The root page lists the user services. The `/admin/` page lists the admin and infrastructure services. The `/how/` page (optional) shows the architecture to visitors.
```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/>(optional)"]
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.
```mermaid
flowchart TD
A[timer fires] --> B[read service config]
B --> C{for each service}
C -->|wip| D[mark WIP]
C -->|probe| E[TCP connect to host:port]
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, dnsmasq, 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 # the page, shared logic, per-host service list injected
├── style.css
└── services.json # service list: name, hostname, port, description, docs link
shared/homepage/
├── index.html # root page (user services)
├── admin.html # admin + infrastructure services
├── how.html # optional architecture page
├── style.css # terminal TUI theme
├── app.js # loads status.json, renders the badges
├── services.json # service config (user + admin)
└── fonts/ # Terminus (woff2) for @font-face
```
The page fetches `services.json` and pings each service's health endpoint (or the proxy hostname) to set an up/down badge. It is static; the status check runs in the browser via `fetch`.
`status.json` is generated at runtime in `/srv/homepage/`. It is not committed.
## Auth
Caddy wraps the homepage with HTTP basic auth:
Caddy wraps the site with HTTP basic auth. The root page uses the shared password. The `/admin/` page uses a second, operator-only password. The hashes come from the host `.env` (git-ignored). Do not commit the hash or the password.
```
dankclaw.local {
dankclaw.franksdank.cloud {
@admin path /admin/*
basicauth {
dank $2a$14$HASH
dank {env.CADDY_BASICAUTH_HASH}
}
basicauth @admin {
frank {env.CADDY_ADMIN_HASH}
}
root * /srv/homepage
file_server
}
```
Generate the hash with `caddy hash-password` at deploy time. Do not commit the hash or the password.
## Service Entry Format
## Service entry format
```json
{
"name": "SearXNG",
"hostname": "http://search.dankclaw.local",
"category": "user",
"description": "Privacy-respecting metasearch engine.",
"docs": "/docs/Dankclaw/SearXNG.md"
"url": "http://search.franksdank.cloud",
"docs": "/usage/searxng/",
"probe": "127.0.0.1:8081",
"wip": false,
"showLink": true
}
```
- `category` — `user` or `admin`. Selects the page.
- `url` — the "Open" link (the Caddy site).
- `docs` — the per-service usage page.
- `probe` — the backend `host:port` the checker probes.
- `wip` — when true, the badge shows WIP and the checker skips the probe.
- `showLink` — when false, the card shows status only (no "Open" link).
## See Also
- [[Homepage Design Grilling]] — the decision log and open questions
- [[Caddy]] — the proxy that serves it
- [[Dankclaw Stack]], [[Dankserver Stack]] — the services it lists
+1
View File
@@ -65,6 +65,7 @@ covers both workloads).
- [[LAN-DNS]] — name resolution, dnsmasq, router DNS hand-off
- [[Caddy]] — reverse proxy and TLS
- [[Homepage]] — password-protected service index
- [[Homepage Design Grilling]] — homepage decision log and open questions
- [[Observability]] — Grafana, Prometheus, Loki, Alloy
- [[Grafana]] — dashboards
- [[Prometheus]] — metrics collection
+219
View File
@@ -0,0 +1,219 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Frank Program — Homepage Grilling — 2026-08-22</title>
<style>
:root{
--bg:#050805; --fg:#33ff66; --dim:#2f8f52; --down:#ff5555; --wip:#ffcc33;
--link:#33ccff; --head:#aaffcc; --border:#1a4d2e; --panel:#0a120a;
}
*{box-sizing:border-box}
body{
background:var(--bg); color:var(--fg);
font-family:"Terminus","Fira Code","DejaVu Sans Mono","Consolas",monospace;
font-size:15px; line-height:1.55; margin:0; padding:24px;
text-shadow:0 0 4px rgba(51,255,102,.30);
}
body::before{
content:""; position:fixed; inset:0; pointer-events:none; z-index:9999;
background:repeating-linear-gradient(0deg, rgba(0,0,0,0) 0 2px, rgba(0,0,0,.14) 2px 4px);
}
.wrap{max-width:1120px; margin:0 auto}
h1{color:var(--head); font-size:20px; border-bottom:1px solid var(--border); padding-bottom:10px; margin:0 0 4px}
h2{color:var(--head); font-size:16px; margin:34px 0 10px}
h2::before{content:"## "; color:var(--dim)}
h3{color:var(--head); font-size:14px; margin:22px 0 6px}
.sub{color:var(--dim); font-size:13px; margin-top:0}
.panel{border:1px solid var(--border); background:var(--panel); padding:14px 16px; margin:12px 0}
pre{background:#030603; border:1px solid var(--border); padding:12px; overflow:auto; color:var(--fg); font-size:13px; line-height:1.4}
code{color:var(--head)}
table{border-collapse:collapse; width:100%; font-size:13px; margin:8px 0}
th,td{border:1px solid var(--border); padding:6px 9px; text-align:left; vertical-align:top}
th{color:var(--head); background:#081108}
.up{color:var(--fg)} .down{color:var(--down)} .wip{color:var(--wip)} .und{color:var(--wip)}
.badge{border:1px solid currentColor; padding:0 6px; border-radius:2px; white-space:nowrap}
a{color:var(--link); text-decoration:underline}
.q{border-left:3px solid var(--border); padding:6px 14px; margin:14px 0}
.qn{color:var(--head); font-weight:bold}
.rec{color:var(--fg); background:#08130a; border:1px dashed var(--border); padding:7px 11px; margin-top:7px; display:block}
.rec b{color:var(--head)}
.verdict{color:var(--dim); font-size:12px; margin-top:7px}
.verdict b{color:var(--wip)}
.cursor{display:inline-block; width:9px; height:17px; background:var(--fg); vertical-align:-2px; animation:blink 1s steps(1) infinite}
@keyframes blink{50%{opacity:0}}
.dim{color:var(--dim)}
.ok{color:var(--fg)} .no{color:var(--down)}
.tag{display:inline-block; border:1px solid var(--border); padding:0 6px; border-radius:2px; font-size:12px; color:var(--dim); margin-right:6px}
</style>
</head>
<body>
<div class="wrap">
<h1>frank@program:~/homepage$ grilling --progress <span class="cursor"></span></h1>
<p class="sub">Frank Program &middot; homepage design grilling &middot; overnight progress report &middot; 2026-08-22 &middot; from the chief designer</p>
<div class="panel">
<pre>
summary settled: 5 decisions + styling open: 38 questions (Q6-Q43)
baseline dankclaw: UP (10/10) dankserver: media stack DOWN (no .env)
state WIP — /srv/homepage/ not deployed; Caddy site live (401 / 302)
next answer Q6-Q43 in the morning; then build order is fixed
</pre>
</div>
<h2>What is settled</h2>
<table>
<tr><th>#</th><th>Decision</th><th>Value</th></tr>
<tr><td>Q1</td><td>How the page learns up/down</td><td><b>Server-side</b> checker writes <code>status.json</code>; the page reads only that file. (Eventual target: client-side probing — documented.)</td></tr>
<tr><td>Q2</td><td>How the checker runs</td><td><b>systemd timer</b>, rewrites <code>status.json</code> every <b>3 minutes</b>.</td></tr>
<tr><td>Q3</td><td>Page model</td><td><b>Single site</b> on Dankclaw. Root = user services (no host labels). <code>/admin/</code> = admin services, second operator-only password. Optional <code>/how/</code> tourist page.</td></tr>
<tr><td>Q5</td><td>Service split</td><td>User: SearXNG, ComfyUI, OpenWebUI, Gitea, Calibre-Web, RomM, Copyparty, Plex, Jellyfin. Admin: Hermes, Grafana, Prometheus, Loki, Caddy, dnsmasq, Gluetun, LLM fleet, low-level backends (status-only). *arr stack <b>excluded</b> (separate effort).</td></tr>
<tr><td>—</td><td>Styling</td><td>Terminal TUI. Terminus (monospace fallback). Green-on-black. 1995 phosphor palette.</td></tr>
</table>
<p class="dim">Palette: bg <code>#050805</code> · text <code>#33ff66</code> · up <code>#33ff66</code> · down <code>#ff5555</code> · WIP <code>#ffcc33</code> · links <code>#33ccff</code> · headers <code>#aaffcc</code>.</p>
<h2>Stack baseline (verified 2026-08-22)</h2>
<p class="dim">Source: real HTTP fetch through Caddy + <code>ss -ltn</code> (they agree). The earlier <code>/dev/tcp</code> probe was the false one — it reported every port down while Caddy returned 200.</p>
<h3>Dankclaw 192.168.0.143 — AI / search / dev / observability</h3>
<table>
<tr><th>Service</th><th>Site</th><th>HTTP</th><th>Port</th><th>State</th></tr>
<tr><td>Homepage</td><td>dankclaw</td><td>401</td><td>—</td><td class="wip"><span class="badge wip">WIP</span> auth-gated, not deployed</td></tr>
<tr><td>SearXNG</td><td>search</td><td>401</td><td>8081 ✓</td><td class="up"><span class="badge up">UP</span> (auth-gated)</td></tr>
<tr><td>ComfyUI</td><td>comfyui</td><td>200</td><td>8188 ✓</td><td class="up"><span class="badge up">UP</span></td></tr>
<tr><td>OpenWebUI</td><td>chat</td><td>401</td><td>3005 ✓</td><td class="up"><span class="badge up">UP</span> (auth-gated)</td></tr>
<tr><td>Hermes</td><td>hermes</td><td>401</td><td>8090 ✓</td><td class="up"><span class="badge up">UP</span> (auth-gated)</td></tr>
<tr><td>Gitea</td><td>gitea</td><td>200</td><td>3000 ✓</td><td class="up"><span class="badge up">UP</span></td></tr>
<tr><td>Grafana</td><td>grafana</td><td>401</td><td>3002 ✓</td><td class="up"><span class="badge up">UP</span> (auth-gated)</td></tr>
<tr><td>Prometheus</td><td>prometheus</td><td>401</td><td>9090 ✓</td><td class="up"><span class="badge up">UP</span> (auth-gated)</td></tr>
<tr><td>Loki</td><td>loki</td><td>404</td><td>3100 ✓</td><td class="up"><span class="badge up">UP</span> (no root page)</td></tr>
<tr><td>frank-default-large</td><td>:7770</td><td>ok</td><td>7770 ✓</td><td class="up"><span class="badge up">UP</span></td></tr>
</table>
<h3>Dankserver 192.168.0.235 — media</h3>
<table>
<tr><th>Service</th><th>Site</th><th>HTTP</th><th>Port</th><th>State</th></tr>
<tr><td>Calibre-Web</td><td>books</td><td>302</td><td>8083 ✓</td><td class="up"><span class="badge up">UP</span></td></tr>
<tr><td>frank-default-small</td><td>:7770</td><td>ok</td><td>7770 ✓</td><td class="up"><span class="badge up">UP</span></td></tr>
<tr><td>Plex</td><td>plex</td><td>502 / 000</td><td>32400 bound</td><td class="und"><span class="badge und">UNDETERMINED</span> port bound, no HTTP reply</td></tr>
<tr><td>RomM</td><td>romm</td><td>401</td><td>8085 ✗</td><td class="down"><span class="badge down">DOWN</span> (401 = auth challenge only)</td></tr>
<tr><td>Copyparty</td><td>files</td><td>401</td><td>3923 ✗</td><td class="down"><span class="badge down">DOWN</span> (401 = auth challenge only)</td></tr>
<tr><td>Jellyfin</td><td>jellyfin</td><td>502</td><td>8096 ✗</td><td class="down"><span class="badge down">DOWN</span></td></tr>
<tr><td>Radarr / Sonarr / Lidarr / Readarr / Prowlarr / qBittorrent</td><td>—</td><td>502</td><td>✗</td><td class="down"><span class="badge down">DOWN</span> (not started — *arr effort)</td></tr>
</table>
<h2>My processing — what I worked out overnight</h2>
<div class="panel">
<p><span class="tag">1</span><b>The port-probe contradiction is resolved.</b> My first probe (<code>/dev/tcp</code> nested in <code>bash -c</code> over SSH) reported every port down while Caddy returned 200 in the same call. The <code>ss -ltn</code> + HTTP-fetch pair is the ground truth and they agree. Do not trust the <code>/dev/tcp</code> one-liner in an SSH'd subshell.</p>
<p><span class="tag">2</span><b>The 401 trap.</b> For basicauth sites, Caddy serves the 401 challenge <i>before</i> the proxy. So a 401 does <b>not</b> prove the backend is up — RomM and Copyparty return 401 while their ports are down. This is why the checker must probe the backend <code>host:port</code> directly, never the Caddy site.</p>
<p><span class="tag">3</span><b>The homepage's core mechanism is architecturally blocked.</b> The page is <code>https://dankclaw.franksdank.cloud</code>; the service URLs are <code>http://&lt;name&gt;.franksdank.cloud</code>. A browser <code>fetch</code> of those is cross-origin (CORS-blocked), mixed content (https→http, hard-blocked), and auth-gated (401). Three independent failures. Hence the server-side <code>status.json</code> design.</p>
<p><span class="tag">4</span><b>The Dankserver media stack is down, and the cause is a missing <code>.env</code>.</b> Only Calibre-Web (no secret deps) and the LLM (separate systemd unit) are up. <code>compose.yaml</code> needs <code>MARIADB_ROOT_PASSWORD</code>, <code>ROMM_*</code>, <code>COPAY_PASS</code>, … — none present on the target. This is a <b>separate, urgent work item</b>, distinct from the homepage.</p>
<p><span class="tag">5</span><b>Plex is in an odd state.</b> Port 32400 is bound on 0.0.0.0 but a direct HTTP request gets no reply (000). Needs a look — it may be a stale socket or a non-HTTP listener. Flagged as UNDETERMINED, not counted up or down.</p>
<p><span class="tag">6</span><b>LLM "loading" nuance.</b> During model load the LLM <code>/health</code> returns 503. A TCP-connect probe still reads "up" (the port is listening). Kept to three states for now; a dedicated "loading" state is a later refinement.</p>
<p><span class="tag">7</span><b>My host is outside the LAN.</b> <code>*.franksdank.cloud</code> resolves to the public IP here; the services are LAN-only (dnsmasq maps them to 192.168.0.143 for LAN clients). So page loads and probes must run <b>from Dankclaw</b>.</p>
</div>
<h2>Work threads identified</h2>
<table>
<tr><th>Thread</th><th>What it is</th><th>Status</th></tr>
<tr><td>1 · Homepage</td><td>This design. Server-side status, user + admin pages, terminal TUI.</td><td class="wip"><span class="badge wip">GRILLING</span> 38 open questions</td></tr>
<tr><td>2 · Dankserver media stack</td><td>Down. Needs a <code>.env</code> (secrets) + a compose deploy.</td><td class="down"><span class="badge down">BLOCKED</span> separate, urgent</td></tr>
<tr><td>3 · *arr stack</td><td>Radarr/Sonarr/Lidarr/Readarr/Prowlarr/qBittorrent. Not started; possibly a different host.</td><td class="wip"><span class="badge wip">NOT STARTED</span></td></tr>
<tr><td>4 · Usage-docs</td><td>Per-service how-to pages, maintained separately from the reference docs.</td><td class="wip"><span class="badge wip">NOT STARTED</span></td></tr>
<tr><td>5 · "How it's built"</td><td>Optional public architecture page for tourists.</td><td class="wip"><span class="badge wip">OPTIONAL</span></td></tr>
<tr><td>6 · Plex</td><td>Investigate the bound-but-silent port 32400.</td><td class="und"><span class="badge und">INVESTIGATE</span></td></tr>
<tr><td>7 · Client-side probing</td><td>Documented future intent: same-origin https paths + per-service health + CORS.</td><td class="dim"><span class="badge">LATER</span></td></tr>
</table>
<h2>Open questions — answer these in the morning</h2>
<p class="dim">Each carries a recommendation. Reply <b>AGREE</b> to accept it, or state the change. Group by category letter.</p>
<h3>A · Architecture and structure</h3>
<div class="q"><span class="qn">Q6</span> — The optional <code>/how/</code> "tourist" page: build now or defer?<span class="rec"><b>Rec:</b> defer. When built, make it <b>public</b> (no auth) so a visitor can see the architecture; render the mermaid diagrams from this repo.</span><span class="verdict">verdict: <b>AGREE</b> · or change to: ____</span></div>
<div class="q"><span class="qn">Q7</span> — One <code>status.json</code> or two (<code>user-status.json</code> + <code>admin-status.json</code>)?<span class="rec"><b>Rec:</b> one <code>status.json</code>; each service carries a <code>category</code> field; each page filters client-side.</span><span class="verdict">verdict: <b>AGREE</b> · or change to: ____</span></div>
<div class="q"><span class="qn">Q8</span> — One <code>services.json</code> or split by page?<span class="rec"><b>Rec:</b> one <code>services.json</code> with a <code>category</code> field; the checker reads one file.</span><span class="verdict">verdict: <b>AGREE</b> · or change to: ____</span></div>
<h3>B · The checker</h3>
<div class="q"><span class="qn">Q9</span> — What language for the checker script?<span class="rec"><b>Rec:</b> <b>python3</b> (available on Dankclaw, no deps, clean JSON). A bash+<code>nc</code> variant works but hand-rolled JSON is error-prone.</span><span class="verdict">verdict: <b>AGREE</b> · or change to: ____</span></div>
<div class="q"><span class="qn">Q10</span> — Probe method: TCP connect or HTTP GET?<span class="rec"><b>Rec:</b> <b>TCP connect</b> to <code>host:port</code>. Uniform, works for non-HTTP backends, matches the verified ground truth.</span><span class="verdict">verdict: <b>AGREE</b> · or change to: ____</span></div>
<div class="q"><span class="qn">Q11</span> — Probe timeout per service?<span class="rec"><b>Rec:</b> <b>3 s</b>. A dead port refuses fast; a black-holed one times out at 3 s. Well under the 3-minute interval.</span><span class="verdict">verdict: <b>AGREE</b> · or change to: ____</span></div>
<div class="q"><span class="qn">Q12</span> — LLM "loading" state (health 503 during model load)?<span class="rec"><b>Rec:</b> keep three states; a listening port is Up. Optionally probe <code>/health</code> and treat 503 as Up. A "loading" state is a later refinement.</span><span class="verdict">verdict: <b>AGREE</b> · or change to: ____</span></div>
<div class="q"><span class="qn">Q13</span> — Where does the checker live in the repo?<span class="rec"><b>Rec:</b> <code>scripts/homepage-status.py</code> + <code>scripts/homepage-status.service</code> + <code>scripts/homepage-status.timer</code>, tracked in git.</span><span class="verdict">verdict: <b>AGREE</b> · or change to: ____</span></div>
<div class="q"><span class="qn">Q14</span> — How does the checker reach the Dankserver backends?<span class="rec"><b>Rec:</b> the <code>probe</code> field is a full <code>host:port</code> — <code>127.0.0.1:PORT</code> for Dankclaw, <code>192.168.0.235:PORT</code> for Dankserver. Verified reachable over the LAN.</span><span class="verdict">verdict: <b>AGREE</b> · or change to: ____</span></div>
<h3>C · The status model</h3>
<div class="q"><span class="qn">Q15</span> — How is a service marked WIP?<span class="rec"><b>Rec:</b> a static <code>"wip": true</code> field; the checker skips the probe. Drop the old self-referential "Homepage" entry.</span><span class="verdict">verdict: <b>AGREE</b> · or change to: ____</span></div>
<div class="q"><span class="qn">Q16</span> — Staleness: what if the timer fails and <code>status.json</code> is old?<span class="rec"><b>Rec:</b> write a <code>generated_at</code> timestamp; the page warns "status may be stale" when it is older than 10 minutes.</span><span class="verdict">verdict: <b>AGREE</b> · or change to: ____</span></div>
<div class="q"><span class="qn">Q17</span> — Atomic writes?<span class="rec"><b>Rec:</b> write to a temp file, then rename over <code>status.json</code>. A crash never leaves a half-written file.</span><span class="verdict">verdict: <b>AGREE</b> · or change to: ____</span></div>
<h3>D · Auth</h3>
<div class="q"><span class="qn">Q18</span> — The admin password: a second <code>.env</code> variable?<span class="rec"><b>Rec:</b> yes — <code>CADDY_ADMIN_HASH</code> in the host <code>.env</code> (git-ignored), rendered at deploy, operator-only. Root keeps <code>CADDY_BASICAUTH_HASH</code>.</span><span class="verdict">verdict: <b>AGREE</b> · or change to: ____</span></div>
<div class="q"><span class="qn">Q19</span> — Per-path basicauth in Caddy v2?<span class="rec"><b>Rec:</b> an <code>@admin path /admin/*</code> matcher with a second <code>basicauth @admin</code> block. Confirm exact syntax against the Caddy docs during implementation.</span><span class="verdict">verdict: <b>AGREE</b> · or change to: ____</span></div>
<h3>E · The pages and content</h3>
<div class="q"><span class="qn">Q20</span> — Where do the one-line descriptions come from?<span class="rec"><b>Rec:</b> hand-written one-liners in <code>services.json</code>, drafted from the existing reference docs.</span><span class="verdict">verdict: <b>AGREE</b> · or change to: ____</span></div>
<div class="q"><span class="qn">Q21</span> — The "Open" link: the Caddy site or the direct backend?<span class="rec"><b>Rec:</b> the <b>Caddy site</b> URL (what a user opens). Status-only backends set <code>showLink: false</code>.</span><span class="verdict">verdict: <b>AGREE</b> · or change to: ____</span></div>
<div class="q"><span class="qn">Q22</span> — The "docs" link: usage page or the existing reference doc?<span class="rec"><b>Rec:</b> link to the <b>existing</b> reference doc as a placeholder; swap to the per-service usage page when that workstream is built.</span><span class="verdict">verdict: <b>AGREE</b> · or change to: ____</span></div>
<div class="q"><span class="qn">Q23</span> — Page layout: cards or a terminal-style list?<span class="rec"><b>Rec:</b> a <b>terminal-style list</b> (one service per line, like <code>systemctl list-units</code>). Columns: <code>STATUS | NAME | description | (open) (docs)</code>.</span><span class="verdict">verdict: <b>AGREE</b> · or change to: ____</span></div>
<div class="q"><span class="qn">Q24</span> — A summary line at the top?<span class="rec"><b>Rec:</b> yes — one line per page, e.g. <code>UP 9 DOWN 2 WIP 1</code>.</span><span class="verdict">verdict: <b>AGREE</b> · or change to: ____</span></div>
<h3>F · Styling</h3>
<div class="q"><span class="qn">Q25</span> — Terminus: bundle the font or use a fallback?<span class="rec"><b>Rec:</b> <b>bundle</b> Terminus (woff2) via <code>@font-face</code>, with a monospace fallback. Confirm the license (freely distributable) and drop files in <code>fonts/</code>.</span><span class="verdict">verdict: <b>AGREE</b> · or change to: ____</span></div>
<div class="q"><span class="qn">Q26</span> — Confirm the palette?<span class="rec"><b>Rec:</b> as proposed (green <code>#33ff66</code>, red <code>#ff5555</code>, amber <code>#ffcc33</code>, cyan <code>#33ccff</code>, bg <code>#050805</code>). Tune for contrast.</span><span class="verdict">verdict: <b>AGREE</b> · or change to: ____</span></div>
<div class="q"><span class="qn">Q27</span> — CRT effects (scanlines + glow)?<span class="rec"><b>Rec:</b> subtle scanlines + a soft green <code>text-shadow</code>. Make it toggleable (<code>?crt=0</code>).</span><span class="verdict">verdict: <b>AGREE</b> · or change to: ____</span></div>
<div class="q"><span class="qn">Q28</span> — Blinking cursor / animation?<span class="rec"><b>Rec:</b> a subtle CSS-only blinking block cursor on the header; respect <code>prefers-reduced-motion</code>.</span><span class="verdict">verdict: <b>AGREE</b> · or change to: ____</span></div>
<h3>G · The usage-docs workstream</h3>
<div class="q"><span class="qn">Q29</span> — Scope of the per-service usage pages?<span class="rec"><b>Rec:</b> concise <b>how-to</b> guides (what it is, how to open it, the common tasks). Distinct from the deep reference docs.</span><span class="verdict">verdict: <b>AGREE</b> · or change to: ____</span></div>
<div class="q"><span class="qn">Q30</span> — Where do the usage pages live?<span class="rec"><b>Rec:</b> served from the homepage site, same origin, at <code>/usage/&lt;svc&gt;/</code>; source under <code>shared/homepage/usage/&lt;svc&gt;/</code>.</span><span class="verdict">verdict: <b>AGREE</b> · or change to: ____</span></div>
<div class="q"><span class="qn">Q31</span> — Are the usage pages password-protected?<span class="rec"><b>Rec:</b> yes — user pages behind the shared password, admin pages behind the operator password.</span><span class="verdict">verdict: <b>AGREE</b> · or change to: ____</span></div>
<div class="q"><span class="qn">Q32</span> — Is the usage-docs workstream a separate effort?<span class="rec"><b>Rec:</b> yes, after the core homepage is live. For now the "docs" link points to the existing reference doc (Q22).</span><span class="verdict">verdict: <b>AGREE</b> · or change to: ____</span></div>
<h3>H · Deployment and operations</h3>
<div class="q"><span class="qn">Q33</span> — How is the homepage deployed?<span class="rec"><b>Rec:</b> extend <code>deploy.sh</code> (Dankclaw path): copy <code>shared/homepage/</code> to <code>/srv/homepage/</code>, install the timer + service, render the Caddyfile (both hashes), reload Caddy.</span><span class="verdict">verdict: <b>AGREE</b> · or change to: ____</span></div>
<div class="q"><span class="qn">Q34</span> — Who creates <code>/srv/homepage/</code>?<span class="rec"><b>Rec:</b> <code>deploy.sh</code> creates it and copies the assets; the checker writes <code>status.json</code> there at runtime.</span><span class="verdict">verdict: <b>AGREE</b> · or change to: ____</span></div>
<div class="q"><span class="qn">Q35</span> — Is the timer enabled at boot?<span class="rec"><b>Rec:</b> yes — a systemd timer + service, enabled, survives reboots, runs every 3 minutes.</span><span class="verdict">verdict: <b>AGREE</b> · or change to: ____</span></div>
<h3>I · Testing</h3>
<div class="q"><span class="qn">Q36</span> — Tests for the checker?<span class="rec"><b>Rec:</b> yes — a bats test runs the checker against a fake up port, a fake down port, and a WIP service, then validates the <code>status.json</code> schema. TDD, per repo rules.</span><span class="verdict">verdict: <b>AGREE</b> · or change to: ____</span></div>
<div class="q"><span class="qn">Q37</span> — Tests for the pages and config?<span class="rec"><b>Rec:</b> yes — validate the <code>services.json</code> schema, run <code>caddy validate</code>, and check the pages reference <code>status.json</code>.</span><span class="verdict">verdict: <b>AGREE</b> · or change to: ____</span></div>
<div class="q"><span class="qn">Q38</span> — A security test for the two passwords?<span class="rec"><b>Rec:</b> yes — assert the Caddyfile carries two distinct basicauth hashes (root vs <code>/admin/</code>).</span><span class="verdict">verdict: <b>AGREE</b> · or change to: ____</span></div>
<h3>J · Edge cases and robustness</h3>
<div class="q"><span class="qn">Q39</span> — A backend moves hosts (e.g. the *arr stack to a new box)?<span class="rec"><b>Rec:</b> no code change — the <code>probe</code> field is a full <code>host:port</code>; a move is a config edit.</span><span class="verdict">verdict: <b>AGREE</b> · or change to: ____</span></div>
<div class="q"><span class="qn">Q40</span> — A slow LAN hop to a Dankserver backend?<span class="rec"><b>Rec:</b> the 3 s timeout handles it — a slow-but-alive backend connects (Up); a dead one times out (Down).</span><span class="verdict">verdict: <b>AGREE</b> · or change to: ____</span></div>
<div class="q"><span class="qn">Q41</span> — Client-side auto-refresh?<span class="rec"><b>Rec:</b> the page polls <code>status.json</code> every <b>60 s</b> (the file updates every 3 min).</span><span class="verdict">verdict: <b>AGREE</b> · or change to: ____</span></div>
<div class="q"><span class="qn">Q42</span> — Mobile responsiveness?<span class="rec"><b>Rec:</b> yes — stack the columns and shrink the font on narrow screens.</span><span class="verdict">verdict: <b>AGREE</b> · or change to: ____</span></div>
<div class="q"><span class="qn">Q43</span> — A "last checked" timestamp on the page?<span class="rec"><b>Rec:</b> yes — the footer shows <code>last checked HH:MM:SS</code> from <code>generated_at</code>.</span><span class="verdict">verdict: <b>AGREE</b> · or change to: ____</span></div>
<h2>What I changed (knowledge base)</h2>
<table>
<tr><th>File</th><th>Change</th></tr>
<tr><td><code>docs/Infrastructure/Homepage.md</code></td><td>Rewritten to the settled design: structure, status model, checker, auth, classification, layout. Added three mermaid diagrams (site structure, status states, checker flow).</td></tr>
<tr><td><code>docs/Infrastructure/Homepage Design Grilling.md</code></td><td>New. The decision log: settled Q1–Q5 + the 38 open questions (Q6–Q43) with recommendations + the build-order diagram.</td></tr>
<tr><td><code>docs/index.md</code></td><td>Linked the new grilling page (satisfies the "index links every page" test).</td></tr>
<tr><td><code>~/.pi/agent/docs/log.md</code></td><td>Added a homepage-design bullet to the 2026-08-22 entry (design, baseline, threads).</td></tr>
<tr><td><code>report/homepage-grilling-2026-08-22.html</code></td><td>This report. Self-contained, terminal-TUI styled.</td></tr>
</table>
<h2>How to continue</h2>
<div class="panel">
<p>Reply to the open questions (Q6–Q43) — <b>AGREE</b> or the change, grouped by category. Once settled, the build order is fixed:</p>
<pre>
settle Q6-Q43 -&gt; write services.json + checker -&gt; write pages + theme
-&gt; extend deploy.sh + Caddyfile -&gt; tests green -&gt; deploy to Dankclaw
-&gt; verify in browser
</pre>
<p class="dim">Separate threads to schedule (not part of the homepage build): the Dankserver media stack (missing <code>.env</code>), the *arr stack, the usage-docs workstream, the optional "how it's built" page, and the Plex investigation.</p>
<p class="dim">Report is self-contained (no network needed). A copy is committed at <code>report/homepage-grilling-2026-08-22.html</code> and served at the URL below.</p>
</div>
<p class="dim" style="margin-top:28px; border-top:1px solid var(--border); padding-top:12px">
generated 2026-08-22 (night) · frank program · chief designer · <span class="ok">tests: 97/97 green</span>
</p>
</div>
</body>
</html>