Files
Jonas MenzelandClaude Opus 5.5 64ea91d107
CI / test (push) Successful in 2m29s
CI / image (push) Successful in 39s
Spool page for NFC links, bridge address; vendor-first new spool form
- /s/<id> shows a spool: rest in Spoolman and CC2 Control, where it is,
  last booking, history, links to Spoolman and the tag generator.
- Setting "Adresse der Bridge"; the tag generator can put <address>/s/<id>
  on the tag, so an iPhone held to the roll opens that page.
- New spool: vendor and material chosen from Spoolman, the vendor's
  filaments offered, values taken from similar filaments, duplicates
  pointed out and never created twice.

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

78 lines
4.3 KiB
HTML
Raw Permalink 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 – Spule {{ spool_id }}{% endblock %}
{% block heading %}Spule{% endblock %}
{% block head %}
<style>
.wrap { max-width: 720px; }
.hero { display: flex; gap: 20px; align-items: center; flex-wrap: wrap; }
.hero h2 { font-size: 26px; letter-spacing: -.02em; overflow-wrap: anywhere; }
.big { font-family: var(--mono); font-size: 28px; }
.where { font-size: 15px; padding: 6px 12px; }
.acts { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
.acts .btn { min-height: 48px; }
.entry { display: flex; gap: 12px; padding: 12px 0; border-top: 1px solid var(--line2); }
.entry:first-child { border-top: 0; }
</style>
{% endblock %}
{% block content %}
<div class="wrap stack" id="page" data-id="{{ spool_id }}"><div class="card muted">Lade …</div></div>
{% endblock %}
{% block script %}
<script>
const ID = Number($('page').dataset.id);
function render(d) {
const s = d.spool, color = s.color ? '#' + s.color : 'var(--faint)';
const initial = s.initial || 1000, rest = s.remaining;
const pct = rest == null ? 0 : Math.max(0, Math.min(100, rest / initial * 100));
let where;
if (s.archived) where = '<span class="chip where">Archiviert (leer)</span>';
else if (d.slot && d.slot.takeout) where = `<span class="chip warn where">${esc(d.slot.printer)} · Slot ${d.slot.slot} (herausgenommen, steckt noch)</span>`;
else if (d.slot) where = `<span class="chip accent dot where">${esc(d.slot.printer)} · Slot ${d.slot.slot}</span>`;
else where = `<span class="chip where">${esc(s.location || d.storage)}</span>`;
const rows = [
['Rest in Spoolman', rest == null ? '–' : `${fmtNum(rest, 1)} g`],
...d.copies.map((c) => [`Rest in CC2 Control (${esc(c.printer)})`, c.remaining == null ? '–' : `${fmtNum(c.remaining, 1)} g`]),
['Ort in Spoolman', esc(s.location || '–')],
['Zuletzt gebucht', d.last_booking ? `${fmtNum(d.last_booking.grams, 2)} g · ${fmtTime(d.last_booking.time)}${d.last_booking.details ? `<br><span class="small muted">${esc(d.last_booking.details)}</span>` : ''}` : '–'],
['Material', `${esc(s.vendor)} ${esc(s.material)}${s.diameter ? ` · ${s.diameter} mm` : ''}${s.density ? ` · ${s.density} g/cm³` : ''}`],
['Düse', s.extruder_temp ? `${s.extruder_temp} °C` : '–'],
['Leergewicht', s.spool_weight ? `${fmtNum(s.spool_weight)} g` : '–'],
];
$('page').innerHTML = `
<div class="card stack">
<div class="hero">
<div class="ring" style="width:120px;height:120px;background:conic-gradient(${color} 0 ${pct}%, var(--track) ${pct}% 100%)" aria-hidden="true">
<div style="width:88px;height:88px"><span class="mono" style="font-size:20px;line-height:1">${rest == null ? '?' : Math.round(rest)}</span><span class="small muted">von ${Math.round(initial)} g</span></div>
</div>
<div style="flex:1;min-width:200px" class="stack">
<div><div class="small muted">Spoolman #${s.id}</div><h2>${esc(s.label)}</h2><div class="muted">${esc(d.filament_name)}</div></div>
<div>${where}</div>
</div>
</div>
<dl class="kv">${rows.map(([k, v]) => `<dt>${k}</dt><dd>${v}</dd>`).join('')}</dl>
<div class="acts">
<a class="btn" href="${esc(d.spoolman_url)}" target="_blank" rel="noopener">In Spoolman öffnen</a>
<a class="btn" href="/tags?spool=${s.id}">Tag neu schreiben</a>
</div>
</div>
${d.log.length ? `<div class="label section-label" style="margin:8px 4px 0">Verlauf</div>
<div class="group" style="padding:0 14px">${d.log.map((e) => {
const [ic, bg, fg] = logIcon(e), [pn, msg] = splitPrinter(e.msg, d.copies.map((c) => c.printer).concat(d.slot ? [d.slot.printer] : []));
return `<div class="entry"><div class="icon-badge" style="background:${bg};color:${fg}">${ic}</div>
<div style="min-width:0"><div style="font-size:14px;overflow-wrap:anywhere">${esc(msg)}</div><div class="small muted">${esc(pn)} · ${fmtTime(e.time)}</div></div></div>`;
}).join('')}</div>` : ''}`;
}
async function load() {
try {
const d = await api('GET', `/api/spool/${ID}`);
setMode(d.dry_run);
render(d);
} catch (e) { $('page').innerHTML = `<div class="banner err">${esc(e.message)}</div>`; }
}
load();
setInterval(() => { if (!document.hidden) load(); }, 10000);
</script>
{% endblock %}