1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
6 * @brief System Administration example featuring a dark-themed HTML/CSS dashboard, stats API, and reboot sequencer.
8 * This example showcases:
9 * 1. Serving a single-page web dashboard using PROGMEM static resources.
10 * 2. Responsive CSS Grid styling, sleek dark-mode aesthetics, and micro-animations.
11 * 3. Hardware statistics endpoint (/api/sysinfo) tracking CPU, memory heap, and WiFi RSSI.
12 * 4. Clean reboot sequence: responding to the client first, then restarting the device
13 * after a 1-second delay in loop() to allow lwIP buffers to flush cleanly.
14 * 5. Input validation and config updates via POST endpoints.
15 * 6. Restricting admin APIs with a custom headers check (X-Admin-Token).
17 * To run this example:
18 * - Configure SSID/PASSWORD, and upload to an ESP32.
19 * - Find the ESP32 IP in the Serial Monitor, open it in any web browser.
20 * - Interact with the dashboard to view real-time diagnostics or reboot the chip.
23#include "DeterministicESPAsyncWebServer.h"
24#include "network_drivers/physical.h"
27static const char *SSID = "YOUR_SSID";
28static const char *PASSWORD = "YOUR_PASSWORD";
32// Simple authentication token for AJAX API queries
33static const char *ADMIN_TOKEN = "admin123";
35// System control flags for asynchronous reboot scheduling
36static bool pending_reboot = false;
37static unsigned long reboot_trigger_ms = 0;
39// --- Embedded Dashboard HTML (Premium Dark Theme) ---
40static const char ADMIN_HTML[] PROGMEM = R"rawhtml(
44 <meta charset="UTF-8">
45 <meta name="viewport" content="width=device-width, initial-scale=1.0">
46 <title>ESP32 Admin Node Console</title>
51 --text-color: #e2e8f0;
52 --text-muted: #94a3b8;
54 --accent-glow: rgba(99, 102, 241, 0.4);
60 box-sizing: border-box;
65 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
66 background-color: var(--bg-color);
67 color: var(--text-color);
71 flex-direction: column;
78 flex-direction: column;
83 justify-content: space-between;
85 border-bottom: 1px solid var(--border);
86 padding-bottom: 1.5rem;
96 background: linear-gradient(135deg, #a5b4fc, #6366f1);
97 -webkit-background-clip: text;
98 -webkit-text-fill-color: transparent;
103 background-color: var(--success);
105 box-shadow: 0 0 10px var(--success);
106 animation: pulse 2s infinite;
109 0% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7); }
110 70% { box-shadow: 0 0 0 8px rgba(16, 185, 129, 0); }
111 100% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); }
114 background-color: var(--card-bg);
115 border: 1px solid var(--border);
122 .token-input-card label {
124 color: var(--text-muted);
127 .token-input-card input {
128 background-color: var(--bg-color);
129 border: 1px solid var(--border);
131 color: var(--text-color);
132 padding: 0.5rem 0.75rem;
134 font-family: monospace;
137 .token-input-card input:focus {
138 border-color: var(--accent);
139 box-shadow: 0 0 5px var(--accent-glow);
143 grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
147 background-color: var(--card-bg);
148 border: 1px solid var(--border);
152 flex-direction: column;
154 transition: transform 0.2s, border-color 0.2s;
157 transform: translateY(-2px);
158 border-color: var(--accent);
162 text-transform: uppercase;
163 letter-spacing: 0.05em;
164 color: var(--text-muted);
175 flex-direction: column;
180 justify-content: space-between;
184 color: var(--text-muted);
188 font-family: monospace;
196 padding: 0.75rem 1.5rem;
202 transition: background 0.2s, transform 0.1s;
205 justify-content: center;
209 transform: scale(0.98);
212 background-color: var(--accent);
216 background-color: #5558e6;
219 background-color: var(--danger);
223 background-color: #dc2626;
228 top: 0; left: 0; right: 0; bottom: 0;
229 background-color: rgba(11, 15, 25, 0.85);
231 justify-content: center;
233 backdrop-filter: blur(4px);
236 background-color: var(--card-bg);
237 border: 1px solid var(--border);
244 flex-direction: column;
246 box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5);
251 <div class="container">
253 <div class="logo-area">
254 <div class="status-dot"></div>
255 <h1>ESP32 Admin Console</h1>
257 <span style="font-family: monospace; font-size: 0.9rem;" id="ip-addr">Node Active</span>
260 <div class="token-input-card">
261 <label for="token">Access Token:</label>
262 <input type="password" id="token" value="admin123" placeholder="Enter X-Admin-Token">
267 <div class="card-title">Memory Allocation</div>
268 <div class="card-value" id="free-heap">-- KB</div>
269 <ul class="data-list">
270 <li class="data-item">
271 <span class="data-label">Min Free Heap</span>
272 <span class="data-value" id="min-heap">-- KB</span>
274 <li class="data-item">
275 <span class="data-label">Max Alloc Block</span>
276 <span class="data-value" id="max-alloc">-- KB</span>
282 <div class="card-title">System Metrics</div>
283 <ul class="data-list" style="margin-top: 0.5rem;">
284 <li class="data-item">
285 <span class="data-label">Uptime</span>
286 <span class="data-value" id="uptime">-- s</span>
288 <li class="data-item">
289 <span class="data-label">Reset Reason</span>
290 <span class="data-value" id="reset-reason">--</span>
292 <li class="data-item">
293 <span class="data-label">Chip Revision</span>
294 <span class="data-value" id="chip-rev">--</span>
296 <li class="data-item">
297 <span class="data-label">CPU Freq</span>
298 <span class="data-value" id="cpu-freq">-- MHz</span>
304 <div class="card-title">Network Info</div>
305 <div class="card-value" id="wifi-rssi">-- dBm</div>
306 <ul class="data-list">
307 <li class="data-item">
308 <span class="data-label">SSID</span>
309 <span class="data-value" id="wifi-ssid">--</span>
311 <li class="data-item">
312 <span class="data-label">Channel</span>
313 <span class="data-value" id="wifi-channel">--</span>
319 <div class="actions">
320 <button class="btn-primary" onclick="loadStats()">Refresh Stats</button>
321 <button class="btn-danger" onclick="confirmReboot()">Restart Device</button>
325 <!-- Reboot Modal -->
326 <div class="modal" id="reboot-modal">
327 <div class="modal-content">
328 <h2>Restart ESP32?</h2>
329 <p style="color: var(--text-muted);">This will temporarily disconnect all socket sessions and reboot the node firmware.</p>
330 <div class="actions" style="justify-content: center;">
331 <button class="btn-primary" onclick="closeRebootModal()">Cancel</button>
332 <button class="btn-danger" onclick="triggerReboot()">Confirm Restart</button>
338 const getHeaders = () => {
340 'X-Admin-Token': document.getElementById('token').value
344 async function loadStats() {
346 const res = await fetch('/api/sysinfo', { headers: getHeaders() });
347 if (res.status === 401) {
348 alert('Unauthorized: Invalid Access Token!');
351 const data = await res.json();
353 document.getElementById('free-heap').innerText = Math.round(data.free_heap / 1024) + ' KB';
354 document.getElementById('min-heap').innerText = Math.round(data.min_free_heap / 1024) + ' KB';
355 document.getElementById('max-alloc').innerText = Math.round(data.max_alloc_heap / 1024) + ' KB';
356 document.getElementById('uptime').innerText = Math.round(data.uptime_ms / 1000) + ' s';
357 document.getElementById('reset-reason').innerText = data.reset_reason;
358 document.getElementById('chip-rev').innerText = 'Rev ' + data.chip_revision;
359 document.getElementById('cpu-freq').innerText = data.cpu_freq_mhz + ' MHz';
360 document.getElementById('wifi-rssi').innerText = data.wifi_rssi + ' dBm';
361 document.getElementById('wifi-ssid').innerText = data.wifi_ssid;
362 document.getElementById('wifi-channel').innerText = data.wifi_channel;
363 document.getElementById('ip-addr').innerText = data.ip_address;
365 console.error('Failed to load system stats', err);
369 function confirmReboot() {
370 document.getElementById('reboot-modal').style.display = 'flex';
373 function closeRebootModal() {
374 document.getElementById('reboot-modal').style.display = 'none';
377 async function triggerReboot() {
380 const res = await fetch('/api/restart', {
382 headers: getHeaders()
384 if (res.status === 200) {
385 alert('Reboot signal accepted. Reconnecting in 5 seconds...');
386 setTimeout(() => window.location.reload(), 5000);
388 alert('Error: Reboot rejected by node.');
391 alert('Connection lost. Node is rebooting...');
396 window.onload = loadStats;
402// --- Helper Functions ---
405 * @brief Translates ESP32 reset reason enum into user-friendly text string.
407const char *get_reset_reason_string(esp_reset_reason_t reason)
411 case ESP_RST_POWERON:
414 return "External Pin Reset";
416 return "Software Reboot";
418 return "Software Panic / Crash";
419 case ESP_RST_INT_WDT:
420 return "Interrupt Watchdog";
421 case ESP_RST_TASK_WDT:
422 return "Task Watchdog";
424 return "Generic Watchdog";
425 case ESP_RST_DEEPSLEEP:
426 return "Deep Sleep Exit";
427 case ESP_RST_BROWNOUT:
428 return "Brownout Event";
430 return "SDIO Interface Reset";
437 * @brief Verifies the custom administration security token header.
439bool verify_admin_privileges(const HttpReq *req)
441 const char *token = http_get_header(req, "X-Admin-Token");
442 return (token && strcmp(token, ADMIN_TOKEN) == 0);
445// --- Route Handlers ---
449 * Serves the embedded single-page HTML console dashboard.
451void handle_serve_dashboard(uint8_t slot_id, HttpReq *req)
453 server.send(slot_id, 200, "text/html", ADMIN_HTML);
457 * @brief GET /api/sysinfo
458 * Collects and serializes hardware telemetry to JSON. Requires valid auth token header.
460void handle_get_sysinfo(uint8_t slot_id, HttpReq *req)
462 if (!verify_admin_privileges(req))
464 server.send(slot_id, 401, "application/json", "{\"error\":\"Unauthorized\"}");
468 char response_buf[384];
469 snprintf(response_buf, sizeof(response_buf),
472 "\"min_free_heap\":%u,"
473 "\"max_alloc_heap\":%u,"
475 "\"reset_reason\":\"%s\","
476 "\"chip_revision\":%d,"
477 "\"cpu_freq_mhz\":%d,"
479 "\"wifi_ssid\":\"%s\","
480 "\"wifi_channel\":%d,"
481 "\"ip_address\":\"%s\""
483 ESP.getFreeHeap(), ESP.getMinFreeHeap(), ESP.getMaxAllocHeap(), millis(),
484 get_reset_reason_string(esp_reset_reason()), ESP.getChipRevision(), ESP.getCpuFreqMHz(), WiFi.RSSI(),
485 WiFi.SSID().c_str(), WiFi.channel(), WiFi.localIP().toString().c_str());
487 server.send(slot_id, 200, "application/json", response_buf);
491 * @brief POST /api/restart
492 * Receives reboot command and arms reboot timer.
494void handle_post_restart(uint8_t slot_id, HttpReq *req)
496 if (!verify_admin_privileges(req))
498 server.send(slot_id, 401, "application/json", "{\"error\":\"Unauthorized\"}");
502 // Acknowledge request immediately to send response headers back to client
503 server.send(slot_id, 200, "application/json", "{\"status\":\"rebooting\"}");
505 // Schedule the physical restart after response dispatch completes
506 pending_reboot = true;
507 reboot_trigger_ms = millis();
512 Serial.begin(115200);
514 Serial.println("\n--- DetWebServer SysAdmin Control Console ---");
516 init_wifi_physical(SSID, PASSWORD);
517 while (!wifi_ready())
522 Serial.println("\nWiFi Online!");
523 Serial.print("Access the Admin Dashboard via: http://");
524 Serial.println(WiFi.localIP());
526 server.set_cors("*");
528 // Register UI & REST services
529 server.on("/", HTTP_GET, handle_serve_dashboard);
530 server.on("/api/sysinfo", HTTP_GET, handle_get_sysinfo);
531 server.on("/api/restart", HTTP_POST, handle_post_restart);
533 if (server.begin(80))
535 Serial.println("SysAdmin Node Console successfully initialized.");
543 // Perform scheduled reboot sequence
544 if (pending_reboot && (millis() - reboot_trigger_ms >= 1000))
546 Serial.println("Reboot sequence triggered. Rebooting hardware now!");