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 %} -
-
-
- {{ attention.placeholder_text }} -
- view details → -
- {% 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 += '
' + esc(data.attention.placeholder_text) + '
view details →
'; - } - 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():