diff --git a/solstone/apps/home/health_glance.py b/solstone/apps/home/health_glance.py
new file mode 100644
index 000000000..8d6ff4df6
--- /dev/null
+++ b/solstone/apps/home/health_glance.py
@@ -0,0 +1,156 @@
+# SPDX-License-Identifier: AGPL-3.0-only
+# Copyright (c) 2026 sol pbc
+
+from __future__ import annotations
+
+import logging
+from typing import Any
+
+from solstone.apps.home.needs_you import format_degraded_capture_line
+
+logger = logging.getLogger(__name__)
+
+_HEALTH_DETAIL_HREF = "/app/health#focus=recent-errors&day=today"
+
+
+def build_health_glance(
+ capture_health: Any,
+ pipeline_status: Any,
+ last_observe_relative: str | None,
+) -> dict:
+ issues = []
+
+ capture_issue = _issue_safely(
+ "capture health", capture_health, _build_capture_issue
+ )
+ if capture_issue is not None:
+ issues.append(capture_issue)
+
+ pipeline_issue = _issue_safely(
+ "pipeline status", pipeline_status, _build_pipeline_issue
+ )
+ if pipeline_issue is not None:
+ issues.append(pipeline_issue)
+
+ if issues:
+ severity = (
+ "red" if any(issue["severity"] == "red" for issue in issues) else "amber"
+ )
+ count = len(issues)
+ headline = (
+ "1 thing needs your attention"
+ if count == 1
+ else f"{count} things need your attention"
+ )
+ return {
+ "verdict": "attention",
+ "severity": severity,
+ "headline": headline,
+ "last_observation": None,
+ "cta": None,
+ "issues": issues,
+ }
+
+ status = _observer_state(capture_health)
+ if status == "active":
+ return {
+ "verdict": "ok",
+ "severity": "green",
+ "headline": "everything's working",
+ "last_observation": last_observe_relative,
+ "cta": None,
+ "issues": [],
+ }
+ if status == "no_observers":
+ return {
+ "verdict": "ok",
+ "severity": "green",
+ "headline": "no observers yet",
+ "last_observation": None,
+ "cta": {"text": "set one up →", "href": "/app/observer/"},
+ "issues": [],
+ }
+ return {
+ "verdict": "unavailable",
+ "severity": "amber",
+ "headline": "observer status unavailable",
+ "last_observation": None,
+ "cta": None,
+ "issues": [],
+ }
+
+
+def _issue_safely(label: str, value: Any, builder: Any) -> dict | None:
+ try:
+ return builder(value)
+ except Exception:
+ logger.warning("omitting malformed %s signal", label, exc_info=True)
+ return None
+
+
+def _build_capture_issue(capture_health: Any) -> dict | None:
+ if not isinstance(capture_health, dict):
+ return None
+ status = capture_health.get("status")
+ if status == "degraded":
+ text = format_degraded_capture_line(capture_health)
+ if not text:
+ text = "an observer isn't reaching your journal"
+ return {"text": text, "severity": "red", "href": "/app/health"}
+ if status == "offline":
+ return {
+ "text": "no observer is reaching your journal",
+ "severity": "red",
+ "href": "/app/health",
+ }
+ if status == "stale":
+ name = _first_stale_observer_name(capture_health.get("observers"))
+ text = (
+ f"{name} hasn't reported recently"
+ if name
+ else "an observer hasn't reported recently"
+ )
+ return {"text": text, "severity": "amber", "href": "/app/health"}
+ return None
+
+
+def _build_pipeline_issue(pipeline_status: Any) -> dict | None:
+ if not isinstance(pipeline_status, dict) or not pipeline_status:
+ return None
+ headline = pipeline_status.get("headline")
+ text = headline.strip() if isinstance(headline, str) else ""
+ if not text:
+ text = "processing is behind"
+ return {
+ "text": text,
+ "severity": "amber",
+ "href": _pipeline_href(pipeline_status.get("suggested_action")),
+ }
+
+
+def _pipeline_href(suggested_action: Any) -> str:
+ if suggested_action == "open_support":
+ return "/app/support"
+ return _HEALTH_DETAIL_HREF
+
+
+def _first_stale_observer_name(observers: Any) -> str | None:
+ if not isinstance(observers, list):
+ return None
+ for observer in observers:
+ if not isinstance(observer, dict) or observer.get("status") != "stale":
+ continue
+ name = observer.get("name")
+ if isinstance(name, str) and name.strip():
+ return name.strip()
+ return None
+ return None
+
+
+def _observer_state(capture_health: Any) -> str:
+ if not isinstance(capture_health, dict):
+ return "unknown"
+ status = capture_health.get("status")
+ if status in {"active", "no_observers"}:
+ return status
+ return "unknown"
diff --git a/solstone/apps/home/needs_you.py b/solstone/apps/home/needs_you.py
index 3cff0bf9f..97e8e331b 100644
--- a/solstone/apps/home/needs_you.py
+++ b/solstone/apps/home/needs_you.py
@@ -38,19 +38,9 @@ class NeedsYouItem:
def classify_needs_you(
attention: Any,
pulse_needs: list[Any],
- capture_health: dict | None = None,
) -> list[NeedsYouItem]:
items: list[NeedsYouItem] = []
- if capture_health is not None:
- degraded_line = format_degraded_capture_line(capture_health)
- if degraded_line:
- items.append(
- NeedsYouItem(
- text=degraded_line, kind="route", payload={"href": "/app/health"}
- )
- )
-
if attention:
item = _classify_safely("attention", attention, _classify_attention)
if item is not None:
diff --git a/solstone/apps/home/routes.py b/solstone/apps/home/routes.py
index 0c6ec4407..44cea4cc6 100644
--- a/solstone/apps/home/routes.py
+++ b/solstone/apps/home/routes.py
@@ -17,10 +17,8 @@ logger = logging.getLogger(__name__)
import frontmatter
from flask import Blueprint, jsonify, render_template
-from solstone.apps.home.needs_you import (
- classify_needs_you,
- format_degraded_capture_line,
-)
+from solstone.apps.home.health_glance import build_health_glance
+from solstone.apps.home.needs_you import classify_needs_you
from solstone.convey.apps import _resolve_attention
from solstone.convey.bridge import get_cached_state
from solstone.convey.utils import DATE_RE, format_date, relative_time
@@ -655,53 +653,6 @@ def _format_heatmap_summary(stats_data: dict[str, Any]) -> str | None:
return "I watched most closely during " + " · ".join(ranges) + "."
-def _format_capture_vitals_text(capture: dict[str, Any], now: datetime) -> str:
- """Return compact owner-facing observer status for the home vitals strip."""
- status = capture.get("status")
- if status == "no_observers":
- return "observer no observers"
- if status == "degraded":
- return (
- format_degraded_capture_line(capture)
- or "an observer isn't reaching your journal"
- )
-
- observers = [
- observer
- for observer in capture.get("observers", [])
- if observer.get("status") in {"stale", "offline"}
- ]
- seen_observers = [
- observer
- for observer in observers
- if isinstance(observer.get("last_seen"), (int, float))
- ]
- if seen_observers:
- names = [
- str(observer.get("name") or "observer").strip() or "observer"
- for observer in seen_observers
- ]
- last_seen = max(float(observer["last_seen"]) for observer in seen_observers)
- delta_seconds = max(0.0, (now.timestamp() * 1000 - last_seen) / 1000)
- label = "observer" if len(names) == 1 else "observers"
- joined_names = ", ".join(names[:2])
- if len(names) > 2:
- joined_names += f", +{len(names) - 2}"
- return (
- f"{label} {joined_names} last reported {relative_time(delta_seconds)} ago"
- )
-
- if observers:
- names = [
- str(observer.get("name") or "observer").strip() or "observer"
- for observer in observers
- ]
- label = "observer" if len(names) == 1 else "observers"
- return f"{label} {', '.join(names[:2])} has not reported yet"
-
- return f"observer {status or 'unknown'}"
-
-
def _format_gap_links(
pipeline_summary: dict[str, Any],
knowledge_graph: dict[str, Any],
@@ -925,8 +876,6 @@ def _build_pulse_context() -> dict[str, Any]:
journal_age_days = _count_journal_age_days(today)
capture_health = get_capture_health()
- capture_status = capture_health["status"]
- capture_display_text = _format_capture_vitals_text(capture_health, now)
cached = get_cached_state()
last_observe_ts = cached.get("last_observe_ts")
attention = _resolve_attention(get_current())
@@ -934,8 +883,6 @@ def _build_pulse_context() -> dict[str, Any]:
stats_data = _load_stats(today)
stats = stats_data.get("stats", {})
segment_count = stats.get("transcript_segments", 0)
- duration_seconds = stats.get("transcript_duration", 0)
- duration_minutes = round(duration_seconds / 60) if duration_seconds else 0
facet_data = stats_data.get("facet_data", {})
flow_content, flow_mtime = _load_flow_md(today)
@@ -1002,9 +949,7 @@ def _build_pulse_context() -> dict[str, Any]:
today_summary_parts.append(f"{n} {'activities' if n != 1 else 'activity'}")
today_summary = ", ".join(today_summary_parts)
- needs_you_items = classify_needs_you(
- attention, pulse_needs, capture_health=capture_health
- )
+ needs_you_items = classify_needs_you(attention, pulse_needs)
needs_count = len(needs_you_items)
needs_summary = ""
if needs_count:
@@ -1043,19 +988,19 @@ def _build_pulse_context() -> dict[str, Any]:
summary = read_steward_summary()
if summary:
pipeline_status = {**pipeline_status, **summary}
+ health_glance = build_health_glance(
+ capture_health, pipeline_status, last_observe_relative
+ )
yesterday_processing = _summarize_yesterday_processing(yesterday, journal_age_days)
return {
"today": today,
"now": now,
- "capture_status": capture_status,
- "capture_display_text": capture_display_text,
- "last_observe_relative": last_observe_relative,
+ "health_glance": health_glance,
"attention": attention,
"pipeline_status": pipeline_status,
"segment_count": segment_count,
- "duration_minutes": duration_minutes,
"facet_data": facet_data,
"narrative_content": narrative_content,
"narrative_updated_at": narrative_updated_at,
diff --git a/solstone/apps/home/workspace.html b/solstone/apps/home/workspace.html
index c295d5559..4e47dfb2b 100644
--- a/solstone/apps/home/workspace.html
+++ b/solstone/apps/home/workspace.html
@@ -46,12 +46,9 @@
top: 0px;
}
-.pulse-vitals-dot.active { background: #4ade80; }
-.pulse-vitals-dot.stale { background: #fbbf24; }
-.pulse-vitals-dot.offline { background: #f87171; }
-.pulse-vitals-dot.degraded { background: #dc2626; }
-.pulse-vitals-dot.unknown { background: #9ca3af; }
-.pulse-vitals-degraded { color: #b91c1c; }
+.pulse-vitals-dot.green { background: #4ade80; }
+.pulse-vitals-dot.amber { background: #fbbf24; }
+.pulse-vitals-dot.red { background: #dc2626; }
.pulse-vitals-sep {
width: 1px;
@@ -76,38 +73,28 @@
color: var(--orange-ink);
}
-.pulse-vitals-attention {
- color: #dc2626;
- font-weight: 500;
-}
-
-.pulse-vitals-headline {
+.pulse-vitals-verdict {
font-weight: 600;
font-size: 0.8rem;
- color: #b45309;
-}
-
-.pulse-vitals-action {
- display: flex;
- flex-direction: column;
- align-items: flex-start;
- gap: 0.2rem;
- cursor: pointer;
-}
-
-.pulse-vitals-action .pulse-vitals-item::before {
- content: "○";
- color: #cbd5e1;
- flex-shrink: 0;
- width: 0.75rem;
- text-align: center;
}
+.pulse-vitals-verdict.green { color: #166534; }
+.pulse-vitals-verdict.amber { color: #b45309; }
+.pulse-vitals-verdict.red { color: #b91c1c; }
-.pulse-vitals-action-link {
- color: var(--orange-ink);
- font-size: 0.75rem;
+.pulse-vitals-chip {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.25rem;
+ font-size: 0.78rem;
+ padding: 0.15rem 0.5rem;
+ border-radius: 999px;
+ border: 1px solid transparent;
text-decoration: none;
}
+.pulse-vitals-chip.red { color: #b91c1c; background: #fef2f2; border-color: #fecaca; }
+.pulse-vitals-chip.amber { color: #b45309; background: #fffbeb; border-color: #fde68a; }
+
+.pulse-vitals-cta { color: var(--orange-ink); text-decoration: none; }
/* Section Headers */
.pulse-section-header {
@@ -833,49 +820,28 @@
+ {% set health_glance = health_glance|default({}) %}
- {% set dot_class = 'active' if capture_status == 'active' else ('stale' if capture_status == 'stale' else ('offline' if capture_status == 'offline' else ('degraded' if capture_status == 'degraded' else 'unknown'))) %}
-
-
- {{ capture_display_text }}
-
- {% if segment_count > 0 %}
-
- {{ segment_count }} segment{{ 's' if segment_count != 1 else '' }}{% if duration_minutes > 0 %} · {{ duration_minutes }}m{% endif %}
+
+ {{ health_glance.headline }}
+ {% if health_glance.verdict == 'attention' %}
+ {% for issue in health_glance.issues %}
+
{{ issue.text }} →
+ {% endfor %}
+ {% else %}
+ {% if health_glance.last_observation %}
+
+
last observation {{ health_glance.last_observation }}
+ {% endif %}
+ {% if health_glance.cta %}
+
+
{{ health_glance.cta.text }}
+ {% endif %}
{% endif %}
- {% if last_observe_relative %}
-
-
- Last observation {{ last_observe_relative }}
-
- {% endif %}
- {% if attention %}
-
-
- {% endif %}
- {% if pipeline_status %}
-
-
- {% if pipeline_status.headline %}
{{ pipeline_status.headline }}
{% endif %}
-
{{ pipeline_status.summary_sentence or pipeline_status.message }}
- {% if pipeline_status.suggested_action == 'reprocess_stale' %}
-
review stale items →
- {% elif pipeline_status.suggested_action == 'open_support' %}
-
open a support ticket →
- {% elif pipeline_status.suggested_action == 'open_health_detail' or not pipeline_status.suggested_action %}
-
view details →
- {% endif %}
-
- {% endif %}
-
health →
-
+
health →
+
{% if yesterday_processing %}
@@ -1381,40 +1347,25 @@
.then(data => {
const el = document.getElementById('pulse-vitals');
if (!el) return;
- if (typeof data.capture_status !== 'string') {
+ var g = data.health_glance;
+ if (!g || typeof g !== 'object') {
throw malformedHomeResponse('/app/home/api/pulse', 'Malformed pulse response');
}
renderNeedsYouFromPulse(data);
clearPulseRefreshError('pulse-vitals');
- let dotClass = data.capture_status === 'active' ? 'active' : (data.capture_status === 'stale' ? 'stale' : (data.capture_status === 'offline' ? 'offline' : (data.capture_status === 'degraded' ? 'degraded' : 'unknown')));
- let displayStatus = data.capture_status === 'no_observers' ? 'no observers' : data.capture_status;
- let captureText = data.capture_display_text || ('observer ' + displayStatus);
- var captureTextClass = data.capture_status === 'degraded' ? ' class="pulse-vitals-degraded"' : '';
- let html = '' + esc(captureText) + '
';
- if (data.segment_count > 0) {
- html += '' + data.segment_count + ' segment' + (data.segment_count !== 1 ? 's' : '');
- if (data.duration_minutes > 0) html += ' · ' + data.duration_minutes + 'm';
- html += '
';
- }
- if (data.last_observe_relative) {
- html += 'last observation ' + esc(data.last_observe_relative) + '
';
- }
- if (data.attention) {
- html += '';
- }
- if (data.pipeline_status) {
- var ps = data.pipeline_status;
- var headlineHtml = ps.headline ? '' + esc(ps.headline) + '
' : '';
- var sentence = ps.summary_sentence || ps.message || '';
- var actionHtml = '';
- if (ps.suggested_action === 'reprocess_stale') {
- actionHtml = 'review stale items →';
- } else if (ps.suggested_action === 'open_support') {
- actionHtml = 'open a support ticket →';
- } else if (ps.suggested_action === 'open_health_detail' || !ps.suggested_action) {
- actionHtml = 'view details →';
+ var sev = g.severity || 'amber';
+ var html = '' + esc(g.headline || '') + '
';
+ if (g.verdict === 'attention') {
+ (g.issues || []).forEach(function(issue) {
+ html += '' + esc(issue.text || '') + ' →';
+ });
+ } else {
+ if (g.last_observation) {
+ html += 'last observation ' + esc(g.last_observation) + '
';
+ }
+ if (g.cta) {
+ html += '' + esc(g.cta.text || '') + '';
}
- html += '' + headlineHtml + '
' + esc(sentence) + '
' + actionHtml + '
';
}
html += 'health →';
el.innerHTML = html;
diff --git a/tests/test_home_health_glance.py b/tests/test_home_health_glance.py
new file mode 100644
index 000000000..5cd9dd0a3
--- /dev/null
+++ b/tests/test_home_health_glance.py
@@ -0,0 +1,265 @@
+# SPDX-License-Identifier: AGPL-3.0-only
+# Copyright (c) 2026 sol pbc
+
+from __future__ import annotations
+
+import re
+from datetime import datetime
+
+import pytest
+
+from solstone.apps.home.health_glance import build_health_glance
+
+BANNED_RE = re.compile(
+ r"\b(watch|capture|record|monitor|track|collect)\b",
+ re.IGNORECASE,
+)
+HEALTH_DETAIL_HREF = "/app/health#focus=recent-errors&day=today"
+
+
+def _june_22_ms() -> float:
+ return datetime(2026, 6, 22, 12, 0, 0).timestamp() * 1000
+
+
+def _degraded_observer(name: str) -> dict:
+ return {
+ "name": name,
+ "status": "degraded",
+ "ingest_rejection": {
+ "reason_code": "ingest_contract_invalid",
+ "active_count": 79,
+ "first_ts": _june_22_ms(),
+ "latest_ts": _june_22_ms(),
+ "summary": "screen.jsonl:2: value is invalid",
+ "stream": name,
+ "version": "0.3.1",
+ "segment": "20260622/120000_300",
+ },
+ }
+
+
+def _degraded_capture(*names: str) -> dict:
+ return {
+ "status": "degraded",
+ "observers": [_degraded_observer(name) for name in names or ("fedora",)],
+ }
+
+
+def _active_capture() -> dict:
+ return {"status": "active", "observers": [{"name": "fedora", "status": "active"}]}
+
+
+def test_degraded_capture_returns_red_attention_issue():
+ result = build_health_glance(_degraded_capture("fedora"), None, None)
+
+ assert result["verdict"] == "attention"
+ assert result["severity"] == "red"
+ assert len(result["issues"]) == 1
+ assert result["issues"][0]["severity"] == "red"
+ assert result["issues"][0]["href"] == "/app/health"
+ assert "fedora" in result["issues"][0]["text"]
+ assert result["headline"] != "everything's working"
+
+
+def test_degraded_capture_collapses_multiple_observers_to_one_issue():
+ result = build_health_glance(
+ _degraded_capture("fedora", "phone", "tablet"), None, None
+ )
+
+ assert len(result["issues"]) == 1
+ assert "and 2 more" in result["issues"][0]["text"]
+
+
+def test_degraded_capture_and_pipeline_warning_returns_two_issues_red_verdict():
+ result = build_health_glance(
+ _degraded_capture("fedora"),
+ {"status": "warning", "headline": "processing needs attention"},
+ None,
+ )
+
+ assert result["verdict"] == "attention"
+ assert result["severity"] == "red"
+ assert len(result["issues"]) == 2
+ assert result["headline"] != "everything's working"
+
+
+def test_pipeline_warning_alone_returns_amber_attention_issue():
+ result = build_health_glance(
+ _active_capture(),
+ {"status": "warning", "headline": "processing needs attention"},
+ None,
+ )
+
+ assert result["verdict"] == "attention"
+ assert result["severity"] == "amber"
+ assert len(result["issues"]) == 1
+ assert result["issues"][0]["href"] == HEALTH_DETAIL_HREF
+ assert result["headline"] != "everything's working"
+
+
+def test_pipeline_warning_without_headline_uses_fallback_text():
+ result = build_health_glance(
+ _active_capture(),
+ {"status": "warning", "message": "some bullet"},
+ None,
+ )
+
+ assert result["verdict"] != "ok"
+ assert result["headline"] != "everything's working"
+ assert result["issues"][0]["text"] == "processing is behind"
+
+
+@pytest.mark.parametrize(
+ "capture_health",
+ [
+ {"status": "offline", "observers": []},
+ {"status": "stale", "observers": [{"name": "fedora", "status": "stale"}]},
+ ],
+)
+def test_offline_and_stale_capture_do_not_return_ok(capture_health):
+ result = build_health_glance(capture_health, None, None)
+
+ assert result["verdict"] != "ok"
+ assert result["headline"] != "everything's working"
+
+
+def test_active_capture_without_pipeline_returns_ok_with_last_observation():
+ result = build_health_glance(_active_capture(), None, "5m ago")
+
+ assert result["verdict"] == "ok"
+ assert result["severity"] == "green"
+ assert result["headline"] == "everything's working"
+ assert result["last_observation"] == "5m ago"
+ assert result["cta"] is None
+
+
+def test_no_observers_returns_ok_with_setup_cta():
+ result = build_health_glance(
+ {"status": "no_observers", "observers": []}, None, None
+ )
+
+ assert result["verdict"] == "ok"
+ assert result["severity"] == "green"
+ assert result["headline"] == "no observers yet"
+ assert result["last_observation"] is None
+ assert result["cta"] == {"text": "set one up →", "href": "/app/observer/"}
+
+
+def test_unknown_observer_state_returns_unavailable():
+ result = build_health_glance({"status": "unknown", "observers": []}, None, None)
+
+ assert result["verdict"] == "unavailable"
+ assert result["severity"] == "amber"
+ assert result["headline"] == "observer status unavailable"
+ assert result["verdict"] != "ok"
+ assert result["headline"] != "everything's working"
+
+
+def test_pipeline_open_support_action_routes_to_support():
+ result = build_health_glance(
+ _active_capture(),
+ {
+ "status": "warning",
+ "headline": "processing needs attention",
+ "suggested_action": "open_support",
+ },
+ None,
+ )
+
+ assert result["issues"][0]["href"] == "/app/support"
+
+
+@pytest.mark.parametrize(
+ "pipeline_status",
+ [
+ {
+ "status": "warning",
+ "headline": "processing needs attention",
+ "suggested_action": "reprocess_stale",
+ },
+ {
+ "status": "warning",
+ "headline": "processing needs attention",
+ "suggested_action": "open_health_detail",
+ },
+ {
+ "status": "warning",
+ "headline": "processing needs attention",
+ "suggested_action": "none",
+ },
+ {"status": "warning", "headline": "processing needs attention"},
+ ],
+)
+def test_pipeline_health_actions_route_to_recent_errors(pipeline_status):
+ result = build_health_glance(_active_capture(), pipeline_status, None)
+
+ assert result["issues"][0]["href"] == HEALTH_DETAIL_HREF
+
+
+def test_all_issue_and_cta_hrefs_are_local_paths():
+ states = [
+ build_health_glance(_degraded_capture("fedora"), None, None),
+ build_health_glance({"status": "offline", "observers": []}, None, None),
+ build_health_glance(
+ {"status": "stale", "observers": [{"name": "fedora", "status": "stale"}]},
+ None,
+ None,
+ ),
+ build_health_glance(
+ _active_capture(),
+ {
+ "status": "warning",
+ "headline": "processing needs attention",
+ "suggested_action": "open_support",
+ },
+ None,
+ ),
+ build_health_glance({"status": "no_observers", "observers": []}, None, None),
+ ]
+
+ for state in states:
+ for issue in state["issues"]:
+ assert issue["href"].startswith("/")
+ assert not issue["href"].startswith("//")
+ if state["cta"] is not None:
+ assert state["cta"]["href"].startswith("/")
+ assert not state["cta"]["href"].startswith("//")
+
+
+def test_malformed_pipeline_drops_only_pipeline_issue():
+ result = build_health_glance(_degraded_capture("fedora"), "warning", None)
+
+ assert result["verdict"] == "attention"
+ assert result["severity"] == "red"
+ assert len(result["issues"]) == 1
+ assert result["issues"][0]["severity"] == "red"
+
+
+def test_owner_facing_strings_use_allowed_terms():
+ states = [
+ build_health_glance(_degraded_capture("fedora"), None, None),
+ build_health_glance({"status": "offline", "observers": []}, None, None),
+ build_health_glance(
+ {"status": "stale", "observers": [{"name": "fedora", "status": "stale"}]},
+ None,
+ None,
+ ),
+ build_health_glance(_active_capture(), None, "5m ago"),
+ build_health_glance({"status": "no_observers", "observers": []}, None, None),
+ build_health_glance({"status": "unknown", "observers": []}, None, None),
+ build_health_glance(
+ _active_capture(),
+ {"status": "warning", "headline": "processing needs attention"},
+ None,
+ ),
+ ]
+
+ for state in states:
+ strings = [state["headline"]]
+ if state["last_observation"] is not None:
+ strings.append(state["last_observation"])
+ if state["cta"] is not None:
+ strings.append(state["cta"]["text"])
+ strings.extend(issue["text"] for issue in state["issues"])
+ for text in strings:
+ assert BANNED_RE.findall(text) == []
diff --git a/tests/test_home_needs_you.py b/tests/test_home_needs_you.py
index 7bedb78a3..5f8f99b28 100644
--- a/tests/test_home_needs_you.py
+++ b/tests/test_home_needs_you.py
@@ -128,42 +128,10 @@ def test_format_degraded_capture_line_fallbacks_and_non_degraded():
assert format_degraded_capture_line({"status": "active", "observers": []}) is None
-def test_classify_needs_you_prepends_degraded_route_item():
- capture = _degraded_capture()
- line = format_degraded_capture_line(capture)
-
- items = classify_needs_you(
- {"placeholder_text": "Pipeline needs review"},
- ["Review the launch checklist"],
- capture_health=capture,
- )
-
- assert items[0] == NeedsYouItem(
- text=line,
- kind="route",
- payload={"href": "/app/health"},
- )
- assert [item.text for item in items] == [
- line,
- "Pipeline needs review",
- "Review the launch checklist",
- ]
-
-
-def test_classify_needs_you_does_not_add_health_route_without_degraded_capture():
- healthy_items = classify_needs_you(
- None,
- ["Review the launch checklist"],
- capture_health={"status": "active", "observers": []},
- )
- none_items = classify_needs_you(
- None,
- ["Review the launch checklist"],
- capture_health=None,
- )
+def test_classify_needs_you_no_longer_emits_capture_route():
+ items = classify_needs_you({"placeholder_text": "x"}, ["y"])
- assert all(item.payload != {"href": "/app/health"} for item in healthy_items)
- assert all(item.payload != {"href": "/app/health"} for item in none_items)
+ assert all(item.payload != {"href": "/app/health"} for item in items)
def test_classify_needs_you_warns_and_omits_malformed(caplog):
diff --git a/tests/test_home_reflections.py b/tests/test_home_reflections.py
index 90ec6d0ac..4ccfcf531 100644
--- a/tests/test_home_reflections.py
+++ b/tests/test_home_reflections.py
@@ -19,12 +19,17 @@ def _minimal_pulse_context(latest_weekly_reflection):
return {
"today": "20260310",
"now": datetime(2026, 3, 10, 12, 0, 0),
- "capture_status": "active",
- "last_observe_relative": None,
+ "health_glance": {
+ "verdict": "ok",
+ "severity": "green",
+ "headline": "everything's working",
+ "last_observation": None,
+ "cta": None,
+ "issues": [],
+ },
"attention": None,
"pipeline_status": None,
"segment_count": 0,
- "duration_minutes": 0,
"facet_data": {},
"narrative_content": None,
"narrative_updated_at": None,
diff --git a/tests/test_home_routes.py b/tests/test_home_routes.py
index d0200290d..bb02cd27d 100644
--- a/tests/test_home_routes.py
+++ b/tests/test_home_routes.py
@@ -7,90 +7,10 @@ from datetime import datetime
import pytest
-from solstone.apps.home.routes import _format_capture_vitals_text
from solstone.convey import create_app
from solstone.think.day_accumulator import append_record
-def _june_22_ms() -> float:
- return datetime(2026, 6, 22, 12, 0, 0).timestamp() * 1000
-
-
-def _degraded_capture(name: str = "fedora") -> dict:
- return {
- "status": "degraded",
- "observers": [
- {
- "name": name,
- "status": "degraded",
- "ingest_rejection": {
- "reason_code": "ingest_contract_invalid",
- "active_count": 79,
- "first_ts": _june_22_ms(),
- "latest_ts": _june_22_ms(),
- "summary": "screen.jsonl:2: value is invalid",
- "stream": name,
- "version": "0.3.1",
- },
- }
- ],
- }
-
-
-def test_format_capture_vitals_text_degraded_single():
- assert (
- _format_capture_vitals_text(_degraded_capture(), datetime.now())
- == "fedora isn't reaching your journal — 79 rejected since jun 22"
- )
-
-
-def test_format_capture_vitals_text_degraded_multiple():
- capture = _degraded_capture()
- capture["observers"].append(
- {
- "name": "phone",
- "status": "degraded",
- "ingest_rejection": {
- "reason_code": "ingest_contract_invalid",
- "active_count": 2,
- "first_ts": _june_22_ms(),
- "latest_ts": _june_22_ms(),
- "summary": "screen.jsonl:2: value is invalid",
- "stream": "phone",
- "version": None,
- },
- }
- )
-
- assert (
- _format_capture_vitals_text(capture, datetime.now())
- == "fedora isn't reaching your journal — 79 rejected since jun 22, and 1 more"
- )
-
-
-def test_format_capture_vitals_text_degraded_without_usable_observer():
- result = _format_capture_vitals_text(
- {
- "status": "degraded",
- "observers": [{"name": "fedora", "status": "degraded"}],
- },
- datetime.now(),
- )
-
- assert result == "an observer isn't reaching your journal"
- assert "since None" not in result
-
-
-def test_format_capture_vitals_text_active_unchanged():
- assert (
- _format_capture_vitals_text(
- {"status": "active", "observers": [{"name": "fedora", "status": "active"}]},
- datetime.now(),
- )
- == "observer active"
- )
-
-
def test_api_pulse_includes_needs_you_items_json_shape(journal_copy, monkeypatch):
import solstone.apps.home.routes as home_routes
diff --git a/tests/test_post_action_terminology.py b/tests/test_post_action_terminology.py
index 0a201d478..9f372526c 100644
--- a/tests/test_post_action_terminology.py
+++ b/tests/test_post_action_terminology.py
@@ -34,13 +34,23 @@ def _minimal_home_context() -> dict:
return {
"today": "20260524",
"now": datetime(2026, 5, 24, 12, 0),
- "capture_status": "offline",
- "capture_display_text": "observer offline",
- "last_observe_relative": None,
+ "health_glance": {
+ "verdict": "attention",
+ "severity": "red",
+ "headline": "1 thing needs your attention",
+ "last_observation": None,
+ "cta": None,
+ "issues": [
+ {
+ "text": "no observer is reaching your journal",
+ "severity": "red",
+ "href": "/app/health",
+ }
+ ],
+ },
"attention": None,
"pipeline_status": None,
"segment_count": 0,
- "duration_minutes": 0,
"facet_data": {},
"narrative_content": None,
"narrative_updated_at": None,
@@ -117,6 +127,22 @@ def test_home_needs_you_strings_use_allowed_terms(journal_copy, monkeypatch):
start = html.index('", start)
_assert_clean(html[start:end])
+ v_start = html.index('
", html.index("pulse-vitals-health-link", v_start))
+ _assert_clean(html[v_start:v_end])
+
+
+def test_home_vitals_strip_green_is_contrast_passing():
+ source = (
+ Path(__file__).resolve().parents[1]
+ / "solstone"
+ / "apps"
+ / "home"
+ / "workspace.html"
+ ).read_text(encoding="utf-8")
+
+ assert "#166534" in source
+ assert "#16a34a" not in source
def test_dashboard_truthfulness_strings_use_allowed_terms():