feat(app): Implement station removal and refactor to common header
This major update introduces the ability to remove stations and significantly refactors the frontend architecture for better code reuse and maintainability. ### Features - **Remove Station:** Implemented full-stack station removal functionality. - Added `DELETE /api/stations/<id>` endpoint to the backend. - The MQTT client for the removed station is now gracefully disconnected. - Added a trash can icon button to the station selection card for removal. - **Enhanced UI:** Improved the UI on several pages. - Added icons for Product ID and Location on station cards. - Updated card layout to display Product ID directly under the station name. - Implemented a fully functional, dynamic header for the dashboard, logs, and analytics pages.main
parent
494e6b4c7c
commit
526bc8e8fa
Binary file not shown.
Binary file not shown.
|
|
@ -1,3 +1,87 @@
|
|||
# import paho.mqtt.client as mqtt
|
||||
# import uuid
|
||||
# import time
|
||||
# import threading
|
||||
# import socket
|
||||
|
||||
# class MqttClient:
|
||||
# """
|
||||
# Handles the connection and message processing for a single MQTT station.
|
||||
# This is a standard Python class, with no GUI dependencies.
|
||||
# """
|
||||
# def __init__(self, broker, port, user, password, station_id, on_message_callback):
|
||||
# self.broker = broker
|
||||
# self.port = port
|
||||
# self.user = user
|
||||
# self.password = password
|
||||
# self.station_id = station_id
|
||||
# self.on_message_callback = on_message_callback
|
||||
|
||||
# unique_id = str(uuid.uuid4())
|
||||
# self.client_id = f"WebApp-Backend-{self.station_id}-{unique_id}"
|
||||
|
||||
# self.client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, self.client_id)
|
||||
|
||||
# # Assign callback functions
|
||||
# self.client.on_connect = self.on_connect
|
||||
# self.client.on_message = self.on_message
|
||||
# self.client.on_disconnect = self.on_disconnect
|
||||
|
||||
# if self.user and self.password:
|
||||
# self.client.username_pw_set(self.user, self.password)
|
||||
|
||||
# self.is_connected = False
|
||||
# self.reconnect_delay = 1
|
||||
# self.max_reconnect_delay = 60
|
||||
# self.stop_thread = False
|
||||
|
||||
# # --- CORRECTED CALLBACK SIGNATURES ---
|
||||
# def on_connect(self, client, userdata, flags, reason_code, properties):
|
||||
# """Callback for when the client connects to the broker."""
|
||||
# if reason_code == 0:
|
||||
# self.is_connected = True
|
||||
# self.reconnect_delay = 1
|
||||
# print(f"Successfully connected to MQTT broker for station: {self.station_id}")
|
||||
# topic_base = f"VEC/batterySmartStation/v100/{self.station_id}/#"
|
||||
# # topic_base = f"VEC/batterySmartStation/v100/+/+"
|
||||
# self.client.subscribe(topic_base)
|
||||
# print(f"Subscribed to: {topic_base}")
|
||||
# else:
|
||||
# print(f"Failed to connect to MQTT for station {self.station_id}, return code {reason_code}")
|
||||
|
||||
# def on_disconnect(self, client, userdata, disconnect_flags, reason_code, properties):
|
||||
# """Callback for when the client disconnects."""
|
||||
# print(f"Disconnected from MQTT for station {self.station_id}. Will attempt to reconnect...")
|
||||
|
||||
# def on_message(self, client, userdata, msg):
|
||||
# """Callback for when a message is received from the broker."""
|
||||
# try:
|
||||
# self.on_message_callback(self.station_id, msg.topic, msg.payload)
|
||||
# except Exception as e:
|
||||
# print(f"Error processing message in callback for topic {msg.topic}: {e}")
|
||||
|
||||
# def connect(self):
|
||||
# """Connects the client to the MQTT broker."""
|
||||
# print(f"Attempting to connect to {self.broker}:{self.port} with client ID: {self.client_id}")
|
||||
# try:
|
||||
# self.client.connect(self.broker, self.port, 60)
|
||||
# except Exception as e:
|
||||
# print(f"Error connecting to MQTT for station {self.station_id}: {e}")
|
||||
|
||||
# def start(self):
|
||||
# """Starts the MQTT client's network loop in a separate thread."""
|
||||
# self.connect()
|
||||
# self.client.loop_start()
|
||||
|
||||
# def stop(self):
|
||||
# """Stops the MQTT client's network loop."""
|
||||
# print(f"Stopping MQTT client for station: {self.station_id}")
|
||||
# self.client.loop_stop()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
import paho.mqtt.client as mqtt
|
||||
import uuid
|
||||
import time
|
||||
|
|
@ -31,19 +115,15 @@ class MqttClient:
|
|||
self.client.username_pw_set(self.user, self.password)
|
||||
|
||||
self.is_connected = False
|
||||
self.reconnect_delay = 1
|
||||
self.max_reconnect_delay = 60
|
||||
self.stop_thread = False
|
||||
self.stop_thread = False # <-- We will use this flag
|
||||
|
||||
# --- CORRECTED CALLBACK SIGNATURES ---
|
||||
# --- (Your on_connect, on_disconnect, and on_message methods stay the same) ---
|
||||
def on_connect(self, client, userdata, flags, reason_code, properties):
|
||||
"""Callback for when the client connects to the broker."""
|
||||
if reason_code == 0:
|
||||
self.is_connected = True
|
||||
self.reconnect_delay = 1
|
||||
print(f"Successfully connected to MQTT broker for station: {self.station_id}")
|
||||
topic_base = f"VEC/batterySmartStation/v100/{self.station_id}/#"
|
||||
# topic_base = f"VEC/batterySmartStation/v100/+/+"
|
||||
self.client.subscribe(topic_base)
|
||||
print(f"Subscribed to: {topic_base}")
|
||||
else:
|
||||
|
|
@ -51,7 +131,13 @@ class MqttClient:
|
|||
|
||||
def on_disconnect(self, client, userdata, disconnect_flags, reason_code, properties):
|
||||
"""Callback for when the client disconnects."""
|
||||
self.is_connected = False
|
||||
# Only print reconnect message if it wasn't a deliberate stop
|
||||
if not self.stop_thread:
|
||||
print(f"Disconnected from MQTT for station {self.station_id}. Will attempt to reconnect...")
|
||||
else:
|
||||
print(f"Intentionally disconnected from MQTT for station {self.station_id}.")
|
||||
|
||||
|
||||
def on_message(self, client, userdata, msg):
|
||||
"""Callback for when a message is received from the broker."""
|
||||
|
|
@ -60,20 +146,33 @@ class MqttClient:
|
|||
except Exception as e:
|
||||
print(f"Error processing message in callback for topic {msg.topic}: {e}")
|
||||
|
||||
def connect(self):
|
||||
"""Connects the client to the MQTT broker."""
|
||||
print(f"Attempting to connect to {self.broker}:{self.port} with client ID: {self.client_id}")
|
||||
def run(self):
|
||||
"""A blocking loop that handles connection and reconnection."""
|
||||
while not self.stop_thread:
|
||||
try:
|
||||
print(f"Attempting to connect to {self.broker}:{self.port} for station {self.station_id}")
|
||||
self.client.connect(self.broker, self.port, 60)
|
||||
self.client.loop_forever() # This is a blocking call
|
||||
break # Exit loop if loop_forever finishes cleanly
|
||||
except socket.error as e:
|
||||
print(f"Connection error for {self.station_id}: {e}. Retrying in 5 seconds...")
|
||||
time.sleep(5)
|
||||
except Exception as e:
|
||||
print(f"Error connecting to MQTT for station {self.station_id}: {e}")
|
||||
print(f"An unexpected error occurred for {self.station_id}: {e}. Retrying in 5 seconds...")
|
||||
time.sleep(5)
|
||||
|
||||
def start(self):
|
||||
"""Starts the MQTT client's network loop in a separate thread."""
|
||||
self.connect()
|
||||
self.client.loop_start()
|
||||
# --- CHANGED ---
|
||||
# We now run our custom `run` method in a thread
|
||||
main_thread = threading.Thread(target=self.run)
|
||||
main_thread.daemon = True
|
||||
main_thread.start()
|
||||
|
||||
def stop(self):
|
||||
"""Stops the MQTT client's network loop."""
|
||||
# --- CHANGED ---
|
||||
# This is the complete, correct way to stop the client
|
||||
print(f"Stopping MQTT client for station: {self.station_id}")
|
||||
self.client.loop_stop()
|
||||
self.stop_thread = True
|
||||
self.client.disconnect() # This tells the client to disconnect gracefully
|
||||
|
|
@ -191,6 +191,7 @@ def add_station():
|
|||
|
||||
new_station = Station(
|
||||
station_id=data['station_id'],
|
||||
product_id=data['product_id'],
|
||||
name=data['name'],
|
||||
location=data['location'],
|
||||
mqtt_broker=data['mqtt_broker'],
|
||||
|
|
@ -207,6 +208,38 @@ def add_station():
|
|||
return jsonify({"message": "Station added successfully."}), 201
|
||||
|
||||
|
||||
# The new function with logging
|
||||
@app.route('/api/stations/<string:station_id>', methods=['DELETE'])
|
||||
def remove_station(station_id):
|
||||
"""
|
||||
Removes a station from the database and stops its MQTT client.
|
||||
"""
|
||||
print(f"\n--- REMOVE REQUEST RECEIVED for station: {station_id} ---")
|
||||
|
||||
# 1. Find the station in the database
|
||||
station = Station.query.filter_by(station_id=station_id).first_or_404()
|
||||
print(f"[LOG] Found station '{station.name}' in the database.")
|
||||
|
||||
# 2. Stop the running MQTT client for this station
|
||||
client_to_stop = mqtt_clients.get(station_id)
|
||||
if client_to_stop:
|
||||
print(f"[LOG] Found active MQTT client. Attempting to stop it now...")
|
||||
client_to_stop.stop()
|
||||
mqtt_clients.pop(station_id, None)
|
||||
print(f"[LOG] Successfully stopped and removed client object for {station_id}.")
|
||||
else:
|
||||
print(f"[LOG] No active MQTT client was found for {station_id}. No action needed.")
|
||||
|
||||
# 3. Delete the station from the database
|
||||
print(f"[LOG] Attempting to delete {station_id} from the database...")
|
||||
db.session.delete(station)
|
||||
db.session.commit()
|
||||
print(f"[LOG] Successfully deleted station from the database.")
|
||||
|
||||
print(f"--- REMOVE REQUEST COMPLETED for station: {station_id} ---\n")
|
||||
return jsonify({"message": f"Station {station_id} removed successfully."}), 200
|
||||
|
||||
|
||||
@app.route('/api/stations', methods=['GET'])
|
||||
def get_stations():
|
||||
try:
|
||||
|
|
@ -222,6 +255,7 @@ def get_stations():
|
|||
"id": s.station_id,
|
||||
"name": s.name,
|
||||
"location": s.location,
|
||||
"product_id": s.product_id,
|
||||
"status": "Online" if is_online else "Offline"
|
||||
})
|
||||
return jsonify(station_list)
|
||||
|
|
@ -508,6 +542,7 @@ if __name__ == '__main__':
|
|||
print("No stations found. Adding a default station.")
|
||||
default_station = Station(
|
||||
station_id="V16000862287077265957",
|
||||
product_id="VEC_PROD_001",
|
||||
name="Test Station 1",
|
||||
mqtt_broker="mqtt.vecmocon.com",
|
||||
mqtt_port=1883,
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ class Station(db.Model):
|
|||
"""Represents a battery swap station in the database."""
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
station_id = db.Column(db.String(120), unique=True, nullable=False)
|
||||
product_id = db.Column(db.String(80), unique=True, nullable=False)
|
||||
name = db.Column(db.String(120), nullable=True)
|
||||
location = db.Column(db.String(200), nullable=True)
|
||||
mqtt_broker = db.Column(db.String(255), nullable=False)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
<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"> </div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-center scale-100">
|
||||
<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>Station ID:</span>
|
||||
<span id="station-id-display" class="font-semibold">—</span>
|
||||
</span>
|
||||
<span class="badge border-white/10 bg-white/5 text-slate-200">
|
||||
<span>Product ID:</span>
|
||||
<span id="product-id-display" class="font-semibold">—</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">Waiting...</span>
|
||||
</span>
|
||||
<span id="connection-status-chip" class="cham_chip cham_chip-amber" title="Connection to backend">
|
||||
<span class="h-2 w-2 rounded-full bg-amber-400"></span> Connecting...
|
||||
</span>
|
||||
<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 id="main-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 text-gray-400 hover:text-gray-200 font-semibold">Main</a>
|
||||
<a href="./logs.html" class="px-4 py-2 text-sm text-gray-400 hover:text-gray-200 font-semibold">Logs</a>
|
||||
<a href="./analytics.html" class="px-4 py-2 text-sm text-gray-400 hover:text-gray-200 font-semibold">Analytics</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
|
@ -5,6 +5,9 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Swap Station – Analytics</title>
|
||||
|
||||
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
|
||||
|
||||
<script src="js/common-header.js"></script>
|
||||
<script src="js/auth-guard.js"></script>
|
||||
|
||||
<!-- Inter + Tailwind -->
|
||||
|
|
@ -12,6 +15,10 @@
|
|||
<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 src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
|
||||
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
|
|
@ -46,6 +53,14 @@
|
|||
.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)}
|
||||
|
||||
.glass { background: rgba(30,41,59,.45); border: 1px solid rgba(255,255,255,.10); border-radius: .9rem; backdrop-filter: saturate(150%) blur(12px); }
|
||||
.tile { background: rgba(2,6,23,.45); border:1px solid rgba(255,255,255,.12); border-radius:.9rem; padding:1rem; }
|
||||
.btn { font-weight:800; font-size:12px; padding:.5rem .7rem; border-radius:.6rem; border:1px solid rgba(255,255,255,.12); background:rgba(255,255,255,.06); transition:.18s; }
|
||||
|
|
@ -62,10 +77,8 @@
|
|||
</head>
|
||||
<body class="min-h-screen text-gray-100 bg-glow">
|
||||
|
||||
<!-- STATUS BAR + TABS -->
|
||||
<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">
|
||||
<!-- Left -->
|
||||
<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"
|
||||
|
|
@ -83,32 +96,47 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Center -->
|
||||
<div class="flex items-center justify-center">
|
||||
<div class="flex items-center justify-center scale-100">
|
||||
<img src="./assets/vec_logo.png" alt="VECMOCON"
|
||||
class="h-7 w-auto opacity-90" onerror="this.style.display='none'"/>
|
||||
</div>
|
||||
|
||||
<!-- Right badges/actions -->
|
||||
<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>Product ID:</span>
|
||||
<span id="product-id" class="font-semibold">—</span>
|
||||
</span>
|
||||
|
||||
<span class="badge border-white/10 bg-white/5 text-slate-200">
|
||||
<span>Station ID:</span>
|
||||
<span id="device-id" class="font-semibold">—</span>
|
||||
</span>
|
||||
|
||||
<!-- <span class="badge border-white/10 bg-white/5 text-slate-200">
|
||||
<span>Device ID:</span>
|
||||
<span id="device-id">—</span>
|
||||
</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 id="last-update-status">Waiting...</span>
|
||||
</span>
|
||||
|
||||
<span class="chip chip-emerald">
|
||||
<span class="h-2 w-2 rounded-full bg-emerald-400 animate-pulseDot"></span> Online
|
||||
<span id="connection-status-chip" class="cham_chip cham_chip-amber" title="Connection to backend">
|
||||
<span class="h-2 w-2 rounded-full bg-amber-400"></span> Connecting...
|
||||
</span>
|
||||
|
||||
<span class="chip chip-amber" title="Running on backup supply">On Backup</span>
|
||||
<button id="station-reset-btn" class="btn btn-danger !p-2" title="Station Reset">
|
||||
<svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M18.36 6.64a9 9 0 1 1-12.73 0"/><line x1="12" y1="2" x2="12" y2="12"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- <span id="backup-power-chip" class="cham_chip cham_chip-slate" title="Station power source" style="display: none;">On Backup</span> -->
|
||||
<!-- <span class="chip chip-amber" title="Running on backup supply">On Backup</span> -->
|
||||
|
||||
<div class="hidden sm:block w-px h-5 bg-white/10"></div>
|
||||
|
||||
|
|
@ -132,7 +160,6 @@
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="border-t border-white/10 bg-black/10">
|
||||
|
|
@ -144,6 +171,7 @@
|
|||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
<!-- CONTENT -->
|
||||
<main class="relative z-10 mx-auto max-w-7.5xl px-3 sm:px-4 py-4 flex flex-col gap-4">
|
||||
<!-- Device + Date Range -->
|
||||
|
|
@ -249,13 +277,6 @@
|
|||
|
||||
<script>
|
||||
// Fill device id from selected station (if stored by station_selection)
|
||||
(function setDevice() {
|
||||
const el = document.querySelector('#device-id');
|
||||
try {
|
||||
const sel = JSON.parse(localStorage.getItem('selected_station') || '{}');
|
||||
el.textContent = sel?.id || sel?.station_id || '—';
|
||||
} catch { el.textContent = '—'; }
|
||||
})();
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Replace 'VEC-STN-0128' with the actual ID of the station you want to load
|
||||
|
|
@ -303,9 +324,6 @@
|
|||
window.location.href = './index.html';
|
||||
});
|
||||
document.querySelector('#refreshBtn')?.addEventListener('click', () => location.reload());
|
||||
document.querySelector('#downloadBtn')?.addEventListener('click', () => {
|
||||
alert('Hook this to your /api/logs/export (or analytics export) endpoint.');
|
||||
});
|
||||
|
||||
// Date range apply (wire to backend later)
|
||||
document.querySelector('#applyRange')?.addEventListener('click', () => {
|
||||
|
|
@ -315,5 +333,8 @@
|
|||
alert(`Apply analytics range:\n${f} → ${t}`);
|
||||
});
|
||||
</script>
|
||||
|
||||
<script src="./js/analytics.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -159,6 +159,7 @@
|
|||
<div class="absolute -bottom-24 -right-24 w-[36rem] h-[36rem] rounded-full bg-sky-500/10 blur-3xl"></div>
|
||||
</div>
|
||||
|
||||
<!-- <header> -->
|
||||
<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">
|
||||
|
|
@ -186,9 +187,19 @@
|
|||
<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>Product ID:</span>
|
||||
<span id="product-id" class="font-semibold">—</span>
|
||||
</span>
|
||||
|
||||
<span class="badge border-white/10 bg-white/5 text-slate-200">
|
||||
<span>Station ID:</span>
|
||||
<span id="device-id" class="font-semibold">—</span>
|
||||
</span>
|
||||
|
||||
<!-- <span class="badge border-white/10 bg-white/5 text-slate-200">
|
||||
<span>Device ID:</span>
|
||||
<span id="device-id">—</span>
|
||||
</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">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,117 @@
|
|||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// --- CONFIGURATION ---
|
||||
const SOCKET_URL = "http://192.168.1.12:5000";
|
||||
const API_BASE = "http://192.168.1.12:5000/api";
|
||||
|
||||
// --- DOM ELEMENT REFERENCES ---
|
||||
const stationNameEl = document.getElementById('station-name');
|
||||
const stationLocationEl = document.getElementById('station-location');
|
||||
const deviceIdEl = document.getElementById('device-id');
|
||||
const lastUpdateEl = document.getElementById('last-update-status');
|
||||
const connChip = document.getElementById('connection-status-chip');
|
||||
const requestLogArea = document.getElementById('request-log-area');
|
||||
const eventLogArea = document.getElementById('event-log-area');
|
||||
const clearReqBtn = document.getElementById('clear-req');
|
||||
const clearEvtBtn = document.getElementById('clear-evt');
|
||||
const clearAllBtn = document.getElementById('clear-all');
|
||||
const refreshBtn = document.getElementById('refreshBtn');
|
||||
const downloadBtn = document.getElementById('downloadBtn');
|
||||
const logoutBtn = document.getElementById('logout-btn');
|
||||
const resetBtn = document.getElementById('station-reset-btn');
|
||||
|
||||
// --- STATE ---
|
||||
let selectedStation = null;
|
||||
let socket;
|
||||
let statusPollingInterval;
|
||||
|
||||
// --- HELPER FUNCTIONS ---
|
||||
const prependLog = (textarea, data) => {
|
||||
if (!textarea) return;
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
const formattedJson = JSON.stringify(data, null, 2);
|
||||
const newLog = `[${timestamp}]\n${formattedJson}\n\n---------------------------------\n\n`;
|
||||
textarea.value = newLog + textarea.value;
|
||||
};
|
||||
|
||||
const sendCommand = (command, data = null) => {
|
||||
if (!selectedStation || !socket || !socket.connected) {
|
||||
console.error(`Cannot send command '${command}', not connected.`);
|
||||
return;
|
||||
}
|
||||
const payload = { station_id: selectedStation.id, command: command, data: data };
|
||||
socket.emit('rpc_request', payload);
|
||||
};
|
||||
|
||||
const checkStationStatus = async () => {
|
||||
if (!selectedStation) return;
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/stations`);
|
||||
if (!response.ok) return;
|
||||
const stations = await response.json();
|
||||
const thisStation = stations.find(s => s.id === selectedStation.id);
|
||||
if (thisStation) {
|
||||
stationNameEl.textContent = thisStation.name;
|
||||
stationLocationEl.textContent = thisStation.location;
|
||||
if (thisStation.status === 'Online') {
|
||||
connChip.innerHTML = `<span class="h-2 w-2 rounded-full bg-emerald-400 animate-pulseDot"></span> Online`;
|
||||
connChip.className = 'cham_chip cham_chip-emerald';
|
||||
} else {
|
||||
connChip.innerHTML = `<span class="h-2 w-2 rounded-full bg-rose-500"></span> Offline`;
|
||||
connChip.className = 'cham_chip cham_chip-rose';
|
||||
}
|
||||
}
|
||||
} catch (error) { console.error("Failed to fetch station status:", error); }
|
||||
};
|
||||
|
||||
// --- INITIALIZATION ---
|
||||
try {
|
||||
selectedStation = JSON.parse(localStorage.getItem('selected_station'));
|
||||
if (!selectedStation || !selectedStation.id) {
|
||||
throw new Error('No station selected. Please go back to the selection page.');
|
||||
}
|
||||
deviceIdEl.textContent = selectedStation.id;
|
||||
} catch (e) {
|
||||
document.body.innerHTML = `<div class="text-center p-8 text-rose-400">${e.message}<a href="./station_selection.html" class="underline ml-2">Go Back</a></div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
// --- SOCKET.IO CONNECTION ---
|
||||
socket = io(SOCKET_URL);
|
||||
socket.on('connect', () => {
|
||||
console.log("Connected to WebSocket for logs.");
|
||||
socket.emit('join_station_room', { station_id: selectedStation.id });
|
||||
});
|
||||
|
||||
socket.on('dashboard_update', (message) => {
|
||||
const { stationId, topic, data } = message;
|
||||
if (stationId !== selectedStation.id) return;
|
||||
|
||||
lastUpdateEl.textContent = 'Last Recv ' + new Date().toLocaleTimeString();
|
||||
|
||||
if (topic.endsWith('EVENTS')) {
|
||||
prependLog(eventLogArea, data);
|
||||
} else if (topic.endsWith('REQUEST')) {
|
||||
prependLog(requestLogArea, data);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if(logoutBtn) logoutBtn.addEventListener('click', () => {
|
||||
localStorage.clear();
|
||||
window.location.href = 'index.html';
|
||||
});
|
||||
if(refreshBtn) refreshBtn.addEventListener('click', () => location.reload());
|
||||
// if(downloadBtn) downloadBtn.addEventListener('click', () => alert("Download functionality can be added here.")); // Placeholder for download modal
|
||||
if(resetBtn) resetBtn.addEventListener('click', () => {
|
||||
if (confirm('Are you sure you want to reset the station?')) {
|
||||
sendCommand('STATION_RESET');
|
||||
}
|
||||
});
|
||||
|
||||
// --- STARTUP ---
|
||||
checkStationStatus();
|
||||
statusPollingInterval = setInterval(checkStationStatus, 10000);
|
||||
if (typeof lucide !== 'undefined') {
|
||||
lucide.createIcons();
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,239 @@
|
|||
// frontend/js/common-header.js
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// --- CONFIGURATION ---
|
||||
const SOCKET_URL = "http://192.168.1.12:5000";
|
||||
const API_BASE = "http://192.168.1.12:5000/api";
|
||||
|
||||
// --- STATE & SELECTED STATION ---
|
||||
let selectedStation = null;
|
||||
let socket;
|
||||
let statusPollingInterval;
|
||||
|
||||
try {
|
||||
selectedStation = JSON.parse(localStorage.getItem('selected_station'));
|
||||
if (!selectedStation || !selectedStation.id) {
|
||||
window.location.href = './station_selection.html';
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
window.location.href = './station_selection.html';
|
||||
return;
|
||||
}
|
||||
|
||||
// --- HEADER DOM ELEMENTS ---
|
||||
const stationNameEl = document.getElementById('station-name');
|
||||
const stationLocationEl = document.getElementById('station-location');
|
||||
const stationIdEl = document.getElementById('station-id-display');
|
||||
const productIdEl = document.getElementById('product-id-display');
|
||||
const connChip = document.getElementById('connection-status-chip');
|
||||
const lastUpdateEl = document.getElementById('last-update-status');
|
||||
const logoutBtn = document.getElementById('logout-btn');
|
||||
const navLinks = document.getElementById('main-nav');
|
||||
const downloadBtn = document.getElementById('downloadBtn');
|
||||
|
||||
// --- INITIALIZATION ---
|
||||
function initializeHeader() {
|
||||
// Populate header with initial data
|
||||
if (stationNameEl) stationNameEl.textContent = selectedStation.name || 'Unknown';
|
||||
if (stationLocationEl) stationLocationEl.textContent = selectedStation.location || 'No location';
|
||||
if (stationIdEl) stationIdEl.textContent = selectedStation.id;
|
||||
if (productIdEl) productIdEl.textContent = selectedStation.product_id;
|
||||
|
||||
// Highlight the active tab
|
||||
if (navLinks) {
|
||||
const currentPage = window.location.pathname.split('/').pop();
|
||||
const activeLink = navLinks.querySelector(`a[href="./${currentPage}"]`);
|
||||
if (activeLink) {
|
||||
activeLink.classList.remove('text-gray-400', 'hover:text-gray-200');
|
||||
activeLink.classList.add('border-b-2', 'border-emerald-400/70', 'text-white');
|
||||
}
|
||||
}
|
||||
|
||||
// Start polling and connect WebSocket
|
||||
checkStationStatus();
|
||||
statusPollingInterval = setInterval(checkStationStatus, 10000);
|
||||
connectSocket();
|
||||
|
||||
// Add event listener for the logout button
|
||||
if (logoutBtn) {
|
||||
logoutBtn.addEventListener('click', () => {
|
||||
localStorage.clear();
|
||||
window.location.href = './index.html';
|
||||
});
|
||||
}
|
||||
|
||||
if (downloadBtn) {
|
||||
downloadBtn.addEventListener('click', showDownloadModal);
|
||||
}
|
||||
}
|
||||
|
||||
// --- POLLING FOR ONLINE/OFFLINE STATUS ---
|
||||
const checkStationStatus = async () => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/stations`);
|
||||
if (!response.ok) return;
|
||||
const stations = await response.json();
|
||||
const thisStation = stations.find(s => s.id === selectedStation.id);
|
||||
|
||||
if (thisStation && connChip) {
|
||||
if (thisStation.status === 'Online') {
|
||||
connChip.innerHTML = `<span class="h-2 w-2 rounded-full bg-emerald-400 animate-pulseDot"></span> Online`;
|
||||
connChip.className = 'cham_chip cham_chip-emerald';
|
||||
} else {
|
||||
connChip.innerHTML = `<span class="h-2 w-2 rounded-full bg-rose-500"></span> Offline`;
|
||||
connChip.className = 'cham_chip cham_chip-rose';
|
||||
if (lastUpdateEl) lastUpdateEl.textContent = "Waiting for data...";
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch station status:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const showDownloadModal = () => {
|
||||
const modalOverlay = document.createElement('div');
|
||||
modalOverlay.className = "fixed inset-0 bg-black/70 backdrop-blur-sm flex items-center justify-center z-50";
|
||||
|
||||
modalOverlay.innerHTML = `
|
||||
<div class="bg-slate-800 border border-slate-700 rounded-lg shadow-xl p-6 w-full max-w-md">
|
||||
<h3 class="text-lg font-bold text-white mb-4">Export Logs</h3>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Quick Time Ranges</label>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<button data-range="1" class="time-range-btn btn btn-ghost !py-1.5">Last Hour</button>
|
||||
<button data-range="6" class="time-range-btn btn btn-ghost !py-1.5">Last 6 Hours</button>
|
||||
<button data-range="24" class="time-range-btn btn btn-ghost !py-1.5">Last 24 Hours</button>
|
||||
<button data-range="today" class="time-range-btn btn btn-ghost !py-1.5">Today</button>
|
||||
<button data-range="yesterday" class="time-range-btn btn btn-ghost !py-1.5">Yesterday</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="log-type" class="block text-sm font-medium text-gray-300">Log Type</label>
|
||||
<select id="log-type" class="mt-1 block w-full bg-slate-700 border-slate-600 rounded-md p-2 text-white">
|
||||
<option value="PERIODIC">Periodic Data</option>
|
||||
<option value="EVENT">Events & RPC</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="start-datetime" class="block text-sm font-medium text-gray-300">Start Date & Time</label>
|
||||
<input type="text" id="start-datetime" class="mt-1 block w-full bg-slate-700 border-slate-600 rounded-md p-2 text-white">
|
||||
</div>
|
||||
<div>
|
||||
<label for="end-datetime" class="block text-sm font-medium text-gray-300">End Date & Time</label>
|
||||
<input type="text" id="end-datetime" class="mt-1 block w-full bg-slate-700 border-slate-600 rounded-md p-2 text-white">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6 flex justify-end gap-3">
|
||||
<button id="cancel-download" class="btn btn-ghost px-4 py-2">Cancel</button>
|
||||
<button id="confirm-download" class="btn btn-primary px-4 py-2">Download CSV</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(modalOverlay);
|
||||
|
||||
const startInput = document.getElementById('start-datetime');
|
||||
const endInput = document.getElementById('end-datetime');
|
||||
|
||||
const fpConfig = {
|
||||
enableTime: true,
|
||||
dateFormat: "Y-m-d\\TH:i",
|
||||
time_24hr: true
|
||||
};
|
||||
const fpStart = flatpickr(startInput, fpConfig);
|
||||
const fpEnd = flatpickr(endInput, fpConfig);
|
||||
|
||||
const now = new Date();
|
||||
fpStart.setDate(new Date(now.getTime() - 3600 * 1000), true);
|
||||
fpEnd.setDate(now, true);
|
||||
|
||||
modalOverlay.querySelectorAll('.time-range-btn').forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
const range = button.dataset.range;
|
||||
const now = new Date();
|
||||
let start = new Date();
|
||||
if (range === 'today') {
|
||||
start.setHours(0, 0, 0, 0);
|
||||
} else if (range === 'yesterday') {
|
||||
start.setDate(start.getDate() - 1);
|
||||
start.setHours(0, 0, 0, 0);
|
||||
now.setDate(now.getDate() - 1);
|
||||
now.setHours(23, 59, 59, 999);
|
||||
} else {
|
||||
start.setHours(now.getHours() - parseInt(range, 10));
|
||||
}
|
||||
fpStart.setDate(start, true);
|
||||
fpEnd.setDate(now, true);
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('cancel-download').onclick = () => document.body.removeChild(modalOverlay);
|
||||
|
||||
document.getElementById('confirm-download').onclick = async () => {
|
||||
const logType = document.getElementById('log-type').value;
|
||||
const startDateStr = document.getElementById('start-datetime').value;
|
||||
const endDateStr = document.getElementById('end-datetime').value;
|
||||
const confirmBtn = document.getElementById('confirm-download');
|
||||
|
||||
if (!startDateStr || !endDateStr) {
|
||||
return alert('Please select both a start and end date/time.');
|
||||
}
|
||||
|
||||
confirmBtn.textContent = 'Fetching...';
|
||||
confirmBtn.disabled = true;
|
||||
|
||||
const downloadUrl = `${API_BASE}/logs/export?station_id=${selectedStation.id}&start_datetime=${startDateStr}&end_datetime=${endDateStr}&log_type=${logType}`;
|
||||
|
||||
try {
|
||||
const response = await fetch(downloadUrl);
|
||||
if (response.ok) {
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.style.display = 'none';
|
||||
a.href = url;
|
||||
|
||||
let filename = `${selectedStation.name || selectedStation.id}_${logType}_${startDateStr.split('T')[0]}.csv`;
|
||||
const disposition = response.headers.get('Content-Disposition');
|
||||
if (disposition && disposition.includes('attachment')) {
|
||||
const filenameMatch = disposition.match(/filename="(.+?)"/);
|
||||
if (filenameMatch && filenameMatch.length === 2) filename = filenameMatch[1];
|
||||
}
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
document.body.removeChild(modalOverlay);
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
alert(`Could not download: ${errorData.message}`);
|
||||
}
|
||||
} catch (error) {
|
||||
alert('An unexpected error occurred. Please check the console.');
|
||||
console.error('Download error:', error);
|
||||
} finally {
|
||||
confirmBtn.textContent = 'Download CSV';
|
||||
confirmBtn.disabled = false;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// --- WEBSOCKET CONNECTION ---
|
||||
const connectSocket = () => {
|
||||
socket = io(SOCKET_URL);
|
||||
socket.on('connect', () => {
|
||||
console.log("Header: Connected to WebSocket.");
|
||||
socket.emit('join_station_room', { station_id: selectedStation.id });
|
||||
});
|
||||
socket.on('disconnect', () => console.log("Header: Disconnected from WebSocket."));
|
||||
socket.on('dashboard_update', (message) => {
|
||||
if (message.stationId === selectedStation.id && lastUpdateEl) {
|
||||
lastUpdateEl.textContent = `Last Recv ${new Date().toLocaleTimeString()}`;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// --- START THE SCRIPT ---
|
||||
initializeHeader();
|
||||
});
|
||||
|
|
@ -107,7 +107,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
window.location.href = 'index.html';
|
||||
});
|
||||
if(refreshBtn) refreshBtn.addEventListener('click', () => location.reload());
|
||||
if(downloadBtn) downloadBtn.addEventListener('click', () => alert("Download functionality can be added here.")); // Placeholder for download modal
|
||||
// if(downloadBtn) downloadBtn.addEventListener('click', () => alert("Download functionality can be added here.")); // Placeholder for download modal
|
||||
if(resetBtn) resetBtn.addEventListener('click', () => {
|
||||
if (confirm('Are you sure you want to reset the station?')) {
|
||||
sendCommand('STATION_RESET');
|
||||
|
|
|
|||
|
|
@ -0,0 +1,79 @@
|
|||
// // frontend/js/page-header.js
|
||||
// document.addEventListener('DOMContentLoaded', () => {
|
||||
// // 1. Get the station data from Local Storage
|
||||
// const selectedStation = JSON.parse(localStorage.getItem('selected_station'));
|
||||
|
||||
// // 2. Safety check: If no station is selected, go back to the selection page
|
||||
// if (!selectedStation) {
|
||||
// alert('No station selected. Redirecting to the selection page.');
|
||||
// window.location.href = 'station_selection.html';
|
||||
// return;
|
||||
// }
|
||||
|
||||
// // 3. Find all the display elements in the header
|
||||
// const stationNameEl = document.getElementById('station-name');
|
||||
// const stationLocationEl = document.getElementById('station-location');
|
||||
// const stationIdEl = document.getElementById('station-id-display');
|
||||
// const productIdEl = document.getElementById('product-id-display');
|
||||
|
||||
// // 4. Update the elements with the station's data
|
||||
// if (stationNameEl) stationNameEl.textContent = selectedStation.name;
|
||||
// if (stationLocationEl) stationLocationEl.textContent = selectedStation.location;
|
||||
// if (stationIdEl) stationIdEl.textContent = selectedStation.id;
|
||||
// if (productIdEl) productIdEl.textContent = selectedStation.product_id;
|
||||
// });
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// frontend/js/page-header.js
|
||||
|
||||
// This function fetches the common header and injects it into the page
|
||||
async function loadHeader() {
|
||||
try {
|
||||
const response = await fetch('_header.html');
|
||||
if (!response.ok) {
|
||||
throw new Error('Could not load the header file.');
|
||||
}
|
||||
const headerHTML = await response.text();
|
||||
// Adds the header right after the opening <body> tag
|
||||
document.body.insertAdjacentHTML('afterbegin', headerHTML);
|
||||
} catch (error) {
|
||||
console.error('Failed to load header:', error);
|
||||
// Optionally, display an error to the user
|
||||
}
|
||||
}
|
||||
|
||||
// This function populates the header with data from localStorage
|
||||
function populateHeaderData() {
|
||||
const selectedStation = JSON.parse(localStorage.getItem('selected_station'));
|
||||
|
||||
if (!selectedStation) {
|
||||
alert('No station selected. Redirecting...');
|
||||
window.location.href = 'station_selection.html';
|
||||
return;
|
||||
}
|
||||
|
||||
const stationNameEl = document.getElementById('station-name');
|
||||
const stationLocationEl = document.getElementById('station-location');
|
||||
const stationIdEl = document.getElementById('station-id-display');
|
||||
const productIdEl = document.getElementById('product-id-display');
|
||||
|
||||
if (stationNameEl) stationNameEl.textContent = selectedStation.name;
|
||||
if (stationLocationEl) stationLocationEl.textContent = selectedStation.location;
|
||||
if (stationIdEl) stationIdEl.textContent = selectedStation.id;
|
||||
if (productIdEl) productIdEl.textContent = selectedStation.product_id;
|
||||
}
|
||||
|
||||
// Main execution block
|
||||
// We use an async function to make sure the header is loaded BEFORE we try to fill it
|
||||
async function initializePageHeader() {
|
||||
await loadHeader();
|
||||
populateHeaderData();
|
||||
}
|
||||
|
||||
// Run the initialization when the page content is loaded
|
||||
document.addEventListener('DOMContentLoaded', initializePageHeader);
|
||||
|
|
@ -1,25 +1,232 @@
|
|||
// document.addEventListener('DOMContentLoaded', () => {
|
||||
// // --- DOM ELEMENTS ---
|
||||
// const stationsGrid = document.getElementById('stations-grid');
|
||||
// const stationTemplate = document.getElementById('stationCardTemplate');
|
||||
// const errorMessage = document.getElementById('error-message');
|
||||
// const stationCountEl = document.getElementById('station-count');
|
||||
// // Note: SocketIO is not needed on this page anymore
|
||||
|
||||
// let allStations = []; // To store the master list of stations
|
||||
|
||||
// // --- AUTHENTICATION & USER INFO (Your existing code is perfect) ---
|
||||
// const user = JSON.parse(localStorage.getItem('user'));
|
||||
// if (!user) {
|
||||
// window.location.href = 'index.html'; // Redirect if not logged in
|
||||
// return;
|
||||
// }
|
||||
|
||||
// // User info and logout button logic... (omitted for brevity, no changes needed)
|
||||
|
||||
// // --- ADMIN FEATURES (Your existing code is perfect) ---
|
||||
// // Admin button and add station card logic... (omitted for brevity, no changes needed)
|
||||
|
||||
|
||||
// // --- HELPER FUNCTIONS ---
|
||||
// const getStatusAttributes = (status) => {
|
||||
// switch (status) {
|
||||
// case 'Online': return { color: 'text-green-500', bgColor: 'bg-green-100/60 dark:bg-green-500/10', icon: 'power' };
|
||||
// case 'Offline': return { color: 'text-red-500', bgColor: 'bg-red-100/60 dark:bg-red-500/10', icon: 'power-off' };
|
||||
// default: return { color: 'text-gray-500', bgColor: 'bg-gray-100/60 dark:bg-gray-500/10', icon: 'help-circle' };
|
||||
// }
|
||||
// };
|
||||
|
||||
// const handleStationSelect = (stationId) => {
|
||||
// window.location.href = `dashboard.html?station_id=${stationId}`;
|
||||
// };
|
||||
|
||||
// // This function now only renders the initial grid
|
||||
// // const renderStations = (stations) => {
|
||||
// // stationsGrid.innerHTML = '';
|
||||
// // stationCountEl.textContent = `${stations.length} stations found. Select one to monitor.`;
|
||||
|
||||
// // stations.forEach(station => {
|
||||
// // const status = getStatusAttributes(station.status);
|
||||
// // const card = document.createElement('div');
|
||||
// // card.className = "group bg-gray-900/60 backdrop-blur-xl rounded-2xl shadow-lg border border-gray-700 transition-transform duration-300 ease-out cursor-pointer flex flex-col justify-between 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)]";
|
||||
// // card.id = `station-${station.id}`;
|
||||
// // card.onclick = () => handleStationSelect(station.id);
|
||||
|
||||
// // card.innerHTML = `
|
||||
// // <div class="p-5 flex-grow">
|
||||
// // <div class="flex justify-between items-start">
|
||||
// // <h3 class="text-lg font-bold text-white pr-2">${station.name}</h3>
|
||||
// // <div class="status-badge flex items-center text-xs font-semibold px-3 py-1 rounded-full ${status.bgColor} ${status.color}">
|
||||
// // <i data-lucide="${status.icon}" class="w-4 h-4 mr-1.5"></i>
|
||||
// // <span class="status-text">${station.status}</span>
|
||||
// // </div>
|
||||
// // </div>
|
||||
// // <p class="text-sm text-gray-400 mt-1">${station.id}</p>
|
||||
// // </div>
|
||||
// // <div class="border-t border-gray-700/50 px-5 py-2 flex justify-end">
|
||||
// // <button
|
||||
// // class="remove-btn text-xs font-semibold text-red-500 hover:text-red-400 transition"
|
||||
// // data-id="${station.id}"
|
||||
// // data-name="${station.name}"
|
||||
// // >
|
||||
// // Remove Station
|
||||
// // </button>
|
||||
// // </div>
|
||||
// // `;
|
||||
// // stationsGrid.appendChild(card);
|
||||
// // });
|
||||
// // lucide.createIcons();
|
||||
// // };
|
||||
|
||||
// const renderStations = (stations) => {
|
||||
// stationsGrid.innerHTML = '';
|
||||
// stationCountEl.textContent = `${stations.length} stations found. Select one to monitor.`;
|
||||
|
||||
// stations.forEach(station => {
|
||||
// const status = getStatusAttributes(station.status);
|
||||
// const card = document.createElement('div');
|
||||
// card.className = "group bg-gray-900/60 backdrop-blur-xl rounded-2xl shadow-lg border border-gray-700 transition-transform duration-300 ease-out flex flex-col justify-between 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)]";
|
||||
// card.id = `station-${station.id}`;
|
||||
|
||||
// // --- Make the main card content clickable ---
|
||||
// const mainContent = document.createElement('div');
|
||||
// mainContent.className = "p-5 flex-grow cursor-pointer";
|
||||
// mainContent.onclick = () => handleStationSelect(station.id);
|
||||
// mainContent.innerHTML = `
|
||||
// <div class="flex justify-between items-start">
|
||||
// <h3 class="text-lg font-bold text-white pr-2">${station.name}</h3>
|
||||
// <div class="status-badge flex items-center text-xs font-semibold px-3 py-1 rounded-full ${status.bgColor} ${status.color}">
|
||||
// <i data-lucide="${status.icon}" class="w-4 h-4 mr-1.5"></i>
|
||||
// <span class="status-text">${station.status}</span>
|
||||
// </div>
|
||||
// </div>
|
||||
// <p class="text-sm text-gray-400 mt-1">${station.id}</p>
|
||||
// `;
|
||||
|
||||
// // --- Create the footer with the remove button ---
|
||||
// const footer = document.createElement('div');
|
||||
// footer.className = "border-t border-gray-700/50 px-5 py-2 flex justify-end";
|
||||
// footer.innerHTML = `
|
||||
// <button
|
||||
// class="remove-btn text-xs font-semibold text-red-500 hover:text-red-400 transition"
|
||||
// data-id="${station.id}"
|
||||
// data-name="${station.name}"
|
||||
// >
|
||||
// Remove Station
|
||||
// </button>
|
||||
// `;
|
||||
|
||||
// card.appendChild(mainContent);
|
||||
// card.appendChild(footer);
|
||||
// stationsGrid.appendChild(card);
|
||||
// });
|
||||
// lucide.createIcons();
|
||||
// };
|
||||
|
||||
// // --- NEW: Function to update statuses without redrawing everything ---
|
||||
// const updateStationStatuses = (stations) => {
|
||||
// stations.forEach(station => {
|
||||
// const card = document.getElementById(`station-${station.id}`);
|
||||
// if (card) {
|
||||
// const status = getStatusAttributes(station.status);
|
||||
// const statusBadge = card.querySelector('.status-badge');
|
||||
// const statusText = card.querySelector('.status-text');
|
||||
// const statusIcon = card.querySelector('i[data-lucide]');
|
||||
|
||||
// if (statusBadge && statusText && statusIcon) {
|
||||
// statusBadge.className = `status-badge flex items-center text-xs font-semibold px-3 py-1 rounded-full ${status.bgColor} ${status.color}`;
|
||||
// statusText.textContent = station.status;
|
||||
// statusIcon.setAttribute('data-lucide', status.icon);
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
// lucide.createIcons(); // Re-render icons if any changed
|
||||
// };
|
||||
|
||||
// // --- DATA FETCHING & STATUS POLLING ---
|
||||
// const loadAndPollStations = async () => {
|
||||
// try {
|
||||
// const response = await fetch('http://192.168.1.12:5000/api/stations');
|
||||
// if (!response.ok) throw new Error('Failed to fetch stations');
|
||||
// const stations = await response.json();
|
||||
|
||||
// // Check if this is the first time loading data
|
||||
// if (allStations.length === 0) {
|
||||
// allStations = stations;
|
||||
// renderStations(allStations); // Initial full render
|
||||
// } else {
|
||||
// allStations = stations;
|
||||
// updateStationStatuses(allStations); // Subsequent, efficient updates
|
||||
// }
|
||||
|
||||
// } catch (error) {
|
||||
// console.error(error);
|
||||
// stationCountEl.textContent = 'Could not load stations. Is the backend running?';
|
||||
// // Stop polling on error
|
||||
// if (pollingInterval) clearInterval(pollingInterval);
|
||||
// }
|
||||
// };
|
||||
|
||||
// // In station_selection.js
|
||||
// stationsGrid.addEventListener('click', async (event) => {
|
||||
// const removeButton = event.target.closest('.remove-btn');
|
||||
|
||||
// if (!removeButton) return;
|
||||
|
||||
// // Stop the click from triggering the card's navigation
|
||||
// event.stopPropagation();
|
||||
|
||||
// const stationId = removeButton.dataset.id;
|
||||
// const stationName = removeButton.dataset.name;
|
||||
|
||||
// if (!confirm(`Are you sure you want to permanently remove "${stationName}"?`)) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// try {
|
||||
// const response = await fetch(`http://192.168.1.12:5000/api/stations/${stationId}`, {
|
||||
// method: 'DELETE',
|
||||
// });
|
||||
|
||||
// if (response.ok) {
|
||||
// alert(`Station "${stationName}" removed successfully.`);
|
||||
// allStations = [];
|
||||
// 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.');
|
||||
// }
|
||||
// });
|
||||
|
||||
// // --- INITIALIZATION ---
|
||||
// loadAndPollStations(); // Load immediately on page start
|
||||
// // Then, set an interval to refresh the statuses every 10 seconds
|
||||
// const pollingInterval = setInterval(loadAndPollStations, 10000);
|
||||
// });
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// --- DOM ELEMENTS ---
|
||||
const stationsGrid = document.getElementById('stations-grid');
|
||||
const stationTemplate = document.getElementById('stationCardTemplate');
|
||||
const errorMessage = document.getElementById('error-message');
|
||||
const stationCountEl = document.getElementById('station-count');
|
||||
// Note: SocketIO is not needed on this page anymore
|
||||
const stationCountEl = document.getElementById('station-count'); // Make sure you have an element with this ID in your HTML
|
||||
|
||||
let allStations = []; // To store the master list of stations
|
||||
// --- CONFIG & STATE ---
|
||||
const API_BASE = 'http://192.168.1.12:5000/api';
|
||||
let allStations = []; // Master list of stations from the API
|
||||
let pollingInterval = null;
|
||||
|
||||
// --- AUTHENTICATION & USER INFO (Your existing code is perfect) ---
|
||||
// --- AUTHENTICATION ---
|
||||
const user = JSON.parse(localStorage.getItem('user'));
|
||||
if (!user) {
|
||||
window.location.href = 'index.html'; // Redirect if not logged in
|
||||
return;
|
||||
}
|
||||
|
||||
// User info and logout button logic... (omitted for brevity, no changes needed)
|
||||
|
||||
// --- ADMIN FEATURES (Your existing code is perfect) ---
|
||||
// Admin button and add station card logic... (omitted for brevity, no changes needed)
|
||||
|
||||
// (Your other button listeners for logout, add user, etc., can go here)
|
||||
// document.getElementById('logoutBtn').onclick = () => { ... };
|
||||
|
||||
// --- HELPER FUNCTIONS ---
|
||||
const getStatusAttributes = (status) => {
|
||||
|
|
@ -34,20 +241,22 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
window.location.href = `dashboard.html?station_id=${stationId}`;
|
||||
};
|
||||
|
||||
// This function now only renders the initial grid
|
||||
// --- UI RENDERING ---
|
||||
// This function's only job is to build the HTML. It does not add event listeners.
|
||||
const renderStations = (stations) => {
|
||||
stationsGrid.innerHTML = '';
|
||||
stationCountEl.textContent = `${stations.length} stations found. Select one to monitor.`;
|
||||
stationsGrid.innerHTML = ''; // Clear the grid
|
||||
stationCountEl.textContent = `${stations.length} stations found.`;
|
||||
|
||||
stations.forEach(station => {
|
||||
const status = getStatusAttributes(station.status);
|
||||
const card = document.createElement('div');
|
||||
card.className = "group bg-gray-900/60 backdrop-blur-xl rounded-2xl shadow-lg border border-gray-700 transition-transform duration-300 ease-out cursor-pointer flex flex-col justify-between 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)]";
|
||||
card.id = `station-${station.id}`;
|
||||
card.onclick = () => handleStationSelect(station.id);
|
||||
// Add station ID to the card's dataset for easy access
|
||||
card.dataset.stationId = station.id;
|
||||
card.dataset.stationName = station.name;
|
||||
card.className = "group bg-gray-900/60 backdrop-blur-xl rounded-2xl shadow-lg border border-gray-700 transition-transform duration-300 ease-out flex flex-col justify-between 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)]";
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="p-5">
|
||||
<div class="main-content p-5 flex-grow cursor-pointer">
|
||||
<div class="flex justify-between items-start">
|
||||
<h3 class="text-lg font-bold text-white pr-2">${station.name}</h3>
|
||||
<div class="status-badge flex items-center text-xs font-semibold px-3 py-1 rounded-full ${status.bgColor} ${status.color}">
|
||||
|
|
@ -57,22 +266,30 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
</div>
|
||||
<p class="text-sm text-gray-400 mt-1">${station.id}</p>
|
||||
</div>
|
||||
<div class="border-t border-gray-700/50 px-5 py-2 flex justify-end">
|
||||
<button
|
||||
class="remove-btn text-xs font-semibold text-red-500 hover:text-red-400 transition"
|
||||
>
|
||||
Remove Station
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
stationsGrid.appendChild(card);
|
||||
});
|
||||
|
||||
if (window.lucide) {
|
||||
lucide.createIcons();
|
||||
}
|
||||
};
|
||||
|
||||
// --- NEW: Function to update statuses without redrawing everything ---
|
||||
const updateStationStatuses = (stations) => {
|
||||
stations.forEach(station => {
|
||||
const card = document.getElementById(`station-${station.id}`);
|
||||
const card = stationsGrid.querySelector(`[data-station-id="${station.id}"]`);
|
||||
if (card) {
|
||||
const status = getStatusAttributes(station.status);
|
||||
const statusBadge = card.querySelector('.status-badge');
|
||||
const statusText = card.querySelector('.status-text');
|
||||
const statusIcon = card.querySelector('i[data-lucide]');
|
||||
|
||||
if (statusBadge && statusText && statusIcon) {
|
||||
statusBadge.className = `status-badge flex items-center text-xs font-semibold px-3 py-1 rounded-full ${status.bgColor} ${status.color}`;
|
||||
statusText.textContent = station.status;
|
||||
|
|
@ -80,35 +297,74 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
}
|
||||
}
|
||||
});
|
||||
lucide.createIcons(); // Re-render icons if any changed
|
||||
if (window.lucide) {
|
||||
lucide.createIcons();
|
||||
}
|
||||
};
|
||||
|
||||
// --- DATA FETCHING & STATUS POLLING ---
|
||||
const loadAndPollStations = async () => {
|
||||
try {
|
||||
const response = await fetch('http://192.168.1.12:5000/api/stations');
|
||||
if (!response.ok) throw new Error('Failed to fetch stations');
|
||||
const stations = await response.json();
|
||||
// --- MAIN EVENT LISTENER ---
|
||||
// This single listener handles all clicks on the grid for efficiency.
|
||||
stationsGrid.addEventListener('click', async (event) => {
|
||||
const mainContent = event.target.closest('.main-content');
|
||||
const removeButton = event.target.closest('.remove-btn');
|
||||
|
||||
// Check if this is the first time loading data
|
||||
if (allStations.length === 0) {
|
||||
allStations = stations;
|
||||
renderStations(allStations); // Initial full render
|
||||
} else {
|
||||
allStations = stations;
|
||||
updateStationStatuses(allStations); // Subsequent, efficient updates
|
||||
if (mainContent) {
|
||||
const card = mainContent.closest('[data-station-id]');
|
||||
if (card) {
|
||||
handleStationSelect(card.dataset.stationId);
|
||||
}
|
||||
} else if (removeButton) {
|
||||
event.stopPropagation(); // Prevent main content click
|
||||
const card = removeButton.closest('[data-station-id]');
|
||||
const stationId = card.dataset.stationId;
|
||||
const stationName = card.dataset.stationName;
|
||||
|
||||
if (!confirm(`Are you sure you want to permanently remove "${stationName}"?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/stations/${stationId}`, { method: 'DELETE' });
|
||||
if (response.ok) {
|
||||
alert(`Station "${stationName}" removed successfully.`);
|
||||
allStations = []; // Force a full refresh on next poll
|
||||
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.');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// --- DATA FETCHING & POLLING ---
|
||||
const loadAndPollStations = async () => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/stations`);
|
||||
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);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
stationCountEl.textContent = 'Could not load stations. Is the backend running?';
|
||||
// Stop polling on error
|
||||
if (pollingInterval) clearInterval(pollingInterval);
|
||||
}
|
||||
};
|
||||
|
||||
// --- INITIALIZATION ---
|
||||
loadAndPollStations(); // Load immediately on page start
|
||||
// Then, set an interval to refresh the statuses every 10 seconds
|
||||
const pollingInterval = setInterval(loadAndPollStations, 10000);
|
||||
pollingInterval = setInterval(loadAndPollStations, 10000);
|
||||
});
|
||||
|
|
@ -5,6 +5,8 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Swap Station – Logs</title>
|
||||
|
||||
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
|
||||
|
||||
<script src="js/auth-guard.js"></script>
|
||||
|
||||
<!-- Font + Tailwind -->
|
||||
|
|
@ -12,6 +14,9 @@
|
|||
<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.jsdelivr.net/npm/flatpickr"></script>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
|
||||
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
|
|
@ -99,9 +104,19 @@
|
|||
<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>Product ID:</span>
|
||||
<span id="product-id" class="font-semibold">—</span>
|
||||
</span>
|
||||
|
||||
<span class="badge border-white/10 bg-white/5 text-slate-200">
|
||||
<span>Station ID:</span>
|
||||
<span id="device-id" class="font-semibold">—</span>
|
||||
</span>
|
||||
|
||||
<!-- <span class="badge border-white/10 bg-white/5 text-slate-200">
|
||||
<span>Device ID:</span>
|
||||
<span id="device-id">—</span>
|
||||
</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">
|
||||
|
|
@ -204,6 +219,7 @@
|
|||
</div>
|
||||
</main>
|
||||
|
||||
<script src="./js/logs.js"></script>
|
||||
<script src="js/common-header.js"></script>
|
||||
<script src="./js/logs.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@
|
|||
<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 = {
|
||||
|
|
@ -111,7 +113,16 @@
|
|||
<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="mt-1 text-xs text-gray-400 station-location">Location</p>
|
||||
<!-- <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>
|
||||
|
|
@ -122,6 +133,10 @@
|
|||
<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 -->
|
||||
|
|
@ -140,9 +155,21 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="open-btn mt-4 w-full rounded-xl bg-gradient-to-r from-emerald-500 via-teal-500 to-cyan-500 px-3 py-2 text-xs font-semibold text-white transition group-hover:brightness-110 group-hover:-translate-y-px"> Open
|
||||
<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>
|
||||
|
||||
|
|
@ -181,6 +208,7 @@
|
|||
<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>
|
||||
|
|
@ -246,6 +274,7 @@
|
|||
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(),
|
||||
|
|
@ -253,6 +282,7 @@
|
|||
mqtt_user: mqttUsername.value || null,
|
||||
mqtt_password: mqttPassword.value || null,
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/stations`, {
|
||||
method:'POST',
|
||||
|
|
@ -291,6 +321,17 @@
|
|||
const node = stationCardTmpl.content.cloneNode(true);
|
||||
const card = node.querySelector('div');
|
||||
card.querySelector('.station-name').textContent = s.name ?? `Station ${s.id || s.station_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 ?? '—';
|
||||
const idVal = s.id || s.station_id || '—';
|
||||
const idEl = card.querySelector('.station-id');
|
||||
|
|
@ -317,6 +358,35 @@
|
|||
const id = encodeURIComponent(s.id || s.station_id);
|
||||
window.location.href = `./dashboard.html?stationId=${id}`;
|
||||
});
|
||||
// --- ADD THIS NEW BLOCK FOR THE REMOVE BUTTON ---
|
||||
card.querySelector('.remove-btn').addEventListener('click', async () => {
|
||||
const stationId = s.id || s.station_id;
|
||||
const stationName = s.name;
|
||||
|
||||
// 1. Confirm with the user
|
||||
if (!confirm(`Are you sure you want to permanently remove "${stationName}"?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 2. Call the DELETE API endpoint
|
||||
const response = await fetch(`${API_BASE}/stations/${stationId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert(`Station "${stationName}" removed successfully.`);
|
||||
// 3. Refresh the entire list from the server
|
||||
loadStations();
|
||||
} 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);
|
||||
}
|
||||
|
|
@ -327,6 +397,10 @@
|
|||
const addCard = addNode.querySelector('div');
|
||||
addCard.addEventListener('click', () => openModal(stationModal));
|
||||
grid.appendChild(addNode);
|
||||
|
||||
if (window.lucide) {
|
||||
lucide.createIcons();
|
||||
}
|
||||
}
|
||||
|
||||
statusBtn.addEventListener('click', () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue