428 lines
22 KiB
HTML
428 lines
22 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<title>Swap Station – Select a Station</title>
|
||
|
||
<script src="js/auth-guard.js"></script>
|
||
|
||
<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;500;600;700;800&display=swap" rel="stylesheet">
|
||
|
||
<script src="https://unpkg.com/lucide@latest" defer></script>
|
||
|
||
<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.25)', opacity: .65 } },
|
||
fadeUp: { '0%': { opacity:0, transform:'translateY(8px)' }, '100%': { opacity:1, transform:'translateY(0)' } },
|
||
},
|
||
animation: { pulseDot: 'pulseDot 1.2s ease-in-out infinite', fadeUp: 'fadeUp .25s ease-out both' }
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style>
|
||
:root { color-scheme: dark; }
|
||
html, body { height: 100%; }
|
||
</style>
|
||
</head>
|
||
<body class="min-h-screen bg-[#0a0a0a] text-gray-100">
|
||
<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 with Logout and Add User -->
|
||
<header class="relative z-10 border-b border-white/10 bg-black/20 backdrop-blur">
|
||
<div class="mx-auto max-w-7xl px-4 py-4 grid grid-cols-3 items-center gap-3">
|
||
|
||
<div>
|
||
<h1 class="text-xl md:text-2xl font-extrabold tracking-tight">Select a Station</h1>
|
||
</div>
|
||
|
||
<div class="flex 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 justify-end gap-4">
|
||
<div class="flex items-center gap-4 text-xs text-gray-400">
|
||
<span class="inline-flex items-center gap-1"><span class="h-2 w-2 rounded-full bg-emerald-400"></span> Online</span>
|
||
<span class="inline-flex items-center gap-1"><span class="h-2 w-2 rounded-full bg-rose-500"></span> Offline</span>
|
||
</div>
|
||
|
||
<div class="flex items-center gap-2">
|
||
<button id="addUserBtn" class="inline-flex items-center gap-2 rounded-lg border border-white/10 bg-white/5 px-3 py-2 text-xs font-semibold hover:border-emerald-400/30">
|
||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.5 20.25a7.5 7.5 0 0115 0"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 9v6m3-3H9"/></svg>
|
||
Add User
|
||
</button>
|
||
<button id="logoutBtn" class="inline-flex items-center gap-2 rounded-lg border border-white/10 bg-white/5 px-3 py-2 text-xs font-semibold hover:border-rose-400/30">
|
||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6A2.25 2.25 0 005.25 5.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 9l3 3m0 0l-3 3m3-3H3"/></svg>
|
||
Logout
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</header>
|
||
|
||
<main class="relative z-10 mx-auto max-w-7xl px-4 py-6">
|
||
<div class="mb-6 grid grid-cols-1 md:grid-cols-3 gap-3">
|
||
<div class="md:col-span-2">
|
||
<input id="search" type="text" placeholder="Search by name, ID or location"
|
||
class="w-full rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-sm placeholder-gray-500 outline-none focus:ring-2 focus:ring-emerald-500/60" />
|
||
</div>
|
||
<!-- THEMED STATUS DROPDOWN -->
|
||
<div class="relative" id="statusFilterWrap">
|
||
<button id="statusBtn"
|
||
class="w-full flex items-center justify-between rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-sm outline-none
|
||
focus:ring-2 focus:ring-emerald-500/60 hover:border-emerald-400/30 transition">
|
||
<span id="statusLabel">All statuses</span>
|
||
<svg class="h-4 w-4 opacity-80" viewBox="0 0 20 20" fill="currentColor"><path d="M5.23 7.21a.75.75 0 011.06.02L10 11l3.71-3.77a.75.75 0 011.08 1.04l-4.25 4.33a.75.75 0 01-1.08 0L5.21 8.27a.75.75 0 01.02-1.06z"/></svg>
|
||
</button>
|
||
|
||
<div id="statusMenu"
|
||
class="hidden absolute z-20 mt-2 w-full rounded-xl border border-white/10 bg-black/85 backdrop-blur-xl shadow-2xl overflow-hidden">
|
||
<button data-value="all" class="w-full text-left px-4 py-2 text-sm hover:bg-white/10">All statuses</button>
|
||
<button data-value="online" class="w-full text-left px-4 py-2 text-sm hover:bg-white/10">Online</button>
|
||
<button data-value="offline" class="w-full text-left px-4 py-2 text-sm hover:bg-white/10">Offline</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Grid -->
|
||
<div id="stations-grid" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 items-start"></div>
|
||
<div id="empty-state" class="hidden mt-12 text-center text-gray-400">No stations match your filters.</div>
|
||
<div id="error-state" class="hidden mt-12 text-center text-rose-300"></div>
|
||
</main>
|
||
|
||
<!-- Station Card Template (with metrics) -->
|
||
<template id="station-card-template">
|
||
<div class="group rounded-2xl border border-white/10 bg-white/5 p-4 transition animate-fadeUp hover:-translate-y-1.5 hover:border-emerald-400/60 hover:shadow-[0_0_0_1px_rgba(16,185,129,0.25),0_20px_40px_rgba(0,0,0,0.45)]">
|
||
<div class="flex items-start justify-between">
|
||
<div class="min-w-0">
|
||
<div class="flex items-center gap-2">
|
||
<span class="status-dot h-2.5 w-2.5 rounded-full"></span>
|
||
<p class="truncate text-sm text-gray-400"><span class="font-semibold text-gray-200 station-name">Station</span></p>
|
||
</div>
|
||
<!-- <p class="product-id mt-1 text-xs text-gray-400" title="Product ID">-</p> -->
|
||
<div class="product-id-wrapper mt-1 flex items-center gap-1.5 text-xs text-gray-400">
|
||
<i data-lucide="hash" class="h-3 w-3 text-gray-500"></i>
|
||
<span class="product-id">—</span>
|
||
</div>
|
||
<!-- <p class="mt-1 text-xs text-gray-400 station-location">Location</p> -->
|
||
<div class="location-wrapper mt-1 flex items-center gap-1.5 text-xs text-gray-400">
|
||
<i data-lucide="map-pin" class="h-3 w-3 text-gray-500"></i>
|
||
<span class="station-location">Location</span>
|
||
</div>
|
||
</div>
|
||
<span class="status-badge rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide"></span>
|
||
</div>
|
||
|
||
<!-- ID + status row -->
|
||
<div class="mt-4 grid grid-cols-1 gap-2 text-xs">
|
||
<div class="rounded-lg border border-white/10 bg-black/20 p-2 min-w-0">
|
||
<p class="text-[10px] text-gray-400">Station ID</p>
|
||
<p class="station-id font-semibold text-gray-200 whitespace-nowrap overflow-x-auto leading-snug text-[11px]" title="Station ID">—</p>
|
||
</div>
|
||
<!-- <div class="rounded-lg border border-white/10 bg-black/20 p-2 min-w-0">
|
||
<p class="text-[10px] text-gray-400">Product ID</p>
|
||
<p class="product-id font-semibold text-gray-200 whitespace-nowrap overflow-x-auto leading-snug text-[11px]" title="Product ID">—</p>
|
||
</div> -->
|
||
</div>
|
||
|
||
<!-- Metrics Row -->
|
||
<div class="mt-3 grid grid-cols-3 gap-2">
|
||
<div class="rounded-lg border border-white/10 bg-black/20 p-2 text-center">
|
||
<p class="text-[10px] text-gray-400">Total Starts</p>
|
||
<p class="metric-starts text-sm font-semibold">0</p>
|
||
</div>
|
||
<div class="rounded-lg border border-white/10 bg-black/20 p-2 text-center">
|
||
<p class="text-[10px] text-gray-400">Completed</p>
|
||
<p class="metric-success text-sm font-semibold">0</p>
|
||
</div>
|
||
<div class="rounded-lg border border-white/10 bg-black/20 p-2 text-center">
|
||
<p class="text-[10px] text-gray-400">Aborted</p>
|
||
<p class="metric-aborted text-sm font-semibold">0</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mt-4 flex items-center gap-2">
|
||
<button class="open-btn w-full flex-grow rounded-lg bg-gradient-to-r from-emerald-500 to-teal-500 px-3 py-2 text-xs font-semibold text-white transition hover:brightness-110">
|
||
Open
|
||
</button>
|
||
|
||
<button class="remove-btn flex-shrink-0 rounded-lg border border-white/10 bg-white/5 p-2 text-rose-400 transition hover:border-rose-400/60 hover:bg-rose-500/10">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-trash-2">
|
||
<path d="M3 6h18"/>
|
||
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/>
|
||
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/>
|
||
<line x1="10" x2="10" y1="11" y2="17"/>
|
||
<line x1="14" x2="14" y1="11" y2="17"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- Add Station Card Template (goes LAST) -->
|
||
<template id="add-station-card-template">
|
||
<div class="flex h-full min-h-[160px] w-full items-center justify-center rounded-2xl border-2 border-dashed border-emerald-400/40 bg-emerald-500/5 p-6 text-emerald-300 hover:border-emerald-300 hover:text-emerald-200 cursor-pointer transition animate-fadeUp">
|
||
<div class="flex flex-col items-center gap-2">
|
||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" /></svg>
|
||
<span class="font-semibold">Add Station</span>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- User Modal -->
|
||
<div id="userModal" class="fixed inset-0 z-50 hidden bg-black/95">
|
||
<div class="absolute inset-0 flex items-center justify-center p-4">
|
||
<div class="w-full max-w-md rounded-2xl bg-white/10 p-6">
|
||
<h2 class="text-lg font-bold mb-4">Add User</h2>
|
||
<form id="userForm" class="space-y-4">
|
||
<input type="text" id="newUsername" placeholder="Username" class="w-full rounded-lg border border-white/20 bg-black/40 px-3 py-2" required>
|
||
<input type="password" id="newPassword" placeholder="Password" class="w-full rounded-lg border border-white/20 bg-black/40 px-3 py-2" required>
|
||
<label class="flex items-center gap-2 text-sm"><input type="checkbox" id="isAdmin" class="h-4 w-4"> Is Admin</label>
|
||
<div class="flex justify-end gap-2">
|
||
<button type="button" id="cancelUserBtn" class="px-4 py-2 rounded bg-gray-600/40">Cancel</button>
|
||
<button type="submit" class="px-4 py-2 rounded bg-emerald-600">Save</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Station Modal -->
|
||
<div id="stationModal" class="fixed inset-0 z-50 hidden bg-black/95">
|
||
<div class="absolute inset-0 flex items-center justify-center p-4">
|
||
<div class="w-full max-w-lg rounded-2xl bg-white/10 p-6">
|
||
<h2 class="text-lg font-bold mb-4">Add Station</h2>
|
||
<form id="stationForm" class="space-y-3">
|
||
<input type="text" placeholder="Station ID" id="stationId" class="w-full rounded-lg border border-white/20 bg-black/40 px-3 py-2" required>
|
||
<input type="text" placeholder="Product ID" id="stationProductId" class="w-full rounded-lg border border-white/20 bg-black/40 px-3 py-2" required>
|
||
<input type="text" placeholder="Name" id="stationName" class="w-full rounded-lg border border-white/20 bg-black/40 px-3 py-2" required>
|
||
<input type="text" placeholder="Location" id="stationLocation" class="w-full rounded-lg border border-white/20 bg-black/40 px-3 py-2" required>
|
||
<input type="text" placeholder="MQTT Broker" id="mqttBroker" class="w-full rounded-lg border border-white/20 bg-black/40 px-3 py-2" required>
|
||
<input type="number" placeholder="MQTT Port" id="mqttPort" class="w-full rounded-lg border border-white/20 bg-black/40 px-3 py-2" required>
|
||
<input type="text" placeholder="MQTT Username" id="mqttUsername" class="w-full rounded-lg border border-white/20 bg-black/40 px-3 py-2">
|
||
<input type="password" placeholder="MQTT Password" id="mqttPassword" class="w-full rounded-lg border border-white/20 bg-black/40 px-3 py-2">
|
||
<div class="flex justify-end gap-2">
|
||
<button type="button" id="cancelStationBtn" class="px-4 py-2 rounded bg-gray-600/40">Cancel</button>
|
||
<button type="submit" class="px-4 py-2 rounded bg-emerald-600">Save</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const API_BASE = 'http://192.168.1.10:5000/api';
|
||
|
||
// --- DOM Elements ---
|
||
const grid = document.getElementById('stations-grid');
|
||
const addStationCardTmpl = document.getElementById('add-station-card-template');
|
||
const stationCardTmpl = document.getElementById('station-card-template');
|
||
const searchEl = document.getElementById('search');
|
||
const emptyState = document.getElementById('empty-state');
|
||
const errorState = document.getElementById('error-state');
|
||
const statusBtn = document.getElementById('statusBtn');
|
||
const statusMenu = document.getElementById('statusMenu');
|
||
const statusLabel = document.getElementById('statusLabel');
|
||
const userModal = document.getElementById('userModal');
|
||
const stationModal = document.getElementById('stationModal');
|
||
|
||
// --- State Variables ---
|
||
let allStations = []; // Master list of stations
|
||
let statusValue = 'all';
|
||
let pollingInterval = null; // To hold our interval ID
|
||
|
||
// --- Modal & Form Logic (No changes needed here) ---
|
||
const openModal = (el) => { el.classList.remove('hidden'); el.classList.add('block'); };
|
||
const closeModal = (el) => { el.classList.add('hidden'); el.classList.remove('block'); };
|
||
document.getElementById('addUserBtn').onclick = () => openModal(userModal);
|
||
document.getElementById('cancelUserBtn').onclick = () => closeModal(userModal);
|
||
document.getElementById('logoutBtn').onclick = () => { localStorage.clear(); window.location.href = './index.html'; };
|
||
document.getElementById('cancelStationBtn').onclick = () => closeModal(stationModal);
|
||
// (Your form submission logic for users and stations can remain the same)
|
||
// ...
|
||
|
||
function statusStyles(status) {
|
||
const online = { dot: 'bg-emerald-400 animate-pulseDot', badge: 'bg-emerald-500/15 text-emerald-300 border border-emerald-400/20', text: 'Online' };
|
||
const offline = { dot: 'bg-rose-500', badge: 'bg-rose-500/15 text-rose-300 border border-rose-400/20', text: 'Offline' };
|
||
return String(status).toLowerCase() === 'online' ? online : offline;
|
||
}
|
||
|
||
// --- Main Rendering Function ---
|
||
function render(stationsToDisplay) {
|
||
grid.innerHTML = '';
|
||
errorState.classList.add('hidden');
|
||
|
||
if (!stationsToDisplay || stationsToDisplay.length === 0) {
|
||
emptyState.classList.remove('hidden');
|
||
} else {
|
||
emptyState.classList.add('hidden');
|
||
stationsToDisplay.forEach(s => {
|
||
const node = stationCardTmpl.content.cloneNode(true);
|
||
const card = node.querySelector('div');
|
||
card.dataset.stationId = s.id || s.station_id; // IMPORTANT for stats update
|
||
|
||
// --- Populate Card Details (No changes here) ---
|
||
card.querySelector('.station-name').textContent = s.name ?? `Station ${s.id || s.station_id}`;
|
||
card.querySelector('.product-id').textContent = s.product_id || '—';
|
||
card.querySelector('.station-location').textContent = s.location ?? '—';
|
||
const idVal = s.id || s.station_id || '—';
|
||
const idEl = card.querySelector('.station-id');
|
||
idEl.textContent = idVal;
|
||
idEl.setAttribute('title', idVal);
|
||
|
||
const styles = statusStyles(s.status);
|
||
const dot = card.querySelector('.status-dot');
|
||
dot.className = `status-dot h-2.5 w-2.5 rounded-full ${styles.dot}`;
|
||
const badge = card.querySelector('.status-badge');
|
||
badge.className = `status-badge rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide ${styles.badge}`;
|
||
badge.textContent = styles.text;
|
||
|
||
card.querySelector('.metric-starts').textContent = 0;
|
||
card.querySelector('.metric-success').textContent = 0;
|
||
card.querySelector('.metric-aborted').textContent = 0;
|
||
|
||
// --- FIX: Added the full logic for the Open button ---
|
||
card.querySelector('.open-btn').addEventListener('click', () => {
|
||
const id = encodeURIComponent(s.id || s.station_id);
|
||
window.location.href = `./dashboard.html?stationId=${id}`;
|
||
});
|
||
|
||
// --- FIX: Added the full logic for the Remove button ---
|
||
card.querySelector('.remove-btn').addEventListener('click', async () => {
|
||
const stationId = s.id || s.station_id;
|
||
const stationName = s.name;
|
||
|
||
if (!confirm(`Are you sure you want to permanently remove "${stationName}"?`)) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`${API_BASE}/stations/${stationId}`, {
|
||
method: 'DELETE',
|
||
credentials: 'include'
|
||
});
|
||
|
||
if (response.ok) {
|
||
alert(`Station "${stationName}" removed successfully.`);
|
||
// Refresh the list immediately
|
||
loadAndPollStations();
|
||
} else {
|
||
const error = await response.json();
|
||
alert(`Failed to remove station: ${error.message}`);
|
||
}
|
||
} catch (error) {
|
||
console.error('Error removing station:', error);
|
||
alert('An error occurred while trying to remove the station.');
|
||
}
|
||
});
|
||
|
||
grid.appendChild(node);
|
||
});
|
||
}
|
||
// Append the "Add Station" card
|
||
const addNode = addStationCardTmpl.content.cloneNode(true);
|
||
addNode.querySelector('div').addEventListener('click', () => openModal(stationModal));
|
||
grid.appendChild(addNode);
|
||
|
||
if (window.lucide) {
|
||
lucide.createIcons();
|
||
}
|
||
}
|
||
|
||
function applyFilters() {
|
||
const q = (searchEl.value || '').trim().toLowerCase();
|
||
const filtered = allStations.filter(s => {
|
||
const matchesQ = !q || [s.name, s.id, s.station_id, s.location].filter(Boolean).some(v => String(v).toLowerCase().includes(q));
|
||
const matchesStatus = statusValue === 'all' || String(s.status).toLowerCase() === statusValue;
|
||
return matchesQ && matchesStatus;
|
||
});
|
||
render(filtered);
|
||
}
|
||
|
||
// --- NEW: Function to Fetch and Apply Daily Stats ---
|
||
const fetchAndApplyStats = async () => {
|
||
try {
|
||
const response = await fetch(`${API_BASE}/stations/daily-stats`);
|
||
if (!response.ok) return; // Fail silently if stats aren't available
|
||
|
||
const stats = await response.json();
|
||
|
||
for (const stationId in stats) {
|
||
const stationCard = grid.querySelector(`[data-station-id="${stationId}"]`);
|
||
if (stationCard) {
|
||
const statData = stats[stationId];
|
||
stationCard.querySelector('.metric-starts').textContent = statData.total_starts;
|
||
stationCard.querySelector('.metric-success').textContent = statData.completed;
|
||
stationCard.querySelector('.metric-aborted').textContent = statData.aborted;
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error("Could not fetch daily stats:", error);
|
||
}
|
||
};
|
||
|
||
// --- MODIFIED: Main Function to Load and Poll for Data ---
|
||
const loadAndPollStations = async () => {
|
||
try {
|
||
const res = await fetch(`${API_BASE}/stations`);
|
||
if (!res.ok) throw new Error('Failed to fetch stations');
|
||
|
||
const data = await res.json();
|
||
const newStationList = Array.isArray(data) ? data : (data.stations || []);
|
||
|
||
// Only re-render the whole grid if the number of stations changes
|
||
if (newStationList.length !== allStations.length) {
|
||
allStations = newStationList;
|
||
applyFilters(); // This will call render()
|
||
} else {
|
||
// If station count is the same, just update the master list
|
||
allStations = newStationList;
|
||
}
|
||
|
||
// Always fetch fresh stats after getting the station list
|
||
await fetchAndApplyStats();
|
||
|
||
} catch (err) {
|
||
console.error(err);
|
||
errorState.textContent = 'Failed to load stations. Ensure API is running.';
|
||
errorState.classList.remove('hidden');
|
||
if (pollingInterval) clearInterval(pollingInterval); // Stop polling on error
|
||
}
|
||
};
|
||
|
||
// --- Event Listeners for Filters ---
|
||
searchEl.addEventListener('input', () => setTimeout(applyFilters, 150));
|
||
statusBtn.addEventListener('click', () => statusMenu.classList.toggle('hidden'));
|
||
statusMenu.querySelectorAll('button').forEach(b => {
|
||
b.addEventListener('click', () => {
|
||
statusValue = b.dataset.value;
|
||
statusLabel.textContent = b.textContent.trim();
|
||
statusMenu.classList.add('hidden');
|
||
applyFilters();
|
||
});
|
||
});
|
||
|
||
// --- MODIFIED: Start Everything on Page Load ---
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
loadAndPollStations(); // Load once immediately
|
||
pollingInterval = setInterval(loadAndPollStations, 10000); // Then poll every 10 seconds
|
||
});
|
||
|
||
</script>
|
||
</body>
|
||
</html>
|