Files
cc2-spoolman-bridge/app/templates/status.html
T
Jonas MenzelandClaude Opus 5.5 bf32667f77
CI / test (push) Successful in 1m4s
CI / image (push) Successful in 1m1s
CC2 ↔ Spoolman bridge with NFC tag generator
- Reads /opt/usr/canvas_record_info.json over SSH (pinned host key,
  restricted key), decodes Spoolman spool ids from the tag weight
- Mirrors spools into CC2 Control's spool library and assigns slots,
  keeps its own slot mapping, sets Spoolman locations
- Books the drop of CC2 Control's measured remaining weight to Spoolman
  after each print, idempotent across restarts and interrupted bookings
- Dry-run by default; web UI for settings, SSH key, status and log
- Tag generator for NFC Tools command chains (NTAG213/215/216)
- Tests, mock environment, Dockerfile, Gitea Actions CI

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GcEk5tkfFSYD6NTF66NkC
2026-10-25 03:01:17 +01:00

98 lines
5.4 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% 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>
<p class="small muted" id="lastpoll"></p>
<h2>Protokoll</h2>
<div class="panel table-wrap">
<table><thead><tr><th style="width:170px">Zeit</th><th style="width:80px"></th><th>Aktion</th></tr></thead>
<tbody id="log"></tbody></table>
</div>
{% endblock %}
{% block script %}
<script>
const TRAY = { 0: ['leer', 'warn'], 1: ['eingelegt', 'ok'], 2: ['fördert', 'ok'] };
const LEVEL = { info: ['Info', ''], warn: ['Warnung', 'warn'], error: ['Fehler', 'err'], dry: ['Dry-Run', 'dry'] };
function colorOf(ch) {
const c = (ch.color || '').replace(/^0x/i, '');
return /^[0-9a-f]{6}$/i.test(c) ? c : null;
}
function slotCard(s) {
const ch = s.record.channel || {};
const tray = TRAY[s.tray_status];
const col = colorOf(ch);
const m = s.mapping, sm = s.spoolman, cc = s.cc2_spool;
let tag = '<span class="muted">kein Tag</span>';
if (s.record.occupied) {
tag = `${col ? `<span class="swatch" style="background:#${col}"></span> ` : ''}${esc(ch.detailed_type || ch.type)}` +
` <span class="muted small">${esc(ch.manufacturer)}, weight ${esc(ch.weight)}</span>`;
}
let assign = '<span class="muted">–</span>';
if (m) {
assign = s.cc2_matches ? '<span class="tag ok">zugewiesen</span>'
: (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 lb = s.last_booking;
return `<div class="panel">
<h3>Slot ${s.slot + 1} ${tray ? `<span class="tag ${tray[1]}">${tray[0]}</span>` : ''}</h3>
<dl class="kv">
<dt>Tag</dt><dd>${tag}</dd>
<dt>Tag-ID</dt><dd>${s.record.spool_id ?? '<span class="muted">–</span>'}</dd>
<dt>Spoolman</dt><dd>${m ? `${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)}, Rest ${sm.remaining != null ? Math.round(sm.remaining) + ' g' : '?'}</span>` : '') : '<span class="muted">nicht zugeordnet</span>'}</dd>
<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>
${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>` : ''}
</div>`;
}
async function slotAction(slot, action) {
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?`;
if (!confirm(text)) return;
try { await api('POST', `/api/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;
try {
const r = await api('POST', '/api/cc2/enable', { confirm: true });
toast(r.dry_run ? 'Dry-Run: nur protokolliert' : 'Wird eingeschaltet');
setTimeout(load, 1500);
} catch (e) { toast(e.message); }
}
async function load() {
let st;
try { st = await api('GET', '/api/status'); } catch (e) { $('banners').innerHTML = `<div class="banner err">${esc(e.message)}</div>`; return; }
setMode(st.dry_run);
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>`);
$('banners').innerHTML = b.join('');
if (st.slots) $('slots').innerHTML = st.slots.map(slotCard).join('');
$('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, ''];
return `<tr><td class="small muted">${fmtTime(e.time)}</td><td><span class="tag ${l[1]}">${l[0]}</span></td><td>${esc(e.msg)}</td></tr>`;
}).join('') || '<tr><td colspan="3" class="muted">Noch keine Einträge</td></tr>';
}
load();
setInterval(load, 3000);
</script>
{% endblock %}