#!/usr/bin/env python3 """Render detached status HTML from stack-status.json (no probing).""" from __future__ import annotations import argparse import html import json import os import sys import time from pathlib import Path HERE = Path(__file__).resolve().parent sys.path.insert(0, str(HERE)) import stack_status_lib as L # noqa: E402 # Human labels instead of colour names (llm-control style) _STATUS_LABEL = { "green": "OK", "blue": "Present", "yellow": "Soft", "orange": "Degraded", "red": "Down", "black": "Long down", "white": "Gone", } def _logo_svg(pkg: str, *, fill: str, text: str, size: str = "16") -> str: return ( f'' ) _LOGO_SVG = { "nginx": lambda: _logo_svg("nginx", fill="#009639", text="ngx", size="18"), "apache": lambda: _logo_svg("apache", fill="#d22128", text="httpd", size="14"), "caddy": lambda: _logo_svg("caddy", fill="#1f9ceb", text="caddy", size="14"), "postfix": lambda: _logo_svg("postfix", fill="#f0a000", text="pfx", size="16"), "mta": lambda: _logo_svg("mta", fill="#c08a00", text="mta", size="16"), "taler": lambda: _logo_svg("taler", fill="#5a32a3", text="T", size="22"), "taler-exchange": lambda: _logo_svg("taler-exchange", fill="#5a32a3", text="ex", size="16"), "taler-merchant": lambda: _logo_svg("taler-merchant", fill="#7b4cc7", text="mer", size="14"), "taler-bank": lambda: _logo_svg("taler-bank", fill="#3d2a80", text="bnk", size="14"), "webui": lambda: _logo_svg("webui", fill="#0d9488", text="ui", size="16"), "wallet": lambda: _logo_svg("wallet", fill="#2563eb", text="wal", size="14"), } CSS = """ :root { --bg: #0f1419; --fg: #e7ecf1; --muted: #8b98a5; --card: #1a222c; --line: #243040; --dot-green: #3ddc84; --dot-blue: #61afef; --dot-yellow: #e6c07b; --dot-orange: #ff9f43; --dot-red: #ff5c5c; --dot-black: #2a2a2a; --ring-black: #9aa0a6; } * { box-sizing: border-box; } body { margin: 0; font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, sans-serif; background: var(--bg); color: var(--fg); line-height: 1.3; } header { padding: .7rem 1rem; border-bottom: 1px solid var(--line); display: flex; flex-wrap: wrap; gap: .4rem 1rem; align-items: center; } header h1 { font-size: 1rem; margin: 0; font-weight: 650; } header .meta { color: var(--muted); font-size: .8rem; } header .score { display: inline-flex; align-items: center; gap: .4rem; font-size: .85rem; font-weight: 650; border: 1px solid #314050; border-radius: 999px; padding: .2rem .65rem; background: rgba(0,0,0,.2); } header .score .dot { width: .65rem; height: .65rem; } .dot.white { background: #f5f7fa; box-shadow: 0 0 0 1px #9aa0a6; } .score.light-green { border-color: rgba(61,220,132,.55); } .score.light-blue { border-color: rgba(97,175,239,.55); } .score.light-yellow { border-color: rgba(230,192,123,.55); } .score.light-orange { border-color: rgba(255,159,67,.55); } .score.light-red { border-color: rgba(255,92,92,.55); } .score.light-black { border-color: rgba(154,160,166,.45); } .score.light-white { border-color: rgba(245,247,250,.45); color: #c5ccd4; } h3.sec { margin: 1rem 1rem .35rem; font-size: .75rem; color: var(--muted); text-transform: uppercase; letter-spacing: .04em; font-weight: 600; } .grid { display: grid; gap: .7rem; padding: .55rem 1rem .85rem; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); } .card { background: var(--card); border-radius: 10px; padding: .7rem .8rem; border: 2px solid #314050; min-width: 0; cursor: pointer; } .card:focus-visible { outline: 2px solid var(--dot-blue); outline-offset: 2px; } .card h2 { margin: 0; font-size: .9rem; font-weight: 650; overflow-wrap: anywhere; } .card .sub { color: var(--muted); font-size: .72rem; margin: .15rem 0 .45rem; display: flex; flex-wrap: wrap; gap: .25rem .55rem; align-items: center; } .card .sub .bad { color: var(--dot-orange); } .card .sub .crit { color: var(--dot-red); } .card .sub .ok { color: var(--dot-green); } .card .why { font-size: .68rem; color: var(--muted); margin: 0 0 .35rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .rows { display: flex; flex-direction: column; gap: .15rem; } .row { display: flex; align-items: center; gap: .4rem; font-size: .78rem; min-width: 0; } .row .role { color: var(--muted); width: 3.6rem; flex: 0 0 auto; font-size: .65rem; text-transform: uppercase; } .row .host { flex: 1 1 auto; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .dot { width: .55rem; height: .55rem; border-radius: 50%; flex: 0 0 auto; box-shadow: 0 0 0 1px rgba(255,255,255,.1); } .dot.green { background: var(--dot-green); } .dot.blue { background: var(--dot-blue); } .dot.yellow { background: var(--dot-yellow); } .dot.orange { background: var(--dot-orange); } .dot.red { background: var(--dot-red); } .dot.black { background: var(--dot-black); box-shadow: 0 0 0 1px var(--ring-black); } .tags { display: flex; flex-wrap: wrap; gap: .3rem; margin-top: .5rem; padding-top: .45rem; border-top: 1px solid var(--line); align-items: center; } .tag { font-size: .62rem; color: var(--muted); border: 1px solid #314050; border-radius: 999px; padding: .1rem .45rem; background: rgba(0,0,0,.15); text-decoration: none; } a.tag:hover { filter: brightness(1.15); } .tag.warn { color: var(--dot-orange); border-color: rgba(255,159,67,.45); } .tag.crit { color: var(--dot-red); border-color: rgba(255,92,92,.45); } .logo { width: 1.05rem; height: 1.05rem; vertical-align: middle; flex: 0 0 auto; } .swrow { display: flex; flex-wrap: wrap; gap: .35rem; margin-top: .45rem; } .sw { display: inline-flex; align-items: center; gap: .3rem; font-size: .62rem; border-radius: 8px; padding: .15rem .4rem .15rem .25rem; border: 1px solid #314050; background: rgba(0,0,0,.18); color: var(--muted); max-width: 100%; } .sw .ver { color: var(--fg); font-variant-numeric: tabular-nums; white-space: nowrap; } .sw.light-green { border-color: rgba(61,220,132,.55); } .sw.light-blue { border-color: rgba(97,175,239,.5); } .sw.light-yellow { border-color: rgba(230,192,123,.55); color: var(--dot-yellow); } .sw.light-orange { border-color: rgba(255,159,67,.6); color: var(--dot-orange); } .sw.light-red { border-color: rgba(255,92,92,.65); color: var(--dot-red); } .card.ok-green { border-color: rgba(61,220,132,.5); } .card.ok-blue { border-color: rgba(97,175,239,.5); } .card.muted-green { border-color: rgba(61,220,132,.3); } .card.muted-blue { border-color: rgba(97,175,239,.3); } .card.muted-yellow { border-color: rgba(230,192,123,.4); } .card.muted-orange { border-color: rgba(255,159,67,.45); } .card.muted-red { border-color: rgba(255,92,92,.4); } .card.muted-black { border-color: rgba(154,160,166,.35); } .card.full-red { border-color: var(--dot-red); background: #3a1414; } .card.full-black { border-color: var(--ring-black); background: #0a0a0a; } .card.full-orange { border-color: var(--dot-orange); background: #3a2210; } .stale { color: var(--dot-orange); } footer { padding: .7rem 1rem 1.2rem; color: var(--muted); font-size: .7rem; } .result-index { margin: .4rem 1rem 1.2rem; padding: .75rem .9rem; border: 1px solid var(--line); border-radius: 10px; background: #121820; } .result-index h3 { margin: 0 0 .55rem; font-size: .78rem; color: var(--muted); text-transform: uppercase; letter-spacing: .04em; font-weight: 600; } .result-index .ri-group { margin: 0 0 .65rem; } .result-index .ri-group:last-child { margin-bottom: 0; } .result-index .ri-hd { display: flex; align-items: center; gap: .4rem; font-size: .78rem; font-weight: 600; margin: 0 0 .3rem; } .result-index ul { margin: 0; padding-left: 1.05rem; font-size: .72rem; color: var(--muted); } .result-index li { margin: .18rem 0; } .result-index .ri-stack { color: var(--fg); font-weight: 500; } .result-index .ri-cmd { display: inline-block; margin-left: .35rem; padding: .05rem .35rem; border-radius: 4px; border: 1px solid #314050; background: rgba(0,0,0,.25); color: var(--dot-blue); font-family: ui-monospace, Menlo, Consolas, monospace; font-size: .68rem; } dialog { border: 1px solid var(--line); border-radius: 12px; background: #121820; color: var(--fg); padding: 0; max-width: min(520px, 94vw); width: 100%; } dialog::backdrop { background: rgba(0,0,0,.55); } .dlg-hd { display: flex; justify-content: space-between; gap: .5rem; align-items: start; padding: .85rem 1rem; border-bottom: 1px solid var(--line); } .dlg-hd h2 { margin: 0; font-size: 1rem; } .dlg-hd button { background: transparent; border: 1px solid #314050; color: var(--fg); border-radius: 8px; padding: .2rem .55rem; cursor: pointer; } .dlg-bd { padding: .85rem 1rem 1.1rem; font-size: .82rem; } .dlg-bd .agg { margin: 0 0 .7rem; color: var(--muted); } .dlg-bd ul { margin: 0; padding-left: 1.1rem; } .dlg-bd li { margin: .25rem 0; } .dlg-bd .cve { margin-top: .8rem; padding-top: .7rem; border-top: 1px solid var(--line); } .dlg-bd a { color: var(--dot-blue); } """ def _esc(s: object) -> str: return html.escape(str(s if s is not None else ""), quote=True) def _label(light: object) -> str: return _STATUS_LABEL.get(str(light or ""), str(light or "?")) def _short_host(host: str) -> str: h = host or "" if h.upper() == "IBAN": return "IBAN" return h def _osv_url(cve_id: str) -> str: cid = (cve_id or "").strip() if not cid: return "https://osv.dev/" return f"https://osv.dev/vulnerability/{cid}" def _probe_command_for_endpoint(host: str, ep: dict) -> str: """Concrete outside-in probe (HTTP GET) for one endpoint row.""" if ep.get("skipped"): path = ep.get("path") or ep.get("id") or "?" return f"(skip) GET https://{host}{path}" url = str(ep.get("url") or "").strip() if not url: path = ep.get("path") or "" url = f"https://{host}{path}" if host and path else path or "?" return f"GET {url}" def _probe_command_for_member(m: dict) -> str: role = str(m.get("role") or "member") host = str(m.get("host") or "") reason = str(m.get("reason") or "") if role == "ssh" or reason.startswith("SSH-") or reason in ("ssh_probe", "timeout", "no_host"): target = host or "?" return f"TCP {target}:22 banner" if host.upper() == "IBAN" or reason in ("ebics_iban", "ebics_no_bank"): return f"(no HTTP) {role} · {reason or 'ebics'}" if host: return f"GET https://{host}/config" return f"probe {role}" def collect_result_index_rows(doc: dict) -> list[dict]: """Flatten probe/test rows across stacks/services/mail for the bottom index.""" rows: list[dict] = [] for section, key in ( ("stack", "stacks"), ("service", "services"), ("mail", "mail"), ): for st in doc.get(key) or []: if not isinstance(st, dict): continue sid = str(st.get("id") or "?") for m in st.get("members") or []: if not isinstance(m, dict): continue role = str(m.get("role") or "") if role == "cve": continue host = str(m.get("host") or "") endpoints = [e for e in (m.get("endpoints") or []) if isinstance(e, dict)] if endpoints: for ep in endpoints: light = str(ep.get("light") or m.get("light") or "black") eid = str(ep.get("id") or ep.get("path") or "ep") reason = str(ep.get("reason") or "") label = f"{role}/{eid}" rows.append( { "section": section, "stack": sid, "role": role, "host": host, "id": eid, "label": label, "light": light, "reason": reason, "command": _probe_command_for_endpoint(host, ep), } ) else: light = str(m.get("light") or "black") rows.append( { "section": section, "stack": sid, "role": role, "host": host, "id": role or "member", "label": role or "member", "light": light, "reason": str(m.get("reason") or ""), "command": _probe_command_for_member(m), } ) net = st.get("network") if isinstance(st.get("network"), dict) else {} ssh = net.get("ssh") if isinstance(net.get("ssh"), dict) else {} if ssh.get("expected") and ssh.get("ok") is not None: shown = str(ssh.get("host") or "") # Prefer real candidates when not hidden; else keep blank host in cmd hosts = list(ssh.get("candidates") or []) cmd_host = shown or (hosts[0] if hosts else "") rows.append( { "section": section, "stack": sid, "role": "ssh", "host": shown, "id": "ssh", "label": "ssh", "light": str(ssh.get("light") or ("green" if ssh.get("ok") else "orange")), "reason": str(ssh.get("reason") or ""), "command": f"TCP {cmd_host or '∅'}:22 banner", } ) return rows def _render_result_index(doc: dict, *, mode: str | None = None) -> str: """Optional bottom section: all tests grouped by light colour (+ optional command).""" mode = (mode or getattr(L, "RESULT_INDEX_MODE", "off") or "off").lower() if mode in ("0", "off", "false", "no", ""): return "" show_cmd = mode in ("cmd", "full", "commands", "2") rows = collect_result_index_rows(doc) if not rows: return "" order = ("green", "blue", "yellow", "orange", "red", "black", "white") by: dict[str, list[dict]] = {} for r in rows: by.setdefault(str(r.get("light") or "black"), []).append(r) groups = [] for light in order: items = by.get(light) or [] if not items: continue lis = [] for r in items: tip = r.get("reason") or _label(light) cmd = "" if show_cmd and r.get("command"): cmd = f' {_esc(r["command"])}' host = r.get("host") or "" host_bit = f" @ {_esc(host)}" if host else "" lis.append( "
  • " f'{_esc(r.get("stack"))} · ' f'{_esc(r.get("label"))}{host_bit}' f' ({_esc(_label(light))})' f"{cmd}" "
  • " ) groups.append( f'
    ' f'
    ' f'{_esc(_label(light))} · {len(items)}
    ' f'
    ' ) title = "all probes by colour" if show_cmd: title += " · with command" return ( f'
    ' f"

    {_esc(title)} ({len(rows)})

    " f'{"".join(groups)}' f"
    " ) def _agg_text(st: dict) -> str: bd = st.get("breakdown") if isinstance(st.get("breakdown"), dict) else {} by = bd.get("by_light") if isinstance(bd.get("by_light"), dict) else {} if not by: # fallback from members by = {} for m in st.get("members") or []: if (m.get("reason") or "") in getattr(L, "_AGG_SKIP_REASONS", ()): continue Lgt = str(m.get("light") or "black") by[Lgt] = by.get(Lgt, 0) + 1 if not by: return "" order = ("green", "blue", "yellow", "orange", "red", "black") parts = [] for k in order: n = int(by.get(k) or 0) if n: parts.append(f"{n}× {_label(k)}") worst = st.get("light") or bd.get("worst") or "" if not parts: return "" return f"{', '.join(parts)} → {_label(worst)}" def _software_items(st: dict) -> list[dict]: raw = st.get("software") if isinstance(st.get("software"), list) else [] if raw: return [x for x in raw if isinstance(x, dict) and x.get("package")] # Fallback: vulns.servers only vulns = st.get("vulns") if isinstance(st.get("vulns"), dict) else {} out = [] for srv in vulns.get("servers") or []: p = str((srv or {}).get("package") or "") if not p: continue out.append( { "package": p, "version": (srv or {}).get("version") or "", "host": (srv or {}).get("host") or "", "light": "green", } ) return out def _render_software_row(st: dict) -> str: items = _software_items(st) if not items: return "" # Collapse identical package+version (multi-host) into one chip. merged: dict[str, dict] = {} for it in items: pkg = str(it.get("package") or "") ver = str(it.get("version") or "") key = f"{pkg}|{ver}" if key not in merged: merged[key] = { "package": pkg, "version": ver, "label": it.get("label") or pkg, "light": it.get("light") or "green", "hosts": [], "source": it.get("source") or "", "currency_status": it.get("currency_status") or "", "behind": it.get("behind") or "", "published_upstream": it.get("published_upstream") or "", "delta": it.get("delta"), } h = str(it.get("host") or "") if h and h not in merged[key]["hosts"]: merged[key]["hosts"].append(h) # keep worst light Lgt = str(it.get("light") or "green") cur = str(merged[key]["light"] or "green") if L.SEVERITY.get(Lgt, 0) > L.SEVERITY.get(cur, 0): merged[key]["light"] = Lgt if it.get("currency_status") == "behind": merged[key]["currency_status"] = "behind" merged[key]["behind"] = it.get("behind") or merged[key].get("behind") merged[key]["published_upstream"] = ( it.get("published_upstream") or merged[key].get("published_upstream") ) if it.get("delta") is not None: merged[key]["delta"] = it.get("delta") chips = [] for it in list(merged.values())[:8]: pkg = str(it.get("package") or "") ver = str(it.get("version") or "") light = str(it.get("light") or "green") hosts = it.get("hosts") or [] label = str(it.get("label") or pkg) logo_fn = _LOGO_SVG.get(pkg) logo = logo_fn() if callable(logo_fn) else _logo_svg(pkg, fill="#445566", text=(pkg[:3] or "?"), size="14") cur = str(it.get("currency_status") or "") behind = str(it.get("behind") or it.get("published_upstream") or "") delta = it.get("delta") ver_txt = ver or "?" if cur == "behind" and behind: lag = f" −{delta}" if isinstance(delta, int) and delta > 0 else "" ver_txt = f"{ver_txt} → {behind}{lag}" elif cur == "current" and ver: ver_txt = f"{ver_txt} ✓" tip = " · ".join( x for x in ( label, ver, f"pub {behind}" if behind else "", cur, ", ".join(hosts[:4]), it.get("source") or "", ) if x ) chips.append( f'' f"{logo}" f'{_esc(ver_txt)}' f"" ) return f'
    {"".join(chips)}
    ' def _card_dialog_payload(st: dict) -> dict: """JSON-serializable detail for the open dialog.""" members = [] for m in st.get("members") or []: reason = m.get("reason") or "" es = m.get("endpoints_summary") if isinstance(m.get("endpoints_summary"), dict) else {} # If endpoints pulled the light up, surface worst endpoint id/reason. if es and (m.get("light") or "") not in ("green", "blue", None, ""): bad = [] for ep in m.get("endpoints") or []: if ep.get("skipped"): continue el = ep.get("light") or "green" if el in ("yellow", "orange", "red", "black"): bad.append(f"{ep.get('id') or ep.get('path')}:{ep.get('reason') or el}") if bad and reason in ("ok", "", None): reason = ", ".join(bad[:4]) elif bad: reason = f"{reason} · " + ", ".join(bad[:3]) members.append( { "role": m.get("role"), "host": m.get("host"), "light": m.get("light"), "label": _label(m.get("light")), "reason": reason, "code": m.get("code"), } ) hits = [] vulns = st.get("vulns") if isinstance(st.get("vulns"), dict) else {} for h in vulns.get("hits") or []: hits.append( { "id": h.get("id"), "severity": h.get("severity"), "package": h.get("package"), "version": h.get("version"), "host": h.get("host"), "summary": h.get("summary") or "", "url": _osv_url(str(h.get("id") or "")), } ) tls = st.get("tls") if isinstance(st.get("tls"), dict) else {} ports = st.get("ports") if isinstance(st.get("ports"), dict) else {} software = [] for it in _software_items(st)[:12]: software.append( { "package": it.get("package"), "label": it.get("label") or it.get("package"), "version": it.get("version") or "", "host": it.get("host") or "", "light": it.get("light") or "green", "source": it.get("source") or "", "currency_status": it.get("currency_status") or "", "published": it.get("published") or "", "behind": it.get("behind") or "", "delta": it.get("delta"), } ) return { "id": st.get("id"), "light": st.get("light"), "label": _label(st.get("light")), "agg": _agg_text(st), "members": members, "hits": hits, "tls": tls, "ports": ports.get("suspicious") or [], "software": software, "deliverability": st.get("deliverability") if isinstance(st.get("deliverability"), dict) else {}, "kind": st.get("kind"), "label_soft": st.get("service_label") or "", } def _render_card(st: dict, *, section: str = "stack") -> str: light = st.get("light") or "black" intensity = st.get("intensity") or "muted" klass = f"card {intensity}-{light}" cur = st.get("currency") or "" ci = st.get("currency_info") if isinstance(st.get("currency_info"), dict) else {} symbol = ci.get("symbol") or "" backend = st.get("bank_backend") or "" service_label = st.get("service_label") or "" bits = [x for x in (service_label, cur, symbol if symbol != cur else "", _label(light)) if x] if backend == "ebics": bits.insert(-1 if bits else 0, "IBAN") elif backend == "x-taler-bank": bits.insert(-1 if bits else 0, "x-taler-bank") net = st.get("network") if isinstance(st.get("network"), dict) else {} ssh = net.get("ssh") if isinstance(net.get("ssh"), dict) else {} sub_extra = [] if net.get("zone"): sub_extra.append(_esc(str(net.get("zone")))) if ssh.get("expected"): if ssh.get("ok") is True: sub_extra.append('ssh') elif ssh.get("ok") is False: sub_extra.append('ssh!') vulns = st.get("vulns") if isinstance(st.get("vulns"), dict) else {} hits = vulns.get("hits") or [] if hits: top = hits[0] cls = "crit" if (vulns.get("worst") or "") == "CRITICAL" else "bad" sub_extra.append( f'cve' ) why = _agg_text(st) why_html = f'
    {_esc(why)}
    ' if why else "" rows = [] for m in st.get("members") or []: role = m.get("role") or "" if role == "cve": continue host = m.get("host") or "" if not host and (m.get("reason") or "") in ("ebics_iban", "ebics_no_bank"): host = "IBAN" ml = m.get("light") or "black" tip = m.get("reason") or _label(ml) rows.append( "
    " f'' f'{_esc(role)}' f'{_esc(_short_host(host))}' "
    " ) tags = [] if st.get("commercial_prod"): tags.append('prod') if backend: tags.append(f'{_esc(backend)}') if net.get("zone"): tags.append(f'{_esc(net.get("zone"))}') if ssh.get("expected"): if ssh.get("ok") is True: tags.append('ssh') elif ssh.get("ok") is False: tags.append('ssh') tls = st.get("tls") if isinstance(st.get("tls"), dict) else {} if tls.get("days_left") is not None: try: days = int(tls["days_left"]) except (TypeError, ValueError): days = None if days is not None: if days < 14: tags.append(f'cert {days}d') elif days < 30: tags.append(f'cert {days}d') else: tags.append(f'cert') if st.get("orphan"): tags.append('orphan?') deliver = st.get("deliverability") if isinstance(st.get("deliverability"), dict) else {} if deliver: dl = str(deliver.get("light") or "yellow") reason = str(deliver.get("reason") or "deliver") age = deliver.get("age_s") age_s = "" try: if age is not None: a = float(age) age_s = f"{int(a // 3600)}h" if a >= 3600 else f"{int(a // 60)}m" except (TypeError, ValueError): age_s = "" tip = " · ".join(x for x in (reason, deliver.get("rcpt") or "", age_s) if x) cls = "tag" if dl in ("orange", "red"): cls = "tag warn" if dl == "orange" else "tag crit" elif dl == "yellow" or deliver.get("stale"): cls = "tag warn" tags.append(f'deliver') ports = st.get("ports") if isinstance(st.get("ports"), dict) else {} n_sus = int(ports.get("n_suspicious") or 0) if n_sus: tip = ", ".join( f"{s.get('label') or s.get('port')}" + (f"@{s.get('host')}" if s.get("host") else "") for s in (ports.get("suspicious") or [])[:8] ) tags.append( f'ports:{n_sus}' ) for h in hits[:3]: cid = str(h.get("id") or "cve") cls = "tag crit" if (h.get("severity") or "") == "CRITICAL" else "tag warn" tip = " · ".join( x for x in ( h.get("severity") or "", f"{h.get('package')}/{h.get('version')}" if h.get("package") else "", (h.get("summary") or "")[:80], ) if x ) tags.append( f'{_esc(cid)}' ) stats = st.get("stats") if isinstance(st.get("stats"), dict) else {} if stats.get("present"): tags.append('stats') denoms = (ci.get("denoms") or [])[:6] if denoms: tags.append( f'denoms' ) payload = _esc(json.dumps(_card_dialog_payload(st), ensure_ascii=False)) title = st.get("id") or "?" sw_html = _render_software_row(st) return ( f'
    ' f"

    {_esc(title)}

    " f'
    {_esc(" · ".join(bits))}' f'{(" · " + " ".join(sub_extra)) if sub_extra else ""}
    ' f"{why_html}" f'
    {"".join(rows)}
    ' f"{sw_html}" f'
    {"".join(tags)}
    ' "
    " ) JS = """ function openStack(el) { let raw = el.getAttribute('data-detail') || '{}'; let d; try { d = JSON.parse(raw); } catch (e) { return; } const dlg = document.getElementById('stack-dlg'); const title = document.getElementById('dlg-title'); const body = document.getElementById('dlg-body'); title.textContent = (d.id || '?') + ' · ' + (d.label || ''); let html = ''; if (d.agg) html += '

    ' + escapeHtml(d.agg) + '

    '; html += ''; if (d.tls && (d.tls.not_after || d.tls.days_left != null)) { html += '

    TLS · days_left=' + escapeHtml(d.tls.days_left) + (d.tls.not_after ? ' · until ' + escapeHtml(d.tls.not_after) : '') + '

    '; if (d.tls.sans && d.tls.sans.length) { html += '

    SANs: ' + escapeHtml(d.tls.sans.join(', ')) + '

    '; } } if (d.hits && d.hits.length) { html += '
    CVEs
    '; } if (d.software && d.software.length) { html += '
    Software
    '; } if (d.deliverability && (d.deliverability.reason || d.deliverability.light)) { const deliv = d.deliverability; html += '
    Deliverability
    '; } if (d.ports && d.ports.length) { html += '
    Suspicious ports
    '; } body.innerHTML = html; if (typeof dlg.showModal === 'function') dlg.showModal(); else dlg.setAttribute('open', ''); } function escapeHtml(s) { return String(s == null ? '' : s) .replace(/&/g,'&').replace(//g,'>') .replace(/"/g,'"'); } """ def render_html(doc: dict) -> str: stacks = doc.get("stacks") or [] services = doc.get("services") or [] mail = doc.get("mail") or [] orphans = doc.get("orphans") or [] ports_doc = doc.get("ports") if isinstance(doc.get("ports"), dict) else {} published = doc.get("published") if isinstance(doc.get("published"), dict) else {} ts = doc.get("ts") age = None try: age = time.time() - float(ts) except (TypeError, ValueError): pass stale = age is not None and age > 3 * float(doc.get("acute_down_s") or L.ACUTE_DOWN_S) if age is None: updated = "?" elif age < 120: updated = f"{int(age)}s" elif age < 3600: updated = f"{int(age // 60)}m" else: updated = f"{age / 3600:.1f}h" try: updated += " · " + time.strftime("%H:%M", time.localtime(float(ts))) except (TypeError, ValueError, OSError): pass stack_cards = [_render_card(st, section="stack") for st in stacks] # Firefly-plane (git / Mattermost / buildbot / gnunet) near mail; VeciGate edge; rest = company def _zone(st: dict) -> str: return str((st.get("network") or {}).get("zone") or st.get("zone") or "").lower() firefly_services = [st for st in services if _zone(st) == "firefly"] vecigate_services = [st for st in services if _zone(st) == "vecigate"] other_services = [ st for st in services if _zone(st) not in ("firefly", "vecigate") ] service_cards = [_render_card(st, section="service") for st in other_services] firefly_cards = [_render_card(st, section="service") for st in firefly_services] vecigate_cards = [_render_card(st, section="service") for st in vecigate_services] mail_cards = [_render_card(st, section="mail") for st in mail] score = doc.get("score") if isinstance(doc.get("score"), dict) else None if not score: score = L.compute_total_score(stacks, services, mail) s_light = str(score.get("light") or "white") s_green = int(score.get("green") or 0) s_total = int(score.get("total") or 0) s_pct = int(score.get("pct") or 0) score_html = ( f'
    ' f'' f'{s_green}/{s_total} green' f'({s_pct}%) · {_esc(_label(s_light))}' f"
    " ) orphan_html = "" if orphans: items = "".join( f'
  • {_esc(o.get("host"))} · {_esc(o.get("reason") or "orphan")}
  • ' for o in orphans[:40] ) orphan_html = f'

    orphans

    ' ports_html = "" sus_all = ports_doc.get("suspicious") or [] if ports_doc.get("enabled") and sus_all: n_hard = int(ports_doc.get("n_hard") or 0) items = "".join( f'
  • ' f'{_esc(s.get("label") or s.get("port"))}' f'{(" @ " + _esc(s.get("host"))) if s.get("host") else ""}' f' · {_esc(s.get("severity") or "soft")}
  • ' for s in sus_all[:60] ) ports_html = ( f'

    suspicious ports ({len(sus_all)}' f'{f", {n_hard} hard" if n_hard else ""}) · whitelist: ports-whitelist.conf

    ' f'' ) sections = [] sections.append("

    taler stacks

    ") empty = '

    No stacks.

    ' sections.append(f'
    {"".join(stack_cards) if stack_cards else empty}
    ') if service_cards: sections.append('

    company / services

    ') sections.append(f'
    {"".join(service_cards)}
    ') if firefly_cards or mail_cards: sections.append('

    firefly plane · git / Mattermost / buildbot / mail

    ') if firefly_cards: sections.append(f'
    {"".join(firefly_cards)}
    ') if mail_cards: sections.append(f'
    {"".join(mail_cards)}
    ') if vecigate_cards: sections.append('

    VeciGate edge · WAN / tortuna companion ports

    ') sections.append(f'
    {"".join(vecigate_cards)}
    ') apps = published.get("apps") or [] apps_html = "" if apps: chips = [] for app in apps[:12]: if not isinstance(app, dict): continue logo_key = str(app.get("logo") or app.get("kind") or "webui") logo_fn = _LOGO_SVG.get(logo_key) or _LOGO_SVG.get("webui") logo = logo_fn() if callable(logo_fn) else _logo_svg(logo_key, fill="#0d9488", text="app", size="14") pub = str(app.get("published_upstream") or app.get("published") or "?") light = str(app.get("light") or "green") tip = " · ".join( x for x in ( app.get("label") or app.get("package"), app.get("package"), app.get("published"), published.get("suite") or "", app.get("source") or "deb.taler.net", ) if x ) name = str(app.get("label") or app.get("package") or "app") chips.append( f'' f"{logo}" f'{_esc(name)} {_esc(pub)}' f"" ) suite = published.get("suite") or "" apps_html = ( f'

    published apps / SPAs' f'{(" · " + _esc(suite)) if suite else ""} · deb.taler.net

    ' f'
    {"".join(chips)}
    ' ) if apps_html: sections.append(apps_html) if ports_html: sections.append(ports_html) if orphan_html: sections.append(orphan_html) result_index_html = _render_result_index(doc) return f""" LLM Lightweight Monitoring · stacks

    LLM · stacks

    {score_html}
    updated {_esc(updated)} · Lightweight Monitoring
    {''.join(sections)} {result_index_html}

    detail

    """ def main() -> int: ap = argparse.ArgumentParser(description="Render stack status HTML from JSON") ap.add_argument("--status", type=Path, default=None) ap.add_argument("--out", type=Path, required=True) args = ap.parse_args() if L.load_monitoring_env(): L.refresh_runtime_flags_from_env() doc = L.read_status(args.status) if not doc: doc = {"schema": L.SCHEMA, "ts": time.time(), "stacks": [], "legend": L.LEGEND} html_text = render_html(doc) args.out.parent.mkdir(parents=True, exist_ok=True) tmp = args.out.with_name(f".{args.out.name}.tmp.{os.getpid()}") try: tmp.write_text(html_text, encoding="utf-8") os.replace(tmp, args.out) finally: if tmp.exists(): try: tmp.unlink() except OSError: pass print(args.out) return 0 if __name__ == "__main__": raise SystemExit(main())