Files
cc2-spoolman-bridge/app/templates/base.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

132 lines
6.7 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.
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}CC2 Bridge{% endblock %}</title>
<style>
:root {
--bg: #f6f7f9; --panel: #ffffff; --text: #1d2330; --muted: #5d6677; --line: #dde1e8;
--accent: #2856d9; --accent-text: #fff; --ok: #1b7f3b; --warn: #a15c00; --err: #b3261e;
--ok-bg: #e7f5ec; --warn-bg: #fff4e0; --err-bg: #fde8e7; --dry-bg: #eef1ff; --code: #f0f2f5;
--radius: 8px;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #14171c; --panel: #1c2027; --text: #e6e9ef; --muted: #9aa3b2; --line: #2e343e;
--accent: #6d8dff; --accent-text: #0d1020; --ok: #5cc982; --warn: #f0b55c; --err: #ff8a80;
--ok-bg: #16301f; --warn-bg: #33270f; --err-bg: #3a1a18; --dry-bg: #1e2340; --code: #232832;
}
}
* { box-sizing: border-box; }
body { margin: 0; background: var(--bg); color: var(--text);
font: 15px/1.45 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; }
header { background: var(--panel); border-bottom: 1px solid var(--line); }
.wrap { max-width: 1180px; margin: 0 auto; padding: 0 16px; }
header .wrap { display: flex; align-items: center; gap: 24px; min-height: 52px; flex-wrap: wrap; }
header h1 { font-size: 16px; margin: 0; font-weight: 650; }
nav { display: flex; gap: 4px; flex-wrap: wrap; }
nav a { color: var(--muted); text-decoration: none; padding: 6px 10px; border-radius: 6px; }
nav a.on, nav a:hover { color: var(--text); background: var(--code); }
.mode { margin-left: auto; font-size: 13px; padding: 3px 10px; border-radius: 99px; font-weight: 600; }
.mode.dry { background: var(--dry-bg); color: var(--accent); }
.mode.live { background: var(--err-bg); color: var(--err); }
main { padding: 20px 0 48px; }
h2 { font-size: 18px; margin: 28px 0 12px; }
h2:first-child { margin-top: 0; }
h3 { font-size: 15px; margin: 0 0 8px; }
.panel { background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); padding: 16px; }
.grid { display: grid; gap: 12px; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); }
.muted { color: var(--muted); }
.small { font-size: 13px; }
.banner { border-radius: var(--radius); padding: 10px 14px; margin-bottom: 12px; display: flex; gap: 12px;
align-items: center; flex-wrap: wrap; }
.banner.err { background: var(--err-bg); color: var(--err); }
.banner.warn { background: var(--warn-bg); color: var(--warn); }
.banner.ok { background: var(--ok-bg); color: var(--ok); }
.banner.dry { background: var(--dry-bg); color: var(--accent); }
button, .btn { font: inherit; border: 1px solid var(--line); background: var(--panel); color: var(--text);
padding: 6px 12px; border-radius: 6px; cursor: pointer; }
button.primary { background: var(--accent); color: var(--accent-text); border-color: var(--accent); }
button.danger { color: var(--err); }
button:disabled { opacity: .5; cursor: default; }
input, select, textarea { font: inherit; color: var(--text); background: var(--bg); border: 1px solid var(--line);
border-radius: 6px; padding: 6px 8px; width: 100%; }
textarea { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px; }
label { display: block; font-size: 13px; color: var(--muted); margin-bottom: 4px; }
.field { margin-bottom: 12px; }
.row { display: flex; gap: 12px; flex-wrap: wrap; align-items: flex-end; }
.row > .field { flex: 1 1 160px; }
code, .mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px; }
code { background: var(--code); padding: 1px 5px; border-radius: 4px; overflow-wrap: anywhere; }
pre { background: var(--code); padding: 10px; border-radius: 6px; overflow-x: auto; white-space: pre-wrap;
word-break: break-all; margin: 0; }
table { width: 100%; border-collapse: collapse; }
th, td { text-align: left; padding: 6px 8px; border-bottom: 1px solid var(--line); vertical-align: top; }
th { font-size: 13px; color: var(--muted); font-weight: 600; }
.table-wrap { overflow-x: auto; }
.swatch { display: inline-block; width: 14px; height: 14px; border-radius: 3px; border: 1px solid var(--line);
vertical-align: -2px; }
.tag { display: inline-block; font-size: 12px; padding: 1px 7px; border-radius: 99px; background: var(--code); }
.tag.ok { background: var(--ok-bg); color: var(--ok); }
.tag.warn { background: var(--warn-bg); color: var(--warn); }
.tag.err { background: var(--err-bg); color: var(--err); }
.tag.dry { background: var(--dry-bg); color: var(--accent); }
dl.kv { display: grid; grid-template-columns: max-content 1fr; gap: 3px 12px; margin: 0; font-size: 14px; }
dl.kv dt { color: var(--muted); }
dl.kv dd { margin: 0; overflow-wrap: anywhere; }
.toast { position: fixed; bottom: 16px; left: 50%; transform: translateX(-50%); background: var(--text);
color: var(--bg); padding: 8px 14px; border-radius: 6px; opacity: 0; transition: opacity .2s; pointer-events: none; }
.toast.show { opacity: 1; }
</style>
</head>
<body>
<header><div class="wrap">
<h1>CC2 ↔ Spoolman</h1>
<nav>
<a href="/" class="{{ 'on' if active == 'status.html' }}">Status</a>
<a href="/tags" class="{{ 'on' if active == 'tags.html' }}">Tag-Generator</a>
<a href="/settings" class="{{ 'on' if active == 'settings.html' }}">Einstellungen</a>
</nav>
<span id="mode" class="mode"></span>
</div></header>
<main><div class="wrap">
{% block content %}{% endblock %}
</div></main>
<div id="toast" class="toast"></div>
<script>
const $ = (id) => document.getElementById(id);
const esc = (v) => String(v ?? '').replace(/[&<>"']/g, (c) => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
async function api(method, url, body) {
const opt = { method, headers: { 'X-Bridge-Request': '1' } };
if (body !== undefined) { opt.headers['Content-Type'] = 'application/json'; opt.body = JSON.stringify(body); }
const r = await fetch(url, opt);
let data = null;
try { data = await r.json(); } catch (e) {}
if (!r.ok) throw new Error((data && (data.detail || data.error)) || ('HTTP ' + r.status));
return data;
}
function toast(msg) {
const t = $('toast'); t.textContent = msg; t.classList.add('show');
clearTimeout(toast.t); toast.t = setTimeout(() => t.classList.remove('show'), 2200);
}
async function copyText(text) {
try { await navigator.clipboard.writeText(text); }
catch (e) {
const ta = document.createElement('textarea'); ta.value = text; document.body.appendChild(ta);
ta.select(); document.execCommand('copy'); ta.remove();
}
toast('Kopiert');
}
function fmtTime(t) { return t ? new Date(t * 1000).toLocaleString('de-DE') : '–'; }
function setMode(dry) {
const m = $('mode');
m.textContent = dry ? 'Dry-Run' : 'Schreiben aktiv';
m.className = 'mode ' + (dry ? 'dry' : 'live');
}
api('GET', '/api/settings').then((s) => setMode(s.dry_run)).catch(() => {});
</script>
{% block script %}{% endblock %}
</body>
</html>