diff --git a/serverAppConfigs-docs/Infrastructure/Homepage.md b/serverAppConfigs-docs/Infrastructure/Homepage.md index c58e8d6..b86d52d 100644 --- a/serverAppConfigs-docs/Infrastructure/Homepage.md +++ b/serverAppConfigs-docs/Infrastructure/Homepage.md @@ -115,6 +115,19 @@ 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). +**Icons v2, clickable tiles, header nav (2026-10-08, third wave):** five +icons replaced on request — Copyparty gets an improvised amber folder, +Marreta a crumbling wall (bricks + red crack), Calibre-Web a book, +OpenWebUI its official logo (`openwebui.com/logo.png`), Seerr the official +`seerr.dev/os_logo_filled.svg`. Tiles with an open link are now clickable +whole (`.panel.clickable`, hover lift + glow; clicks on inner links pass +through). Any panel whose live state is DOWN gets a red gradient wash + +slow pulsing glow (`.panel.down`, and `.mi-card:has(.badge.down)` for +model cards). docs/admin/account-settings moved from the footer to the +header (`nav.top-links`). The zealot compaction advice moved out of the +Use paragraph into a bordered glowing `model.compaction` callout +(`.mi-comp`), pinned by the component seam test. + ## 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. diff --git a/shared/homepage/app.js b/shared/homepage/app.js index 3f46793..eac9e18 100644 --- a/shared/homepage/app.js +++ b/shared/homepage/app.js @@ -90,7 +90,7 @@ function miCard(svc, state) { if (m.spec) parts.push('
' + esc(m.spec) + '
'); if (m.inputs && m.inputs.length) parts.push('
Inputs: ' + esc(m.inputs.join(' · ')) + '
'); if (m.use) parts.push('
Use: ' + esc(m.use) + '
'); - if (m.voice) parts.push(voiceBtn(m.voice.text, m.voice.sfx)); + if (m.compaction) parts.push('
⚡ ' + esc(m.compaction) + '
'); if (m.supervision) parts.push('
' + esc(m.supervision) + '
'); if (m.permission) parts.push('
⚠ ' + esc(m.permission) + '
'); if (m.warn) parts.push('
' + @@ -137,6 +137,12 @@ function renderPanel(svc, state) { '
' + '
' + descHTML(svc) + '
' + warn + ''; + if (svc.showLink && svc.url) { + panel.classList.add('clickable'); + panel.addEventListener('click', function (ev) { + if (!ev.target.closest('a')) window.location.href = svc.url; + }); + } return panel; } @@ -220,6 +226,7 @@ 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/img/apps/mono-cp.svg b/shared/homepage/img/apps/mono-cp.svg index c41ed65..fbc6fa4 100644 --- a/shared/homepage/img/apps/mono-cp.svg +++ b/shared/homepage/img/apps/mono-cp.svg @@ -1 +1 @@ -CP \ No newline at end of file + \ No newline at end of file diff --git a/shared/homepage/img/apps/mono-cw.svg b/shared/homepage/img/apps/mono-cw.svg index d160cd0..0143ad6 100644 --- a/shared/homepage/img/apps/mono-cw.svg +++ b/shared/homepage/img/apps/mono-cw.svg @@ -1 +1 @@ -CW \ No newline at end of file + \ No newline at end of file diff --git a/shared/homepage/img/apps/mono-mr.svg b/shared/homepage/img/apps/mono-mr.svg index 67eef21..5df9715 100644 --- a/shared/homepage/img/apps/mono-mr.svg +++ b/shared/homepage/img/apps/mono-mr.svg @@ -1 +1 @@ -MR \ No newline at end of file + \ No newline at end of file diff --git a/shared/homepage/img/apps/openwebui.png b/shared/homepage/img/apps/openwebui.png new file mode 100644 index 0000000..135908d Binary files /dev/null and b/shared/homepage/img/apps/openwebui.png differ diff --git a/shared/homepage/img/apps/seerr.svg b/shared/homepage/img/apps/seerr.svg new file mode 100644 index 0000000..e3b105e --- /dev/null +++ b/shared/homepage/img/apps/seerr.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/shared/homepage/index.html b/shared/homepage/index.html index 54e60a5..24cfefd 100644 --- a/shared/homepage/index.html +++ b/shared/homepage/index.html @@ -13,17 +13,17 @@
frank@home:~$ services - bare +
diff --git a/shared/homepage/services.json b/shared/homepage/services.json index 254c249..992a61e 100644 --- a/shared/homepage/services.json +++ b/shared/homepage/services.json @@ -24,7 +24,7 @@ "method": "http", "wip": false, "showLink": true, - "art": "/img/apps/mono-ow.svg" + "art": "/img/apps/openwebui.png" }, { "name": "Gitea", @@ -115,7 +115,7 @@ "method": "http", "wip": "down", "showLink": false, - "art": "/img/apps/mono-sr.svg" + "art": "/img/apps/seerr.svg" }, { "name": "Marreta", @@ -303,7 +303,7 @@ "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.", + "use": "Code editor autocomplete, agentic search, RAG, research subagents, translation, roleplay, and summarization.", "voice": { "text": "I LONG FOR COMBAT!", "sfx": [ @@ -311,7 +311,8 @@ ] }, "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." + "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.", + "compaction": "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." }, "url": "", "docs": "/docs#models", diff --git a/shared/homepage/style.css b/shared/homepage/style.css index 3cff933..16ae435 100644 --- a/shared/homepage/style.css +++ b/shared/homepage/style.css @@ -71,6 +71,26 @@ body::before { } .panel { border: 1px solid var(--border); background: var(--panel); padding: 12px 14px; border-radius: 2px; } +.panel.clickable { cursor: pointer; transition: border-color 0.15s, box-shadow 0.15s, transform 0.15s; } +.panel.clickable:hover { + border-color: var(--link); + box-shadow: 0 0 14px color-mix(in srgb, var(--link) 35%, transparent); + transform: translateY(-2px); +} +.panel.down { + background: linear-gradient(180deg, + color-mix(in srgb, var(--down) 12%, var(--panel)), var(--panel)); + animation: down-pulse 3.2s ease-in-out infinite; +} +@keyframes down-pulse { + 50% { border-color: color-mix(in srgb, var(--down) 70%, var(--border)); + box-shadow: 0 0 16px color-mix(in srgb, var(--down) 40%, transparent); } +} +.mi-card:has(.badge.down) { + background: linear-gradient(180deg, + color-mix(in srgb, var(--down) 12%, var(--panel)), var(--panel)); + animation: down-pulse 3.2s ease-in-out infinite; +} .panel-head { display: flex; align-items: center; gap: 8px; } .panel-head .name { flex: 1; } .app-art { @@ -108,6 +128,8 @@ body::before { justify-content: space-between; } .foot a { color: var(--link); text-decoration: none; } +.top-links { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; } +.top .foot-btn { color: var(--link); text-decoration: none; } .foot-links { display: flex; gap: 10px; flex-wrap: wrap; } .foot-btn { border: 1px solid var(--link); @@ -235,9 +257,9 @@ html[data-bare="1"] .mi-card { border-color: #000; box-shadow: none; background: 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-role, html[data-bare="1"] .mi-spec, html[data-bare="1"] .mi-use, html[data-bare="1"] .mi-comp, 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; } +html[data-bare="1"] .svc-warn, html[data-bare="1"] .mi-perm, html[data-bare="1"] .mi-comp { color: #000; border: none; text-shadow: none; padding: 0; } +html[data-bare="1"] .mi-perm, html[data-bare="1"] .mi-comp { border: 1px solid #000; } diff --git a/tests/homepage_config.bats b/tests/homepage_config.bats index 9513fb7..ac8df88 100644 --- a/tests/homepage_config.bats +++ b/tests/homepage_config.bats @@ -94,7 +94,8 @@ for s in fleet: 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 "compaction" in zealot["model"].get("compaction", ""), "zealot must be recommended as compaction model" +assert zealot["model"].get("compaction", "") != "", "compaction advice must be its own visible field" assert "supervision" in zealot["model"]["supervision"].lower() for s in fleet: m = s["model"]