diff --git a/dankclaw/Caddyfile b/dankclaw/Caddyfile index 5a32c37..4d05768 100644 --- a/dankclaw/Caddyfile +++ b/dankclaw/Caddyfile @@ -337,6 +337,15 @@ http://:8017 { try_files /docs.html file_server } + # /whoami echoes the outpost-injected identity headers so the page + # can gate the admin button client-side (the /admin* 403 above stays + # the real gate; this only decides what is shown). The outpost strips + # client-supplied X-Authentik-* before injecting, so this is spoof-proof. + handle /whoami { + templates + header Content-Type application/json + respond "{\"user\":\"{header.X-Authentik-Username}\",\"groups\":\"{header.X-Authentik-Groups}\"}" + } @data path /services.json /status.json /app.js /style.css /deployed.js /img/* /sfx/* handle @data { file_server diff --git a/serverAppConfigs-docs/Infrastructure/Homepage.md b/serverAppConfigs-docs/Infrastructure/Homepage.md index 0ee4888..c58e8d6 100644 --- a/serverAppConfigs-docs/Infrastructure/Homepage.md +++ b/serverAppConfigs-docs/Infrastructure/Homepage.md @@ -87,6 +87,34 @@ from the dankcloud SSO; SearXNG is obfuscated, not anonymous (upstream API keys tie to Frank). The user docs `/docs#models` section carries the same renames, api-id mapping, supervision and compaction notes. +**Icons, card polish, admin identity gate (2026-10-08, later wave):** every +app card now carries hex-clipped art beside its name (`services.json` `art` +field, files under `img/apps/`): 19 brand marks tinted amber from the +Simple Icons CDN plus 19 hand-improvised monogram SVGs (amber hex + VT323 +initials) for apps with no mark — Copyparty, Marreta, Hermes (+dashboard), +node-exporter, Gluetun, OpenWebUI, Calibre-Web, RomM, Seerr, Loki, Alloy, +Alertmanager, Valkey, Prowlarr, Readarr, Lidarr, Tautulli, ComfyUI. Model +cards render in the order zealot → dragoon → archon (registry order is the +contract; `homepage_config.bats` pins it). Dragoon and zealot share one +common uncensored legal warning (adult content, harmful/hateful/illegal +descriptions, "sensitive offensive cybersecurity topics", severe legal +risk, "don't ruin this server for everybody"), rendered as the hover +label; archon's ask-Frank rule is a bordered red `.mi-perm` callout. +SearXNG's obfuscation-not-anonymity note moved out of its description into +a card-level `warn` (`.svc-warn` hover label) and the same hover treatment +in the docs. Gitea's note now documents self-service signup — no need to +ask Frank. Footer: docs / admin / account settings are bordered glowing +`.foot-btn` buttons; **the admin button is identity-gated** — Caddy's +`/whoami` echoes the outpost-injected `X-Authentik-Username`/`X-Authentik-Groups` +headers (the outpost strips client-supplied copies, so the echo is +spoof-proof), and `app.js` shows the button only to `homepage-admins` +members. The `/admin*` 403 stays the real gate; the e2e specs pin both +sides (standard user: hidden + 403; admin: visible). Docs were swept for +naming consistency: Protoss names primary everywhere, bracketed api ids +at the model headings, and a note that a future switchover renames the +ids themselves (OpenWebUI picker + raw API + models.yml all move together +— not done yet). + ## 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. @@ -182,7 +210,9 @@ shared/homepage/ ├── 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) +├── img/ # served at /img: docs screenshots, unit portraits +│ ├── apps/ # (unit-*.jpg) and app card icons (apps/*.svg) +├── sfx/ # StarCraft unit voice .ogg files served at /sfx └── fonts/ # optional self-hosted fonts (deployed with the page; VT323 comes from the Google Fonts CDN, this dir is absent) ``` diff --git a/shared/homepage/admin.html b/shared/homepage/admin.html index f79c122..1c7e89e 100644 --- a/shared/homepage/admin.html +++ b/shared/homepage/admin.html @@ -19,7 +19,7 @@
diff --git a/shared/homepage/app.js b/shared/homepage/app.js index 471eb85..3f46793 100644 --- a/shared/homepage/app.js +++ b/shared/homepage/app.js @@ -92,6 +92,7 @@ function miCard(svc, state) { if (m.use) parts.push('
Use: ' + esc(m.use) + '
'); if (m.voice) parts.push(voiceBtn(m.voice.text, m.voice.sfx)); if (m.supervision) parts.push('
' + esc(m.supervision) + '
'); + if (m.permission) parts.push('
⚠ ' + esc(m.permission) + '
'); if (m.warn) parts.push('
' + '⚠ ' + esc(m.warn.split(':')[0]) + ' \u2014 hover to read
'); const card = document.createElement('article'); @@ -125,12 +126,16 @@ function renderPanel(svc, state) { const links = []; if (svc.showLink && svc.url) links.push('open'); if (svc.docs) links.push('docs'); + const art = svc.art ? '' : ''; + const warn = svc.warn + ? '
⚠ privacy note \u2014 hover to read
' + : ''; panel.innerHTML = - '
' + + '
' + art + ' ' + BADGE_LABEL[state] + '' + ' ' + esc(svc.name) + '' + '
' + - '
' + descHTML(svc) + '
' + + '
' + descHTML(svc) + '
' + warn + ''; return panel; } @@ -197,9 +202,24 @@ function fillDeployed() { if (el && window.DEPLOYED_AT) el.textContent = window.DEPLOYED_AT; } +// Show the admin button only to members of the authentik homepage-admins +// group (the same group the /admin* server gate enforces). The data comes +// from Caddy's /whoami, which echoes the outpost-injected identity headers. +function gateAdminLink() { + const link = document.getElementById('admin-link'); + if (!link) return; + fetch('/whoami', { headers: { accept: 'application/json' } }) + .then(function (r) { return r.ok ? r.json() : null; }) + .then(function (me) { + if (me && /homepage-admins/i.test(me.groups || '')) link.hidden = false; + }) + .catch(function () {}); +} + function main() { applyBare(); fillDeployed(); + gateAdminLink(); Promise.all([load('services.json'), load('status.json')]) .then(function (pair) { renderPanels(pair[0], pair[1]); }) .catch(function (e) { diff --git a/shared/homepage/docs.html b/shared/homepage/docs.html index b0efe55..5640e2c 100644 --- a/shared/homepage/docs.html +++ b/shared/homepage/docs.html @@ -55,26 +55,9 @@ The homepage "Model Information" panel shows whether each one is up right now — and plays the unit's voice if you click its quoted line.

-

frank-dragoon — deep reasoning (api id: frank-large)

- - -

frank-archon — the big one (api id: frank-primary)

- +

Naming: the Open WebUI picker and the raw API show the + endpoint api ids in brackets below. The Protoss names are the family + names; a future switchover will rename the ids to match.

frank-zealot — the fast utility model (api id: frank-secondary)

+ +

frank-dragoon — deep reasoning (api id: frank-large)

+ diff --git a/shared/homepage/img/apps/caddy.svg b/shared/homepage/img/apps/caddy.svg new file mode 100644 index 0000000..68a355e --- /dev/null +++ b/shared/homepage/img/apps/caddy.svg @@ -0,0 +1 @@ +Caddy \ No newline at end of file diff --git a/shared/homepage/img/apps/gitea.svg b/shared/homepage/img/apps/gitea.svg new file mode 100644 index 0000000..f850d03 --- /dev/null +++ b/shared/homepage/img/apps/gitea.svg @@ -0,0 +1 @@ +Gitea \ No newline at end of file diff --git a/shared/homepage/img/apps/grafana.svg b/shared/homepage/img/apps/grafana.svg new file mode 100644 index 0000000..d77d4d3 --- /dev/null +++ b/shared/homepage/img/apps/grafana.svg @@ -0,0 +1 @@ +Grafana \ No newline at end of file diff --git a/shared/homepage/img/apps/jellyfin.svg b/shared/homepage/img/apps/jellyfin.svg new file mode 100644 index 0000000..cbba387 --- /dev/null +++ b/shared/homepage/img/apps/jellyfin.svg @@ -0,0 +1 @@ +Jellyfin \ No newline at end of file diff --git a/shared/homepage/img/apps/mariadb.svg b/shared/homepage/img/apps/mariadb.svg new file mode 100644 index 0000000..ec6c871 --- /dev/null +++ b/shared/homepage/img/apps/mariadb.svg @@ -0,0 +1 @@ +MariaDB \ No newline at end of file diff --git a/shared/homepage/img/apps/mono-am.svg b/shared/homepage/img/apps/mono-am.svg new file mode 100644 index 0000000..c5e2c0a --- /dev/null +++ b/shared/homepage/img/apps/mono-am.svg @@ -0,0 +1 @@ +AM \ No newline at end of file diff --git a/shared/homepage/img/apps/mono-ay.svg b/shared/homepage/img/apps/mono-ay.svg new file mode 100644 index 0000000..6f47c09 --- /dev/null +++ b/shared/homepage/img/apps/mono-ay.svg @@ -0,0 +1 @@ +AY \ No newline at end of file diff --git a/shared/homepage/img/apps/mono-cp.svg b/shared/homepage/img/apps/mono-cp.svg new file mode 100644 index 0000000..c41ed65 --- /dev/null +++ b/shared/homepage/img/apps/mono-cp.svg @@ -0,0 +1 @@ +CP \ No newline at end of file diff --git a/shared/homepage/img/apps/mono-cu.svg b/shared/homepage/img/apps/mono-cu.svg new file mode 100644 index 0000000..6000cfc --- /dev/null +++ b/shared/homepage/img/apps/mono-cu.svg @@ -0,0 +1 @@ +CU \ No newline at end of file diff --git a/shared/homepage/img/apps/mono-cw.svg b/shared/homepage/img/apps/mono-cw.svg new file mode 100644 index 0000000..d160cd0 --- /dev/null +++ b/shared/homepage/img/apps/mono-cw.svg @@ -0,0 +1 @@ +CW \ No newline at end of file diff --git a/shared/homepage/img/apps/mono-gt.svg b/shared/homepage/img/apps/mono-gt.svg new file mode 100644 index 0000000..359a148 --- /dev/null +++ b/shared/homepage/img/apps/mono-gt.svg @@ -0,0 +1 @@ +GT \ No newline at end of file diff --git a/shared/homepage/img/apps/mono-hd.svg b/shared/homepage/img/apps/mono-hd.svg new file mode 100644 index 0000000..a07c466 --- /dev/null +++ b/shared/homepage/img/apps/mono-hd.svg @@ -0,0 +1 @@ +HD \ No newline at end of file diff --git a/shared/homepage/img/apps/mono-hm.svg b/shared/homepage/img/apps/mono-hm.svg new file mode 100644 index 0000000..ef77017 --- /dev/null +++ b/shared/homepage/img/apps/mono-hm.svg @@ -0,0 +1 @@ +HM \ No newline at end of file diff --git a/shared/homepage/img/apps/mono-ld.svg b/shared/homepage/img/apps/mono-ld.svg new file mode 100644 index 0000000..ac2bc94 --- /dev/null +++ b/shared/homepage/img/apps/mono-ld.svg @@ -0,0 +1 @@ +LD \ No newline at end of file diff --git a/shared/homepage/img/apps/mono-lk.svg b/shared/homepage/img/apps/mono-lk.svg new file mode 100644 index 0000000..0075d6a --- /dev/null +++ b/shared/homepage/img/apps/mono-lk.svg @@ -0,0 +1 @@ +LK \ No newline at end of file diff --git a/shared/homepage/img/apps/mono-mr.svg b/shared/homepage/img/apps/mono-mr.svg new file mode 100644 index 0000000..67eef21 --- /dev/null +++ b/shared/homepage/img/apps/mono-mr.svg @@ -0,0 +1 @@ +MR \ No newline at end of file diff --git a/shared/homepage/img/apps/mono-ne.svg b/shared/homepage/img/apps/mono-ne.svg new file mode 100644 index 0000000..5112bcf --- /dev/null +++ b/shared/homepage/img/apps/mono-ne.svg @@ -0,0 +1 @@ +NE \ No newline at end of file diff --git a/shared/homepage/img/apps/mono-ow.svg b/shared/homepage/img/apps/mono-ow.svg new file mode 100644 index 0000000..c92dbb7 --- /dev/null +++ b/shared/homepage/img/apps/mono-ow.svg @@ -0,0 +1 @@ +OW \ No newline at end of file diff --git a/shared/homepage/img/apps/mono-pw.svg b/shared/homepage/img/apps/mono-pw.svg new file mode 100644 index 0000000..2b8165c --- /dev/null +++ b/shared/homepage/img/apps/mono-pw.svg @@ -0,0 +1 @@ +PW \ No newline at end of file diff --git a/shared/homepage/img/apps/mono-ra.svg b/shared/homepage/img/apps/mono-ra.svg new file mode 100644 index 0000000..c6e5ae2 --- /dev/null +++ b/shared/homepage/img/apps/mono-ra.svg @@ -0,0 +1 @@ +RA \ No newline at end of file diff --git a/shared/homepage/img/apps/mono-rm.svg b/shared/homepage/img/apps/mono-rm.svg new file mode 100644 index 0000000..5f3fab7 --- /dev/null +++ b/shared/homepage/img/apps/mono-rm.svg @@ -0,0 +1 @@ +RM \ No newline at end of file diff --git a/shared/homepage/img/apps/mono-sr.svg b/shared/homepage/img/apps/mono-sr.svg new file mode 100644 index 0000000..2158d97 --- /dev/null +++ b/shared/homepage/img/apps/mono-sr.svg @@ -0,0 +1 @@ +SR \ No newline at end of file diff --git a/shared/homepage/img/apps/mono-tt.svg b/shared/homepage/img/apps/mono-tt.svg new file mode 100644 index 0000000..205afb4 --- /dev/null +++ b/shared/homepage/img/apps/mono-tt.svg @@ -0,0 +1 @@ +TT \ No newline at end of file diff --git a/shared/homepage/img/apps/mono-vk.svg b/shared/homepage/img/apps/mono-vk.svg new file mode 100644 index 0000000..e989881 --- /dev/null +++ b/shared/homepage/img/apps/mono-vk.svg @@ -0,0 +1 @@ +VK \ No newline at end of file diff --git a/shared/homepage/img/apps/pihole.svg b/shared/homepage/img/apps/pihole.svg new file mode 100644 index 0000000..e8b5255 --- /dev/null +++ b/shared/homepage/img/apps/pihole.svg @@ -0,0 +1 @@ +Pi-hole \ No newline at end of file diff --git a/shared/homepage/img/apps/plex.svg b/shared/homepage/img/apps/plex.svg new file mode 100644 index 0000000..3eb7eda --- /dev/null +++ b/shared/homepage/img/apps/plex.svg @@ -0,0 +1 @@ +Plex \ No newline at end of file diff --git a/shared/homepage/img/apps/postgresql.svg b/shared/homepage/img/apps/postgresql.svg new file mode 100644 index 0000000..d399011 --- /dev/null +++ b/shared/homepage/img/apps/postgresql.svg @@ -0,0 +1 @@ +PostgreSQL \ No newline at end of file diff --git a/shared/homepage/img/apps/prometheus.svg b/shared/homepage/img/apps/prometheus.svg new file mode 100644 index 0000000..0094bb3 --- /dev/null +++ b/shared/homepage/img/apps/prometheus.svg @@ -0,0 +1 @@ +Prometheus \ No newline at end of file diff --git a/shared/homepage/img/apps/qbittorrent.svg b/shared/homepage/img/apps/qbittorrent.svg new file mode 100644 index 0000000..25764f0 --- /dev/null +++ b/shared/homepage/img/apps/qbittorrent.svg @@ -0,0 +1 @@ +qbittorrent \ No newline at end of file diff --git a/shared/homepage/img/apps/radarr.svg b/shared/homepage/img/apps/radarr.svg new file mode 100644 index 0000000..44e52cb --- /dev/null +++ b/shared/homepage/img/apps/radarr.svg @@ -0,0 +1 @@ +radarr \ No newline at end of file diff --git a/shared/homepage/img/apps/searxng.svg b/shared/homepage/img/apps/searxng.svg new file mode 100644 index 0000000..656a6d5 --- /dev/null +++ b/shared/homepage/img/apps/searxng.svg @@ -0,0 +1 @@ +SearXNG \ No newline at end of file diff --git a/shared/homepage/img/apps/sonarr.svg b/shared/homepage/img/apps/sonarr.svg new file mode 100644 index 0000000..0807277 --- /dev/null +++ b/shared/homepage/img/apps/sonarr.svg @@ -0,0 +1 @@ +sonarr \ No newline at end of file diff --git a/shared/homepage/index.html b/shared/homepage/index.html index 062987a..54e60a5 100644 --- a/shared/homepage/index.html +++ b/shared/homepage/index.html @@ -19,9 +19,11 @@
diff --git a/shared/homepage/services.json b/shared/homepage/services.json index 0d4a730..254c249 100644 --- a/shared/homepage/services.json +++ b/shared/homepage/services.json @@ -3,13 +3,15 @@ "name": "SearXNG", "category": "user", "section": "Web Apps", - "description": "AI-free, VPN-routed search engine, fully hosted on Frank's server. Your searches are obfuscated, not anonymous: upstream API keys are tied to Frank's identity.", + "description": "AI-free, VPN-routed search engine, fully hosted on Frank's server.", "url": "http://search.franksdank.cloud", "docs": "", "probe": "127.0.0.1:8081", "method": "http", "wip": false, - "showLink": true + "showLink": true, + "warn": "Searches are obfuscated, not anonymous: SearXNG hides you from the upstream engines and the VPN tunnel hides the traffic from the LAN — but the upstream search API keys are tied to Frank’s identity. Don’t treat it as Tor.", + "art": "/img/apps/searxng.svg" }, { "name": "OpenWebUI", @@ -21,19 +23,21 @@ "probe": "127.0.0.1:3005", "method": "http", "wip": false, - "showLink": true + "showLink": true, + "art": "/img/apps/mono-ow.svg" }, { "name": "Gitea", "category": "user", "section": "Web Apps", - "description": "Git code hosting and review platform. Needs its own login, separate from the dankcloud single sign-on (ask Frank).", + "description": "Git code hosting and review platform. Needs its own login, separate from the dankcloud single sign-on — use the sign-up flow in the app to get an account; no need to ask Frank.", "url": "http://gitea.franksdank.cloud", "docs": "", "probe": "127.0.0.1:3000", "method": "http", "wip": false, - "showLink": true + "showLink": true, + "art": "/img/apps/gitea.svg" }, { "name": "Calibre-Web", @@ -45,7 +49,8 @@ "probe": "192.168.0.235:8083", "method": "http", "wip": "down", - "showLink": false + "showLink": false, + "art": "/img/apps/mono-cw.svg" }, { "name": "RomM", @@ -57,7 +62,8 @@ "probe": "192.168.0.235:8085", "method": "http", "wip": "down", - "showLink": false + "showLink": false, + "art": "/img/apps/mono-rm.svg" }, { "name": "Copyparty", @@ -69,7 +75,8 @@ "probe": "192.168.0.235:3923", "method": "http", "wip": false, - "showLink": true + "showLink": true, + "art": "/img/apps/mono-cp.svg" }, { "name": "Plex", @@ -81,7 +88,8 @@ "probe": "192.168.0.235:32400", "method": "tcp", "wip": false, - "showLink": true + "showLink": true, + "art": "/img/apps/plex.svg" }, { "name": "Jellyfin", @@ -93,7 +101,8 @@ "probe": "192.168.0.235:8096", "method": "http", "wip": "down", - "showLink": false + "showLink": false, + "art": "/img/apps/jellyfin.svg" }, { "name": "Seerr", @@ -105,7 +114,8 @@ "probe": "127.0.0.1:5055", "method": "http", "wip": "down", - "showLink": false + "showLink": false, + "art": "/img/apps/mono-sr.svg" }, { "name": "Marreta", @@ -117,7 +127,8 @@ "probe": "127.0.0.1:8082", "method": "http", "wip": false, - "showLink": true + "showLink": true, + "art": "/img/apps/mono-mr.svg" }, { "name": "Sonarr", @@ -129,7 +140,8 @@ "probe": "192.168.0.112:8989", "method": "http", "wip": false, - "showLink": true + "showLink": true, + "art": "/img/apps/sonarr.svg" }, { "name": "Hermes", @@ -141,7 +153,8 @@ "probe": "127.0.0.1:8787", "method": "http", "wip": false, - "showLink": true + "showLink": true, + "art": "/img/apps/mono-hm.svg" }, { "name": "Hermes dashboard", @@ -153,7 +166,8 @@ "probe": "192.168.0.143:9119", "method": "http", "wip": false, - "showLink": true + "showLink": true, + "art": "/img/apps/mono-hd.svg" }, { "name": "Grafana", @@ -165,7 +179,8 @@ "probe": "127.0.0.1:3002", "method": "http", "wip": false, - "showLink": true + "showLink": true, + "art": "/img/apps/grafana.svg" }, { "name": "Prometheus", @@ -177,7 +192,8 @@ "probe": "127.0.0.1:9090", "method": "http", "wip": false, - "showLink": true + "showLink": true, + "art": "/img/apps/prometheus.svg" }, { "name": "Loki", @@ -189,7 +205,8 @@ "probe": "127.0.0.1:3100", "method": "http", "wip": false, - "showLink": true + "showLink": true, + "art": "/img/apps/mono-lk.svg" }, { "name": "node-exporter", @@ -201,7 +218,8 @@ "probe": "127.0.0.1:9100", "method": "http", "wip": false, - "showLink": false + "showLink": false, + "art": "/img/apps/mono-ne.svg" }, { "name": "alloy", @@ -213,7 +231,8 @@ "probe": "127.0.0.1:12345", "method": "http", "wip": false, - "showLink": false + "showLink": false, + "art": "/img/apps/mono-ay.svg" }, { "name": "alertmanager", @@ -225,7 +244,8 @@ "probe": "127.0.0.1:9093", "method": "http", "wip": false, - "showLink": false + "showLink": false, + "art": "/img/apps/mono-am.svg" }, { "name": "Caddy", @@ -237,7 +257,8 @@ "probe": "127.0.0.1:80", "method": "http", "wip": false, - "showLink": false + "showLink": false, + "art": "/img/apps/caddy.svg" }, { "name": "Pi-hole", @@ -249,7 +270,8 @@ "probe": "192.168.0.143:8083", "method": "http", "wip": false, - "showLink": true + "showLink": true, + "art": "/img/apps/pihole.svg" }, { "name": "Gluetun", @@ -261,6 +283,41 @@ "probe": "127.0.0.1:51820", "method": "tcp", "wip": "closed", + "showLink": false, + "art": "/img/apps/mono-gt.svg" + }, + { + "name": "frank-zealot", + "category": "user", + "section": "AI", + "model": { + "unit": "zealot", + "role": "Frank's utility model", + "tagline": "HONOR GUIDE ME!", + "sfx": [ + "zealot-honor.ogg" + ], + "equiv": "Equivalent to last year's frontier level (2025).", + "spec": "Qwen3.5-9B-Uncensored (Q6_K) on an NVIDIA RTX 3090 — ~95 tokens/s; 200k-token context, two submissions at a time; sees images.", + "inputs": [ + "text", + "image" + ], + "use": "Code editor autocomplete, agentic search, RAG, research subagents, translation, roleplay, and summarization. If your agent harness lets you set a compaction model, set this one: a compaction job on the bigger models blocks them for far longer.", + "voice": { + "text": "I LONG FOR COMBAT!", + "sfx": [ + "zealot-combat.ogg" + ] + }, + "supervision": "Requires tight human supervision.", + "warn": "Uncensored model: output can include adult content, content that describes harmful, hateful, or illegal acts, content concerning “sensitive offensive cybersecurity topics”, and other output that may carry severe legal risk. Take this warning seriously — don’t ruin this server for everybody." + }, + "url": "", + "docs": "/docs#models", + "probe": "127.0.0.1:7771", + "method": "http", + "wip": false, "showLink": false }, { @@ -271,14 +328,25 @@ "unit": "dragoon", "role": "Frank's deep reasoning model", "tagline": "I have returned — make use of me.", - "sfx": ["dragoon-returned.ogg", "dragoon-makeuse.ogg"], + "sfx": [ + "dragoon-returned.ogg", + "dragoon-makeuse.ogg" + ], "equiv": "Equivalent to Claude Sonnet 5 / OpenAI-Luna (Summer 2026 frontier intelligence).", "spec": "Qwen3.8-Flash-Next v2 Swift1.5 abliterated (halogen flash-server, ~62 GiB) on the Strix Halo iGPU — 200k-token context, two submissions at a time; sees images.", - "inputs": ["text", "image"], + "inputs": [ + "text", + "image" + ], "use": "Complex agentic tasks, programming, system administration, and \"claw\"-style personal agents.", - "voice": { "text": "Awaiting instructions.", "sfx": ["dragoon-awaiting.ogg"] }, + "voice": { + "text": "Awaiting instructions.", + "sfx": [ + "dragoon-awaiting.ogg" + ] + }, "supervision": "Safe for autonomous, low-supervision use.", - "warn": "Abliterated (censorship-removed) model: output can include adult themes. You are responsible for the legal consequences of how you use it." + "warn": "Uncensored model: output can include adult content, content that describes harmful, hateful, or illegal acts, content concerning “sensitive offensive cybersecurity topics”, and other output that may carry severe legal risk. Take this warning seriously — don’t ruin this server for everybody." }, "url": "", "docs": "/docs#models", @@ -295,13 +363,24 @@ "unit": "archon", "role": "Frank's strongest and fastest model runtime", "tagline": "...Power Overwhelming...", - "sfx": ["archon-power.ogg"], + "sfx": [ + "archon-power.ogg" + ], "equiv": "Equivalent to Claude Opus 4.8 / OpenAI-Terra (Summer 2026 frontier intelligence).", "spec": "Qwen3.8-Flash-Next at native FP8 precision (radiance, 173 GiB) on twin AMD R9700s with more than 200 GB of system RAM — 200k-token context, two submissions at a time; sees images.", - "inputs": ["text", "image"], - "use": "Software engineering, system administration, and instruction tasks. Please ask Frank before use, as he uses this one primarily.", - "voice": { "text": "...we BURN.", "sfx": ["archon-burn.ogg"] }, - "supervision": "Safe for autonomous, low-supervision use." + "inputs": [ + "text", + "image" + ], + "use": "Software engineering, system administration, and instruction tasks.", + "voice": { + "text": "...we BURN.", + "sfx": [ + "archon-burn.ogg" + ] + }, + "supervision": "Safe for autonomous, low-supervision use.", + "permission": "Ask Frank for permission before use — this is his primary model; he uses it more than anything else on the fleet." }, "url": "", "docs": "/docs#models", @@ -310,30 +389,6 @@ "wip": false, "showLink": false }, - { - "name": "frank-zealot", - "category": "user", - "section": "AI", - "model": { - "unit": "zealot", - "role": "Frank's utility model", - "tagline": "HONOR GUIDE ME!", - "sfx": ["zealot-honor.ogg"], - "equiv": "Equivalent to last year's frontier level (2025).", - "spec": "Qwen3.5-9B-Uncensored (Q6_K) on an NVIDIA RTX 3090 — ~95 tokens/s; 200k-token context, two submissions at a time; sees images.", - "inputs": ["text", "image"], - "use": "Code editor autocomplete, agentic search, RAG, research subagents, translation, roleplay, and summarization. If your agent harness lets you set a compaction model, set this one: a compaction job on the bigger models blocks them for far longer.", - "voice": { "text": "I LONG FOR COMBAT!", "sfx": ["zealot-combat.ogg"] }, - "supervision": "Requires tight human supervision.", - "warn": "Uncensored model: output can include adult content. You are responsible for the legal consequences of how you use it." - }, - "url": "", - "docs": "/docs#models", - "probe": "127.0.0.1:7771", - "method": "http", - "wip": false, - "showLink": false - }, { "name": "mariadb", "category": "admin", @@ -344,7 +399,8 @@ "probe": "192.168.0.235:3306", "method": "tcp", "wip": "closed", - "showLink": false + "showLink": false, + "art": "/img/apps/mariadb.svg" }, { "name": "valkey", @@ -356,7 +412,8 @@ "probe": "127.0.0.1:6379", "method": "tcp", "wip": "closed", - "showLink": false + "showLink": false, + "art": "/img/apps/mono-vk.svg" }, { "name": "Prowlarr", @@ -368,7 +425,8 @@ "probe": "192.168.0.112:9696", "method": "http", "wip": false, - "showLink": true + "showLink": true, + "art": "/img/apps/mono-pw.svg" }, { "name": "Radarr", @@ -380,7 +438,8 @@ "probe": "192.168.0.112:7878", "method": "http", "wip": false, - "showLink": true + "showLink": true, + "art": "/img/apps/radarr.svg" }, { "name": "Readarr", @@ -392,7 +451,8 @@ "probe": "192.168.0.112:8787", "method": "http", "wip": false, - "showLink": true + "showLink": true, + "art": "/img/apps/mono-ra.svg" }, { "name": "Lidarr", @@ -404,7 +464,8 @@ "probe": "192.168.0.112:8686", "method": "http", "wip": false, - "showLink": true + "showLink": true, + "art": "/img/apps/mono-ld.svg" }, { "name": "qBittorrent", @@ -416,7 +477,8 @@ "probe": "192.168.0.112:8080", "method": "http", "wip": false, - "showLink": true + "showLink": true, + "art": "/img/apps/qbittorrent.svg" }, { "name": "Tautulli", @@ -428,7 +490,8 @@ "probe": "192.168.0.235:8181", "method": "http", "wip": false, - "showLink": true + "showLink": true, + "art": "/img/apps/mono-tt.svg" }, { "name": "Postgres", @@ -440,7 +503,8 @@ "probe": "127.0.0.1:5432", "method": "tcp", "wip": "closed", - "showLink": false + "showLink": false, + "art": "/img/apps/postgresql.svg" }, { "name": "ComfyUI", @@ -452,6 +516,7 @@ "probe": "127.0.0.1:8188", "method": "http", "wip": false, - "showLink": true + "showLink": true, + "art": "/img/apps/mono-cu.svg" } ] diff --git a/shared/homepage/style.css b/shared/homepage/style.css index d23536d..3cff933 100644 --- a/shared/homepage/style.css +++ b/shared/homepage/style.css @@ -71,7 +71,19 @@ body::before { } .panel { border: 1px solid var(--border); background: var(--panel); padding: 12px 14px; border-radius: 2px; } -.panel-head { display: flex; justify-content: space-between; align-items: center; } +.panel-head { display: flex; align-items: center; gap: 8px; } +.panel-head .name { flex: 1; } +.app-art { + width: 40px; + height: 40px; + flex: 0 0 auto; + object-fit: contain; + padding: 4px; + clip-path: polygon(25% 3%, 75% 3%, 100% 50%, 75% 97%, 25% 97%, 0% 50%); + border: 1px solid var(--dim); + background: color-mix(in srgb, var(--fg) 8%, transparent); + filter: saturate(0.85) brightness(0.95); +} .name { color: var(--head); } .badge { border: 1px solid currentColor; padding: 0 6px; border-radius: 2px; font-size: 12px; } .badge.up { color: var(--fg); } @@ -96,6 +108,19 @@ body::before { justify-content: space-between; } .foot a { color: var(--link); text-decoration: none; } +.foot-links { display: flex; gap: 10px; flex-wrap: wrap; } +.foot-btn { + border: 1px solid var(--link); + padding: 2px 14px; + border-radius: 2px; + text-transform: uppercase; + letter-spacing: 1px; + text-shadow: 0 0 6px color-mix(in srgb, var(--link) 55%, transparent); +} +.foot-btn:hover { + background: color-mix(in srgb, var(--link) 16%, transparent); + box-shadow: 0 0 10px color-mix(in srgb, var(--link) 45%, transparent); +} /* The user docs page (static prose). */ .docs { max-width: 780px; } @@ -107,6 +132,8 @@ body::before { .docs pre code { color: var(--fg); } .docs img { display: block; max-width: 100%; border: 1px solid var(--border); border-radius: 2px; margin: 10px 0; } .docs .key-warn { color: var(--down); } +.docs [title].key-warn { cursor: help; + text-decoration: underline dotted color-mix(in srgb, var(--down) 60%, transparent); } .docs .foot-note { color: var(--dim); } html[data-bare="1"] .docs h1, html[data-bare="1"] .docs h2 { color: #000; border-color: #000; } html[data-bare="1"] .docs a, html[data-bare="1"] .docs code { color: #000; } @@ -123,6 +150,7 @@ html[data-bare="1"] .badge, html[data-bare="1"] .name, html[data-bare="1"] .desc html[data-bare="1"] .desc-role, html[data-bare="1"] .desc-spec, html[data-bare="1"] .desc-inputs, html[data-bare="1"] .desc-use, html[data-bare="1"] .desc-equiv { color: #000; font-weight: normal; } html[data-bare="1"] .section-title { border-color: #000; } html[data-bare="1"] .links a, html[data-bare="1"] .toggle, html[data-bare="1"] .foot a { color: #000; } +html[data-bare="1"] .foot-btn { border-color: #000; text-shadow: none; } /* Model Information: one fused panel for the fleet models. Informational panels, so the type is smaller than the app panels; each model wears its @@ -191,6 +219,16 @@ html[data-bare="1"] .links a, html[data-bare="1"] .toggle, html[data-bare="1"] . .mi-super { color: var(--link); margin-top: 6px; } .mi-warn { color: var(--down); margin-top: 4px; cursor: help; text-decoration: underline dotted color-mix(in srgb, var(--down) 60%, transparent); } +.mi-perm { + margin-top: 8px; + padding: 6px 8px; + border: 1px solid var(--down); + color: var(--down); + font-weight: bold; + text-shadow: 0 0 6px color-mix(in srgb, var(--down) 45%, transparent); +} +.svc-warn { color: var(--down); margin-top: 6px; cursor: help; + text-decoration: underline dotted color-mix(in srgb, var(--down) 60%, transparent); } /* Bare mode strips the model component to plain prose too. */ html[data-bare="1"] .mi-card { border-color: #000; box-shadow: none; background: none; } @@ -200,3 +238,6 @@ html[data-bare="1"] .mi-voice { color: #000; text-decoration: underline; } html[data-bare="1"] .mi-role, html[data-bare="1"] .mi-spec, html[data-bare="1"] .mi-use, html[data-bare="1"] .mi-inputs, html[data-bare="1"] .mi-equiv, html[data-bare="1"] .mi-super, html[data-bare="1"] .mi-warn { color: #000; } +html[data-bare="1"] .app-art { display: none; } +html[data-bare="1"] .svc-warn, html[data-bare="1"] .mi-perm { color: #000; border: none; text-shadow: none; padding: 0; } +html[data-bare="1"] .mi-perm { border: 1px solid #000; } diff --git a/tests/e2e/specs/10-homepage.spec.ts b/tests/e2e/specs/10-homepage.spec.ts index e7c124f..63f19cb 100644 --- a/tests/e2e/specs/10-homepage.spec.ts +++ b/tests/e2e/specs/10-homepage.spec.ts @@ -47,8 +47,11 @@ test("standard user is denied the admin panel (RBAC)", async ({ page }) => { expect(resp.status()).toBe(403); expect(await resp.text()).toContain("Forbidden: homepage admins only"); - // The footer link a standard user can click also dead-ends at the gate. + // The admin button is identity-gated: /whoami reports the authentik + // groups, and the page hides the button from non-admins (the server-side + // 403 above stays the real gate). Direct navigation dead-ends all the same. await page.goto("/"); - await page.locator('footer a[href="/admin/"]').click(); + await expect(page.locator("#admin-link")).toBeHidden(); + await page.goto("/admin/"); await expect(page.locator("body")).toContainText("Forbidden: homepage admins only"); }); diff --git a/tests/e2e/specs/11-admin.spec.ts b/tests/e2e/specs/11-admin.spec.ts index 912e852..c6b757e 100644 --- a/tests/e2e/specs/11-admin.spec.ts +++ b/tests/e2e/specs/11-admin.spec.ts @@ -4,6 +4,10 @@ import { expect, test } from "@playwright/test"; // allow-leg: the same path returns 403 for the standard user (10-homepage). test("admin panel renders admin-category services", async ({ page }) => { + // Identity gate (admin side): a homepage-admins member sees the admin + // button on the user homepage (the standard user does not: 10-homepage). + await page.goto("/"); + await expect(page.locator("#admin-link")).toBeVisible({ timeout: 60_000 }); await page.goto("/admin/"); await expect(page).toHaveTitle(/frank — admin/, { timeout: 60_000 }); diff --git a/tests/homepage_config.bats b/tests/homepage_config.bats index 52a3411..9513fb7 100644 --- a/tests/homepage_config.bats +++ b/tests/homepage_config.bats @@ -101,7 +101,35 @@ for s in fleet: uncensored = "ncensored" in m["spec"] or "bliterated" in m["spec"] if uncensored: assert "legal" in m.get("warn", "").lower(), f"{s['name']} is uncensored but has no legal warning" +assert [s["name"] for s in fleet] == ["frank-zealot", "frank-dragoon", "frank-archon"], \ + "card render order must be zealot, dragoon, archon: " + str([s["name"] for s in fleet]) +w = {s["name"]: s["model"].get("warn", "") for s in fleet} +assert w["frank-zealot"] == w["frank-dragoon"] != "", "the legal warnings must be the common uncensored text" +assert "legal risk" in w["frank-zealot"].lower() and "everybody" in w["frank-zealot"].lower() +archon = next(s for s in fleet if s["name"] == "frank-archon") +assert "permission" in archon["model"], "archon card needs the ask-Frank permission callout" PY grep -q "renderModelInfo" "$REPO_ROOT/shared/homepage/app.js" grep -q ".mi-card" "$REPO_ROOT/shared/homepage/style.css" } + +@test "every app card carries icon art and SearXNG carries the privacy note" { + # The family sees unit-portrait art on every card, not just the models: + # brand marks where Simple Icons has them, improvised monograms otherwise. + # SearXNG's card must warn (hover) that obfuscation is not anonymity. + python3 - "$REPO_ROOT/shared/homepage/services.json" "$REPO_ROOT/shared/homepage" <<'PY' +import json, os, sys +services = json.load(open(sys.argv[1])) +root = sys.argv[2] +apps = [s for s in services if "model" not in s] +for s in apps: + art = s.get("art", "") + assert art.startswith("/img/apps/"), f"{s['name']} has no app art" + assert os.path.isfile(os.path.join(root, art.lstrip("/"))), f"{s['name']} art file missing: {art}" +sx = next(s for s in services if s["name"] == "SearXNG") +assert "anonymous" in sx.get("warn", "").lower(), "SearXNG must warn that searches are obfuscated, not anonymous" +PY + grep -q "app-art" "$REPO_ROOT/shared/homepage/app.js" + grep -q ".app-art" "$REPO_ROOT/shared/homepage/style.css" + grep -q "/whoami" "$REPO_ROOT/dankclaw/Caddyfile" +} diff --git a/tests/homepage_pages.bats b/tests/homepage_pages.bats index 41b7f73..6e58d39 100644 --- a/tests/homepage_pages.bats +++ b/tests/homepage_pages.bats @@ -79,7 +79,7 @@ setup() { @test "the user docs page exists and is reachable from the service pages" { [ -f "$BASE/docs.html" ] - grep -q 'docs' "$BASE/index.html" + grep -q 'class="foot-btn" href="/docs"' "$BASE/index.html" grep -q 'id="models"' "$BASE/docs.html" grep -q '' "$BASE/docs.html" grep -q 'style.css' "$BASE/docs.html"