Files
the-daily-epub/src/web/templates/dashboard/article.html
T
thalladaandClaude Fable 5.1 a4c338a9f9 Web dashboard v2 step 3: dashboard restyle
Restyle every template under src/web/templates/dashboard/ plus the shared
partials _candidate_row.html, _signals_table.html and _pagination.html with
Tailwind utilities and the design system step 2 built, so the dashboard reads
as the sans-serif instrument-panel half of the same publication as the reader
pages: a page header on every page, stat tiles, compact tables with sticky
heads inside .scroll-x, tinted badges, one inline filter form per page,
settings groups with a sticky save bar, job cards, a scrolling journal block
and a rating widget that fits inside a table cell.

tailwind.css gains the dashboard vocabulary (.page-head/.page-desc/
.page-actions, .tiles/.tile*, .cell-wrap, .scroll-x.tall, .filters,
.pager/.tabs, .setting*/.save-bar, .disclosure, .sparklines/.spark-figure,
td .rating overrides, tr.superseded) plus a styled <meter> and a disabled
control state. Renames class="inline" to form-inline: Tailwind emits an
.inline display utility that was beating form.inline's layout.

Three small backend changes: job cards show the status and time of their last
run (attach_last_runs, no extra query, unit-tested); the Stats page passes
"stats" as its nav key so the admin nav no longer highlights Overview; and the
settings load-error banner drops its "! " prefix, with its assertion updated.

cargo test: 477 passed. npm run css:check clean.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NHyYupFdBiR4VfoUM7NjSM
2026-09-03 21:30:09 +00:00

