Files
ExoMy_Cuno/ExoMy_Software-master/gui/admin.html
T
2026-05-22 08:28:44 +02:00

389 lines
14 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="./admin.css">
</head>
<body>
<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-main">
<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">Wartung</span>
<span class="p-tag">Maintenance</span>
</div>
<div class="link-card">
<div class="link-card-text">
<strong>Motorentest</strong>
<p>Separate Testseite für Lenkung und Antrieb je Rad.</p>
</div>
<div class="link-card-action">
<a class="link-btn" href="./admin-motor-test.html">Öffnen</a>
</div>
</div>
</div>
<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))">
<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>
<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==';
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 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 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(',', '.'));
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 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(); });
updateClock();
window.setInterval(updateClock, 1000);
fetchStatus();
fetchDelay();
fetchSpeedLimit();
window.setInterval(fetchStatus, 10000);
window.setInterval(fetchDelay, 5000);
window.setInterval(fetchSpeedLimit, 5000);
});
</script>
</body>
</html>