code wiki / (root) / nx_iot_page.nx

nx_iot_page.nx source

↩ module page · 212 lines · 16028 B

1// nx_iot_page.nx -- EMIT the nishifamily.com/iot hub control panel. 2// 3// The UI surface for the IoT hub (operator directive 2026-06-20: "in 4// everything we build we need a UI we are iterating on -- that's the big 5// point of the nishifamily.com hosting"). This organ AUTHORS the page; 6// no JS/HTML is hand-written elsewhere. Same emit -> push -> verify loop 7// every property reuses (publishing is the separate sovereign step 8// nx_aw_push, additive + vault-backed). 9// 10// DATA-DRIVEN (CLAUDE.md #11): the per-device control widgets are NOT 11// hardcoded -- the page asks nx_iot_capability what each device class can 12// do (cap flags) and renders exactly the right controls, and prints the 13// capability JSON the /iot front-end would consume. Change a device's 14// class and its controls change, with zero edits here. 15// 16// HONEST SCOPE this iteration (feedback-show-raw-output-no-overclaim): 17// - READ-ONLY DASHBOARD, safe to be public: it surfaces the hub design, 18// the data-driven control layout (demo devices), the auto-pair + 19// self-heal state machines, and the MEASURED-exceed numbers. 20// - The control widgets are the live UI surface but DO NOT actuate yet; 21// actuation is wired in the next pass behind operator-only OPAQUE auth 22// (a control panel must not be public -- the law in 23// feedback-every-capability-needs-iterating-ui). We iterate from here. 24// 25// genealogy_id: NISHI_IOT_HUB_ROADMAP.md + project-iot-hub-multi-vendor-kickoff-2026-05-16 26// license_tier: ORIGINAL 27 28import "nx_syscalls.nx" 29import "nx_iot_capability.nx" 30 31const IOT_OUT: *u8 = "web_assets/iot.html" 32const IOT_CAP_BYTES: i64 = 262144 33 34// append a NUL-terminated string into dst at off; returns new off. 35func iot_cat(dst: *u8, off: i64, s: *u8) -> i64 { 36 var o: i64 = off 37 var i: i64 = 0 38 while s[i] != 0 as u8 { dst[o] = s[i]; o = o + 1; i = i + 1 } 39 return o 40} 41 42// append a signed decimal into dst at off; returns new off. 43func iot_catn(dst: *u8, off: i64, v: i64) -> i64 { 44 var o: i64 = off 45 var n: i64 = v 46 if n < 0 { dst[o] = 45 as u8; o = o + 1; n = 0 - n } 47 let tmp: *u8 = sys_mmap(32) 48 var k: i64 = 0 49 if n == 0 { tmp[0] = 48 as u8; k = 1 } 50 while n > 0 { tmp[k] = (48 + (n % 10)) as u8; n = n / 10; k = k + 1 } 51 var j: i64 = k - 1 52 while j >= 0 { dst[o] = tmp[j]; o = o + 1; j = j - 1 } 53 return o 54} 55 56// Render the control widgets a device advertises -- driven entirely by the 57// capability flags the organ computes, never hardcoded. 58func iot_widgets(h: *u8, off: i64, caps: i64) -> i64 { 59 var w: i64 = off 60 if nx_iot_cap_has(caps, NX_IOT_CAP_ONOFF) == 1 { 61 w = iot_cat(h, w, "<button class=\"w tog\" disabled>Toggle</button>" as *u8) 62 } 63 if nx_iot_cap_has(caps, NX_IOT_CAP_BRIGHTNESS) == 1 { 64 w = iot_cat(h, w, "<label class=\"w\">Brightness<input type=\"range\" min=\"0\" max=\"100\" value=\"70\" disabled></label>" as *u8) 65 } 66 if nx_iot_cap_has(caps, NX_IOT_CAP_COLOR) == 1 { 67 w = iot_cat(h, w, "<div class=\"w sw\"><i style=\"background:#f85149\"></i><i style=\"background:#3fb950\"></i><i style=\"background:#4cc2ff\"></i><i style=\"background:#d2a8ff\"></i></div>" as *u8) 68 } 69 if nx_iot_cap_has(caps, NX_IOT_CAP_COLOR_TEMP) == 1 { 70 w = iot_cat(h, w, "<label class=\"w\">Warmth<input type=\"range\" min=\"" as *u8) 71 w = iot_catn(h, w, NX_IOT_COLOR_TEMP_MIN) 72 w = iot_cat(h, w, "\" max=\"" as *u8) 73 w = iot_catn(h, w, NX_IOT_COLOR_TEMP_MAX) 74 w = iot_cat(h, w, "\" value=\"4000\" disabled></label>" as *u8) 75 } 76 if nx_iot_cap_has(caps, NX_IOT_CAP_SCENE) == 1 { 77 w = iot_cat(h, w, "<select class=\"w\" disabled><option>Scene&hellip;</option></select>" as *u8) 78 } 79 if nx_iot_cap_has(caps, NX_IOT_CAP_ENERGY) == 1 { 80 w = iot_cat(h, w, "<span class=\"w en\">&#9889; energy</span>" as *u8) 81 } 82 return w 83} 84 85// One device card: header + the data-driven widget row + the capability 86// descriptor the organ emits (the proof the UI is model-driven). 87func iot_card(h: *u8, off: i64, name: *u8, room: *u8, vlabel: *u8, 88 dev_class: i64) -> i64 { 89 var w: i64 = off 90 let caps: i64 = nx_iot_cap_for_class(0, dev_class) 91 w = iot_cat(h, w, "<div class=\"card\"><div class=\"cn\">" as *u8) 92 w = iot_cat(h, w, name) 93 w = iot_cat(h, w, "</div><div class=\"cr\">" as *u8) 94 w = iot_cat(h, w, room) 95 w = iot_cat(h, w, " &middot; " as *u8) 96 w = iot_cat(h, w, vlabel) 97 w = iot_cat(h, w, "</div><div class=\"row\">" as *u8) 98 w = iot_widgets(h, w, caps) 99 w = iot_cat(h, w, "</div><div class=\"caps\">caps " as *u8) 100 let cj: *u8 = sys_mmap(128) 101 let cl: i64 = nx_iot_cap_emit_json(caps, cj, 128) 102 var i: i64 = 0 103 while i < cl { h[w] = cj[i]; w = w + 1; i = i + 1 } 104 w = iot_cat(h, w, "</div></div>\n" as *u8) 105 return w 106} 107 108func main() -> i64 { 109 let now: i64 = sys_now_realtime_sec() 110 let h: *u8 = sys_mmap(IOT_CAP_BYTES) 111 var w: i64 = 0 112 113 w = iot_cat(h, w, "<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\">\n" as *u8) 114 w = iot_cat(h, w, "<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n" as *u8) 115 w = iot_cat(h, w, "<title>Nishi IoT Hub &mdash; Control Panel</title>\n" as *u8) 116 w = iot_cat(h, w, "<style>\n" as *u8) 117 w = iot_cat(h, w, ":root{--bg:#0b0f14;--panel:#131b24;--ink:#e6edf3;--mut:#8aa0b4;--acc:#4cc2ff;--good:#3fb950;--bad:#f85149;--line:#243140}\n" as *u8) 118 w = iot_cat(h, w, "*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font:15px/1.55 system-ui,Segoe UI,Roboto,sans-serif}\n" as *u8) 119 w = iot_cat(h, w, "header{padding:30px 20px 16px;max-width:960px;margin:0 auto}h1{margin:0;font-size:29px}\n" as *u8) 120 w = iot_cat(h, w, ".sub{color:var(--mut);margin:.5em 0 0}.stamp{color:var(--mut);font-size:12.5px;margin:.4em 0 0}\n" as *u8) 121 w = iot_cat(h, w, "main{max-width:960px;margin:0 auto;padding:0 20px 60px}section{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:16px 18px;margin:16px 0}\n" as *u8) 122 w = iot_cat(h, w, "h2{margin:.1em 0 .5em;font-size:18px}code{color:var(--acc);font-size:.92em}\n" as *u8) 123 w = iot_cat(h, w, ".grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px}\n" as *u8) 124 w = iot_cat(h, w, ".card{background:#0e1620;border:1px solid var(--line);border-radius:10px;padding:12px 13px}\n" as *u8) 125 w = iot_cat(h, w, ".cn{font-weight:600;font-size:15px}.cr{color:var(--mut);font-size:12.5px;margin:.1em 0 .6em}\n" as *u8) 126 w = iot_cat(h, w, ".row{display:flex;flex-wrap:wrap;gap:7px;align-items:center}.w{font-size:12.5px}\n" as *u8) 127 w = iot_cat(h, w, ".tog{background:var(--acc);color:#04121c;border:0;border-radius:6px;padding:5px 11px;font-weight:600}\n" as *u8) 128 w = iot_cat(h, w, "input[type=range]{vertical-align:middle;width:96px}label.w{display:inline-flex;flex-direction:column;color:var(--mut);gap:2px}\n" as *u8) 129 w = iot_cat(h, w, ".sw i{display:inline-block;width:15px;height:15px;border-radius:50%;margin-right:3px;border:1px solid #0006}\n" as *u8) 130 w = iot_cat(h, w, ".en{color:var(--good)}select.w{background:#0e1620;color:var(--ink);border:1px solid var(--line);border-radius:6px;padding:3px}\n" as *u8) 131 w = iot_cat(h, w, ".caps{color:var(--mut);font-size:11px;margin-top:7px;font-family:ui-monospace,monospace}\n" as *u8) 132 w = iot_cat(h, w, "table{border-collapse:collapse;width:100%;margin:.4em 0}th,td{padding:6px 9px;text-align:left;border-bottom:1px solid var(--line);font-size:13.5px}\n" as *u8) 133 w = iot_cat(h, w, "th{color:var(--mut);font-weight:600;font-size:12px;text-transform:uppercase;letter-spacing:.4px}\n" as *u8) 134 w = iot_cat(h, w, ".pipe{display:flex;flex-wrap:wrap;gap:6px;align-items:center;color:var(--mut);font-size:13px}\n" as *u8) 135 w = iot_cat(h, w, ".st{background:#0e1620;border:1px solid var(--line);border-radius:20px;padding:3px 11px}.ar{color:var(--acc)}\n" as *u8) 136 w = iot_cat(h, w, ".good{color:var(--good)}.warn{color:#d29922}.note{background:#10261a;border:1px solid #1c3b28;border-radius:9px;padding:10px 12px;color:#9fd2af;font-size:13px}\n" as *u8) 137 w = iot_cat(h, w, "footer{max-width:960px;margin:0 auto;padding:8px 20px 40px;color:var(--mut);font-size:12.5px}\n" as *u8) 138 w = iot_cat(h, w, "</style></head><body>\n" as *u8) 139 140 w = iot_cat(h, w, "<header><h1>Nishi IoT Hub &mdash; Control Panel</h1>\n" as *u8) 141 w = iot_cat(h, w, "<p class=\"sub\">A sovereign, LAN-first home hub built 100&percnt; on the Nishi stack &mdash; no cloud round-trip, no Python, no hand-written JS/HTML. Replaces the Smart Life / Kasa apps to kill the blinking-lights / re-pair / duplicate failures. This page was emitted by the organ <code>nx_iot_page</code>.</p>\n" as *u8) 142 w = iot_cat(h, w, "<p class=\"stamp\">Generated LIVE at epoch " as *u8) 143 w = iot_catn(h, w, now) 144 w = iot_cat(h, w, " &mdash; device controls below are rendered from the capability model (<code>nx_iot_capability</code>) at render time, not typed in.</p></header>\n" as *u8) 145 146 w = iot_cat(h, w, "<main>\n" as *u8) 147 148 // ===== devices: data-driven control grid ===== 149 w = iot_cat(h, w, "<section><h2>Devices</h2>\n" as *u8) 150 w = iot_cat(h, w, "<p class=\"stamp\">Each card's controls are chosen by the hub from the device's capability flags &mdash; a switch gets a toggle, a colour bulb gets brightness + colour + warmth + scenes. (Demo fixtures; live actuation arrives behind operator-only sign-in next.)</p>\n" as *u8) 151 w = iot_cat(h, w, "<div class=\"grid\">\n" as *u8) 152 w = iot_card(h, w, "Kitchen Switch" as *u8, "Kitchen" as *u8, "Kasa HS210" as *u8, NX_IOT_CLASS_SWITCH) 153 w = iot_card(h, w, "Living Room Lamp" as *u8, "Living Room" as *u8, "Kasa KL130 colour" as *u8, NX_IOT_CLASS_BULB_COLOR) 154 w = iot_card(h, w, "Hallway Dimmer" as *u8, "Hallway" as *u8, "dimmer" as *u8, NX_IOT_CLASS_DIMMER) 155 w = iot_card(h, w, "Patio Plug" as *u8, "Patio" as *u8, "energy plug" as *u8, NX_IOT_CLASS_PLUG_ENERGY) 156 w = iot_card(h, w, "Bedroom Bulb" as *u8, "Bedroom" as *u8, "tunable white" as *u8, NX_IOT_CLASS_BULB_TUNABLE) 157 w = iot_cat(h, w, "</div></section>\n" as *u8) 158 159 // ===== auto-pair flow (provision_softap) ===== 160 w = iot_cat(h, w, "<section><h2>Auto-pair &mdash; no more manual re-pairing</h2>\n" as *u8) 161 w = iot_cat(h, w, "<p>When a fixture drops to its open pairing mode, the hub re-onboards it on its own &mdash; the state machine <code>nx_iot_provision_softap</code> drives it end to end:</p>\n" as *u8) 162 w = iot_cat(h, w, "<div class=\"pipe\"><span class=\"st\">scan</span><span class=\"ar\">&rarr;</span><span class=\"st\">join AP</span><span class=\"ar\">&rarr;</span><span class=\"st\">push creds</span><span class=\"ar\">&rarr;</span><span class=\"st\">rejoin LAN</span><span class=\"ar\">&rarr;</span><span class=\"st\">verify</span><span class=\"ar\">&rarr;</span><span class=\"st\">anchor</span><span class=\"ar\">&rarr;</span><span class=\"st good\">done</span></div>\n" as *u8) 163 w = iot_cat(h, w, "<p class=\"note\">Never-brick by construction: the hub only ever pushes a reversible Wi-Fi credential &mdash; it has no firmware-write action at all, and the safety classifier defaults any un-reviewed action to blocked. A rejected push just leaves the device safe in pairing mode.</p></section>\n" as *u8) 164 165 // ===== self-heal (watchdog) ===== 166 w = iot_cat(h, w, "<section><h2>Self-healing &mdash; the blink loop, closed</h2>\n" as *u8) 167 w = iot_cat(h, w, "<p>The watchdog (<code>nx_iot_watchdog</code>) watches each device and, after a bounded run of missed heartbeats, declares it down; if it reverts to pairing it auto-re-provisions and re-anchors &mdash; same identity, no duplicate, no human:</p>\n" as *u8) 168 w = iot_cat(h, w, "<div class=\"pipe\"><span class=\"st good\">healthy</span><span class=\"ar\">&rarr;</span><span class=\"st\">missed</span><span class=\"ar\">&rarr;</span><span class=\"st warn\">unreachable</span><span class=\"ar\">&rarr;</span><span class=\"st\">recovering</span><span class=\"ar\">&rarr;</span><span class=\"st good\">healthy</span></div></section>\n" as *u8) 169 170 // ===== measured exceed vs the incumbent app ===== 171 w = iot_cat(h, w, "<section><h2>Where it beats the app (measured, not waved)</h2>\n" as *u8) 172 w = iot_cat(h, w, "<table><tr><th>Axis</th><th>Smart Life / Kasa app</th><th>Nishi hub</th></tr>\n" as *u8) 173 w = iot_cat(h, w, "<tr><td>Duplicate device rows on re-pair</td><td class=\"warn\">one per re-pair</td><td class=\"good\">0 &mdash; anchored by hardware id</td></tr>\n" as *u8) 174 w = iot_cat(h, w, "<tr><td>Manual steps to re-pair a dropped light</td><td class=\"warn\">several, by hand</td><td class=\"good\">0 &mdash; auto re-provision</td></tr>\n" as *u8) 175 w = iot_cat(h, w, "<tr><td>Works with the internet down</td><td class=\"warn\">no (cloud)</td><td class=\"good\">yes &mdash; LAN-only</td></tr>\n" as *u8) 176 w = iot_cat(h, w, "<tr><td>Brick risk on provisioning</td><td class=\"warn\">trust the vendor</td><td class=\"good\">none by construction</td></tr>\n" as *u8) 177 w = iot_cat(h, w, "</table>\n" as *u8) 178 w = iot_cat(h, w, "<p class=\"stamp\">Each row is a property a sovereign gate proves (anchor duplicates_prevented, watchdog recover_count, provision never-brick alphabet) &mdash; not a marketing claim.</p></section>\n" as *u8) 179 180 // ===== build status ===== 181 w = iot_cat(h, w, "<section><h2>Backbone &mdash; from the hardware rung up</h2>\n" as *u8) 182 w = iot_cat(h, w, "<table><tr><th>Organ</th><th>Role</th><th>Gate</th></tr>\n" as *u8) 183 w = iot_cat(h, w, "<tr><td><code>nx_iot_anchor</code></td><td>WHO &mdash; stable identity, no duplicates</td><td class=\"good\">&#10003; green</td></tr>\n" as *u8) 184 w = iot_cat(h, w, "<tr><td><code>nx_iot_classify</code></td><td>PROBE &mdash; vendor router</td><td class=\"good\">&#10003; green</td></tr>\n" as *u8) 185 w = iot_cat(h, w, "<tr><td><code>nx_iot_capability</code></td><td>WHAT &mdash; which controls to show</td><td class=\"good\">&#10003; green</td></tr>\n" as *u8) 186 w = iot_cat(h, w, "<tr><td><code>nx_iot_local_kasa</code></td><td>HOW &mdash; Kasa LAN protocol</td><td class=\"good\">&#10003; green</td></tr>\n" as *u8) 187 w = iot_cat(h, w, "<tr><td><code>nx_iot_provision_softap</code></td><td>auto-pair state machine</td><td class=\"good\">&#10003; green</td></tr>\n" as *u8) 188 w = iot_cat(h, w, "<tr><td><code>nx_iot_watchdog</code></td><td>self-heal / blink loop</td><td class=\"good\">&#10003; green</td></tr>\n" as *u8) 189 w = iot_cat(h, w, "<tr><td><code>nx_iot_page</code></td><td>this control panel</td><td class=\"good\">&#10003; live</td></tr>\n" as *u8) 190 w = iot_cat(h, w, "</table>\n" as *u8) 191 w = iot_cat(h, w, "<p class=\"stamp\">Honest scope: the controls are a live UI we iterate on; they don't actuate yet (operator-only sign-in + the Windows-native radio runner are the next rungs). The logic above is gate-proven offline; live LAN control follows.</p></section>\n" as *u8) 192 193 w = iot_cat(h, w, "</main>\n" as *u8) 194 w = iot_cat(h, w, "<footer>Sovereign Nishi stack &middot; emitted by <code>nx_iot_page</code> &middot; controls model: <code>nx_iot_capability</code> &middot; published via <code>nx_aw_push</code> &middot; nishifamily.com/iot</footer>\n" as *u8) 195 w = iot_cat(h, w, "</body></html>\n" as *u8) 196 197 let fd: i64 = sys_openat_wr(IOT_OUT, 0x1a4) 198 if fd < 0 { sys_write(2, "FATAL: cannot open web_assets/iot.html\n" as *u8, 39); sys_exit(2); return 2 } 199 sys_write(fd, h, w) 200 sys_close(fd) 201 sys_write(1, "[nx_iot_page] wrote iot.html bytes=" as *u8, 35) 202 let nb: *u8 = sys_mmap(28) 203 var k2: i64 = 0 204 var m2: i64 = w 205 if m2 == 0 { nb[0] = 48 as u8; k2 = 1 } 206 while m2 > 0 { nb[k2] = (48 + (m2 % 10)) as u8; m2 = m2 / 10; k2 = k2 + 1 } 207 var qd: i64 = k2 - 1 208 while qd >= 0 { let o1: *u8 = sys_mmap(1); o1[0] = nb[qd]; sys_write(1, o1, 1); qd = qd - 1 } 209 sys_write(1, "\n" as *u8, 1) 210 sys_exit(0) 211 return 0 212}