82 lines
8.4 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "layout.html" %}{% block content %}<section class="dashboard">
<header class="page-head"><div>
<p class="page-eyebrow"><a class="text-muted no-underline hover:text-accent" href="/dashboard/articles">Articles</a> › {{ id }}</p>
<h1 class="mt-1"><a class="text-ink no-underline hover:text-accent" href="{{ url }}">{{ title }}</a></h1>
<p class="page-desc"><a href="/dashboard/articles?feed={{ feed_id }}">{{ feed }}</a>{% if let Some(category) = category %} · {{ category }}{% endif %}{% if let Some(author) = author %} · {{ author }}{% endif %}</p>
</div><div class="page-actions">{% if let Some(rating) = rating %}<span class="badge {{ rating_class }}">{{ rating }}</span>{% else %}<span class="badge">unrated</span>{% endif %}<a class="btn" href="{{ url }}">Open source</a></div></header>
<div class="cards">
<section class="card"><h2>Article</h2>
<dl class="kv">
<dt>Canonical URL</dt><dd><a href="{{ canonical_url }}">{{ canonical_url }}</a></dd>
<dt>Published</dt><dd>{{ published_at }}</dd>
<dt>First seen</dt><dd>{{ first_seen }}</dd>
<dt>Words</dt><dd>{{ words }}{% if excerpt_only %} <span class="badge">excerpt only</span>{% endif %}</dd>
<dt>Images</dt><dd>{{ image_count }}</dd>
<dt>Current rating</dt><dd>{% if let Some(rating) = rating %}<span class="badge {{ rating_class }}">{{ rating }}</span>{% else %}<span class="muted">unrated</span>{% endif %}</dd>
</dl>
</section>
<section class="card"><h2>Provenance</h2>
<dl class="kv">
<dt>Sources</dt><dd>{% if sources.is_empty() %}<span class="muted">none recorded</span>{% else %}{% for source in sources %}{{ source.kind }} · {{ source.feed }}{% if let Some(category) = source.category %} ({{ category }}){% endif %}{% if !loop.last %}<br>{% endif %}{% endfor %}{% endif %}</dd>
<dt>Social</dt><dd>{% if social.is_empty() %}<span class="muted">none</span>{% else %}{% for item in social %}{% if let Some(url) = item.url %}<a href="{{ url }}">{{ item.source }}</a>{% else %}{{ item.source }}{% endif %} · {{ item.score }} points · {{ item.comments }} comments{% if !loop.last %}<br>{% endif %}{% endfor %}{% endif %}</dd>
<dt>In issues</dt><dd>{% if in_issues.is_empty() %}<span class="muted">never published</span>{% else %}{% for issue in in_issues %}<a href="{{ issue.href }}">{{ issue.date }}</a> · {{ issue.section }} · position {{ issue.position }}{% if issue.is_lead %} · lead{% endif %}{% if !loop.last %}<br>{% endif %}{% endfor %}{% endif %}</dd>
</dl>
</section>
</div>
<h2>Your verdict</h2>
{% include "_rating_widget.html" %}
{% if let Some(text) = explain %}<details class="explain disclosure" id="article-explain"><summary>Explain (text)</summary><div class="disclosure-body"><pre class="block">{{ text }}</pre></div></details>{% endif %}
<h2>Assessments</h2>
{% if assessments.is_empty() %}<p class="muted text-sm">No LLM assessment stored.</p>{% else %}<div class="cards">{% for assessment in assessments %}<section class="card">
<h3 class="flex flex-wrap items-center gap-2">{{ assessment.stage }}{% if assessment.rejected %} <span class="badge down">rejected by provider</span>{% endif %}</h3>
{% if assessment.rejected %}<p>{{ assessment.rationale }}</p><p class="muted text-xs">{{ assessment.model }} · prompt v{{ assessment.prompt_version }} · {{ assessment.assessed_at }}</p>{% else %}<dl class="kv">
{% if assessment.stage == "deep" %}<dt>Quality</dt><dd class="tabular-nums">{{ assessment.score }}</dd><dt>Fit</dt><dd class="tabular-nums">{{ assessment.fit }}</dd><dt>Category</dt><dd>{{ assessment.category }}</dd><dt>Format</dt><dd>{{ assessment.kind }}</dd><dt>Paywalled guess</dt><dd>{% if assessment.paywalled %}yes{% else %}no{% endif %}</dd>{% else %}<dt>Interest</dt><dd class="tabular-nums">{{ assessment.score }}</dd><dt>Kind</dt><dd>{{ assessment.kind }}</dd>{% endif %}
<dt>Rationale</dt><dd>{{ assessment.rationale }}</dd>
<dt>Model</dt><dd>{{ assessment.model }} · prompt v{{ assessment.prompt_version }} · profile v{{ assessment.profile_version }}</dd>
<dt>Assessed</dt><dd>{{ assessment.assessed_at }}</dd>
</dl>
{% if !assessment.facets.is_empty() %}<div class="scroll-x"><table class="facets"><thead><tr><th>facet</th><th>value</th></tr></thead><tbody>{% for facet in assessment.facets %}<tr><td>{{ facet.name }}</td><td class="cell-wrap">{{ facet.value }}</td></tr>{% endfor %}</tbody></table></div>{% endif %}{% endif %}
</section>{% endfor %}</div>{% endif %}
<h2>Run history</h2>
{% if history.is_empty() %}<p class="muted text-sm">Never considered by a run.</p>{% else %}<div class="scroll-x"><table class="history">
<thead><tr><th>run</th><th>date</th><th>stage</th><th>reason</th><th>admitted by</th><th class="num">utility</th><th class="num">rank</th><th class="num">cluster</th><th>editor</th></tr></thead>
<tbody>{% for row in history %}<tr>
<td class="cell-wrap"><details class="signals"><summary><a href="{{ row.run_href }}">run {{ row.run_id }}</a> <span class="badge {{ row.status }}">{{ row.status }}</span></summary>{% let signals = row.signals %}{% include "_signals_table.html" %}</details></td>
<td class="cell-tight text-muted">{{ row.date }}</td>
<td class="cell-tight"><span class="badge {{ row.stage }}">{{ row.stage }}</span></td>
<td class="cell-tight text-muted">{% if let Some(reason) = row.reason %}{{ reason }}{% endif %}</td>
<td class="cell-tight text-muted">{% if let Some(first) = row.admitted_first %}{{ first }}{% if !row.admitted_rest.is_empty() %} <span class="muted text-xs">({{ row.admitted_rest }})</span>{% endif %}{% endif %}</td>
<td class="num">{{ row.utility }}</td>
<td class="num">{{ row.rank }}</td>
<td class="num">{{ row.cluster }}</td>
<td class="cell-wrap text-muted">{% if let Some(why) = row.editor_why %}{{ why }}{% endif %}</td>
</tr>{% endfor %}</tbody></table></div>{% endif %}
<h2>Neighbours and interests</h2>
{% if let Some(signals) = latest_signals %}{% if signals.top_interests.is_empty() && signals.neighbours.is_empty() %}<p class="muted text-sm">The latest run recorded no interests or neighbours for this article.</p>{% else %}<div class="cards">
<section class="card"><h3>Top interests</h3>{% if signals.top_interests.is_empty() %}<p class="muted">None.</p>{% else %}<div class="scroll-x"><table><thead><tr><th>interest</th><th class="num">z</th><th class="num">cos</th></tr></thead><tbody>{% for interest in signals.top_interests %}<tr><td>{{ interest.name }}</td><td class="num">{{ interest.z }}</td><td class="num">{{ interest.cos }}</td></tr>{% endfor %}</tbody></table></div>{% endif %}</section>
<section class="card"><h3>Nearest rated neighbours</h3>{% if signals.neighbours.is_empty() %}<p class="muted">None.</p>{% else %}<div class="scroll-x"><table><thead><tr><th>label</th><th class="num">cos</th><th>article</th></tr></thead><tbody>{% for neighbour in signals.neighbours %}<tr><td><span class="badge {{ neighbour.label }}">{{ neighbour.label }}</span></td><td class="num">{{ neighbour.cos }}</td><td class="cell-wrap"><a href="/dashboard/articles/{{ neighbour.article_id }}">{{ neighbour.title }}</a></td></tr>{% endfor %}</tbody></table></div>{% endif %}</section>
</div>{% endif %}{% else %}<p class="muted text-sm">No signals recorded.</p>{% endif %}
<h2>Embedding</h2>
{% if let Some(embedding) = embedding %}<dl class="kv"><dt>Model</dt><dd>{{ embedding.model }}</dd><dt>Dimension</dt><dd class="tabular-nums">{{ embedding.dimension }}</dd><dt>Created</dt><dd>{{ embedding.created_at }}</dd><dt>Input hash</dt><dd><code>{{ embedding.input_hash }}</code></dd></dl>{% else %}<p class="muted text-sm">No embedding stored.</p>{% endif %}
<h2>Rating events</h2>
{% if events.is_empty() %}<p class="muted text-sm">No rating events.</p>{% else %}<div class="scroll-x"><table class="events">
<thead><tr><th>when</th><th>label</th><th class="num">value</th><th>kind</th><th>source</th><th>user</th><th>issue</th><th>note</th></tr></thead>
<tbody>{% for event in events %}<tr>
<td class="cell-tight text-muted">{{ event.event_at }}</td>
<td class="cell-tight"><span class="badge {{ event.label_class }}">{{ event.label }}</span></td>
<td class="num">{{ event.value }}</td>
<td class="cell-tight text-muted">{{ event.kind }}</td>
<td class="cell-tight text-muted">{{ event.source }}</td>
<td class="cell-tight">{% if let Some(user) = event.user %}{{ user }}{% else %}<span class="muted">—</span>{% endif %}</td>
<td class="cell-tight">{% if let Some(date) = event.issue_date %}<a href="/issues/{{ date }}">{{ date }}</a>{% endif %}</td>
<td class="cell-wrap text-muted">{% if let Some(note) = event.note %}{{ note }}{% endif %}</td>
</tr>{% endfor %}</tbody></table></div>{% endif %}
</section>{% endblock %}