From 327d6a69ae6185447a8f49bb8583f2db7a3f087c Mon Sep 17 00:00:00 2001 From: Connor Johnstone Date: Tue, 8 Sep 2026 17:29:27 -0400 Subject: [PATCH] Add shared navigation to every page The pages were only reachable by typing URLs, and the digest had no navigation at all. A single _nav.html partial now appears on all four, marking the current page, so Triage / Digest / Tune / Config are one click apart. Parameterised test asserts every page links to the others and marks itself active, so a page added later cannot ship stranded. --- news/templates/_nav.html | 7 +++++++ news/templates/config.html | 11 +++++++++-- news/templates/digest.html | 17 ++++++++++++++--- news/templates/triage.html | 20 +++++++++++++++----- news/templates/tune.html | 8 +++++++- news/web.py | 10 ++++++---- tests/test_delivery.py | 11 +++++++++++ 7 files changed, 69 insertions(+), 15 deletions(-) create mode 100644 news/templates/_nav.html diff --git a/news/templates/_nav.html b/news/templates/_nav.html new file mode 100644 index 0000000..6f0304e --- /dev/null +++ b/news/templates/_nav.html @@ -0,0 +1,7 @@ +{#- Shared navigation. `current` marks the active page. -#} + diff --git a/news/templates/config.html b/news/templates/config.html index 17bf42c..700babe 100644 --- a/news/templates/config.html +++ b/news/templates/config.html @@ -47,14 +47,21 @@ .nearmiss { border-left:2px solid var(--warn); padding-left:.7rem; margin-top:.6rem; } .ok { color:var(--pos); } .err { color:var(--neg); } .gate { color:var(--warn); } + .sitenav { display:flex; gap:.15rem; flex-wrap:wrap; } + .sitenav a { color:var(--muted); text-decoration:none; font-size:.85rem; + padding:.2rem .6rem; border-radius:6px; border:1px solid transparent; } + .sitenav a:hover { color:var(--fg); background:var(--rule); } + .sitenav a.here { color:var(--accent); border-color:var(--rule); font-weight:600; } +

Config

-
triage · tune · - digest · {{ clusters }} clusters scored
+ {% include "_nav.html" %}
+

{{ clusters }} clusters scored

+

Try a filter

diff --git a/news/templates/digest.html b/news/templates/digest.html index bec5ea6..2b9f29f 100644 --- a/news/templates/digest.html +++ b/news/templates/digest.html @@ -13,7 +13,9 @@ BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; -webkit-text-size-adjust:100%; } main { max-width:38rem; margin:0 auto; padding:1.5rem 1.15rem 4rem; } - header { border-bottom:1px solid var(--rule); padding-bottom:.75rem; margin-bottom:1.5rem; } + header { border-bottom:1px solid var(--rule); padding-bottom:.75rem; margin-bottom:1.5rem; + display:flex; gap:1rem; flex-wrap:wrap; align-items:baseline; + justify-content:space-between; } h1 { font-size:1.3rem; margin:0 0 .15rem; letter-spacing:-.01em; } .date { color:var(--muted); font-size:.85rem; } .briefing p { margin:0 0 1rem; } @@ -30,11 +32,20 @@ nav { display:flex; justify-content:space-between; margin-top:2rem; font-size:.9rem; } nav a { color:var(--accent); text-decoration:none; } .empty { color:var(--muted); font-style:italic; } + .sitenav { display:flex; gap:.15rem; flex-wrap:wrap; } + .sitenav a { color:var(--muted); text-decoration:none; font-size:.85rem; + padding:.2rem .6rem; border-radius:6px; border:1px solid transparent; } + .sitenav a:hover { color:var(--fg); background:var(--rule); } + .sitenav a.here { color:var(--accent); border-color:var(--rule); font-weight:600; } +

-

Daily briefing

-
{{ day.strftime('%A, %-d %B %Y') }}
+
+

Daily briefing

