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
+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