diff --git a/bullseye/status.py b/bullseye/status.py
new file mode 100644
index 0000000..cfc31f9
--- /dev/null
+++ b/bullseye/status.py
@@ -0,0 +1,91 @@
+"""Render bullpen-doctor --json output as a self-contained HTML fragment."""
+
+from html import escape
+
+
+def render_status(doctor_json):
+ """Return a single HTML fragment string describing the doctor's report."""
+ ts = doctor_json["ts"]
+ hosts = doctor_json["hosts"]
+ agents = doctor_json["agents"]
+ grinds = doctor_json["grinds"]
+ verdict = doctor_json["verdict"]
+ flags = doctor_json["flags"]
+
+ parts = []
+
+ # Timestamp
+ parts.append(f'
{escape(str(ts))}
')
+
+ # Hosts table
+ if hosts:
+ parts.append('')
+ for hostname in sorted(hosts):
+ info = hosts[hostname]
+ load = info["load"]
+ temp_c = info["temp_c"]
+ mem = info["mem"]
+ reachable = info["reachable"]
+
+ # None-safe rendering
+ load_str = escape(str(load)) if load is not None else "-"
+ temp_str = escape(str(temp_c)) if temp_c is not None else "-"
+ mem_str = escape(str(mem)) if mem is not None else "-"
+
+ # Hot styling
+ hot_style = ' style="background-color:#ffe0e0;font-weight:bold;"' if temp_c is not None and temp_c >= 80 else ''
+
+ unreachable_text = ' unreachable ' if not reachable else ''
+
+ parts.append(
+ f''
+ f'{escape(hostname)} '
+ f'{load_str} '
+ f'{temp_str} '
+ f'{mem_str} '
+ f'{unreachable_text} '
+ f' '
+ )
+ parts.append('
')
+ else:
+ parts.append('No hosts
')
+
+ # Agents
+ if agents:
+ parts.append('Agents:
')
+ for agent in agents:
+ name = agent["agent"]
+ model = agent["model"]
+ age = agent["age"]
+ parts.append(
+ f''
+ f'{escape(name)}: {escape(model)} ({escape(age)})'
+ f'
'
+ )
+ else:
+ parts.append('No agents
')
+
+ # Grinds
+ parts.append(f'')
+ if grinds:
+ parts.append('
Grinds: ')
+ for g in grinds:
+ parts.append(f'
{escape(g["age"])}
')
+ else:
+ parts.append('No grinds')
+ parts.append('
')
+
+ # Verdict
+ verdict_styles = {
+ "busy-healthy": ' style="color:green;font-weight:bold;"',
+ "idle": ' style="color:grey;font-weight:bold;"',
+ "needs-a-look": ' style="color:orange;font-weight:bold;"',
+ }
+ vs = verdict_styles.get(verdict, '')
+ parts.append(
+ f''
+ f'{escape(verdict)}'
+ f'
'
+ )
+
+ return ''.join(parts)
diff --git a/tests/test_bullseye_status.py b/tests/test_bullseye_status.py
new file mode 100644
index 0000000..f05ed74
--- /dev/null
+++ b/tests/test_bullseye_status.py
@@ -0,0 +1,367 @@
+# Executable spec for bullseye/status.py :: render_status(doctor_json) -> str
+#
+# Input: the dict emitted by `bullpen-doctor --json`:
+# ts (epoch int); hosts (hostname -> {load: str|None, temp_c: int|None,
+# mem: str|None, reachable: bool} — load/mem/temp_c are ALL null when the
+# host is fully unreachable); agents (list of {agent, model, pid, age});
+# grinds (list
+# of {pid, age}); verdict ("busy-healthy"|"idle"|"needs-a-look");
+# flags ({hot: list, long_jobs: int}).
+#
+# Contract (machine-checkable hooks the implementation MUST provide):
+# * Output is a single self-contained HTML fragment string. Inline style="..."
+# only — no , ": {"load": "0.10", "temp_c": 40,
+ "mem": "1/2MB &", "reachable": True},
+ },
+ "agents": [{"agent": "",
+ "model": "evil&co ", "pid": "1", "age": "9s"}],
+ "grinds": [],
+ "verdict": "idle",
+ "flags": {"hot": [], "long_jobs": 0},
+ }
+
+def one_host(temp_c):
+ """Single host whose only variable is temp_c (for the 80-degree boundary).
+ flags.hot mirrors doctor behaviour."""
+ hot = ["vulcan"] if (temp_c is not None and temp_c >= 80) else []
+ return {
+ "ts": 1784624757,
+ "hosts": {"vulcan": {"load": "0.50", "temp_c": temp_c,
+ "mem": "1000/2000MB", "reachable": True}},
+ "agents": [],
+ "grinds": [],
+ "verdict": "idle",
+ "flags": {"hot": hot, "long_jobs": 0},
+ }
+
+# ---------------------------------------------------------------- helpers
+
+MARKER = re.compile(r'data-(host|agent|verdict|grinds)\s*=\s*"([^"]*)"')
+
+def marked_chunks(html):
+ """Map (kind, value) -> chunk from that data-* marker to the next one."""
+ ms = list(MARKER.finditer(html))
+ out = {}
+ for i, m in enumerate(ms):
+ key = (m.group(1), m.group(2))
+ assert key not in out, "duplicate marker %s=%r" % key
+ end = ms[i + 1].start() if i + 1 < len(ms) else len(html)
+ out[key] = html[m.start():end]
+ return out
+
+def host_chunk(html, hostname):
+ by_key = marked_chunks(html)
+ key = ("host", escape(hostname, quote=True))
+ assert key in by_key, "no data-host marker for %r; markers: %s" \
+ % (hostname, sorted(by_key))
+ return by_key[key]
+
+def styles(chunk):
+ """Set of inline style attribute values in a chunk."""
+ found = re.findall(r"style\s*=\s*(?:\"([^\"]*)\"|'([^']*)')", chunk)
+ return frozenset((a or b).strip() for a, b in found)
+
+# --- crude color classifier: green / grey / amber / other -----------------
+
+NAMED_COLORS = {
+ "green": (0, 128, 0), "darkgreen": (0, 100, 0),
+ "forestgreen": (34, 139, 34), "seagreen": (46, 139, 87),
+ "limegreen": (50, 205, 50), "mediumseagreen": (60, 179, 113),
+ "grey": (128, 128, 128), "gray": (128, 128, 128),
+ "dimgrey": (105, 105, 105), "dimgray": (105, 105, 105),
+ "darkgrey": (169, 169, 169), "darkgray": (169, 169, 169),
+ "lightgrey": (211, 211, 211), "lightgray": (211, 211, 211),
+ "silver": (192, 192, 192), "slategray": (112, 128, 144),
+ "slategrey": (112, 128, 144),
+ "orange": (255, 165, 0), "darkorange": (255, 140, 0),
+ "goldenrod": (218, 165, 32), "gold": (255, 215, 0),
+}
+
+def classify(r, g, b):
+ if max(r, g, b) - min(r, g, b) <= 32:
+ return "grey"
+ if g > r + 16 and g > b + 16:
+ return "green"
+ if r >= 140 and r >= g and g >= b + 40 and g >= 70:
+ return "amber"
+ return "other"
+
+def hues_in(chunk):
+ """Classes of every color literal found inside style attributes."""
+ hues = set()
+ for style in styles(chunk):
+ s = style.lower()
+ for h in re.findall(r"#([0-9a-f]{6})\b", s):
+ hues.add(classify(*(int(h[i:i + 2], 16) for i in (0, 2, 4))))
+ for h in re.findall(r"#([0-9a-f]{3})\b", s):
+ hues.add(classify(*(int(c * 2, 16) for c in h)))
+ for r_, g_, b_ in re.findall(
+ r"rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)", s):
+ hues.add(classify(int(r_), int(g_), int(b_)))
+ for name, rgb in NAMED_COLORS.items():
+ if re.search(r"\b%s\b" % name, s):
+ hues.add(classify(*rgb))
+ return hues
+
+# ---------------------------------------------------------------- tests
+
+def test_returns_html_fragment_for_all_fixtures():
+ for fx in (healthy(), degraded(), idle_empty(), hostile()):
+ out = render_status(fx)
+ assert isinstance(out, str) and out.strip()
+ assert "<" in out and ">" in out
+
+def test_host_row_shows_load_temp_mem():
+ fx = healthy()
+ out = render_status(fx)
+ for name, vitals in fx["hosts"].items():
+ chunk = host_chunk(out, name)
+ assert vitals["load"] in chunk, "%s: load missing" % name
+ assert str(vitals["temp_c"]) in chunk, "%s: temp missing" % name
+ assert vitals["mem"] in chunk, "%s: mem missing" % name
+
+def test_null_temp_renders_without_crash_or_none_leak():
+ out = render_status(degraded()) # noether has temp_c=None
+ chunk = host_chunk(out, "noether")
+ assert "None" not in chunk, "str(None) leaked into host row"
+ assert "0.00" in chunk and "0/7820MB" in chunk, \
+ "load/mem must still render when temp_c is null"
+
+def test_hot_temp_marker_switches_exactly_at_80():
+ cool_lo = styles(host_chunk(render_status(one_host(45)), "vulcan"))
+ cool_hi = styles(host_chunk(render_status(one_host(79)), "vulcan"))
+ hot_edge = styles(host_chunk(render_status(one_host(80)), "vulcan"))
+ hot = styles(host_chunk(render_status(one_host(85)), "vulcan"))
+ assert hot_edge, "hot host row has no inline style at all"
+ assert cool_lo == cool_hi, \
+ "styling must not vary between cool temps 45 and 79: %s vs %s" \
+ % (sorted(cool_lo), sorted(cool_hi))
+ assert hot_edge != cool_hi, "temp_c=80 must be styled as hot (>= boundary)"
+ assert hot != cool_hi, "temp_c=85 must be styled as hot"
+
+def test_unreachable_host_carries_marker():
+ out = render_status(degraded())
+ assert "unreachable" in host_chunk(out, "noether").lower()
+ assert "unreachable" not in host_chunk(out, "boltzmann").lower(), \
+ "reachable host must not be flagged unreachable"
+
+def test_agents_show_name_model_and_age():
+ for fx in (healthy(), degraded()):
+ out = render_status(fx)
+ by_key = marked_chunks(out)
+ for a in fx["agents"]:
+ key = ("agent", escape(a["agent"], quote=True))
+ assert key in by_key, "no data-agent marker for %r" % a["agent"]
+ chunk = by_key[key]
+ assert a["model"] in chunk, "%s: model missing" % a["agent"]
+ assert a["age"] in chunk, "%s: age missing" % a["agent"]
+
+def test_grinds_count_and_ages_shown():
+ fx = degraded()
+ out = render_status(fx)
+ assert out.count('data-grinds="2"') == 1, \
+ "expected exactly one data-grinds=\"2\" marker"
+ for g in fx["grinds"]:
+ assert g["age"] in out, "grind age %r missing" % g["age"]
+
+def test_idle_empty_input_renders_empty_sections():
+ out = render_status(idle_empty())
+ by_key = marked_chunks(out)
+ kinds = [k for k, _ in by_key]
+ assert "host" not in kinds and "agent" not in kinds, \
+ "empty input must not invent host/agent rows"
+ assert ("grinds", "0") in by_key, "data-grinds=\"0\" marker missing"
+ assert ("verdict", "idle") in by_key, "data-verdict marker missing"
+
+def test_verdict_distinct_color_signal_per_verdict():
+ expected_hue = {"busy-healthy": "green", "idle": "grey",
+ "needs-a-look": "amber"}
+ signatures = {}
+ for verdict, hue in expected_hue.items():
+ fx = healthy()
+ fx["verdict"] = verdict
+ out = render_status(fx)
+ by_key = marked_chunks(out)
+ key = ("verdict", verdict)
+ assert key in by_key, "no data-verdict marker for %r" % verdict
+ chunk = by_key[key]
+ sans_marker = chunk.replace('data-verdict="%s"' % verdict, "", 1)
+ assert verdict in sans_marker, \
+ "verdict %r not visible as text" % verdict
+ sig = styles(chunk)
+ assert sig, "verdict %r element has no inline style" % verdict
+ assert hue in hues_in(chunk), \
+ "verdict %r must signal %s; styles: %s" % (verdict, hue, sorted(sig))
+ signatures[verdict] = sig
+ assert len(set(signatures.values())) == 3, \
+ "verdict styles must be pairwise distinct, got: %s" % signatures
+
+def test_html_escaping_no_raw_passthrough():
+ out = render_status(hostile())
+ assert "") in out, \
+ "escaped hostname payload missing"
+ assert escape("") in out, \
+ "escaped agent-name payload missing"
+ assert "evil" not in out and escape("evil&co ") in out, \
+ "model string not escaped"
+ assert "&" not in out and "1/2MB &<x>" in out, \
+ "mem string not escaped"
+ # markers must use the escaped value too (attribute context)
+ by_key = marked_chunks(out)
+ assert ("host", escape("", quote=True)) in by_key
+ assert ("agent", escape("", quote=True)) in by_key
+
+def test_no_external_resources_anywhere():
+ for fx in (healthy(), degraded(), idle_empty(), hostile()):
+ out = render_status(fx).lower()
+ for banned in (" ([^<>]*)<", chunk)]
+ dashes = [c for c in cells if c in ("-", "–", "—")]
+ assert len(dashes) >= 2, \
+ "null load and null mem must each render as a dash cell " \
+ "(same None-safe approach as temp_c); cell texts: %r" \
+ % [c for c in cells if c]
+ assert "1.04" in host_chunk(out, "boltzmann"), \
+ "reachable sibling host must still render its real load"
+
+def test_host_rows_wrapped_in_table_for_innerhtml():
+ for fx in (healthy(), degraded(), hostile()):
+ out = render_status(fx)
+ first_tr = out.find("")
+ assert first_tr != -1 and last_tr_close != -1, \
+ "host rows must be elements"
+ table_open = out.find("")
+ assert table_open != -1 and table_open < first_tr, \
+ "' fragment " \
+ "is dropped by innerHTML parsing in body context"
+ assert table_close > last_tr_close, \
+ "'
' must follow the last ''"