fix: resolved the analytics and logs page bugs
parent
45d2bbdab8
commit
3a2cda3263
|
|
@ -39,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://10.10.1.183:5500","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://localhost:5173"}}) , "http://127.0.0.1:5500"
|
||||
# This tells Flask: "For any route starting with /api/, allow requests
|
||||
|
|
@ -104,8 +104,18 @@ def on_message_handler(station_id, topic, payload):
|
|||
'data': decoded_data
|
||||
}, room=station_id)
|
||||
|
||||
if message_type == 'PERIODIC':
|
||||
# For periodic messages, only calculate and send the live status
|
||||
# This logic is from your /api/stations route
|
||||
last_msg_time = last_message_timestamps.get(station_id)
|
||||
is_online = last_msg_time is not None and (time.time() - last_msg_time) < STATION_TIMEOUT_SECONDS
|
||||
status_text = "Online" if is_online else "Offline"
|
||||
|
||||
print(f"Sending live status update: {status_text}")
|
||||
socketio.emit('status_update', {'status': status_text}, room=station_id)
|
||||
|
||||
# Emit update notification to the analytics page
|
||||
if message_type in ['EVENTS', 'PERIODIC']:
|
||||
if message_type in ['EVENTS', 'REQUEST']:
|
||||
socketio.emit('analytics_updated', room=station_id)
|
||||
|
||||
|
||||
|
|
@ -277,8 +287,8 @@ def get_all_station_stats():
|
|||
"""
|
||||
try:
|
||||
# --- CHANGE THESE TWO LINES ---
|
||||
today_start = datetime.combine(datetime.now().date(), time.min) # Use local time
|
||||
today_end = datetime.combine(datetime.now().date(), time.max) # Use local time
|
||||
today_start = datetime.combine(datetime.utcnow().date(), time.min)
|
||||
today_end = datetime.combine(datetime.utcnow().date(), time.max)
|
||||
|
||||
# This is an efficient query that groups by station_id and counts events in one go
|
||||
stats = db.session.query(
|
||||
|
|
@ -309,30 +319,35 @@ def get_all_station_stats():
|
|||
|
||||
@app.route('/api/logs/recent/<string:station_id>', methods=['GET'])
|
||||
def get_recent_logs(station_id):
|
||||
"""
|
||||
Fetches the 50 most recent non-periodic (EVENTS and REQUEST) logs for a
|
||||
given station from the database.
|
||||
"""
|
||||
# Get parameters from the request, with defaults
|
||||
start_date_str = request.args.get('start_date', datetime.now().strftime('%Y-%m-%d'))
|
||||
end_date_str = request.args.get('end_date', datetime.now().strftime('%Y-%m-%d'))
|
||||
limit_count = request.args.get('count', 50, type=int)
|
||||
|
||||
try:
|
||||
# --- THIS IS THE FIX ---
|
||||
# Query the MqttLog table, filtering for only EVENTS and REQUEST topic types.
|
||||
start_date = datetime.strptime(start_date_str, '%Y-%m-%d').date()
|
||||
end_date = datetime.strptime(end_date_str, '%Y-%m-%d').date()
|
||||
start_datetime = datetime.combine(start_date, datetime.min.time()) # <-- FIX
|
||||
end_datetime = datetime.combine(end_date, datetime.max.time())
|
||||
except ValueError:
|
||||
return jsonify({"message": "Invalid date format."}), 400
|
||||
|
||||
try:
|
||||
# The query now uses all three filters
|
||||
logs = MqttLog.query.filter(
|
||||
MqttLog.station_id == station_id,
|
||||
MqttLog.topic_type.in_(['EVENTS', 'REQUEST'])
|
||||
).order_by(desc(MqttLog.timestamp)).limit(50).all()
|
||||
MqttLog.topic_type.in_(['EVENTS', 'REQUEST']),
|
||||
MqttLog.timestamp.between(start_datetime, end_datetime)
|
||||
).order_by(desc(MqttLog.timestamp)).limit(limit_count).all()
|
||||
|
||||
# We reverse the list so the oldest are first, for correct display order
|
||||
logs.reverse()
|
||||
|
||||
log_list = [{
|
||||
"topic": log.topic,
|
||||
"payload": log.payload,
|
||||
"timestamp": log.timestamp.isoformat()
|
||||
"topic": log.topic, "payload": log.payload, "timestamp": log.timestamp.isoformat()
|
||||
} for log in logs]
|
||||
|
||||
return jsonify(log_list)
|
||||
except Exception as e:
|
||||
print(f"Error fetching recent logs: {e}")
|
||||
return jsonify({"message": "Could not fetch recent logs."}), 500
|
||||
|
||||
|
||||
|
|
@ -570,7 +585,7 @@ def get_analytics_data():
|
|||
daily_aborted[log_date] = daily_aborted.get(log_date, 0) + 1
|
||||
reason = log.payload.get('eventData', {}).get('swapAbortReason', 'ABORT_UNKNOWN')
|
||||
abort_reason_counts[reason] = abort_reason_counts.get(reason, 0) + 1
|
||||
elif event_type == 'EVENT_BATTERY_EXIT':
|
||||
elif event_type == 'EVENT_SLOT_LOCK_DISENEGAGED':
|
||||
slot_id = log.payload.get('eventData', {}).get('slotId')
|
||||
if slot_id and slot_id in slot_utilization_counts:
|
||||
slot_utilization_counts[slot_id] += 1
|
||||
|
|
@ -938,5 +953,5 @@ if __name__ == '__main__':
|
|||
mqtt_thread = threading.Thread(target=start_mqtt_clients, daemon=True)
|
||||
mqtt_thread.start()
|
||||
|
||||
print(f"Starting Flask-SocketIO server on http://10.10.1.183:5000")
|
||||
socketio.run(app, host='10.10.1.183', port=5000)
|
||||
print(f"Starting Flask-SocketIO server on http://192.168.1.10:5000")
|
||||
socketio.run(app, host='192.168.1.10', port=5000)
|
||||
|
|
|
|||
|
|
@ -1,340 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<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 -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&display=swap" rel="stylesheet">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
|
||||
<script 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: {
|
||||
extend: {
|
||||
fontFamily: { sans: ["Inter","ui-sans-serif","system-ui"] },
|
||||
keyframes: { pulseDot: { "0%,100%": { transform:"scale(1)", opacity:1 }, "50%": { transform:"scale(1.2)", opacity:.7 } } },
|
||||
animation: { pulseDot: "pulseDot 1.2s ease-in-out infinite" }
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
:root { color-scheme: dark; }
|
||||
html, body { height: 100%; }
|
||||
body { background:#0a0a0a; }
|
||||
|
||||
/* soft background glow */
|
||||
.bg-glow::before,
|
||||
.bg-glow::after {
|
||||
content:""; position:fixed; pointer-events:none; z-index:-1; border-radius:9999px; filter: blur(64px);
|
||||
}
|
||||
.bg-glow::before { width:34rem; height:34rem; left:-8rem; top:-8rem; background: rgba(16,185,129,.12); }
|
||||
.bg-glow::after { width:36rem; height:36rem; right:-10rem; bottom:-10rem; background: rgba(56,189,248,.10); }
|
||||
|
||||
.badge { font-size:12px; font-weight:800; padding:.35rem .6rem; border-radius:.6rem; border:1px solid; display:inline-flex; align-items:center; gap:.4rem; }
|
||||
.chip{display:inline-flex;align-items:center;gap:.4rem;padding:.4rem .5rem;border-radius:.4rem;
|
||||
font-size:10px;font-weight:800;letter-spacing:.02em;text-transform:uppercase;border:1px solid}
|
||||
.chip-emerald{background:rgba(16,185,129,.15);color:#a7f3d0;border-color:rgba(16,185,129,.35)}
|
||||
.chip-rose{background:rgba(244,63,94,.16);color:#fecaca;border-color:rgba(244,63,94,.35)}
|
||||
.chip-amber{background:rgba(245,158,11,.18);color:#fde68a;border-color:rgba(245,158,11,.40)}
|
||||
.chip-sky{background:rgba(56,189,248,.15);color:#bae6fd;border-color:rgba(56,189,248,.35)}
|
||||
.chip-slate{background:rgba(148,163,184,.12);color:#cbd5e1;border-color:rgba(148,163,184,.30)}
|
||||
|
||||
.cham_chip{display:inline-flex;align-items:center;gap:.4rem;padding:.4rem .5rem;border-radius:.4rem;
|
||||
font-size:10px;font-weight:800;letter-spacing:.02em;text-transform:uppercase;border:1px solid}
|
||||
.cham_chip-emerald{background:rgba(16,185,129,.15);color:#a7f3d0;border-color:rgba(16,185,129,.35)}
|
||||
.cham_chip-rose{background:rgba(244,63,94,.16);color:#fecaca;border-color:rgba(244,63,94,.35)}
|
||||
.cham_chip-amber{background:rgba(245,158,11,.18);color:#fde68a;border-color:rgba(245,158,11,.40)}
|
||||
.cham_chip-sky{background:rgba(56,189,248,.15);color:#bae6fd;border-color:rgba(56,189,248,.35)}
|
||||
.cham_chip-slate{background:rgba(148,163,184,.12);color:#cbd5e1;border-color:rgba(148,163,184,.30)}
|
||||
|
||||
.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; }
|
||||
.btn:hover { border-color: rgba(16,185,129,.45); background: rgba(16,185,129,.10); }
|
||||
.btn-ghost { border-color: rgba(255,255,255,.10); background: rgba(255,255,255,.05); }
|
||||
.btn-danger { border-color: rgba(244,63,94,.35); background: rgba(244,63,94,.14); color:#fecaca; }
|
||||
.btn-danger:hover { background: rgba(244,63,94,.22); }
|
||||
.badge { font-size:12px; font-weight:800; padding:.35rem .6rem; border-radius:.6rem; border:1px solid; display:inline-flex; align-items:center; gap:.4rem; }
|
||||
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; }
|
||||
|
||||
/* mini bar chart bars */
|
||||
.bar { width: 10px; border-radius: 6px 6px 0 0; background: linear-gradient(180deg,#22c55e,#0ea5e9); }
|
||||
</style>
|
||||
</head>
|
||||
<body class="min-h-screen text-gray-100 bg-glow">
|
||||
|
||||
<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>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 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="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>
|
||||
|
||||
<button id="refreshBtn" class="btn btn-ghost !p-2">
|
||||
<svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2">
|
||||
<path d="M21 12a9 9 0 10-3.5 7.1M21 12h-4m4 0l-2.5-2.5"></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button id="downloadBtn" class="btn btn-ghost !p-2">
|
||||
<svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2">
|
||||
<path d="M12 3v12m0 0l4-4m-4 4l-4-4"></path><path d="M5 21h14"></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button id="logout-btn" class="btn btn-danger !p-2">
|
||||
<svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2">
|
||||
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path>
|
||||
<path d="M16 17l5-5-5-5"></path><path d="M21 12H9"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="border-t border-white/10 bg-black/10">
|
||||
<nav class="mx-auto max-w-7.5xl px-3 sm:px-4 flex">
|
||||
<a href="./dashboard.html" class="px-4 py-2 text-sm text-gray-400 hover:text-gray-200 hover:border-b-2 hover:border-white/30 font-semibold">Main</a>
|
||||
<a href="./logs.html" class="px-4 py-2 text-sm text-gray-400 hover:text-gray-200 hover:border-b-2 hover:border-white/30 font-semibold">Logs</a>
|
||||
<a href="./analytics.html" class="px-4 py-2 text-sm font-semibold border-b-2 border-emerald-400/70 text-white">Analytics</a>
|
||||
</nav>
|
||||
</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 -->
|
||||
<section class="flex flex-wrap items-center gap-2">
|
||||
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg border border-white/10 bg-white/5 text-slate-200">
|
||||
<span>Device ID:</span>
|
||||
<span id="device-id" class="font-bold mono">—</span>
|
||||
</span>
|
||||
|
||||
<div class="ml-auto flex items-center gap-2">
|
||||
<input id="from" type="date" class="rounded-md bg-white/5 border border-white/10 px-2 py-1.5 text-sm outline-none focus:ring-2 focus:ring-emerald-500/60">
|
||||
<span class="text-gray-500">to</span>
|
||||
<input id="to" type="date" class="rounded-md bg-white/5 border border-white/10 px-2 py-1.5 text-sm outline-none focus:ring-2 focus:ring-emerald-500/60">
|
||||
<button id="applyRange" class="btn">Apply</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Stat Tiles -->
|
||||
<section class="grid grid-cols-1 md:grid-cols-4 gap-3">
|
||||
<div class="tile">
|
||||
<p class="text-xs text-gray-400">Total Swaps (Today)</p>
|
||||
<p class="text-3xl font-extrabold">142</p>
|
||||
</div>
|
||||
<div class="tile">
|
||||
<p class="text-xs text-gray-400">Avg. Swap Time</p>
|
||||
<p class="text-3xl font-extrabold">2.1 <span class="text-lg font-bold text-gray-300">min</span></p>
|
||||
</div>
|
||||
<div class="tile">
|
||||
<p class="text-xs text-gray-400">Station Uptime</p>
|
||||
<p class="text-3xl font-extrabold text-emerald-400">99.8%</p>
|
||||
</div>
|
||||
<div class="tile">
|
||||
<p class="text-xs text-gray-400">Peak Hours</p>
|
||||
<p class="text-3xl font-extrabold">5–7 PM</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Charts -->
|
||||
<section class="grid grid-cols-1 lg:grid-cols-2 gap-4 min-h-[20rem]">
|
||||
<!-- Weekly Swaps - CSS bars -->
|
||||
<div class="glass p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="font-extrabold">Swaps This Week</h3>
|
||||
<span class="text-xs text-gray-400">Mon → Sun</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 h-64 rounded-lg border border-white/10 bg-white/5 p-4 flex items-end gap-4 min-h-[20.8rem]">
|
||||
<!-- Each group: bar + label -->
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<div class="bar" style="height:50%"></div><span class="text-xs text-gray-400">Mon</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<div class="bar" style="height:74%"></div><span class="text-xs text-gray-400">Tue</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<div class="bar" style="height:60%"></div><span class="text-xs text-gray-400">Wed</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<div class="bar" style="height:85%"></div><span class="text-xs text-gray-400">Thu</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<div class="bar" style="height:92%"></div><span class="text-xs text-gray-400">Fri</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<div class="bar" style="height:42%"></div><span class="text-xs text-gray-400">Sat</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<div class="bar" style="height:30%"></div><span class="text-xs text-gray-400">Sun</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Battery Health - donut style -->
|
||||
<div class="glass p-4">
|
||||
<h3 class="font-extrabold">Battery Health</h3>
|
||||
<div class="h-64 flex items-center justify-center">
|
||||
<div class="relative w-52 h-52">
|
||||
<svg class="w-full h-full" viewBox="0 0 36 36">
|
||||
<path d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none" stroke="#1f2937" stroke-width="3"/>
|
||||
<path d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831"
|
||||
fill="none" stroke="#ef4444" stroke-width="3" stroke-dasharray="20, 100"/>
|
||||
<path d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831"
|
||||
fill="none" stroke="#f59e0b" stroke-width="3" stroke-dasharray="30, 100"/>
|
||||
<path d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831"
|
||||
fill="none" stroke="#22c55e" stroke-width="3" stroke-dasharray="50, 100"/>
|
||||
</svg>
|
||||
<div class="absolute inset-0 flex flex-col items-center justify-center">
|
||||
<span class="text-2xl font-extrabold">250</span>
|
||||
<span class="text-xs text-gray-400">Total Batteries</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 grid grid-cols-3 gap-2 text-xs">
|
||||
<div class="flex items-center gap-2"><span class="h-2.5 w-2.5 rounded-full bg-emerald-400"></span><span>Good</span></div>
|
||||
<div class="flex items-center gap-2"><span class="h-2.5 w-2.5 rounded-full bg-amber-400"></span><span>Warning</span></div>
|
||||
<div class="flex items-center gap-2"><span class="h-2.5 w-2.5 rounded-full bg-rose-400"></span><span>Poor</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// Fill device id from selected station (if stored by station_selection)
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Replace 'VEC-STN-0128' with the actual ID of the station you want to load
|
||||
const stationId = 'VEC-STN-0128';
|
||||
loadStationInfo(stationId);
|
||||
});
|
||||
|
||||
// This function fetches data from your backend and updates the page
|
||||
async function loadStationInfo(stationId) {
|
||||
// Find the HTML elements by their IDs
|
||||
const nameElement = document.getElementById('station-name');
|
||||
const locationElement = document.getElementById('station-location');
|
||||
|
||||
try {
|
||||
// 1. Fetch data from your backend API endpoint
|
||||
// You must replace this URL with your actual API endpoint
|
||||
const response = await fetch(`/api/stations/${stationId}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
|
||||
// 2. Convert the response into JSON format
|
||||
// Example JSON: { "name": "VEC-STN-0128", "location": "Sector 62, Noida" }
|
||||
const stationData = await response.json();
|
||||
|
||||
// 3. Update the HTML content with the data from the database
|
||||
nameElement.textContent = stationData.name;
|
||||
locationElement.textContent = stationData.location;
|
||||
|
||||
} catch (error) {
|
||||
// If something goes wrong, show an error message
|
||||
nameElement.textContent = 'Loading...';
|
||||
// locationElement.textContent = 'Could not fetch data.';
|
||||
console.error('Error fetching station data:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Demo "last recv" timestamp
|
||||
document.querySelector('#last-update-status').textContent =
|
||||
'Last Recv ' + new Date().toLocaleString();
|
||||
|
||||
// Actions
|
||||
document.querySelector('#logout-btn')?.addEventListener('click', () => {
|
||||
window.location.href = './index.html';
|
||||
});
|
||||
document.querySelector('#refreshBtn')?.addEventListener('click', () => location.reload());
|
||||
|
||||
// Date range apply (wire to backend later)
|
||||
document.querySelector('#applyRange')?.addEventListener('click', () => {
|
||||
const f = document.querySelector('#from').value;
|
||||
const t = document.querySelector('#to').value;
|
||||
if (!f || !t) return alert('Choose a date range first.');
|
||||
alert(`Apply analytics range:\n${f} → ${t}`);
|
||||
});
|
||||
</script>
|
||||
|
||||
<script src="./js/analytics.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,267 +1,84 @@
|
|||
// document.addEventListener('DOMContentLoaded', () => {
|
||||
// // --- CONFIGURATION ---
|
||||
// const SOCKET_URL = "http://10.10.1.183:5000";
|
||||
// const API_BASE = "http://10.10.1.183:5000/api";
|
||||
|
||||
// // --- DOM ELEMENT REFERENCES ---
|
||||
// const stationNameEl = document.getElementById('station-name');
|
||||
// const stationLocationEl = document.getElementById('station-location');
|
||||
// const deviceIdEl = document.getElementById('device-id');
|
||||
// const productIdEl = document.getElementById('product-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("Loading...", 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;
|
||||
// productIdEl.textContent = selectedStation.product_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();
|
||||
// }
|
||||
// });
|
||||
|
||||
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// --- CONFIGURATION ---
|
||||
const API_BASE = "http://10.10.1.183:5000/api";
|
||||
const SOCKET_URL = "http://192.168.1.10:5000";
|
||||
const API_BASE = "http://192.168.1.10:5000/api";
|
||||
|
||||
// --- DOM ELEMENT REFERENCES ---
|
||||
// KPI Tiles
|
||||
const stationNameEl = document.getElementById('station-name');
|
||||
const stationLocationEl = document.getElementById('station-location');
|
||||
const deviceIdEl = document.getElementById('device-id');
|
||||
const productIdEl = document.getElementById('product-id');
|
||||
const connChip = document.getElementById('connection-status-chip');
|
||||
const totalInitiatedEl = document.getElementById('total-swaps-initiated');
|
||||
const totalStartedEl = document.getElementById('total-swaps-started');
|
||||
const completedSwapsEl = document.getElementById('completed-swaps');
|
||||
const successRateEl = document.getElementById('success-rate');
|
||||
const abortedSwapsEl = document.getElementById('aborted-swaps');
|
||||
const abortRateEl = document.getElementById('abort-rate');
|
||||
const avgSwapTimeEl = document.getElementById('avg-swap-time');
|
||||
|
||||
// Date Range Elements
|
||||
const avgSwapTimeValueEl = document.getElementById('avg-swap-time-value');
|
||||
const stationUptimeEl = document.getElementById('station-uptime');
|
||||
const fromDateInput = document.getElementById('from');
|
||||
const toDateInput = document.getElementById('to');
|
||||
const applyRangeBtn = document.getElementById('applyRange');
|
||||
const quickRangeBtns = document.querySelectorAll('.date-range-btn');
|
||||
|
||||
// Chart Canvases
|
||||
const swapActivityCanvas = document.getElementById('swapActivityChart');
|
||||
const hourlyDistributionCanvas = document.getElementById('hourlyDistributionChart');
|
||||
const abortReasonsCanvas = document.getElementById('abortReasonsChart');
|
||||
|
||||
const stationUptimeEl = document.getElementById('station-uptime');
|
||||
|
||||
const heatmapGridEl = document.getElementById('heatmap-grid');
|
||||
|
||||
const avgSwapTimeValueEl = document.getElementById('avg-swap-time-value');
|
||||
|
||||
//status elements
|
||||
const stationNameEl = document.getElementById('station-name');
|
||||
const stationLocationEl = document.getElementById('station-location');
|
||||
const deviceIdEl = document.getElementById('device-id');
|
||||
const productIdEl = document.getElementById('product-id');
|
||||
const refreshBtn = document.getElementById('refreshBtn');
|
||||
const resetBtn = document.getElementById('station-reset-btn');
|
||||
const logoutBtn = document.getElementById('logout-btn');
|
||||
const refreshBtn = document.getElementById('refreshBtn');
|
||||
|
||||
let socket;
|
||||
|
||||
// --- CONSTANTS ---
|
||||
|
||||
const chartDefaults = {
|
||||
color: 'rgba(203, 213, 225, 0.7)', // Light gray for text
|
||||
borderColor: 'rgba(255, 255, 255, 0.1)', // Subtle grid lines
|
||||
};
|
||||
|
||||
// --- STATE ---
|
||||
// --- STATE & CONSTANTS ---
|
||||
let selectedStation = null;
|
||||
let socket;
|
||||
let fromDatePicker, toDatePicker;
|
||||
let swapChartInstance, hourlyChartInstance, abortChartInstance; // To hold chart instances
|
||||
let swapChartInstance, hourlyChartInstance, abortChartInstance;
|
||||
let lastKpis = {};
|
||||
const chartDefaults = { color: 'rgba(203, 213, 225, 0.7)', borderColor: 'rgba(255, 255, 255, 0.1)' };
|
||||
const ABORT_REASON_COLORS = { "Unknown": "#94a3b8", "Battery Exit Timeout": "#f43f5e", "Battery Entry Timeout": "#ec4899", "Door Close Timeout": "#f59e0b", "Door Open Timeout": "#eab308", "Invalid Parameter": "#a855f7", "Remote Abort": "#8b5cf6", "Invalid Battery": "#3b82f6" };
|
||||
|
||||
// --- HELPER FUNCTIONS ---
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the main KPI tiles with data from the API.
|
||||
* @param {object} data - The kpis data from the backend.
|
||||
*/
|
||||
const updateStatTiles = (data) => {
|
||||
if (!data) { // Used for loading state or on error
|
||||
totalInitiatedEl.textContent = '...';
|
||||
totalStartedEl.textContent = '...';
|
||||
completedSwapsEl.textContent = '...';
|
||||
successRateEl.textContent = '(...%)';
|
||||
abortedSwapsEl.textContent = '...';
|
||||
abortRateEl.textContent = '(...%)';
|
||||
avgSwapTimeValueEl.textContent = '...';
|
||||
stationUptimeEl.textContent = '... %';
|
||||
return;
|
||||
if (!data) {
|
||||
totalInitiatedEl.textContent = '...'; totalStartedEl.textContent = '...'; completedSwapsEl.textContent = '...';
|
||||
successRateEl.textContent = '(...%)'; abortedSwapsEl.textContent = '...'; abortRateEl.textContent = '(...%)';
|
||||
avgSwapTimeValueEl.textContent = '...'; stationUptimeEl.textContent = '... %'; lastKpis = {}; return;
|
||||
}
|
||||
|
||||
// Assign new data from the backend
|
||||
totalInitiatedEl.textContent = data.total_swaps_initiated ?? 0;
|
||||
totalStartedEl.textContent = data.total_swaps_started ?? 0;
|
||||
|
||||
const completed = data.completed_swaps ?? 0;
|
||||
const aborted = data.aborted_swaps ?? 0;
|
||||
|
||||
completedSwapsEl.textContent = completed;
|
||||
abortedSwapsEl.textContent = aborted;
|
||||
|
||||
// Use total_swaps_started for calculating success/abort rates
|
||||
const totalStarts = data.total_swaps_started ?? 0;
|
||||
const updateIfNeeded = (element, newValue, formatter = val => val) => {
|
||||
const key = element.id; const formattedValue = formatter(newValue);
|
||||
if (lastKpis[key] !== formattedValue) { element.textContent = formattedValue; lastKpis[key] = formattedValue; }
|
||||
};
|
||||
updateIfNeeded(totalInitiatedEl, data.total_swaps_initiated ?? 0);
|
||||
updateIfNeeded(totalStartedEl, data.total_swaps_started ?? 0);
|
||||
const completed = data.completed_swaps ?? 0; const aborted = data.aborted_swaps ?? 0; const totalStarts = data.total_swaps_started ?? 0;
|
||||
updateIfNeeded(completedSwapsEl, completed); updateIfNeeded(abortedSwapsEl, aborted);
|
||||
const successRate = totalStarts > 0 ? ((completed / totalStarts) * 100).toFixed(1) : 0;
|
||||
successRateEl.textContent = `(${successRate}%)`;
|
||||
|
||||
updateIfNeeded(successRateEl, successRate, val => `(${val}%)`);
|
||||
const abortRate = totalStarts > 0 ? ((aborted / totalStarts) * 100).toFixed(1) : 0;
|
||||
abortRateEl.textContent = `(${abortRate}%)`;
|
||||
|
||||
const avgTimeInSeconds = data.avg_swap_time_seconds != null ? Math.round(data.avg_swap_time_seconds) : '—';
|
||||
avgSwapTimeValueEl.textContent = avgTimeInSeconds;
|
||||
|
||||
stationUptimeEl.textContent = `${data.station_uptime ?? '...'} %`;
|
||||
|
||||
updateIfNeeded(abortRateEl, abortRate, val => `(${val}%)`);
|
||||
const avgTimeInSeconds = data.avg_swap_time_seconds != null ? Math.round(data.avg_swap_time_seconds) : '0';
|
||||
updateIfNeeded(avgSwapTimeValueEl, avgTimeInSeconds);
|
||||
const uptime = data.station_uptime ?? '...';
|
||||
updateIfNeeded(stationUptimeEl, uptime, val => `${val} %`);
|
||||
};
|
||||
|
||||
|
||||
// --- CHART.JS VISUALIZATION CODE ---
|
||||
|
||||
/**
|
||||
* Renders the "Swap Activity Over Time" bar chart.
|
||||
* @param {object} data - The chart data from the backend.
|
||||
*/
|
||||
const renderSwapActivityChart = (data) => {
|
||||
if (swapChartInstance) {
|
||||
swapChartInstance.destroy();
|
||||
}
|
||||
|
||||
// Dynamically calculate the max value for the Y-axis
|
||||
const maxValue = Math.max(...data.completed_data, ...data.aborted_data);
|
||||
const yAxisMax = Math.ceil(maxValue * 1.2) + 1; // Add 20% padding + 1
|
||||
|
||||
// If the chart doesn't exist yet, create it.
|
||||
if (!swapChartInstance) {
|
||||
|
||||
swapChartInstance = new Chart(swapActivityCanvas, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: data.labels,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Completed Swaps',
|
||||
data: data.completed_data,
|
||||
backgroundColor: 'rgba(16, 185, 129, 0.6)',
|
||||
borderColor: 'rgba(16, 185, 129, 1)',
|
||||
borderWidth: 1
|
||||
},
|
||||
{
|
||||
label: 'Aborted Swaps',
|
||||
data: data.aborted_data,
|
||||
backgroundColor: 'rgba(244, 63, 94, 0.6)',
|
||||
borderColor: 'rgba(244, 63, 94, 1)',
|
||||
borderWidth: 1
|
||||
}
|
||||
{ label: 'Completed Swaps', data: data.completed_data, backgroundColor: 'rgba(16, 185, 129, 0.6)' },
|
||||
{ label: 'Aborted Swaps', data: data.aborted_data, backgroundColor: 'rgba(244, 63, 94, 0.6)' }
|
||||
]
|
||||
},
|
||||
options: {
|
||||
|
|
@ -287,32 +104,29 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// If the chart already exists, just update its data.
|
||||
swapChartInstance.data.labels = data.labels;
|
||||
swapChartInstance.data.datasets[0].data = data.completed_data;
|
||||
swapChartInstance.data.datasets[1].data = data.aborted_data;
|
||||
swapChartInstance.options.scales.y.max = yAxisMax;
|
||||
swapChartInstance.update(); // This smoothly animates the changes.
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the "Hourly Swap Distribution" bar chart.
|
||||
* @param {object} data - The chart data from the backend.
|
||||
*/
|
||||
const renderHourlyDistributionChart = (data) => {
|
||||
if (hourlyChartInstance) {
|
||||
hourlyChartInstance.destroy();
|
||||
}
|
||||
|
||||
// Dynamically calculate the max value for the Y-axis
|
||||
const maxValue = Math.max(...data.swap_data);
|
||||
const yAxisMax = Math.ceil(maxValue * 1.2) + 1; // Add 20% padding + 1
|
||||
|
||||
if (!hourlyChartInstance) {
|
||||
|
||||
hourlyChartInstance = new Chart(hourlyDistributionCanvas, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: data.labels,
|
||||
datasets: [{
|
||||
label: 'Total Swaps',
|
||||
data: data.swap_data,
|
||||
backgroundColor: 'rgba(56, 189, 248, 0.6)',
|
||||
borderColor: 'rgba(56, 189, 248, 1)',
|
||||
borderWidth: 1
|
||||
}]
|
||||
datasets: [{ label: 'Total Swaps', data: data.swap_data, backgroundColor: 'rgba(56, 189, 248, 0.6)' }]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
|
|
@ -340,30 +154,21 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
hourlyChartInstance.data.labels = data.labels;
|
||||
hourlyChartInstance.data.datasets[0].data = data.swap_data;
|
||||
hourlyChartInstance.options.scales.y.max = yAxisMax;
|
||||
hourlyChartInstance.update();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the "Swap Abort Reasons" donut chart.
|
||||
* @param {object} data - The chart data from the backend.
|
||||
*/
|
||||
const renderAbortReasonsChart = (data) => {
|
||||
if (abortChartInstance) {
|
||||
abortChartInstance.destroy();
|
||||
}
|
||||
if (!abortChartInstance) {
|
||||
abortChartInstance = new Chart(abortReasonsCanvas, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: data.labels, // e.g., ['Timeout', 'User Cancelled', ...]
|
||||
datasets: [{
|
||||
label: 'Count',
|
||||
data: data.reason_data, // e.g., [10, 5, ...]
|
||||
backgroundColor: [
|
||||
'rgba(244, 63, 94, 0.7)',
|
||||
'rgba(245, 158, 11, 0.7)',
|
||||
'rgba(139, 92, 246, 0.7)',
|
||||
'rgba(56, 189, 248, 0.7)',
|
||||
],
|
||||
}]
|
||||
labels: data.labels,
|
||||
datasets: [{ label: 'Count', data: data.reason_data, backgroundColor: data.labels.map(label => ABORT_REASON_COLORS[label] || '#cccccc') }]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
|
|
@ -376,55 +181,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches all analytics data from the backend.
|
||||
* @param {string} startDate - The start date in 'YYYY-MM-DD' format.
|
||||
* @param {string} endDate - The end date in 'YYYY-MM-DD' format.
|
||||
*/
|
||||
const fetchAnalyticsData = async (startDate, endDate) => {
|
||||
if (!selectedStation) return;
|
||||
|
||||
updateStatTiles(null); // Set UI to loading state
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
station_id: selectedStation.id,
|
||||
start_date: startDate,
|
||||
end_date: endDate
|
||||
});
|
||||
|
||||
const response = await fetch(`${API_BASE}/analytics?${params}`);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.message || 'Failed to fetch analytics data');
|
||||
}
|
||||
|
||||
const analyticsData = await response.json();
|
||||
|
||||
// Update all sections of the page with the new data
|
||||
updateStatTiles(analyticsData.kpis);
|
||||
renderSwapActivityChart(analyticsData.swap_activity);
|
||||
renderHourlyDistributionChart(analyticsData.hourly_distribution);
|
||||
renderAbortReasonsChart(analyticsData.abort_reasons);
|
||||
renderSlotHeatmap(analyticsData.slot_utilization);
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error fetching analytics data:", error);
|
||||
updateStatTiles(null); // Reset UI on error
|
||||
// Destroy charts on error to show a blank state
|
||||
if(swapChartInstance) swapChartInstance.destroy();
|
||||
if(hourlyChartInstance) hourlyChartInstance.destroy();
|
||||
if(abortChartInstance) abortChartInstance.destroy();
|
||||
} else {
|
||||
abortChartInstance.data.labels = data.labels;
|
||||
abortChartInstance.data.datasets[0].data = data.reason_data;
|
||||
abortChartInstance.data.datasets[0].backgroundColor = data.labels.map(label => ABORT_REASON_COLORS[label] || '#cccccc');
|
||||
abortChartInstance.update();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the Slot Utilization Heatmap.
|
||||
* @param {object} data - The slot_utilization data from the backend.
|
||||
*/
|
||||
const renderSlotHeatmap = (data) => {
|
||||
heatmapGridEl.innerHTML = ''; // Clear previous heatmap
|
||||
if (!data || !data.counts || data.counts.length === 0) return;
|
||||
|
|
@ -455,86 +219,99 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
});
|
||||
};
|
||||
|
||||
// --- INITIALIZATION ---
|
||||
const fetchAnalyticsData = async (startDate, endDate) => {
|
||||
if (!selectedStation) return;
|
||||
updateStatTiles(null);
|
||||
try {
|
||||
const params = new URLSearchParams({ station_id: selectedStation.id, start_date: startDate, end_date: endDate });
|
||||
const response = await fetch(`${API_BASE}/analytics?${params}`);
|
||||
if (!response.ok) throw new Error((await response.json()).message || 'Failed to fetch analytics data');
|
||||
const analyticsData = await response.json();
|
||||
updateStatTiles(analyticsData.kpis);
|
||||
renderSwapActivityChart(analyticsData.swap_activity);
|
||||
renderHourlyDistributionChart(analyticsData.hourly_distribution);
|
||||
renderAbortReasonsChart(analyticsData.abort_reasons);
|
||||
renderSlotHeatmap(analyticsData.slot_utilization);
|
||||
} catch (error) { console.error("Error fetching analytics data:", error); updateStatTiles(null); }
|
||||
};
|
||||
|
||||
const checkStationStatus = async () => { /* ... Your correct status check logic ... */ };
|
||||
|
||||
const connectSocket = () => {
|
||||
socket = io(SOCKET_URL);
|
||||
|
||||
socket.on('connect', () => {
|
||||
console.log("Analytics: Connected to WebSocket.");
|
||||
if (selectedStation) {
|
||||
socket.emit('join_station_room', { station_id: selectedStation.id });
|
||||
}
|
||||
});
|
||||
|
||||
// This listener handles the full refresh for important events
|
||||
socket.on('analytics_updated', () => {
|
||||
console.log("Analytics update received (Event/Request). Refetching data.");
|
||||
applyRangeBtn.click();
|
||||
});
|
||||
|
||||
// --- ADD THIS NEW LISTENER for lightweight status updates ---
|
||||
socket.on('status_update', (data) => {
|
||||
// data will look like: { status: 'Online' }
|
||||
console.log("Live status update received:", data.status);
|
||||
if (connChip) {
|
||||
if (data.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';
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function init() {
|
||||
try {
|
||||
selectedStation = JSON.parse(localStorage.getItem('selected_station'));
|
||||
if (!selectedStation || !selectedStation.id) {
|
||||
throw new Error('No station selected.');
|
||||
}
|
||||
if (!selectedStation || !selectedStation.id) throw new Error('No station selected.');
|
||||
} catch (e) { window.location.href = './station_selection.html'; return; }
|
||||
|
||||
stationNameEl.textContent = selectedStation.name;
|
||||
stationLocationEl.textContent = selectedStation.location;
|
||||
deviceIdEl.textContent = selectedStation.id;
|
||||
productIdEl.textContent = selectedStation.product_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;
|
||||
}
|
||||
|
||||
// Initialize Flatpickr
|
||||
fromDatePicker = flatpickr(fromDateInput, { dateFormat: "Y-m-d", altInput: true, altFormat: "M j, Y" });
|
||||
toDatePicker = flatpickr(toDateInput, { dateFormat: "Y-m-d", altInput: true, altFormat: "M j, Y" });
|
||||
|
||||
// Default to today and fetch initial data
|
||||
const todayStr = new Date().toISOString().split('T')[0];
|
||||
fromDatePicker.setDate(todayStr, false);
|
||||
toDatePicker.setDate(todayStr, false);
|
||||
fetchAnalyticsData(todayStr, todayStr);
|
||||
|
||||
connectSocket();
|
||||
|
||||
// --- ADD THIS BLOCK FOR AUTO-REFRESH ---
|
||||
setInterval(() => {
|
||||
console.log("Auto-refreshing analytics data...");
|
||||
// Use .selectedDates[0] to get the date object
|
||||
const startDateObj = fromDatePicker.selectedDates[0];
|
||||
const endDateObj = toDatePicker.selectedDates[0];
|
||||
|
||||
if (startDateObj && endDateObj) {
|
||||
// Use the formatDate utility to get the string in the correct format
|
||||
const startDate = flatpickr.formatDate(startDateObj, "Y-m-d");
|
||||
const endDate = flatpickr.formatDate(endDateObj, "Y-m-d");
|
||||
fetchAnalyticsData(startDate, endDate);
|
||||
}
|
||||
}, 30000); // Refreshes every 30 seconds (30000 milliseconds)
|
||||
|
||||
// --- EVENT LISTENERS ---
|
||||
applyRangeBtn.addEventListener('click', () => {
|
||||
const startDate = fromDateInput.value;
|
||||
const endDate = toDateInput.value;
|
||||
if (!startDate || !endDate) return alert('Please select both a start and end date.');
|
||||
if (new Date(startDate) > new Date(endDate)) return alert('Start date cannot be after the end date.');
|
||||
|
||||
fetchAnalyticsData(startDate, endDate);
|
||||
});
|
||||
|
||||
// (The rest of your button listeners are unchanged)
|
||||
if(logoutBtn) logoutBtn.addEventListener('click', () => {
|
||||
localStorage.clear();
|
||||
window.location.href = 'index.html';
|
||||
});
|
||||
if (logoutBtn) logoutBtn.addEventListener('click', () => { localStorage.clear(); window.location.href = 'index.html'; });
|
||||
if (refreshBtn) refreshBtn.addEventListener('click', () => location.reload());
|
||||
if(resetBtn) resetBtn.addEventListener('click', () => {
|
||||
if (confirm('Are you sure you want to reset the station?')) {
|
||||
sendCommand('STATION_RESET');
|
||||
|
||||
applyRangeBtn.addEventListener('click', () => {
|
||||
const startDate = fromDatePicker.selectedDates[0];
|
||||
const endDate = toDatePicker.selectedDates[0];
|
||||
if (startDate && endDate) {
|
||||
fetchAnalyticsData(flatpickr.formatDate(startDate, "Y-m-d"), flatpickr.formatDate(endDate, "Y-m-d"));
|
||||
}
|
||||
});
|
||||
|
||||
quickRangeBtns.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const range = btn.dataset.range;
|
||||
const today = new Date();
|
||||
let startDate = new Date();
|
||||
const today = new Date(); let startDate = new Date();
|
||||
if (range === 'today') startDate = today;
|
||||
else startDate.setDate(today.getDate() - (parseInt(range, 10) - 1));
|
||||
fromDatePicker.setDate(startDate, true); toDatePicker.setDate(today, true);
|
||||
applyRangeBtn.click();
|
||||
});
|
||||
});
|
||||
|
||||
if (range === 'today') {
|
||||
startDate = today;
|
||||
} else {
|
||||
startDate.setDate(today.getDate() - (parseInt(range, 10) - 1));
|
||||
fromDatePicker = flatpickr(fromDateInput, { dateFormat: "Y-m-d", altInput: true, altFormat: "M j, Y" });
|
||||
toDatePicker = flatpickr(toDateInput, { dateFormat: "Y-m-d", altInput: true, altFormat: "M j, Y" });
|
||||
const todayStr = new Date().toISOString().split('T')[0];
|
||||
fromDatePicker.setDate(todayStr, false); toDatePicker.setDate(todayStr, false);
|
||||
|
||||
fetchAnalyticsData(todayStr, todayStr);
|
||||
checkStationStatus();
|
||||
// setInterval(checkStationStatus, 10000);
|
||||
connectSocket();
|
||||
}
|
||||
|
||||
const startDateStr = startDate.toISOString().split('T')[0];
|
||||
const endDateStr = today.toISOString().split('T')[0];
|
||||
|
||||
fromDatePicker.setDate(startDateStr, false);
|
||||
toDatePicker.setDate(endDateStr, false);
|
||||
fetchAnalyticsData(startDateStr, endDateStr);
|
||||
});
|
||||
});
|
||||
init();
|
||||
});
|
||||
|
|
@ -11,7 +11,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
const password = document.getElementById('password').value;
|
||||
|
||||
try {
|
||||
const response = await fetch('http://10.10.1.183:5000/api/login', {
|
||||
const response = await fetch('http://192.168.1.10:5000/api/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password }),
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
// frontend/js/common-header.js
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// --- CONFIGURATION ---
|
||||
const SOCKET_URL = "http://10.10.1.183:5000";
|
||||
const API_BASE = "http://10.10.1.183:5000/api";
|
||||
const SOCKET_URL = "http://192.168.1.10:5000";
|
||||
const API_BASE = "http://192.168.1.10:5000/api";
|
||||
|
||||
// --- STATE & SELECTED STATION ---
|
||||
let selectedStation = null;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// --- CONFIGURATION ---
|
||||
const SOCKET_URL = "http://10.10.1.183:5000";
|
||||
const API_BASE = "http://10.10.1.183:5000/api"; // Added for API calls
|
||||
const SOCKET_URL = "http://192.168.1.10:5000";
|
||||
const API_BASE = "http://192.168.1.10:5000/api"; // Added for API calls
|
||||
|
||||
// --- DOM ELEMENT REFERENCES ---
|
||||
const grid = document.getElementById('chambersGrid');
|
||||
|
|
@ -232,7 +232,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
batPill.className = 'battery-status-pill chip chip-emerald';
|
||||
|
||||
const chgPill = card.querySelector('.charger-status-pill');
|
||||
if (slot.chargingStatus === 1) {
|
||||
if (slot.chargerPresent && slot.current > 0) {
|
||||
chgPill.innerHTML = `<span class="h-2 w-2 rounded-full bg-sky-400 animate-pulseDot"></span> Charging`;
|
||||
chgPill.className = 'charger-status-pill chip chip-sky';
|
||||
} else {
|
||||
|
|
@ -285,6 +285,32 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
});
|
||||
};
|
||||
|
||||
|
||||
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 && 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...";
|
||||
// Optionally reset the dashboard if the station goes offline
|
||||
// resetDashboardUI();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch station status:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const resetDashboardUI = () => {
|
||||
grid.querySelectorAll('.chamber-card').forEach(card => {
|
||||
card.querySelector('.bat-id-big').textContent = 'Waiting...';
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// --- CONFIGURATION ---
|
||||
const SOCKET_URL = "http://10.10.1.183:5000";
|
||||
const API_BASE = "http://10.10.1.183:5000/api";
|
||||
const SOCKET_URL = "http://192.168.1.10:5000";
|
||||
const API_BASE = "http://192.168.1.10:5000/api";
|
||||
|
||||
// --- DOM ELEMENT REFERENCES ---
|
||||
const stationNameEl = document.getElementById('station-name');
|
||||
|
|
@ -16,18 +16,23 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
const clearAllBtn = document.getElementById('clear-all');
|
||||
const refreshBtn = document.getElementById('refreshBtn');
|
||||
const logoutBtn = document.getElementById('logout-btn');
|
||||
const logCountInput = document.getElementById('log-count');
|
||||
const fromDateInput = document.getElementById('from-date');
|
||||
const toDateInput = document.getElementById('to-date');
|
||||
const applyFiltersBtn = document.getElementById('apply-filters-btn');
|
||||
|
||||
// --- STATE ---
|
||||
let selectedStation = null;
|
||||
let socket;
|
||||
let statusPollingInterval;
|
||||
let fromDatePicker, toDatePicker;
|
||||
|
||||
// --- HELPER FUNCTIONS --
|
||||
|
||||
const appendLog = (textarea, data, topic, timestampStr) => {
|
||||
if (!textarea) return;
|
||||
|
||||
const timestamp = new Date(timestampStr).toLocaleTimeString();
|
||||
const timestamp = new Date(timestampStr).toLocaleString();
|
||||
const formattedJson = JSON.stringify(data, null, 2);
|
||||
|
||||
// Clean up the topic for better display
|
||||
|
|
@ -40,9 +45,49 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
textarea.scrollTop = textarea.scrollHeight;
|
||||
};
|
||||
|
||||
// const fetchRecentLogs = async () => {
|
||||
// try {
|
||||
// const response = await fetch(`${API_BASE}/logs/recent/${selectedStation.id}`);
|
||||
// if (!response.ok) throw new Error('Failed to fetch recent logs');
|
||||
// const logs = await response.json();
|
||||
|
||||
// requestLogArea.value = '';
|
||||
// eventLogArea.value = '';
|
||||
|
||||
// logs.forEach(log => {
|
||||
// if (log.topic.endsWith('EVENTS')) {
|
||||
// appendLog(eventLogArea, log.payload, log.topic, log.timestamp);
|
||||
// } else if (log.topic.endsWith('REQUEST')) {
|
||||
// appendLog(requestLogArea, log.payload, log.topic, log.timestamp);
|
||||
// }
|
||||
// });
|
||||
// console.log(`Successfully fetched and rendered ${logs.length} recent logs.`);
|
||||
// } catch (error) {
|
||||
// console.error(error);
|
||||
// }
|
||||
// };
|
||||
|
||||
|
||||
|
||||
const fetchRecentLogs = async () => {
|
||||
// Get values from all filters
|
||||
const count = logCountInput.value;
|
||||
const startDate = fromDatePicker.selectedDates[0];
|
||||
const endDate = toDatePicker.selectedDates[0];
|
||||
|
||||
if (!startDate || !endDate) return;
|
||||
|
||||
const startDateStr = flatpickr.formatDate(startDate, "Y-m-d");
|
||||
const endDateStr = flatpickr.formatDate(endDate, "Y-m-d");
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/logs/recent/${selectedStation.id}`);
|
||||
// Build URL with all parameters
|
||||
const params = new URLSearchParams({
|
||||
start_date: startDateStr,
|
||||
end_date: endDateStr,
|
||||
count: count
|
||||
});
|
||||
const response = await fetch(`${API_BASE}/logs/recent/${selectedStation.id}?${params}`);
|
||||
if (!response.ok) throw new Error('Failed to fetch recent logs');
|
||||
const logs = await response.json();
|
||||
|
||||
|
|
@ -96,6 +141,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
productIdEl.textContent = selectedStation.product_id;
|
||||
console.log("5. Header populated.");
|
||||
|
||||
fromDatePicker = flatpickr(fromDateInput, { dateFormat: "Y-m-d", defaultDate: "today" });
|
||||
toDatePicker = flatpickr(toDateInput, { dateFormat: "Y-m-d", defaultDate: "today" });
|
||||
|
||||
applyFiltersBtn.addEventListener('click', fetchRecentLogs);
|
||||
|
||||
// Step 3: Set up button event listeners.
|
||||
if(clearReqBtn) clearReqBtn.addEventListener('click', () => requestLogArea.value = '');
|
||||
if(clearEvtBtn) clearEvtBtn.addEventListener('click', () => eventLogArea.value = '');
|
||||
|
|
|
|||
|
|
@ -1,220 +1,10 @@
|
|||
// 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://10.10.1.183: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://10.10.1.183: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 stationCountEl = document.getElementById('station-count'); // Make sure you have an element with this ID in your HTML
|
||||
|
||||
// --- CONFIG & STATE ---
|
||||
const API_BASE = 'http://10.10.1.183:5000/api';
|
||||
const API_BASE = 'http://192.168.1.10:5000/api';
|
||||
let allStations = []; // Master list of stations from the API
|
||||
let pollingInterval = null;
|
||||
|
||||
|
|
@ -253,7 +43,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
// 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.className = "station-card 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="main-content p-5 flex-grow cursor-pointer" data-station-json='${JSON.stringify(station)}'>
|
||||
|
|
|
|||
|
|
@ -174,6 +174,25 @@
|
|||
<main class="relative z-10 mx-auto max-w-7.5xl px-3 sm:px-4 py-4 flex flex-col gap-4">
|
||||
<!-- Top row: Device badge + hint -->
|
||||
|
||||
<!-- <section class="flex items-center gap-2 mb-4">
|
||||
<label for="log-date-picker" class="text-sm font-bold text-gray-300">Select Date:</label>
|
||||
<input id="log-date-picker" type="text" placeholder="Select a date" class="rounded-md bg-white/5 border border-white/10 px-2 py-1.5 text-sm outline-none focus:ring-2 focus:ring-emerald-500/60">
|
||||
</section> -->
|
||||
|
||||
<section class="flex flex-wrap items-center gap-4 mb-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<label for="log-count" class="text-sm font-bold text-gray-300">Log Count:</label>
|
||||
<input id="log-count" type="number" value="50" min="10" max="500" class="w-20 rounded-md bg-white/5 border border-white/10 px-2 py-1.5 text-sm outline-none">
|
||||
</div>
|
||||
|
||||
<div class="ml-auto flex items-center gap-2">
|
||||
<input id="from-date" type="text" placeholder="Start Date" class="rounded-md bg-white/5 border border-white/10 px-2 py-1.5 text-sm outline-none">
|
||||
<span class="text-gray-500">to</span>
|
||||
<input id="to-date" type="text" placeholder="End Date" class="rounded-md bg-white/5 border border-white/10 px-2 py-1.5 text-sm outline-none">
|
||||
<button id="apply-filters-btn" class="btn">Apply</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Logs panels -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 min-h-[18rem]">
|
||||
<!-- Request -->
|
||||
|
|
|
|||
|
|
@ -225,7 +225,7 @@
|
|||
</div>
|
||||
|
||||
<script>
|
||||
const API_BASE = 'http://10.10.1.183:5000/api';
|
||||
const API_BASE = 'http://192.168.1.10:5000/api';
|
||||
|
||||
const grid = document.getElementById('stations-grid');
|
||||
const addStationCardTmpl = document.getElementById('add-station-card-template');
|
||||
|
|
|
|||
Loading…
Reference in New Issue