code wiki / (root) / nx_link_sentinel.nx

nx_link_sentinel.nx source

↩ module page · 379 lines · 23989 B

1// nx_link_sentinel.nx -- LINK LIVENESS MONITOR for the nav. Probes every surface in knowledge/site/surfaces.reg 2// over the sovereign TLS stack (nx_https_fetch_follow + Mozilla trust store -- the public-edge path a real browser 3// takes), classifies each LIVE/GATED/REDIRECT/DEAD, and keeps a HEALTH LEDGER over time (last-live epoch + fail 4// streak) so no nav link is ever a SILENT orphan. A link stays ACTIVE while reachable; a dead public link becomes 5// DEGRADED (flagged, still shown) then an ARCHIVE candidate after N consecutive fails -- removal stays deliberate 6// (surfaces.reg status=archived). Modes: gate (console+update ledger) | json | html (dashboard). Ledger file: 7// knowledge/site/link_health.tsv (route \t last_status \t class \t last_live_unix \t fail_streak \t checked_unix). 8// nx_cc traps honored (no '#'/'!' in literals; classes not ids; w2 ' -> "). license_tier: ORIGINAL expect_exit:0 9import "nx_syscalls.nx" 10import "nx_connect.nx" // bounded connect: a raw sys_connect hangs ~127s on a black-holed host 11import "nx_csprng.nx" 12import "nx_x509_trust_store.nx" 13import "nx_trust_store_load_from_certdata.nx" 14import "nx_https_fetch_follow.nx" 15const ARCHIVE_MAGIC_65535: i64 = 65535 16const ARCHIVE_MAGIC_4194304: i64 = 4194304 17const ARCHIVE_MAGIC_65536: i64 = 65536 18const ARCHIVE_MAGIC_1048576: i64 = 1048576 19const ARCHIVE_MAGIC_8388608: i64 = 8388608 20const ARCHIVE_MAGIC_1024: i64 = 1024 21 22const HOST: *u8 = "https://nishifamily.com" as *u8 23const ARCHIVE_THRESHOLD: i64 = 6 // consecutive fails before a dead link becomes an archive candidate 24 25func w(fd: i64, s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(fd, s, n); return 0 } 26func wc(fd: i64, code: i64) -> i64 { let t: *u8 = sys_mmap(2); t[0] = code as u8; sys_write(fd, t, 1); return 0 } 27func wn(fd: i64, v: i64) -> i64 { 28 var m: i64 = v; if m < 0 { w(fd, "-" as *u8); m = 0 - m } 29 let t: *u8 = sys_mmap(24); var k: i64 = 0; if m == 0 { t[0] = 48 as u8; k = 1 } 30 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 31 let o: *u8 = sys_mmap(24); var i: i64 = 0; while i < k { o[i] = t[k-1-i]; i = i + 1 } sys_write(fd, o, k); return 0 32} 33func lsent_streq(a: *u8, b: *u8) -> i64 { var i: i64 = 0; while a[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 } if b[i] != (0 as u8) { return 0 } return 1 } 34func c_read(path: *u8, buf: *u8, cap: i64) -> i64 { 35 let fd: i64 = sys_openat_rd(path); if fd < 0 { return 0 - 1 } 36 var tot: i64 = 0 37 while tot < cap { let r: i64 = sys_read(fd, (buf as i64 + tot) as *u8, cap - tot); if r <= 0 { break } tot = tot + r } 38 sys_close(fd); return tot 39} 40func scopy(dst: *u8, doff: i64, src: *u8) -> i64 { var i: i64 = 0; while src[i] != (0 as u8) { dst[doff+i] = src[i]; i = i + 1 } return doff + i } 41// encode integer v as decimal into buf at off; return new off 42func enc_int(buf: *u8, off: i64, v: i64) -> i64 { 43 var m: i64 = v; if m < 0 { buf[off] = 45 as u8; off = off + 1; m = 0 - m } 44 if m == 0 { buf[off] = 48 as u8; return off + 1 } 45 let t: *u8 = sys_mmap(24); var k: i64 = 0 46 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 47 var j: i64 = k - 1 48 while j >= 0 { buf[off] = t[j]; off = off + 1; j = j - 1 } 49 return off 50} 51func w2(fd: i64, s: *u8) -> i64 { var i: i64 = 0; while s[i] != (0 as u8) { if (s[i] as i64) == 39 { wc(fd, 34) } else { wc(fd, s[i] as i64) } i = i + 1 } return 0 } 52// extract |-delimited field idx from buf[ls,le) into out, trimming spaces 53func field(buf: *u8, ls: i64, le: i64, idx: i64, out: *u8) -> i64 { 54 var cur: i64 = 0; var s: i64 = ls; var i: i64 = ls 55 while i < le { if cur == idx { break } if buf[i] == (124 as u8) { cur = cur + 1; s = i + 1 } i = i + 1 } 56 if cur < idx { out[0] = 0 as u8; return 0 } 57 var e: i64 = s 58 while e < le { if buf[e] == (124 as u8) { break } e = e + 1 } 59 var a: i64 = s 60 while a < e { if buf[a] != (32 as u8) { break } a = a + 1 } 61 var b: i64 = e 62 while b > a { if buf[b-1] != (32 as u8) { break } b = b - 1 } 63 var o: i64 = 0 64 while a < b { out[o] = buf[a]; o = o + 1; a = a + 1 } 65 out[o] = 0 as u8 66 return o 67} 68// class: 0=DEAD 1=LIVE 2=GATED 3=REDIRECT. Scope-aware caller decides health. 69func classify(st: i64) -> i64 { 70 if st >= 200 { if st < 300 { return 1 } } 71 if st == 301 { return 3 } 72 if st == 302 { return 3 } 73 if st == 303 { return 3 } 74 if st == 307 { return 3 } 75 if st == 308 { return 3 } 76 if st == 401 { return 2 } 77 if st == 403 { return 2 } 78 return 0 79} 80func class_name(c: i64) -> *u8 { 81 if c == 1 { return "LIVE" as *u8 } 82 if c == 2 { return "GATED" as *u8 } 83 if c == 3 { return "REDIRECT" as *u8 } 84 return "DEAD" as *u8 85} 86// health: 1 = reachable/expected, 0 = broken. Gated scope treats GATED/LIVE/REDIRECT as healthy. 87func is_healthy(cls: i64, scope_gated: i64) -> i64 { 88 if cls == 1 { return 1 } 89 if cls == 3 { return 1 } 90 if cls == 2 { if scope_gated == 1 { return 1 } return 0 } 91 return 0 92} 93// parse a decimal integer from a null-terminated slice 94func lsent_atoi(s: *u8) -> i64 { var v: i64 = 0; var i: i64 = 0; while s[i] != (0 as u8) { let c: i64 = s[i] as i64; if c >= 48 { if c <= 57 { v = v * 10 + (c - 48) } } i = i + 1 } return v } 95// tab-field extract from ledger line [ls,le) idx -> out 96func tfield(buf: *u8, ls: i64, le: i64, idx: i64, out: *u8) -> i64 { 97 var cur: i64 = 0; var s: i64 = ls; var i: i64 = ls 98 while i < le { if cur == idx { break } if buf[i] == (9 as u8) { cur = cur + 1; s = i + 1 } i = i + 1 } 99 if cur < idx { out[0] = 0 as u8; return 0 } 100 var e: i64 = s 101 while e < le { if buf[e] == (9 as u8) { break } e = e + 1 } 102 var o: i64 = 0; while s < e { out[o] = buf[s]; o = o + 1; s = s + 1 } out[o] = 0 as u8; return o 103} 104// parse "a.b.c.d:port" -> big-endian packed ip + port. 1 ok / 0 bad. (nx_https_get_cli pattern) 105func sent_parse_ipport(s: *u8, ip_out: *i64, port_out: *i64) -> i64 { 106 var packed: i64 = 0; var val: i64 = 0; var nocts: i64 = 0; var port: i64 = 0; var indots: i64 = 1; var i: i64 = 0 107 while s[i] != (0 as u8) { 108 let c: i64 = s[i] as i64 109 if indots == 1 { 110 if c == 46 { packed = (packed << 8) | (val & 0xff); nocts = nocts + 1; val = 0 } 111 else { if c == 58 { packed = (packed << 8) | (val & 0xff); nocts = nocts + 1; val = 0; indots = 0 } 112 else { if c < 48 { return 0 } if c > 57 { return 0 } val = val * 10 + (c - 48) } } 113 } else { 114 if c < 48 { return 0 } 115 if c > 57 { return 0 } 116 port = port * 10 + (c - 48) 117 } 118 i = i + 1 119 } 120 if indots == 1 { return 0 } 121 if nocts != 4 { return 0 } 122 if port <= 0 { return 0 } 123 if port > ARCHIVE_MAGIC_65535 { return 0 } 124 ip_out[0] = packed; port_out[0] = port 125 return 1 126} 127// CONNECT-OVERRIDE fetch (curl --connect-to): TCP+TLS to ip:port while SNI + Host + cert-name stay the URL's 128// host. THE NAS-side probe path -- from the NAS, DNS-resolving our own domain lands on the DSM-nginx :443 129// coin-flip; the override goes straight to the sovereign edge (127.0.0.1:8443). Status -> stbox[0]; body -> out. 130// Returns body length or negative. Redirects are NOT followed (a 3xx classifies healthy anyway). 131func sent_fetch_via(url: *u8, ovip: i64, ovport: i64, store: *TrustStore, out: *u8, out_cap: i64, stbox: *i64) -> i64 { 132 stbox[0] = 0 133 let now: i64 = sys_now_realtime_sec() 134 let url_p: *NxUrl = nx_url_new() 135 let target_raw: *u8 = sys_mmap(32) 136 let target: *NxHttpsTarget = target_raw as *NxHttpsTarget 137 target.url = url_p; target.port = 0 138 if nx_https_url_for_fetch(url, target) != NX_HTTPS_URL_OK { return 0 - 1 } 139 let sa: *u8 = sys_mmap(16) 140 nx_https_build_sockaddr(sa, ovip, ovport) 141 let fd: i64 = sys_socket(NX_HTTPS_AF_INET, NX_HTTPS_SOCK_STREAM, 0) 142 if fd < 0 { return 0 - 2 } 143 sys_set_socket_timeout(fd, 20) 144 if nx_connect_bounded(fd, sa, 16, NX_CONN_DEFAULT_MS) < 0 { sys_close(fd); return 0 - 3 } 145 let cr: *u8 = sys_mmap(32); nx_csprng_fill(cr, 32) 146 let priv: *u8 = sys_mmap(32); nx_csprng_fill(priv, 32) 147 let val_raw: *u8 = sys_mmap(64) 148 let vc: *TlsValidationContext = val_raw as *TlsValidationContext 149 vc.store = store 150 vc.sni_host = url + target.url.host_off 151 vc.sni_host_len = target.url.host_len 152 vc.now_epoch = now 153 let sr: i64 = nx_tls13_client_session_run(fd, url + target.url.host_off, target.url.host_len, cr, priv, vc) 154 if sr < 0 { sys_close(fd); return 0 - 4 } 155 let session: *Tls13ClientSession = sr as *Tls13ClientSession 156 var path_ptr: *u8 = url + target.url.path_off 157 var path_len: i64 = target.url.path_len 158 if path_len == 0 { let dp: *u8 = sys_mmap(2); dp[0] = 47 as u8; path_ptr = dp; path_len = 1 } 159 let n: i64 = nx_https_get_complete(session, fd, path_ptr, path_len, url + target.url.host_off, target.url.host_len, out, out_cap) 160 sys_close(fd) 161 if n < 0 { return n } 162 // parse "HTTP/1.x NNN" 163 var st: i64 = 0 164 if n >= 12 { var k: i64 = 9; while k < 12 { let c: i64 = out[k] as i64; if c >= 48 { if c <= 57 { st = st * 10 + (c - 48) } } k = k + 1 } } 165 stbox[0] = st 166 return n 167} 168// look up prior ledger for `route` -> writes last_live to llbox[0], fail_streak to fsbox[0]. Returns 1 if found. 169func ledger_lookup(led: *u8, ln: i64, route: *u8, llbox: *i64, fsbox: *i64) -> i64 { 170 let rb: *u8 = sys_mmap(512); let vb: *u8 = sys_mmap(64) 171 var i: i64 = 0 172 while i < ln { 173 var j: i64 = i 174 while j < ln { if led[j] == (10 as u8) { break } j = j + 1 } 175 if j > i { 176 tfield(led, i, j, 0, rb) 177 if lsent_streq(rb, route) == 1 { 178 tfield(led, i, j, 3, vb); llbox[0] = lsent_atoi(vb) 179 tfield(led, i, j, 4, vb); fsbox[0] = lsent_atoi(vb) 180 return 1 181 } 182 } 183 i = j + 1 184 } 185 llbox[0] = 0; fsbox[0] = 0; return 0 186} 187 188func main(argc: i64, argv: *i64) -> i64 { 189 var mode: i64 = 0 190 if argc >= 2 { 191 if lsent_streq(argv[1] as *u8, "json" as *u8) == 1 { mode = 1 } 192 if lsent_streq(argv[1] as *u8, "html" as *u8) == 1 { mode = 2 } 193 } 194 // optional argv[2] = connect-override "a.b.c.d:port" (NAS-side probing via the loopback sovereign edge) 195 var ovip: i64 = 0 196 var ovport: i64 = 0 197 if argc >= 3 { 198 let ipb: *i64 = sys_mmap(8) as *i64 199 let ptb: *i64 = sys_mmap(8) as *i64 200 if sent_parse_ipport(argv[2] as *u8, ipb, ptb) == 1 { ovip = ipb[0]; ovport = ptb[0] } 201 } 202 // trust store (public-edge TLS) 203 let r: i64 = nx_trust_store_load_from_certdata("data/mozilla_certdata.txt" as *u8, 512, ARCHIVE_MAGIC_4194304) 204 if r <= 0 { w(2, "SENTINEL: trust store load FAIL (need data/mozilla_certdata.txt in cwd)\n" as *u8); sys_exit(2); return 2 } 205 let store: *TrustStore = r as *TrustStore 206 207 let cap: i64 = ARCHIVE_MAGIC_65536 208 let sbuf: *u8 = sys_mmap(cap) 209 var sn: i64 = c_read("knowledge/site/surfaces.reg" as *u8, sbuf, cap) 210 if sn <= 0 { sn = c_read("surfaces.reg" as *u8, sbuf, cap) } 211 // prior ledger (may be absent on first run); dual-path: repo layout, else CWD (NAS: knowledge/ is root-owned) 212 let led: *u8 = sys_mmap(cap) 213 var ln: i64 = c_read("knowledge/site/link_health.tsv" as *u8, led, cap) 214 if ln <= 0 { ln = c_read("link_health.tsv" as *u8, led, cap) } 215 if ln < 0 { ln = 0 } 216 217 let now: i64 = sys_now_realtime_sec() 218 let out: *u8 = sys_mmap(ARCHIVE_MAGIC_1048576) 219 // big-body retry window: a surface whose body exceeds the probe buffer (e.g. a 4.4MB static page) 220 // must NOT read as DEAD -- hoisted once (no mmap in the probe loop), used only on a failed first probe. 221 let bigcap: i64 = ARCHIVE_MAGIC_8388608 222 let bigout: *u8 = sys_mmap(bigcap) 223 let stbox: *i64 = sys_mmap(8) as *i64 224 let route: *u8 = sys_mmap(512); let title: *u8 = sys_mmap(256); let section: *u8 = sys_mmap(256) 225 let scope: *u8 = sys_mmap(64); let url: *u8 = sys_mmap(ARCHIVE_MAGIC_1024) 226 let probeb: *u8 = sys_mmap(ARCHIVE_MAGIC_1024) 227 let llbox: *i64 = sys_mmap(8) as *i64; let fsbox: *i64 = sys_mmap(8) as *i64 228 // freshly built ledger accumulates here 229 let nled: *u8 = sys_mmap(cap); var nlo: i64 = 0 230 231 // counters 232 var total: i64 = 0; var healthy: i64 = 0; var dead: i64 = 0; var degraded: i64 = 0; var archcand: i64 = 0 233 234 // ★ CANARY (false-alarm guard): probe an always-up anchor (/compare = a static docroot page, served iff the 235 // edge is up) BEFORE any stdout write or ledger touch. If it is unreachable, EVERY surface would read "down" 236 // -- that is a MONITORING BLINDSPOT (the edge/probe-path bounced, e.g. an every-minute reconcile cron 237 // restarting sites.elf mid-probe), NOT a mass outage. Publishing all-down is a false alarm, so ABORT: emit 238 // nothing, do NOT rewrite the ledger, exit 3 -> the caller keeps the last-good dashboard. 239 let canurl: *u8 = sys_mmap(256) 240 var cuo: i64 = scopy(canurl, 0, HOST); cuo = scopy(canurl, cuo, "/compare" as *u8); canurl[cuo] = 0 as u8 241 stbox[0] = 0 242 var cgot: i64 = 0 243 if ovport > 0 { cgot = sent_fetch_via(canurl, ovip, ovport, store, out, cap, stbox) } else { cgot = nx_https_fetch_follow(canurl, store, out, cap, 6, stbox) } 244 var canst: i64 = stbox[0] 245 if cgot < 0 { canst = 0 } 246 if canst < 200 { w(2, "SENTINEL: canary /compare unreachable (status=" as *u8); wn(2, canst); w(2, ") -- edge/probe-path down, ABORT (last-good kept)\n" as *u8); sys_exit(3); return 3 } 247 248 // fd for emit: console (gate) prints as we go; json/html buffer via a second pass would need storage, 249 // so we PROBE once and emit inline into the chosen format, accumulating counts + the ledger. 250 if mode == 2 { 251 w2(1, "<" as *u8); wc(1, 33); w2(1, "DOCTYPE html>\n<html lang='en'><head><meta charset='utf-8'><meta name='viewport' content='width=device-width, initial-scale=1'>\n" as *u8) 252 w2(1, "<title>Link Health &mdash; Nishi nav sentinel</title>\n<style>\n" as *u8) 253 w2(1, ":root{--bg:rgb(252,252,254);--fg:rgb(22,22,34);--ac:rgb(42,77,143);--line:rgb(228,228,236);--soft:rgb(246,247,251);--mut:rgb(104,104,118);--ok:rgb(26,127,55);--warn:rgb(178,106,0);--bad:rgb(179,38,30)}*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--fg);font-family:-apple-system,Segoe UI,Roboto,sans-serif;line-height:1.55}main{max-width:920px;margin:0 auto;padding:0 clamp(16px,5vw,26px) 8vh}\n" as *u8) 254 w2(1, ".hero h1{font-size:clamp(1.7rem,5vw,2.2rem);margin:22px 0 4px}.hero p{color:var(--mut);margin:0 0 12px}.tot{display:flex;gap:10px;flex-wrap:wrap;margin:14px 0 22px}.tot span{padding:7px 13px;border-radius:20px;font-weight:700;font-size:.85rem;border:1px solid var(--line);background:var(--soft)}\n" as *u8) 255 w2(1, "table{width:100%;border-collapse:collapse;font-size:.9rem}th,td{text-align:left;padding:9px 10px;border-bottom:1px solid var(--line)}th{font-size:.72rem;text-transform:uppercase;letter-spacing:.06em;color:var(--mut)}tr:hover td{background:var(--soft)}a{color:var(--ac);text-decoration:none}a:hover{text-decoration:underline}\n" as *u8) 256 w2(1, ".b{font-weight:700;font-size:.7rem;padding:2px 9px;border-radius:20px;color:rgb(255,255,255)}.b.live{background:var(--ok)}.b.gated{background:var(--ac)}.b.redir{background:var(--warn)}.b.dead{background:var(--bad)}.foot{margin-top:26px;color:var(--mut);font-size:.78rem;border-top:1px solid var(--line);padding-top:12px}\n" as *u8) 257 w2(1, "@media(prefers-color-scheme:dark){:root{--bg:rgb(14,14,19);--fg:rgb(231,231,239);--line:rgb(38,38,48);--soft:rgb(22,22,30);--mut:rgb(156,156,168)}}\n</style></head><body><main>\n" as *u8) 258 w2(1, "<div class='hero'><h1>Link Health</h1><p>Every navigation link, probed live over the sovereign TLS edge. No orphans &mdash; a dead link is flagged here, never silently dropped.</p></div>\n" as *u8) 259 } 260 if mode == 1 { w2(1, "{'api':'nishi-link-health','generated_unix':" as *u8); wn(1, now); w2(1, ",'links':[" as *u8) } 261 if mode == 0 { w(1, "=== NX-LINK-SENTINEL -- nav link liveness ===\n" as *u8) } 262 263 // rows accumulate for html so we can put the summary badges before the table? We stream table rows after 264 // the head above; the summary line is emitted at the end (json/gate) or via a trailing note (html). 265 if mode == 2 { w2(1, "<table><thead><tr><th>Surface</th><th>Route</th><th>Status</th><th>Health</th><th>Fail streak</th></tr></thead><tbody>\n" as *u8) } 266 267 var jfirst: i64 = 1 268 var i: i64 = 0 269 while i < sn { 270 var j: i64 = i 271 while j < sn { if sbuf[j] == (10 as u8) { break } j = j + 1 } 272 if sbuf[i] != (35 as u8) { if j > i { 273 field(sbuf, i, j, 0, route) 274 if route[0] != (0 as u8) { 275 field(sbuf, i, j, 2, title); field(sbuf, i, j, 1, section); field(sbuf, i, j, 4, scope) 276 let sg: i64 = lsent_streq(scope, "gated" as *u8) 277 let sm: i64 = lsent_streq(scope, "machine" as *u8) 278 // build url = HOST + (probe-override field 7 if present, else route). Machine endpoints whose 279 // bare route is only a 404 guard are probed at their REAL function path (honest liveness). 280 field(sbuf, i, j, 7, probeb) 281 var uo: i64 = scopy(url, 0, HOST) 282 if probeb[0] != (0 as u8) { uo = scopy(url, uo, probeb) } else { uo = scopy(url, uo, route) } 283 url[uo] = 0 as u8 284 // probe (override -> straight to the sovereign edge; else public DNS path) 285 stbox[0] = 0 286 var got: i64 = 0 287 if ovport > 0 { got = sent_fetch_via(url, ovip, ovport, store, out, cap, stbox) } else { got = nx_https_fetch_follow(url, store, out, cap, 6, stbox) } 288 var st: i64 = stbox[0] 289 if got < 0 { st = 0 } 290 // failed first probe -> ONE retry with the 8MB window before any DEAD verdict 291 if st == 0 { 292 stbox[0] = 0 293 var got2: i64 = 0 294 if ovport > 0 { got2 = sent_fetch_via(url, ovip, ovport, store, bigout, bigcap, stbox) } else { got2 = nx_https_fetch_follow(url, store, bigout, bigcap, 6, stbox) } 295 st = stbox[0] 296 if got2 < 0 { st = 0 } 297 } 298 let cls: i64 = classify(st) 299 var hh: i64 = is_healthy(cls, sg) 300 // machine scope (API endpoint, not a browsable page): ANY real HTTP answer = alive 301 if sm == 1 { hh = 0; if st >= 100 { if st < 500 { hh = 1 } } } 302 // class label: machine endpoints answering (even 404) are "API", never DEAD/REDIRECT 303 var clabel: *u8 = class_name(cls) 304 if sm == 1 { if hh == 1 { clabel = "API" as *u8 } } 305 // ledger update 306 ledger_lookup(led, ln, route, llbox, fsbox) 307 var last_live: i64 = llbox[0] 308 var streak: i64 = fsbox[0] 309 if hh == 1 { last_live = now; streak = 0 } else { streak = streak + 1 } 310 total = total + 1 311 if hh == 1 { healthy = healthy + 1 } else { dead = dead + 1; if streak >= ARCHIVE_THRESHOLD { archcand = archcand + 1 } else { degraded = degraded + 1 } } 312 // append to new ledger: route \t class \t status \t last_live \t streak \t now \n 313 nlo = scopy(nled, nlo, route); nled[nlo] = 9 as u8; nlo = nlo + 1 314 nlo = scopy(nled, nlo, clabel); nled[nlo] = 9 as u8; nlo = nlo + 1 315 // status + last_live + streak + now as decimals -- reuse wn into buffer? write direct via small ints 316 // (encode numbers into nled) 317 nlo = enc_int(nled, nlo, st); nled[nlo] = 9 as u8; nlo = nlo + 1 318 nlo = enc_int(nled, nlo, last_live); nled[nlo] = 9 as u8; nlo = nlo + 1 319 nlo = enc_int(nled, nlo, streak); nled[nlo] = 9 as u8; nlo = nlo + 1 320 nlo = enc_int(nled, nlo, now); nled[nlo] = 10 as u8; nlo = nlo + 1 321 // emit row 322 if mode == 0 { 323 w(1, " "); 324 if hh == 1 { w(1, "[ok] " as *u8) } else { w(1, "[DEAD]" as *u8) } 325 w(1, " " as *u8); w(1, route); w(1, " " as *u8); w(1, clabel); w(1, " (" as *u8); wn(1, st); w(1, ")" as *u8) 326 if streak > 0 { w(1, " fails=" as *u8); wn(1, streak) } 327 w(1, "\n" as *u8) 328 } 329 if mode == 1 { 330 if jfirst == 0 { wc(1, 44) } 331 jfirst = 0 332 w2(1, "{'route':'" as *u8); w(1, route); w2(1, "','title':'" as *u8); w(1, title); w2(1, "','status':" as *u8); wn(1, st) 333 w2(1, ",'class':'" as *u8); w(1, clabel); w2(1, "','healthy':" as *u8); wn(1, hh) 334 w2(1, ",'fail_streak':" as *u8); wn(1, streak); w2(1, ",'last_live_unix':" as *u8); wn(1, last_live); w2(1, "}" as *u8) 335 } 336 if mode == 2 { 337 w2(1, "<tr><td>" as *u8); w(1, title); w2(1, "</td><td><a href='" as *u8); w(1, route); w2(1, "'>" as *u8); w(1, route); w2(1, "</a></td><td>" as *u8); wn(1, st) 338 w2(1, "</td><td><span class='b " as *u8) 339 if hh == 1 { 340 if cls == 1 { w2(1, "live'>LIVE" as *u8) } else { 341 if cls == 2 { w2(1, "gated'>GATED" as *u8) } else { 342 if cls == 3 { w2(1, "redir'>REDIRECT" as *u8) } else { w2(1, "gated'>API" as *u8) } 343 } 344 } 345 } else { w2(1, "dead'>DEAD" as *u8) } 346 w2(1, "</span></td><td>" as *u8); wn(1, streak); w2(1, "</td></tr>\n" as *u8) 347 } 348 } 349 } } 350 i = j + 1 351 } 352 353 // write the refreshed ledger ONLY when the probe is credible (>=60% healthy). A mid-loop edge bounce that 354 // fails many surfaces (canary passed at the start, then sites.elf restarted) must NOT overwrite the 355 // last-good ledger -- home/sitemap regenerate from it. Dual-path (repo layout, else CWD/root-owned NAS). 356 let credible: i64 = ((total > 0) as i64) & (((healthy * 100) >= (total * 60)) as i64) 357 if credible == 1 { 358 var lfd: i64 = sys_openat_wr("knowledge/site/link_health.tsv" as *u8, 0x1a4) 359 if lfd < 0 { lfd = sys_openat_wr("link_health.tsv" as *u8, 0x1a4) } 360 if lfd >= 0 { sys_write(lfd, nled, nlo); sys_close(lfd) } 361 } 362 363 if mode == 2 { 364 w2(1, "</tbody></table>\n<div class='tot'><span>" as *u8); wn(1, total); w2(1, " links</span><span>" as *u8); wn(1, healthy) 365 w2(1, " healthy</span><span>" as *u8); wn(1, dead); w2(1, " down</span><span>" as *u8); wn(1, archcand); w2(1, " archive candidates</span></div>\n" as *u8) 366 w2(1, "<p class='foot'>Probed by nx_link_sentinel over the sovereign TLS edge. Archive candidate = " as *u8); wn(1, ARCHIVE_THRESHOLD); w2(1, "+ consecutive failed probes. Links stay live until deliberately archived.</p>\n</main></body></html>\n" as *u8) 367 sys_exit(0); return 0 368 } 369 if mode == 1 { 370 w2(1, "],'tally':{'total':" as *u8); wn(1, total); w2(1, ",'healthy':" as *u8); wn(1, healthy); w2(1, ",'down':" as *u8); wn(1, dead) 371 w2(1, ",'archive_candidates':" as *u8); wn(1, archcand); w2(1, "}}" as *u8); wc(1, 10) 372 sys_exit(0); return 0 373 } 374 w(1, " ----\n total=" as *u8); wn(1, total); w(1, " healthy=" as *u8); wn(1, healthy); w(1, " down=" as *u8); wn(1, dead); w(1, " archive-candidates=" as *u8); wn(1, archcand); w(1, "\n" as *u8) 375 w(1, " LIAR-KILL: probed-all=" as *u8); wn(1, (total > 0) as i64); w(1, " ledger-written=" as *u8); wn(1, (nlo > 0) as i64); w(1, "\n" as *u8) 376 w(1, "NX-LINK-SENTINEL verdict=" as *u8) 377 if total > 0 { w(1, "GREEN (probed " as *u8); wn(1, total); w(1, ", " as *u8); wn(1, dead); w(1, " down)\n" as *u8); sys_exit(0); return 0 } 378 w(1, "RED (no surfaces probed)\n" as *u8); sys_exit(1); return 1 379}