417 lines
15 KiB
HTML
417 lines
15 KiB
HTML
<!DOCTYPE html>
|
||
<html>
|
||
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||
<title>CUNO Admin</title>
|
||
<link rel="stylesheet" type="text/css" href="style.css">
|
||
</head>
|
||
|
||
<body class="admin-page">
|
||
<main>
|
||
<div class="admin_shell">
|
||
<section class="panel admin_panel">
|
||
<div class="panel_header">
|
||
<div class="pilot_brand">
|
||
<a class="pilot_logo_link" href="./index.html" aria-label="Zurück zur Steuerung">
|
||
<img class="pilot_logo" src="./Logo_Cuno.png" alt="CUNO Logo" />
|
||
</a>
|
||
<div>
|
||
<p class="eyebrow">Admin</p>
|
||
<h1>CUNO System</h1>
|
||
</div>
|
||
</div>
|
||
<p id="admin_status" class="status_badge">Lade...</p>
|
||
</div>
|
||
|
||
<p class="admin_copy">
|
||
Diese Seite bündelt die wichtigsten Systemfunktionen und Statuswerte des Raspberry Pi.
|
||
</p>
|
||
|
||
<div class="admin_content">
|
||
<div class="admin_section">
|
||
<div class="admin_section_header">
|
||
<p class="eyebrow">Aktionen und Status</p>
|
||
</div>
|
||
<div class="admin_action_row">
|
||
<div class="admin_actions">
|
||
<button class="button buttonWarm" onclick="runAdminAction('restart-camera')">Kameradienst neu starten</button>
|
||
<button class="button buttonDanger" onclick="runAdminAction('restart-container')">ExoMy-Container neu starten</button>
|
||
<button class="button buttonWarm" onclick="runAdminAction('reboot')">Raspberry Pi neu starten</button>
|
||
<button class="button buttonDanger" onclick="runAdminAction('shutdown')">Raspberry Pi herunterfahren</button>
|
||
<button class="button rumble_btn" onclick="runRumble()">Controller vibrieren</button>
|
||
</div>
|
||
|
||
<div class="service_compact">
|
||
<div class="service_compact_row">
|
||
<span>cam</span>
|
||
<strong id="service_camera" class="service_state">-</strong>
|
||
</div>
|
||
<div class="service_compact_row">
|
||
<span>api</span>
|
||
<strong id="service_admin_api" class="service_state">-</strong>
|
||
</div>
|
||
<div class="service_compact_row">
|
||
<span>exo</span>
|
||
<strong id="service_exomy" class="service_state">-</strong>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="admin_section">
|
||
<div class="admin_section_header">
|
||
<p class="eyebrow">Wartung</p>
|
||
</div>
|
||
<div class="admin_quick_link_card">
|
||
<div>
|
||
<strong>Motorentest pro Rad</strong>
|
||
<p>Separate Testseite für Lenkung links, Lenkung rechts, Vorwärts und Rückwärts je ausgewähltem Rad.</p>
|
||
</div>
|
||
<a class="button buttonX admin_quick_link_button" href="./admin-motor-test.html">Motortest öffnen</a>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="admin_section">
|
||
<div class="admin_section_header">
|
||
<p class="eyebrow">Latenz-Simulation</p>
|
||
</div>
|
||
<div class="admin_quick_link_card">
|
||
<div>
|
||
<strong>Signallaufzeit</strong>
|
||
<p>Verzögert Steuerbefehle (Controller + Web-GUI) und Kamerabild gleichzeitig.</p>
|
||
</div>
|
||
<div class="delay_control">
|
||
<select id="delay_select" class="delay_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="delay_status_text"></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="admin_section">
|
||
<div class="admin_section_header">
|
||
<p class="eyebrow">Geschwindigkeit</p>
|
||
</div>
|
||
<div class="admin_quick_link_card">
|
||
<div>
|
||
<strong>Maximale Geschwindigkeit</strong>
|
||
<p>Begrenzt die Höchstgeschwindigkeit des Rovers für Controller und Web-GUI.</p>
|
||
</div>
|
||
<div class="delay_control">
|
||
<select id="speed_limit_select" class="delay_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 % – keine Begrenzung</option>
|
||
</select>
|
||
<span id="speed_limit_status" class="delay_status_text"></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="admin_section">
|
||
<div class="admin_section_header">
|
||
<p class="eyebrow">Systemstatus</p>
|
||
</div>
|
||
<div class="info_grid">
|
||
<div class="info_card">
|
||
<span class="info_label">WLAN-Status</span>
|
||
<strong id="info_wifi">-</strong>
|
||
</div>
|
||
<div class="info_card">
|
||
<span class="info_label">IP-Adressen</span>
|
||
<strong id="info_ips">-</strong>
|
||
</div>
|
||
<div class="info_card">
|
||
<span class="info_label">CPU-Temperatur</span>
|
||
<strong id="info_temp">-</strong>
|
||
</div>
|
||
<div class="info_card">
|
||
<span class="info_label">Unterspannung</span>
|
||
<strong id="info_undervoltage" class="state_unknown">-</strong>
|
||
</div>
|
||
<div class="info_card">
|
||
<span class="info_label">Uptime</span>
|
||
<strong id="info_uptime">-</strong>
|
||
</div>
|
||
<div class="info_card">
|
||
<span class="info_label">Freier Speicherplatz</span>
|
||
<strong id="info_disk">-</strong>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<a class="admin_back" href="./index.html">Zurück zur Steuerung</a>
|
||
</section>
|
||
</div>
|
||
</main>
|
||
|
||
<div id="auth_overlay" class="auth_overlay">
|
||
<div class="auth_box">
|
||
<p class="eyebrow" style="margin:0;">Admin-Zugang</p>
|
||
<h2 style="margin:0;">CUNO System</h2>
|
||
<p style="margin:0; font-size:0.88rem; opacity:.6;">Passwort eingeben um fortzufahren</p>
|
||
<input type="password" id="auth_input" class="auth_input" placeholder="••••••••" autocomplete="current-password">
|
||
<p id="auth_error" class="auth_error"></p>
|
||
<button class="button buttonX" 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 status = document.getElementById('admin_status');
|
||
if (status) {
|
||
status.innerHTML = 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 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);
|
||
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);
|
||
}
|
||
|
||
var data = await response.json();
|
||
renderStatus(data);
|
||
} catch (error) {
|
||
console.error(error);
|
||
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 (error) {
|
||
console.error(error);
|
||
setAdminStatus('Fehler');
|
||
alert('Der Admin-Befehl konnte nicht ausgeführt werden.');
|
||
}
|
||
}
|
||
|
||
function updateDelayButtons(seconds) {
|
||
var sel = document.getElementById('delay_select');
|
||
if (sel) sel.value = String(seconds);
|
||
}
|
||
|
||
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 fetchDelay() {
|
||
try {
|
||
var response = await fetch(adminApiBase + '/api/delay');
|
||
if (!response.ok) return;
|
||
var data = await response.json();
|
||
updateDelayButtons(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();
|
||
updateDelayButtons(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 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');
|
||
}
|
||
}
|
||
|
||
window.addEventListener('load', function () {
|
||
var input = document.getElementById('auth_input');
|
||
input.focus();
|
||
input.addEventListener('keydown', function(e) {
|
||
if (e.key === 'Enter') submitPassword();
|
||
});
|
||
|
||
fetchStatus();
|
||
fetchDelay();
|
||
fetchSpeedLimit();
|
||
window.setInterval(fetchStatus, 10000);
|
||
window.setInterval(fetchDelay, 5000);
|
||
window.setInterval(fetchSpeedLimit, 5000);
|
||
});
|
||
</script>
|
||
</body>
|
||
|
||
</html>
|