- 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
98 lines
5.4 KiB
HTML
98 lines
5.4 KiB
HTML
{% 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 %}
|