Redesigned status, log, tag generator and settings pages: spool rings in filament colour, live job card (progress and time left read from CC2 Control's /api/printer while a job runs), log page with filters, iOS-style settings with a write/dry-run switch. On phones: bottom tab bar, printer switcher, slot details in a bottom sheet. Dark mode follows the system. Web app manifest and home screen icons for "Add to Home Screen". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GcEk5tkfFSYD6NTF66NkC
359 lines
21 KiB
HTML
359 lines
21 KiB
HTML
{% extends "base.html" %}
|
||
{% block title %}CC2 Bridge – Status{% endblock %}
|
||
{% block heading %}Status{% endblock %}
|
||
{% block head %}
|
||
<style>
|
||
.layout { display: flex; flex-wrap: wrap; gap: 28px; align-items: flex-start; }
|
||
.printers { flex: 999 1 640px; min-width: 0; display: flex; flex-direction: column; gap: 32px; }
|
||
.aside { flex: 1 1 300px; min-width: 0; padding: 18px 18px 8px; }
|
||
.phead { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
|
||
.phead h2 { font-size: 24px; letter-spacing: -.02em; }
|
||
.phead .chip { margin-left: auto; font-size: 13px; padding: 5px 12px; }
|
||
.printer { display: flex; flex-direction: column; gap: 14px; }
|
||
|
||
.job { background: var(--hero); color: var(--hero-ink); border-radius: var(--radius); padding: 18px 20px; display: flex; flex-direction: column; gap: 12px; }
|
||
.job .k { font-size: 12px; color: var(--hero-muted); }
|
||
.job .v { font-family: var(--mono); font-size: 20px; }
|
||
.job .file { font-weight: 700; font-size: 16px; overflow-wrap: anywhere; }
|
||
.job .stats { display: flex; gap: 28px; flex-wrap: wrap; }
|
||
.job .bar { height: 6px; border-radius: 99px; background: var(--hero-track); overflow: hidden; }
|
||
.job .bar div { height: 6px; border-radius: 99px; background: var(--hero-accent); }
|
||
.job .note { font-size: 13px; color: var(--hero-muted); }
|
||
.job .label { color: var(--hero-muted); }
|
||
|
||
.slots { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: 12px; }
|
||
.slot { display: flex; flex-direction: column; gap: 14px; min-height: 236px; }
|
||
.slot .sh { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
||
.slot .sh b { font-size: 14px; font-weight: 800; }
|
||
.spool { display: flex; gap: 14px; align-items: center; text-align: left; border: 0; background: none; padding: 0; min-height: 0;
|
||
border-radius: 12px; font-weight: inherit; color: inherit; width: 100%; }
|
||
.spool:hover .nm { text-decoration: underline; }
|
||
.spool .nm { font-weight: 800; font-size: 16px; overflow-wrap: anywhere; }
|
||
.facts { display: flex; flex-direction: column; gap: 6px; font-size: 13px; }
|
||
.facts div { display: flex; justify-content: space-between; gap: 8px; }
|
||
.facts span:first-child { color: var(--muted); }
|
||
.facts span:last-child { text-align: right; }
|
||
.slot .acts { margin-top: auto; display: flex; gap: 8px; }
|
||
.slot .acts button { flex: 1; font-size: 13px; min-height: 36px; padding: 0 8px; }
|
||
.emptybox { flex: 1; border: 2px dashed var(--line); border-radius: 12px; display: flex; align-items: center; justify-content: center;
|
||
color: var(--muted); font-size: 13px; font-weight: 600; min-height: 100px; }
|
||
.dashed { border: 3px dashed var(--faint); }
|
||
.hint { margin: 0; font-size: 13px; color: var(--muted); }
|
||
.hint.warn { color: var(--warn); }
|
||
|
||
.seg { display: grid; gap: 4px; background: var(--line); padding: 4px; border-radius: 12px; }
|
||
.seg button { border: 0; min-height: 36px; border-radius: 9px; background: transparent; color: var(--muted); font-weight: 600; }
|
||
.seg button[aria-selected=true] { background: var(--surface); color: var(--ink); font-weight: 700; box-shadow: 0 1px 2px rgba(0,0,0,.08); }
|
||
.seg button::before { content: ""; width: 7px; height: 7px; border-radius: 99px; background: var(--dot, var(--faint)); }
|
||
|
||
.logrow { display: flex; gap: 12px; padding: 12px 0; border-top: 1px solid var(--line2); }
|
||
.logrow .msg { font-size: 14px; overflow-wrap: anywhere; }
|
||
.logrow .meta { font-size: 12px; color: var(--muted); }
|
||
.lastbook { display: flex; gap: 12px; align-items: center; }
|
||
|
||
@media (max-width: 759px) {
|
||
.printer:not(.sel) { display: none; }
|
||
.printers { gap: 14px; }
|
||
.phead { display: none; }
|
||
.single .phead { display: flex; }
|
||
.job { padding: 16px; }
|
||
.job .stats { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
|
||
.job .v { font-size: 17px; }
|
||
}
|
||
</style>
|
||
{% endblock %}
|
||
{% block content %}
|
||
<div id="banners" class="stack" style="margin-bottom:14px"></div>
|
||
<div id="seg" class="seg phone-only" role="tablist" aria-label="Drucker" style="margin-bottom:14px"></div>
|
||
<div class="layout">
|
||
<div id="printers" class="printers"><div class="card muted">Lade …</div></div>
|
||
<aside class="card aside desktop-only" aria-label="Protokoll">
|
||
<div class="row" style="justify-content:space-between;margin-bottom:6px"><h2 style="font-size:17px">Protokoll</h2><a href="/log" class="small" style="font-weight:700;text-decoration:none">Alle</a></div>
|
||
<div id="log"></div>
|
||
</aside>
|
||
</div>
|
||
<dialog id="sheet" class="sheet" aria-labelledby="sheet-title"><div class="sheet-in" id="sheet-in"></div></dialog>
|
||
{% endblock %}
|
||
{% block script %}
|
||
<script>
|
||
const QUESTION = { inserted: 'eingelegt', changed: 'geändert' };
|
||
const TRAY = { 0: ['leer', ''], 1: ['eingelegt', 'ok'], 2: ['fördert', 'accent'] };
|
||
let ST = null, NAMES = {}, SEL = null, SHEET = null;
|
||
try { SEL = localStorage.getItem('printer'); } catch (e) {}
|
||
|
||
function hex(c) { c = String(c || '').replace(/^0x|^#/i, ''); return /^[0-9a-f]{6}$/i.test(c) ? '#' + c : null; }
|
||
|
||
function kindOf(s) {
|
||
if (s.mapping) return 'managed';
|
||
if (s.takeout) return 'takeout';
|
||
if (s.tray_status === 1 || s.tray_status === 2) return 'unmanaged';
|
||
return 'empty';
|
||
}
|
||
|
||
function info(s) {
|
||
const m = s.mapping, sm = s.spoolman, cc = s.cc2_spool, ch = s.record.channel || {};
|
||
const kind = kindOf(s);
|
||
const tagColor = hex(ch.color);
|
||
const out = { kind, tray: TRAY[s.tray_status] };
|
||
if (kind === 'managed') {
|
||
const rest = cc ? Number(cc.remaining) : (sm && sm.remaining != null ? Number(sm.remaining) : null);
|
||
const initial = (sm && sm.initial) || 1000;
|
||
out.color = (sm && hex(sm.color)) || tagColor || '#9AA3B0';
|
||
out.rest = rest; out.initial = initial;
|
||
out.pct = rest == null ? 0 : Math.max(0, Math.min(100, rest / initial * 100));
|
||
out.title = m.name || ('Spule ' + m.spool);
|
||
out.sub = sm ? [sm.vendor, sm.material].filter(Boolean).join(' ') : '';
|
||
if (s.cc2_matches) out.cc2 = ['zugewiesen', 'ok'];
|
||
else out.cc2 = m.cc2 ? ['nicht zugewiesen', 'warn'] : ['noch nicht angelegt', 'warn'];
|
||
if (s.cc2_question) out.question = 'CC2 fragt: Spule ' + (QUESTION[s.cc2_question] || s.cc2_question);
|
||
} else if (kind === 'takeout') {
|
||
out.title = s.takeout.name; out.color = tagColor || '#9AA3B0';
|
||
out.sub = 'In CC2 Control herausgenommen';
|
||
out.hint = `${s.takeout.name} steht in Spoolman im Lager, steckt aber noch. Was dieser Slot druckt, bucht die Bridge trotzdem auf ${s.takeout.name}, bis die Rolle entnommen wird. Wieder zuordnen: Rolle herausnehmen und neu einlegen.`;
|
||
} else if (kind === 'unmanaged') {
|
||
out.color = tagColor;
|
||
out.sub = s.record.occupied ? [ch.manufacturer, ch.detailed_type || ch.type].filter(Boolean).join(' ') : 'Rolle ohne Tag-Daten';
|
||
if (s.record.spool_id != null) {
|
||
out.title = 'Tag ' + s.record.spool_id + ' ohne Zuordnung';
|
||
out.hint = `Tag ${s.record.spool_id} steht noch im Slot, ist aber keiner Rolle zugeordnet. Liegt diese Rolle hier, sie herausnehmen und neu einlegen (oder in CC2 Control auswählen).`;
|
||
} else {
|
||
out.title = 'Ohne Spoolman-Tag';
|
||
out.hint = 'Verbrauch wird nicht gebucht. Rolle taggen und scannen, um sie zu verwalten.';
|
||
}
|
||
}
|
||
return out;
|
||
}
|
||
|
||
function ringHtml(i, size, hole, inner) {
|
||
const bg = i.kind === 'managed'
|
||
? `conic-gradient(${i.color} 0 ${i.pct}%, var(--track) ${i.pct}% 100%)`
|
||
: (i.kind === 'empty' ? 'var(--bg)' : 'var(--surface)');
|
||
const cls = i.kind === 'unmanaged' || i.kind === 'takeout' ? ' dashed' : '';
|
||
return `<div class="ring${cls}" style="width:${size}px;height:${size}px;background:${bg}" aria-hidden="true">
|
||
<div style="width:${hole}px;height:${hole}px">${inner}</div></div>`;
|
||
}
|
||
|
||
function stateOf(p) {
|
||
if (!p.enabled) return ['Pausiert', ''];
|
||
if (Object.values(p.errors || {}).some(Boolean)) return ['Fehler', 'err'];
|
||
if (p.cc2_enabled === false) return ['Verwaltung aus', 'warn'];
|
||
if (p.job_active) return ['Druckt', 'accent'];
|
||
return ['Bereit', 'ok'];
|
||
}
|
||
|
||
function fmtLeft(sec) {
|
||
if (sec == null || sec < 0) return '–';
|
||
const h = Math.floor(sec / 3600), m = Math.round((sec % 3600) / 60);
|
||
return h ? `${h}:${String(m).padStart(2, '0')} h` : `${m} min`;
|
||
}
|
||
|
||
function jobCard(p) {
|
||
const j = p.job;
|
||
if (!j) return '';
|
||
const prog = j.progress != null ? Math.round(j.progress) : null;
|
||
return `<div class="job">
|
||
<div class="row" style="justify-content:space-between;gap:8px">
|
||
<span class="label">Druck läuft</span>
|
||
${j.layer ? `<span class="mono small" style="color:var(--hero-muted)">Schicht ${j.layer}/${j.layers || '?'}</span>` : ''}
|
||
</div>
|
||
<div class="file">${esc(j.file || 'Unbekannte Datei')}</div>
|
||
${prog != null ? `<div class="bar"><div style="width:${prog}%"></div></div>` : ''}
|
||
<div class="stats">
|
||
<div><div class="k">Fortschritt</div><div class="v">${prog != null ? prog + '%' : '–'}</div></div>
|
||
<div><div class="k">Restzeit</div><div class="v">${fmtLeft(j.remaining)}</div></div>
|
||
<div><div class="k">Verbraucht</div><div class="v">${fmtNum(j.grams, 1)} g</div></div>
|
||
</div>
|
||
<div class="note">Wird nach Druckende auf die Spoolman-Spulen gebucht.</div>
|
||
</div>`;
|
||
}
|
||
|
||
function slotCard(p, s) {
|
||
const i = info(s), lb = s.last_booking;
|
||
const tray = i.tray ? `<span class="chip ${i.tray[1]}">${i.tray[0]}</span>` : '';
|
||
let body;
|
||
if (i.kind === 'managed') {
|
||
body = `<button class="spool" onclick="openSheet('${p.id}', ${s.slot})">
|
||
${ringHtml(i, 76, 54, `<span class="mono" style="font-size:14px;line-height:1">${i.rest == null ? '?' : Math.round(i.rest)}</span><span style="font-size:10px;color:var(--muted)">g</span>`)}
|
||
<span style="min-width:0"><span class="nm" style="display:block">${esc(i.title)} <span class="muted small" style="font-weight:500">#${s.mapping.spool}</span></span>
|
||
<span class="small muted" style="display:block">${esc(i.sub)}</span>
|
||
<span class="small muted" style="display:block">von ${Math.round(i.initial)} g</span></span>
|
||
</button>
|
||
<div class="facts">
|
||
<div><span>CC2 Control</span><span style="font-weight:700;color:var(--${i.cc2[1]})">${i.cc2[0]}</span></div>
|
||
${i.question ? `<div><span></span><span class="chip warn">${esc(i.question)}</span></div>` : ''}
|
||
<div><span>Zuletzt gebucht</span><span class="mono">${lb ? fmtNum(lb.grams, 2) + ' g · ' + fmtClock(lb.time) : '–'}</span></div>
|
||
</div>
|
||
<div class="acts">
|
||
<button onclick="slotAction('${p.id}', ${s.slot}, 'reassign')">Erneut senden</button>
|
||
<button class="danger" onclick="slotAction('${p.id}', ${s.slot}, 'release')">Aufheben</button>
|
||
</div>`;
|
||
} else if (i.kind === 'empty') {
|
||
body = '<div class="emptybox">Kein Filament</div>';
|
||
} else {
|
||
body = `<button class="spool" onclick="openSheet('${p.id}', ${s.slot})">
|
||
${ringHtml(i, 76, 54, i.color ? `<span class="swatch" style="width:22px;height:22px;background:${i.color}"></span>` : '')}
|
||
<span style="min-width:0"><span class="nm" style="display:block;font-size:15px">${esc(i.title)}</span>
|
||
<span class="small muted" style="display:block">${esc(i.sub)}</span></span>
|
||
</button>
|
||
<p class="hint${i.kind === 'takeout' ? ' warn' : ''}" style="margin-top:auto">${esc(i.hint)}</p>`;
|
||
}
|
||
return `<article class="card slot"><div class="sh"><b>Slot ${s.slot + 1}</b>${tray}</div>${body}</article>`;
|
||
}
|
||
|
||
function slotRow(p, s) {
|
||
const i = info(s);
|
||
const inner = `<span style="font-weight:800;font-size:13px">${s.slot + 1}</span>`;
|
||
let title = i.title, sub = i.sub, rest = '';
|
||
if (i.kind === 'empty') { title = 'Leer'; sub = 'Kein Filament'; }
|
||
if (i.kind === 'managed') {
|
||
rest = i.rest == null ? '?' : Math.round(i.rest) + ' g';
|
||
sub = [i.sub, '#' + s.mapping.spool, i.question || i.cc2[0]].filter(Boolean).join(' · ');
|
||
}
|
||
const tray = i.tray || ['', ''];
|
||
const trayColor = { ok: 'var(--ok)', accent: 'var(--accent)' }[tray[1]] || 'var(--muted)';
|
||
const tag = i.kind === 'empty' ? 'div' : 'button';
|
||
return `<${tag} class="item" ${tag === 'button' ? `onclick="openSheet('${p.id}', ${s.slot})"` : ''}>
|
||
${ringHtml(i, 48, 32, inner)}
|
||
<span class="grow"><span class="title" style="display:block">${esc(title)}</span><span class="sub" style="display:block">${esc(sub)}</span></span>
|
||
<span style="text-align:right"><span class="mono" style="display:block">${rest}</span><span style="display:block;font-size:12px;font-weight:700;color:${trayColor}">${tray[0]}</span></span>
|
||
${tag === 'button' ? CHEV : '<span style="width:8px"></span>'}
|
||
</${tag}>`;
|
||
}
|
||
|
||
function lastBooking(p) {
|
||
const b = p.slots.map((s) => s.last_booking && { ...s.last_booking, name: s.mapping && s.mapping.name, spool: s.mapping && s.mapping.spool })
|
||
.filter(Boolean).sort((a, b) => b.time - a.time)[0];
|
||
if (!b) return '';
|
||
return `<div class="card lastbook phone-only">
|
||
<div class="icon-badge" style="background:var(--ok-bg);color:var(--ok)">g</div>
|
||
<div style="min-width:0"><div class="small muted">Letzte Buchung · ${fmtClock(b.time)}</div>
|
||
<div style="font-weight:700;font-size:14px">${fmtNum(b.grams, 2)} g auf ${esc(b.name || '')}${b.spool ? ` (Spoolman ${b.spool})` : ''}</div>
|
||
${b.details ? `<div class="small muted" style="overflow-wrap:anywhere">${esc(b.details)}</div>` : ''}</div>
|
||
</div>`;
|
||
}
|
||
|
||
function printerSection(p) {
|
||
const st = stateOf(p), pb = [];
|
||
if (!p.enabled) pb.push('<div class="banner warn">Abfrage pausiert (in den Einstellungen deaktiviert).</div>');
|
||
else if (ST.ssh_key && !p.host_key) pb.push('<div class="banner warn">Host-Key noch nicht bestätigt. <a href="/settings">Verbindung testen</a></div>');
|
||
if (p.cc2_enabled === false) pb.push(`<div class="banner err"><span style="flex:1 1 220px">Spulenverwaltung in CC2 Control ist ausgeschaltet. Verbrauch wird nicht gezählt.</span><button class="primary" onclick="enableCC2('${p.id}')">Einschalten …</button></div>`);
|
||
for (const v of Object.values(p.errors || {})) if (v) pb.push(`<div class="banner err">${esc(v)}</div>`);
|
||
return `<section class="printer${p.id === SEL ? ' sel' : ''}" aria-label="${esc(p.name)}">
|
||
<div class="phead"><h2>${esc(p.name)}</h2><span class="mono small muted host">${esc(p.host)}</span><span class="chip dot ${st[1]}">${st[0]}</span></div>
|
||
${pb.join('')}
|
||
${jobCard(p)}
|
||
<div class="slots desktop-only">${p.slots.map((s) => slotCard(p, s)).join('')}</div>
|
||
<div class="group phone-only">${p.slots.map((s) => slotRow(p, s)).join('')}</div>
|
||
${lastBooking(p)}
|
||
</section>`;
|
||
}
|
||
|
||
function renderSeg(printers) {
|
||
const seg = $('seg');
|
||
if (printers.length < 2) { seg.style.display = 'none'; return; }
|
||
seg.style.display = '';
|
||
seg.style.gridTemplateColumns = `repeat(${printers.length}, minmax(0, 1fr))`;
|
||
const dot = { ok: 'var(--ok)', accent: 'var(--accent)', err: 'var(--err)', warn: 'var(--warn)' };
|
||
seg.innerHTML = printers.map((p) => `<button role="tab" aria-selected="${p.id === SEL}" style="--dot:${dot[stateOf(p)[1]] || 'var(--faint)'}"
|
||
onclick="selectPrinter('${p.id}')">${esc(p.name)}</button>`).join('');
|
||
}
|
||
function selectPrinter(id) {
|
||
SEL = id;
|
||
try { localStorage.setItem('printer', id); } catch (e) {}
|
||
render();
|
||
}
|
||
|
||
function sheetHtml(p, s) {
|
||
const i = info(s), m = s.mapping, sm = s.spoolman, cc = s.cc2_spool, ch = s.record.channel || {}, lb = s.last_booking;
|
||
const rows = [];
|
||
rows.push(['Tag', s.record.occupied ? `${hex(ch.color) ? `<span class="swatch" style="background:${hex(ch.color)}"></span> ` : ''}${esc(ch.detailed_type || ch.type)} <span class="muted small">${esc(ch.manufacturer)}</span>` : '<span class="muted">kein Tag</span>']);
|
||
rows.push(['Tag-ID', s.record.spool_id ?? '<span class="muted">–</span>']);
|
||
if (m) rows.push(['Spoolman', `${sm && sm.color ? `<span class="swatch" style="background:#${sm.color}"></span> ` : ''}<b>${esc(m.name || m.spool)}</b> <span class="muted">#${m.spool}</span>` +
|
||
(sm ? `<br><span class="small muted">${esc(sm.vendor)} ${esc(sm.material)}${sm.name ? ' · ' + esc(sm.name) : ''}, Rest ${sm.remaining != null ? Math.round(sm.remaining) + ' g' : '?'}</span>` : '')]);
|
||
rows.push(['CC2 Control', (cc ? `${esc(cc.name)} <span class="small muted">Rest ${fmtNum(cc.remaining, 1)} g</span><br>` : '') +
|
||
(i.cc2 ? `<span class="chip ${i.cc2[1]}">${i.cc2[0]}</span>` : '<span class="muted">–</span>') + (i.question ? ` <span class="chip warn">${esc(i.question)}</span>` : '')]);
|
||
rows.push(['Ort in Spoolman', esc(s.location)]);
|
||
rows.push(['Buchung', lb ? `${fmtNum(lb.grams, 2)} g <span class="small muted">${fmtTime(lb.time)}</span>${lb.details ? `<br><span class="small muted">${esc(lb.details)}</span>` : ''}` : '<span class="muted">–</span>']);
|
||
const inner = i.kind === 'managed' ? `<span class="mono" style="font-size:15px;line-height:1">${i.rest == null ? '?' : Math.round(i.rest)}</span><span style="font-size:10px;color:var(--muted)">g</span>` : '';
|
||
return `<div class="grip"></div>
|
||
<div class="row" style="flex-wrap:nowrap;gap:14px">
|
||
${ringHtml(i, 64, 44, inner)}
|
||
<div style="flex:1;min-width:0"><div class="small muted">${esc(p.name)} · Slot ${s.slot + 1}${i.tray ? ' · ' + i.tray[0] : ''}</div>
|
||
<h2 id="sheet-title" style="overflow-wrap:anywhere">${esc(i.title || 'Leer')}</h2>${i.sub ? `<div class="small muted">${esc(i.sub)}</div>` : ''}</div>
|
||
<button aria-label="Schließen" onclick="closeSheet()" style="min-width:40px;padding:0;border-radius:99px">✕</button>
|
||
</div>
|
||
${i.hint ? `<p class="hint${i.kind === 'takeout' ? ' warn' : ''}">${esc(i.hint)}</p>` : ''}
|
||
<dl class="kv">${rows.map(([k, v]) => `<dt>${k}</dt><dd>${v}</dd>`).join('')}</dl>
|
||
${m ? `<div class="row" style="flex-wrap:nowrap"><button style="flex:1;min-height:48px" onclick="slotAction('${p.id}', ${s.slot}, 'reassign')">Erneut senden</button>
|
||
<button class="danger" style="flex:1;min-height:48px" onclick="slotAction('${p.id}', ${s.slot}, 'release')">Aufheben</button></div>` : ''}`;
|
||
}
|
||
function openSheet(pid, slot) {
|
||
SHEET = { pid, slot };
|
||
renderSheet();
|
||
if (!$('sheet').open) $('sheet').showModal();
|
||
}
|
||
function renderSheet() {
|
||
if (!SHEET || !ST) return;
|
||
const p = ST.printers.find((x) => x.id === SHEET.pid);
|
||
if (!p) { closeSheet(); return; }
|
||
$('sheet-in').innerHTML = sheetHtml(p, p.slots[SHEET.slot]);
|
||
}
|
||
function closeSheet() { SHEET = null; if ($('sheet').open) $('sheet').close(); }
|
||
$('sheet').addEventListener('close', () => { SHEET = null; });
|
||
$('sheet').addEventListener('click', (ev) => { if (ev.target === $('sheet')) closeSheet(); });
|
||
|
||
async function slotAction(pid, slot, action) {
|
||
const name = NAMES[pid] || pid;
|
||
const text = action === 'release'
|
||
? `${name}: Zuordnung von Slot ${slot + 1} aufheben? Die Spule wird in Spoolman ins Lager gebucht.`
|
||
: `${name}: Zuordnung von Slot ${slot + 1} erneut an CC2 Control senden?`;
|
||
if (!confirm(text)) return;
|
||
try { await api('POST', `/api/printers/${pid}/slots/${slot}/${action}`); toast('Eingereiht'); setTimeout(load, 800); }
|
||
catch (e) { toast(e.message); }
|
||
}
|
||
|
||
async function enableCC2(pid) {
|
||
const name = NAMES[pid] || pid;
|
||
if (!confirm(`Spulenverwaltung in CC2 Control von ${name} einschalten?\n\nAb dann zählt CC2 Control den Verbrauch je Slot.`)) return;
|
||
try {
|
||
const r = await api('POST', `/api/printers/${pid}/cc2/enable`, { confirm: true });
|
||
toast(r.dry_run ? 'Dry-Run: nur protokolliert' : 'Wird eingeschaltet');
|
||
setTimeout(load, 1500);
|
||
} catch (e) { toast(e.message); }
|
||
}
|
||
|
||
function render() {
|
||
const st = ST, printers = st.printers || [];
|
||
if (!printers.some((p) => p.id === SEL)) SEL = printers.length ? printers[0].id : null;
|
||
const b = [];
|
||
if (st.dry_run) b.push('<div class="banner dry">Dry-Run aktiv: Schreibaktionen werden nur protokolliert („würde …“). Umschalten unter Einstellungen.</div>');
|
||
if (!st.ssh_key) b.push('<div class="banner warn">Kein SSH-Schlüssel hinterlegt. <a href="/settings">Einstellungen</a></div>');
|
||
if (st.spoolman_error) b.push(`<div class="banner err">${esc(st.spoolman_error)}</div>`);
|
||
$('banners').innerHTML = b.join('');
|
||
$('banners').style.display = b.length ? '' : 'none';
|
||
renderSeg(printers);
|
||
$('printers').classList.toggle('single', printers.length < 2);
|
||
$('printers').innerHTML = printers.map(printerSection).join('')
|
||
|| '<div class="card muted">Kein Drucker eingerichtet. <a href="/settings">Einstellungen</a></div>';
|
||
$('log').innerHTML = (st.log || []).slice(0, 8).map((e) => {
|
||
const [ic, bg, fg] = logIcon(e), [pn, msg] = splitPrinter(e.msg, Object.values(NAMES));
|
||
return `<div class="logrow"><div class="icon-badge" style="background:${bg};color:${fg}">${ic}</div>
|
||
<div style="min-width:0"><div class="msg">${esc(msg)}</div><div class="meta">${esc(pn)} · ${fmtClock(e.time)}</div></div></div>`;
|
||
}).join('') || '<p class="muted small">Noch keine Einträge</p>';
|
||
renderSheet();
|
||
}
|
||
|
||
async function load() {
|
||
try { ST = await api('GET', '/api/status'); }
|
||
catch (e) { $('banners').style.display = ''; $('banners').innerHTML = `<div class="banner err">${esc(e.message)}</div>`; return; }
|
||
setMode(ST.dry_run);
|
||
setPoll(ST.last_poll);
|
||
NAMES = Object.fromEntries((ST.printers || []).map((p) => [p.id, p.name]));
|
||
render();
|
||
}
|
||
load();
|
||
setInterval(() => { if (!document.hidden) load(); }, 3000);
|
||
document.addEventListener('visibilitychange', () => { if (!document.hidden) load(); });
|
||
</script>
|
||
{% endblock %}
|