+
{{ day.strftime('%A, %-d %B %Y') }}
+
+ {% include "_nav.html" %}
{% if briefing %} diff --git a/news/templates/triage.html b/news/templates/triage.html index 5a3ee4e..9c427f3 100644 --- a/news/templates/triage.html +++ b/news/templates/triage.html @@ -21,6 +21,8 @@ h1 { font-size:1.15rem; margin:0; letter-spacing:-.01em; } .stats, .filters { color:var(--muted); font-size:.83rem; } .filters a { color:var(--accent); text-decoration:none; margin-left:.6rem; } + .subbar { display:flex; gap:1rem; flex-wrap:wrap; justify-content:space-between; + margin:-.4rem 0 1rem; } .filters a.on { text-decoration:underline; } article { background:var(--card); border:1px solid var(--rule); border-radius:9px; @@ -54,18 +56,26 @@ border-top:1px solid var(--rule); padding:.45rem 1rem; font-size:.78rem; color:var(--muted); display:flex; gap:1rem; justify-content:center; } .empty { color:var(--muted); font-style:italic; padding:2rem 0; } + .sitenav { display:flex; gap:.15rem; flex-wrap:wrap; } + .sitenav a { color:var(--muted); text-decoration:none; font-size:.85rem; + padding:.2rem .6rem; border-radius:6px; border:1px solid transparent; } + .sitenav a:hover { color:var(--fg); background:var(--rule); } + .sitenav a.here { color:var(--accent); border-color:var(--rule); font-weight:600; } + @media (max-width:34rem) { .help { display:none; } }

Triage

-
{{ rows|length }} shown · {{ labelled }} labelled · {{ total }} clusters
-
show + {% include "_nav.html" %} +
+
+ {{ rows|length }} shown · {{ labelled }} labelled · {{ total }} clusters + show unlabelled all - · digest -
- + +
{% for r in rows %}

Tune

- + {% include "_nav.html" %}
diff --git a/news/web.py b/news/web.py index 64d241d..4ec81c8 100644 --- a/news/web.py +++ b/news/web.py @@ -136,7 +136,7 @@ async def triage(request): return TEMPLATES.TemplateResponse( request, "triage.html", {"rows": rows, "show": show, "total": len(scores), "labelled": len(labelled), - "signed": bool(secret)}) + "signed": bool(secret), "current": "triage"}) async def tune(request): @@ -196,7 +196,7 @@ async def tune(request): request, "tune.html", {"routing": routing, "counts": counts, "alerts": alerts, "digest": digest, "per_day": (counts[IMPORTANT] + counts[INTEREST]) / days, "days": days, - "defaults": cfg.routing}) + "defaults": cfg.routing, "current": "tune"}) def _centroids(con): @@ -254,7 +254,8 @@ async def config_page(request): {"interests": summarise("interest", "___", 0.60), "vetoes": summarise("veto", "threshold", 0.60), "demotes": summarise("demote", "___", 0.55), - "clusters": len(rows), "scoring": cfg.scoring, "routing": cfg.routing}) + "clusters": len(rows), "scoring": cfg.scoring, "routing": cfg.routing, + "current": "config"}) async def config_preview(request): @@ -383,7 +384,8 @@ async def digest_page(request): request, "digest.html", {"day": when, "stories": stories, "briefing": prose["briefing"] if prose else None, - "prev": when - timedelta(days=1), "next": when + timedelta(days=1)}, + "prev": when - timedelta(days=1), "next": when + timedelta(days=1), + "current": "digest"}, ) diff --git a/tests/test_delivery.py b/tests/test_delivery.py index ba7feb7..d4e4148 100644 --- a/tests/test_delivery.py +++ b/tests/test_delivery.py @@ -232,3 +232,14 @@ def test_labelling_is_disabled_when_no_secret_is_configured(tmp_path, monkeypatc from news import web web._VECTORS = None assert "labelling is disabled" in TestClient(web.app).get("/").text + + +@pytest.mark.parametrize("path,active", [ + ("/", "Triage"), ("/tune", "Tune"), ("/config", "Config"), ("/digest", "Digest")]) +def test_every_page_carries_the_nav(client, path, active): + """Navigation must not require knowing the URLs.""" + c, _ = client + html = c.get(path).text + for target in ("/tune", "/config", "/digest"): + assert f'href="{target}"' in html, f"{path} is missing a link to {target}" + assert f'class="here">{active}<' in html, f"{path} does not mark itself active"