- 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
132 lines
6.7 KiB
HTML
132 lines
6.7 KiB
HTML
<!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) => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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>
|