aephiday commited on
Commit
d849ff5
·
verified ·
1 Parent(s): b3e19e2

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +621 -18
index.html CHANGED
@@ -1,19 +1,622 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Video Analytics Health Dashboard</title>
7
+ <!-- Tailwind CSS CDN -->
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <!-- Font Awesome for icons -->
10
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
11
+ <style>
12
+ /* Custom styles for status colors and general aesthetics */
13
+ :root {
14
+ --color-healthy: #10B981; /* Green-500 */
15
+ --color-warning: #F59E0B; /* Amber-500 */
16
+ --color-critical: #EF4444; /* Red-500 */
17
+ --color-info: #3B82F6; /* Blue-500 */
18
+ }
19
+
20
+ body {
21
+ font-family: 'Inter', sans-serif;
22
+ background-color: #f3f4f6; /* Light gray background */
23
+ }
24
+
25
+ .status-indicator {
26
+ width: 2.5rem; /* Equivalent to w-10 */
27
+ height: 2.5rem; /* Equivalent to h-10 */
28
+ border-radius: 9999px; /* Equivalent to rounded-full */
29
+ display: flex;
30
+ align-items: center;
31
+ justify-content: center;
32
+ color: white;
33
+ font-size: 1.25rem; /* Equivalent to text-xl */
34
+ }
35
+
36
+ .status-healthy { background-color: var(--color-healthy); }
37
+ .status-warning { background-color: var(--color-warning); }
38
+ .status-critical { background-color: var(--color-critical); }
39
+
40
+ .progress-bar-bg {
41
+ background-color: #e5e7eb; /* Gray-200 */
42
+ border-radius: 0.375rem; /* rounded-md */
43
+ }
44
+
45
+ .progress-bar-fill {
46
+ height: 100%;
47
+ border-radius: 0.375rem; /* rounded-md */
48
+ transition: width 0.3s ease-in-out;
49
+ }
50
+ </style>
51
+ </head>
52
+ <body class="p-4 md:p-8">
53
+ <div class="max-w-7xl mx-auto bg-white shadow-lg rounded-xl p-6 md:p-8">
54
+ <!-- Header -->
55
+ <h1 class="text-3xl md:text-4xl font-extrabold text-gray-900 mb-6 text-center">
56
+ <i class="fas fa-heartbeat text-red-500 mr-3"></i>Video Analytics Health Dashboard
57
+ </h1>
58
+
59
+ <!-- Overall Status & Summary -->
60
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
61
+ <!-- Overall Status Card -->
62
+ <div id="overall-status-card" class="col-span-1 md:col-span-2 bg-gradient-to-r from-red-600 to-red-400 text-white p-6 rounded-lg shadow-md flex items-center justify-between">
63
+ <div>
64
+ <p class="text-sm font-semibold opacity-90">Overall System Status</p>
65
+ <h2 id="overall-status-text" class="text-4xl font-bold mt-1">Critical</h2>
66
+ <p class="text-xs opacity-80 mt-2">Immediate Attention Required</p>
67
+ </div>
68
+ <div class="text-6xl">
69
+ <i class="fas fa-exclamation-triangle"></i>
70
+ </div>
71
+ </div>
72
+
73
+ <!-- Summary Statistics -->
74
+ <div class="col-span-1 grid grid-cols-2 gap-4">
75
+ <div class="bg-blue-500 text-white p-4 rounded-lg shadow-md text-center">
76
+ <p class="text-sm opacity-90">Total Cameras</p>
77
+ <p id="total-cameras" class="text-2xl font-bold mt-1">40</p>
78
+ </div>
79
+ <div class="bg-blue-500 text-white p-4 rounded-lg shadow-md text-center">
80
+ <p class="text-sm opacity-90">Active Cameras</p>
81
+ <p id="active-cameras" class="text-2xl font-bold mt-1">0</p>
82
+ </div>
83
+ <div class="bg-blue-500 text-white p-4 rounded-lg shadow-md text-center">
84
+ <p class="text-sm opacity-90">Total Services</p>
85
+ <p id="total-services" class="text-2xl font-bold mt-1">4</p>
86
+ </div>
87
+ <div class="bg-blue-500 text-white p-4 rounded-lg shadow-md text-center">
88
+ <p class="text-sm opacity-90">Healthy Services</p>
89
+ <p id="healthy-services" class="text-2xl font-bold mt-1">1</p>
90
+ </div>
91
+ </div>
92
+ </div>
93
+
94
+ <!-- System Metrics -->
95
+ <div class="bg-gray-50 p-6 rounded-lg shadow-md mb-8">
96
+ <h3 class="text-2xl font-bold text-gray-800 mb-4 flex items-center">
97
+ <i class="fas fa-server text-gray-600 mr-2"></i>System Metrics
98
+ </h3>
99
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
100
+ <!-- CPU Usage -->
101
+ <div>
102
+ <p class="text-sm font-medium text-gray-700 mb-1">CPU Usage</p>
103
+ <div class="w-full bg-gray-200 rounded-full h-2.5">
104
+ <div id="cpu-progress" class="bg-blue-600 h-2.5 rounded-full" style="width: 8.4%"></div>
105
+ </div>
106
+ <p id="cpu-percent" class="text-xs text-gray-500 mt-1 text-right">8.4%</p>
107
+ </div>
108
+ <!-- Memory Usage -->
109
+ <div>
110
+ <p class="text-sm font-medium text-gray-700 mb-1">Memory Usage</p>
111
+ <div class="w-full bg-gray-200 rounded-full h-2.5">
112
+ <div id="memory-progress" class="bg-red-600 h-2.5 rounded-full" style="width: 76.3%"></div>
113
+ </div>
114
+ <p id="memory-percent" class="text-xs text-gray-500 mt-1 text-right">76.3%</p>
115
+ </div>
116
+ <!-- Disk Usage -->
117
+ <div>
118
+ <p class="text-sm font-medium text-gray-700 mb-1">Disk Usage</p>
119
+ <div class="w-full bg-gray-200 rounded-full h-2.5">
120
+ <div id="disk-progress" class="bg-yellow-600 h-2.5 rounded-full" style="width: 21.73%"></div>
121
+ </div>
122
+ <p id="disk-percent" class="text-xs text-gray-500 mt-1 text-right">21.73%</p>
123
+ </div>
124
+ <!-- Network IO -->
125
+ <div class="md:col-span-2 lg:col-span-3">
126
+ <p class="text-sm font-medium text-gray-700 mb-1">Network I/O</p>
127
+ <div class="grid grid-cols-2 gap-2 text-sm text-gray-600">
128
+ <p>Bytes Sent: <span id="bytes-sent" class="font-semibold">193,119,181</span></p>
129
+ <p>Bytes Received: <span id="bytes-recv" class="font-semibold">291,075,046</span></p>
130
+ <p>Packets Sent: <span id="packets-sent" class="font-semibold">493,719</span></p>
131
+ <p>Packets Received: <span id="packets-recv" class="font-semibold">598,814</span></p>
132
+ </div>
133
+ </div>
134
+ <!-- Process Count & Load Average -->
135
+ <div>
136
+ <p class="text-sm font-medium text-gray-700 mb-1">Process Count</p>
137
+ <p id="process-count" class="text-lg font-semibold text-gray-800">301</p>
138
+ </div>
139
+ <div>
140
+ <p class="text-sm font-medium text-gray-700 mb-1">Load Average</p>
141
+ <p id="load-average" class="text-lg font-semibold text-gray-800">0, 0, 0</p>
142
+ </div>
143
+ </div>
144
+ <p id="system-timestamp" class="text-xs text-gray-500 mt-4 text-right">Last updated: August 1, 2025, 10:04:25</p>
145
+ </div>
146
+
147
+ <!-- Services Status -->
148
+ <div class="bg-gray-50 p-6 rounded-lg shadow-md mb-8">
149
+ <h3 class="text-2xl font-bold text-gray-800 mb-4 flex items-center">
150
+ <i class="fas fa-cogs text-gray-600 mr-2"></i>Service Status
151
+ </h3>
152
+ <div class="overflow-x-auto">
153
+ <table class="min-w-full divide-y divide-gray-200">
154
+ <thead class="bg-gray-100">
155
+ <tr>
156
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider rounded-tl-lg">Service</th>
157
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
158
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Response Time (ms)</th>
159
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Last Check</th>
160
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider rounded-tr-lg">Details</th>
161
+ </tr>
162
+ </thead>
163
+ <tbody id="services-table-body" class="bg-white divide-y divide-gray-200">
164
+ <!-- Service rows will be injected here by JavaScript -->
165
+ </tbody>
166
+ </table>
167
+ </div>
168
+ </div>
169
+
170
+ <!-- Camera Status -->
171
+ <div class="bg-gray-50 p-6 rounded-lg shadow-md">
172
+ <h3 class="text-2xl font-bold text-gray-800 mb-4 flex items-center">
173
+ <i class="fas fa-video text-gray-600 mr-2"></i>Camera Status
174
+ </h3>
175
+ <div class="flex flex-col md:flex-row justify-between items-center mb-4 gap-4">
176
+ <div class="flex-grow w-full md:w-auto">
177
+ <input type="text" id="camera-search" placeholder="Search Camera ID..." class="w-full p-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500">
178
+ </div>
179
+ <div class="flex flex-wrap gap-2 w-full md:w-auto justify-center md:justify-end">
180
+ <button class="filter-btn px-4 py-2 rounded-md text-sm font-medium bg-blue-500 text-white hover:bg-blue-600 transition" data-filter="all">All (<span id="count-all">0</span>)</button>
181
+ <button class="filter-btn px-4 py-2 rounded-md text-sm font-medium bg-green-500 text-white hover:bg-green-600 transition" data-filter="healthy">Healthy (<span id="count-healthy">0</span>)</button>
182
+ <button class="filter-btn px-4 py-2 rounded-md text-sm font-medium bg-orange-500 text-white hover:bg-orange-600 transition" data-filter="warning">Warning (<span id="count-warning">0</span>)</button>
183
+ <button class="filter-btn px-4 py-2 rounded-md text-sm font-medium bg-red-500 text-white hover:bg-red-600 transition" data-filter="error">Error (<span id="count-error">0</span>)</button>
184
+ </div>
185
+ </div>
186
+ <div id="cameras-grid" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
187
+ <!-- Camera cards will be injected here by JavaScript -->
188
+ </div>
189
+ <p id="camera-summary" class="text-sm text-gray-600 mt-4 text-center"></p>
190
+ </div>
191
+ </div>
192
+ <script>
193
+ // Raw JSON data provided by the user
194
+ const healthData = {
195
+ "overall_status": "warning",
196
+ "timestamp": "2025-08-01T10:04:32.861557",
197
+ "uptime_seconds": 283.5514087677002,
198
+ "services": [
199
+ {
200
+ "name": "database",
201
+ "status": "warning",
202
+ "last_check": "2025-08-01T10:04:23.669585",
203
+ "response_time_ms": 9404.791831970215,
204
+ "details": {
205
+ "missing_collections": [
206
+ "detection_results"
207
+ ]
208
+ },
209
+ "uptime_seconds": null
210
+ },
211
+ {
212
+ "name": "detection_service",
213
+ "status": "critical",
214
+ "last_check": "2025-08-01T10:04:23.802771",
215
+ "response_time_ms": 133.18586349487305,
216
+ "details": {
217
+ "total_detections_5min": 0,
218
+ "total_detections_1hour": 0,
219
+ "active_cameras_5min": 0,
220
+ "active_cameras_1hour": 0,
221
+ "per_camera_recent": [],
222
+ "per_camera_hourly": [],
223
+ "avg_fps_per_camera": 0
224
+ },
225
+ "uptime_seconds": null
226
+ },
227
+ {
228
+ "name": "license_plate_service",
229
+ "status": "warning",
230
+ "last_check": "2025-08-01T10:04:23.862083",
231
+ "response_time_ms": 59.31258201599121,
232
+ "details": {
233
+ "message": "License plate collection not found"
234
+ },
235
+ "uptime_seconds": null
236
+ },
237
+ {
238
+ "name": "api_service",
239
+ "status": "healthy",
240
+ "last_check": "2025-08-01T10:04:23.862083",
241
+ "response_time_ms": 0,
242
+ "details": {
243
+ "uptime_seconds": 274.5519349575043
244
+ },
245
+ "uptime_seconds": 274.5519349575043
246
+ }
247
+ ],
248
+ "system_metrics": {
249
+ "cpu_percent": 8.4,
250
+ "memory_percent": 76.3,
251
+ "disk_percent": 21.731145031029676,
252
+ "network_io": {
253
+ "bytes_sent": 193119181,
254
+ "bytes_recv": 291075046,
255
+ "packets_sent": 493719,
256
+ "packets_recv": 598814
257
+ },
258
+ "process_count": 301,
259
+ "load_average": [
260
+ 0,
261
+ 0,
262
+ 0
263
+ ],
264
+ "timestamp": "2025-08-01T10:04:25.127934"
265
+ },
266
+ "cameras": [
267
+ { "cam_id": "CAM-Kelapa-Dermaga-utama", "status": "healthy", "last_detection": "2025-08-01T10:04:23.669585", "detection_rate_fps": 30, "image_count_24h": 2592000, "last_image_path": "./img.jpg", "error_count_24h": 0 },
268
+ { "cam_id": "CAM-TEST", "status": "healthy", "last_detection": "2025-08-01T10:04:23.669585", "detection_rate_fps": 30, "image_count_24h": 2592000, "last_image_path": "./img.jpg", "error_count_24h": 0 },
269
+ { "cam_id": "CAM-CIKOKO-002", "status": "healthy", "last_detection": "2025-08-01T10:04:23.669585", "detection_rate_fps": 30, "image_count_24h": 2592000, "last_image_path": "./img.jpg", "error_count_24h": 0 },
270
+ { "cam_id": "CAM-JTPULO-002", "status": "healthy", "last_detection": "2025-08-01T10:04:23.669585", "detection_rate_fps": 30, "image_count_24h": 2592000, "last_image_path": "./img.jpg", "error_count_24h": 0 },
271
+ { "cam_id": "CAM-GBK-003", "status": "healthy", "last_detection": "2025-08-01T10:04:23.669585", "detection_rate_fps": 30, "image_count_24h": 2592000, "last_image_path": "./img.jpg", "error_count_24h": 0 },
272
+ { "cam_id": "CAM-TANGERANG-001", "status": "healthy", "last_detection": "2025-08-01T10:04:23.669585", "detection_rate_fps": 30, "image_count_24h": 2592000, "last_image_path": "./img.jpg", "error_count_24h": 0 },
273
+ { "cam_id": "CAM-DUREN-TIGA51", "status": "healthy", "last_detection": "2025-08-01T10:04:23.669585", "detection_rate_fps": 30, "image_count_24h": 2592000, "last_image_path": "./img.jpg", "error_count_24h": 0 },
274
+ { "cam_id": "CAM-GBK-009", "status": "healthy", "last_detection": "2025-08-01T10:04:23.669585", "detection_rate_fps": 30, "image_count_24h": 2592000, "last_image_path": "./img.jpg", "error_count_24h": 0 },
275
+ { "cam_id": "CAM-LENTENGAGUNG-811", "status": "healthy", "last_detection": "2025-08-01T10:04:23.669585", "detection_rate_fps": 30, "image_count_24h": 2592000, "last_image_path": "./img.jpg", "error_count_24h": 0 },
276
+ { "cam_id": "CAMTEST-GROGOL-11", "status": "healthy", "last_detection": "2025-08-01T10:04:23.669585", "detection_rate_fps": 30, "image_count_24h": 2592000, "last_image_path": "./img.jpg", "error_count_24h": 0 },
277
+ { "cam_id": "CAM-GBK-005", "status": "healthy", "last_detection": "2025-08-01T10:04:23.669585", "detection_rate_fps": 30, "image_count_24h": 2592000, "last_image_path": "./img.jpg", "error_count_24h": 0 },
278
+ { "cam_id": "CAM-KELAPA-GADING-TIMUR-64", "status": "healthy", "last_detection": "2025-08-01T10:04:23.669585", "detection_rate_fps": 30, "image_count_24h": 2592000, "last_image_path": "./img.jpg", "error_count_24h": 0 },
279
+ { "cam_id": "CAM-Panggang-Dermaga-utama", "status": "healthy", "last_detection": "2025-08-01T10:04:23.669585", "detection_rate_fps": 30, "image_count_24h": 2592000, "last_image_path": "./img.jpg", "error_count_24h": 0 },
280
+ { "cam_id": "CAM-BENDUNG-HILIR-003", "status": "healthy", "last_detection": "2025-08-01T10:04:23.669585", "detection_rate_fps": 30, "image_count_24h": 2592000, "last_image_path": "./img.jpg", "error_count_24h": 0 },
281
+ { "cam_id": "CAM-CENGKARENG-BARAT-001", "status": "healthy", "last_detection": "2025-08-01T10:04:23.669585", "detection_rate_fps": 30, "image_count_24h": 2592000, "last_image_path": "./img.jpg", "error_count_24h": 0 },
282
+ { "cam_id": "CAM-JATIPULO-003", "status": "healthy", "last_detection": "2025-08-01T10:04:23.669585", "detection_rate_fps": 30, "image_count_24h": 2592000, "last_image_path": "./img.jpg", "error_count_24h": 0 },
283
+ { "cam_id": "CAM-TOMANG-002", "status": "healthy", "last_detection": "2025-08-01T10:04:23.669585", "detection_rate_fps": 30, "image_count_24h": 2592000, "last_image_path": "./img.jpg", "error_count_24h": 0 },
284
+ { "cam_id": "CAM-BENDUNG-HILIR-002", "status": "healthy", "last_detection": "2025-08-01T10:04:23.669585", "detection_rate_fps": 30, "image_count_24h": 2592000, "last_image_path": "./img.jpg", "error_count_24h": 0 },
285
+ { "cam_id": "CAM-CIKOKO-004", "status": "healthy", "last_detection": "2025-08-01T10:04:23.669585", "detection_rate_fps": 30, "image_count_24h": 2592000, "last_image_path": "./img.jpg", "error_count_24h": 0 },
286
+ { "cam_id": "CAM-JATIPULO-004", "status": "healthy", "last_detection": "2025-08-01T10:04:23.669585", "detection_rate_fps": 30, "image_count_24h": 2592000, "last_image_path": "./img.jpg", "error_count_24h": 0 },
287
+ { "cam_id": "CAM-TOMANG-003", "status": "healthy", "last_detection": "2025-08-01T10:04:23.669585", "detection_rate_fps": 30, "image_count_24h": 2592000, "last_image_path": "./img.jpg", "error_count_24h": 0 },
288
+ { "cam_id": "CAM-PARKIR-P1-02", "status": "healthy", "last_detection": "2025-08-01T10:04:23.669585", "detection_rate_fps": 30, "image_count_24h": 2592000, "last_image_path": "./img.jpg", "error_count_24h": 0 },
289
+ { "cam_id": "CAM-GUDANG-A3-04", "status": "healthy", "last_detection": "2025-08-01T10:04:23.669585", "detection_rate_fps": 30, "image_count_24h": 2592000, "last_image_path": "./img.jpg", "error_count_24h": 0 },
290
+ { "cam_id": "CAM-ROOFTOP-01", "status": "healthy", "last_detection": "2025-08-01T10:04:23.669585", "detection_rate_fps": 30, "image_count_24h": 2592000, "last_image_path": "./img.jpg", "error_count_24h": 0 },
291
+ { "cam_id": "CAM-KAMAL37", "status": "error", "last_detection": "2025-07-31T10:04:23.669585", "detection_rate_fps": 0, "image_count_24h": 100, "last_image_path": "./img.jpg", "error_count_24h": 2591900 },
292
+ { "cam_id": "CAM-CIPEDAK15", "status": "healthy", "last_detection": "2025-08-01T10:04:23.669585", "detection_rate_fps": 30, "image_count_24h": 2592000, "last_image_path": "./img.jpg", "error_count_24h": 0 },
293
+ { "cam_id": "CAM-PONDOKPINANG53", "status": "healthy", "last_detection": "2025-08-01T10:04:23.669585", "detection_rate_fps": 30, "image_count_24h": 2592000, "last_image_path": "./img.jpg", "error_count_24h": 0 },
294
+ { "cam_id": "CAM-ULUJAMI319", "status": "error", "last_detection": "2025-07-31T10:04:23.669585", "detection_rate_fps": 0, "image_count_24h": 200, "last_image_path": "./img.jpg", "error_count_24h": 200000 },
295
+ { "cam_id": "CAM-LUBANGBUAYA42", "status": "healthy", "last_detection": "2025-08-01T10:04:23.669585", "detection_rate_fps": 30, "image_count_24h": 2592000, "last_image_path": "./img.jpg", "error_count_24h": 0 },
296
+ { "cam_id": "CAM-SETU44", "status": "warning", "last_detection": "2025-08-01T10:04:23.669585", "detection_rate_fps": 30, "image_count_24h": 1500000, "last_image_path": "./img.jpg", "error_count_24h": 0 },
297
+ { "cam_id": "CAM-KEMBANGANUTARA44", "status": "healthy", "last_detection": "2025-08-01T10:04:23.669585", "detection_rate_fps": 30, "image_count_24h": 2592000, "last_image_path": "./img.jpg", "error_count_24h": 0 },
298
+ { "cam_id": "CAM-RAWABADAKUTARA711", "status": "healthy", "last_detection": "2025-08-01T10:04:23.669585", "detection_rate_fps": 30, "image_count_24h": 2592000, "last_image_path": "./img.jpg", "error_count_24h": 0 },
299
+ { "cam_id": "CAM-SEMPERBARAT1615", "status": "healthy", "last_detection": "2025-08-01T10:04:23.669585", "detection_rate_fps": 30, "image_count_24h": 2592000, "last_image_path": "./img.jpg", "error_count_24h": 0 },
300
+ { "cam_id": "CAM-PASARMINGGU17", "status": "healthy", "last_detection": "2025-08-01T10:04:23.669585", "detection_rate_fps": 30, "image_count_24h": 2592000, "last_image_path": "./img.jpg", "error_count_24h": 0 },
301
+ { "cam_id": "CAM-SUSUKAN45", "status": "healthy", "last_detection": "2025-08-01T10:04:23.669585", "detection_rate_fps": 30, "image_count_24h": 2592000, "last_image_path": "./img.jpg", "error_count_24h": 0 },
302
+ { "cam_id": "CAM-MERUYAUTARA418", "status": "healthy", "last_detection": "2025-08-01T10:04:23.669585", "detection_rate_fps": 30, "image_count_24h": 2592000, "last_image_path": "./img.jpg", "error_count_24h": 0 },
303
+ { "cam_id": "CAM-KELGADINGBARAT221", "status": "healthy", "last_detection": "2025-08-01T10:04:23.669585", "detection_rate_fps": 30, "image_count_24h": 2592000, "last_image_path": "./img.jpg", "error_count_24h": 0 },
304
+ { "cam_id": "CAM-CILANDAKBARAT14", "status": "healthy", "last_detection": "2025-08-01T10:04:23.669585", "detection_rate_fps": 30, "image_count_24h": 2592000, "last_image_path": "./img.jpg", "error_count_24h": 0 },
305
+ { "cam_id": "CAM-PETOGOGAN311", "status": "healthy", "last_detection": "2025-08-01T10:04:23.669585", "detection_rate_fps": 30, "image_count_24h": 2592000, "last_image_path": "./img.jpg", "error_count_24h": 0 },
306
+ { "cam_id": "CAM-MAMPANGPRAPATAN11", "status": "healthy", "last_detection": "2025-08-01T10:04:23.669585", "detection_rate_fps": 30, "image_count_24h": 2592000, "last_image_path": "./img.jpg", "error_count_24h": 0 }
307
+ ],
308
+ "summary": {
309
+ "total_cameras": 40,
310
+ "active_cameras": 37, // Assuming healthy cameras are active
311
+ "total_services": 4,
312
+ "healthy_services": 1
313
+ }
314
+ };
315
+
316
+ // Helper function to format timestamp
317
+ function formatTimestamp(isoString) {
318
+ if (!isoString) return 'N/A';
319
+ const date = new Date(isoString);
320
+ const options = {
321
+ year: 'numeric', month: 'long', day: 'numeric',
322
+ hour: '2-digit', minute: '2-digit', second: '2-digit',
323
+ hour12: false
324
+ };
325
+ return date.toLocaleDateString('en-US', options);
326
+ }
327
+
328
+ // Helper function to format uptime
329
+ function formatUptime(seconds) {
330
+ if (seconds === null) return 'N/A';
331
+ const d = Math.floor(seconds / (3600 * 24));
332
+ const h = Math.floor(seconds % (3600 * 24) / 3600);
333
+ const m = Math.floor(seconds % 3600 / 60);
334
+ const s = Math.floor(seconds % 60);
335
+
336
+ const parts = [];
337
+ if (d > 0) parts.push(`${d} days`);
338
+ if (h > 0) parts.push(`${h} hours`);
339
+ if (m > 0) parts.push(`${m} minutes`);
340
+ if (s > 0 || parts.length === 0) parts.push(`${s} seconds`); // Ensure "0 seconds" if uptime is very low
341
+
342
+ return parts.join(' ');
343
+ }
344
+
345
+ // Render Overall Status and Summary
346
+ function renderOverallStatus() {
347
+ const overallStatusCard = document.getElementById('overall-status-card');
348
+ const overallStatusText = document.getElementById('overall-status-text');
349
+ const totalCameras = document.getElementById('total-cameras');
350
+ const activeCameras = document.getElementById('active-cameras');
351
+ const totalServices = document.getElementById('total-services');
352
+ const healthyServices = document.getElementById('healthy-services');
353
+
354
+ overallStatusText.textContent = healthData.overall_status.charAt(0).toUpperCase() + healthData.overall_status.slice(1);
355
+ overallStatusCard.className = `col-span-1 md:col-span-2 p-6 rounded-lg shadow-md flex items-center justify-between text-white`;
356
+
357
+ let bgColorClass = '';
358
+ let iconClass = '';
359
+ let message = '';
360
+
361
+ switch (healthData.overall_status) {
362
+ case 'healthy':
363
+ bgColorClass = 'from-green-600 to-green-400';
364
+ iconClass = 'fas fa-check-circle';
365
+ message = 'All Systems Running Normally';
366
+ break;
367
+ case 'warning':
368
+ bgColorClass = 'from-amber-600 to-amber-400';
369
+ iconClass = 'fas fa-exclamation-triangle';
370
+ message = 'Some Issues Detected';
371
+ break;
372
+ case 'critical':
373
+ bgColorClass = 'from-red-600 to-red-400';
374
+ iconClass = 'fas fa-exclamation-circle';
375
+ message = 'Immediate Attention Required';
376
+ break;
377
+ default:
378
+ bgColorClass = 'from-gray-600 to-gray-400';
379
+ iconClass = 'fas fa-question-circle';
380
+ message = 'Status Unknown';
381
+ }
382
+
383
+ // Fix: Split the bgColorClass string into individual classes
384
+ overallStatusCard.classList.add('bg-gradient-to-r', ...bgColorClass.split(' '));
385
+ overallStatusCard.querySelector('.text-6xl i').className = iconClass;
386
+ overallStatusCard.querySelector('p:last-child').textContent = message;
387
+
388
+ totalCameras.textContent = healthData.summary.total_cameras;
389
+ activeCameras.textContent = healthData.summary.active_cameras;
390
+ totalServices.textContent = healthData.summary.total_services;
391
+ healthyServices.textContent = healthData.summary.healthy_services;
392
+ }
393
+
394
+ // Render System Metrics
395
+ function renderSystemMetrics() {
396
+ const sysMetrics = healthData.system_metrics;
397
+
398
+ document.getElementById('cpu-progress').style.width = `${sysMetrics.cpu_percent}%`;
399
+ document.getElementById('cpu-percent').textContent = `${sysMetrics.cpu_percent}%`;
400
+
401
+ document.getElementById('memory-progress').style.width = `${sysMetrics.memory_percent}%`;
402
+ document.getElementById('memory-percent').textContent = `${sysMetrics.memory_percent}%`;
403
+
404
+ document.getElementById('disk-progress').style.width = `${sysMetrics.disk_percent.toFixed(2)}%`;
405
+ document.getElementById('disk-percent').textContent = `${sysMetrics.disk_percent.toFixed(2)}%`;
406
+
407
+ document.getElementById('bytes-sent').textContent = sysMetrics.network_io.bytes_sent.toLocaleString();
408
+ document.getElementById('bytes-recv').textContent = sysMetrics.network_io.bytes_recv.toLocaleString();
409
+ document.getElementById('packets-sent').textContent = sysMetrics.network_io.packets_sent.toLocaleString();
410
+ document.getElementById('packets-recv').textContent = sysMetrics.network_io.packets_recv.toLocaleString();
411
+
412
+ document.getElementById('process-count').textContent = sysMetrics.process_count;
413
+ document.getElementById('load-average').textContent = sysMetrics.load_average.join(', ');
414
+ document.getElementById('system-timestamp').textContent = `Last updated: ${formatTimestamp(sysMetrics.timestamp)}`;
415
+ }
416
+
417
+ // Render Services Status
418
+ function renderServicesStatus() {
419
+ const tableBody = document.getElementById('services-table-body');
420
+ tableBody.innerHTML = ''; // Clear previous rows
421
+
422
+ healthData.services.forEach(service => {
423
+ const row = document.createElement('tr');
424
+ let statusClass = '';
425
+ let statusIcon = '';
426
+ switch (service.status) {
427
+ case 'healthy':
428
+ statusClass = 'bg-green-100 text-green-800';
429
+ statusIcon = '<i class="fas fa-check-circle mr-1"></i>';
430
+ break;
431
+ case 'warning':
432
+ statusClass = 'bg-amber-100 text-amber-800';
433
+ statusIcon = '<i class="fas fa-exclamation-triangle mr-1"></i>';
434
+ break;
435
+ case 'critical':
436
+ statusClass = 'bg-red-100 text-red-800';
437
+ statusIcon = '<i class="fas fa-times-circle mr-1"></i>';
438
+ break;
439
+ default:
440
+ statusClass = 'bg-gray-100 text-gray-800';
441
+ statusIcon = '<i class="fas fa-question-circle mr-1"></i>';
442
+ }
443
+
444
+ let detailsHtml = '';
445
+ if (service.details) {
446
+ if (service.details.message) {
447
+ detailsHtml = service.details.message;
448
+ } else if (service.details.missing_collections && service.details.missing_collections.length > 0) {
449
+ detailsHtml = `Missing collections: ${service.details.missing_collections.join(', ')}`;
450
+ } else if (service.name === 'detection_service') {
451
+ detailsHtml = `Detections 5m: ${service.details.total_detections_5min}, Active Cameras 5m: ${service.details.active_cameras_5min}, Avg FPS: ${service.details.avg_fps_per_camera}`;
452
+ } else if (service.details.uptime_seconds !== undefined) {
453
+ detailsHtml = `Uptime: ${formatUptime(service.details.uptime_seconds)}`;
454
+ }
455
+ }
456
+
457
+ // Determine response time color
458
+ let responseTimeClass = 'text-gray-900';
459
+ if (service.response_time_ms > 5000) { // Example threshold for slow response
460
+ responseTimeClass = 'text-red-600 font-semibold';
461
+ } else if (service.response_time_ms > 1000) {
462
+ responseTimeClass = 'text-amber-600';
463
+ }
464
+
465
+ row.innerHTML = `
466
+ <td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">${service.name}</td>
467
+ <td class="px-6 py-4 whitespace-nowrap">
468
+ <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${statusClass}">
469
+ ${statusIcon}${service.status.charAt(0).toUpperCase() + service.status.slice(1)}
470
+ </span>
471
+ </td>
472
+ <td class="px-6 py-4 whitespace-nowrap text-sm ${responseTimeClass}">
473
+ ${service.response_time_ms !== null ? service.response_time_ms.toFixed(2) : 'N/A'}
474
+ </td>
475
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${formatTimestamp(service.last_check)}</td>
476
+ <td class="px-6 py-4 text-sm text-gray-500">${detailsHtml || 'N/A'}</td>
477
+ `;
478
+ tableBody.appendChild(row);
479
+ });
480
+ }
481
+
482
+ // Render Camera Status
483
+ function renderCameras(filter = 'all', searchTerm = '') {
484
+ const camerasGrid = document.getElementById('cameras-grid');
485
+ camerasGrid.innerHTML = ''; // Clear previous cards
486
+
487
+ let filteredCameras = healthData.cameras.filter(camera => {
488
+ const matchesSearch = camera.cam_id.toLowerCase().includes(searchTerm.toLowerCase());
489
+ const matchesFilter = filter === 'all' || camera.status === filter;
490
+ return matchesSearch && matchesFilter;
491
+ });
492
+
493
+ // Update filter counts
494
+ const totalCount = healthData.cameras.length;
495
+ const errorCount = healthData.cameras.filter(c => c.status === 'error').length;
496
+ const warningCount = healthData.cameras.filter(c => c.status === 'warning').length;
497
+ const healthyCount = healthData.cameras.filter(c => c.status === 'healthy').length;
498
+
499
+ document.getElementById('count-all').textContent = totalCount;
500
+ document.getElementById('count-error').textContent = errorCount;
501
+ document.getElementById('count-warning').textContent = warningCount; // Fixed: Update warning count
502
+ document.getElementById('count-healthy').textContent = healthyCount;
503
+
504
+ // Update camera summary text
505
+ const cameraSummary = document.getElementById('camera-summary');
506
+ cameraSummary.textContent = `Displaying ${filteredCameras.length} of ${totalCount} cameras. ${healthyCount} healthy, ${warningCount} warning, ${errorCount} error.`;
507
+
508
+
509
+ if (filteredCameras.length === 0) {
510
+ camerasGrid.innerHTML = '<p class="text-gray-500 text-center col-span-full py-8">No cameras matching criteria.</p>';
511
+ return;
512
+ }
513
+
514
+ filteredCameras.forEach(camera => {
515
+ const card = document.createElement('div');
516
+ let statusClass = '';
517
+ let statusIcon = '';
518
+ switch (camera.status) {
519
+ case 'healthy':
520
+ statusClass = 'bg-green-500';
521
+ statusIcon = '<i class="fas fa-check-circle"></i>';
522
+ break;
523
+ case 'warning':
524
+ statusClass = 'bg-amber-500'; // Using amber for warning
525
+ statusIcon = '<i class="fas fa-exclamation-triangle"></i>';
526
+ break;
527
+ case 'error':
528
+ statusClass = 'bg-red-500';
529
+ statusIcon = '<i class="fas fa-times-circle"></i>';
530
+ break;
531
+ default:
532
+ statusClass = 'bg-gray-500';
533
+ statusIcon = '<i class="fas fa-question-circle"></i>';
534
+ }
535
+
536
+ card.className = `bg-white rounded-lg shadow-md overflow-hidden transition-transform transform hover:scale-105`;
537
+ card.innerHTML = `
538
+ <div class="p-4">
539
+ <h4 class="text-lg font-semibold text-gray-800 mb-2">${camera.cam_id}</h4>
540
+ <div class="flex items-center text-white px-3 py-1 rounded-full text-sm font-medium w-fit ${statusClass}">
541
+ ${statusIcon} <span class="ml-2">${camera.status.charAt(0).toUpperCase() + camera.status.slice(1)}</span>
542
+ </div>
543
+ <p class="text-xs text-gray-500 mt-2">Last Detection: ${camera.last_detection ? formatTimestamp(camera.last_detection) : 'N/A'}</p>
544
+ <p class="text-xs text-gray-500">Detection FPS: ${camera.detection_rate_fps !== null ? camera.detection_rate_fps : 'N/A'}</p>
545
+ <p class="text-xs text-gray-500">Images (24h): ${camera.image_count_24h}</p>
546
+ <p class="text-xs text-gray-500">Errors (24h): ${camera.error_count_24h}</p>
547
+ <p class="text-xs text-gray-500">Last Image Path: ${camera.last_image_path || 'N/A'}</p>
548
+ </div>
549
+ `;
550
+ camerasGrid.appendChild(card);
551
+ });
552
+ }
553
+
554
+ // Initialize dashboard on DOMContentLoaded
555
+ document.addEventListener('DOMContentLoaded', () => {
556
+ renderOverallStatus();
557
+ renderSystemMetrics();
558
+ renderServicesStatus();
559
+ renderCameras(); // Initial render of all cameras
560
+
561
+ // Event Listeners for Camera Filters
562
+ document.querySelectorAll('.filter-btn').forEach(button => {
563
+ button.addEventListener('click', (event) => {
564
+ // Remove active class from all buttons
565
+ document.querySelectorAll('.filter-btn').forEach(btn => {
566
+ const filterType = btn.dataset.filter;
567
+ // Reset to default 500 color based on filter type
568
+ if (filterType === 'all') {
569
+ btn.classList.remove('bg-blue-600');
570
+ btn.classList.add('bg-blue-500');
571
+ } else if (filterType === 'error') {
572
+ btn.classList.remove('bg-red-600');
573
+ btn.classList.add('bg-red-500');
574
+ } else if (filterType === 'healthy') {
575
+ btn.classList.remove('bg-green-600');
576
+ btn.classList.add('bg-green-500');
577
+ } else if (filterType === 'warning') { // Fixed: Reset warning button color
578
+ btn.classList.remove('bg-orange-600');
579
+ btn.classList.add('bg-orange-500');
580
+ }
581
+ });
582
+
583
+ // Add active class to the clicked button
584
+ const clickedButton = event.target;
585
+ const clickedFilterType = clickedButton.dataset.filter;
586
+ if (clickedFilterType === 'all') {
587
+ clickedButton.classList.remove('bg-blue-500');
588
+ clickedButton.classList.add('bg-blue-600');
589
+ } else if (clickedFilterType === 'error') {
590
+ clickedButton.classList.remove('bg-red-500');
591
+ clickedButton.classList.add('bg-red-600');
592
+ } else if (clickedFilterType === 'healthy') {
593
+ clickedButton.classList.remove('bg-green-500');
594
+ clickedButton.classList.add('bg-green-600');
595
+ } else if (clickedFilterType === 'warning') { // Fixed: Set warning button color
596
+ clickedButton.classList.remove('bg-orange-500');
597
+ clickedButton.classList.add('bg-orange-600');
598
+ }
599
+
600
+ const filter = clickedFilterType;
601
+ const searchTerm = document.getElementById('camera-search').value;
602
+ renderCameras(filter, searchTerm);
603
+ });
604
+ });
605
+
606
+ // Set initial active filter button (All)
607
+ document.querySelector('.filter-btn[data-filter="all"]').classList.remove('bg-blue-500');
608
+ document.querySelector('.filter-btn[data-filter="all"]').classList.add('bg-blue-600');
609
+
610
+
611
+ // Event Listener for Camera Search
612
+ document.getElementById('camera-search').addEventListener('keyup', (event) => {
613
+ const searchTerm = event.target.value;
614
+ // Get the currently active filter button's data-filter value
615
+ const activeFilterBtn = document.querySelector('.filter-btn.bg-blue-600, .filter-btn.bg-red-600, .filter-btn.bg-green-600, .filter-btn.bg-orange-600'); // Fixed: Include orange in active button check
616
+ const filter = activeFilterBtn ? activeFilterBtn.dataset.filter : 'all'; // Fallback to 'all' if no active button found
617
+ renderCameras(filter, searchTerm);
618
+ });
619
+ });
620
+ </script>
621
+ </body>
622
  </html>