Files
serverAppConfigs/shared/homepage/app.js
T
Frank 57fd42af6a homepage: rework LLM model cards to structured role/spec/use lines
Each LLM card now carries a details object (role/spec/use) rendered as a
bold pale-green role, a bright-green spec line, and a dim 'Use:' line.
app.js descHTML() falls back to the plain description for non-LLM cards;
new .desc-role/.desc-spec/.desc-use CSS + bare-mode reset.

Roles made neutral (no nicknames): frank-large = deep reasoning,
frank-medium = primary model, frank-small = fast utility model.

The /docs model guide was fixed — it had drifted to retired model info
(frank-large as Qwen3.8-Flash-Next 87.3 GiB/200k, frank-medium as FP8
~31 GiB) — now Qwen3.8-27B UD-Q8_K_XL 31.5 GiB/262k and MXFP4/W4A8
19.4 GiB, reordered large->medium->small. bats 30/30 + live render.
2026-09-25 23:51:41 -07:00

131 lines
4.8 KiB
JavaScript

// The homepage front end. It reads status.json and services.json once on load
// (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: ['Web Apps', 'Media Library Apps', 'AI'],
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(); });
}
function esc(s) {
return String(s).replace(/[&<>"']/g, function (c) {
return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
});
}
// The description: structured role/spec/use lines when the service defines
// them (the LLM cards), else the plain description string.
function descHTML(svc) {
const d = svc.details;
if (d && (d.role || d.spec || d.use)) {
const lines = [];
if (d.role) lines.push('<div class="desc-role">' + esc(d.role) + '</div>');
if (d.spec) lines.push('<div class="desc-spec">' + esc(d.spec) + '</div>');
if (d.use) lines.push('<div class="desc-use">Use: ' + esc(d.use) + '</div>');
return lines.join('');
}
return esc(svc.description || '');
}
// 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">' + descHTML(svc) + '</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 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-' + 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));
}
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;
}
// The styling toggle (Q27): ?bare=1 strips the page to bare, unstyled HTML.
function applyBare() {
const bare = new URLSearchParams(window.location.search).get('bare') === '1';
document.documentElement.setAttribute('data-bare', bare ? '1' : '0');
const t = document.getElementById('bare-toggle');
if (t) {
t.textContent = bare ? 'styled' : 'bare';
t.href = bare ? '?bare=0' : '?bare=1';
}
}
// The last master deployment datetime (Q43), baked in at deploy time.
function fillDeployed() {
const el = document.getElementById('deployed-at');
if (el && window.DEPLOYED_AT) el.textContent = window.DEPLOYED_AT;
}
function main() {
applyBare();
fillDeployed();
Promise.all([load('services.json'), load('status.json')])
.then(function (pair) { renderPanels(pair[0], pair[1]); })
.catch(function (e) {
document.getElementById('summary').textContent = 'status unavailable: ' + e;
});
}
main();