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…</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\">⚡ 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, " · " 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 — 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 — Control Panel</h1>\n" as *u8)
141 w = iot_cat(h, w, "<p class=\"sub\">A sovereign, LAN-first home hub built 100% on the Nishi stack — 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, " — 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 — 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 — 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 — 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\">→</span><span class=\"st\">join AP</span><span class=\"ar\">→</span><span class=\"st\">push creds</span><span class=\"ar\">→</span><span class=\"st\">rejoin LAN</span><span class=\"ar\">→</span><span class=\"st\">verify</span><span class=\"ar\">→</span><span class=\"st\">anchor</span><span class=\"ar\">→</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 — 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 — 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 — 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\">→</span><span class=\"st\">missed</span><span class=\"ar\">→</span><span class=\"st warn\">unreachable</span><span class=\"ar\">→</span><span class=\"st\">recovering</span><span class=\"ar\">→</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 — 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 — 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 — 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) — not a marketing claim.</p></section>\n" as *u8)
179
180 // ===== build status =====
181 w = iot_cat(h, w, "<section><h2>Backbone — 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 — stable identity, no duplicates</td><td class=\"good\">✓ green</td></tr>\n" as *u8)
184 w = iot_cat(h, w, "<tr><td><code>nx_iot_classify</code></td><td>PROBE — vendor router</td><td class=\"good\">✓ green</td></tr>\n" as *u8)
185 w = iot_cat(h, w, "<tr><td><code>nx_iot_capability</code></td><td>WHAT — which controls to show</td><td class=\"good\">✓ green</td></tr>\n" as *u8)
186 w = iot_cat(h, w, "<tr><td><code>nx_iot_local_kasa</code></td><td>HOW — Kasa LAN protocol</td><td class=\"good\">✓ 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\">✓ 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\">✓ 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\">✓ 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 · emitted by <code>nx_iot_page</code> · controls model: <code>nx_iot_capability</code> · published via <code>nx_aw_push</code> · 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}