Compare commits

...

3 Commits

15 changed files with 226 additions and 256 deletions

View File

@ -6,7 +6,8 @@ SECRET_KEY="80473e17c5707e19252ef3736fba32805be21a9b3e914190"
# --- PostgreSQL Database Connection --- # --- PostgreSQL Database Connection ---
# Replace with your actual database credentials. # Replace with your actual database credentials.
# Format: postgresql://<user>:<password>@<host>:<port>/<dbname> # Format: postgresql://<user>:<password>@<host>:<port>/<dbname>
DATABASE_URL="postgresql://swap_app_user:2004@localhost:5432/swap_station_db" # DATABASE_URL="postgresql://swap_app_user:2004@localhost:5432/swap_station_db"
DATABASE_URL="postgresql://swap_app_user:Vec%40123@localhost:5432/swap_station_db"
# --- MQTT Broker Connection --- # --- MQTT Broker Connection ---
MQTT_BROKER="mqtt-dev.upgrid.in" MQTT_BROKER="mqtt-dev.upgrid.in"

View File

@ -5,8 +5,7 @@ import json
import csv import csv
import io import io
import time import time
from datetime import datetime, timedelta, timezone, time as dt_time from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
from flask import Flask, jsonify, request, Response from flask import Flask, jsonify, request, Response
from flask_socketio import SocketIO, join_room from flask_socketio import SocketIO, join_room
from flask_cors import CORS from flask_cors import CORS
@ -40,7 +39,7 @@ app = Flask(__name__)
# CORS(app, resources={r"/api/*": {"origins": "http://127.0.0.1:5500"}}, supports_credentials=True, expose_headers='Content-Disposition') # CORS(app, resources={r"/api/*": {"origins": "http://127.0.0.1:5500"}}, supports_credentials=True, expose_headers='Content-Disposition')
CORS(app, resources={r"/api/*": {"origins": ["http://192.168.1.10:5500","http://127.0.0.1:5500"]}}, supports_credentials=True, expose_headers='Content-Disposition') CORS(app, resources={r"/api/*": {"origins": ["http://10.10.2.47:5501","http://127.0.0.1:5501"]}}, supports_credentials=True, expose_headers='Content-Disposition')
# CORS(app, resources={r"/api/*": {"origins": "http://localhost:5173"}}) , "http://127.0.0.1:5500" # CORS(app, resources={r"/api/*": {"origins": "http://localhost:5173"}}) , "http://127.0.0.1:5500"
# This tells Flask: "For any route starting with /api/, allow requests # This tells Flask: "For any route starting with /api/, allow requests
@ -56,8 +55,6 @@ app.config['SECRET_KEY'] = os.getenv("SECRET_KEY", "a_very_secret_key")
db.init_app(app) db.init_app(app)
socketio = SocketIO(app, cors_allowed_origins="*") socketio = SocketIO(app, cors_allowed_origins="*")
IST = ZoneInfo("Asia/Kolkata")
# --- User Loader for Flask-Login --- # --- User Loader for Flask-Login ---
@login_manager.user_loader @login_manager.user_loader
def load_user(user_id): def load_user(user_id):
@ -286,29 +283,25 @@ def get_stations():
@app.route('/api/stations/daily-stats', methods=['GET']) @app.route('/api/stations/daily-stats', methods=['GET'])
def get_all_station_stats(): def get_all_station_stats():
""" """
Calculates the swap statistics for the current calendar day in the IST timezone. Calculates the swap statistics for today for all stations.
""" """
try: try:
# Get the current time and date in your timezone (IST is defined globally) # --- CHANGE THESE TWO LINES ---
now_ist = datetime.now(IST) today_start = datetime.combine(datetime.utcnow().date(), time.min)
today_ist = now_ist.date() today_end = datetime.combine(datetime.utcnow().date(), time.max)
# Calculate the precise start and end of that day # This is an efficient query that groups by station_id and counts events in one go
start_of_day_ist = datetime.combine(today_ist, dt_time.min, tzinfo=IST)
end_of_day_ist = datetime.combine(today_ist, dt_time.max, tzinfo=IST)
# --- The rest of the query uses this new date range ---
stats = db.session.query( stats = db.session.query(
MqttLog.station_id, MqttLog.station_id,
func.count(case((MqttLog.payload['eventType'].astext == 'EVENT_SWAP_START', 1))).label('total_starts'), func.count(case((MqttLog.payload['eventType'] == 'EVENT_SWAP_START', 1))).label('total_starts'),
func.count(case((MqttLog.payload['eventType'].astext == 'EVENT_SWAP_ENDED', 1))).label('completed'), func.count(case((MqttLog.payload['eventType'] == 'EVENT_SWAP_ENDED', 1))).label('completed'),
func.count(case((MqttLog.payload['eventType'].astext == 'EVENT_SWAP_ABORTED', 1))).label('aborted') func.count(case((MqttLog.payload['eventType'] == 'EVENT_SWAP_ABORTED', 1))).label('aborted')
).filter( ).filter(
MqttLog.topic_type == 'EVENTS', MqttLog.topic_type == 'EVENTS',
# Use the new timezone-aware date range MqttLog.timestamp.between(today_start, today_end)
MqttLog.timestamp.between(start_of_day_ist, end_of_day_ist)
).group_by(MqttLog.station_id).all() ).group_by(MqttLog.station_id).all()
# Convert the list of tuples into a dictionary for easy lookup
stats_dict = { stats_dict = {
station_id: { station_id: {
"total_starts": total_starts, "total_starts": total_starts,
@ -580,7 +573,7 @@ def get_analytics_data():
hourly_swaps[log_hour] += 1 hourly_swaps[log_hour] += 1
if session_id: if session_id:
swap_starts_map[session_id] = log.timestamp swap_starts_map[session_id] = log.timestamp
elif event_type == 'EVENT_SWAP_ENDED': elif event_type == 'EVENT_BATTERY_EXIT':
completed_swaps += 1 completed_swaps += 1
daily_completed[log_date] = daily_completed.get(log_date, 0) + 1 daily_completed[log_date] = daily_completed.get(log_date, 0) + 1
if session_id and session_id in swap_starts_map: if session_id and session_id in swap_starts_map:
@ -960,5 +953,5 @@ if __name__ == '__main__':
mqtt_thread = threading.Thread(target=start_mqtt_clients, daemon=True) mqtt_thread = threading.Thread(target=start_mqtt_clients, daemon=True)
mqtt_thread.start() mqtt_thread.start()
print(f"Starting Flask-SocketIO server on http://192.168.1.10:5000") print(f"Starting Flask-SocketIO server on http://10.10.2.47:5000")
socketio.run(app, host='192.168.1.10', port=5000) socketio.run(app, host='10.10.2.47', port=5000)

View File

@ -1,7 +1,7 @@
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
// --- CONFIGURATION --- // --- CONFIGURATION ---
const SOCKET_URL = "http://192.168.1.10:5000"; const SOCKET_URL = "http://10.10.2.47:5000";
const API_BASE = "http://192.168.1.10:5000/api"; const API_BASE = "http://10.10.2.47:5000/api";
// --- DOM ELEMENT REFERENCES --- // --- DOM ELEMENT REFERENCES ---
const stationNameEl = document.getElementById('station-name'); const stationNameEl = document.getElementById('station-name');
@ -126,7 +126,7 @@ document.addEventListener('DOMContentLoaded', () => {
type: 'bar', type: 'bar',
data: { data: {
labels: data.labels, labels: data.labels,
datasets: [{ label: 'Total Swaps', data: data.swap_data, backgroundColor: 'rgba(56, 189, 248, 0.6)' }] datasets: [{ label: 'Total Swaps initiated', data: data.swap_data, backgroundColor: 'rgba(56, 189, 248, 0.6)' }]
}, },
options: { options: {
responsive: true, responsive: true,

View File

@ -11,7 +11,7 @@ document.addEventListener('DOMContentLoaded', () => {
const password = document.getElementById('password').value; const password = document.getElementById('password').value;
try { try {
const response = await fetch('http://192.168.1.10:5000/api/login', { const response = await fetch('http://10.10.2.47:5000/api/login', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }), body: JSON.stringify({ username, password }),

View File

@ -1,8 +1,8 @@
// frontend/js/common-header.js // frontend/js/common-header.js
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
// --- CONFIGURATION --- // --- CONFIGURATION ---
const SOCKET_URL = "http://192.168.1.10:5000"; const SOCKET_URL = "http://10.10.2.47:5000";
const API_BASE = "http://192.168.1.10:5000/api"; const API_BASE = "http://10.10.2.47:5000/api";
// --- STATE & SELECTED STATION --- // --- STATE & SELECTED STATION ---
let selectedStation = null; let selectedStation = null;

View File

@ -1,7 +1,7 @@
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
// --- CONFIGURATION --- // --- CONFIGURATION ---
const SOCKET_URL = "http://192.168.1.10:5000"; const SOCKET_URL = "http://10.10.2.47:5000";
const API_BASE = "http://192.168.1.10:5000/api"; // Added for API calls const API_BASE = "http://10.10.2.47:5000/api"; // Added for API calls
// --- DOM ELEMENT REFERENCES --- // --- DOM ELEMENT REFERENCES ---
const grid = document.getElementById('chambersGrid'); const grid = document.getElementById('chambersGrid');

View File

@ -1,7 +1,7 @@
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
// --- CONFIGURATION --- // --- CONFIGURATION ---
const SOCKET_URL = "http://192.168.1.10:5000"; const SOCKET_URL = "http://10.10.2.47:5000";
const API_BASE = "http://192.168.1.10:5000/api"; const API_BASE = "http://10.10.2.47:5000/api";
// --- DOM ELEMENT REFERENCES --- // --- DOM ELEMENT REFERENCES ---
const stationNameEl = document.getElementById('station-name'); const stationNameEl = document.getElementById('station-name');

View File

@ -4,7 +4,7 @@ document.addEventListener('DOMContentLoaded', () => {
const stationCountEl = document.getElementById('station-count'); // Make sure you have an element with this ID in your HTML const stationCountEl = document.getElementById('station-count'); // Make sure you have an element with this ID in your HTML
// --- CONFIG & STATE --- // --- CONFIG & STATE ---
const API_BASE = 'http://192.168.1.10:5000/api'; const API_BASE = 'http://10.10.2.47:5000/api';
let allStations = []; // Master list of stations from the API let allStations = []; // Master list of stations from the API
let pollingInterval = null; let pollingInterval = null;
@ -117,10 +117,7 @@ document.addEventListener('DOMContentLoaded', () => {
//-- NEW: Fetch and apply daily stats to each card --- //-- NEW: Fetch and apply daily stats to each card ---
const fetchAndApplyStats = async () => { const fetchAndApplyStats = async () => {
try { try {
const response = await fetch(`${API_BASE}/stations/daily-stats`, { const response = await fetch(`${API_BASE}/stations/daily-stats`);
method: 'GET',
credentials: 'include' // <-- ADD THIS LINE
});
if (!response.ok) return; // Fail silently if stats aren't available if (!response.ok) return; // Fail silently if stats aren't available
const stats = await response.json(); const stats = await response.json();
@ -161,10 +158,7 @@ document.addEventListener('DOMContentLoaded', () => {
} }
try { try {
const response = await fetch(`${API_BASE}/stations/${stationId}`, { const response = await fetch(`${API_BASE}/stations/${stationId}`, { method: 'DELETE' });
method: 'DELETE',
credentials: 'include' // <-- ADD THIS LINE
});
if (response.ok) { if (response.ok) {
alert(`Station "${stationName}" removed successfully.`); alert(`Station "${stationName}" removed successfully.`);
allStations = []; // Force a full refresh on next poll allStations = []; // Force a full refresh on next poll
@ -181,61 +175,26 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
// --- DATA FETCHING & POLLING --- // --- DATA FETCHING & POLLING ---
// const loadAndPollStations = async () => {
// try {
// const response = await fetch(`${API_BASE}/stations`, {
// method: 'GET',
// credentials: 'include' // <-- ADD THIS LINE
// });
// if (!response.ok) throw new Error('Failed to fetch stations');
// const newStationList = await response.json();
// // If the number of stations has changed, we must do a full re-render.
// if (newStationList.length !== allStations.length) {
// allStations = newStationList;
// renderStations(allStations);
// } else {
// // Otherwise, we can do a more efficient status-only update.
// allStations = newStationList;
// updateStationStatuses(allStations);
// fetchAndApplyStats(); // Fetch and update daily stats
// }
// } catch (error) {
// console.error(error);
// stationCountEl.textContent = 'Could not load stations. Is the backend running?';
// if (pollingInterval) clearInterval(pollingInterval);
// }
// };
const loadAndPollStations = async () => { const loadAndPollStations = async () => {
try { try {
const response = await fetch(`${API_BASE}/stations`, { credentials: 'include' }); const response = await fetch(`${API_BASE}/stations`);
if (response.status === 401) {
localStorage.clear();
window.location.href = 'index.html';
return;
}
if (!response.ok) throw new Error('Failed to fetch stations'); if (!response.ok) throw new Error('Failed to fetch stations');
const newStationList = await response.json(); const newStationList = await response.json();
// If the number of stations has changed, we must do a full re-render.
if (newStationList.length !== allStations.length) { if (newStationList.length !== allStations.length) {
allStations = newStationList; allStations = newStationList;
renderStations(allStations); renderStations(allStations);
} else { } else {
// Otherwise, we can do a more efficient status-only update.
allStations = newStationList; allStations = newStationList;
// A more efficient status-only update could go here later updateStationStatuses(allStations);
renderStations(allStations); // Re-render to update statuses fetchAndApplyStats(); // Fetch and update daily stats
} }
// --- THIS IS THE FIX ---
// Call this AFTER the if/else, so it always runs on a successful fetch.
fetchAndApplyStats();
} catch (error) { } catch (error) {
console.error(error); console.error(error);
if (stationCountEl) stationCountEl.textContent = 'Could not load stations. Is the backend running?'; stationCountEl.textContent = 'Could not load stations. Is the backend running?';
if (pollingInterval) clearInterval(pollingInterval); if (pollingInterval) clearInterval(pollingInterval);
} }
}; };

View File

@ -225,35 +225,75 @@
</div> </div>
<script> <script>
const API_BASE = 'http://192.168.1.10:5000/api'; const API_BASE = 'http://10.10.2.47:5000/api';
// --- DOM Elements ---
const grid = document.getElementById('stations-grid'); const grid = document.getElementById('stations-grid');
const addStationCardTmpl = document.getElementById('add-station-card-template'); const addStationCardTmpl = document.getElementById('add-station-card-template');
const stationCardTmpl = document.getElementById('station-card-template'); const stationCardTmpl = document.getElementById('station-card-template');
const searchEl = document.getElementById('search'); const searchEl = document.getElementById('search');
const emptyState = document.getElementById('empty-state'); const emptyState = document.getElementById('empty-state');
const errorState = document.getElementById('error-state'); const errorState = document.getElementById('error-state');
// THEMED STATUS DROPDOWN LOGIC
const statusBtn = document.getElementById('statusBtn'); const statusBtn = document.getElementById('statusBtn');
const statusMenu = document.getElementById('statusMenu'); const statusMenu = document.getElementById('statusMenu');
const statusLabel = document.getElementById('statusLabel'); const statusLabel = document.getElementById('statusLabel');
let statusValue = 'all';
// Modals
const userModal = document.getElementById('userModal'); const userModal = document.getElementById('userModal');
const stationModal = document.getElementById('stationModal'); 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 openModal = (el) => { el.classList.remove('hidden'); el.classList.add('block'); };
const closeModal = (el) => { el.classList.add('hidden'); el.classList.remove('block'); }; const closeModal = (el) => { el.classList.add('hidden'); el.classList.remove('block'); };
// Header buttons
document.getElementById('addUserBtn').onclick = () => openModal(userModal); document.getElementById('addUserBtn').onclick = () => openModal(userModal);
document.getElementById('cancelUserBtn').onclick = () => closeModal(userModal); document.getElementById('cancelUserBtn').onclick = () => closeModal(userModal);
document.getElementById('logoutBtn').onclick = () => { localStorage.clear(); window.location.href = './index.html'; }; document.getElementById('logoutBtn').onclick = () => { localStorage.clear(); window.location.href = './index.html'; };
document.getElementById('cancelStationBtn').onclick = () => closeModal(stationModal); document.getElementById('cancelStationBtn').onclick = () => closeModal(stationModal);
// (Your form submission logic for users and stations can remain the same)
// ... // Forms
document.getElementById('userForm').onsubmit = async (e)=>{
e.preventDefault();
const payload = { username: newUsername.value.trim(), password: newPassword.value, is_admin: isAdmin.checked };
try {
const res = await fetch(`${API_BASE}/users`, {
method:'POST',
headers:{'Content-Type':'application/json'},
body: JSON.stringify(payload),
credentials: 'include'
});
if(!res.ok) throw new Error('Failed to add user');
closeModal(userModal); alert('User added');
} catch(err){ alert(err.message); }
}
document.getElementById('stationForm').onsubmit = async (e)=>{
e.preventDefault();
const payload = {
station_id: stationId.value.trim(),
product_id: stationProductId.value.trim(),
name: stationName.value.trim(),
location: stationLocation.value.trim(),
mqtt_broker: mqttBroker.value.trim(),
mqtt_port: Number(mqttPort.value),
mqtt_user: mqttUsername.value || null,
mqtt_password: mqttPassword.value || null,
};
try {
const res = await fetch(`${API_BASE}/stations`, {
method:'POST',
headers:{'Content-Type':'application/json'},
body: JSON.stringify(payload),
credentials: 'include'
});
if(!res.ok) throw new Error('Failed to add station');
closeModal(stationModal); await loadStations();
} catch(err){ alert(err.message); }
}
function statusStyles(status){ 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 online = { dot:'bg-emerald-400 animate-pulseDot', badge:'bg-emerald-500/15 text-emerald-300 border border-emerald-400/20', text:'Online' };
@ -261,28 +301,41 @@
return String(status).toLowerCase()==='online'?online:offline; return String(status).toLowerCase()==='online'?online:offline;
} }
// --- Main Rendering Function --- function setStatus(val, label) {
function render(stationsToDisplay) { statusValue = val;
grid.innerHTML = ''; statusLabel.textContent = label;
errorState.classList.add('hidden'); statusMenu.classList.add('hidden');
applyFilters(); // reuse your existing function
}
if (!stationsToDisplay || stationsToDisplay.length === 0) { let allStations = [];
function render(stations){
grid.innerHTML = '';
if(!stations || stations.length===0){
emptyState.classList.remove('hidden'); emptyState.classList.remove('hidden');
} else { } else {
emptyState.classList.add('hidden'); emptyState.classList.add('hidden');
stationsToDisplay.forEach(s => { for(const s of stations){
const node = stationCardTmpl.content.cloneNode(true); const node = stationCardTmpl.content.cloneNode(true);
const card = node.querySelector('div'); 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('.station-name').textContent = s.name ?? `Station ${s.id || s.station_id}`;
card.querySelector('.product-id').textContent = s.product_id || '—'; // const productIdVal = s.product_id || '—';
// const productIdEl = card.querySelector('.product-id');
// if (productIdEl) {
// // Use .innerHTML and add a styled <span> for the title
// productIdEl.innerHTML = `<span class="font-semibold text-white-500">Product ID: </span>${productIdVal}`;
// }
const productIdVal = s.product_id || '—';
const productIdEl = card.querySelector('.product-id');
if (productIdEl) {
productIdEl.textContent = productIdVal;
}
card.querySelector('.station-location').textContent = s.location ?? '—'; card.querySelector('.station-location').textContent = s.location ?? '—';
const idVal = s.id || s.station_id || '—'; const idVal = s.id || s.station_id || '—';
const idEl = card.querySelector('.station-id'); const idEl = card.querySelector('.station-id');
idEl.textContent = idVal; idEl.textContent = idVal; idEl.setAttribute('title', idVal);
idEl.setAttribute('title', idVal);
const styles = statusStyles(s.status); const styles = statusStyles(s.status);
const dot = card.querySelector('.status-dot'); const dot = card.querySelector('.status-dot');
@ -291,35 +344,40 @@
badge.className = `status-badge rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide ${styles.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; badge.textContent = styles.text;
card.querySelector('.metric-starts').textContent = 0; // Metrics
card.querySelector('.metric-success').textContent = 0; const starts = s.total_swaps_started ?? s.metrics?.total_starts ?? 0;
card.querySelector('.metric-aborted').textContent = 0; const success = s.total_swaps_success ?? s.metrics?.total_completed ?? 0;
const aborted = s.total_swaps_aborted ?? s.metrics?.total_aborted ?? 0;
card.querySelector('.metric-starts').textContent = starts;
card.querySelector('.metric-success').textContent = success;
card.querySelector('.metric-aborted').textContent = aborted;
// --- FIX: Added the full logic for the Open button --- // Open
card.querySelector('.open-btn').addEventListener('click', () => { card.querySelector('.open-btn').addEventListener('click', () => {
localStorage.setItem('selected_station', JSON.stringify(s));
const id = encodeURIComponent(s.id || s.station_id); const id = encodeURIComponent(s.id || s.station_id);
window.location.href = `./dashboard.html?stationId=${id}`; window.location.href = `./dashboard.html?stationId=${id}`;
}); });
// --- ADD THIS NEW BLOCK FOR THE REMOVE BUTTON ---
// --- FIX: Added the full logic for the Remove button ---
card.querySelector('.remove-btn').addEventListener('click', async () => { card.querySelector('.remove-btn').addEventListener('click', async () => {
const stationId = s.id || s.station_id; const stationId = s.id || s.station_id;
const stationName = s.name; const stationName = s.name;
// 1. Confirm with the user
if (!confirm(`Are you sure you want to permanently remove "${stationName}"?`)) { if (!confirm(`Are you sure you want to permanently remove "${stationName}"?`)) {
return; return;
} }
try { try {
// 2. Call the DELETE API endpoint
const response = await fetch(`${API_BASE}/stations/${stationId}`, { const response = await fetch(`${API_BASE}/stations/${stationId}`, {
method: 'DELETE', method: 'DELETE',
credentials: 'include'
}); });
if (response.ok) { if (response.ok) {
alert(`Station "${stationName}" removed successfully.`); alert(`Station "${stationName}" removed successfully.`);
// Refresh the list immediately // 3. Refresh the entire list from the server
loadAndPollStations(); loadStations();
} else { } else {
const error = await response.json(); const error = await response.json();
alert(`Failed to remove station: ${error.message}`); alert(`Failed to remove station: ${error.message}`);
@ -331,11 +389,13 @@
}); });
grid.appendChild(node); grid.appendChild(node);
});
} }
// Append the "Add Station" card }
// Finally, append the Add Station card LAST
const addNode = addStationCardTmpl.content.cloneNode(true); const addNode = addStationCardTmpl.content.cloneNode(true);
addNode.querySelector('div').addEventListener('click', () => openModal(stationModal)); const addCard = addNode.querySelector('div');
addCard.addEventListener('click', () => openModal(stationModal));
grid.appendChild(addNode); grid.appendChild(addNode);
if (window.lucide) { if (window.lucide) {
@ -343,85 +403,42 @@
} }
} }
statusBtn.addEventListener('click', () => {
statusMenu.classList.toggle('hidden');
});
statusMenu.querySelectorAll('button').forEach(b=>{
b.addEventListener('click', () => setStatus(b.dataset.value, b.textContent.trim()));
});
function applyFilters(){ function applyFilters(){
const q = (searchEl.value||'').trim().toLowerCase(); const q = (searchEl.value||'').trim().toLowerCase();
const status = statusValue; // 'all' | 'online' | 'offline'
const filtered = allStations.filter(s=>{ 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 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; const matchesStatus = status==='all' || String(s.status).toLowerCase()===status;
return matchesQ && matchesStatus; return matchesQ && matchesStatus;
}); });
render(filtered); render(filtered);
} }
// --- NEW: Function to Fetch and Apply Daily Stats --- searchEl.addEventListener('input', ()=> setTimeout(applyFilters,150));
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(); async function loadStations(){
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{ try{
const res = await fetch(`${API_BASE}/stations`); const res = await fetch(`${API_BASE}/stations`);
if (!res.ok) throw new Error('Failed to fetch stations');
const data = await res.json(); const data = await res.json();
const newStationList = Array.isArray(data) ? data : (data.stations || []); allStations = Array.isArray(data) ? data : (data.stations||[]);
applyFilters();
// 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){ }catch(err){
console.error(err);
errorState.textContent = 'Failed to load stations. Ensure API is running.'; errorState.textContent = 'Failed to load stations. Ensure API is running.';
errorState.classList.remove('hidden'); errorState.classList.remove('hidden');
if (pollingInterval) clearInterval(pollingInterval); // Stop polling on error
} }
}; }
document.addEventListener('click', (e)=>{
// --- Event Listeners for Filters --- if (!document.getElementById('statusFilterWrap').contains(e.target)) statusMenu.classList.add('hidden');
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
}); });
document.addEventListener('DOMContentLoaded', loadStations);
</script> </script>
</body> </body>
</html> </html>