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
+223
-71
@@ -2,98 +2,223 @@
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>{% block title %}CC2 Bridge{% endblock %}</title>
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-title" content="CC2 Bridge">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
||||
<meta name="theme-color" content="#EDEFF2" media="(prefers-color-scheme: light)">
|
||||
<meta name="theme-color" content="#0E1116" media="(prefers-color-scheme: dark)">
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<link rel="manifest" href="/manifest.webmanifest">
|
||||
<link rel="icon" href="/static/icon.svg" type="image/svg+xml">
|
||||
<link rel="apple-touch-icon" href="/static/icon-180.png">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<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;
|
||||
--bg: #EDEFF2; --surface: #FFFFFF; --surface2: #F6F7F9; --ink: #14181F; --muted: #5B6472; --faint: #9AA3B0;
|
||||
--line: #DCE0E6; --line2: #EDEFF2; --track: #E4E7EC;
|
||||
--accent: #2F5BEA; --accent-ink: #FFFFFF; --accent-bg: #E6ECFD;
|
||||
--ok: #17803D; --ok-bg: #E3F4E9; --warn: #9A5B00; --warn-bg: #FFF1DC; --err: #B42318; --err-bg: #FDE7E5; --err-line: #F3C6C1;
|
||||
--hero: #14181F; --hero-ink: #FFFFFF; --hero-muted: #A9B3C2; --hero-track: #2B323D; --hero-accent: #6E8BFF; --hero-line: #3A4250;
|
||||
--tabbar: rgba(255, 255, 255, .92);
|
||||
--radius: 16px; --radius-sm: 10px;
|
||||
--font: Manrope, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
||||
--mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
color-scheme: light;
|
||||
}
|
||||
@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;
|
||||
--bg: #0E1116; --surface: #171B22; --surface2: #1E232C; --ink: #E8ECF2; --muted: #9AA4B2; --faint: #6B7482;
|
||||
--line: #2A303A; --line2: #222731; --track: #2A303A;
|
||||
--accent: #7C97FF; --accent-ink: #0B1230; --accent-bg: #1D2747;
|
||||
--ok: #5CC98A; --ok-bg: #13291C; --warn: #F0B55C; --warn-bg: #33270F; --err: #FF8A80; --err-bg: #3A1A18; --err-line: #5A2622;
|
||||
--hero: #1F2531; --hero-track: #333B48; --hero-line: #3F4857;
|
||||
--tabbar: rgba(23, 27, 34, .92);
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
||||
* { 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); }
|
||||
html { -webkit-text-size-adjust: 100%; }
|
||||
body { margin: 0; background: var(--bg); color: var(--ink); font: 15px/1.45 var(--font); -webkit-font-smoothing: antialiased;
|
||||
-webkit-tap-highlight-color: transparent; }
|
||||
a { color: var(--accent); }
|
||||
|
||||
/* top bar (wide screens) */
|
||||
.top { background: var(--surface); border-bottom: 1px solid var(--line); padding-top: env(safe-area-inset-top); }
|
||||
.top .in { max-width: 1240px; margin: 0 auto; padding: 0 24px; min-height: 64px; display: flex; align-items: center; gap: 28px; flex-wrap: wrap; }
|
||||
.brand { display: flex; align-items: center; gap: 10px; font-weight: 800; font-size: 17px; letter-spacing: -.01em; color: var(--ink); text-decoration: none; }
|
||||
.brand img { width: 30px; height: 30px; border-radius: 8px; }
|
||||
.top nav { display: flex; gap: 4px; flex-wrap: wrap; }
|
||||
.top nav a { text-decoration: none; color: var(--muted); padding: 8px 14px; border-radius: var(--radius-sm); font-weight: 600; }
|
||||
.top nav a:hover { color: var(--ink); }
|
||||
.top nav a.on { color: var(--ink); background: var(--bg); font-weight: 700; }
|
||||
.top .right { margin-left: auto; display: flex; align-items: center; gap: 14px; }
|
||||
.poll { font-size: 13px; color: var(--muted); }
|
||||
|
||||
/* mode pill */
|
||||
.mode { display: inline-flex; align-items: center; gap: 7px; font-weight: 700; font-size: 13px; padding: 6px 12px; border-radius: 99px;
|
||||
text-decoration: none; white-space: nowrap; }
|
||||
.mode::before { content: ""; width: 8px; height: 8px; border-radius: 99px; background: currentColor; }
|
||||
.mode.dry { background: var(--accent-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)); }
|
||||
.mode:empty { display: none; }
|
||||
|
||||
main { max-width: 1240px; margin: 0 auto; padding: 28px 24px 56px; }
|
||||
.pagehead { display: none; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 14px; }
|
||||
.pagehead h1 { margin: 0; font-size: 32px; font-weight: 800; letter-spacing: -.02em; line-height: 1.15; }
|
||||
h2 { font-size: 20px; font-weight: 800; letter-spacing: -.01em; margin: 0; }
|
||||
h3 { font-size: 15px; font-weight: 800; margin: 0; }
|
||||
|
||||
/* tab bar (phones) */
|
||||
.tabbar { display: none; position: fixed; left: 0; right: 0; bottom: 0; z-index: 20; background: var(--tabbar);
|
||||
-webkit-backdrop-filter: saturate(180%) blur(20px); backdrop-filter: saturate(180%) blur(20px);
|
||||
border-top: 1px solid var(--line); padding: 6px 8px calc(6px + env(safe-area-inset-bottom));
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
||||
.tabbar a { text-decoration: none; color: var(--muted); display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
gap: 3px; min-height: 48px; font-size: 11px; font-weight: 700; }
|
||||
.tabbar a.on { color: var(--accent); }
|
||||
.tabbar svg { width: 24px; height: 24px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
|
||||
|
||||
@media (max-width: 759px) {
|
||||
.top { display: none; }
|
||||
.tabbar { display: grid; }
|
||||
main { padding: calc(14px + env(safe-area-inset-top)) max(16px, env(safe-area-inset-right)) calc(96px + env(safe-area-inset-bottom)) max(16px, env(safe-area-inset-left)); }
|
||||
.pagehead { display: flex; }
|
||||
.desktop-only { display: none !important; }
|
||||
}
|
||||
@media (min-width: 760px) { .phone-only { display: none !important; } }
|
||||
|
||||
/* building blocks */
|
||||
.card { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); padding: 16px; }
|
||||
@media (max-width: 759px) { .card { border: 0; } }
|
||||
.stack { display: flex; flex-direction: column; gap: 14px; }
|
||||
.row { display: flex; gap: 12px; flex-wrap: wrap; align-items: center; }
|
||||
.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; }
|
||||
.mono { font-family: var(--mono); }
|
||||
.label { font-size: 12px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
|
||||
.section-label { margin: 22px 4px 8px; }
|
||||
.chip { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 700; padding: 3px 9px; border-radius: 99px;
|
||||
background: var(--bg); color: var(--muted); white-space: nowrap; }
|
||||
.chip.dot::before { content: ""; width: 7px; height: 7px; border-radius: 99px; background: currentColor; }
|
||||
.chip.ok { background: var(--ok-bg); color: var(--ok); }
|
||||
.chip.warn { background: var(--warn-bg); color: var(--warn); }
|
||||
.chip.err { background: var(--err-bg); color: var(--err); }
|
||||
.chip.accent { background: var(--accent-bg); color: var(--accent); }
|
||||
.banner { border-radius: 14px; padding: 12px 14px; display: flex; gap: 12px; align-items: center; flex-wrap: wrap; font-size: 14px; font-weight: 600; }
|
||||
.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); }
|
||||
.banner.dry { background: var(--accent-bg); color: var(--accent); }
|
||||
.banner a { color: inherit; }
|
||||
|
||||
button, .btn { font: inherit; font-size: 14px; font-weight: 700; min-height: 40px; padding: 0 14px; border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--line); background: var(--surface); color: var(--ink); cursor: pointer; display: inline-flex; align-items: center;
|
||||
justify-content: center; gap: 8px; text-decoration: none; }
|
||||
button:hover, .btn:hover { border-color: var(--faint); }
|
||||
button.primary { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); }
|
||||
button.danger { color: var(--err); border-color: var(--err-line); }
|
||||
button.plain { border: 0; background: transparent; color: var(--accent); padding: 0; min-height: 44px; }
|
||||
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; }
|
||||
button:focus-visible, a:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-visible, summary:focus-visible {
|
||||
outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||
|
||||
input, select, textarea { font: inherit; font-size: 16px; color: var(--ink); background: var(--surface2); border: 1px solid var(--line);
|
||||
border-radius: var(--radius-sm); padding: 0 12px; min-height: 44px; width: 100%; }
|
||||
textarea { padding: 10px 12px; font-family: var(--mono); font-size: 13px; }
|
||||
input[type=color] { padding: 4px; width: 52px; flex: 0 0 52px; }
|
||||
.field { display: flex; flex-direction: column; gap: 5px; }
|
||||
.field > label, .flabel { font-size: 13px; color: var(--muted); font-weight: 600; }
|
||||
.fields { display: grid; gap: 12px; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); }
|
||||
|
||||
/* iOS-style switch */
|
||||
.switch { position: relative; display: inline-flex; flex: 0 0 auto; }
|
||||
.switch input { position: absolute; inset: 0; opacity: 0; margin: 0; min-height: 0; cursor: pointer; }
|
||||
.switch span { width: 52px; height: 32px; border-radius: 99px; background: var(--track); transition: background .2s; position: relative; }
|
||||
.switch span::after { content: ""; position: absolute; top: 3px; left: 3px; width: 26px; height: 26px; border-radius: 99px; background: #fff;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, .25); transition: transform .2s; }
|
||||
.switch input:checked + span { background: var(--ok); }
|
||||
.switch.danger input:checked + span { background: var(--err); }
|
||||
.switch input:checked + span::after { transform: translateX(20px); }
|
||||
.switch input:focus-visible + span { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||
|
||||
/* grouped lists */
|
||||
.group { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; }
|
||||
@media (max-width: 759px) { .group { border: 0; } }
|
||||
.item { display: flex; align-items: center; gap: 12px; padding: 12px 14px; min-height: 56px; border-top: 1px solid var(--line2); }
|
||||
.item:first-child { border-top: 0; }
|
||||
.item .grow { flex: 1; min-width: 0; }
|
||||
.item .title { font-weight: 800; }
|
||||
.item .sub { font-size: 13px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.chev { width: 8px; height: 14px; flex: 0 0 8px; fill: none; stroke: var(--faint); stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
|
||||
button.item { width: 100%; text-align: left; border: 0; border-top: 1px solid var(--line2); border-radius: 0; background: transparent;
|
||||
font-weight: inherit; font-size: inherit; color: inherit; justify-content: flex-start; }
|
||||
button.item:first-child { border-top: 0; }
|
||||
button.item:hover { background: var(--surface2); }
|
||||
|
||||
.swatch { display: inline-block; width: 14px; height: 14px; border-radius: 99px; border: 1px solid var(--line); vertical-align: -2px; }
|
||||
.ring { border-radius: 99px; display: flex; align-items: center; justify-content: center; flex: 0 0 auto; box-shadow: inset 0 0 0 1px var(--line); }
|
||||
.ring > div { border-radius: 99px; background: var(--surface); display: flex; flex-direction: column; align-items: center; justify-content: center; }
|
||||
.icon-badge { width: 32px; height: 32px; flex: 0 0 32px; border-radius: 10px; display: flex; align-items: center; justify-content: center;
|
||||
font-weight: 800; font-size: 14px; background: var(--bg); color: var(--ink); }
|
||||
.icon-badge svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round; }
|
||||
|
||||
code { font-family: var(--mono); font-size: 13px; background: var(--surface2); padding: 1px 6px; border-radius: 6px; overflow-wrap: anywhere; }
|
||||
pre { font-family: var(--mono); font-size: 12px; background: var(--surface2); padding: 12px; border-radius: var(--radius-sm); overflow-x: auto;
|
||||
white-space: pre-wrap; word-break: break-all; margin: 0; }
|
||||
table { width: 100%; border-collapse: collapse; font-size: 14px; }
|
||||
th, td { text-align: left; padding: 8px; border-bottom: 1px solid var(--line2); vertical-align: top; }
|
||||
th { font-size: 12px; color: var(--muted); font-weight: 700; }
|
||||
.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 { display: grid; grid-template-columns: max-content 1fr; gap: 6px 14px; 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; }
|
||||
|
||||
dialog.sheet { border: 0; padding: 0; background: var(--surface); color: var(--ink); border-radius: 20px; width: min(480px, calc(100% - 32px));
|
||||
max-height: calc(100% - 48px); box-shadow: 0 20px 60px rgba(0, 0, 0, .3); }
|
||||
dialog.sheet::backdrop { background: rgba(10, 12, 16, .45); }
|
||||
dialog.sheet .sheet-in { padding: 18px 18px 20px; display: flex; flex-direction: column; gap: 16px; }
|
||||
@media (max-width: 759px) {
|
||||
dialog.sheet { width: 100%; max-width: 100%; margin: auto 0 0; border-radius: 22px 22px 0 0; max-height: 88vh; }
|
||||
dialog.sheet .sheet-in { padding-bottom: calc(20px + env(safe-area-inset-bottom)); }
|
||||
dialog.sheet .grip { display: block; }
|
||||
}
|
||||
.grip { display: none; width: 40px; height: 5px; border-radius: 99px; background: var(--line); margin: 0 auto -6px; }
|
||||
|
||||
.toast { position: fixed; left: 50%; bottom: calc(24px + env(safe-area-inset-bottom)); transform: translateX(-50%); background: var(--ink);
|
||||
color: var(--bg); padding: 10px 16px; border-radius: 12px; font-weight: 700; font-size: 14px; opacity: 0; transition: opacity .2s;
|
||||
pointer-events: none; z-index: 50; max-width: calc(100% - 32px); }
|
||||
@media (max-width: 759px) { .toast { bottom: calc(88px + env(safe-area-inset-bottom)); } }
|
||||
.toast.show { opacity: 1; }
|
||||
</style>
|
||||
{% block head %}{% endblock %}
|
||||
</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>
|
||||
{% set tabs = [
|
||||
('/', 'status.html', 'Status', 'M12 3a9 9 0 1 0 0 18a9 9 0 1 0 0-18M12 8a4 4 0 1 0 0 8a4 4 0 1 0 0-8'),
|
||||
('/log', 'log.html', 'Protokoll', 'M8 6h12M8 12h12M8 18h12M4 6h.01M4 12h.01M4 18h.01'),
|
||||
('/tags', 'tags.html', 'Tags', 'M6 8.5a8 8 0 0 1 0 7M10 6a12 12 0 0 1 0 12M14 3.5a16 16 0 0 1 0 17'),
|
||||
('/settings', 'settings.html', 'Einstellungen', 'M4 7h10M18 7h2M4 17h4M12 17h8M16 5v4M10 15v4'),
|
||||
] %}
|
||||
<header class="top"><div class="in">
|
||||
<a class="brand" href="/"><img src="/static/icon.svg" alt="">CC2 Bridge</a>
|
||||
<nav aria-label="Hauptnavigation">
|
||||
{% for href, name, label, icon in tabs %}<a href="{{ href }}" class="{{ 'on' if active == name }}" {% if active == name %}aria-current="page"{% endif %}>{{ 'Tag-Generator' if name == 'tags.html' else label }}</a>{% endfor %}
|
||||
</nav>
|
||||
<span id="mode" class="mode"></span>
|
||||
<div class="right"><span class="poll" id="lastpoll"></span><a href="/settings" class="mode" id="mode"></a></div>
|
||||
</div></header>
|
||||
<main><div class="wrap">
|
||||
{% block content %}{% endblock %}
|
||||
</div></main>
|
||||
<div id="toast" class="toast"></div>
|
||||
<main>
|
||||
<div class="pagehead"><h1>{% block heading %}CC2 Bridge{% endblock %}</h1><a href="/settings" class="mode" id="mode2"></a></div>
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
<nav class="tabbar" aria-label="Hauptnavigation">
|
||||
{% for href, name, label, icon in tabs %}<a href="{{ href }}" class="{{ 'on' if active == name }}" {% if active == name %}aria-current="page"{% endif %}><svg viewBox="0 0 24 24" aria-hidden="true"><path d="{{ icon }}"/></svg>{{ label }}</a>{% endfor %}
|
||||
</nav>
|
||||
<div id="toast" class="toast" role="status" aria-live="polite"></div>
|
||||
<script>
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const esc = (v) => String(v ?? '').replace(/[&<>"']/g, (c) => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||||
@@ -119,11 +244,38 @@ async function copyText(text) {
|
||||
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');
|
||||
function fmtClock(t, sec) {
|
||||
return t ? new Date(t * 1000).toLocaleTimeString('de-DE', sec ? {} : { hour: '2-digit', minute: '2-digit' }) : '–';
|
||||
}
|
||||
function fmtNum(v, d = 0) { return Number(v).toLocaleString('de-DE', { minimumFractionDigits: d, maximumFractionDigits: d }); }
|
||||
function setMode(dry) {
|
||||
for (const id of ['mode', 'mode2']) {
|
||||
const m = $(id);
|
||||
m.textContent = dry ? 'Dry-Run' : 'Schreiben aktiv';
|
||||
m.className = 'mode ' + (dry ? 'dry' : 'live');
|
||||
}
|
||||
}
|
||||
function setPoll(t) { $('lastpoll').textContent = t ? 'Abgefragt ' + fmtClock(t, true) : ''; }
|
||||
function logIcon(e) {
|
||||
if (e.level === 'error') return ['!', 'var(--err-bg)', 'var(--err)'];
|
||||
if (e.level === 'warn') return ['!', 'var(--warn-bg)', 'var(--warn)'];
|
||||
if (e.level === 'dry') return ['D', 'var(--accent-bg)', 'var(--accent)'];
|
||||
if (/gebucht/.test(e.msg)) return ['g', 'var(--ok-bg)', 'var(--ok)'];
|
||||
if (/fragt nach/.test(e.msg)) return ['↻', 'var(--warn-bg)', 'var(--warn)'];
|
||||
if (/zugeordnet/.test(e.msg)) return ['+', 'var(--accent-bg)', 'var(--accent)'];
|
||||
return ['✓', 'var(--bg)', 'var(--ink)'];
|
||||
}
|
||||
function splitPrinter(msg, names) {
|
||||
// "erledigt: CC2 Links: in CC2 Control …" -> ["CC2 Links", "In CC2 Control …"]; the check mark already says "erledigt".
|
||||
let pre = '';
|
||||
if (msg.startsWith('erledigt: ')) msg = msg.slice(10);
|
||||
else if (msg.startsWith('würde ')) { pre = 'würde '; msg = msg.slice(6); }
|
||||
let printer = 'Bridge';
|
||||
for (const n of names) if (msg.startsWith(n + ': ')) { printer = n; msg = msg.slice(n.length + 2); break; }
|
||||
msg = pre + msg;
|
||||
return [printer, msg.charAt(0).toUpperCase() + msg.slice(1)];
|
||||
}
|
||||
const CHEV = '<svg class="chev" viewBox="0 0 8 14" aria-hidden="true"><path d="M1 1l6 6-6 6"/></svg>';
|
||||
api('GET', '/api/settings').then((s) => setMode(s.dry_run)).catch(() => {});
|
||||
</script>
|
||||
{% block script %}{% endblock %}
|
||||
|
||||
Reference in new issue
Block a user