nx_ac_monitor_page.nx source
↩ module page · 200 lines · 10545 B
1// nx_ac_monitor_page.nx -- emit the HUBLESS, browser-first AC monitoring
2// dashboard: ZERO-JS, responsive (mobile + desktop), server-rendered.
3//
4// Operator: "hubless and work from the browser on mobile and non-mobile." The
5// answer is the same no-JS doctrine the IoT control page proved: a self-
6// contained HTML+CSS page (viewport meta + auto-fit CSS grid = phone and
7// desktop, nothing to install, renders identically in the Nishi browser and
8// Chrome/Firefox/Edge/Safari). HUBLESS = the page + the monitoring brain
9// (nx_ac_monitor) are a portable organ that can run on the sensor node, the
10// NAS, or be served from anywhere on the LAN -- no mandatory central hub, no
11// cloud at runtime. The TRUST verdict (liar-killer) is shown on every card, so
12// the reading you see is one you can trust -- or is clearly flagged UNVERIFIED.
13//
14// Renders a MonitorResult (nx_ac_monitor) per unit into a caller buffer.
15// NEVER-BRICK #26: read-only display; emits no device command, no firmware.
16// license_tier: ORIGINAL
17// genealogy_id: project-iot-discovery-pairing-revival-2026-06-22 (nx_iot_control_page)
18// + project-hvac-efficiency-sclass-2026-06-23 (R5 live UI)
19
20import "nx_syscalls.nx"
21import "nx_ac_monitor.nx"
22
23const MP_CAP: i64 = 262144
24
25func mp_cat(dst: *u8, off: i64, s: *u8) -> i64 {
26 var o: i64 = off
27 var i: i64 = 0
28 while s[i] != 0 as u8 { dst[o] = s[i]; o = o + 1; i = i + 1 }
29 return o
30}
31func mp_catn(dst: *u8, off: i64, v: i64) -> i64 {
32 var o: i64 = off
33 var n: i64 = v
34 if n < 0 { dst[o] = 45 as u8; o = o + 1; n = 0 - n }
35 let tmp: *u8 = sys_mmap(32)
36 var k: i64 = 0
37 if n == 0 { tmp[0] = 48 as u8; k = 1 }
38 while n > 0 { tmp[k] = (48 + (n % 10)) as u8; n = n / 10; k = k + 1 }
39 var j: i64 = k - 1
40 while j >= 0 { dst[o] = tmp[j]; o = o + 1; j = j - 1 }
41 return o
42}
43// COP x100 -> "3.58" (or "n/a" if not computable)
44func mp_cat_cop(dst: *u8, off: i64, cop_x100: i64) -> i64 {
45 var o: i64 = off
46 if cop_x100 < 0 { return mp_cat(dst, o, "n/a" as *u8) }
47 o = mp_catn(dst, o, cop_x100 / 100)
48 dst[o] = 46 as u8
49 o = o + 1
50 let frac: i64 = cop_x100 % 100
51 if frac < 10 { dst[o] = 48 as u8; o = o + 1 }
52 o = mp_catn(dst, o, frac)
53 return o
54}
55// substring search (gate assertions)
56func mp_find(buf: *u8, len: i64, pat: *u8, pat_len: i64) -> i64 {
57 if pat_len > len { return 0 - 1 }
58 var i: i64 = 0
59 let end: i64 = len - pat_len + 1
60 var hit: i64 = 0 - 1
61 while i < end {
62 if hit == (0 - 1) {
63 var j: i64 = 0
64 var ok: i64 = 1
65 while j < pat_len {
66 if (buf[i + j] as i64) != (pat[j] as i64) { ok = 0; j = pat_len } else { j = j + 1 }
67 }
68 if ok == 1 { hit = i }
69 }
70 i = i + 1
71 }
72 return hit
73}
74
75func mp_head(h: *u8, off: i64) -> i64 {
76 var w: i64 = off
77 w = mp_cat(h, w, "<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\">" as *u8)
78 w = mp_cat(h, w, "<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">" as *u8)
79 w = mp_cat(h, w, "<title>AC Health Monitor</title><style>" as *u8)
80 w = mp_cat(h, w, ":root{--bg:#0d1117;--card:#161b22;--fg:#e6edf3;--mut:#8b949e;--ln:#30363d;--ok:#3fb950;--info:#4cc2ff;--warn:#d29922;--crit:#f85149;--unv:#8b949e}" as *u8)
81 w = mp_cat(h, w, "@media(prefers-color-scheme:light){:root{--bg:#f6f8fa;--card:#fff;--fg:#1f2328;--mut:#59636e;--ln:#d0d7de}}" as *u8)
82 w = mp_cat(h, w, "*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--fg);font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif}" as *u8)
83 w = mp_cat(h, w, "header{padding:18px 20px;border-bottom:1px solid var(--ln)}h1{margin:0;font-size:20px}.sub{color:var(--mut);font-size:13px;margin-top:4px}" as *u8)
84 w = mp_cat(h, w, ".wrap{max-width:920px;margin:0 auto;padding:16px;display:grid;gap:16px;grid-template-columns:repeat(auto-fit,minmax(270px,1fr))}" as *u8)
85 w = mp_cat(h, w, ".card{background:var(--card);border:1px solid var(--ln);border-radius:12px;padding:16px}.nm{font-size:17px;font-weight:600}" as *u8)
86 w = mp_cat(h, w, ".badge{display:inline-block;padding:4px 10px;border-radius:999px;font-size:12px;font-weight:700;margin-top:8px}" as *u8)
87 w = mp_cat(h, w, ".ok{background:rgba(63,185,80,.16);color:var(--ok)}.info{background:rgba(76,194,255,.16);color:var(--info)}" as *u8)
88 w = mp_cat(h, w, ".warn{background:rgba(210,153,34,.16);color:var(--warn)}.crit{background:rgba(248,81,73,.16);color:var(--crit)}.unv{background:rgba(139,148,158,.16);color:var(--unv)}" as *u8)
89 w = mp_cat(h, w, ".metrics{display:flex;gap:18px;margin-top:14px;flex-wrap:wrap}.m{font-size:12px;color:var(--mut)}.m b{display:block;font-size:20px;color:var(--fg);font-weight:600}" as *u8)
90 w = mp_cat(h, w, ".msg{margin-top:12px;font-size:14px;line-height:1.45}.trust{margin-top:10px;font-size:12px;color:var(--mut)}" as *u8)
91 w = mp_cat(h, w, "footer{max-width:920px;margin:0 auto;padding:8px 20px 24px;color:var(--mut);font-size:12px;line-height:1.5}</style></head><body>" as *u8)
92 w = mp_cat(h, w, "<header><h1>AC Health Monitor</h1><div class=\"sub\">Local · no cloud · no hub required · every reading physics-verified</div></header><div class=\"wrap\">" as *u8)
93 return w
94}
95
96// alert badge (class + label) for one card
97func mp_badge(h: *u8, off: i64, lvl: i64) -> i64 {
98 var w: i64 = off
99 w = mp_cat(h, w, "<span class=\"badge " as *u8)
100 if lvl == NX_ALERT_OK { w = mp_cat(h, w, "ok\">HEALTHY" as *u8) }
101 if lvl == NX_ALERT_INFO { w = mp_cat(h, w, "info\">NOTICE" as *u8) }
102 if lvl == NX_ALERT_WARN { w = mp_cat(h, w, "warn\">SERVICE SOON" as *u8) }
103 if lvl == NX_ALERT_CRITICAL { w = mp_cat(h, w, "crit\">CRITICAL" as *u8) }
104 if lvl == NX_ALERT_UNTRUSTED { w = mp_cat(h, w, "unv\">UNVERIFIED" as *u8) }
105 w = mp_cat(h, w, "</span>" as *u8)
106 return w
107}
108
109// plain-language, actionable message from the fault + trust
110func mp_message(h: *u8, off: i64, res: *MonitorResult) -> i64 {
111 var w: i64 = off
112 if res.alert_level == NX_ALERT_UNTRUSTED {
113 w = mp_cat(h, w, "Sensor reading failed physics/consistency checks — not diagnosing until the sensors agree. The liar-killer is protecting you from a false alarm." as *u8)
114 return w
115 }
116 if res.fdd_verdict == NX_FDD_UNDERCHARGE {
117 w = mp_cat(h, w, "Low refrigerant charge (high superheat, low subcooling) — likely a slow leak. Book service before it fails on a hot day." as *u8)
118 return w
119 }
120 if res.fdd_verdict == NX_FDD_OVERCHARGE {
121 w = mp_cat(h, w, "Refrigerant overcharge (high subcooling) — have the charge corrected to restore efficiency." as *u8)
122 return w
123 }
124 if res.fdd_verdict == NX_FDD_LIQUID_RESTRICTION {
125 w = mp_cat(h, w, "Liquid-line restriction (high superheat AND subcooling) — check the filter-drier / metering device." as *u8)
126 return w
127 }
128 if res.fdd_verdict == NX_FDD_FOULED_CONDENSER {
129 w = mp_cat(h, w, "Outdoor coil is fouled — condensing temperature is running high. Rinse the coil to cut energy use." as *u8)
130 return w
131 }
132 if res.fdd_verdict == NX_FDD_LOW_AIRFLOW {
133 w = mp_cat(h, w, "Low airflow (high evaporator split) — replace the filter and check ducts / blower." as *u8)
134 return w
135 }
136 if res.fdd_verdict == NX_FDD_FROZEN_COIL {
137 w = mp_cat(h, w, "Evaporator coil is freezing — switch to FAN-ONLY to thaw, then check airflow and charge." as *u8)
138 return w
139 }
140 if res.fdd_verdict == NX_FDD_SHORT_CYCLING {
141 w = mp_cat(h, w, "Short-cycling — the system starts and stops too often (oversizing / controls)." as *u8)
142 return w
143 }
144 if res.alert_level == NX_ALERT_OK {
145 w = mp_cat(h, w, "Running normally — capacity, efficiency and refrigerant signature all within range." as *u8)
146 return w
147 }
148 w = mp_cat(h, w, "Efficiency has drifted from its baseline — the unit is using more energy per degree of cooling than it used to." as *u8)
149 return w
150}
151
152// trust line (liar-killer verdict), shown on every card
153func mp_trust(h: *u8, off: i64, res: *MonitorResult) -> i64 {
154 var w: i64 = off
155 w = mp_cat(h, w, "<div class=\"trust\">Reading: " as *u8)
156 if res.trust_verdict == NX_ACV_CORROBORATED { w = mp_cat(h, w, "corroborated by 2 independent sensors" as *u8) }
157 if res.trust_verdict == NX_ACV_PLAUSIBLE { w = mp_cat(h, w, "plausible (single source)" as *u8) }
158 if res.trust_verdict == NX_ACV_CONFLICTED { w = mp_cat(h, w, "CONFLICTED — independent sensors disagree" as *u8) }
159 if res.trust_verdict == NX_ACV_IMPLAUSIBLE_PHYSICS { w = mp_cat(h, w, "IMPOSSIBLE — violates physics" as *u8) }
160 if res.trust_verdict == NX_ACV_INSUFFICIENT_DATA { w = mp_cat(h, w, "insufficient data" as *u8) }
161 if res.trust_verdict == NX_ACV_BAD_ARG { w = mp_cat(h, w, "invalid mode" as *u8) }
162 w = mp_cat(h, w, "</div>" as *u8)
163 return w
164}
165
166// one unit card
167func mp_card(h: *u8, off: i64, name: *u8, res: *MonitorResult) -> i64 {
168 var w: i64 = off
169 w = mp_cat(h, w, "<div class=\"card\"><div class=\"nm\">" as *u8)
170 w = mp_cat(h, w, name)
171 w = mp_cat(h, w, "</div>" as *u8)
172 w = mp_badge(h, w, res.alert_level)
173 w = mp_cat(h, w, "<div class=\"metrics\"><div class=\"m\">COP<b>" as *u8)
174 w = mp_cat_cop(h, w, res.cop_x100)
175 w = mp_cat(h, w, "</b></div><div class=\"m\">Efficiency drift<b>" as *u8)
176 w = mp_catn(h, w, res.efficiency_drift_pm / 10)
177 w = mp_cat(h, w, "%</b></div><div class=\"m\">Cooling<b>" as *u8)
178 if res.q_cool_w < 0 { w = mp_cat(h, w, "n/a" as *u8) } else { w = mp_catn(h, w, res.q_cool_w) }
179 w = mp_cat(h, w, " W</b></div></div><div class=\"msg\">" as *u8)
180 w = mp_message(h, w, res)
181 w = mp_cat(h, w, "</div>" as *u8)
182 w = mp_trust(h, w, res)
183 w = mp_cat(h, w, "</div>\n" as *u8)
184 return w
185}
186
187func mp_foot(h: *u8, off: i64) -> i64 {
188 var w: i64 = off
189 w = mp_cat(h, w, "</div><footer>Sovereign AC Health Monitor. Every reading is checked against physics (superheat ≥ 0, COP ≤ Carnot, psychrometric feasibility) and cross-sensor agreement before it can raise an alert — no cloud, no hub, no subscription.</footer></body></html>" as *u8)
190 return w
191}
192
193// full single-unit page (demo / one unit)
194func mp_page(h: *u8, name: *u8, res: *MonitorResult) -> i64 {
195 var w: i64 = 0
196 w = mp_head(h, w)
197 w = mp_card(h, w, name, res)
198 w = mp_foot(h, w)
199 return w
200}