SwapStation_WebApp/frontend/dashboard_copy.html

434 lines
21 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Swap Station Dashboard</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&display=swap" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
fontFamily: { sans: ["Inter","ui-sans-serif","system-ui"] },
keyframes: {
pulseDot: { "0%,100%": { transform:"scale(1)", opacity: 1 }, "50%": { transform:"scale(1.2)", opacity: .7 } }
},
animation: { pulseDot: "pulseDot 1.2s ease-in-out infinite" }
}
}
}
</script>
<style>
:root { color-scheme: dark; }
html, body { height: 100%; margin: 0; }
body { background:#0a0a0a; }
.page { max-width:1400px; }
.glass {
background: rgba(30,41,59,.45);
border: 1px solid rgba(255,255,255,.10);
border-radius: .9rem;
backdrop-filter: saturate(150%) blur(12px);
}
.badge { font-size:12px; font-weight:800; padding:.35rem .6rem; border-radius:.6rem; border:1px solid; display:inline-flex; align-items:center; gap:.4rem; }
.chip{display:inline-flex;align-items:center;gap:.4rem;padding:.1rem .5rem;border-radius:.4rem;
font-size:10px;font-weight:800;letter-spacing:.02em;text-transform:uppercase;border:1px solid}
.chip-emerald{background:rgba(16,185,129,.15);color:#a7f3d0;border-color:rgba(16,185,129,.35)}
.chip-rose{background:rgba(244,63,94,.16);color:#fecaca;border-color:rgba(244,63,94,.35)}
.chip-amber{background:rgba(245,158,11,.18);color:#fde68a;border-color:rgba(245,158,11,.40)}
.chip-sky{background:rgba(56,189,248,.15);color:#bae6fd;border-color:rgba(56,189,248,.35)}
.chip-slate{background:rgba(148,163,184,.12);color:#cbd5e1;border-color:rgba(148,163,184,.30)}
.cham_chip{display:inline-flex;align-items:center;gap:.4rem;padding:.4rem .5rem;border-radius:.4rem;
font-size:10px;font-weight:800;letter-spacing:.02em;text-transform:uppercase;border:1px solid}
.cham_chip-emerald{background:rgba(16,185,129,.15);color:#a7f3d0;border-color:rgba(16,185,129,.35)}
.cham_chip-rose{background:rgba(244,63,94,.16);color:#fecaca;border-color:rgba(244,63,94,.35)}
.cham_chip-amber{background:rgba(245,158,11,.18);color:#fde68a;border-color:rgba(245,158,11,.40)}
.cham_chip-sky{background:rgba(56,189,248,.15);color:#bae6fd;border-color:rgba(56,189,248,.35)}
.cham_chip-slate{background:rgba(148,163,184,.12);color:#cbd5e1;border-color:rgba(148,163,184,.30)}
.btn { font-weight:700; font-size:10px; padding: 0.15rem 0.5rem; border-radius:.5rem; border:1px solid rgba(255,255,255,.12); background:rgba(255,255,255,.06); transition:.18s; }
.btn:hover { border-color: rgba(16,185,129,.45); background: rgba(16,185,129,.10); }
.btn-primary{background-image:linear-gradient(to right,#10b981,#14b8a6,#06b6d4);color:#fff;border-color:transparent}
.btn-primary:hover{filter:brightness(1.05);transform:translateY(-1px)}
.btn-danger { border-color: rgba(244,63,94,.35); background: rgba(244,63,94,.14); color: #fecaca; }
.btn-danger:hover { background: rgba(244,63,94,.22); }
.btn-ghost { border-color: rgba(255,255,255,.10); background: rgba(255,255,255,.05); }
.field{font-size:10px;color:#9ca3af}
.value{font-size:12px;font-weight:600;color:#e5e7eb}
.mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace}
.chamber-card.paired{border-color:#34d399!important;box-shadow:0 0 0 2px rgba(52,211,153,.45)}
.chamber-card.pending{border-color:#60a5fa!important;box-shadow:0 0 0 2px rgba(96,165,250,.45)}
.door-pill{color:#fff;font-size:10px;font-weight:700;padding:4px;border-radius:6px; width: 100%; text-align: center;}
.door-open{background:#22c55e}.door-close{background:#ef4444}
.bat-id-big{font-size:14px;font-weight:800;border-radius:.5rem;padding:.2rem .4rem;
background:rgba(0,0,0,.25);border:1px solid rgba(255,255,255,.10)}
</style>
</head>
<body class="min-h-screen text-gray-100 flex flex-col">
<div class="pointer-events-none fixed inset-0">
<div class="absolute -top-24 -left-24 w-[32rem] h-[32rem] rounded-full bg-emerald-500/10 blur-3xl"></div>
<div class="absolute -bottom-24 -right-24 w-[36rem] h-[36rem] rounded-full bg-sky-500/10 blur-3xl"></div>
</div>
<header class="relative z-10 border-b border-white/10 bg-black/20 backdrop-blur">
<div class="mx-auto max-w-7.5xl px-3 sm:px-4 py-2 grid grid-cols-[auto_1fr_auto] items-center gap-2">
<div class="flex items-center gap-2 sm:gap-3">
<a href="./station_selection.html"
class="h-9 w-9 flex items-center justify-center rounded-full bg-white/5 hover:bg-white/10 transition"
title="Back">
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7"/>
</svg>
</a>
<div class="flex flex-col leading-tight">
<div id="station-name" class="text-base sm:text-lg font-extrabold tracking-tight">
Loading...
</div>
<div id="station-location" class="text-xs sm:text-sm text-slate-100">
&nbsp; </div>
</div>
</div>
<div class="flex items-center justify-center">
<img src="./assets/vec_logo.png" alt="VECMOCON"
class="h-7 w-auto opacity-90" onerror="this.style.display='none'"/>
</div>
<div class="flex items-center flex-wrap justify-end gap-1.5 sm:gap-2">
<span class="badge border-white/10 bg-white/5 text-slate-200">
<span>Device ID:</span>
<span id="device-id"></span>
</span>
<span class="badge border-white/10 bg-white/5 text-slate-200">
<svg class="w-2.5 h-2.5 opacity-90" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
<circle cx="12" cy="12" r="9"></circle><path d="M12 7v5l3 3"></path>
</svg>
<span id="last-update-status">Last Recv —</span>
</span>
<span class="cham_chip cham_chip-emerald">
<span class="h-2 w-2 rounded-full bg-emerald-400 animate-pulseDot"></span> Online
</span>
<span class="cham_chip cham_chip-amber" title="Running on backup supply">On Backup</span>
<div class="hidden sm:block w-px h-5 bg-white/10"></div>
<button id="refreshBtn" class="btn btn-ghost !p-2">
<svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2">
<path d="M21 12a9 9 0 10-3.5 7.1M21 12h-4m4 0l-2.5-2.5"></path>
</svg>
</button>
<button id="downloadBtn" class="btn btn-ghost !p-2">
<svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2">
<path d="M12 3v12m0 0l4-4m-4 4l-4-4"></path><path d="M5 21h14"></path>
</svg>
</button>
<button id="logout-btn" class="btn btn-danger !p-2">
<svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path>
<path d="M16 17l5-5-5-5"></path><path d="M21 12H9"></path>
</svg>
</button>
</div>
</div>
<div class="border-t border-white/10 bg-black/10">
<nav class="mx-auto max-w-7.5xl px-3 sm:px-4 flex">
<a href="./dashboard.html" class="px-4 py-2 text-sm font-semibold border-b-2 border-emerald-400/70 text-white">Main</a>
<a href="./logs.html" class="px-4 py-2 text-sm text-gray-400 hover:text-gray-200 hover:border-b-2 hover:border-white/30 font-semibold">Logs</a>
<a href="./analytics.html" class="px-4 py-2 text-sm text-gray-400 hover:text-gray-200 hover:border-b-2 hover:border-white/30 font-semibold">Analytics</a>
</nav>
</div>
</header>
<main class="relative z-10 flex-1 w-full px-3 py-3 overflow-y-auto lg:overflow-hidden">
<div class="page mx-auto flex flex-col lg:h-full lg:flex-row gap-3">
<section id="chambersGrid" class="flex-1 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 lg:grid-rows-3 gap-3"></section>
<aside class="w-full lg:w-96 lg:shrink-0 flex flex-col gap-3 overflow-y-auto">
<section class="glass p-4">
<div class="flex items-center justify-between mb-3">
<span class="text-xm font-bold mb-2">System Diagnostics Code</span>
<span class="text-sm font-bold text-emerald-300">148</span>
</div>
<div class="grid grid-cols-2 gap-x-8 gap-y-1 text-sm">
<div class="text-rose-300 text-center">Lock Power Cut</div><div class="text-rose-300 text-center">Main Power Cut</div>
<div class="text-rose-300 text-center">Relayboard CAN</div><div class="text-rose-300 text-center">DB CAN Recv</div>
<div class="text-rose-300 text-center">Smoke Alarm</div><div class="text-rose-300 text-center">Water Alarm</div>
<div class="text-rose-300 text-center">Phase Failure</div><div class="text-rose-300 text-center">Earth Leakage</div>
</div>
<button id="station-reset-btn" class="btn btn-danger w-full mt-3 !py-2">Station Reset</button>
</section>
<section id="swap-panel" class="glass p-4 flex flex-col min-h-[220px]">
<h3 class="text-sm font-bold mb-2">Swap Process</h3>
<div id="swap-pairs-list" class="flex-1 flex flex-wrap gap-2 content-center justify-center">
<p id="swap-idle-text" class="w-full text-sm text-center text-gray-400">
Click a <span class="text-sky-300 font-semibold">empty</span> slot, then an <span class="text-emerald-300 font-semibold">full</span> slot.
</p>
</div>
<div class="mt-3">
<div class="grid grid-cols-2 gap-2">
<button id="start-swap-btn" class="btn btn-primary !py-2" disabled>Start Swaps</button>
<button id="abort-swap-btn" class="btn btn-danger !py-2">Abort Swap</button>
</div>
<button id="clear-swap-btn" class="btn w-full mt-2 !py-2">Clear Selection</button>
</div>
</section>
<section class="glass p-4">
<h3 class="text-sm font-bold mb-2">Audio Command</h3>
<select class="w-full rounded-md bg-white/5 border border-white/10 px-2 py-2 text-sm outline-none focus:ring-2 focus:ring-emerald-500/60">
<option>English</option><option>Hindi</option><option>Tamil</option>
</select>
</section>
<section class="glass p-4 flex-1 flex flex-col min-h-[610px]">
<h3 class="text-sm font-bold mb-2">Instance Log</h3>
<textarea class="flex-1 bg-black/20 border border-white/10 rounded-md p-2 text-xs font-mono resize-none" readonly>[--:--:--] Waiting for data…</textarea>
</section>
</aside>
</div>
</main>
<template id="chamberTemplate">
<div class="chamber-card relative glass rounded-xl p-2 flex flex-col transition border border-white/20">
<div class="text-center absolute left-0 right-0 top-0 -translate-y-1/2">
<span class="bg-slate-800 px-2 text-xs font-extrabold text-gray-200 tracking-wide rounded">
CHAMBER <span class="slotNo">#</span>
</span>
</div>
<div class="mt-2 mb-1.5">
<div class="flex items-center gap-2">
<h4 class="field font-bold text-gray-300 shrink-0">BAT_ID</h4>
<div class="bat-id-big mono truncate flex-1 text-left" title="—"></div>
</div>
</div>
<div class="flex-1 flex gap-2">
<div class="flex-1 space-y-0.5">
<div class="flex items-center justify-between border-b border-white/10 pb-0.5 mb-1">
<h4 class="field font-bold text-gray-300">BATTERY</h4>
<span class="battery-status-pill chip chip-slate"></span>
</div>
<div class="flex justify-between items-baseline"><span class="field">SOC</span><span class="value soc"></span></div>
<div class="flex justify-between items-baseline"><span class="field">Voltage</span><span class="value voltage"></span></div>
<div class="flex justify-between items-baseline"><span class="field">Bat Temp</span><span class="value bat-temp"></span></div>
<div class="flex justify-between items-baseline"><span class="field">Bat Fault</span><span class="value bat-fault text-rose-300"></span></div>
</div>
<div class="flex-1 space-y-0.5 border-l border-white/10 pl-2">
<div class="flex items-center justify-between border-b border-white/10 pb-0.5 mb-1">
<h4 class="field font-bold text-gray-300">CHARGER</h4>
<span class="charger-status-pill chip chip-slate"></span>
</div>
<div class="flex justify-between items-baseline"><span class="field">Current</span><span class="value current"></span></div>
<div class="flex justify-between items-baseline"><span class="field">Slot Temp</span><span class="value slot-temp"></span></div>
<div class="flex justify-between items-baseline"><span class="field">Chg Temp</span><span class="value chg-temp"></span></div>
<div class="flex justify-between items-baseline"><span class="field">Chg Fault</span><span class="value chg-fault text-rose-300"></span></div>
</div>
</div>
<div class="grid grid-cols-4 gap-1.5 mt-2 shrink-0">
<div class="door-pill door-close">CLOSED</div>
<button class="btn">OPEN</button>
<button class="btn">CHG ON</button>
<button class="btn">CHG OFF</button>
</div>
</div>
</template>
<script>
// SCRIPT IS UNCHANGED
const qs = (s) => document.querySelector(s);
(function setDevice() {
const el = document.querySelector('#device-id');
if (!el) return;
try {
const sel = JSON.parse(localStorage.getItem('selected_station') || '{}');
el.textContent = sel?.id || sel?.station_id || '—';
} catch {
el.textContent = '—';
}
})();
document.addEventListener('DOMContentLoaded', () => {
const stationId = 'VEC-STN-0128';
loadStationInfo(stationId);
});
async function loadStationInfo(stationId) {
const nameElement = document.getElementById('station-name');
const locationElement = document.getElementById('station-location');
try {
const mockStationData = { name: "VEC-STN-0128", location: "Sector 62, Noida" };
nameElement.textContent = mockStationData.name;
locationElement.textContent = mockStationData.location;
} catch (error) {
nameElement.textContent = 'Error Loading Station';
locationElement.textContent = 'Could not fetch data.';
console.error('Error fetching station data:', error);
}
}
(function setLastRecv() {
const el = qs('#last-update-status');
if (el) {
// Handle both badge and text styles
const textContent = 'Last Recv ' + new Date().toLocaleTimeString();
if (el.tagName === 'SPAN') {
el.textContent = textContent;
} else {
// Fallback for different structures if needed
el.innerHTML = textContent;
}
}
})();
qs('#refreshBtn')?.addEventListener('click', () => location.reload());
qs('#downloadBtn')?.addEventListener('click', () => {
alert('Hook this to your /api/logs/export endpoint.');
});
qs('#logout-btn')?.addEventListener('click', () => {
window.location.href = './index.html';
});
const grid = document.getElementById('chambersGrid');
const tmpl = document.getElementById('chamberTemplate');
grid.innerHTML = '';
for (let i = 1; i <= 9; i++) {
const node = tmpl.content.cloneNode(true);
const card = node.querySelector('.chamber-card');
card.dataset.chamberId = i;
card.querySelector('.slotNo').textContent = i;
const batIdBig = node.querySelector('.bat-id-big');
const batteryStatus = node.querySelector('.battery-status-pill');
const chargerStatus = node.querySelector('.charger-status-pill');
const socEl = node.querySelector('.soc');
const voltageEl = node.querySelector('.voltage');
const batFaultEl = node.querySelector('.bat-fault');
const chgFaultEl = node.querySelector('.chg-fault');
const batTempEl = node.querySelector('.bat-temp');
const currentEl = node.querySelector('.current');
const slotTempEl = node.querySelector('.slot-temp');
const chgTempEl = node.querySelector('.chg-temp');
const doorPill = node.querySelector('.door-pill');
const present = i % 3 !== 0;
batIdBig.textContent = present ? `TK${510000 + i}X00${200 + i}` : '—';
if (present) {
batteryStatus.textContent = 'Present';
batteryStatus.className = 'battery-status-pill chip chip-emerald';
chargerStatus.textContent = 'Charging';
chargerStatus.className = 'charger-status-pill chip chip-sky';
socEl.textContent = `${Math.max(20, 96 - i*3)}%`;
voltageEl.textContent = `5${i}.2 V`;
batTempEl.textContent = `${25 + i}.0 °C`;
currentEl.textContent = '10.5 A';
slotTempEl.textContent = `${28 + i}.0 °C`;
chgTempEl.textContent = `${35 + i}.0 °C`;
if (i === 5) batFaultEl.textContent = 'OVERHEAT';
} else {
batteryStatus.textContent = 'Absent';
batteryStatus.className = 'battery-status-pill chip chip-rose';
chargerStatus.textContent = 'Idle';
chargerStatus.className = 'charger-status-pill chip chip-slate';
chgFaultEl.textContent = (i === 6) ? 'COMM_FAIL' : '—';
}
card.querySelectorAll('.btn').forEach(b => b.addEventListener('click', e => e.stopPropagation()));
grid.appendChild(node);
}
const swapIdleText = document.getElementById('swap-idle-text');
const swapPairsList = document.getElementById('swap-pairs-list');
const startSwapBtn = document.getElementById('start-swap-btn');
const abortSwapBtn = document.getElementById('abort-swap-btn');
const clearSwapBtn = document.getElementById('clear-swap-btn');
const resetBtn = document.getElementById('station-reset-btn');
let currentPair = [], swapPairs = [];
function updateSwapUI(){
const isBuilding = currentPair.length || swapPairs.length;
if (!swapIdleText) return;
swapIdleText.classList.toggle('hidden', isBuilding);
startSwapBtn.disabled = swapPairs.length === 0;
const pairedOut = swapPairs.map(p=>p[0]), pairedIn = swapPairs.map(p=>p[1]);
document.querySelectorAll('.chamber-card').forEach(card=>{
const n = +card.dataset.chamberId;
card.classList.remove('paired','pending');
if (pairedOut.includes(n) || pairedIn.includes(n)) card.classList.add('paired');
else if (currentPair.includes(n)) card.classList.add('pending');
});
swapPairsList.innerHTML = isBuilding ? '' : swapIdleText.outerHTML;
if (isBuilding){
swapPairs.forEach(p=>{
const e = document.createElement('div');
e.className = 'text-sm font-semibold flex items-center justify-center gap-2 bg-black/20 rounded p-1.5';
e.innerHTML = `<span class="text-emerald-300">${p[0]}</span><span>→</span><span class="text-sky-300">${p[1]}</span>`;
swapPairsList.appendChild(e);
});
if (currentPair.length){
const e = document.createElement('div');
e.className = 'text-sm font-semibold flex items-center justify-center gap-2 bg-black/20 rounded p-1.5 ring-2 ring-sky-500';
e.innerHTML = `<span class="text-emerald-300 font-bold">${currentPair[0]}</span><span>→</span><span class="text-gray-400">?</span>`;
swapPairsList.appendChild(e);
}
}
}
function handleChamberClick(num){
if (swapPairs.length >= 4 && currentPair.length === 0) return alert('Maximum of 4 swap pairs reached.');
const usedOut = swapPairs.map(p=>p[0]), usedIn = swapPairs.map(p=>p[1]);
if ((currentPair.length === 0 && usedOut.includes(num)) ||
(currentPair.length === 1 && usedIn.includes(num)) ||
(currentPair.length === 1 && currentPair[0] === num)) return;
currentPair.push(num);
if (currentPair.length === 2){ swapPairs.push([...currentPair]); currentPair = []; }
updateSwapUI();
}
function clearSelection(){ currentPair=[]; swapPairs=[]; updateSwapUI(); }
document.querySelectorAll('.chamber-card').forEach(c =>
c.addEventListener('click', () => handleChamberClick(+c.dataset.chamberId))
);
clearSwapBtn?.addEventListener('click', clearSelection);
startSwapBtn?.addEventListener('click', () => {
if (swapPairs.length){ alert('Executing swaps:\n'+swapPairs.map(p=>`${p[0]}${p[1]}`).join('\n')); clearSelection(); }
});
abortSwapBtn?.addEventListener('click', () => alert('Sending Swap Abort command!'));
resetBtn?.addEventListener('click', () => { if (confirm('Reset station?')) alert('Sending Station Reset…'); });
updateSwapUI();
</script>
</body>
</html>