New web UI with iPhone web app
Redesigned status, log, tag generator and settings pages: spool rings in filament colour, live job card (progress and time left read from CC2 Control's /api/printer while a job runs), log page with filters, iOS-style settings with a write/dry-run switch. On phones: bottom tab bar, printer switcher, slot details in a bottom sheet. Dark mode follows the system. Web app manifest and home screen icons for "Add to Home Screen". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GcEk5tkfFSYD6NTF66NkC
This commit is contained in:
1 parent
67c3b54903
commit
ae44441cfb
16 files changed
+982
-285
No files matched your search
@@ -0,0 +1,78 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}CC2 Bridge – Protokoll{% endblock %}
|
||||
{% block heading %}Protokoll{% endblock %}
|
||||
{% block head %}
|
||||
<style>
|
||||
.wrap { max-width: 860px; }
|
||||
.chips { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 6px; }
|
||||
.chips button { border-radius: 99px; min-height: 36px; font-size: 13px; }
|
||||
.chips button[aria-pressed=true] { background: var(--ink); border-color: var(--ink); color: var(--bg); }
|
||||
.day { margin: 18px 4px 8px; }
|
||||
.entries { padding: 0 14px; }
|
||||
.entry { display: flex; gap: 12px; padding: 12px 0; border-top: 1px solid var(--line2); }
|
||||
.entry:first-child { border-top: 0; }
|
||||
.entry .msg { font-size: 14px; line-height: 1.4; overflow-wrap: anywhere; }
|
||||
.entry .meta { font-size: 12px; color: var(--muted); margin-top: 2px; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="wrap">
|
||||
<div class="chips" id="chips"></div>
|
||||
<div id="list"><div class="card muted">Lade …</div></div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block script %}
|
||||
<script>
|
||||
const FILTERS = [
|
||||
['all', 'Alle', () => true],
|
||||
['book', 'Buchungen', (e) => /gebucht/.test(e.msg) && e.level !== 'dry'],
|
||||
['warn', 'Hinweise', (e) => e.level === 'warn' || e.level === 'dry'],
|
||||
['error', 'Fehler', (e) => e.level === 'error'],
|
||||
];
|
||||
let FILTER = 'all', DATA = null;
|
||||
|
||||
function renderChips() {
|
||||
$('chips').innerHTML = FILTERS.map(([k, label]) =>
|
||||
`<button aria-pressed="${k === FILTER}" onclick="setFilter('${k}')">${label}</button>`).join('');
|
||||
}
|
||||
function setFilter(k) { FILTER = k; renderChips(); render(); }
|
||||
|
||||
function dayLabel(t) {
|
||||
const d = new Date(t * 1000), today = new Date();
|
||||
const start = (x) => new Date(x.getFullYear(), x.getMonth(), x.getDate()).getTime();
|
||||
const diff = Math.round((start(today) - start(d)) / 86400000);
|
||||
if (diff === 0) return 'Heute';
|
||||
if (diff === 1) return 'Gestern';
|
||||
return d.toLocaleDateString('de-DE', { weekday: 'long', day: 'numeric', month: 'long' });
|
||||
}
|
||||
|
||||
function render() {
|
||||
if (!DATA) return;
|
||||
const names = (DATA.printers || []).map((p) => p.name);
|
||||
const fn = FILTERS.find((f) => f[0] === FILTER)[2];
|
||||
const groups = [];
|
||||
for (const e of (DATA.log || []).filter(fn)) {
|
||||
const day = dayLabel(e.time);
|
||||
if (!groups.length || groups[groups.length - 1].day !== day) groups.push({ day, items: [] });
|
||||
groups[groups.length - 1].items.push(e);
|
||||
}
|
||||
$('list').innerHTML = groups.map((g) => `<div class="label day">${esc(g.day)}</div>
|
||||
<div class="group entries">${g.items.map((e) => {
|
||||
const [ic, bg, fg] = logIcon(e), [pn, msg] = splitPrinter(e.msg, names);
|
||||
return `<div class="entry"><div class="icon-badge" style="background:${bg};color:${fg}">${ic}</div>
|
||||
<div style="flex:1;min-width:0"><div class="msg">${esc(msg)}</div>
|
||||
<div class="meta">${esc(pn)} · <span class="mono">${fmtClock(e.time, true)}</span>${e.level === 'dry' ? ' · Dry-Run' : ''}</div></div></div>`;
|
||||
}).join('')}</div>`).join('') || '<div class="card muted">Keine Einträge</div>';
|
||||
}
|
||||
|
||||
async function load() {
|
||||
try { DATA = await api('GET', '/api/status'); } catch (e) { $('list').innerHTML = `<div class="banner err">${esc(e.message)}</div>`; return; }
|
||||
setMode(DATA.dry_run);
|
||||
setPoll(DATA.last_poll);
|
||||
render();
|
||||
}
|
||||
renderChips();
|
||||
load();
|
||||
setInterval(() => { if (!document.hidden) load(); }, 5000);
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in new issue
Block a user