Multiple printers, printer/slot in Spoolman, kept in sync
- One worker per printer (name, IP, CC2 Control URL, own pinned host key); a spool sits in at most one slot of one printer, moves between printers - Spoolman: location "<printer> Slot N" plus extra fields "drucker"/"slot" (created by the bridge), cleared and "Lager" on removal - Full comparison every minute: the printer wins over manual edits of loaded spools; stale printer locations go to storage; own locations untouched - Automatic migration of v1 settings, state and host key - Status hint when a tag is still in a tray but unmapped (e.g. after "Take out" in CC2 Control) - SSH test: connection drops during exec give a clean error, not a 500 - Mock environment with several printers; tests for all of the above Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GcEk5tkfFSYD6NTF66NkC
This commit is contained in:
1 parent
bf32667f77
commit
a9041eb538
15 files changed
+1400
-684
No files matched your search
+94
-55
@@ -1,38 +1,44 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}CC2 Bridge – Einstellungen{% endblock %}
|
||||
{% block content %}
|
||||
<h2>Einstellungen</h2>
|
||||
<form id="settings" class="panel" onsubmit="saveSettings(event)">
|
||||
<h2>Drucker</h2>
|
||||
<div id="printers"></div>
|
||||
<div class="row" style="margin-bottom:8px">
|
||||
<button onclick="addPrinter()">Drucker hinzufügen</button>
|
||||
<button class="primary" onclick="saveSettings()">Speichern</button>
|
||||
<span id="saved1" class="small muted"></span>
|
||||
</div>
|
||||
<p class="small muted">Neue oder geänderte Drucker erst speichern, dann „Verbindung testen“.</p>
|
||||
|
||||
<h2>Allgemein</h2>
|
||||
<form id="settings" class="panel" onsubmit="event.preventDefault(); saveSettings()">
|
||||
<div class="row">
|
||||
<div class="field"><label for="printer_host">Drucker-IP</label><input id="printer_host" required></div>
|
||||
<div class="field"><label for="ssh_port">SSH-Port</label><input id="ssh_port" type="number" min="1" max="65535"></div>
|
||||
<div class="field"><label for="ssh_user">SSH-Benutzer</label><input id="ssh_user"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="field"><label for="cc2_url">CC2-Control-URL</label><input id="cc2_url" required></div>
|
||||
<div class="field"><label for="spoolman_url">Spoolman-URL</label><input id="spoolman_url" required></div>
|
||||
<div class="field"><label for="id_offset">ID-Offset</label><input id="id_offset" type="number" min="0" max="65000"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="field"><label for="id_offset">ID-Offset</label><input id="id_offset" type="number" min="0" max="65000"></div>
|
||||
<div class="field"><label for="poll_interval">Abfrage-Intervall (s)</label><input id="poll_interval" type="number" step="0.5" min="1" max="300"></div>
|
||||
<div class="field"><label for="empty_grace">Slot leer nach (s)</label><input id="empty_grace" type="number" min="0"></div>
|
||||
<div class="field"><label for="settle_seconds">Buchen nach Ruhe (s)</label><input id="settle_seconds" type="number" min="0"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="field"><label for="location_slot">Ort im Slot (<code>{n}</code> = 1–4)</label><input id="location_slot" required></div>
|
||||
<div class="field"><label for="location_slot">Ort im Slot (<code>{printer}</code> = Druckername, <code>{n}</code> = 1–4)</label><input id="location_slot" required></div>
|
||||
<div class="field"><label for="location_storage">Ort im Lager</label><input id="location_storage" required></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label><input type="checkbox" id="dry_run" style="width:auto"> Dry-Run (Schreibaktionen nur protokollieren)</label>
|
||||
<label style="color:var(--text)"><input type="checkbox" id="spoolman_fields" style="width:auto"> Zusatzfelder „Drucker“ und „Slot“ in Spoolman pflegen</label>
|
||||
</div>
|
||||
<div class="row"><button class="primary" type="submit">Speichern</button><span id="saved" class="small muted"></span></div>
|
||||
<div class="field">
|
||||
<label style="color:var(--text)"><input type="checkbox" id="dry_run" style="width:auto"> Dry-Run (Schreibaktionen nur protokollieren)</label>
|
||||
</div>
|
||||
<div class="row"><button class="primary" type="submit">Speichern</button><span id="saved2" class="small muted"></span></div>
|
||||
</form>
|
||||
|
||||
<h2>SSH-Schlüssel für den Drucker</h2>
|
||||
<h2>SSH-Schlüssel für die Drucker</h2>
|
||||
<div class="panel">
|
||||
<div id="keyinfo" class="field muted">Lade …</div>
|
||||
<div class="field">
|
||||
<label for="keytext">Privaten Schlüssel einfügen (ed25519, ohne Passphrase) oder Datei wählen</label>
|
||||
<label for="keytext">Privaten Schlüssel einfügen (ed25519, ohne Passphrase) oder Datei wählen – gilt für alle Drucker</label>
|
||||
<textarea id="keytext" rows="6" placeholder="-----BEGIN OPENSSH PRIVATE KEY-----" autocomplete="off" spellcheck="false"></textarea>
|
||||
</div>
|
||||
<div class="row">
|
||||
@@ -42,39 +48,75 @@
|
||||
</div>
|
||||
<p class="small muted">Der Schlüssel wird mit Dateirechten 600 im Daten-Volume gespeichert und nie wieder angezeigt.</p>
|
||||
</div>
|
||||
|
||||
<h2>Verbindung testen</h2>
|
||||
<div class="panel">
|
||||
<div id="hostkey" class="field muted"></div>
|
||||
<div class="row"><button class="primary" onclick="testSSH()">Verbindung testen</button>
|
||||
<button class="danger" onclick="forgetHost()">Host-Key vergessen</button></div>
|
||||
<div id="testresult" style="margin-top:12px"></div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block script %}
|
||||
<script>
|
||||
const FIELDS = ['printer_host', 'ssh_port', 'ssh_user', 'cc2_url', 'spoolman_url', 'id_offset', 'poll_interval',
|
||||
'empty_grace', 'settle_seconds', 'location_slot', 'location_storage'];
|
||||
let current = {};
|
||||
const FIELDS = ['spoolman_url', 'id_offset', 'poll_interval', 'empty_grace', 'settle_seconds', 'location_slot', 'location_storage'];
|
||||
const PFIELDS = [['name', 'Name'], ['host', 'Drucker-IP'], ['ssh_port', 'SSH-Port'], ['ssh_user', 'SSH-Benutzer'], ['cc2_url', 'CC2-Control-URL']];
|
||||
let current = {}, printers = [], hostKeys = {}, saved = new Set();
|
||||
|
||||
function printerCard(p, i) {
|
||||
const hk = hostKeys[p.id];
|
||||
const isSaved = saved.has(p.id);
|
||||
return `<div class="panel" style="margin-bottom:12px" id="pc${i}">
|
||||
<div class="row">${PFIELDS.map(([f, l]) => `<div class="field"><label>${l}</label>
|
||||
<input data-i="${i}" data-f="${f}" value="${esc(p[f])}" ${f === 'ssh_port' ? 'type="number" min="1" max="65535"' : ''}></div>`).join('')}</div>
|
||||
<div class="row" style="align-items:center">
|
||||
<label style="margin:0;color:var(--text);flex:0 0 auto"><input type="checkbox" data-i="${i}" data-f="enabled" ${p.enabled ? 'checked' : ''} style="width:auto"> Abfrage aktiv</label>
|
||||
<span class="small" style="flex:1 1 200px">${isSaved ? (hk ? `Host-Key <code>${esc(hk.fingerprint)}</code>` : '<span class="tag warn">Host-Key nicht bestätigt</span>') : '<span class="tag">noch nicht gespeichert</span>'}</span>
|
||||
${isSaved ? `<button class="primary" onclick="testSSH(${i})">Verbindung testen</button>
|
||||
<button onclick="forgetHost(${i})">Host-Key vergessen</button>` : ''}
|
||||
<button class="danger" onclick="removePrinter(${i})">Entfernen</button>
|
||||
</div>
|
||||
<div id="test${i}" style="margin-top:10px"></div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderPrinters() {
|
||||
$('printers').innerHTML = printers.map(printerCard).join('') || '<div class="panel muted">Kein Drucker eingerichtet.</div>';
|
||||
document.querySelectorAll('#printers input[data-f]').forEach((el) => el.addEventListener('input', () => {
|
||||
const p = printers[el.dataset.i];
|
||||
p[el.dataset.f] = el.type === 'checkbox' ? el.checked : el.value;
|
||||
}));
|
||||
}
|
||||
|
||||
function addPrinter() {
|
||||
printers.push({ id: '', name: '', host: '', ssh_port: 22, ssh_user: 'root', cc2_url: 'http://', enabled: true });
|
||||
renderPrinters();
|
||||
}
|
||||
|
||||
function removePrinter(i) {
|
||||
const p = printers[i];
|
||||
if (!confirm(`Drucker „${p.name || 'neu'}“ entfernen?\n\nSeine Spulen werden nach dem Speichern in Spoolman ins Lager gebucht.`)) return;
|
||||
printers.splice(i, 1);
|
||||
renderPrinters();
|
||||
}
|
||||
|
||||
async function loadSettings() {
|
||||
current = await api('GET', '/api/settings');
|
||||
for (const f of FIELDS) $(f).value = current[f];
|
||||
$('dry_run').checked = current.dry_run;
|
||||
$('spoolman_fields').checked = current.spoolman_fields;
|
||||
printers = current.printers.map((p) => ({ ...p }));
|
||||
saved = new Set(printers.map((p) => p.id));
|
||||
setMode(current.dry_run);
|
||||
}
|
||||
|
||||
async function saveSettings(ev) {
|
||||
ev.preventDefault();
|
||||
const body = {};
|
||||
async function saveSettings() {
|
||||
const body = { printers };
|
||||
for (const f of FIELDS) body[f] = $(f).value;
|
||||
body.dry_run = $('dry_run').checked;
|
||||
body.spoolman_fields = $('spoolman_fields').checked;
|
||||
if (current.dry_run && !body.dry_run && !confirm(
|
||||
'Dry-Run ausschalten?\n\nDie Bridge legt dann Spulen in CC2 Control an, weist Slots zu, setzt Orte in Spoolman und bucht Verbrauch.')) return;
|
||||
'Dry-Run ausschalten?\n\nDie Bridge legt dann Spulen in CC2 Control an, weist Slots zu, pflegt Ort und Zusatzfelder in Spoolman und bucht Verbrauch.')) return;
|
||||
try {
|
||||
current = await api('POST', '/api/settings', body);
|
||||
printers = current.printers.map((p) => ({ ...p }));
|
||||
saved = new Set(printers.map((p) => p.id));
|
||||
setMode(current.dry_run);
|
||||
$('saved').textContent = 'Gespeichert ' + new Date().toLocaleTimeString('de-DE');
|
||||
const t = 'Gespeichert ' + new Date().toLocaleTimeString('de-DE');
|
||||
$('saved1').textContent = t; $('saved2').textContent = t;
|
||||
await loadSSH();
|
||||
} catch (e) { toast(e.message); }
|
||||
}
|
||||
|
||||
@@ -82,21 +124,19 @@ function renderKey(info) {
|
||||
const k = info.key;
|
||||
if (!k) { $('keyinfo').innerHTML = '<span class="tag warn">Kein Schlüssel hinterlegt</span>'; return; }
|
||||
if (k.error) { $('keyinfo').innerHTML = `<span class="tag err">${esc(k.error)}</span>`; return; }
|
||||
const host = $('printer_host').value || '10.10.2.202';
|
||||
$('keyinfo').innerHTML = `<dl class="kv"><dt>Typ</dt><dd>${esc(k.type)}</dd><dt>Fingerprint</dt><dd><code>${esc(k.fingerprint)}</code></dd></dl>
|
||||
<p class="small muted" style="margin:10px 0 4px">Zeile für <code>/root/.ssh/authorized_keys</code> auf dem Drucker (öffentlicher Schlüssel, eingeschränkt):</p>
|
||||
<p class="small muted" style="margin:10px 0 4px">Zeile für <code>/root/.ssh/authorized_keys</code> auf jedem Drucker (öffentlicher Schlüssel, eingeschränkt):</p>
|
||||
<pre id="authline">command="cat /opt/usr/canvas_record_info.json",from="10.10.6.10",no-pty,no-port-forwarding,no-agent-forwarding,no-X11-forwarding ${esc(k.public)}</pre>
|
||||
<button style="margin-top:6px" onclick="copyText($('authline').textContent)">Kopieren</button>`;
|
||||
}
|
||||
|
||||
function renderHost(hk) {
|
||||
$('hostkey').innerHTML = hk
|
||||
? `<dl class="kv"><dt>Bestätigter Host-Key</dt><dd><code>${esc(hk.fingerprint)}</code> <span class="muted small">(${esc(hk.type)}, ${esc(hk.host)})</span></dd></dl>`
|
||||
: '<span class="tag warn">Host-Key des Druckers noch nicht bestätigt</span>';
|
||||
async function loadSSH() {
|
||||
const info = await api('GET', '/api/ssh');
|
||||
hostKeys = info.host_keys || {};
|
||||
renderKey(info);
|
||||
renderPrinters();
|
||||
}
|
||||
|
||||
async function loadSSH() { const info = await api('GET', '/api/ssh'); renderKey(info); renderHost(info.host_key); }
|
||||
|
||||
$('keyfile').addEventListener('change', async (ev) => {
|
||||
const f = ev.target.files[0];
|
||||
if (f) $('keytext').value = await f.text();
|
||||
@@ -117,19 +157,18 @@ async function deleteKey() {
|
||||
await api('DELETE', '/api/ssh/key'); loadSSH();
|
||||
}
|
||||
|
||||
async function forgetHost() {
|
||||
if (!confirm('Gespeicherten Host-Key vergessen? Danach muss er beim nächsten Test neu bestätigt werden.')) return;
|
||||
await api('DELETE', '/api/ssh/hostkey'); loadSSH();
|
||||
async function forgetHost(i) {
|
||||
const p = printers[i];
|
||||
if (!confirm(`Host-Key von ${p.name} vergessen? Danach muss er beim nächsten Test neu bestätigt werden.`)) return;
|
||||
await api('DELETE', `/api/printers/${p.id}/ssh/hostkey`); loadSSH();
|
||||
}
|
||||
|
||||
async function testSSH() {
|
||||
$('testresult').innerHTML = '<span class="muted">Verbinde …</span>';
|
||||
async function testSSH(i) {
|
||||
const p = printers[i], out = $('test' + i);
|
||||
out.innerHTML = '<span class="muted">Verbinde …</span>';
|
||||
let r;
|
||||
try { r = await api('POST', '/api/ssh/test'); } catch (e) { $('testresult').innerHTML = `<div class="banner err">${esc(e.message)}</div>`; return; }
|
||||
if (!r.ok) {
|
||||
$('testresult').innerHTML = `<div class="banner err">${esc(r.error)}</div>`;
|
||||
return;
|
||||
}
|
||||
try { r = await api('POST', `/api/printers/${p.id}/ssh/test`); } catch (e) { out.innerHTML = `<div class="banner err">${esc(e.message)}</div>`; return; }
|
||||
if (!r.ok) { out.innerHTML = `<div class="banner err">${esc(r.error)}</div>`; return; }
|
||||
const rows = r.slots.map((s) => {
|
||||
const ch = s.channel || {};
|
||||
const c = (ch.color || '').replace(/^0x/i, '');
|
||||
@@ -138,16 +177,16 @@ async function testSSH() {
|
||||
}).join('');
|
||||
let host = `<div class="banner ok">Verbunden. Host-Key <code>${esc(r.host_key.fingerprint)}</code> stimmt mit dem gespeicherten überein.</div>`;
|
||||
if (!r.host_key_pinned) {
|
||||
host = `<div class="banner warn">Neuer Host-Key: <code>${esc(r.host_key.fingerprint)}</code> (${esc(r.host_key.type)}).
|
||||
Mit <code>ssh-keygen -lf /etc/dropbear/…</code> bzw. <code>ssh-keyscan</code> vergleichen und bestätigen.
|
||||
<button class="primary" onclick="acceptHost('${esc(r.host_key.fingerprint)}')">Bestätigen und speichern</button></div>`;
|
||||
host = `<div class="banner warn">Neuer Host-Key: <code>${esc(r.host_key.fingerprint)}</code> (${esc(r.host_key.type)}). Vergleichen und bestätigen.
|
||||
<button class="primary" onclick="acceptHost(${i}, '${esc(r.host_key.fingerprint)}')">Bestätigen und speichern</button></div>`;
|
||||
}
|
||||
$('testresult').innerHTML = host + `<div class="table-wrap"><table><thead><tr><th>Slot</th><th>Tag</th><th>weight</th><th>Spoolman-ID</th></tr></thead><tbody>${rows}</tbody></table></div>`;
|
||||
out.innerHTML = host + `<div class="table-wrap"><table><thead><tr><th>Slot</th><th>Tag</th><th>weight</th><th>Spoolman-ID</th></tr></thead><tbody>${rows}</tbody></table></div>`;
|
||||
}
|
||||
|
||||
async function acceptHost(fp) {
|
||||
if (!confirm('Host-Key ' + fp + ' als vertrauenswürdig speichern?')) return;
|
||||
try { const r = await api('POST', '/api/ssh/hostkey', { fingerprint: fp }); renderHost(r.host_key); testSSH(); }
|
||||
async function acceptHost(i, fp) {
|
||||
const p = printers[i];
|
||||
if (!confirm(`Host-Key ${fp} für ${p.name} als vertrauenswürdig speichern?`)) return;
|
||||
try { await api('POST', `/api/printers/${p.id}/ssh/hostkey`, { fingerprint: fp }); await loadSSH(); testSSH(i); }
|
||||
catch (e) { toast(e.message); }
|
||||
}
|
||||
|
||||
|
||||
+30
-17
@@ -2,8 +2,7 @@
|
||||
{% block title %}CC2 Bridge – Status{% endblock %}
|
||||
{% block content %}
|
||||
<div id="banners"></div>
|
||||
<h2>Canvas-Slots</h2>
|
||||
<div class="grid" id="slots"><div class="panel muted">Lade …</div></div>
|
||||
<div id="printers"><div class="panel muted">Lade …</div></div>
|
||||
<p class="small muted" id="lastpoll"></p>
|
||||
<h2>Protokoll</h2>
|
||||
<div class="panel table-wrap">
|
||||
@@ -21,7 +20,7 @@ function colorOf(ch) {
|
||||
return /^[0-9a-f]{6}$/i.test(c) ? c : null;
|
||||
}
|
||||
|
||||
function slotCard(s) {
|
||||
function slotCard(p, s) {
|
||||
const ch = s.record.channel || {};
|
||||
const tray = TRAY[s.tray_status];
|
||||
const col = colorOf(ch);
|
||||
@@ -37,9 +36,11 @@ function slotCard(s) {
|
||||
: (m.cc2 ? '<span class="tag warn">nicht zugewiesen</span>' : '<span class="tag warn">noch nicht angelegt</span>');
|
||||
}
|
||||
if (s.cc2_question) assign += ` <span class="tag warn">CC2 fragt: ${esc(s.cc2_question)}</span>`;
|
||||
const orphan = !m && s.record.spool_id != null && (s.tray_status === 1 || s.tray_status === 2);
|
||||
const lb = s.last_booking;
|
||||
return `<div class="panel">
|
||||
<h3>Slot ${s.slot + 1} ${tray ? `<span class="tag ${tray[1]}">${tray[0]}</span>` : ''}</h3>
|
||||
<p class="small muted" style="margin:-4px 0 8px">Ort in Spoolman: ${esc(s.location)}</p>
|
||||
<dl class="kv">
|
||||
<dt>Tag</dt><dd>${tag}</dd>
|
||||
<dt>Tag-ID</dt><dd>${s.record.spool_id ?? '<span class="muted">–</span>'}</dd>
|
||||
@@ -48,25 +49,30 @@ function slotCard(s) {
|
||||
<dt>CC2 Control</dt><dd>${cc ? esc(cc.name) + ` <span class="small muted">Rest ${Number(cc.remaining).toFixed(1)} g</span><br>` : ''}${assign}</dd>
|
||||
<dt>Buchung</dt><dd>${lb ? `${lb.grams.toFixed(2)} g <span class="small muted">${fmtTime(lb.time)}</span>` : '<span class="muted">–</span>'}</dd>
|
||||
</dl>
|
||||
${orphan ? `<p class="small" style="margin:10px 0 0;color:var(--warn)">Tag ${s.record.spool_id} steckt noch, ist aber keinem Slot zugeordnet
|
||||
(z. B. nach „Take out“ in CC2 Control). Verbrauch wird so nicht gezählt – zum Zuordnen die Spule neu einlegen und scannen.</p>` : ''}
|
||||
${m ? `<div class="row" style="margin-top:10px">
|
||||
<button onclick="slotAction(${s.slot}, 'reassign')">Zuordnung erneut senden</button>
|
||||
<button class="danger" onclick="slotAction(${s.slot}, 'release')">Aufheben</button></div>` : ''}
|
||||
<button onclick="slotAction('${p.id}', ${s.slot}, 'reassign')">Zuordnung erneut senden</button>
|
||||
<button class="danger" onclick="slotAction('${p.id}', ${s.slot}, 'release')">Aufheben</button></div>` : ''}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
async function slotAction(slot, action) {
|
||||
let NAMES = {};
|
||||
async function slotAction(pid, slot, action) {
|
||||
const name = NAMES[pid] || pid;
|
||||
const text = action === 'release'
|
||||
? `Zuordnung von Slot ${slot + 1} aufheben? Die Spule wird in Spoolman ins Lager gebucht.`
|
||||
: `Zuordnung von Slot ${slot + 1} erneut an CC2 Control senden?`;
|
||||
? `${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/slots/${slot}/${action}`); toast('Eingereiht'); setTimeout(load, 800); }
|
||||
try { await api('POST', `/api/printers/${pid}/slots/${slot}/${action}`); toast('Eingereiht'); setTimeout(load, 800); }
|
||||
catch (e) { toast(e.message); }
|
||||
}
|
||||
|
||||
async function enableCC2() {
|
||||
if (!confirm('Spulenverwaltung in CC2 Control einschalten?\n\nAb dann zählt CC2 Control den Verbrauch je Slot.')) return;
|
||||
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/cc2/enable', { confirm: true });
|
||||
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); }
|
||||
@@ -79,12 +85,19 @@ async function load() {
|
||||
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>');
|
||||
else if (!st.host_key) b.push('<div class="banner warn">Host-Key des Druckers noch nicht bestätigt. <a href="/settings">Verbindung testen</a></div>');
|
||||
if (st.cc2_enabled === false) b.push('<div class="banner err"><b>Spulenverwaltung in CC2 Control ist ausgeschaltet.</b> Verbrauch wird nicht gezählt. <button class="primary" onclick="enableCC2()">Einschalten …</button></div>');
|
||||
if (st.job_active) b.push('<div class="banner ok">Druck läuft – Verbrauch wird nach dem Druck gebucht.</div>');
|
||||
for (const [k, v] of Object.entries(st.errors || {})) if (v) b.push(`<div class="banner err">${esc(v)}</div>`);
|
||||
if (st.spoolman_error) b.push(`<div class="banner err">${esc(st.spoolman_error)}</div>`);
|
||||
$('banners').innerHTML = b.join('');
|
||||
if (st.slots) $('slots').innerHTML = st.slots.map(slotCard).join('');
|
||||
NAMES = Object.fromEntries((st.printers || []).map((p) => [p.id, p.name]));
|
||||
if (st.printers) $('printers').innerHTML = st.printers.map((p) => {
|
||||
const 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"><b>Spulenverwaltung in CC2 Control ist ausgeschaltet.</b> Verbrauch wird nicht gezählt. <button class="primary" onclick="enableCC2('${p.id}')">Einschalten …</button></div>`);
|
||||
if (p.job_active) pb.push('<div class="banner ok">Druck läuft – Verbrauch wird nach dem Druck gebucht.</div>');
|
||||
for (const v of Object.values(p.errors || {})) if (v) pb.push(`<div class="banner err">${esc(v)}</div>`);
|
||||
return `<h2>${esc(p.name)} <span class="small muted" style="font-weight:400">${esc(p.host)}</span></h2>${pb.join('')}
|
||||
<div class="grid">${p.slots.map((s) => slotCard(p, s)).join('')}</div>`;
|
||||
}).join('') || '<div class="panel muted">Kein Drucker eingerichtet. <a href="/settings">Einstellungen</a></div>';
|
||||
$('lastpoll').textContent = st.last_poll ? 'Letzte Abfrage: ' + fmtTime(st.last_poll) : 'Noch keine Abfrage';
|
||||
$('log').innerHTML = (st.log || []).map((e) => {
|
||||
const l = LEVEL[e.level] || [e.level, ''];
|
||||
|
||||
Reference in new issue
Block a user