code wiki / (root) / nx_ac_monitor_page.nx

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 &middot; no cloud &middot; no hub required &middot; 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 &mdash; 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) &mdash; 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) &mdash; 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) &mdash; 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 &mdash; 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) &mdash; 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 &mdash; 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 &mdash; 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 &mdash; 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 &mdash; 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 &mdash; independent sensors disagree" as *u8) } 159 if res.trust_verdict == NX_ACV_IMPLAUSIBLE_PHYSICS { w = mp_cat(h, w, "IMPOSSIBLE &mdash; 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 &ge; 0, COP &le; Carnot, psychrometric feasibility) and cross-sensor agreement before it can raise an alert &mdash; 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}