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.
This commit is contained in:
1 parent
5f4bc8d890
commit
327d6a69ae
7 files changed
+69
-15
No files matched your search
@@ -0,0 +1,7 @@
|
|||||||
|
{#- Shared navigation. `current` marks the active page. -#}
|
||||||
|
<nav class="sitenav">
|
||||||
|
<a href="/" class="{{ 'here' if current == 'triage' }}">Triage</a>
|
||||||
|
<a href="/digest" class="{{ 'here' if current == 'digest' }}">Digest</a>
|
||||||
|
<a href="/tune" class="{{ 'here' if current == 'tune' }}">Tune</a>
|
||||||
|
<a href="/config" class="{{ 'here' if current == 'config' }}">Config</a>
|
||||||
|
</nav>
|
||||||
@@ -47,14 +47,21 @@
|
|||||||
.nearmiss { border-left:2px solid var(--warn); padding-left:.7rem; margin-top:.6rem; }
|
.nearmiss { border-left:2px solid var(--warn); padding-left:.7rem; margin-top:.6rem; }
|
||||||
.ok { color:var(--pos); } .err { color:var(--neg); }
|
.ok { color:var(--pos); } .err { color:var(--neg); }
|
||||||
.gate { color:var(--warn); }
|
.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; }
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
<main>
|
<main>
|
||||||
<header>
|
<header>
|
||||||
<h1>Config</h1>
|
<h1>Config</h1>
|
||||||
<div class="note"><a href="/">triage</a> · <a href="/tune">tune</a> ·
|
{% include "_nav.html" %}
|
||||||
<a href="/digest">digest</a> · {{ clusters }} clusters scored</div>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<p class="note" style="margin:-.5rem 0 1rem">{{ clusters }} clusters scored</p>
|
||||||
|
|
||||||
<h2>Try a filter</h2>
|
<h2>Try a filter</h2>
|
||||||
<div class="probe">
|
<div class="probe">
|
||||||
<p class="note" style="margin-top:0">
|
<p class="note" style="margin-top:0">
|
||||||
|
|||||||
@@ -13,7 +13,9 @@
|
|||||||
BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
|
BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
|
||||||
-webkit-text-size-adjust:100%; }
|
-webkit-text-size-adjust:100%; }
|
||||||
main { max-width:38rem; margin:0 auto; padding:1.5rem 1.15rem 4rem; }
|
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; }
|
h1 { font-size:1.3rem; margin:0 0 .15rem; letter-spacing:-.01em; }
|
||||||
.date { color:var(--muted); font-size:.85rem; }
|
.date { color:var(--muted); font-size:.85rem; }
|
||||||
.briefing p { margin:0 0 1rem; }
|
.briefing p { margin:0 0 1rem; }
|
||||||
@@ -30,11 +32,20 @@
|
|||||||
nav { display:flex; justify-content:space-between; margin-top:2rem; font-size:.9rem; }
|
nav { display:flex; justify-content:space-between; margin-top:2rem; font-size:.9rem; }
|
||||||
nav a { color:var(--accent); text-decoration:none; }
|
nav a { color:var(--accent); text-decoration:none; }
|
||||||
.empty { color:var(--muted); font-style:italic; }
|
.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; }
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
<main>
|
<main>
|
||||||
<header>
|
<header>
|
||||||
<h1>Daily briefing</h1>
|
<div>
|
||||||
<div class="date">{{ day.strftime('%A, %-d %B %Y') }}</div>
|
<h1>Daily briefing</h1>
|
||||||
|
<div class="date">{{ day.strftime('%A, %-d %B %Y') }}</div>
|
||||||
|
</div>
|
||||||
|
{% include "_nav.html" %}
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{% if briefing %}
|
{% if briefing %}
|
||||||
|
|||||||
@@ -21,6 +21,8 @@
|
|||||||
h1 { font-size:1.15rem; margin:0; letter-spacing:-.01em; }
|
h1 { font-size:1.15rem; margin:0; letter-spacing:-.01em; }
|
||||||
.stats, .filters { color:var(--muted); font-size:.83rem; }
|
.stats, .filters { color:var(--muted); font-size:.83rem; }
|
||||||
.filters a { color:var(--accent); text-decoration:none; margin-left:.6rem; }
|
.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; }
|
.filters a.on { text-decoration:underline; }
|
||||||
|
|
||||||
article { background:var(--card); border:1px solid var(--rule); border-radius:9px;
|
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;
|
border-top:1px solid var(--rule); padding:.45rem 1rem; font-size:.78rem;
|
||||||
color:var(--muted); display:flex; gap:1rem; justify-content:center; }
|
color:var(--muted); display:flex; gap:1rem; justify-content:center; }
|
||||||
.empty { color:var(--muted); font-style:italic; padding:2rem 0; }
|
.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; } }
|
@media (max-width:34rem) { .help { display:none; } }
|
||||||
</style>
|
</style>
|
||||||
<main>
|
<main>
|
||||||
<header>
|
<header>
|
||||||
<h1>Triage</h1>
|
<h1>Triage</h1>
|
||||||
<div class="stats">{{ rows|length }} shown · {{ labelled }} labelled · {{ total }} clusters</div>
|
{% include "_nav.html" %}
|
||||||
<div class="filters">show
|
</header>
|
||||||
|
<div class="subbar">
|
||||||
|
<span class="stats">{{ rows|length }} shown · {{ labelled }} labelled · {{ total }} clusters</span>
|
||||||
|
<span class="filters">show
|
||||||
<a class="{{ 'on' if show == 'unlabelled' }}" href="?show=unlabelled">unlabelled</a>
|
<a class="{{ 'on' if show == 'unlabelled' }}" href="?show=unlabelled">unlabelled</a>
|
||||||
<a class="{{ 'on' if show == 'all' }}" href="?show=all">all</a>
|
<a class="{{ 'on' if show == 'all' }}" href="?show=all">all</a>
|
||||||
· <a href="/digest">digest</a>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
|
||||||
|
|
||||||
{% for r in rows %}
|
{% for r in rows %}
|
||||||
<article id="c{{ loop.index0 }}" data-id="{{ r.id }}"
|
<article id="c{{ loop.index0 }}" data-id="{{ r.id }}"
|
||||||
|
|||||||
@@ -39,11 +39,17 @@
|
|||||||
letter-spacing:.04em; min-width:5.5rem; }
|
letter-spacing:.04em; min-width:5.5rem; }
|
||||||
.lane.imp { color:var(--neg); }
|
.lane.imp { color:var(--neg); }
|
||||||
.note { color:var(--muted); font-size:.82rem; }
|
.note { color:var(--muted); font-size:.82rem; }
|
||||||
|
.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; }
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
<main>
|
<main>
|
||||||
<header>
|
<header>
|
||||||
<h1>Tune</h1>
|
<h1>Tune</h1>
|
||||||
<div class="note"><a href="/">triage</a> · <a href="/digest">digest</a></div>
|
{% include "_nav.html" %}
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<form method="get">
|
<form method="get">
|
||||||
|
|||||||
+6
-4
@@ -136,7 +136,7 @@ async def triage(request):
|
|||||||
return TEMPLATES.TemplateResponse(
|
return TEMPLATES.TemplateResponse(
|
||||||
request, "triage.html",
|
request, "triage.html",
|
||||||
{"rows": rows, "show": show, "total": len(scores), "labelled": len(labelled),
|
{"rows": rows, "show": show, "total": len(scores), "labelled": len(labelled),
|
||||||
"signed": bool(secret)})
|
"signed": bool(secret), "current": "triage"})
|
||||||
|
|
||||||
|
|
||||||
async def tune(request):
|
async def tune(request):
|
||||||
@@ -196,7 +196,7 @@ async def tune(request):
|
|||||||
request, "tune.html",
|
request, "tune.html",
|
||||||
{"routing": routing, "counts": counts, "alerts": alerts, "digest": digest,
|
{"routing": routing, "counts": counts, "alerts": alerts, "digest": digest,
|
||||||
"per_day": (counts[IMPORTANT] + counts[INTEREST]) / days, "days": days,
|
"per_day": (counts[IMPORTANT] + counts[INTEREST]) / days, "days": days,
|
||||||
"defaults": cfg.routing})
|
"defaults": cfg.routing, "current": "tune"})
|
||||||
|
|
||||||
|
|
||||||
def _centroids(con):
|
def _centroids(con):
|
||||||
@@ -254,7 +254,8 @@ async def config_page(request):
|
|||||||
{"interests": summarise("interest", "___", 0.60),
|
{"interests": summarise("interest", "___", 0.60),
|
||||||
"vetoes": summarise("veto", "threshold", 0.60),
|
"vetoes": summarise("veto", "threshold", 0.60),
|
||||||
"demotes": summarise("demote", "___", 0.55),
|
"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):
|
async def config_preview(request):
|
||||||
@@ -383,7 +384,8 @@ async def digest_page(request):
|
|||||||
request, "digest.html",
|
request, "digest.html",
|
||||||
{"day": when, "stories": stories,
|
{"day": when, "stories": stories,
|
||||||
"briefing": prose["briefing"] if prose else None,
|
"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"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -232,3 +232,14 @@ def test_labelling_is_disabled_when_no_secret_is_configured(tmp_path, monkeypatc
|
|||||||
from news import web
|
from news import web
|
||||||
web._VECTORS = None
|
web._VECTORS = None
|
||||||
assert "labelling is disabled" in TestClient(web.app).get("/").text
|
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"
|
||||||
Reference in new issue
Block a user