11 "<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\" /><meta name=\"viewport\" content=\"width=devic"
12 "e-width, initial-scale=1\" /><title>Dashboard</title> <style>*{box-sizing:border-box;}body{margin:0;font-famil"
13 "y:system-ui,\"Segoe UI\",Roboto,sans-serif;background:#0e1116;color:#e6edf3;}header{display:flex;align-items:c"
14 "enter;gap:10px;padding:14px 20px;font-size:18px;font-weight:600;border-bottom:1px solid #30363d;}#dot{width:10"
15 "px;height:10px;border-radius:50%;background:#f85149;}#dot.live{background:#3fb950;}#status{font-size:13px;font"
16 "-weight:400;color:#8b949e;}#grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:14"
17 "px;padding:18px;}.card{background:#1b2230;border:1px solid #30363d;border-radius:10px;padding:14px;}.card .lab"
18 "el{margin-bottom:8px;font-size:13px;color:#8b949e;}.card .num{font-size:30px;font-weight:600;font-variant-nume"
19 "ric:tabular-nums;}.card .unit{margin-left:4px;font-size:14px;color:#8b949e;}svg{display:block;width:100%;heigh"
20 "t:auto;}svg text{fill:#e6edf3;font-family:inherit;}canvas{display:block;width:100%;height:auto;}.btn{padding:8"
21 "px 14px;font:inherit;color:#fff;background:#238636;border:0;border-radius:6px;cursor:pointer;}.btn:active{back"
22 "ground:#2ea043;}input[type=\"range\"]{width:100%;accent-color:#3fb950;}.sw{width:42px;height:22px;cursor:point"
23 "er;accent-color:#3fb950;}</style> </head><body><header><span id=\"dot\"></span> Dashboard <span id=\"status\">"
24 "</span></header><div id=\"grid\"></div> <script>(function () {\nvar NS = \"http://www.w3.org/2000/svg\";\nvar "
25 "base = location.pathname.replace(/\\/+$/, \"\");\nvar grid = document.getElementById(\"grid\");\nvar dot = doc"
26 "ument.getElementById(\"dot\");\nvar status = document.getElementById(\"status\");\nvar widgets = {};\nvar ws ="
27 " null;\nfunction sendCtrl(k, v) {\nif (ws && ws.readyState === 1)\nws.send(JSON.stringify({ k: k, v: v }));\n}"
28 "\nfunction clamp(f) {\nreturn f < 0 ? 0 : f > 1 ? 1 : f;\n}\nfunction frac(v, mn, mx) {\nreturn mx > mn ? clam"
29 "p((v - mn) / (mx - mn)) : 0;\n}\nfunction fmt(v) {\nreturn Math.abs(v) >= 100 ? v.toFixed(0) : v.toFixed(1);\n"
30 "}\nfunction el(tag, attrs) {\nvar e = document.createElementNS(NS, tag);\nfor (var k in attrs) e.setAttribute("
31 "k, attrs[k]);\nreturn e;\n}\nfunction polar(cx, cy, r, deg) {\nvar a = (deg * Math.PI) / 180;\nreturn [cx + r "
32 "* Math.cos(a), cy - r * Math.sin(a)];\n}\nfunction arc(cx, cy, r, f) {\nvar s = polar(cx, cy, r, 180),\ne = po"
33 "lar(cx, cy, r, 180 - 180 * f);\nreturn (\n\"M\" +\ns[0] +\n\" \" +\ns[1] +\n\" A\" +\nr +\n\" \" +\nr +\n\" 0 "
34 "0 1 \" +\ne[0] +\n\" \" +\ne[1]\n);\n}\nfunction card(w) {\nvar c = document.createElement(\"div\");\nc.classN"
35 "ame = \"card\";\nvar lab = document.createElement(\"div\");\nlab.className = \"label\";\nlab.textContent = w.l"
36 "abel;\nc.appendChild(lab);\nvar upd;\nif (w.type === \"gauge\") {\nvar g = el(\"svg\", { viewBox: \"0 0 120 72"
37 "\" });\ng.appendChild(\nel(\"path\", {\nd: arc(60, 62, 50, 1),\nfill: \"none\",\nstroke: \"#30363d\",\n\"strok"
38 "e-width\": 10,\n\"stroke-linecap\": \"round\",\n}),\n);\nvar gv = el(\"path\", {\nd: arc(60, 62, 50, 0),\nfill"
39 ": \"none\",\nstroke: \"#3fb950\",\n\"stroke-width\": 10,\n\"stroke-linecap\": \"round\",\n});\ng.appendChild(g"
40 "v);\nvar gt = el(\"text\", {\nx: 60,\ny: 58,\n\"text-anchor\": \"middle\",\n\"font-size\": 16,\n\"font-weight"
41 "\": 600,\n});\ngt.textContent = \"-\";\ng.appendChild(gt);\nc.appendChild(g);\nupd = function (v) {\ngv.setAtt"
42 "ribute(\n\"d\",\narc(60, 62, 50, frac(v, w.min, w.max)),\n);\ngt.textContent = fmt(v) + w.unit;\n};\n} else if"
43 " (w.type === \"bar\") {\nvar b = el(\"svg\", { viewBox: \"0 0 200 40\" });\nb.appendChild(\nel(\"rect\", {\nx:"
44 " 0,\ny: 16,\nwidth: 200,\nheight: 12,\nrx: 6,\nfill: \"#30363d\",\n}),\n);\nvar bf = el(\"rect\", {\nx: 0,\ny:"
45 " 16,\nwidth: 0,\nheight: 12,\nrx: 6,\nfill: \"#3fb950\",\n});\nb.appendChild(bf);\nvar bt = el(\"text\", { x: "
46 "0, y: 11, \"font-size\": 12 });\nbt.textContent = \"-\";\nb.appendChild(bt);\nc.appendChild(b);\nupd = functio"
47 "n (v) {\nbf.setAttribute(\n\"width\",\n200 * frac(v, w.min, w.max),\n);\nbt.textContent = fmt(v) + w.unit;\n};"
48 "\n} else if (w.type === \"sparkline\") {\nvar s = el(\"svg\", { viewBox: \"0 0 200 50\" });\nvar pl = el(\"pol"
49 "yline\", {\nfill: \"none\",\nstroke: \"#3fb950\",\n\"stroke-width\": 2,\npoints: \"\",\n});\ns.appendChild(pl)"
50 ";\nc.appendChild(s);\nvar sn = document.createElement(\"div\");\nsn.className = \"num\";\nsn.textContent = \"-"
51 "\";\nc.appendChild(sn);\nvar hist = [];\nupd = function (v) {\nhist.push(v);\nif (hist.length > 40) hist.shift"
52 "();\nvar pts = hist\n.map(function (y, i) {\nreturn (\n(200 * i) / 39 +\n\",\" +\n(50 - 50 * frac(y, w.min, w."
53 "max))\n);\n})\n.join(\" \");\npl.setAttribute(\"points\", pts);\nsn.textContent = fmt(v) + \" \" + w.unit;\n};"
54 "\n} else if (w.type === \"chart\") {\nvar cv = document.createElement(\"canvas\");\ncv.width = 240;\ncv.height"
55 " = 64;\nc.appendChild(cv);\nvar cn = document.createElement(\"div\");\ncn.className = \"num\";\ncn.textContent"
56 " = \"-\";\nc.appendChild(cn);\nvar ctx = cv.getContext(\"2d\");\nvar ch = [];\nupd = function (v) {\nch.push(v"
57 ");\nif (ch.length > cv.width) ch.shift();\nctx.clearRect(0, 0, cv.width, cv.height);\nctx.strokeStyle = \"#3fb"
58 "950\";\nctx.lineWidth = 2;\nctx.beginPath();\nfor (var i = 0; i < ch.length; i++) {\nvar x = (cv.width * i) / "
59 "(cv.width - 1);\nvar y =\ncv.height -\ncv.height * frac(ch[i], w.min, w.max);\nif (i === 0) ctx.moveTo(x, y);"
60 "\nelse ctx.lineTo(x, y);\n}\nctx.stroke();\ncn.textContent = fmt(v) + \" \" + w.unit;\n};\n} else if (w.type ="
61 "== \"button\") {\nvar btn = document.createElement(\"button\");\nbtn.className = \"btn\";\nbtn.textContent = "
62 "\"Press\";\nbtn.onclick = function () {\nsendCtrl(w.key, 1);\n};\nc.appendChild(btn);\nupd = function () {};\n"
63 "} else if (w.type === \"toggle\") {\nvar tg = document.createElement(\"input\");\ntg.type = \"checkbox\";\ntg."
64 "className = \"sw\";\ntg.onchange = function () {\nsendCtrl(w.key, tg.checked ? 1 : 0);\n};\nc.appendChild(tg);"
65 "\nupd = function (v) {\ntg.checked = v >= 0.5;\n};\n} else if (w.type === \"slider\") {\nvar sl = document.cre"
66 "ateElement(\"input\");\nsl.type = \"range\";\nsl.min = w.min;\nsl.max = w.max;\nsl.value = w.min;\nvar sv = do"
67 "cument.createElement(\"span\");\nsv.className = \"unit\";\nsv.textContent = fmt(w.min) + w.unit;\nsl.oninput ="
68 " function () {\nsv.textContent = fmt(+sl.value) + w.unit;\n};\nsl.onchange = function () {\nsendCtrl(w.key, +s"
69 "l.value);\n};\nc.appendChild(sl);\nc.appendChild(sv);\nupd = function () {};\n} else {\nvar n = document.creat"
70 "eElement(\"div\");\nn.className = \"num\";\nvar tn = document.createTextNode(\"-\");\nn.appendChild(tn);\nvar "
71 "u = document.createElement(\"span\");\nu.className = \"unit\";\nu.textContent = w.unit;\nn.appendChild(u);\nc."
72 "appendChild(n);\nupd = function (v) {\ntn.nodeValue = fmt(v);\n};\n}\ngrid.appendChild(c);\nreturn upd;\n}\nfe"
73 "tch(base + \"/layout\")\n.then(function (r) {\nreturn r.json();\n})\n.then(function (list) {\nlist.forEach(fun"
74 "ction (w) {\nwidgets[w.key] = card(w);\n});\nif (\nlist.some(function (w) {\nreturn (\nw.type === \"button\" |"
75 "|\nw.type === \"toggle\" ||\nw.type === \"slider\"\n);\n})\n) {\nvar proto =\nlocation.protocol === \"https:\""
76 "\n? \"wss://\"\n: \"ws://\";\nws = new WebSocket(\nproto + location.host + base + \"/ws\",\n);\n}\nvar es = ne"
77 "w EventSource(base + \"/stream\");\nes.onopen = function () {\ndot.className = \"live\";\nstatus.textContent ="
78 " \"live\";\n};\nes.onerror = function () {\ndot.className = \"\";\nstatus.textContent = \"reconnecting...\";\n"
79 "};\nes.onmessage = function (ev) {\ntry {\nvar d = JSON.parse(ev.data);\nfor (var k in d)\nif (widgets[k]) wid"
80 "gets[k](d[k]);\n} catch (e) {}\n};\n});\n})();</script> </body></html>";
83 "<!doctype html><html><head><meta name=\"viewport\" content=\"width=device-width\" /><title>WiFi setup</title><"
84 "/head><body><h2>WiFi setup</h2><form method=\"POST\" action=\"/save\"> SSID:<br /><input name=\"ssid\" maxleng"
85 "th=\"32\" /><br /> Password:<br /><input name=\"psk\" type=\"password\" maxlength=\"63\" /><br /><br /><input "
86 "type=\"submit\" value=\"Save\" /></form></body></html>";
91 "<!doctype html><html><head><meta charset=\"utf-8\" /><meta name=\"viewport\" content=\"width=device-width,init"
92 "ial-scale=1\" /><title>PC Terminal</title> <style>:root{--g:#2bb35a;--g2:#00c000;--bg:#080c08;}*{box-sizing:bo"
93 "rder-box;}html,body{margin:0;height:100%;background:var(--bg);color:var(--g);font:14px/1.45 \"Consolas\",\"Dej"
94 "aVu Sans Mono\",monospace;}#wrap{display:flex;flex-direction:column;height:100%;position:relative;}#out{flex:1"
95 ";overflow:auto;padding:10px;white-space:pre-wrap;word-break:break-word;text-shadow:0 0 4px rgba(43,179,90,0.55"
96 ");}#bar{display:flex;border-top:1px solid #14401f;background:#0a0f0a;}#ps{padding:8px 4px 8px 10px;color:var(-"
97 "-g2);}#in{flex:1;background:transparent;border:0;color:var(--g);font:inherit;padding:8px;outline:none;}#st{pad"
98 "ding:4px 10px;font-size:11px;color:#1c6e38;border-top:1px solid #14401f;}#crt{position:absolute;inset:0;pointe"
99 "r-events:none;z-index:9;background:repeating-linear-gradient( rgba(0,0,0,0) 0 2px,rgba(0,0,0,0.18) 3px );box-s"
100 "hadow:inset 0 0 120px rgba(0,0,0,0.6);}.e{color:#d2553a;}</style> </head><body><div id=\"wrap\"><div id=\"out"
101 "\"></div><div id=\"bar\"><span id=\"ps\">></span ><input id=\"in\" autocomplete=\"off\" autofocus /></div><"
102 "div id=\"st\">connecting…</div><div id=\"crt\"></div></div> <script>var out = document.getElementById(\"out\")"
103 ",\ninp = document.getElementById(\"in\"),\nst = document.getElementById(\"st\");\nvar hist = [],\nhi = 0,\nws,"
104 "\nurl =\n(location.protocol == \"https:\" ? \"wss\" : \"ws\") +\n\"://\" +\nlocation.host +\nlocation.pathname"
105 ".replace(/\\/$/, \"\") +\n\"/ws\";\nfunction add(t, c) {\nvar b =\nout.scrollHeight - out.clientHeight - out.s"
106 "crollTop < 40;\nvar s = document.createElement(\"span\");\nif (c) s.className = c;\ns.textContent = t;\nout.ap"
107 "pendChild(s);\nif (b) out.scrollTop = out.scrollHeight;\n}\nfunction conn() {\nws = new WebSocket(url);\nws.on"
108 "open = function () {\nst.textContent = \"connected\";\n};\nws.onmessage = function (e) {\nadd(e.data);\n};\nws"
109 ".onclose = function () {\nst.textContent = \"disconnected — retrying…\";\nsetTimeout(conn, 1000);\n};\nws.oner"
110 "ror = function () {\ntry {\nws.close();\n} catch (_) {}\n};\n}\ninp.addEventListener(\"keydown\", function (e)"
111 " {\nif (e.key == \"Enter\") {\nvar v = inp.value;\nif (v) {\nif (ws && ws.readyState == 1) {\nws.send(v);\nadd"
112 "(\"> \" + v + \"\\n\");\n} else add(\"> \" + v + \" (offline)\\n\", \"e\");\nhist.push(v);\nhi = hist.length;"
113 "\n}\ninp.value = \"\";\n} else if (e.key == \"ArrowUp\") {\nif (hi > 0) {\nhi--;\ninp.value = hist[hi] || \"\""
114 ";\n}\n} else if (e.key == \"ArrowDown\") {\nif (hi < hist.length) {\nhi++;\ninp.value = hist[hi] || \"\";\n}\n"
115 "}\n});\nconn();</script> </body></html>";
119 "// Client half of the delivery story. The device is slow and often asleep, so the browser should not\n// wait "
120 "on it for the shell: cache the shell once, serve it from cache instantly, and refresh in the\n// background. T"
121 "hat is RFC 5861 stale-while-revalidate applied on the client, matching what\n// pc_delivery_swr decides server"
122 "-side.\n//\n// The manifest (pc_delivery_sw_manifest) supplies both the file list and a version tag; the cache"
123 " is\n// named after that version, so publishing a new firmware invalidates the old shell exactly once.\n\nvar "
124 "MANIFEST_URL = \"/precache.json\";\nvar CACHE_PREFIX = \"pc-\";\n\nfunction manifest() {\n // cache: \"no-sto"
125 "re\" - the manifest is the freshness signal, so it must never come from a cache.\n return fetch(MANIFEST_URL,"
126 " { cache: \"no-store\" }).then(function (r) {\n if (!r.ok) throw new Error(\"manifest \" + r.status);\n "
127 "return r.json();\n });\n}\n\nfunction cacheName(m) {\n return CACHE_PREFIX + (m && m.version ? m.version : "
128 "\"0\");\n}\n\nself.addEventListener(\"install\", function (e) {\n e.waitUntil(\n manifest()\n .then(f"
129 "unction (m) {\n return caches.open(cacheName(m)).then(function (c) {\n return c.addAll((m && m"
130 ".precache) || []);\n });\n })\n // A failed precache must not wedge the worker: it still inst"
131 "alls and fills the cache lazily\n // on first fetch. Losing the shell is better than losing the page.\n "
132 " .catch(function () {})\n .then(function () {\n return self.skipWaiting();\n })\n );\n});"
133 "\n\nself.addEventListener(\"activate\", function (e) {\n e.waitUntil(\n manifest()\n .then(function ("
134 "m) {\n var keep = cacheName(m);\n return caches.keys().then(function (keys) {\n return "
135 "Promise.all(\n keys.map(function (k) {\n // Only ever delete our own versioned caches."
136 "\n return k.indexOf(CACHE_PREFIX) === 0 && k !== keep ? caches.delete(k) : null;\n })"
137 "\n );\n });\n })\n .catch(function () {})\n .then(function () {\n return"
138 " self.clients.claim();\n })\n );\n});\n\nself.addEventListener(\"fetch\", function (e) {\n var req = e."
139 "request;\n // Only same-origin GETs are ours to cache; anything else goes straight to the network.\n if (req"
140 ".method !== \"GET\" || new URL(req.url).origin !== self.location.origin) return;\n // The manifest drives inv"
141 "alidation, so it is never served from cache.\n if (new URL(req.url).pathname === MANIFEST_URL) return;\n\n e"
142 ".respondWith(\n caches.match(req).then(function (hit) {\n var net = fetch(req)\n .then(function"
143 " (res) {\n // Only cache a real, complete response: a 206 is a byte range, not the whole resource.\n "
144 " if (res && res.ok && res.status === 200) {\n var copy = res.clone();\n caches.k"
145 "eys().then(function (keys) {\n for (var i = 0; i < keys.length; i++) {\n if (keys["
146 "i].indexOf(CACHE_PREFIX) === 0) {\n caches.open(keys[i]).then(function (c) {\n "
147 " c.put(req, copy);\n });\n break;\n }\n }\n "
148 " });\n }\n return res;\n })\n .catch(function () {\n return"
149 " hit;\n });\n // Stale-while-revalidate: hand back the cached copy now, let the refresh land in th"
150 "e cache.\n return hit || net;\n })\n );\n});\n";
154 "{\"uptime_ms\":{{uptime_ms}},\"requests\":{{requests}},\"http_2xx\":{{http_2xx}},\"http_4xx\":{{http_4xx}},\"h"
155 "ttp_5xx\":{{http_5xx}},\"active_conns\":{{active_conns}},\"free_heap\":{{free_heap}}}";
159 "# HELP pc_uptime_seconds Device uptime in seconds.\n# TYPE pc_uptime_seconds gauge\npc_uptime_seconds {{uptime"
160 "_seconds}}\n# HELP pc_http_requests_total Total HTTP responses sent.\n# TYPE pc_http_requests_total counter\np"
161 "c_http_requests_total {{requests_total}}\n# HELP pc_http_responses_total HTTP responses by status class.\n# TY"
162 "PE pc_http_responses_total counter\npc_http_responses_total{class=\"2xx\"} {{resp_2xx}}\npc_http_responses_tot"
163 "al{class=\"4xx\"} {{resp_4xx}}\npc_http_responses_total{class=\"5xx\"} {{resp_5xx}}\n# HELP pc_active_connecti"
164 "ons Currently active connection slots.\n# TYPE pc_active_connections gauge\npc_active_connections {{active_con"
165 "ns}}\n# HELP pc_max_connections Connection slot capacity.\n# TYPE pc_max_connections gauge\npc_max_connections"
166 " {{max_conns}}\n# HELP pc_free_heap_bytes Free heap in bytes.\n# TYPE pc_free_heap_bytes gauge\npc_free_heap_b"
167 "ytes {{free_heap}}\n# HELP pc_min_free_heap_bytes Lowest free heap observed since boot, in bytes.\n# TYPE pc_m"
168 "in_free_heap_bytes gauge\npc_min_free_heap_bytes {{min_free_heap}}\n# HELP pc_heap_size_bytes Total heap size,"
169 " in bytes.\n# TYPE pc_heap_size_bytes gauge\npc_heap_size_bytes {{heap_size}}\n# HELP pc_max_alloc_heap_bytes "
170 "Largest contiguous allocatable heap block, in bytes.\n# TYPE pc_max_alloc_heap_bytes gauge\npc_max_alloc_heap_"
171 "bytes {{max_alloc_heap}}\n";
const char PC_STATS_JSON[]
Runtime stats JSON (rendered via send_template); add/rename fields to taste.
const char PC_PROV_FORM[]
Captive-portal form for WiFi credential entry (provisioning service).
const char PC_METRICS_PROM[]
All available Prometheus metrics; comment a value line out with a leading # to drop it.
const char PC_DASHBOARD_PAGE[]
Real-time SVG telemetry dashboard page (PC_ENABLE_DASHBOARD).
const char PC_SERVICE_WORKER[]
Service worker: precaches the app shell from the versioned manifest and serves it stale-while-revalid...
const char PC_PROV_SAVED_HTML[]
Success page shown after credentials are saved and the device reboots.
const char PC_TERMINAL_PAGE[]
Self-contained WebSocket terminal page (green-phosphor CRT theme; auto ws/wss).
Layer 7 (Application) - embedded web assets generated from web_assets/input/.