homepage: sectioned panels, WIP open/closed, current model facts, Tautulli card

Both pages now group their panels under section headings rendered by shared
renderSection()/renderPanel() components in app.js (SECTION_ORDER fixes each
page's order: user = AI, Web Apps, Media Library Apps; admin = Media Library
Apps, Observability, AI, Infrastructure). The WIP badge splits into WIP: OPEN
and WIP: CLOSED via the wip value ("open"/"closed"); the checker passes the
value through as wip-open/wip-closed. The frank-large card follows the
unsloth llama.cpp cutover, frank-medium follows the FP8 checkpoint, and the
/docs model guide matches. Tautulli (Plex statistics, 192.168.0.235:8181)
joins the admin page's Media Library Apps section with an open link.
This commit is contained in:
Frank
2026-09-21 13:20:15 -07:00
parent 32eafeb7c5
commit 384747a72f
9 changed files with 221 additions and 54 deletions
+59 -19
View File
@@ -1,10 +1,26 @@
// The homepage front end. It reads status.json and services.json once on load
// (no auto-refresh, Q41), renders one TUI panel per service for this page's
// category, offers the styling toggle (Q27), and shows the last master
// deployment datetime (Q43). It never alters the user's cursor (Q28).
// (no auto-refresh, Q41), renders the services for this page's category as
// sectioned grids of TUI panels, offers the styling toggle (Q27), and shows
// the last master deployment datetime (Q43). It never alters the user's
// cursor (Q28).
const CATEGORY = window.location.pathname.indexOf('/admin') !== -1 ? 'admin' : 'user';
// Sections and panels are shared components: both pages render through the
// same renderSection()/renderPanel() below. SECTION_ORDER fixes each page's
// section order; a section with no services renders nothing.
const SECTION_ORDER = {
user: ['AI', 'Web Apps', 'Media Library Apps'],
admin: ['Media Library Apps', 'Observability', 'AI', 'Infrastructure'],
};
const BADGE_LABEL = {
up: 'UP',
down: 'DOWN',
'wip-open': 'WIP: OPEN',
'wip-closed': 'WIP: CLOSED',
};
function load(url) {
return fetch(url, { cache: 'no-store' }).then(function (r) { return r.json(); });
}
@@ -15,32 +31,56 @@ function esc(s) {
});
}
// Shared panel component: one TUI panel for a service in a given state.
function renderPanel(svc, state) {
const panel = document.createElement('div');
panel.className = 'panel ' + state;
const links = [];
if (svc.showLink && svc.url) links.push('<a class="open" href="' + esc(svc.url) + '">open</a>');
if (svc.docs) links.push('<a class="docs" href="' + esc(svc.docs) + '">docs</a>');
panel.innerHTML =
'<div class="panel-head">' +
' <span class="badge ' + state + '">' + BADGE_LABEL[state] + '</span>' +
' <span class="name">' + esc(svc.name) + '</span>' +
'</div>' +
'<div class="desc">' + esc(svc.description || '') + '</div>' +
'<div class="links">' + links.join('') + '</div>';
return panel;
}
// Shared section component: a heading over a grid of panels.
function renderSection(title, panels) {
const section = document.createElement('section');
section.className = 'section';
const heading = document.createElement('h2');
heading.className = 'section-title';
heading.textContent = title;
const grid = document.createElement('div');
grid.className = 'grid';
for (const panel of panels) grid.appendChild(panel);
section.appendChild(heading);
section.appendChild(grid);
return section;
}
function renderPanels(services, status) {
const stateByName = {};
for (const s of (status.services || [])) stateByName[s.name] = s.state;
const grid = document.getElementById('grid');
grid.innerHTML = '';
const root = document.getElementById('grid');
root.innerHTML = '';
let up = 0, down = 0, wip = 0;
const bySection = {};
for (const svc of services) {
if (svc.category !== CATEGORY) continue;
const state = svc.wip ? 'wip' : (stateByName[svc.name] || 'down');
const state = svc.wip ? 'wip-' + svc.wip : (stateByName[svc.name] || 'down');
if (state === 'up') up++; else if (state === 'down') down++; else wip++;
(bySection[svc.section] = bySection[svc.section] || []).push(renderPanel(svc, state));
}
const panel = document.createElement('div');
panel.className = 'panel ' + state;
const links = [];
if (svc.showLink && svc.url) links.push('<a class="open" href="' + esc(svc.url) + '">open</a>');
if (svc.docs) links.push('<a class="docs" href="' + esc(svc.docs) + '">docs</a>');
panel.innerHTML =
'<div class="panel-head">' +
' <span class="badge ' + state + '">' + state.toUpperCase() + '</span>' +
' <span class="name">' + esc(svc.name) + '</span>' +
'</div>' +
'<div class="desc">' + esc(svc.description || '') + '</div>' +
'<div class="links">' + links.join('') + '</div>';
grid.appendChild(panel);
for (const title of SECTION_ORDER[CATEGORY]) {
if (bySection[title]) root.appendChild(renderSection(title, bySection[title]));
}
document.getElementById('summary').textContent = 'UP ' + up + ' DOWN ' + down + ' WIP ' + wip;
+3 -3
View File
@@ -47,9 +47,9 @@
<h2>frank-large — the big brain</h2>
<ul>
<li><strong>Model:</strong> Qwen3.8-Flash CIRU-STRIX-Orca, served by CIRU v4.4.1 llama.cpp with the model's own MTP draft head.</li>
<li><strong>Model:</strong> Qwen3.8-Flash-Next UD-IQ4_XS, served by unsloth llama.cpp with the model's own MTP draft head (depth 2).</li>
<li><strong>Hardware:</strong> Dankclaw's Strix Halo iGPU (gfx1151), shared with the whole host.</li>
<li><strong>Best for:</strong> long, thoughtful text conversations at the model's full trained window.</li>
<li><strong>Best for:</strong> long, thoughtful text conversations at a 200k-token window.</li>
<li><strong>Capacity:</strong> exactly one conversation resident at a time. A second waits — politely, but it waits.</li>
<li><strong>Not for:</strong> images. It cannot see them.</li>
</ul>
@@ -64,7 +64,7 @@
<h2>frank-medium — the 27B generalist</h2>
<ul>
<li><strong>Model:</strong> Qwen3.8-27B-Uncensored at MXFP4, on vllm-radiance with a DFlash2 draft head.</li>
<li><strong>Model:</strong> Qwen3.8-27B-Uncensored at FP8, on vllm-radiance with a DFlash2 draft head.</li>
<li><strong>Hardware:</strong> Dankserver's two Radeon R9700s as ONE tensor-parallel engine (TP=2).</li>
<li><strong>Best for:</strong> general chat and reasoning when the big brain is busy; the biggest pure-language model we run, and the fastest we have measured.</li>
<li><strong>Capacity:</strong> 262k-token context shared by up to eight conversations at once. It can see pictures too; <code>frank-small</code> is the faster eye and holds four conversations.</li>
+51 -6
View File
@@ -2,6 +2,7 @@
{
"name": "SearXNG",
"category": "user",
"section": "Web Apps",
"description": "AI-free, VPN-routed search engine, fully hosted on Frank's server.",
"url": "http://search.franksdank.cloud",
"docs": "",
@@ -13,6 +14,7 @@
{
"name": "OpenWebUI",
"category": "user",
"section": "AI",
"description": "AI Chat/Search interface",
"url": "http://ai.franksdank.cloud",
"docs": "",
@@ -24,6 +26,7 @@
{
"name": "Gitea",
"category": "user",
"section": "Web Apps",
"description": "Git code hosting and review platform.",
"url": "http://gitea.franksdank.cloud",
"docs": "",
@@ -35,6 +38,7 @@
{
"name": "Calibre-Web",
"category": "user",
"section": "Media Library Apps",
"description": "E-book library.",
"url": "http://books.franksdank.cloud",
"docs": "",
@@ -46,6 +50,7 @@
{
"name": "RomM",
"category": "user",
"section": "Media Library Apps",
"description": "ROM and emulator library.",
"url": "http://romm.franksdank.cloud",
"docs": "",
@@ -57,6 +62,7 @@
{
"name": "Copyparty",
"category": "user",
"section": "Web Apps",
"description": "Direct downloads from Frank's server.",
"url": "http://files.franksdank.cloud",
"docs": "",
@@ -68,6 +74,7 @@
{
"name": "Plex",
"category": "user",
"section": "Media Library Apps",
"description": "Closed source media server with apps for everything.",
"url": "http://plex.franksdank.cloud/web/index.html",
"docs": "",
@@ -79,6 +86,7 @@
{
"name": "Jellyfin",
"category": "user",
"section": "Media Library Apps",
"description": "Open source media server.",
"url": "http://jelly.franksdank.cloud",
"docs": "",
@@ -90,6 +98,7 @@
{
"name": "Seerr",
"category": "user",
"section": "Media Library Apps",
"description": "Download Requests",
"url": "http://seerr.franksdank.cloud",
"docs": "",
@@ -101,6 +110,7 @@
{
"name": "Marreta",
"category": "user",
"section": "Web Apps",
"description": "Clean, ad-free article reader. Paste any URL, get just the article.",
"url": "http://marreta.franksdank.cloud",
"docs": "",
@@ -112,6 +122,7 @@
{
"name": "Sonarr",
"category": "admin",
"section": "Media Library Apps",
"description": "TV series library and automation.",
"url": "http://192.168.0.112:8989",
"docs": "",
@@ -123,6 +134,7 @@
{
"name": "Hermes",
"category": "admin",
"section": "AI",
"description": "The agent gateway.",
"url": "",
"docs": "",
@@ -134,6 +146,7 @@
{
"name": "Grafana",
"category": "admin",
"section": "Observability",
"description": "Dashboards.",
"url": "http://grafana.franksdank.cloud",
"docs": "",
@@ -145,6 +158,7 @@
{
"name": "Prometheus",
"category": "admin",
"section": "Observability",
"description": "Metrics collection.",
"url": "http://prometheus.franksdank.cloud",
"docs": "",
@@ -156,6 +170,7 @@
{
"name": "Loki",
"category": "admin",
"section": "Observability",
"description": "Log storage.",
"url": "http://loki.franksdank.cloud",
"docs": "",
@@ -167,6 +182,7 @@
{
"name": "node-exporter",
"category": "admin",
"section": "Observability",
"description": "Host metrics.",
"url": "",
"docs": "",
@@ -178,6 +194,7 @@
{
"name": "alloy",
"category": "admin",
"section": "Observability",
"description": "Log shipping.",
"url": "",
"docs": "",
@@ -189,6 +206,7 @@
{
"name": "alertmanager",
"category": "admin",
"section": "Observability",
"description": "Alert delivery to Signal.",
"url": "",
"docs": "",
@@ -200,6 +218,7 @@
{
"name": "Caddy",
"category": "admin",
"section": "Infrastructure",
"description": "The reverse proxy.",
"url": "",
"docs": "",
@@ -211,6 +230,7 @@
{
"name": "dnsmasq",
"category": "admin",
"section": "Infrastructure",
"description": "LAN name resolution.",
"url": "",
"docs": "",
@@ -222,18 +242,20 @@
{
"name": "Gluetun",
"category": "admin",
"section": "Infrastructure",
"description": "The outbound VPN tunnel.",
"url": "",
"docs": "",
"probe": "127.0.0.1:51820",
"method": "tcp",
"wip": true,
"wip": "closed",
"showLink": false
},
{
"name": "frank-large",
"category": "user",
"description": "The big brain. Qwen3.8-Flash CIRU-STRIX-Orca on CIRU v4.4.1 llama.cpp, running on Dankclaw's Strix Halo iGPU. Text-only; one conversation resident at a time.",
"section": "AI",
"description": "The big brain. Qwen3.8-Flash-Next UD-IQ4_XS on unsloth llama.cpp, running on Dankclaw's Strix Halo iGPU. Text-only; one conversation resident at a time.",
"url": "",
"docs": "/docs#models",
"probe": "127.0.0.1:7770",
@@ -244,7 +266,8 @@
{
"name": "frank-medium",
"category": "user",
"description": "The 27B generalist. Qwen3.8-27B-Uncensored at MXFP4 on vllm-radiance, one tensor-parallel process across Dankserver's two R9700s with a DFlash2 drafter; 262k-token context shared by up to eight conversations.",
"section": "AI",
"description": "The 27B generalist. Qwen3.8-27B-Uncensored at FP8 on vllm-radiance, one tensor-parallel process across Dankserver's two R9700s with a DFlash2 drafter; 262k-token context shared by up to eight conversations.",
"url": "",
"docs": "/docs#models",
"probe": "192.168.0.235:7770",
@@ -255,6 +278,7 @@
{
"name": "frank-small",
"category": "user",
"section": "AI",
"description": "The vision path. HauhauCS Gemma4-12B QAT (Balanced, Q4_K_M) with MTP + ngram spec-decode on Dankclaw's RTX 3090. Sees images; 1M-token pool across four conversations.",
"url": "",
"docs": "/docs#models",
@@ -266,28 +290,31 @@
{
"name": "mariadb",
"category": "admin",
"section": "Infrastructure",
"description": "RomM's database (Dankserver). Not probeable from the monitor: 3306 is container-internal, and publishing a database port on the LAN is not a trade this fleet makes. RomM's own card covers the stack.",
"url": "",
"docs": "",
"probe": "192.168.0.235:3306",
"method": "tcp",
"wip": true,
"wip": "closed",
"showLink": false
},
{
"name": "valkey",
"category": "admin",
"section": "Infrastructure",
"description": "SearXNG's cache/limiter (Dankclaw). Not probeable: 6379 is container-internal. SearXNG's own card covers the stack.",
"url": "",
"docs": "",
"probe": "127.0.0.1:6379",
"method": "tcp",
"wip": true,
"wip": "closed",
"showLink": false
},
{
"name": "Prowlarr",
"category": "admin",
"section": "Media Library Apps",
"description": "Indexer manager.",
"url": "http://prowlarr.franksdank.cloud",
"docs": "",
@@ -299,6 +326,7 @@
{
"name": "Radarr",
"category": "admin",
"section": "Media Library Apps",
"description": "Movie library and automation.",
"url": "http://radarr.franksdank.cloud",
"docs": "",
@@ -310,6 +338,7 @@
{
"name": "Readarr",
"category": "admin",
"section": "Media Library Apps",
"description": "Book library and automation.",
"url": "http://readarr.franksdank.cloud",
"docs": "",
@@ -321,6 +350,7 @@
{
"name": "Lidarr",
"category": "admin",
"section": "Media Library Apps",
"description": "Music library and automation.",
"url": "http://lidarr.franksdank.cloud",
"docs": "",
@@ -332,6 +362,7 @@
{
"name": "qBittorrent",
"category": "admin",
"section": "Media Library Apps",
"description": "Torrent client.",
"url": "http://qbittorrent.franksdank.cloud",
"docs": "",
@@ -340,20 +371,34 @@
"wip": false,
"showLink": true
},
{
"name": "Tautulli",
"category": "admin",
"section": "Media Library Apps",
"description": "Plex statistics and notifications.",
"url": "http://192.168.0.235:8181",
"docs": "",
"probe": "192.168.0.235:8181",
"method": "http",
"wip": false,
"showLink": true
},
{
"name": "Postgres",
"category": "admin",
"section": "Infrastructure",
"description": "Authentik's and Gitea's databases (Dankclaw). Not probeable: 5432 is container-internal. Their apps' cards cover the stacks.",
"url": "",
"docs": "",
"probe": "127.0.0.1:5432",
"method": "tcp",
"wip": true,
"wip": "closed",
"showLink": false
},
{
"name": "ComfyUI",
"category": "admin",
"section": "AI",
"description": "Node-based image generation.",
"url": "",
"docs": "",
+16 -4
View File
@@ -47,10 +47,20 @@ body::before {
padding-bottom: 8px;
}
.title { color: var(--head); }
.toggle { color: var(--link); text-decoration: none; border: 1px solid var(--border); padding: 0 8px; }
.summary { color: var(--dim); margin: 12px 0; letter-spacing: 0.05em; }
.section { margin-bottom: 20px; }
.section-title {
color: var(--head);
font-size: 1rem;
font-weight: normal;
letter-spacing: 0.08em;
text-transform: uppercase;
border-bottom: 1px solid var(--border);
margin: 0 0 12px;
padding-bottom: 4px;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
@@ -63,7 +73,8 @@ body::before {
.badge { border: 1px solid currentColor; padding: 0 6px; border-radius: 2px; font-size: 12px; }
.badge.up { color: var(--fg); }
.badge.down { color: var(--down); }
.badge.wip { color: var(--wip); }
.badge.wip-open { color: var(--wip); }
.badge.wip-closed { color: var(--dim); }
.desc { color: var(--fg); margin: 8px 0; }
.links a { color: var(--link); text-decoration: underline; margin-right: 12px; }
@@ -91,5 +102,6 @@ html[data-bare="1"] .docs a, html[data-bare="1"] .docs code { color: #000; }
html[data-bare="1"] body { background: #fff; color: #000; font-family: serif; text-shadow: none; font-size: 16px; }
html[data-bare="1"] body::before { display: none; }
html[data-bare="1"] .panel, html[data-bare="1"] .top, html[data-bare="1"] .foot { border-color: #000; background: none; }
html[data-bare="1"] .badge, html[data-bare="1"] .name, html[data-bare="1"] .desc, html[data-bare="1"] .summary { color: #000; }
html[data-bare="1"] .badge, html[data-bare="1"] .name, html[data-bare="1"] .desc, html[data-bare="1"] .summary, html[data-bare="1"] .section-title { color: #000; }
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; }