Files
xterra-overland-dashboard/simulator/templates/index.html
T
2026-06-03 00:12:01 -06:00

310 lines
11 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<title>Xterra Dashboard Simulator</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<div id="alarmOverlay" class="alarm-overlay hidden">
<div class="alarm-box">
<div class="alarm-icon"></div>
<h1 id="alarmTitle">ALARM</h1>
<p id="alarmMessage">--</p>
<button onclick="acknowledgeAlarm()">Acknowledge</button>
</div>
</div>
<main>
<header>
<h1>Xterra Dashboard</h1>
<div id="statusLine">RS-485: -- | WiFi: --</div>
</header>
<section id="dashboard" class="screen active">
<div class="big-card">
<h2>Battery</h2>
<p><span id="dashSoc">--</span>%</p>
<small><span id="dashRuntime">--</span> hr runtime</small>
</div>
<div class="grid">
<div class="card"><h2>Fridge A</h2><p><span id="dashFridge1">--</span>°F</p></div>
<div class="card"><h2>Fridge B</h2><p><span id="dashFridge2">--</span>°F</p></div>
<div class="card"><h2>Rear Seat</h2><p><span id="dashRear">--</span>°F</p></div>
<div class="card"><h2>Outside</h2><p><span id="dashOutside">--</span>°F</p></div>
</div>
<div class="relay-row">
<div id="dashStarlink" class="relay-pill">Starlink: --</div>
<div id="dashFridgeRelay" class="relay-pill">Fridge: --</div>
</div>
</section>
<section id="battery" class="screen">
<h2>Battery Detail</h2>
<div class="detail-list">
<div>SOC <strong><span id="batSoc">--</span>%</strong></div>
<div>Voltage <strong><span id="batVoltage">--</span> V</strong></div>
<div>Current <strong><span id="batCurrent">--</span> A</strong></div>
<div>Remaining <strong><span id="batRemaining">--</span> Ah</strong></div>
<div>Runtime <strong><span id="batRuntime">--</span> hr</strong></div>
<div>Battery Temp <strong><span id="batTemp">--</span>°F</strong></div>
</div>
</section>
<section id="temps" class="screen">
<h2>Temperatures</h2>
<div class="grid">
<div class="card"><h2>Fridge Zone 1</h2><p><span id="tempFridge1">--</span>°F</p></div>
<div class="card"><h2>Fridge Zone 2</h2><p><span id="tempFridge2">--</span>°F</p></div>
<div class="card"><h2>Rear Seat</h2><p><span id="tempRear">--</span>°F</p></div>
<div class="card"><h2>Outside</h2><p><span id="tempOutside">--</span>°F</p></div>
</div>
</section>
<section id="power" class="screen">
<h2>Power Control</h2>
<button onclick="toggleRelay('starlink')" id="starlinkBtn">Starlink</button>
<button onclick="toggleRelay('fridge')" id="fridgeBtn">Fridge</button>
</section>
<section id="system" class="screen">
<h2>System</h2>
<div class="detail-list">
<div>RS-485 <strong id="sysRs485">--</strong></div>
<div>WiFi <strong id="sysWifi">--</strong></div>
<div>WiFi Override <strong id="sysWifiOverride">--</strong></div>
</div>
<button onclick="enableWifi()">Enable WiFi 10 min</button>
<h2>Alarm Settings</h2>
<div class="settings-list">
<label>
Rear Seat Warning °F
<input id="rearWarningInput" type="number" value="85">
</label>
<label>
Rear Seat Critical °F
<input id="rearCriticalInput" type="number" value="95">
</label>
<label>
Fridge Zone 1 Warm °F
<input id="fridgeZone1WarmInput" type="number" value="45">
</label>
<label>
Fridge Zone 2 Warm °F
<input id="fridgeZone2WarmInput" type="number" value="15">
</label>
<label>
Battery Low %
<input id="batteryLowInput" type="number" value="20">
</label>
<button onclick="saveAlarmSettings()">Save Alarm Settings</button>
</div>
</section>
</main>
<nav>
<button onclick="showScreen('dashboard')">Dashboard</button>
<button onclick="showScreen('battery')">Battery</button>
<button onclick="showScreen('temps')">Temps</button>
<button onclick="showScreen('power')">Power</button>
<button onclick="showScreen('system')">System</button>
</nav>
<script>
let relayState = {};
let activeAlarmKey = null;
let acknowledgedAlarms = new Set();
let alarmConfig = {
rear_seat_warning: 85,
rear_seat_critical: 95,
fridge_zone_1_warm: 45,
fridge_zone_2_warm: 15,
battery_low: 20
};
function showScreen(id) {
document.querySelectorAll('.screen').forEach(screen => {
screen.classList.remove('active');
});
document.getElementById(id).classList.add('active');
}
function onOff(value) {
return value ? 'ON' : 'OFF';
}
function checkAlarms(data) {
const alarms = [];
if (data.temps.rear_seat >= alarmConfig.rear_seat_critical) {
alarms.push({
key: 'rear_seat_critical',
title: 'REAR SEAT TEMP CRITICAL',
message: `${data.temps.rear_seat}°F detected near car seat area`
});
} else if (data.temps.rear_seat >= alarmConfig.rear_seat_warning) {
alarms.push({
key: 'rear_seat_warning',
title: 'REAR SEAT TEMP HIGH',
message: `${data.temps.rear_seat}°F detected near car seat area`
});
}
if (data.temps.fridge_zone_1 >= alarmConfig.fridge_zone_1_warm) {
alarms.push({
key: 'fridge_zone_1_warm',
title: 'FRIDGE ZONE 1 WARM',
message: `${data.temps.fridge_zone_1}°F`
});
}
if (data.temps.fridge_zone_2 >= alarmConfig.fridge_zone_2_warm) {
alarms.push({
key: 'fridge_zone_2_warm',
title: 'FRIDGE ZONE 2 WARM',
message: `${data.temps.fridge_zone_2}°F`
});
}
if (data.battery.soc <= alarmConfig.battery_low) {
alarms.push({
key: 'battery_low',
title: 'BATTERY LOW',
message: `${data.battery.soc}% remaining`
});
}
const unacked = alarms.find(alarm => !acknowledgedAlarms.has(alarm.key));
if (unacked) {
showAlarm(unacked);
}
}
function showAlarm(alarm) {
activeAlarmKey = alarm.key;
document.getElementById('alarmTitle').textContent = alarm.title;
document.getElementById('alarmMessage').textContent = alarm.message;
document.getElementById('alarmOverlay').classList.remove('hidden');
}
function acknowledgeAlarm() {
if (activeAlarmKey) {
acknowledgedAlarms.add(activeAlarmKey);
}
activeAlarmKey = null;
document.getElementById('alarmOverlay').classList.add('hidden');
}
function loadAlarmSettings() {
const saved = localStorage.getItem('alarmConfig');
if (saved) {
alarmConfig = JSON.parse(saved);
}
document.getElementById('rearWarningInput').value = alarmConfig.rear_seat_warning;
document.getElementById('rearCriticalInput').value = alarmConfig.rear_seat_critical;
if (alarmConfig.fridge_warm !== undefined) {
alarmConfig.fridge_zone_1_warm = alarmConfig.fridge_warm;
alarmConfig.fridge_zone_2_warm = 15;
delete alarmConfig.fridge_warm;
localStorage.setItem('alarmConfig', JSON.stringify(alarmConfig));
}
document.getElementById('fridgeZone1WarmInput').value = alarmConfig.fridge_zone_1_warm;
document.getElementById('fridgeZone2WarmInput').value = alarmConfig.fridge_zone_2_warm;
document.getElementById('batteryLowInput').value = alarmConfig.battery_low;
}
function saveAlarmSettings() {
alarmConfig = {
rear_seat_warning: Number(document.getElementById('rearWarningInput').value),
rear_seat_critical: Number(document.getElementById('rearCriticalInput').value),
fridge_zone_1_warm: Number(document.getElementById('fridgeZone1WarmInput').value),
fridge_zone_2_warm: Number(document.getElementById('fridgeZone2WarmInput').value),
battery_low: Number(document.getElementById('batteryLowInput').value)
};
localStorage.setItem('alarmConfig', JSON.stringify(alarmConfig));
acknowledgedAlarms.clear();
alert('Alarm settings saved');
}
async function loadStatus() {
const res = await fetch('/status');
const data = await res.json();
relayState = data.relays;
document.getElementById('statusLine').textContent =
`RS-485: ${data.network.rs485_connected ? 'Connected' : 'Lost'} | WiFi: ${data.network.wifi_enabled ? 'Enabled' : 'Disabled'}`;
document.getElementById('dashSoc').textContent = data.battery.soc;
document.getElementById('dashRuntime').textContent = data.battery.runtime_hours;
document.getElementById('dashFridge1').textContent = data.temps.fridge_zone_1;
document.getElementById('dashFridge2').textContent = data.temps.fridge_zone_2;
document.getElementById('dashRear').textContent = data.temps.rear_seat;
document.getElementById('dashOutside').textContent = data.temps.outside;
document.getElementById('dashStarlink').textContent = `Starlink: ${onOff(data.relays.starlink)}`;
document.getElementById('dashFridgeRelay').textContent = `Fridge: ${onOff(data.relays.fridge)}`;
document.getElementById('batSoc').textContent = data.battery.soc;
document.getElementById('batVoltage').textContent = data.battery.voltage;
document.getElementById('batCurrent').textContent = data.battery.current;
document.getElementById('batRemaining').textContent = data.battery.remaining_ah;
document.getElementById('batRuntime').textContent = data.battery.runtime_hours;
document.getElementById('batTemp').textContent = data.battery.temperature_f;
document.getElementById('tempFridge1').textContent = data.temps.fridge_zone_1;
document.getElementById('tempFridge2').textContent = data.temps.fridge_zone_2;
document.getElementById('tempRear').textContent = data.temps.rear_seat;
document.getElementById('tempOutside').textContent = data.temps.outside;
document.getElementById('starlinkBtn').textContent = `Starlink: ${onOff(data.relays.starlink)}`;
document.getElementById('fridgeBtn').textContent = `Fridge: ${onOff(data.relays.fridge)}`;
document.getElementById('sysRs485').textContent = data.network.rs485_connected ? 'Connected' : 'Disconnected';
document.getElementById('sysWifi').textContent = data.network.wifi_enabled ? 'Enabled' : 'Disabled';
document.getElementById('sysWifiOverride').textContent = data.network.wifi_override_active ? 'Active' : 'Inactive';
checkAlarms(data);
}
async function toggleRelay(name) {
await fetch(`/relay/${name}`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({state: !relayState[name]})
});
loadStatus();
}
async function enableWifi() {
await fetch('/network/wifi', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({minutes: 10})
});
loadStatus();
}
loadAlarmSettings();
loadStatus();
setInterval(loadStatus, 2000);
</script>
</body>
</html>