Files
ExoMy_Cuno/ExoMy_Software-master/gui/admin.html
T
2026-05-22 09:36:04 +02:00

904 lines
41 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">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CUNO // Admin</title>
<link rel="stylesheet" href="./app.css?v=20260522092731">
</head>
<body class="page-admin">
<header>
<svg class="hdr-icon" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="7" y="11" width="18" height="10" rx="1" fill="#0088FF" opacity=".85"/>
<rect x="11" y="7" width="10" height="4" rx="1" fill="#0088FF" opacity=".5"/>
<circle cx="9" cy="23" r="3.5" fill="none" stroke="#0088FF" stroke-width="1.5"/>
<circle cx="23" cy="23" r="3.5" fill="none" stroke="#0088FF" stroke-width="1.5"/>
<circle cx="9" cy="23" r="1.3" fill="#0088FF"/>
<circle cx="23" cy="23" r="1.3" fill="#0088FF"/>
<rect x="22" y="8" width="4" height="3" rx=".5" fill="#FF6B00" opacity=".9"/>
<line x1="16" y1="7" x2="16" y2="3" stroke="#0088FF" stroke-width="1.5"/>
<circle cx="16" cy="2.5" r="1" fill="#0088FF"/>
<rect x="3" y="14" width="4" height="2" rx="1" fill="#0088FF" opacity=".6"/>
</svg>
<span class="hdr-title">CUNO&nbsp;<em>ADMIN</em></span>
<div class="hdr-div"></div>
<div class="hdr-badge">System Control</div>
<div class="hdr-gap"></div>
<span id="admin_status" class="hdr-status">Lade...</span>
<div class="hdr-div"></div>
<a class="hdr-back" href="./index.html">← Ground Control</a>
<div class="hdr-div"></div>
<div class="hdr-clk">
<div class="hdr-clk-lbl">UTC</div>
<div class="hdr-clk-val" id="utc-clock">--:--:--</div>
</div>
</header>
<div class="admin-shell">
<div class="admin-section-tabs" role="tablist" aria-label="Admin-Bereiche">
<button id="admin_tab_system" class="admin-section-tab is_active" role="tab" aria-selected="true" onclick="switchAdminSection('system')">Admin</button>
<button id="admin_tab_motor" class="admin-section-tab" role="tab" aria-selected="false" onclick="switchAdminSection('motor')">Motorentest</button>
</div>
<section id="admin_panel_system" class="admin-section-panel is_active" role="tabpanel">
<div class="admin-section-grid">
<div class="admin-col">
<div class="panel">
<div class="p-hdr">
<div class="p-dot"></div>
<span class="p-lbl">Aktionen</span>
<span class="p-tag">System Control</span>
</div>
<div class="act-grid">
<button class="act-btn warm" onclick="runAdminAction('restart-camera')">Kamera neu starten</button>
<button class="act-btn danger" onclick="runAdminAction('restart-container')">Container neu starten</button>
<button class="act-btn warm" onclick="runAdminAction('reboot')">Pi neu starten</button>
<button class="act-btn danger" onclick="runAdminAction('shutdown')">Pi herunterfahren</button>
<button class="act-btn subtle full" onclick="runRumble()">Controller vibrieren</button>
</div>
<div class="ss-hdr"><div class="ss-hdr-line"></div><span class="ss-hdr-txt">Dienste</span><div class="ss-hdr-line"></div></div>
<div class="sr">
<span class="sr-led led-ok"></span>
<span class="sr-key">Kamera</span>
<strong id="service_camera" class="sr-val service_state state_unknown">-</strong>
</div>
<div class="sr">
<span class="sr-led led-blue"></span>
<span class="sr-key">Admin API</span>
<strong id="service_admin_api" class="sr-val service_state state_unknown">-</strong>
</div>
<div class="sr">
<span class="sr-led led-ok"></span>
<span class="sr-key">ExoMy</span>
<strong id="service_exomy" class="sr-val service_state state_unknown">-</strong>
</div>
</div>
</div>
<div class="admin-col">
<div class="panel">
<div class="p-hdr">
<div class="p-dot"></div>
<span class="p-lbl">Latenz-Simulation</span>
<span class="p-tag">Signal Delay</span>
</div>
<div class="dropdown-row">
<label>Delay</label>
<select id="delay_select" class="ctrl-select" onchange="setDelay(parseFloat(this.value))">
<option value="0">0 s – kein Delay</option>
<option value="1">1 s</option>
<option value="2">2 s</option>
<option value="3">3 s</option>
<option value="4">4 s</option>
<option value="5">5 s</option>
<option value="6">6 s</option>
<option value="7">7 s</option>
<option value="8">8 s</option>
<option value="9">9 s</option>
<option value="10">10 s</option>
</select>
<span id="delay_status" class="ctrl-status"></span>
</div>
</div>
<div class="panel">
<div class="p-hdr">
<div class="p-dot"></div>
<span class="p-lbl">Geschwindigkeit</span>
<span class="p-tag">Speed Limit</span>
</div>
<div class="dropdown-row">
<label>Limit</label>
<select id="speed_limit_select" class="ctrl-select" onchange="setSpeedLimit(parseInt(this.value, 10))">
<option value="10">10 %</option>
<option value="20">20 %</option>
<option value="30">30 %</option>
<option value="40">40 %</option>
<option value="50">50 %</option>
<option value="60">60 %</option>
<option value="70">70 %</option>
<option value="80">80 %</option>
<option value="90">90 %</option>
<option value="100" selected>100 % – kein Limit</option>
</select>
<span id="speed_limit_status" class="ctrl-status"></span>
</div>
</div>
<div class="panel">
<div class="p-hdr">
<div class="p-dot"></div>
<span class="p-lbl">Systemstatus</span>
<span class="p-tag">Telemetry</span>
</div>
<div class="ss-hdr"><div class="ss-hdr-line"></div><span class="ss-hdr-txt">Netzwerk</span><div class="ss-hdr-line"></div></div>
<div class="sr">
<span class="sr-led led-ok"></span>
<span class="sr-key">WLAN</span>
<span class="sr-val" id="info_wifi">-</span>
</div>
<div class="sr">
<span class="sr-led led-blue"></span>
<span class="sr-key">IP</span>
<span class="sr-val" id="info_ips">-</span>
</div>
<div class="ss-hdr"><div class="ss-hdr-line"></div><span class="ss-hdr-txt">Hardware</span><div class="ss-hdr-line"></div></div>
<div class="sr sr-gauge gauge-temp">
<span class="sr-key">Temp</span>
<div class="gauge"><div class="gauge-fill" id="bar-temp-adm"></div></div>
<span class="gauge-num" id="info_temp">--</span>
</div>
<div class="sr">
<span class="sr-led led-off"></span>
<span class="sr-key">Spannung</span>
<span class="sr-val" id="info_undervoltage">-</span>
</div>
<div class="sr">
<span class="sr-led led-blue"></span>
<span class="sr-key">Uptime</span>
<span class="sr-val" id="info_uptime">-</span>
</div>
<div class="sr">
<span class="sr-led led-blue"></span>
<span class="sr-key">Disk frei</span>
<span class="sr-val" id="info_disk">-</span>
</div>
</div>
</div>
</div>
</section>
<section id="admin_panel_motor" class="admin-section-panel motor-admin-tab" role="tabpanel">
<div class="admin-section-grid">
<div class="admin-col">
<div class="panel">
<div class="p-hdr">
<div class="p-dot"></div>
<span class="p-lbl">Testmodus</span>
<span class="p-tag">Container Control</span>
</div>
<div class="copy-text">
Stoppe zuerst den ExoMy-Container. Erst wenn der Status eindeutig auf gestoppt steht, solltest du Motoren testen oder die Servo-Mitten ausrichten.
</div>
<div class="sr">
<span class="sr-led led-blue"></span>
<span class="sr-key">Container</span>
<span class="sr-val" id="container_name">exomy_autostart</span>
</div>
<div class="sr">
<span class="sr-led led-off"></span>
<span class="sr-key">Status</span>
<strong id="container_state" class="sr-val service_state state_unknown">-</strong>
</div>
<div class="sr">
<span class="sr-led led-off"></span>
<span class="sr-key">Sicher testen</span>
<span class="sr-val" id="container_safe_state">-</span>
</div>
<div class="container-grid">
<button class="button buttonDanger" onclick="setContainerState('stop')">Container stoppen</button>
<button class="button buttonA" onclick="setContainerState('start')">Container starten</button>
</div>
</div>
</div>
<div class="admin-col">
<div class="panel">
<div class="p-hdr">
<div class="p-dot"></div>
<span class="p-lbl">Motorentest</span>
<span class="p-tag">Motor Diagnostics</span>
</div>
<div class="tab-bar" role="tablist" aria-label="Motorentest Bereiche">
<button id="tab_button_impulse" class="motor_test_tab_button is_active" role="tab" aria-selected="true" onclick="switchMotorTestTab('impulse')">Impulstest</button>
<button id="tab_button_neutral" class="motor_test_tab_button" role="tab" aria-selected="false" onclick="switchMotorTestTab('neutral')">Servo-Mitte</button>
<button id="tab_button_drive_neutral" class="motor_test_tab_button" role="tab" aria-selected="false" onclick="switchMotorTestTab('drive-neutral')">Fahr-Null</button>
</div>
<div class="tab-content-wrap">
<section id="tab_panel_impulse" class="motor_test_tab_panel is_active" role="tabpanel">
<div class="tab-row">
<div class="tab-box">
<div class="tab-box-lbl">Rad auswählen</div>
<div class="wheel_selector" role="radiogroup" aria-label="Rad auswählen">
<label class="wheel_option"><input type="radio" name="wheel" value="fl" checked><span>Vorne links</span></label>
<label class="wheel_option"><input type="radio" name="wheel" value="fr"><span>Vorne rechts</span></label>
<label class="wheel_option"><input type="radio" name="wheel" value="cl"><span>Mitte links</span></label>
<label class="wheel_option"><input type="radio" name="wheel" value="cr"><span>Mitte rechts</span></label>
<label class="wheel_option"><input type="radio" name="wheel" value="rl"><span>Hinten links</span></label>
<label class="wheel_option"><input type="radio" name="wheel" value="rr"><span>Hinten rechts</span></label>
</div>
</div>
<div class="tab-box">
<div class="tab-box-lbl">Impulstest</div>
<div class="motor_test_grid">
<button class="button buttonX" onclick="runMotorTest('steer_left')">Lenkung links</button>
<button class="button buttonX" onclick="runMotorTest('steer_right')">Lenkung rechts</button>
<button class="button buttonA" onclick="runMotorTest('drive_forward')">Vorwärts</button>
<button class="button buttonDanger" onclick="runMotorTest('drive_backward')">Rückwärts</button>
</div>
</div>
</div>
</section>
<section id="tab_panel_neutral" class="motor_test_tab_panel" role="tabpanel" hidden>
<div class="copy-text">
Mit <code>-</code> und <code>+</code> wird die aktuelle Mittelstellung direkt am jeweiligen Lenkservo angefahren. <code>Speichern</code> schreibt alle Werte dauerhaft in die <code>exomy.yaml</code>.
</div>
<div class="neutral-toolbar">
<button class="button buttonWarm" onclick="loadSteeringNeutralValues()">Neu laden</button>
<button id="neutral_save_button" class="button buttonA" onclick="saveSteeringNeutralValues()">Speichern</button>
</div>
<div id="steering_neutral_grid" class="steering_neutral_grid"></div>
</section>
<section id="tab_panel_drive_neutral" class="motor_test_tab_panel" role="tabpanel" hidden>
<div class="copy-text">
Mit <code>-</code> und <code>+</code> stellst du den Stillstands-PWM pro Fahrmotor ein. <code>Speichern</code> schreibt alle sechs Werte dauerhaft in die <code>exomy.yaml</code>.
</div>
<div class="neutral-toolbar">
<button class="button buttonWarm" onclick="loadDriveNeutralValue()">Neu laden</button>
<button id="drive_neutral_save_button" class="button buttonA" onclick="saveDriveNeutralValue()">Speichern</button>
</div>
<div id="drive_neutral_grid" class="steering_neutral_grid"></div>
</section>
</div>
</div>
</div>
</div>
</section>
</div>
<div id="auth_overlay" class="auth-overlay">
<div class="auth-box">
<div class="auth-eyebrow">Admin-Zugang</div>
<div class="auth-title">CUNO</div>
<div class="auth-sub">Passwort eingeben um fortzufahren</div>
<input type="password" id="auth_input" class="auth-input" placeholder="••••••••" autocomplete="current-password">
<p id="auth_error" class="auth-error"></p>
<button class="auth-submit" onclick="submitPassword()">Entsperren</button>
</div>
</div>
<script type="text/javascript">
var adminApiBase = window.location.protocol + '//' + window.location.hostname + ':8082';
var AUTH_TOKEN = 'Y3Vubw==';
var activeAdminSection = 'system';
var isMotorTestRunning = false;
var isContainerActionRunning = false;
var isNeutralLoading = false;
var isNeutralPreviewRunning = false;
var isNeutralSaving = false;
var isDriveNeutralLoading = false;
var isDriveNeutralPreviewRunning = false;
var isDriveNeutralSaving = false;
var lastContainerStatus = null;
var activeMotorTestTab = 'impulse';
var steeringNeutralValues = {};
var steeringNeutralSavedValues = {};
var driveNeutralValues = {};
var driveNeutralSavedValues = {};
var STEERING_NEUTRAL_STEP = 5;
var WHEEL_ORDER = ['fl', 'fr', 'cl', 'cr', 'rl', 'rr'];
function submitPassword() {
var input = document.getElementById('auth_input');
var error = document.getElementById('auth_error');
if (btoa(input.value) === AUTH_TOKEN) {
document.getElementById('auth_overlay').remove();
} else {
error.textContent = 'Falsches Passwort';
input.value = '';
input.focus();
}
}
function setAdminStatus(text) {
var el = document.getElementById('admin_status');
if (el) el.textContent = text;
}
function setMotorTestStatus(text) {
setAdminStatus(text);
}
function switchAdminSection(sectionName) {
activeAdminSection = sectionName;
var systemTab = document.getElementById('admin_tab_system');
var motorTab = document.getElementById('admin_tab_motor');
var systemPanel = document.getElementById('admin_panel_system');
var motorPanel = document.getElementById('admin_panel_motor');
var isSystem = sectionName === 'system';
systemTab.classList.toggle('is_active', isSystem);
motorTab.classList.toggle('is_active', !isSystem);
systemTab.setAttribute('aria-selected', isSystem ? 'true' : 'false');
motorTab.setAttribute('aria-selected', isSystem ? 'false' : 'true');
systemPanel.classList.toggle('is_active', isSystem);
motorPanel.classList.toggle('is_active', !isSystem);
if (!isSystem) fetchMotorTestStatus();
}
function setText(id, value) {
var node = document.getElementById(id);
if (node) node.textContent = value || '-';
}
function setServiceState(id, value) {
var node = document.getElementById(id);
if (!node) return;
node.textContent = value || '-';
node.classList.remove('state_active', 'state_inactive', 'state_unknown');
if (value === 'active' || value === 'running') { node.classList.add('state_active'); return; }
if (value === 'inactive' || value === 'failed' || value === 'exited') { node.classList.add('state_inactive'); return; }
node.classList.add('state_unknown');
}
function setMotorStateNode(node, value) {
if (!node) return;
node.textContent = value || '-';
node.classList.remove('state_active', 'state_inactive', 'state_unknown');
if (value === 'running' || value === 'active') { node.classList.add('state_active'); return; }
if (value === 'exited' || value === 'stopped' || value === 'inactive') { node.classList.add('state_inactive'); return; }
node.classList.add('state_unknown');
}
function setUndervoltageState(id, text, state) {
var node = document.getElementById(id);
if (!node) return;
node.textContent = text || '-';
node.classList.remove('state_active', 'state_inactive', 'state_unknown');
if (state === 'active') {
node.classList.add('state_inactive');
node.style.animation = 'statusBlink 0.9s step-end infinite';
return;
}
node.style.animation = '';
if (state === 'past') { node.classList.add('state_unknown'); return; }
if (state === 'clear') { node.classList.add('state_active'); return; }
node.classList.add('state_unknown');
}
function temperatureToWidth(value) {
if (!value || typeof value !== 'string') return '0%';
var n = parseFloat(value.replace('°C', '').replace('°C', '').replace(',', '.'));
if (isNaN(n)) return '0%';
return Math.max(0, Math.min(100, ((n - 20) / 60) * 100)).toFixed(0) + '%';
}
function renderStatus(data) {
setAdminStatus(data.status || 'Bereit');
setText('info_ips', data.system && data.system.ips);
setText('info_wifi', data.system && data.system.wifi);
setText('info_temp', data.system && data.system.cpu_temperature);
var bar = document.getElementById('bar-temp-adm');
if (bar && data.system) bar.style.width = temperatureToWidth(data.system.cpu_temperature);
setUndervoltageState('info_undervoltage', data.system && data.system.undervoltage, data.system && data.system.undervoltage_state);
setText('info_uptime', data.system && data.system.uptime);
setText('info_disk', data.system && data.system.disk_free);
setServiceState('service_camera', data.services && data.services.camera);
setServiceState('service_admin_api', data.services && data.services.admin_api);
setServiceState('service_exomy', data.services && data.services.exomy);
}
async function fetchStatus() {
try {
var response = await fetch(adminApiBase + '/api/status');
if (!response.ok) throw new Error('status ' + response.status);
renderStatus(await response.json());
} catch (e) {
setAdminStatus('API nicht erreichbar');
}
}
async function runAdminAction(action) {
var labels = {
'restart-camera': 'Kameradienst jetzt wirklich neu starten?',
'restart-container': 'ExoMy-Container jetzt wirklich neu starten?',
'reboot': 'Raspberry Pi jetzt wirklich neu starten?',
'shutdown': 'Raspberry Pi jetzt wirklich herunterfahren?'
};
if (!window.confirm(labels[action] || 'Befehl ausführen?')) return;
setAdminStatus('Sende Befehl...');
try {
var response = await fetch(adminApiBase + '/api/' + action, { method: 'POST' });
if (!response.ok) throw new Error('status ' + response.status);
var data = await response.json();
setAdminStatus(data.status || 'Befehl gesendet');
window.setTimeout(fetchStatus, 2000);
} catch (e) {
setAdminStatus('Fehler');
alert('Der Admin-Befehl konnte nicht ausgeführt werden.');
}
}
async function fetchDelay() {
try {
var response = await fetch(adminApiBase + '/api/delay');
if (!response.ok) return;
var data = await response.json();
var sel = document.getElementById('delay_select');
if (sel) sel.value = String(data.delay_seconds);
} catch (e) {}
}
async function setDelay(seconds) {
var statusEl = document.getElementById('delay_status');
if (statusEl) statusEl.textContent = 'Setze…';
try {
var response = await fetch(adminApiBase + '/api/delay', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({delay_seconds: seconds})
});
var data = await response.json();
var sel = document.getElementById('delay_select');
if (sel) sel.value = String(seconds);
if (statusEl) statusEl.textContent = data.status || 'Gesetzt';
window.setTimeout(function() { if (statusEl) statusEl.textContent = ''; }, 2000);
} catch (e) {
if (statusEl) statusEl.textContent = 'Fehler';
}
}
async function fetchSpeedLimit() {
try {
var response = await fetch(adminApiBase + '/api/speed-limit');
if (!response.ok) return;
var data = await response.json();
var sel = document.getElementById('speed_limit_select');
if (sel) sel.value = String(data.speed_limit_percent);
} catch (e) {}
}
async function setSpeedLimit(percent) {
var statusEl = document.getElementById('speed_limit_status');
if (statusEl) statusEl.textContent = 'Setze…';
try {
var response = await fetch(adminApiBase + '/api/speed-limit', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({speed_limit_percent: percent})
});
var data = await response.json();
if (statusEl) statusEl.textContent = data.status || 'Gesetzt';
window.setTimeout(function() { if (statusEl) statusEl.textContent = ''; }, 2000);
} catch (e) {
if (statusEl) statusEl.textContent = 'Fehler';
}
}
async function runRumble() {
try {
var response = await fetch(adminApiBase + '/api/rumble', { method: 'POST' });
var data = await response.json();
setAdminStatus(data.status || 'Vibration ausgelöst');
window.setTimeout(function() { setAdminStatus('Bereit'); }, 2000);
} catch (e) {
setAdminStatus('Fehler');
}
}
function getSelectedWheel() {
var selected = document.querySelector('input[name="wheel"]:checked');
return selected ? selected.value : 'fl';
}
function renderContainerStatus(data) {
var containerStatus = data && data.container_status;
var containerName = document.getElementById('container_name');
var containerState = document.getElementById('container_state');
var containerSafeState = document.getElementById('container_safe_state');
lastContainerStatus = containerStatus || null;
if (containerName && containerStatus && containerStatus.container) containerName.textContent = containerStatus.container;
setMotorStateNode(containerState, containerStatus && containerStatus.state);
if (containerSafeState) containerSafeState.textContent = containerStatus && containerStatus.is_safe_for_motor_test ? 'Ja' : 'Nein';
}
function shouldWarnBeforeLeaving() {
return lastContainerStatus && lastContainerStatus.is_safe_for_motor_test && !isContainerActionRunning;
}
function confirmLeaveIfNeeded() {
if (!shouldWarnBeforeLeaving()) return true;
return window.confirm('Der ExoMy-Container ist noch gestoppt. Wirklich die Admin-Seite verlassen?');
}
function switchMotorTestTab(tabName) {
activeMotorTestTab = tabName;
var impulseButton = document.getElementById('tab_button_impulse');
var neutralButton = document.getElementById('tab_button_neutral');
var driveNeutralButton = document.getElementById('tab_button_drive_neutral');
var impulsePanel = document.getElementById('tab_panel_impulse');
var neutralPanel = document.getElementById('tab_panel_neutral');
var driveNeutralPanel = document.getElementById('tab_panel_drive_neutral');
var isImpulse = tabName === 'impulse';
var isNeutral = tabName === 'neutral';
var isDriveNeutral = tabName === 'drive-neutral';
impulseButton.classList.toggle('is_active', isImpulse);
neutralButton.classList.toggle('is_active', isNeutral);
driveNeutralButton.classList.toggle('is_active', isDriveNeutral);
impulseButton.setAttribute('aria-selected', isImpulse ? 'true' : 'false');
neutralButton.setAttribute('aria-selected', isNeutral ? 'true' : 'false');
driveNeutralButton.setAttribute('aria-selected', isDriveNeutral ? 'true' : 'false');
impulsePanel.classList.toggle('is_active', isImpulse);
neutralPanel.classList.toggle('is_active', isNeutral);
driveNeutralPanel.classList.toggle('is_active', isDriveNeutral);
impulsePanel.hidden = !isImpulse;
neutralPanel.hidden = !isNeutral;
driveNeutralPanel.hidden = !isDriveNeutral;
if (isNeutral && !Object.keys(steeringNeutralValues).length) loadSteeringNeutralValues();
if (isDriveNeutral && !Object.keys(driveNeutralValues).length) loadDriveNeutralValue();
}
function renderSteeringNeutralCards() {
var grid = document.getElementById('steering_neutral_grid');
if (!grid) return;
var html = [];
WHEEL_ORDER.forEach(function(wheel) {
var entry = steeringNeutralValues[wheel];
if (!entry) return;
var savedEntry = steeringNeutralSavedValues[wheel];
var isDirty = !savedEntry || Number(savedEntry.value) !== Number(entry.value);
html.push(
'<article class="steering_neutral_card steering_neutral_card_compact' + (isDirty ? ' is_dirty' : '') + '">' +
'<div class="steering_neutral_compact_row">' +
'<div class="steering_neutral_card_header steering_neutral_card_header_compact">' +
'<strong class="steering_neutral_title">' + entry.wheel_label + '</strong>' +
'</div>' +
'<div class="steering_neutral_value_row steering_neutral_value_row_compact">' +
'<button class="button buttonWarm steering_neutral_adjust" onclick="adjustSteeringNeutralValue(\'' + wheel + '\', -' + STEERING_NEUTRAL_STEP + ')">-</button>' +
'<strong class="steering_neutral_value">' + entry.value + '</strong>' +
'<button class="button buttonA steering_neutral_adjust" onclick="adjustSteeringNeutralValue(\'' + wheel + '\', ' + STEERING_NEUTRAL_STEP + ')">+</button>' +
'</div>' +
'</div>' +
'</article>'
);
});
grid.innerHTML = html.join('');
updateNeutralSaveButton();
}
function hasUnsavedNeutralChanges() {
return WHEEL_ORDER.some(function(wheel) {
var current = steeringNeutralValues[wheel];
var saved = steeringNeutralSavedValues[wheel];
if (!current || !saved) return false;
return Number(current.value) !== Number(saved.value);
});
}
function updateNeutralSaveButton() {
var button = document.getElementById('neutral_save_button');
if (button) button.disabled = isNeutralLoading || isNeutralPreviewRunning || isNeutralSaving || !hasUnsavedNeutralChanges();
}
function renderDriveNeutralCards() {
var grid = document.getElementById('drive_neutral_grid');
if (!grid) return;
var html = [];
WHEEL_ORDER.forEach(function(wheel) {
var entry = driveNeutralValues[wheel];
if (!entry) return;
var savedEntry = driveNeutralSavedValues[wheel];
var isDirty = !savedEntry || Number(savedEntry.value) !== Number(entry.value);
html.push(
'<article class="steering_neutral_card steering_neutral_card_compact' + (isDirty ? ' is_dirty' : '') + '">' +
'<div class="steering_neutral_compact_row">' +
'<div class="steering_neutral_card_header steering_neutral_card_header_compact">' +
'<strong class="steering_neutral_title">' + entry.wheel_label + '</strong>' +
'</div>' +
'<div class="steering_neutral_value_row steering_neutral_value_row_compact">' +
'<button class="button buttonWarm steering_neutral_adjust" onclick="adjustDriveNeutralValue(\'' + wheel + '\', -' + STEERING_NEUTRAL_STEP + ')">-</button>' +
'<strong class="steering_neutral_value">' + entry.value + '</strong>' +
'<button class="button buttonA steering_neutral_adjust" onclick="adjustDriveNeutralValue(\'' + wheel + '\', ' + STEERING_NEUTRAL_STEP + ')">+</button>' +
'</div>' +
'</div>' +
'</article>'
);
});
grid.innerHTML = html.join('');
updateDriveNeutralSaveButton();
}
function hasUnsavedDriveNeutralChanges() {
return WHEEL_ORDER.some(function(wheel) {
var current = driveNeutralValues[wheel];
var saved = driveNeutralSavedValues[wheel];
if (!current || !saved) return false;
return Number(current.value) !== Number(saved.value);
});
}
function updateDriveNeutralSaveButton() {
var button = document.getElementById('drive_neutral_save_button');
if (button) button.disabled = isDriveNeutralLoading || isDriveNeutralPreviewRunning || isDriveNeutralSaving || !hasUnsavedDriveNeutralChanges();
}
async function fetchMotorTestStatus() {
try {
var response = await fetch(adminApiBase + '/api/motor-test/status');
if (!response.ok) throw new Error('status ' + response.status);
renderContainerStatus(await response.json());
} catch (error) {
setMotorTestStatus('API nicht erreichbar');
}
}
async function setContainerState(action) {
if (isContainerActionRunning || isMotorTestRunning || isNeutralLoading || isNeutralPreviewRunning || isNeutralSaving || isDriveNeutralLoading || isDriveNeutralPreviewRunning || isDriveNeutralSaving) return;
isContainerActionRunning = true;
updateNeutralSaveButton();
updateDriveNeutralSaveButton();
setMotorTestStatus(action === 'stop' ? 'Stoppe Container...' : 'Starte Container...');
try {
var response = await fetch(adminApiBase + '/api/motor-test/' + action + '-container', { method: 'POST' });
var data = await response.json();
if (!response.ok) throw new Error(data.status || ('status ' + response.status));
setMotorTestStatus(data.status || 'Fertig');
renderContainerStatus(data);
} catch (error) {
setMotorTestStatus('Fehler');
alert(error.message || 'Die Container-Aktion konnte nicht ausgeführt werden.');
} finally {
isContainerActionRunning = false;
updateNeutralSaveButton();
updateDriveNeutralSaveButton();
}
}
async function runMotorTest(action) {
if (isMotorTestRunning || isContainerActionRunning || isNeutralLoading || isNeutralPreviewRunning || isNeutralSaving || isDriveNeutralLoading || isDriveNeutralPreviewRunning || isDriveNeutralSaving) return;
isMotorTestRunning = true;
updateNeutralSaveButton();
updateDriveNeutralSaveButton();
setMotorTestStatus('Teste...');
try {
var response = await fetch(adminApiBase + '/api/motor-test', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ wheel: getSelectedWheel(), action: action })
});
var data = await response.json();
if (!response.ok) throw new Error(data.status || ('status ' + response.status));
setMotorTestStatus(data.status || 'Test beendet');
renderContainerStatus(data);
} catch (error) {
setMotorTestStatus('Fehler');
alert(error.message || 'Der Motortest konnte nicht ausgeführt werden.');
} finally {
isMotorTestRunning = false;
updateNeutralSaveButton();
updateDriveNeutralSaveButton();
}
}
async function loadSteeringNeutralValues() {
if (isContainerActionRunning || isMotorTestRunning || isNeutralLoading || isNeutralPreviewRunning || isNeutralSaving || isDriveNeutralLoading || isDriveNeutralPreviewRunning || isDriveNeutralSaving) return;
isNeutralLoading = true;
updateNeutralSaveButton();
setMotorTestStatus('Lade Servo-Mitten...');
try {
var response = await fetch(adminApiBase + '/api/motor-test/steering-neutral');
var data = await response.json();
if (!response.ok) throw new Error(data.status || ('status ' + response.status));
steeringNeutralValues = data.neutral_values && data.neutral_values.values ? data.neutral_values.values : {};
steeringNeutralSavedValues = JSON.parse(JSON.stringify(steeringNeutralValues));
renderSteeringNeutralCards();
renderContainerStatus(data);
setMotorTestStatus(data.status || 'Servo-Mitten geladen');
} catch (error) {
setMotorTestStatus('Fehler');
alert(error.message || 'Die Servo-Mitten konnten nicht geladen werden.');
} finally {
isNeutralLoading = false;
updateNeutralSaveButton();
}
}
async function adjustSteeringNeutralValue(wheel, delta) {
if (isContainerActionRunning || isMotorTestRunning || isNeutralLoading || isNeutralPreviewRunning || isNeutralSaving || isDriveNeutralLoading || isDriveNeutralPreviewRunning || isDriveNeutralSaving) return;
var entry = steeringNeutralValues[wheel];
if (!entry) return;
steeringNeutralValues[wheel].value = Math.max(0, Math.min(4095, Number(entry.value) + Number(delta)));
renderSteeringNeutralCards();
isNeutralPreviewRunning = true;
updateNeutralSaveButton();
setMotorTestStatus('Fahre Servo-Mitte an...');
try {
var response = await fetch(adminApiBase + '/api/motor-test/steering-neutral/preview', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ wheel: wheel, value: steeringNeutralValues[wheel].value })
});
var data = await response.json();
if (!response.ok) throw new Error(data.status || ('status ' + response.status));
renderContainerStatus(data);
setMotorTestStatus(data.status || 'Servo angefahren');
} catch (error) {
setMotorTestStatus('Fehler');
alert(error.message || 'Die Servo-Vorschau konnte nicht ausgeführt werden.');
} finally {
isNeutralPreviewRunning = false;
updateNeutralSaveButton();
}
}
async function saveSteeringNeutralValues() {
if (isContainerActionRunning || isMotorTestRunning || isNeutralLoading || isNeutralPreviewRunning || isNeutralSaving || isDriveNeutralLoading || isDriveNeutralPreviewRunning || isDriveNeutralSaving || !hasUnsavedNeutralChanges()) return;
var payload = {};
WHEEL_ORDER.forEach(function(wheel) {
var entry = steeringNeutralValues[wheel];
if (entry) payload[entry.key] = Number(entry.value);
});
isNeutralSaving = true;
updateNeutralSaveButton();
setMotorTestStatus('Speichere Servo-Mitten...');
try {
var response = await fetch(adminApiBase + '/api/motor-test/steering-neutral/save', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ values: payload })
});
var data = await response.json();
if (!response.ok) throw new Error(data.status || ('status ' + response.status));
steeringNeutralValues = data.neutral_values && data.neutral_values.values ? data.neutral_values.values : steeringNeutralValues;
steeringNeutralSavedValues = JSON.parse(JSON.stringify(steeringNeutralValues));
renderSteeringNeutralCards();
renderContainerStatus(data);
setMotorTestStatus(data.status || 'Servo-Mitten gespeichert');
} catch (error) {
setMotorTestStatus('Fehler');
alert(error.message || 'Die Servo-Mitten konnten nicht gespeichert werden.');
} finally {
isNeutralSaving = false;
updateNeutralSaveButton();
}
}
async function loadDriveNeutralValue() {
if (isContainerActionRunning || isMotorTestRunning || isNeutralLoading || isNeutralPreviewRunning || isNeutralSaving || isDriveNeutralLoading || isDriveNeutralPreviewRunning || isDriveNeutralSaving) return;
isDriveNeutralLoading = true;
updateDriveNeutralSaveButton();
setMotorTestStatus('Lade Fahr-Neutralwerte...');
try {
var response = await fetch(adminApiBase + '/api/motor-test/drive-neutral');
var data = await response.json();
if (!response.ok) throw new Error(data.status || ('status ' + response.status));
driveNeutralValues = data.drive_neutral && data.drive_neutral.values ? data.drive_neutral.values : {};
driveNeutralSavedValues = JSON.parse(JSON.stringify(driveNeutralValues));
renderDriveNeutralCards();
renderContainerStatus(data);
setMotorTestStatus(data.status || 'Fahr-Neutralwerte geladen');
} catch (error) {
setMotorTestStatus('Fehler');
alert(error.message || 'Die Fahr-Neutralwerte konnten nicht geladen werden.');
} finally {
isDriveNeutralLoading = false;
updateDriveNeutralSaveButton();
}
}
async function adjustDriveNeutralValue(wheel, delta) {
if (isContainerActionRunning || isMotorTestRunning || isNeutralLoading || isNeutralPreviewRunning || isNeutralSaving || isDriveNeutralLoading || isDriveNeutralPreviewRunning || isDriveNeutralSaving) return;
var entry = driveNeutralValues[wheel];
if (!entry) return;
entry.value = Math.max(0, Math.min(4095, Number(entry.value) + Number(delta)));
renderDriveNeutralCards();
isDriveNeutralPreviewRunning = true;
updateDriveNeutralSaveButton();
setMotorTestStatus('Fahre Fahr-Neutralwerte an...');
var values = {};
WHEEL_ORDER.forEach(function(key) {
if (driveNeutralValues[key]) values[key] = Number(driveNeutralValues[key].value);
});
try {
var response = await fetch(adminApiBase + '/api/motor-test/drive-neutral/preview', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ values: values })
});
var data = await response.json();
if (!response.ok) throw new Error(data.status || ('status ' + response.status));
driveNeutralValues = data.drive_neutral && data.drive_neutral.values ? data.drive_neutral.values : driveNeutralValues;
renderDriveNeutralCards();
renderContainerStatus(data);
setMotorTestStatus(data.status || 'Fahr-Neutralwerte angefahren');
} catch (error) {
setMotorTestStatus('Fehler');
alert(error.message || 'Die Fahr-Vorschau konnte nicht ausgeführt werden.');
} finally {
isDriveNeutralPreviewRunning = false;
updateDriveNeutralSaveButton();
}
}
async function saveDriveNeutralValue() {
if (isContainerActionRunning || isMotorTestRunning || isNeutralLoading || isNeutralPreviewRunning || isNeutralSaving || isDriveNeutralLoading || isDriveNeutralPreviewRunning || isDriveNeutralSaving || !hasUnsavedDriveNeutralChanges()) return;
isDriveNeutralSaving = true;
updateDriveNeutralSaveButton();
setMotorTestStatus('Speichere Fahr-Neutralwerte...');
var values = {};
WHEEL_ORDER.forEach(function(wheel) {
var entry = driveNeutralValues[wheel];
if (entry) values[entry.key] = Number(entry.value);
});
try {
var response = await fetch(adminApiBase + '/api/motor-test/drive-neutral/save', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ values: values })
});
var data = await response.json();
if (!response.ok) throw new Error(data.status || ('status ' + response.status));
driveNeutralValues = data.drive_neutral && data.drive_neutral.values ? data.drive_neutral.values : driveNeutralValues;
driveNeutralSavedValues = JSON.parse(JSON.stringify(driveNeutralValues));
renderDriveNeutralCards();
renderContainerStatus(data);
setMotorTestStatus(data.status || 'Fahr-Neutralwerte gespeichert');
} catch (error) {
setMotorTestStatus('Fehler');
alert(error.message || 'Die Fahr-Neutralwerte konnten nicht gespeichert werden.');
} finally {
isDriveNeutralSaving = false;
updateDriveNeutralSaveButton();
}
}
function updateClock() {
var el = document.getElementById('utc-clock');
if (el) el.textContent = new Date().toUTCString().split(' ')[4];
}
window.addEventListener('load', function() {
var input = document.getElementById('auth_input');
input.focus();
input.addEventListener('keydown', function(e) {
if (e.key === 'Enter') submitPassword();
});
if (window.location.hash === '#motor') {
switchAdminSection('motor');
}
updateClock();
window.setInterval(updateClock, 1000);
fetchStatus();
fetchDelay();
fetchSpeedLimit();
fetchMotorTestStatus();
window.setInterval(fetchStatus, 10000);
window.setInterval(fetchDelay, 5000);
window.setInterval(fetchSpeedLimit, 5000);
window.setInterval(fetchMotorTestStatus, 4000);
});
window.addEventListener('beforeunload', function(event) {
if (!shouldWarnBeforeLeaving()) return;
event.preventDefault();
event.returnValue = '';
});
</script>
</body>
</html>