homepage: Protoss rename + fused Model Information component
The model cards become the StarCraft trio: frank-large -> frank-dragoon, frank-primary -> frank-archon, frank-secondary -> frank-zealot (family names only; endpoint/API ids unchanged). The three cards fuse into one Model Information component: per-unit Protoss accent colours and hex- clipped unit art, 15px type, clickable StarCraft unit-voice quotations (original .ogg files from the fandom wiki, served from /sfx/), supervision notes (dragoon/archon safe for autonomous use, zealot tight supervision), compaction-model recommendation for frank-zealot, and uncensored-model legal notes rendered as hover tooltips instead of wall text. docs.html carries the same renames plus api-id mapping. App-card updates: Seerr, Calibre-Web, RomM, Jellyfin set to WIP: DOWN with Open links removed (new wip "down" badge state); Copyparty notes it serves all other media apps content including WIP apps; Gitea and Plex noted as needing logins separate from SSO; SearXNG noted as obfuscated, not anonymous (upstream API keys tie to Frank). Plumbing: Caddy @data allowlist grew /sfx/*; deploy_homepage copies sfx/; homepage_config.bats pins the new probe names, the component fields, art/voice-file presence, and the wip-down schema.
This commit is contained in:
+1
-1
@@ -337,7 +337,7 @@ http://:8017 {
|
||||
try_files /docs.html
|
||||
file_server
|
||||
}
|
||||
@data path /services.json /status.json /app.js /style.css /deployed.js /img/*
|
||||
@data path /services.json /status.json /app.js /style.css /deployed.js /img/* /sfx/*
|
||||
handle @data {
|
||||
file_server
|
||||
}
|
||||
|
||||
@@ -325,6 +325,9 @@ deploy_homepage() {
|
||||
if [[ -d shared/homepage/img ]]; then
|
||||
run_privileged cp -r shared/homepage/img /srv/homepage/
|
||||
fi
|
||||
if [[ -d shared/homepage/sfx ]]; then
|
||||
run_privileged cp -r shared/homepage/sfx /srv/homepage/
|
||||
fi
|
||||
local deployed_at
|
||||
deployed_at="$(git log -1 --format=%cI)"
|
||||
printf 'window.DEPLOYED_AT = "%s";\n' "$deployed_at" > /tmp/deployed.js
|
||||
|
||||
@@ -62,6 +62,31 @@ intelligence)", frank-primary "equivalent to Claude Opus 4.8 / OpenAI-Terra
|
||||
(2025)". Rendered by `descHTML()` as `.desc-equiv` (head color, italic);
|
||||
pinned in `homepage_config.bats`.
|
||||
|
||||
**Protoss rename + Model Information component (2026-10-08):** the model
|
||||
cards became the StarCraft trio — `frank-large` → **frank-dragoon**
|
||||
(iGPU), `frank-primary` → **frank-archon** (Dankserver), `frank-secondary`
|
||||
→ **frank-zealot** (3090). The endpoint/API ids did NOT change; only the
|
||||
family-facing names. The three cards fused into one **Model Information**
|
||||
component (`app.js` `renderModelInfo()`, `.model-info`/`.mi-*` styles):
|
||||
Protoss unit art (`/img/unit-<unit>.jpg`, hex-clipped), per-unit accent
|
||||
colours (Dragoon gold, Archon cyan, Zealot blue), smaller type (15px vs
|
||||
19px body), and clickable unit-voice lines that play the original
|
||||
StarCraft `.ogg` quotations from `/sfx/` (fetched from
|
||||
starcraft.fandom.com via its MediaWiki API; the quote→file mapping lives
|
||||
in the `model.sfx`/`model.voice.sfx` fields). New `services.json`
|
||||
per-model fields: `unit`, `tagline`, `sfx[]`, `voice`, `supervision`
|
||||
(dragoon/archon autonomous-safe, zealot tight-supervision), and `warn`
|
||||
(uncensored legal note, rendered as a hover tooltip, not wall text).
|
||||
A compaction-model recommendation for frank-zealot rides in its `use`
|
||||
text. The Caddy `@data` allowlist grew `/sfx/*`; `deploy_homepage` copies
|
||||
`sfx/`. A `wip: "down"` badge state ("WIP: DOWN", red) was added; Seerr,
|
||||
Calibre-Web, RomM, and Jellyfin are WIP: DOWN with their Open links
|
||||
removed. Card notes added: Copyparty holds every other media app's
|
||||
content (including the WIP ones); Gitea and Plex need logins separate
|
||||
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.
|
||||
|
||||
## 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.
|
||||
|
||||
+83
-5
@@ -19,6 +19,7 @@ const BADGE_LABEL = {
|
||||
down: 'DOWN',
|
||||
'wip-open': 'WIP: OPEN',
|
||||
'wip-closed': 'WIP: CLOSED',
|
||||
'wip-down': 'WIP: DOWN',
|
||||
};
|
||||
|
||||
function load(url) {
|
||||
@@ -31,14 +32,13 @@ function esc(s) {
|
||||
});
|
||||
}
|
||||
|
||||
// The description: structured role/spec/use lines when the service defines
|
||||
// them (the LLM cards), else the plain description string.
|
||||
// The plain description for ordinary panels. (Model cards render through
|
||||
// renderModelInfo below, not through descHTML.)
|
||||
function descHTML(svc) {
|
||||
const d = svc.details;
|
||||
if (d && (d.role || d.spec || d.inputs || d.use || d.equiv)) {
|
||||
if (d && (d.role || d.spec || d.inputs || d.use)) {
|
||||
const lines = [];
|
||||
if (d.role) lines.push('<div class="desc-role">' + esc(d.role) + '</div>');
|
||||
if (d.equiv) lines.push('<div class="desc-equiv">' + esc(d.equiv) + '</div>');
|
||||
if (d.spec) lines.push('<div class="desc-spec">' + esc(d.spec) + '</div>');
|
||||
if (d.inputs && d.inputs.length) lines.push('<div class="desc-inputs">Inputs: ' + esc(d.inputs.join(' · ')) + '</div>');
|
||||
if (d.use) lines.push('<div class="desc-use">Use: ' + esc(d.use) + '</div>');
|
||||
@@ -47,6 +47,77 @@ function descHTML(svc) {
|
||||
return esc(svc.description || '');
|
||||
}
|
||||
|
||||
// The Model Information component: one panel fusing every fleet model card.
|
||||
// Each model is an informational sub-card (smaller type, Protoss unit art,
|
||||
// unit-coloured flair); the quoted lines are buttons that play the matching
|
||||
// StarCraft unit voice from /sfx/. Audio files are sequential: a click plays
|
||||
// the whole queue (the Dragoon tagline is two lines spliced).
|
||||
let sfxQueue = [];
|
||||
let sfxPlaying = false;
|
||||
|
||||
function playSfx(files, el) {
|
||||
sfxQueue = sfxQueue.concat(files);
|
||||
if (sfxPlaying) return;
|
||||
sfxPlaying = true;
|
||||
el.classList.add('mi-speaking');
|
||||
const next = function () {
|
||||
const f = sfxQueue.shift();
|
||||
if (!f) { sfxPlaying = false; el.classList.remove('mi-speaking'); return; }
|
||||
const a = new Audio('/sfx/' + f);
|
||||
a.volume = 0.5;
|
||||
a.onended = next;
|
||||
a.onerror = next;
|
||||
a.play().then(function () {}, next);
|
||||
};
|
||||
next();
|
||||
}
|
||||
|
||||
function voiceBtn(text, files) {
|
||||
return '<button type="button" class="mi-voice" title="play voice" ' +
|
||||
'data-sfx="' + esc(files.join(',')) + '">▶ ' + esc(text) + '</button>';
|
||||
}
|
||||
|
||||
function miCard(svc, state) {
|
||||
const m = svc.model;
|
||||
const parts = ['<div class="mi-top">' +
|
||||
'<img class="mi-art" src="/img/unit-' + esc(m.unit) + '.jpg" alt="' + esc(m.unit) + '">' +
|
||||
'<div class="mi-head"><div class="mi-id"><span class="badge ' + state + '">' + BADGE_LABEL[state] + '</span>' +
|
||||
'<span class="mi-name">' + esc(svc.name) + '</span></div>' +
|
||||
'<div class="mi-role">' + esc(m.role) + '</div></div></div>',
|
||||
voiceBtn(m.tagline, m.sfx)
|
||||
];
|
||||
if (m.equiv) parts.push('<div class="mi-equiv">' + esc(m.equiv) + '</div>');
|
||||
if (m.spec) parts.push('<div class="mi-spec">' + esc(m.spec) + '</div>');
|
||||
if (m.inputs && m.inputs.length) parts.push('<div class="mi-inputs">Inputs: ' + esc(m.inputs.join(' · ')) + '</div>');
|
||||
if (m.use) parts.push('<div class="mi-use">Use: ' + esc(m.use) + '</div>');
|
||||
if (m.voice) parts.push(voiceBtn(m.voice.text, m.voice.sfx));
|
||||
if (m.supervision) parts.push('<div class="mi-super">' + esc(m.supervision) + '</div>');
|
||||
if (m.warn) parts.push('<div class="mi-warn" title="' + esc(m.warn) + '">' +
|
||||
'⚠ ' + esc(m.warn.split(':')[0]) + ' \u2014 hover to read</div>');
|
||||
const card = document.createElement('article');
|
||||
card.className = 'mi-card mi-' + esc(m.unit);
|
||||
card.innerHTML = parts.join('');
|
||||
return card;
|
||||
}
|
||||
|
||||
function renderModelInfo(models) {
|
||||
const panel = document.createElement('div');
|
||||
panel.className = 'panel model-info';
|
||||
const head = document.createElement('div');
|
||||
head.className = 'mi-title';
|
||||
head.textContent = 'Model Information';
|
||||
panel.appendChild(head);
|
||||
const grid = document.createElement('div');
|
||||
grid.className = 'mi-grid';
|
||||
for (const m of models) grid.appendChild(m.el);
|
||||
panel.appendChild(grid);
|
||||
panel.addEventListener('click', function (ev) {
|
||||
const btn = ev.target.closest('.mi-voice');
|
||||
if (btn) playSfx(btn.getAttribute('data-sfx').split(','), btn);
|
||||
});
|
||||
return panel;
|
||||
}
|
||||
|
||||
// Shared panel component: one TUI panel for a service in a given state.
|
||||
function renderPanel(svc, state) {
|
||||
const panel = document.createElement('div');
|
||||
@@ -88,15 +159,22 @@ function renderPanels(services, status) {
|
||||
let up = 0, down = 0, wip = 0;
|
||||
|
||||
const bySection = {};
|
||||
const modelsBySection = {};
|
||||
for (const svc of services) {
|
||||
if (svc.category !== CATEGORY) continue;
|
||||
const state = svc.wip ? 'wip-' + svc.wip : (stateByName[svc.name] || 'down');
|
||||
if (state === 'up') up++; else if (state === 'down') down++; else wip++;
|
||||
if (svc.model) {
|
||||
(modelsBySection[svc.section] = modelsBySection[svc.section] || []).push({ svc: svc, el: miCard(svc, state) });
|
||||
continue;
|
||||
}
|
||||
(bySection[svc.section] = bySection[svc.section] || []).push(renderPanel(svc, state));
|
||||
}
|
||||
|
||||
for (const title of SECTION_ORDER[CATEGORY]) {
|
||||
if (bySection[title]) root.appendChild(renderSection(title, bySection[title]));
|
||||
const panels = bySection[title] || [];
|
||||
if (modelsBySection[title]) panels.push(renderModelInfo(modelsBySection[title]));
|
||||
if (panels.length) root.appendChild(renderSection(title, panels));
|
||||
}
|
||||
|
||||
document.getElementById('summary').textContent = 'UP ' + up + ' DOWN ' + down + ' WIP ' + wip;
|
||||
|
||||
+32
-22
@@ -40,50 +40,60 @@
|
||||
conversations are saved to your account, so history follows you between devices.</p>
|
||||
<img src="img/openwebui-chat.png" alt="Open WebUI chat screen with a question and the AI's answer">
|
||||
<p>Below the text box, the model box on the right chooses which model answers you.
|
||||
Family accounts get <code>frank-secondary</code> — the fleet's fast all-rounder,
|
||||
Family accounts get <code>frank-zealot</code> — the fleet's fast all-rounder
|
||||
(Open WebUI lists models by their API id; this one is <code>frank-secondary</code>),
|
||||
which also sees images: drag a photo or screenshot straight into the chat box.
|
||||
Frank's account can additionally reach the bigger models.</p>
|
||||
<img src="img/openwebui-model-picker.png" alt="The Open WebUI welcome screen with frank-secondary selected in the model box">
|
||||
<img src="img/openwebui-model-picker.png" alt="The Open WebUI welcome screen with the family model selected in the model box">
|
||||
<p>Open WebUI keeps your chat history; the raw model endpoints (used by developer
|
||||
tools, see <a href="#api-access">API access</a>) do not.</p>
|
||||
</section>
|
||||
|
||||
<section id="models">
|
||||
<h1>The running models</h1>
|
||||
<p>Three models serve the family. The homepage status cards show whether
|
||||
each one is up right now.</p>
|
||||
<p>Three models serve the family, named for the Protoss units of StarCraft.
|
||||
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.</p>
|
||||
|
||||
<h2>frank-large — deep reasoning</h2>
|
||||
<h2>frank-dragoon — deep reasoning <em>(api id: frank-large)</em></h2>
|
||||
<ul>
|
||||
<li><strong>Model:</strong> Qwen3.8-Flash-Next v2 Swift1.5 abliterated (halogen flash-server, ~62 GiB) — an uncensored edition that thinks before answering.</li>
|
||||
<li><strong>Model:</strong> Qwen3.8-Flash-Next v2 Swift1.5 abliterated (halogen flash-server, ~62 GiB) on the Strix Halo iGPU — an abliterated (censorship-removed) edition that thinks before answering.</li>
|
||||
<li><strong>Equivalent:</strong> Claude Sonnet 5 / OpenAI-Luna (Summer 2026 frontier intelligence).</li>
|
||||
<li><strong>Capacity:</strong> 200k-token context, two conversations at a time; sees images.</li>
|
||||
<li><strong>Capacity:</strong> 200k-token context, two submissions at a time; sees images.</li>
|
||||
<li><strong>Inputs:</strong> text, image.</li>
|
||||
<li><strong>Best for:</strong> cybersecurity and debugging. <em>Use requires Frank's permission.</em></li>
|
||||
<li><strong>Best for:</strong> complex agentic tasks, programming, system administration, and "claw"-style personal agents.</li>
|
||||
<li><strong>Supervision:</strong> safe for autonomous, low-supervision use.</li>
|
||||
<li><strong>Warning:</strong> <span class="key-warn" title="Abliterated output can include adult themes. You are responsible for the legal consequences of how you use it.">⚠ legal note — hover to read</span></li>
|
||||
</ul>
|
||||
|
||||
<h2>frank-primary — the primary model</h2>
|
||||
<h2>frank-archon — the big one <em>(api id: frank-primary)</em></h2>
|
||||
<ul>
|
||||
<li><strong>Model:</strong> Qwen3.8-Flash-Next at native FP8 precision (radiance, 173 GiB across both R9700s) — the biggest brain on the network.</li>
|
||||
<li><strong>Model:</strong> Qwen3.8-Flash-Next at native FP8 precision (radiance, 173 GiB) on twin AMD R9700s with more than 200 GB of system RAM — the biggest brain on the network and the fastest runtime.</li>
|
||||
<li><strong>Equivalent:</strong> Claude Opus 4.8 / OpenAI-Terra (Summer 2026 frontier intelligence).</li>
|
||||
<li><strong>Capacity:</strong> 200k-token context, two conversations at a time; sees images.</li>
|
||||
<li><strong>Capacity:</strong> 200k-token context, two submissions at a time; sees images.</li>
|
||||
<li><strong>Inputs:</strong> text, image.</li>
|
||||
<li><strong>Best for:</strong> software engineering, system administration, and instruction tasks.</li>
|
||||
<li><strong>Best for:</strong> software engineering, system administration, and instruction tasks. <em>Please ask Frank before use — he uses this one primarily.</em></li>
|
||||
<li><strong>Supervision:</strong> safe for autonomous, low-supervision use.</li>
|
||||
</ul>
|
||||
|
||||
<h2>frank-secondary — the fast utility model</h2>
|
||||
<h2>frank-zealot — the fast utility model <em>(api id: frank-secondary)</em></h2>
|
||||
<ul>
|
||||
<li><strong>Model:</strong> Qwen3.5-9B-Uncensored (Q6_K, 6.9 GiB) on the RTX 3090 — roughly 95 tokens per second.</li>
|
||||
<li><strong>Model:</strong> Qwen3.5-9B-Uncensored (Q6_K) on the RTX 3090 — roughly 95 tokens per second.</li>
|
||||
<li><strong>Equivalent:</strong> last year's frontier level (2025).</li>
|
||||
<li><strong>Capacity:</strong> two simultaneous conversations, 200k tokens each; sees images.</li>
|
||||
<li><strong>Capacity:</strong> 200k-token context, two submissions at a time; sees images.</li>
|
||||
<li><strong>Inputs:</strong> text, image.</li>
|
||||
<li><strong>Best for:</strong> agentic search, RAG, research subagents, translation, roleplay, and summarization — any small task that doesn't need much intelligence, and anything with a picture (screenshots, photos, diagrams). This is the model Open WebUI offers to family accounts.</li>
|
||||
<li><strong>Best for:</strong> code editor autocomplete, agentic search, RAG, research subagents, translation, roleplay, and summarization — any small task that doesn't need much intelligence, and anything with a picture. This is the model Open WebUI offers to family accounts.</li>
|
||||
<li><strong>Compaction:</strong> if your agent harness supports setting a compaction model, set this one. A compaction job severely slows the bigger models for far longer than it slows this one.</li>
|
||||
<li><strong>Supervision:</strong> requires tight human supervision.</li>
|
||||
<li><strong>Warning:</strong> <span class="key-warn" title="Uncensored model — output can include adult content. You are responsible for the legal consequences of how you use it.">⚠ legal note — hover to read</span></li>
|
||||
</ul>
|
||||
|
||||
<p class="foot-note">Which one to pick? Small utility work, RAG, search, and
|
||||
screenshots: <code>frank-secondary</code>. Software engineering, sysadmin, and
|
||||
instruction tasks: <code>frank-primary</code>. Cybersecurity and debugging:
|
||||
<code>frank-large</code> (with Frank's permission).</p>
|
||||
<p class="foot-note">Which one to pick? Small utility work, autocomplete, RAG,
|
||||
search, and screenshots: <code>frank-zealot</code>. Complex agentic work,
|
||||
programming, and personal agents: <code>frank-dragoon</code>. Software
|
||||
engineering, sysadmin, and instruction tasks: <code>frank-archon</code> (ask
|
||||
Frank first). The names in brackets are the ids the API and Open WebUI
|
||||
use; the family names are the Protoss ones.</p>
|
||||
</section>
|
||||
|
||||
<section id="api-access">
|
||||
@@ -110,7 +120,7 @@
|
||||
<h2>Step 2 — the three details every tool asks for</h2>
|
||||
<ul>
|
||||
<li><strong>Base URL:</strong> <code>https://ai.franksdank.cloud/api/v1</code></li>
|
||||
<li><strong>Model:</strong> <code>frank-secondary</code></li>
|
||||
<li><strong>Model:</strong> <code>frank-secondary</code> — this is <code>frank-zealot</code>, the family utility model (the API uses the id, not the family name)</li>
|
||||
<li><strong>API key:</strong> the <code>sk-…</code> from step 1 — sent as the header <code>Authorization: Bearer sk-…</code></li>
|
||||
</ul>
|
||||
<p>Proof that it works, from any terminal (replace the key):</p>
|
||||
@@ -159,7 +169,7 @@
|
||||
|
||||
<p class="foot-note">The key works only for the family AI — it is not your
|
||||
Authentik password, and losing it costs nothing beyond asking Frank to rotate it.
|
||||
Want a bigger model (frank-primary, frank-large)? Ask Frank to raise your access.</p>
|
||||
Want a bigger model (<code>frank-archon</code>, <code>frank-dragoon</code>)? Ask Frank to raise your access.</p>
|
||||
</section>
|
||||
|
||||
<section id="media">
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 93 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 163 KiB |
@@ -3,7 +3,7 @@
|
||||
"name": "SearXNG",
|
||||
"category": "user",
|
||||
"section": "Web Apps",
|
||||
"description": "AI-free, VPN-routed search engine, fully hosted on Frank's server.",
|
||||
"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.",
|
||||
"url": "http://search.franksdank.cloud",
|
||||
"docs": "",
|
||||
"probe": "127.0.0.1:8081",
|
||||
@@ -27,7 +27,7 @@
|
||||
"name": "Gitea",
|
||||
"category": "user",
|
||||
"section": "Web Apps",
|
||||
"description": "Git code hosting and review platform.",
|
||||
"description": "Git code hosting and review platform. Needs its own login, separate from the dankcloud single sign-on (ask Frank).",
|
||||
"url": "http://gitea.franksdank.cloud",
|
||||
"docs": "",
|
||||
"probe": "127.0.0.1:3000",
|
||||
@@ -39,31 +39,31 @@
|
||||
"name": "Calibre-Web",
|
||||
"category": "user",
|
||||
"section": "Media Library Apps",
|
||||
"description": "E-book library.",
|
||||
"url": "http://books.franksdank.cloud",
|
||||
"description": "E-book library. Work in progress: not open to family users yet.",
|
||||
"url": "",
|
||||
"docs": "",
|
||||
"probe": "192.168.0.235:8083",
|
||||
"method": "http",
|
||||
"wip": false,
|
||||
"showLink": true
|
||||
"wip": "down",
|
||||
"showLink": false
|
||||
},
|
||||
{
|
||||
"name": "RomM",
|
||||
"category": "user",
|
||||
"section": "Media Library Apps",
|
||||
"description": "ROM and emulator library.",
|
||||
"url": "http://romm.franksdank.cloud",
|
||||
"description": "ROM and emulator library. Work in progress: not open to family users yet.",
|
||||
"url": "",
|
||||
"docs": "",
|
||||
"probe": "192.168.0.235:8085",
|
||||
"method": "http",
|
||||
"wip": false,
|
||||
"showLink": true
|
||||
"wip": "down",
|
||||
"showLink": false
|
||||
},
|
||||
{
|
||||
"name": "Copyparty",
|
||||
"category": "user",
|
||||
"section": "Web Apps",
|
||||
"description": "Direct downloads from Frank's server.",
|
||||
"description": "Direct downloads from Frank's server. Everything the other media apps hold — including the work-in-progress ones (Calibre-Web, RomM, Seerr) — is available here.",
|
||||
"url": "http://files.franksdank.cloud",
|
||||
"docs": "",
|
||||
"probe": "192.168.0.235:3923",
|
||||
@@ -75,7 +75,7 @@
|
||||
"name": "Plex",
|
||||
"category": "user",
|
||||
"section": "Media Library Apps",
|
||||
"description": "Closed source media server with apps for everything.",
|
||||
"description": "Closed source media server with apps for everything. Needs its own Plex login, separate from the dankcloud single sign-on (ask Frank).",
|
||||
"url": "http://plex.franksdank.cloud/web/index.html",
|
||||
"docs": "",
|
||||
"probe": "192.168.0.235:32400",
|
||||
@@ -87,25 +87,25 @@
|
||||
"name": "Jellyfin",
|
||||
"category": "user",
|
||||
"section": "Media Library Apps",
|
||||
"description": "Open source media server.",
|
||||
"url": "http://jelly.franksdank.cloud",
|
||||
"description": "Open source media server. Work in progress: not open to family users yet.",
|
||||
"url": "",
|
||||
"docs": "",
|
||||
"probe": "192.168.0.235:8096",
|
||||
"method": "http",
|
||||
"wip": false,
|
||||
"showLink": true
|
||||
"wip": "down",
|
||||
"showLink": false
|
||||
},
|
||||
{
|
||||
"name": "Seerr",
|
||||
"category": "user",
|
||||
"section": "Media Library Apps",
|
||||
"description": "Download Requests",
|
||||
"url": "http://seerr.franksdank.cloud",
|
||||
"description": "Download requests. Work in progress: not open to family users yet.",
|
||||
"url": "",
|
||||
"docs": "",
|
||||
"probe": "127.0.0.1:5055",
|
||||
"method": "http",
|
||||
"wip": false,
|
||||
"showLink": true
|
||||
"wip": "down",
|
||||
"showLink": false
|
||||
},
|
||||
{
|
||||
"name": "Marreta",
|
||||
@@ -264,15 +264,21 @@
|
||||
"showLink": false
|
||||
},
|
||||
{
|
||||
"name": "frank-large",
|
||||
"name": "frank-dragoon",
|
||||
"category": "user",
|
||||
"section": "AI",
|
||||
"details": {
|
||||
"role": "Deep reasoning model",
|
||||
"model": {
|
||||
"unit": "dragoon",
|
||||
"role": "Frank's deep reasoning model",
|
||||
"tagline": "I have returned — make use of me.",
|
||||
"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) — 200k-token context, two conversations at a time; sees images. Speculative decoding always on.",
|
||||
"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"],
|
||||
"use": "Cybersecurity and debugging. Requires Frank's permission."
|
||||
"use": "Complex agentic tasks, programming, system administration, and \"claw\"-style personal agents.",
|
||||
"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."
|
||||
},
|
||||
"url": "",
|
||||
"docs": "/docs#models",
|
||||
@@ -282,15 +288,20 @@
|
||||
"showLink": false
|
||||
},
|
||||
{
|
||||
"name": "frank-primary",
|
||||
"name": "frank-archon",
|
||||
"category": "user",
|
||||
"section": "AI",
|
||||
"details": {
|
||||
"role": "Primary model",
|
||||
"model": {
|
||||
"unit": "archon",
|
||||
"role": "Frank's strongest and fastest model runtime",
|
||||
"tagline": "...Power Overwhelming...",
|
||||
"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) — 200k-token context, two conversations; sees images. Runs on both R9700s with tiered expert memory.",
|
||||
"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."
|
||||
"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."
|
||||
},
|
||||
"url": "",
|
||||
"docs": "/docs#models",
|
||||
@@ -300,15 +311,21 @@
|
||||
"showLink": false
|
||||
},
|
||||
{
|
||||
"name": "frank-secondary",
|
||||
"name": "frank-zealot",
|
||||
"category": "user",
|
||||
"section": "AI",
|
||||
"details": {
|
||||
"role": "Fast utility model",
|
||||
"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, 6.9 GiB) on llama.cpp CUDA — two 200k-token conversations at a time; sees images; ~95 tok/s decode on the RTX 3090.",
|
||||
"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": "Agentic search, RAG, research subagents, translation, roleplay, and summarization — any small task that doesn't need much intelligence."
|
||||
"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",
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -78,6 +78,7 @@ body::before {
|
||||
.badge.down { color: var(--down); }
|
||||
.badge.wip-open { color: var(--wip); }
|
||||
.badge.wip-closed { color: var(--dim); }
|
||||
.badge.wip-down { color: var(--down); }
|
||||
.desc { color: var(--fg); margin: 8px 0; }
|
||||
.desc-role { color: var(--dim); font-weight: bold; }
|
||||
.desc-spec { color: var(--fg); }
|
||||
@@ -122,3 +123,80 @@ 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; }
|
||||
|
||||
/* Model Information: one fused panel for the fleet models. Informational
|
||||
panels, so the type is smaller than the app panels; each model wears its
|
||||
StarCraft Protoss unit's colours and art. */
|
||||
.model-info { grid-column: 1 / -1; }
|
||||
.mi-title {
|
||||
color: var(--head);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.12em;
|
||||
border-bottom: 1px solid var(--border);
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
.mi-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
.mi-card {
|
||||
font-size: 15px;
|
||||
line-height: 1.35;
|
||||
border: 1px solid var(--mi);
|
||||
border-radius: 2px;
|
||||
padding: 10px 12px;
|
||||
background:
|
||||
linear-gradient(160deg, color-mix(in srgb, var(--mi) 7%, var(--panel)), var(--panel) 60%);
|
||||
box-shadow: 0 0 14px color-mix(in srgb, var(--mi) 18%, transparent),
|
||||
inset 0 0 22px color-mix(in srgb, var(--mi) 6%, transparent);
|
||||
}
|
||||
.mi-zealot { --mi: #4fb4ff; } /* Zealot: Khai's blue blades */
|
||||
.mi-archon { --mi: #a8ecff; } /* Archon: pure psychic light */
|
||||
.mi-dragoon { --mi: #ffc94d; } /* Dragoon: gold phase disruptor */
|
||||
.mi-top { display: flex; align-items: center; gap: 10px; }
|
||||
.mi-art {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
object-fit: cover;
|
||||
flex: 0 0 auto;
|
||||
clip-path: polygon(25% 3%, 75% 3%, 100% 50%, 75% 97%, 25% 97%, 0% 50%);
|
||||
border: 1px solid var(--mi);
|
||||
filter: saturate(0.9) brightness(0.95);
|
||||
}
|
||||
.mi-id { display: flex; align-items: baseline; gap: 8px; }
|
||||
.mi-name { color: var(--mi); font-size: 19px; text-shadow: 0 0 6px color-mix(in srgb, var(--mi) 55%, transparent); }
|
||||
.mi-role { color: var(--dim); margin: 6px 0 2px; }
|
||||
.mi-voice {
|
||||
display: block;
|
||||
appearance: none;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 2px 0;
|
||||
margin: 4px 0;
|
||||
text-align: left;
|
||||
font: inherit;
|
||||
color: var(--mi);
|
||||
cursor: pointer;
|
||||
text-decoration: underline dotted color-mix(in srgb, var(--mi) 60%, transparent);
|
||||
}
|
||||
.mi-voice:hover { text-shadow: 0 0 8px var(--mi); }
|
||||
.mi-voice.mi-speaking { animation: mi-pulse 0.7s ease-in-out infinite; }
|
||||
@keyframes mi-pulse { 50% { text-shadow: 0 0 12px var(--mi); } }
|
||||
.mi-equiv { color: var(--head); font-style: italic; margin: 4px 0; }
|
||||
.mi-spec { color: var(--fg); margin: 4px 0; }
|
||||
.mi-inputs { color: var(--dim); }
|
||||
.mi-use { color: var(--fg); margin-top: 4px; }
|
||||
.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); }
|
||||
|
||||
/* Bare mode strips the model component to plain prose too. */
|
||||
html[data-bare="1"] .mi-card { border-color: #000; box-shadow: none; background: none; }
|
||||
html[data-bare="1"] .mi-art { display: none; }
|
||||
html[data-bare="1"] .mi-name { color: #000; text-shadow: none; }
|
||||
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; }
|
||||
|
||||
@@ -34,6 +34,8 @@ setup() {
|
||||
# docs screenshots (/img/*) must be served from disk, not answered by the
|
||||
# index.html catch-all (2026-10-08: PNGs came back as 200 text/html).
|
||||
grep -qE "@data[[:space:]]+path.*(/img/\*|/img/\*\")" "$CADDY"
|
||||
# the model panel's unit voices ride the same allowlist
|
||||
grep -qE "@data[[:space:]]+path.*/sfx/\*" "$CADDY"
|
||||
}
|
||||
|
||||
@test "the homepage revalidates its assets (no stale cache after deploys)" {
|
||||
|
||||
+36
-25
@@ -3,7 +3,7 @@
|
||||
# and the pages render. It must be a JSON list; every entry carries name,
|
||||
# category, section, and a host:port probe. category is user or admin;
|
||||
# section must be one the page's SECTION_ORDER renders. method, when
|
||||
# present, is tcp or http. wip, when set, is "open" or "closed".
|
||||
# present, is tcp or http. wip, when set, is "open", "closed" or "down".
|
||||
|
||||
load helpers
|
||||
|
||||
@@ -24,7 +24,7 @@ for svc in data:
|
||||
if "method" in svc:
|
||||
assert svc["method"] in ("tcp", "http"), svc["method"]
|
||||
wip = svc.get("wip")
|
||||
assert wip is False or wip in ("open", "closed"), f"{svc['name']} wip must be false, 'open' or 'closed'"
|
||||
assert wip is False or wip in ("open", "closed", "down"), f"{svc['name']} wip must be false, 'open', 'closed' or 'down'"
|
||||
host, _, port = svc["probe"].rpartition(":")
|
||||
assert host and port.isdigit(), f"bad probe {svc['probe']}"
|
||||
PY
|
||||
@@ -61,36 +61,47 @@ PY
|
||||
|
||||
@test "the LLM status cards track the fleet, not retired service names" {
|
||||
# This drifted twice: the card probing Dankserver's :7770 was still named
|
||||
# frank-small long after frank-medium took that endpoint, and the endpoint
|
||||
# itself has since been frank-medium (radiance), frank-primary (lucebox)
|
||||
# and frank-primary (llama.cpp). The probe target is what matters; the
|
||||
# name has to follow it.
|
||||
# frank-small long after frank-medium took that endpoint; the endpoints
|
||||
# were renamed to the Protoss set on 2026-10-08. The probe target is what
|
||||
# matters; the name has to follow it.
|
||||
python3 - "$REPO_ROOT/shared/homepage/services.json" <<'PY'
|
||||
import json, sys
|
||||
services = json.load(open(sys.argv[1]))
|
||||
by_probe = {s["probe"]: s["name"] for s in services}
|
||||
primary = by_probe.get("192.168.0.235:7770")
|
||||
assert primary == "frank-primary", f"Dankserver :7770 card is named {primary!r}"
|
||||
# Dankclaw serves two endpoints on two ports: frank-large (unsloth llama.cpp
|
||||
# on the iGPU, 7770) and frank-secondary (llama.cpp on the RTX 3090, 7771).
|
||||
assert by_probe.get("127.0.0.1:7770") == "frank-large", by_probe.get("127.0.0.1:7770")
|
||||
assert by_probe.get("127.0.0.1:7771") == "frank-secondary", by_probe.get("127.0.0.1:7771")
|
||||
assert by_probe.get("192.168.0.235:7770") == "frank-archon", by_probe.get("192.168.0.235:7770")
|
||||
assert by_probe.get("127.0.0.1:7770") == "frank-dragoon", by_probe.get("127.0.0.1:7770")
|
||||
assert by_probe.get("127.0.0.1:7771") == "frank-zealot", by_probe.get("127.0.0.1:7771")
|
||||
PY
|
||||
}
|
||||
|
||||
@test "every fleet LLM card carries a user-facing equivalence note" {
|
||||
# Frank's capability framing (2026-10-08): each card must state, in plain
|
||||
# terms, what the model is equivalent to — the cards' whole purpose for
|
||||
# family users is "how smart is this and what is it for".
|
||||
python3 - "$REPO_ROOT/shared/homepage/services.json" <<'PY'
|
||||
import json, sys
|
||||
@test "the Model Information component is complete: text, art, voices, warnings" {
|
||||
# The fused model panel is the family's only description of the fleet.
|
||||
# Every model card must carry: equivalence note, supervision level, the
|
||||
# unit art, at least one playable voice line, and (for uncensored models)
|
||||
# the legal warning. The compaction recommendation must name the zealot.
|
||||
python3 - "$REPO_ROOT/shared/homepage/services.json" "$REPO_ROOT/shared/homepage" <<'PY'
|
||||
import json, os, sys
|
||||
services = json.load(open(sys.argv[1]))
|
||||
fleet = {s["name"]: s for s in services if s.get("name") in ("frank-large", "frank-primary", "frank-secondary")}
|
||||
assert len(fleet) == 3, sorted(fleet)
|
||||
for name, s in fleet.items():
|
||||
eq = s.get("details", {}).get("equiv", "")
|
||||
assert eq.strip(), f"{name} card has no equivalence note"
|
||||
root = sys.argv[2]
|
||||
fleet = [s for s in services if "model" in s]
|
||||
assert {s["name"] for s in fleet} == {"frank-dragoon", "frank-archon", "frank-zealot"}, [s["name"] for s in fleet]
|
||||
for s in fleet:
|
||||
m = s["model"]
|
||||
assert m["equiv"].strip(), f"{s['name']} has no equivalence note"
|
||||
assert m["supervision"].strip(), f"{s['name']} has no supervision note"
|
||||
assert m["sfx"], f"{s['name']} has no voice line"
|
||||
assert os.path.isfile(os.path.join(root, "img", "unit-" + m["unit"] + ".jpg")), f"{s['name']} art missing"
|
||||
for f in m["sfx"] + m.get("voice", {}).get("sfx", []):
|
||||
assert os.path.isfile(os.path.join(root, "sfx", f)), f"{s['name']} voice {f} missing"
|
||||
zealot = next(s for s in fleet if s["name"] == "frank-zealot")
|
||||
assert "compaction" in zealot["model"]["use"].lower(), "zealot must be recommended as compaction model"
|
||||
assert "supervision" in zealot["model"]["supervision"].lower()
|
||||
for s in fleet:
|
||||
m = s["model"]
|
||||
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"
|
||||
PY
|
||||
grep -q "desc-equiv" "$REPO_ROOT/shared/homepage/app.js"
|
||||
grep -q "desc-equiv" "$REPO_ROOT/shared/homepage/style.css"
|
||||
grep -q "renderModelInfo" "$REPO_ROOT/shared/homepage/app.js"
|
||||
grep -q ".mi-card" "$REPO_ROOT/shared/homepage/style.css"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user