Files
KX-Bridge-Release/web/themes/default/index.html
Phil Merricks b39577ad4d feat: 2-column dashboard layout toggle + local timelapse
2-column layout: settings toggle switches dashboard between 1-col
(current behaviour) and 2-col (Camera|Progress, Temps|AMS side by
side, motion cards in 2×2). Preference stored in localStorage only
(per-browser, not printer config). Mobile always stays 1-col via CSS.

Local timelapse: new TimelapseSpool class captures JPEG frames from
the camera stream during a print at a configurable interval (default
4s) and encodes them to MP4 with ffmpeg on print end. Stored in
data/timelapses/, indexed in SQLite. New /kx/timelapses API + browser
tab in the file store panel with download and delete. Also wires the
undocumented printer-native timelapse MQTT field as an experimental
opt-in setting.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 22:20:35 +01:00

659 lines
47 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" data-theme="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>KX-Bridge</title>
<link rel="stylesheet" href="/kx/ui/style.css">
<body>
<div id="conn-error-banner" style="display:none;background:#c0392b;color:#fff;padding:10px 18px;font-size:14px;text-align:center;position:sticky;top:0;z-index:999;"></div>
<div id="file-ready-banner" style="display:none;background:#1a6e3c;color:#fff;padding:10px 18px;font-size:14px;text-align:center;position:sticky;top:0;z-index:998;display:none;align-items:center;justify-content:center;gap:12px;flex-wrap:wrap">
<span>📄 <span id="file-ready-name"></span></span>
<button id="file-ready-btn" onclick="startReadyFile()"
style="padding:5px 16px;background:#fff;color:#1a6e3c;border:none;border-radius:6px;font-weight:700;cursor:pointer;font-size:13px"></button>
<button id="file-slots-btn" onclick="startReadyFileWithSlots()"
style="padding:5px 16px;background:rgba(255,255,255,0.15);color:#fff;border:1px solid rgba(255,255,255,0.5);border-radius:6px;font-weight:700;cursor:pointer;font-size:13px"></button>
<button id="file-cancel-btn" onclick="cancelReadyFile()"
style="padding:5px 16px;background:rgba(255,255,255,0.15);color:#fff;border:1px solid rgba(255,255,255,0.5);border-radius:6px;font-weight:700;cursor:pointer;font-size:13px"></button>
</div>
<header>
<div class="logo">⬡ KX-Bridge</div>
<div style="flex:1"></div>
<div id="printer-dropdown-wrap" style="display:none;position:relative">
<button id="printer-dropdown-btn" onclick="togglePrinterDropdown()" style="background:var(--raised);border:1px solid var(--border);border-radius:6px;padding:4px 10px;color:var(--txt);cursor:pointer;font-size:13px;display:flex;align-items:center;gap:6px">
<span id="h-pname">Anycubic Kobra X</span><span style="opacity:.5"></span>
</button>
<div id="printer-dropdown-menu" style="display:none;position:absolute;top:calc(100% + 4px);right:0;background:var(--card);border:1px solid var(--border);border-radius:8px;min-width:200px;z-index:200;box-shadow:0 4px 16px #0006;overflow:hidden">
</div>
</div>
<div id="h-pname-single" class="hname">Anycubic Kobra X</div>
<span id="h-version" style="font-size:11px;opacity:.5;margin-left:6px"></span>
<div class="hbadge" id="h-badge"><span class="dot"></span><span id="h-state">Standby</span></div>
<button class="theme-btn" onclick="toggleTheme()">☀ / ☾</button>
<div style="display:flex;align-items:center;gap:6px">
<span aria-hidden="true" style="font-size:15px;line-height:1;opacity:.85">🌐</span>
<select class="theme-btn" id="lang-select" onchange="setLanguageFromSelect()" style="padding:6px 10px">
<option value="de">Deutsch</option>
<option value="en">English</option>
<option value="es">Espanol</option>
<option value="zh-cn">中文(简体)</option>
</select>
</div>
<button class="theme-btn" onclick="openSettings()" id="settings-btn" title="Einstellungen"></button>
<button class="conn-btn disconnected" id="conn-btn" onclick="toggleConnection()">⚡ Verbinden</button>
</header>
<!-- ═══ SETTINGS MODAL ═══ -->
<div class="modal-overlay" id="settings-modal" onclick="if(event.target===this)closeSettings()">
<div class="modal-box">
<div class="modal-header">
<span class="modal-title" id="modal-title-settings">Einstellungen</span>
<button class="modal-close" onclick="closeSettings()"></button>
</div>
<div>
<div class="modal-field" style="margin-bottom:12px">
<label id="lbl-printer-name" style="font-weight:600">Drucker-Name</label>
<input type="text" id="s-printer-name" placeholder="z.B. Kobra X Links">
</div>
<div class="modal-section" id="modal-sec-connection">Verbindung</div>
<div class="modal-field">
<label id="lbl-printer-ip">Drucker-IP</label>
<input type="text" id="s-printer-ip" placeholder="192.168.x.x">
<small id="lbl-ip-hint" style="color:#f80;display:none"></small>
</div>
<div class="modal-field">
<label id="lbl-mqtt-port">MQTT-Port</label>
<input type="number" id="s-mqtt-port" placeholder="9883">
</div>
<div class="modal-field">
<label id="lbl-username">MQTT-Benutzername</label>
<input type="text" id="s-username" placeholder="userXXXXXXXX" autocomplete="new-password">
</div>
<div class="modal-field">
<label id="lbl-password">MQTT-Passwort</label>
<input type="password" id="s-password" autocomplete="new-password">
</div>
<div class="modal-field">
<label id="lbl-device-id">Device-ID</label>
<input type="text" id="s-device-id" placeholder="32 Hex-Zeichen">
</div>
<div class="modal-field">
<label id="lbl-mode-id">Mode-ID</label>
<input type="text" id="s-mode-id" placeholder="20030">
</div>
</div>
<div>
<div class="modal-section" id="modal-sec-print">Druckeinstellungen</div>
<div class="modal-field">
<label id="lbl-default-slot">Standard-Slot (Einfarbdruck)</label>
<select id="s-default-slot">
<option value="auto" id="opt-slot-auto">Auto (alle belegten Slots)</option>
<option value="0" id="opt-slot-0">Slot 1</option>
<option value="1" id="opt-slot-1">Slot 2</option>
<option value="2" id="opt-slot-2">Slot 3</option>
<option value="3" id="opt-slot-3">Slot 4</option>
</select>
</div>
<div class="modal-field" style="flex-direction:row;align-items:center;gap:10px">
<input type="checkbox" id="s-auto-leveling" style="width:auto;margin:0">
<label id="lbl-auto-leveling" style="margin:0;cursor:pointer" for="s-auto-leveling">Auto-Leveling vor Druck</label>
</div>
<div class="modal-field" style="flex-direction:row;align-items:center;gap:10px">
<input type="checkbox" id="s-vibration-compensation" style="width:auto;margin:0">
<label id="lbl-vibration-compensation" style="margin:0;cursor:pointer" for="s-vibration-compensation">Resonance Compensation</label>
</div>
<div class="modal-field" style="flex-direction:row;align-items:center;gap:10px">
<input type="checkbox" id="s-flow-calibration" style="width:auto;margin:0">
<label id="lbl-flow-calibration" style="margin:0;cursor:pointer" for="s-flow-calibration">Flow Calibration</label>
</div>
<div class="modal-field" style="flex-direction:row;align-items:center;gap:10px">
<input type="checkbox" id="s-camera-on-print" style="width:auto;margin:0">
<label id="lbl-camera-on-print" style="margin:0;cursor:pointer" for="s-camera-on-print">Kamera bei Druckstart einschalten</label>
</div>
<div class="modal-field" style="flex-direction:row;align-items:center;gap:10px">
<input type="checkbox" id="s-web-upload-warning" style="width:auto;margin:0">
<label id="lbl-web-upload-warning" style="margin:0;cursor:pointer" for="s-web-upload-warning">Warnung bei Web-Upload-Druck anzeigen</label>
</div>
<div class="modal-field" style="flex-direction:row;align-items:center;gap:10px">
<input type="checkbox" id="s-timelapse-local" style="width:auto;margin:0" onchange="_toggleTlInterval()">
<label id="lbl-timelapse-local" style="margin:0;cursor:pointer" for="s-timelapse-local">Lokales Timelapse während Druck</label>
</div>
<div class="modal-field" id="timelapse-interval-row" style="display:none">
<label id="lbl-timelapse-interval" style="font-size:12px;color:var(--txt2)">Aufnahme-Intervall (Sekunden)</label>
<input type="number" id="s-timelapse-interval" min="1" max="60" value="4" style="width:80px">
</div>
<div class="modal-field" style="flex-direction:row;align-items:center;gap:10px">
<input type="checkbox" id="s-timelapse-printer" style="width:auto;margin:0">
<label id="lbl-timelapse-printer" style="margin:0;cursor:pointer" for="s-timelapse-printer">Drucker-Timelapse aktivieren (experimentell)</label>
</div>
</div>
<div>
<div class="modal-section" id="modal-sec-layout">Layout</div>
<div class="modal-field" style="flex-direction:row;align-items:center;gap:10px;flex-wrap:wrap">
<label id="lbl-layout" style="margin:0;font-size:12px;color:var(--txt2)">Dashboard-Layout</label>
<select id="s-layout" style="width:auto;margin:0">
<option value="1col" id="opt-layout-1col">1 Spalte</option>
<option value="2col" id="opt-layout-2col">2 Spalten</option>
</select>
</div>
</div>
<div>
<div class="modal-section" id="modal-sec-poll">Poll-Intervall</div>
<div class="poll-btns">
<button class="poll-btn" onclick="setPoll(1000)" id="poll-1">1s</button>
<button class="poll-btn active" onclick="setPoll(2000)" id="poll-2">2s</button>
<button class="poll-btn" onclick="setPoll(5000)" id="poll-5">5s</button>
</div>
</div>
<div>
<div class="modal-section" id="modal-sec-version">Version</div>
<div class="update-row">
<span id="s-version-label" style="font-size:13px;color:var(--txt)"></span>
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="checkUpdate()" id="btn-update-check">🔄 <span id="lbl-update-check">Auf Updates prüfen</span></button>
</div>
<div class="update-status" id="update-status" style="margin-top:6px"></div>
<button class="btn btn-sm btn-accent" id="btn-update-apply" style="display:none;margin-top:8px" onclick="applyUpdate()">
<span id="lbl-update-apply">Jetzt installieren</span>
</button>
<div id="update-changelog" style="display:none;margin-top:10px;background:var(--raised);border-radius:6px;padding:10px;font-size:11px;font-family:var(--mono);color:var(--txt2);white-space:pre-wrap;max-height:180px;overflow-y:auto;line-height:1.6"></div>
</div>
<button class="modal-save" onclick="saveSettings()" id="btn-save-settings">Speichern &amp; Neustart</button>
</div>
</div>
<!-- ═══ AMS SLOT EDIT DIALOG ═══ -->
<div class="modal-overlay" id="slot-edit-modal" onclick="if(event.target===this)closeSlotEdit()">
<div class="modal-box" style="max-width:340px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
<span class="modal-title" id="slot-edit-title"></span>
<button onclick="closeSlotEdit()" style="background:none;border:none;color:var(--txt2);font-size:20px;cursor:pointer;line-height:1"></button>
</div>
<div style="display:flex;align-items:center;gap:16px;margin-bottom:20px">
<div id="slot-edit-preview" style="width:56px;height:56px;border-radius:50%;border:3px solid rgba(255,255,255,.2);flex-shrink:0"></div>
<div style="flex:1">
<div style="font-size:11px;color:var(--txt2);margin-bottom:4px" id="lbl-slot-color"></div>
<input type="color" id="slot-edit-color"
oninput="document.getElementById('slot-edit-preview').style.background=this.value"
style="width:100%;height:36px;border:1px solid var(--border);border-radius:6px;background:var(--raised);cursor:pointer;padding:2px">
</div>
</div>
<div style="margin-bottom:20px">
<div style="font-size:11px;color:var(--txt2);margin-bottom:6px" id="lbl-slot-material"></div>
<div style="display:flex;flex-wrap:wrap;gap:6px" id="slot-mat-btns">
</div>
<input type="text" id="slot-edit-mat"
oninput="highlightMatBtn(this.value)"
style="margin-top:8px;width:100%;padding:6px 10px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);font-size:13px;box-sizing:border-box">
</div>
<!-- Orca-Filament-Profil-Override (für AMS-Sync) -->
<div style="margin-bottom:20px">
<div style="font-size:11px;color:var(--txt2);margin-bottom:6px" id="lbl-slot-profile"></div>
<select id="slot-edit-profile"
style="width:100%;padding:6px 10px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);font-size:13px;box-sizing:border-box">
<option value="" id="slot-profile-default-opt"></option>
</select>
<div style="font-size:11px;color:var(--txt2);margin-top:4px" id="slot-profile-hint"></div>
</div>
<button class="btn" id="btn-slot-edit-feed" style="width:100%;margin-bottom:8px" onclick="slotEditFeed()"></button>
<button class="modal-save" id="btn-slot-edit-save" onclick="saveSlotEdit()"></button>
</div>
</div>
<div class="layout">
<nav class="sidebar">
<button class="nav-btn active" onclick="showPanel('dashboard')" id="nb-dashboard">
<span class="nav-icon"></span><span class="nav-text">Dashboard</span></button>
<button class="nav-btn" onclick="showPanel('printers');loadPrinterTab()" id="nb-printers">
<span class="nav-icon">🖨</span><span class="nav-text">Drucker</span></button>
<button class="nav-btn" onclick="showPanel('store');loadStore()" id="nb-store">
<span class="nav-icon">🗂</span><span class="nav-text">Browser</span></button>
<button class="nav-btn" onclick="showPanel('console');clearLogBadge()" id="nb-console">
<span class="nav-icon"></span><span class="nav-text">Konsole</span><span id="log-badge" style="display:none;margin-left:4px;background:var(--err);color:#fff;border-radius:10px;font-size:10px;padding:1px 5px;font-weight:700"></span></button>
</nav>
<main>
<!-- ═══ DASHBOARD ═══ -->
<div class="panel active" id="panel-dashboard">
<div class="grid">
<!-- Kamera -->
<div class="card" id="d-cam-card" style="grid-column:1/-1">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px">
<div class="card-title" style="margin-bottom:0"><span>📷</span> <span id="d-card-cam">Kamera</span></div>
<div style="display:flex;align-items:center;gap:10px">
<span id="d-lbl-light" style="font-size:12px;color:var(--txt2)">💡 Licht</span>
<label class="toggle">
<input type="checkbox" id="d-light-toggle" onchange="setLight()">
<span class="toggle-track"></span>
<span class="toggle-thumb"></span>
</label>
</div>
</div>
<div class="cam-wrap" id="cam-wrap">
<div class="cam-placeholder" id="cam-placeholder"><span id="cam-placeholder-txt">📷 Kamera nicht gestartet</span></div>
<div class="cam-spinner" id="cam-spinner"></div>
<img id="cam-img" style="display:none;width:100%;height:auto" alt="Kamera">
<div class="cam-overlay" id="cam-overlay" style="display:none">
<div style="font-size:12px;color:#fff" id="cam-fname"></div>
</div>
<button class="cam-toggle" onclick="toggleCam()" id="cam-toggle-btn">▶ Kamera</button>
</div>
</div>
<!-- Fortschritt -->
<div class="card" id="d-progress-card" style="grid-column:1/-1">
<div class="card-title"><span></span> <span id="d-card-progress">Fortschritt</span></div>
<img id="d-thumbnail" src="" alt="" style="display:none;width:100%;max-height:160px;object-fit:contain;border-radius:8px;background:#111;margin-bottom:10px">
<div class="pct-big"><span id="d-pct">0</span><small>%</small></div>
<div style="display:flex;align-items:center;gap:10px;margin:8px 0">
<div class="progress-bar" style="flex:1;margin:0"><div class="progress-fill" id="d-pbar" style="width:0%"></div></div>
<div class="time-block" style="padding:6px 10px;min-width:72px;text-align:center;flex-shrink:0">
<div class="time-label" id="d-lbl-layers"></div>
<div class="time-val" style="font-size:16px" id="d-layers"></div>
</div>
</div>
<div class="time-grid">
<div class="time-block">
<div class="time-label" id="d-lbl-elapsed"></div>
<div class="time-val" id="d-elapsed"></div>
</div>
<div class="time-block" id="d-slicer-row" style="display:none">
<div class="time-label" id="d-slicer-label"></div>
<div class="time-val" id="d-slicer-time"></div>
</div>
<div class="time-block" style="color:var(--acc)">
<div class="time-label" id="d-lbl-remain"></div>
<div class="time-val" id="d-remain" style="color:var(--acc)"></div>
</div>
</div>
<div class="fname" id="d-fname" title="" style="margin-top:6px"></div>
<div class="ctrl-btns" id="d-ctrl-btns" style="margin-top:12px;display:none">
<button class="btn btn-pause btn-sm" id="d-btn-pause" onclick="togglePauseResume()">⏸ Pause</button>
<button class="btn btn-skip btn-sm" id="d-btn-skip" onclick="openSkipDialog()" style="display:none"><span id="d-btn-skip-label">Objekte</span></button>
<button class="btn btn-cancel btn-sm" id="d-btn-cancel" onclick="confirmCancel()">✕ Stopp</button>
</div>
</div>
<!-- Temperatursteuerung + Verlauf -->
<div class="card" id="d-temps-card" style="grid-column:1/-1">
<div class="card-title"><span></span> <span id="d-card-temps">Temperaturen</span></div>
<div class="temp-card-inner">
<div class="temp-block">
<div class="temp-label" id="d-lbl-nozzle">Nozzle</div>
<div class="temp-row">
<div class="temp-val" id="d-nt"></div>
<div class="temp-unit">°C</div>
</div>
<div class="temp-target"><span id="d-nt-t">0</span>°C</div>
<div class="progress-bar" style="margin:8px 0 0">
<div class="progress-fill" id="d-ntbar" style="width:0%;background:linear-gradient(90deg,var(--accent2),#ffb020)"></div>
</div>
<div class="temp-edit" style="margin-top:10px">
<input type="number" class="temp-input" id="p-nozzle-inp" placeholder="Ziel" min="0" max="300" style="flex:1">
<button class="btn btn-sm btn-accent" onclick="setNozzle()"><span class="lbl-set">Set</span></button>
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="document.getElementById('p-nozzle-inp').value=0;setNozzle()"><span class="lbl-off">Aus</span></button>
</div>
</div>
<div class="temp-block">
<div class="temp-label" id="d-lbl-bed">Bett</div>
<div class="temp-row">
<div class="temp-val" id="d-bt"></div>
<div class="temp-unit">°C</div>
</div>
<div class="temp-target"><span id="d-bt-t">0</span>°C</div>
<div class="progress-bar" style="margin:8px 0 0">
<div class="progress-fill" id="d-btbar" style="width:0%;background:linear-gradient(90deg,#ff6b35,var(--warn))"></div>
</div>
<div class="temp-edit" style="margin-top:10px">
<input type="number" class="temp-input" id="p-bed-inp" placeholder="Ziel" min="0" max="120" style="flex:1">
<button class="btn btn-sm btn-accent" onclick="setBed()"><span class="lbl-set">Set</span></button>
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="document.getElementById('p-bed-inp').value=0;setBed()"><span class="lbl-off">Aus</span></button>
</div>
</div>
</div>
<div style="margin-top:14px">
<div style="font-size:10px;color:var(--txt2);margin-bottom:4px" id="d-chart-label">Verlauf (letzte 60 Messungen)</div>
<canvas id="d-chart" width="800" height="120" style="width:100%;height:120px;background:var(--raised);border-radius:8px"></canvas>
</div>
</div>
<!-- Achsensteuerung -->
<div class="card">
<div class="card-title"><span></span> <span id="ptitle-motion-xy">XY-Achsen</span></div>
<div class="joypad">
<div></div>
<button class="joy" onclick="move(1,1,getStep())" title="Y+"></button>
<div></div>
<button class="joy" onclick="move(0,-1,getStep())" title="X"></button>
<button class="joy home" onclick="homeAll()" title="Home All"></button>
<button class="joy" onclick="move(0,1,getStep())" title="X+"></button>
<div></div>
<button class="joy" onclick="move(1,-1,getStep())" title="Y"></button>
<div></div>
</div>
<div class="step-btns">
<button class="step-btn" onclick="setStep(this,0.1)">0.1</button>
<button class="step-btn active" onclick="setStep(this,1)">1</button>
<button class="step-btn" onclick="setStep(this,5)">5</button>
<button class="step-btn" onclick="setStep(this,10)">10 mm</button>
</div>
<div class="home-btns">
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="homeZ()"><span class="lbl-home-z">Home Z</span></button>
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="homeXY()"><span class="lbl-home-xy">Home XY</span></button>
<button class="btn btn-sm btn-accent" onclick="homeAll()"><span class="lbl-home-all">Home All</span></button>
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="disableMotors()"><span class="lbl-disable-motors">Motors Off</span></button>
</div>
</div>
<div class="card">
<div class="card-title"><span></span> <span id="ptitle-motion-z">Z-Achse</span></div>
<div class="joypad" style="grid-template-columns:52px;grid-template-rows:repeat(2,52px)">
<button class="joy" onclick="move(2,1,getStep())" title="Z+"></button>
<button class="joy" onclick="move(2,-1,getStep())" title="Z"></button>
</div>
<div style="text-align:center;margin-top:8px;font-size:12px;color:var(--txt2)"><span class="lbl-step">Schrittweite:</span> <span id="step-display">1</span> mm</div>
</div>
<!-- Print Speed -->
<div class="card">
<div class="card-title"><span>🏎</span> <span id="d-card-speed">Druckgeschwindigkeit</span></div>
<div style="display:flex;gap:8px;margin-top:4px">
<button class="spd-btn" id="d-spd-1" onclick="setSpeed(1)">
<span class="spd-icon">🐢</span>
<span id="d-spd-lbl-1">Leise</span>
</button>
<button class="spd-btn spd-active" id="d-spd-2" onclick="setSpeed(2)">
<span class="spd-icon"></span>
<span id="d-spd-lbl-2">Normal</span>
</button>
<button class="spd-btn" id="d-spd-3" onclick="setSpeed(3)">
<span class="spd-icon">🚀</span>
<span id="d-spd-lbl-3">Sport</span>
</button>
</div>
<div class="spd-bar" style="margin-top:12px">
<div class="spd-bar-fill" id="d-spd-bar" style="width:50%"></div>
</div>
</div>
<!-- Lüfter -->
<div class="card">
<div class="card-title"><span>🌀</span> <span id="d-card-lightfan">Lüfter</span></div>
<div class="slider-row">
<input type="range" class="slider" min="0" max="100" value="0" id="d-fan" oninput="document.getElementById('d-fan-val').textContent=this.value" onchange="setFan()">
<span class="slider-val" id="d-fan-val">0</span>
</div>
<div style="margin-top:12px;display:flex;gap:8px;flex-wrap:wrap">
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="quickFan(0)">Aus</button>
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="quickFan(25)">25%</button>
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="quickFan(50)">50%</button>
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="quickFan(75)">75%</button>
<button class="btn btn-sm btn-accent" onclick="quickFan(100)">100%</button>
</div>
</div>
<div id="d-ace-dry-wrap" style="display:none">
<div id="d-ace-dry-grid" style="display:contents"></div>
</div>
<!-- AMS -->
<div class="card" style="grid-column:1/-1" id="d-ams-card">
<div class="card-title"><span></span> <span id="d-card-ams">Filament</span></div>
<div class="ams-slots" id="ams-slots">
<div style="grid-column:1/-1;text-align:center;color:var(--txt2);padding:20px" id="ams-no-data">Keine AMS-Daten empfangen</div>
</div>
</div>
</div>
</div>
<!-- ═══ CONSOLE ═══ -->
<!-- ═══ DRUCKER ═══ -->
<div class="panel" id="panel-printers">
<div class="card">
<div class="card-title" style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
<span id="printers-panel-title">🖨 Drucker</span>
<div style="display:flex;gap:8px">
<button onclick="openAddPrinterDialog()" style="font-size:12px;padding:4px 12px;background:var(--accent);border:none;border-radius:6px;color:#fff;cursor:pointer;font-weight:600">+ <span id="add-printer-btn-label">Drucker hinzufügen</span></button>
<button onclick="loadPrinterTab()" style="font-size:12px;padding:4px 12px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt2);cursor:pointer"></button>
</div>
</div>
<div id="printers-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:14px"></div>
</div>
</div>
<!-- ═══ GCODE STORE ═══ -->
<div class="panel" id="panel-store">
<div class="card">
<div class="card-title" style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px">
<span id="store-panel-title">🗂 Datei-Browser</span>
<div style="display:flex;gap:6px;align-items:center">
<button id="store-tab-files" class="store-tab-btn active" onclick="switchStoreTab('files')">📁 <span id="store-tab-files-label">Dateien</span></button>
<button id="store-tab-timelapses" class="store-tab-btn" onclick="switchStoreTab('timelapses')">🎬 <span id="store-tab-tl-label">Timelapses</span></button>
<button id="store-refresh-btn" onclick="refreshStoreTab()" style="font-size:12px;padding:4px 12px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt2);cursor:pointer"></button>
</div>
</div>
<!-- Dateien-Bereich -->
<div id="store-files-section">
<div style="display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap">
<input id="store-search" type="text" placeholder="🔍 Suche…" oninput="renderStore()"
style="flex:1;min-width:140px;padding:6px 10px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);font-size:13px">
<select id="store-filter" onchange="renderStore()"
style="padding:6px 8px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);font-size:13px">
<option value="all" id="sf-all">Alle</option>
<option value="completed" id="sf-ok">✓ Erfolgreich</option>
<option value="failed" id="sf-err">✗ Fehler</option>
<option value="never" id="sf-new">Neu</option>
</select>
<select id="store-sort" onchange="renderStore()"
style="padding:6px 8px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);font-size:13px">
<option value="date_desc" id="ss-date">↓ Datum</option>
<option value="name_asc" id="ss-name">AZ Name</option>
<option value="duration_asc" id="ss-dur">⏱ Druckzeit</option>
</select>
</div>
<div id="store-upload-zone" onclick="document.getElementById('store-upload-input').click()"
ondragover="event.preventDefault();this.classList.add('drag-over')"
ondragleave="this.classList.remove('drag-over')"
ondrop="event.preventDefault();this.classList.remove('drag-over');uploadGcode(event.dataTransfer.files[0])">
<input type="file" id="store-upload-input" accept=".gcode,.bgcode"
style="display:none" onchange="uploadGcode(this.files[0]);this.value=''">
<span id="store-upload-icon"></span>
<span id="store-upload-label"><span id="store-upload-label-prefix">GCode hierher ziehen oder </span><u id="store-upload-label-browse">durchsuchen</u></span>
<span id="store-upload-status" style="display:none"></span>
</div>
<div id="store-empty" style="display:none;color:var(--txt2);text-align:center;padding:40px 0;font-size:14px">
</div>
<div id="store-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:14px"></div>
</div><!-- /store-files-section -->
<!-- Timelapse-Bereich -->
<div id="store-timelapses-section" style="display:none">
<div id="timelapse-list"><div style="text-align:center;padding:40px;color:var(--txt2)" id="timelapse-empty-msg">Keine Timelapses vorhanden</div></div>
</div>
</div>
</div>
<div class="panel" id="panel-console">
<div class="card">
<div class="card-title" style="display:flex;justify-content:space-between;align-items:center">
<span><span></span> <span id="ptitle-console">Ereignis-Log</span></span>
<a id="btn-log-dl" href="/api/log/download" download
style="font-size:12px;padding:4px 10px;background:var(--raised);border-radius:6px;color:var(--txt2);text-decoration:none">⬇ Download</a>
</div>
<div style="display:flex;gap:6px;margin-bottom:6px;flex-wrap:wrap;align-items:center">
<input id="log-filter" type="text" placeholder="Filter…"
oninput="renderLog()"
style="flex:1;min-width:120px;padding:5px 10px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);font-size:12px;font-family:var(--mono)">
<button id="btn-autoscroll" onclick="toggleAutoScroll()"
style="font-size:12px;padding:5px 10px;border-radius:6px;border:1px solid var(--border);background:var(--accent);color:#fff;cursor:pointer;white-space:nowrap">⬇ Auto</button>
<button onclick="consoleLogs=[];renderLog()"
style="font-size:12px;padding:5px 10px;border-radius:6px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">✕ Clear</button>
</div>
<div style="display:flex;gap:5px;margin-bottom:8px;flex-wrap:wrap">
<span style="font-size:11px;color:var(--txt2);align-self:center;margin-right:2px">Dir:</span>
<button class="log-dir-btn active" id="logdir-all" onclick="setLogDir('all')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer"></button>
<button class="log-dir-btn" id="logdir-rx" onclick="setLogDir('rx')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">RX</button>
<button class="log-dir-btn" id="logdir-tx" onclick="setLogDir('tx')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">TX</button>
<span style="font-size:11px;color:var(--txt2);align-self:center;margin-left:6px;margin-right:2px" id="log-lbl-level">Level:</span>
<button class="log-lvl-btn active" id="loglvl-all" onclick="setLogLevel('all')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer"></button>
<button class="log-lvl-btn" id="loglvl-err" onclick="setLogLevel('err')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">⛔ Errors</button>
<button class="log-lvl-btn" id="loglvl-warn" onclick="setLogLevel('warn')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">⚠ Warn</button>
<span style="font-size:11px;color:var(--txt2);align-self:center;margin-left:6px;margin-right:2px">Topic:</span>
<button class="log-topic-btn" data-topic="multiColorBox" onclick="setLogTopic('multiColorBox')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">AMS</button>
<button class="log-topic-btn" data-topic="print" onclick="setLogTopic('print')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">print</button>
<button class="log-topic-btn" data-topic="info" onclick="setLogTopic('info')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">info</button>
<button class="log-topic-btn" data-topic="status" onclick="setLogTopic('status')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">status</button>
</div>
<div class="console" id="console-log" style="height:calc(100vh - 260px);min-height:160px" onscroll="onLogScroll()"></div>
</div>
</div>
</main>
</div>
<nav class="bottom-nav">
<button class="bnav-btn active" onclick="showPanel('dashboard')" id="bnb-dashboard"><span class="bnav-icon"></span>Dashboard</button>
<button class="bnav-btn" onclick="showPanel('printers');loadPrinterTab()" id="bnb-printers"><span class="bnav-icon">🖨</span>Drucker</button>
<button class="bnav-btn" onclick="showPanel('store');loadStore()" id="bnb-store"><span class="bnav-icon">🗂</span>Browser</button>
<button class="bnav-btn" onclick="showPanel('console');clearLogBadge()" id="bnb-console"><span class="bnav-icon"></span>Log<span id="log-badge-bot" style="display:none;margin-left:3px;background:var(--err);color:#fff;border-radius:10px;font-size:10px;padding:1px 4px;font-weight:700"></span></button>
</nav>
<!-- Web-Upload-Verify-Dialog -->
<div class="modal-overlay" id="store-web-verify-dialog" onclick="if(event.target===this)closeStoreWebVerifyDialog()">
<div class="modal-box" style="max-width:420px;width:100%">
<div class="modal-header" style="margin-bottom:14px">
<span class="modal-title" id="store-web-verify-title">Datei verifizieren</span>
<button onclick="closeStoreWebVerifyDialog()" style="background:none;border:none;font-size:18px;cursor:pointer;color:var(--txt2)"></button>
</div>
<p id="store-web-verify-msg" style="font-size:13px;color:var(--txt);margin-bottom:12px">Bitte bestätige, dass diese Datei für den Anycubic Kobra X erstellt wurde.</p>
<div id="store-web-verify-status" style="font-size:12px;color:var(--txt2);min-height:16px;margin-bottom:8px"></div>
<div style="display:flex;gap:8px;justify-content:flex-end">
<button id="store-web-verify-abort" onclick="closeStoreWebVerifyDialog()" style="padding:8px 16px;background:var(--raised);border:1px solid var(--border);border-radius:8px;color:var(--txt);cursor:pointer">Abbrechen</button>
<button id="store-web-verify-confirm" onclick="confirmStoreWebVerify()" style="padding:8px 18px;background:var(--accent);color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:600">Bestätigen</button>
</div>
</div>
</div>
<!-- Filament-Slot-Dialog -->
<div class="modal-overlay" id="filament-dialog" onclick="if(event.target===this)closeFilamentDialog()">
<div class="modal-box" style="max-width:380px;width:100%">
<div class="modal-header" style="margin-bottom:14px">
<span class="modal-title" id="fd-title" style="font-size:14px;word-break:break-all"></span>
<button onclick="closeFilamentDialog()" style="background:none;border:none;font-size:18px;cursor:pointer;color:var(--txt2)"></button>
</div>
<p id="fd-slots-hint" style="font-size:12px;color:var(--txt2);margin-bottom:10px">GCode-Kanal → AMS-Slot zuweisen:</p>
<div id="fd-slots" style="display:flex;flex-direction:column;gap:8px;margin-bottom:16px"></div>
<div id="fd-objects-section" style="display:none;margin-bottom:16px">
<p id="fd-objects-hint" style="font-size:12px;color:var(--txt2);margin-bottom:8px">Objekte überspringen (optional):</p>
<div id="fd-objects-svg" style="display:none;background:var(--raised);border:1px solid var(--border);border-radius:8px;padding:6px;margin-bottom:8px;text-align:center"></div>
<div id="fd-objects" style="display:flex;flex-direction:column;gap:6px;max-height:140px;overflow-y:auto"></div>
</div>
<div style="display:flex;gap:8px;justify-content:flex-end">
<button id="fd-cancel" onclick="closeFilamentDialog()" style="padding:8px 16px;background:var(--raised);border:1px solid var(--border);border-radius:8px;color:var(--txt);cursor:pointer">Abbrechen</button>
<button id="fd-print" onclick="confirmFilamentPrint()" style="padding:8px 18px;background:var(--accent);color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:600">▶ Drucken</button>
</div>
</div>
</div>
<!-- Drucker-hinzufügen-Dialog -->
<div class="modal-overlay" id="add-printer-dialog" onclick="if(event.target===this)closeAddPrinterDialog()">
<div class="modal-box" style="max-width:380px;width:100%">
<div class="modal-header" style="margin-bottom:14px">
<span class="modal-title" id="apd-title">Drucker hinzufügen</span>
<button onclick="closeAddPrinterDialog()" style="background:none;border:none;font-size:18px;cursor:pointer;color:var(--txt2)"></button>
</div>
<label id="apd-lbl-ip" style="display:block;font-size:12px;color:var(--txt2);margin-bottom:4px">Drucker-IP</label>
<input type="text" id="apd-ip" placeholder="192.168.1.100" style="width:100%;box-sizing:border-box;padding:8px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);margin-bottom:10px">
<label id="apd-lbl-name" style="display:block;font-size:12px;color:var(--txt2);margin-bottom:4px">Name (optional)</label>
<input type="text" id="apd-name" placeholder="z.B. Kobra X Wohnzimmer" style="width:100%;box-sizing:border-box;padding:8px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);margin-bottom:6px">
<div id="apd-status" style="font-size:12px;margin:8px 0;min-height:16px;color:var(--txt2)"></div>
<div style="display:flex;gap:8px;justify-content:flex-end">
<button id="apd-cancel" onclick="closeAddPrinterDialog()" style="padding:8px 16px;background:var(--raised);border:1px solid var(--border);border-radius:8px;color:var(--txt);cursor:pointer">Abbrechen</button>
<button id="apd-confirm" onclick="confirmAddPrinter()" style="padding:8px 18px;background:var(--accent);color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:600">Hinzufügen</button>
</div>
</div>
</div>
<!-- Mid-Print Skip-Dialog -->
<div class="modal-overlay" id="skip-dialog" onclick="if(event.target===this)closeSkipDialog()">
<div class="modal-box" style="max-width:420px;width:100%">
<div class="modal-header" style="margin-bottom:14px">
<span class="modal-title" id="skip-title">✂ Objekte überspringen</span>
<button onclick="closeSkipDialog()" style="background:none;border:none;font-size:18px;cursor:pointer;color:var(--txt2)"></button>
</div>
<p id="skip-hint" style="font-size:12px;color:var(--txt2);margin-bottom:10px">Objekte abwählen, die nicht weiter gedruckt werden sollen:</p>
<div id="skip-svg" style="display:none;background:var(--raised);border:1px solid var(--border);border-radius:8px;padding:6px;margin-bottom:10px;text-align:center"></div>
<div id="skip-list" style="display:flex;flex-direction:column;gap:6px;max-height:200px;overflow-y:auto;margin-bottom:12px"></div>
<div id="skip-status" style="font-size:12px;color:var(--txt2);min-height:16px;margin-bottom:8px"></div>
<div style="display:flex;gap:8px;justify-content:flex-end">
<button onclick="closeSkipDialog()" style="padding:8px 16px;background:var(--raised);border:1px solid var(--border);border-radius:8px;color:var(--txt);cursor:pointer">Abbrechen</button>
<button id="skip-confirm" onclick="confirmSkip()" style="padding:8px 18px;background:var(--accent);color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:600">Überspringen</button>
</div>
</div>
</div>
<!-- ACE Dryer Temp/Time Settings Dialog -->
<div class="modal-overlay" id="ace-dry-dialog" onclick="if(event.target===this)closeAceDryDialog()">
<div class="modal-box" style="max-width:560px;width:100%">
<div class="modal-header" style="margin-bottom:10px">
<span class="modal-title" id="ace-dry-dialog-title">Dryer Temp/Time Settings</span>
<button onclick="closeAceDryDialog()" style="background:none;border:none;font-size:18px;cursor:pointer;color:var(--txt2)"></button>
</div>
<div style="display:flex;align-items:center;gap:12px;margin-bottom:8px">
<label id="ace-dry-dialog-temp-label" style="min-width:190px;font-size:12px;color:var(--txt)">Temperature (30-80°C)</label>
<input id="ace-dry-dialog-temp" type="number" min="30" max="80" step="1"
oninput="aceDryDialogInputsChanged()"
style="width:130px;padding:8px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);text-align:center" value="45">
</div>
<div style="display:flex;align-items:center;gap:12px;margin-bottom:16px">
<label id="ace-dry-dialog-time-label" style="min-width:190px;font-size:12px;color:var(--txt)">Rem. Time (h:m:s)</label>
<div style="display:flex;align-items:center;gap:8px">
<input id="ace-dry-dialog-h" type="number" min="0" max="24" step="1" value="4"
oninput="aceDryDialogInputsChanged()"
style="width:70px;padding:8px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);text-align:center">
<span style="color:var(--txt2)">:</span>
<input id="ace-dry-dialog-m" type="number" min="0" max="59" step="1" value="0"
oninput="aceDryDialogInputsChanged()"
style="width:70px;padding:8px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);text-align:center">
<span style="color:var(--txt2)">:</span>
<input id="ace-dry-dialog-s" type="number" min="0" max="59" step="1" value="0"
oninput="aceDryDialogInputsChanged()"
style="width:70px;padding:8px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);text-align:center">
</div>
</div>
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-bottom:8px">
<button class="ace-dry-preset-btn" data-preset="pla" onclick="aceDryDialogPreset('pla')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">PLA</button>
<button class="ace-dry-preset-btn" data-preset="pla_plus" onclick="aceDryDialogPreset('pla_plus')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">PLA+</button>
<button class="ace-dry-preset-btn" data-preset="petg" onclick="aceDryDialogPreset('petg')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">PETG</button>
<button class="ace-dry-preset-btn" data-preset="tpu" onclick="aceDryDialogPreset('tpu')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">TPU</button>
<button class="ace-dry-preset-btn" data-preset="abs_asa" onclick="aceDryDialogPreset('abs_asa')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">ABS / ASA</button>
<button class="ace-dry-preset-btn" data-preset="pa_pc" onclick="aceDryDialogPreset('pa_pc')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">PA / PC</button>
<button class="ace-dry-preset-btn" data-preset="custom_1" onclick="aceDryDialogPreset('custom_1')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">Custom 1</button>
<button class="ace-dry-preset-btn" data-preset="custom_2" onclick="aceDryDialogPreset('custom_2')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">Custom 2</button>
<button class="ace-dry-preset-btn" data-preset="custom_3" onclick="aceDryDialogPreset('custom_3')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">Custom 3</button>
</div>
<div id="ace-dry-dialog-custom-name-row" style="display:none;align-items:center;gap:12px;margin-bottom:14px">
<label id="ace-dry-dialog-custom-name-label" style="min-width:190px;font-size:12px;color:var(--txt)">Custom Name</label>
<input id="ace-dry-dialog-custom-name" type="text" maxlength="32" oninput="aceDryDialogInputsChanged()"
style="width:220px;padding:8px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt)">
</div>
<div style="display:flex;justify-content:flex-end;gap:8px">
<button id="ace-dry-dialog-reset-default" onclick="resetAceDryPresetToDefault()" style="display:none;padding:8px 14px;background:var(--raised);border:1px solid var(--border);border-radius:8px;color:var(--txt);cursor:pointer">Reset to Default</button>
<button id="ace-dry-dialog-save-preset" onclick="saveAceDryPresetAndRestart()" style="display:none;padding:8px 14px;background:var(--warn);border:1px solid transparent;border-radius:8px;color:#fff;cursor:pointer">Save & Restart</button>
<button id="ace-dry-dialog-cancel" onclick="closeAceDryDialog()" style="padding:8px 14px;background:var(--raised);border:1px solid var(--border);border-radius:8px;color:var(--txt);cursor:pointer">Cancel</button>
<button id="ace-dry-dialog-confirm" onclick="confirmAceDryDialog()" style="padding:8px 16px;background:var(--accent);color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:600">Confirm</button>
</div>
</div>
</div>
<script src="/kx/ui/app.js"></script></head>
<footer style="text-align:center;padding:12px;font-size:11px;color:var(--txt2);border-top:1px solid var(--border);margin-top:auto">
&copy; ViewIT 2026
</footer>
</body>
</html>