-
-
{{ 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 %}
-
health β
-
- {% if yesterday_processing %}
-
-
- {{ yesterday_processing.summary_line }}
-
- {% if yesterday_processing.first_week_framing %}
-
{{ yesterday_processing.first_week_framing }}
- {% endif %}
- {% if yesterday_processing.mode == 'sparse' %}
- {% for line in yesterday_processing.sparse_lines %}
-
{{ line }}
- {% endfor %}
- {% elif yesterday_processing.gap_links or yesterday_processing.details %}
-
- {% for link in yesterday_processing.gap_links %}
- {{ link.text }}
- {% endfor %}
- {% for line in yesterday_processing.details %}
- {{ line }}
- {% endfor %}
-
- {% endif %}
-
-
- {% endif %}
-
-
- {% if show_welcome %}
-
-
welcome to your journal
-
this is where your day comes together β narrative summaries, calendar events, and tasks. as sol experiences your day with you and keeps it here in your journal, sections will appear automatically.
-
check system health β
-
- {% endif %}
-
-
- {% if briefing_exists or briefing_phase == 'pending' %}
-
-
- {% if briefing_summary %}
-
{{ briefing_summary }}
- {% endif %}
- {% if briefing_phase == 'pending' and not briefing_exists %}
-
- {% if briefing_lateness.late %}
- I usually finish your briefing by ~10 am. It's {{ briefing_lateness.late_hours }}h late.
-
Check status
- {% else %}
- your morning briefing is being prepared...
- {% endif %}
-
- {% endif %}
- {% if briefing_exists %}
-
- {% for section_key in ['your_day', 'yesterday', 'needs_attention', 'forward_look', 'reading'] %}
- {% set section_content = briefing_sections.get(section_key) %}
- {% if section_key == 'needs_attention' %}
- {% if briefing_needs_deduped or briefing_needs_badge %}
-
-
needs attention
- {% if briefing_needs_deduped %}
-
-
- {% for item in briefing_needs_deduped %}
- {{ item }}
- {% endfor %}
-
-
- {% endif %}
-
- {% endif %}
- {% elif section_content %}
- {% set section_labels = {'your_day': 'your day', 'yesterday': 'yesterday', 'forward_look': 'forward look', 'reading': 'reading'} %}
-
-
{{ section_labels[section_key] }}
-
-
- {% endif %}
- {% endfor %}
-
- {% endif %}
-
- {% endif %}
-
-
- {% if narrative_content is not none %}
-
-
-
- {{ narrative_summary }}
-
-
-
-
-
tell me more β
- {% if narrative_updated_at %}
-
updated at {{ narrative_updated_at }}
- {% endif %}
-
-
- {% elif segment_count > 0 %}
-
-
-
analysis will be available after the next processing cycle.
-
- {% endif %}
- {% if narrative_content is none and segment_count == 0 and not show_welcome %}
-
- {% endif %}
-
- {% if latest_weekly_reflection %}
-
- {% endif %}
-
-
- {% if anticipated_activities or activities %}
-
-
-
- {{ today_summary }}
-
-
- {% if anticipated_activities %}
-
- {% for event in anticipated_activities %}
- {% set is_past = event.get('occurred', false) or (event.get('end', '') and event['end'] < now.strftime('%H:%M:%S')) %}
-
- {{ event.get('start', '')[:5] }}
- {{ event.get('title', 'Untitled') }}
-
- {% endfor %}
-
- {% endif %}
- {% if activities %}
-
recent activity
-
- {% for act in activities[:6] %}
-
- {{ act.get('display_time', '') }}
- {{ act.get('description', act.get('activity', '')) }}
-
- {% endfor %}
-
- {% endif %}
- {% if facet_data %}
-
- {% for fname, fdata in facet_data.items() %}
- {{ fname }} Β· {{ fdata.get('minutes', 0)|int }}m
- {% endfor %}
-
- {% endif %}
-
-
- {% endif %}
- {% if not anticipated_activities and not activities and not show_welcome %}
-
-
-
no anticipated activities or recent activity yet today.
-
- {% endif %}
-
-
- {% if needs_you_items %}
-
-
-
- {{ needs_summary }}
-
-
-
- {% for item in needs_you_items %}
- {% if item.disabled %}
-
{{ item.text }}{% if item.reason %}{{ item.reason }} {% endif %}
- {% else %}
-
{{ item.text }}
- {% endif %}
- {% endfor %}
-
-
-
- {% endif %}
- {% if not needs_you_items and not show_welcome %}
-
-
-
nothing needs your attention right now.
-
- {% endif %}
-
-
+
diff --git a/tests/test_convey_facet_cookie.py b/tests/test_convey_facet_cookie.py
index fc626a2bb..be3821ee8 100644
--- a/tests/test_convey_facet_cookie.py
+++ b/tests/test_convey_facet_cookie.py
@@ -31,14 +31,14 @@ class TestFacetCookieValidation:
def test_valid_cookie_returned(self, client):
"""Valid cookie for active facet -> returned as-is, no cookie cleared."""
client.set_cookie("selectedFacet", "montague")
- response = client.get("/app/home/")
+ response = client.get("/api/shell")
assert response.status_code == 200
assert not _cookie_deleted(response)
def test_stale_cookie_cleared(self, client, journal_copy):
"""Stale cookie (nonexistent facet) -> cookie expired, config cleared."""
client.set_cookie("selectedFacet", "nonexistent-facet")
- response = client.get("/app/home/")
+ response = client.get("/api/shell")
assert response.status_code == 200
assert _cookie_deleted(response)
@@ -52,7 +52,7 @@ class TestFacetCookieValidation:
config["facets"]["selected"] = "montague"
config_path.write_text(json.dumps(config, indent=2))
- response = client.get("/app/home/")
+ response = client.get("/api/shell")
assert response.status_code == 200
assert not _cookie_deleted(response)
@@ -60,7 +60,7 @@ class TestFacetCookieValidation:
"""Muted facet in cookie -> treated as stale, cookie expired."""
# muted-test fixture already exists in journal_copy (copied from fixtures)
client.set_cookie("selectedFacet", "muted-test")
- response = client.get("/app/home/")
+ response = client.get("/api/shell")
assert response.status_code == 200
assert _cookie_deleted(response)
@@ -70,7 +70,7 @@ class TestFacetCookieValidation:
def test_empty_cookie_cleared(self, client, journal_copy):
"""Empty string cookie -> treated as stale, cookie expired, config cleared."""
client.set_cookie("selectedFacet", "")
- response = client.get("/app/home/")
+ response = client.get("/api/shell")
assert response.status_code == 200
assert _cookie_deleted(response)
@@ -84,7 +84,7 @@ class TestFacetCookieValidation:
config["facets"]["selected"] = "nonexistent-facet"
config_path.write_text(json.dumps(config, indent=2))
- response = client.get("/app/home/")
+ response = client.get("/api/shell")
assert response.status_code == 200
config = json.loads(config_path.read_text())
diff --git a/tests/test_convey_spa_shell.py b/tests/test_convey_spa_shell.py
index af73ec4dd..bec8ef3fe 100644
--- a/tests/test_convey_spa_shell.py
+++ b/tests/test_convey_spa_shell.py
@@ -68,8 +68,8 @@ def test_api_shell_shape_ordering_label_and_backgrounds(client, journal_copy):
assert by_name["sol"]["label"] == "Ada"
assert by_name["search"]["spa"] is True
assert by_name["search"]["workspace_url"] == "/app/search/workspace"
- assert by_name["home"]["spa"] is False
- assert by_name["home"]["workspace_url"] is None
+ assert by_name["network"]["spa"] is False
+ assert by_name["network"]["workspace_url"] is None
assert by_name["timeline"]["background_url"] == "/app/timeline/background"
assert by_name["support"]["background_url"] == "/app/support/background"
assert set(payload["chat_bar"]) == {"placeholder", "attention", "sol_request"}
@@ -120,10 +120,10 @@ def test_search_spa_index_workspace_and_route_resolution(convey_app, client):
def test_unflagged_timeline_workspace_404_and_index_stays_jinja(client):
- response = client.get("/app/home/workspace")
+ response = client.get("/app/network/workspace")
assert response.status_code == 404
- index_response = client.get("/app/home/", follow_redirects=True)
+ index_response = client.get("/app/network/", follow_redirects=True)
assert index_response.status_code == 200
assert b'data-solstone-shell="spa"' not in index_response.data
diff --git a/tests/test_health_readiness_js.py b/tests/test_health_readiness_js.py
index 209a582af..6c2d7076d 100644
--- a/tests/test_health_readiness_js.py
+++ b/tests/test_health_readiness_js.py
@@ -12,7 +12,7 @@ import pytest
from solstone.convey.provider_readiness import semantic_key_for
-WORKSPACE = Path("solstone/apps/health/workspace.html")
+HEALTH_JS = Path("solstone/apps/health/static/health.js")
def _extract_function(source: str, name: str) -> str:
@@ -54,7 +54,7 @@ def test_health_readiness_js_semantic_key_glance_and_grouping():
if node is None:
pytest.skip("node is not available")
- source = WORKSPACE.read_text(encoding="utf-8")
+ source = HEALTH_JS.read_text(encoding="utf-8")
functions = "\n".join(
_extract_function(source, name)
for name in (
diff --git a/tests/test_home_needs_you.py b/tests/test_home_needs_you.py
index 5f8f99b28..a4a1c4b5a 100644
--- a/tests/test_home_needs_you.py
+++ b/tests/test_home_needs_you.py
@@ -223,42 +223,47 @@ def test_classify_needs_you_folds_confirm_to_chat():
]
-def test_unknown_kind_renders_inert():
- workspace = (
+def _home_render_js() -> str:
+ return (
Path(__file__).resolve().parents[1]
/ "solstone"
/ "apps"
/ "home"
- / "workspace.html"
+ / "static"
+ / "home.js"
).read_text(encoding="utf-8")
- dispatch_start = workspace.index("function dispatchNeedsYouItem(item)")
- init_start = workspace.index("function initHome()", dispatch_start)
- dispatch_body = workspace[dispatch_start:init_start]
+
+def test_unknown_kind_renders_inert():
+ render_js = _home_render_js()
+
+ dispatch_start = render_js.index("function dispatchNeedsYouItem(item)")
+ # The dispatch body runs until the next top-level function in the module.
+ next_fn = render_js.index("\n function ", dispatch_start + 1)
+ dispatch_body = render_js[dispatch_start:next_fn]
assert "if (item.kind === 'chat')" in dispatch_body
assert "if (item.kind === 'route')" in dispatch_body
assert "if (item.kind === 'confirm')" in dispatch_body
assert "unsupported confirm needs-you item" in dispatch_body
+ # No catch-all else β an unknown kind falls through inert.
assert "else" not in dispatch_body
def test_disabled_items_render_noninteractive():
- workspace = (
- Path(__file__).resolve().parents[1]
- / "solstone"
- / "apps"
- / "home"
- / "workspace.html"
- ).read_text(encoding="utf-8")
-
- assert "{% if item.disabled %}" in workspace
- assert "pulse-needs-item-disabled" in workspace
- assert "pulse-needs-reason" in workspace
- disabled_branch_start = workspace.index("{% if item.disabled %}")
- disabled_branch_end = workspace.index("{% else %}", disabled_branch_start)
- disabled_branch = workspace[disabled_branch_start:disabled_branch_end]
+ render_js = _home_render_js()
+
+ # The disabled needs-you item renders client-side with the reason and no
+ # interactive affordances; the interactive path carries them.
+ assert "pulse-needs-item-disabled" in render_js
+ assert "pulse-needs-reason" in render_js
+ disabled_start = render_js.index("if (item && item.disabled)")
+ disabled_end = render_js.index(
+ "return", render_js.index("return", disabled_start) + 1
+ )
+ disabled_branch = render_js[disabled_start:disabled_end]
assert 'role="button"' not in disabled_branch
assert "tabindex" not in disabled_branch
assert "data-needs-you-item" not in disabled_branch
- assert "if (item.disabled) return;" in workspace
+ # Dispatch is a no-op for a disabled item.
+ assert "if (item.disabled) return;" in render_js
diff --git a/tests/test_home_reflections.py b/tests/test_home_reflections.py
index 4ccfcf531..6474b7004 100644
--- a/tests/test_home_reflections.py
+++ b/tests/test_home_reflections.py
@@ -76,12 +76,11 @@ def test_home_shows_latest_weekly_reflection_link(monkeypatch, journal_copy):
),
)
- response = client.get("/app/home/")
- html = response.get_data(as_text=True)
+ response = client.get("/app/home/api/pulse")
+ payload = response.get_json()
assert response.status_code == 200
- assert "weekly reflection" in html
- assert 'href="/app/reflections/20260308"' in html
+ assert payload["latest_weekly_reflection"]["url"] == "/app/reflections/20260308"
def test_home_omits_weekly_reflection_card_when_missing(monkeypatch, journal_copy):
@@ -91,8 +90,8 @@ def test_home_omits_weekly_reflection_card_when_missing(monkeypatch, journal_cop
lambda: _minimal_pulse_context(None),
)
- response = client.get("/app/home/")
- html = response.get_data(as_text=True)
+ response = client.get("/app/home/api/pulse")
+ payload = response.get_json()
assert response.status_code == 200
- assert 'href="/app/reflections/' not in html
+ assert payload["latest_weekly_reflection"] is None
diff --git a/tests/test_post_action_terminology.py b/tests/test_post_action_terminology.py
index 9f372526c..a8b8acac3 100644
--- a/tests/test_post_action_terminology.py
+++ b/tests/test_post_action_terminology.py
@@ -120,16 +120,19 @@ def test_home_needs_you_strings_use_allowed_terms(journal_copy, monkeypatch):
monkeypatch.setattr(home_routes, "_build_pulse_context", _minimal_home_context)
client = create_app(str(journal_copy)).test_client()
- response = client.get("/app/home/")
+ response = client.get("/app/home/api/pulse")
assert response.status_code == 200
- html = response.get_data(as_text=True)
- 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])
+ payload = response.get_json()
+ _assert_clean(
+ json.dumps(
+ {
+ "needs_you_items": payload["needs_you_items"],
+ "health_glance": payload["health_glance"],
+ },
+ ensure_ascii=False,
+ )
+ )
def test_home_vitals_strip_green_is_contrast_passing():
@@ -153,7 +156,19 @@ def test_dashboard_truthfulness_strings_use_allowed_terms():
/ "health"
/ "workspace.html"
)
- health_source = health_workspace.read_text(encoding="utf-8")
+ health_js = (
+ Path(__file__).resolve().parents[1]
+ / "solstone"
+ / "apps"
+ / "health"
+ / "static"
+ / "health.js"
+ )
+ health_source = (
+ health_workspace.read_text(encoding="utf-8")
+ + "\n"
+ + health_js.read_text(encoding="utf-8")
+ )
health_strings = [
"couldn't check talent errors today.",
"this host's stream isn't reporting yet β showing",