- 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
157 lines
8.0 KiB
HTML
157 lines
8.0 KiB
HTML
{% extends "base.html" %}
|
||
{% block title %}CC2 Bridge – Einstellungen{% endblock %}
|
||
{% block content %}
|
||
<h2>Einstellungen</h2>
|
||
<form id="settings" class="panel" onsubmit="saveSettings(event)">
|
||
<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>
|
||
<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_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>
|
||
</div>
|
||
<div class="row"><button class="primary" type="submit">Speichern</button><span id="saved" class="small muted"></span></div>
|
||
</form>
|
||
|
||
<h2>SSH-Schlüssel für den 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>
|
||
<textarea id="keytext" rows="6" placeholder="-----BEGIN OPENSSH PRIVATE KEY-----" autocomplete="off" spellcheck="false"></textarea>
|
||
</div>
|
||
<div class="row">
|
||
<input type="file" id="keyfile" style="flex:1 1 220px">
|
||
<button class="primary" onclick="saveKey()">Schlüssel speichern</button>
|
||
<button class="danger" onclick="deleteKey()">Schlüssel löschen</button>
|
||
</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 = {};
|
||
|
||
async function loadSettings() {
|
||
current = await api('GET', '/api/settings');
|
||
for (const f of FIELDS) $(f).value = current[f];
|
||
$('dry_run').checked = current.dry_run;
|
||
setMode(current.dry_run);
|
||
}
|
||
|
||
async function saveSettings(ev) {
|
||
ev.preventDefault();
|
||
const body = {};
|
||
for (const f of FIELDS) body[f] = $(f).value;
|
||
body.dry_run = $('dry_run').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;
|
||
try {
|
||
current = await api('POST', '/api/settings', body);
|
||
setMode(current.dry_run);
|
||
$('saved').textContent = 'Gespeichert ' + new Date().toLocaleTimeString('de-DE');
|
||
} catch (e) { toast(e.message); }
|
||
}
|
||
|
||
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>
|
||
<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'); renderKey(info); renderHost(info.host_key); }
|
||
|
||
$('keyfile').addEventListener('change', async (ev) => {
|
||
const f = ev.target.files[0];
|
||
if (f) $('keytext').value = await f.text();
|
||
});
|
||
|
||
async function saveKey() {
|
||
const key = $('keytext').value.trim();
|
||
if (!key) { toast('Kein Schlüssel eingegeben'); return; }
|
||
try {
|
||
const r = await api('POST', '/api/ssh/key', { key });
|
||
$('keytext').value = ''; $('keyfile').value = '';
|
||
renderKey(r); toast('Schlüssel gespeichert');
|
||
} catch (e) { toast(e.message); }
|
||
}
|
||
|
||
async function deleteKey() {
|
||
if (!confirm('SSH-Schlüssel löschen?')) return;
|
||
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 testSSH() {
|
||
$('testresult').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;
|
||
}
|
||
const rows = r.slots.map((s) => {
|
||
const ch = s.channel || {};
|
||
const c = (ch.color || '').replace(/^0x/i, '');
|
||
return `<tr><td>Slot ${s.slot + 1}</td><td>${s.occupied ? `${/^[0-9a-f]{6}$/i.test(c) ? `<span class="swatch" style="background:#${c}"></span> ` : ''}${esc(ch.detailed_type || ch.type)} <span class="muted small">${esc(ch.manufacturer)}</span>` : '<span class="muted">leer</span>'}</td>
|
||
<td class="mono">${esc(ch.weight)}</td><td>${s.spool_id ?? '<span class="muted">–</span>'}</td></tr>`;
|
||
}).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>`;
|
||
}
|
||
$('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>`;
|
||
}
|
||
|
||
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(); }
|
||
catch (e) { toast(e.message); }
|
||
}
|
||
|
||
loadSettings().then(loadSSH);
|
||
</script>
|
||
{% endblock %}
|