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
This commit is contained in:
2026-09-03 21:30:09 +00:00
co-authored by Claude Fable 5.1
parent 29c66da865
commit a4c338a9f9
24 changed files with 665 additions and 259 deletions
+19 -13
View File
@@ -1,15 +1,21 @@
{% extends "layout.html" %}{% block content %}<section class="dashboard settings">
<header class="page-head"><div>
<h1>Settings</h1>
<p class="meta">{% match config_path %}{% when Some with (path) %}Editing <code>{{ path }}</code>. Saved settings apply to the next run; a hand edit on disk shows up on the next page view.{% when None %}No config file is configured; start the server with <code>--config</code> to edit settings here. The values below are the built-in defaults plus the environment.{% endmatch %} · <a href="/dashboard/settings/history">History</a></p>
{% match load_error %}{% when Some with (error) %}<p class="error banner">! config.toml on disk does not load: {{ error }} — the previous configuration stays live.</p>{% when None %}{% endmatch %}
<p class="page-desc">{% match config_path %}{% when Some with (path) %}Editing <code>{{ path }}</code>. Saved settings apply to the next run; a hand edit on disk shows up on the next page view.{% when None %}No config file is configured; start the server with <code>--config</code> to edit settings here. The values below are the built-in defaults plus the environment.{% endmatch %}</p>
</div><div class="page-actions"><a class="btn" href="/dashboard/settings/history">History</a><a class="btn" href="#add-provider">Add provider</a></div></header>
{% match load_error %}{% when Some with (error) %}<p class="error banner">config.toml on disk does not load: {{ error }} — the previous configuration stays live.</p>{% when None %}{% endmatch %}
{% if !errors.is_empty() %}<ul class="error banner">{% for error in errors %}<li>{{ error }}</li>{% endfor %}</ul>{% endif %}
<nav class="flex flex-wrap gap-x-3 gap-y-1 border-y border-rule py-3 font-mono text-xs" aria-label="Settings groups">{% for group in groups %}<a class="text-muted no-underline hover:text-accent" href="#{{ group.id }}">{{ group.id }}</a>{% endfor %}</nav>
<form method="post" action="/dashboard/settings" class="settings-form" id="settings-form">
{% for group in groups %}<section class="card" id="{{ group.id }}">
<h2>{{ group.title }}</h2>
{% match group.provider %}{% when Some with (provider) %}<p class="meta">Key: <code>{{ crate::config::ProviderConfig::api_key_env_var(provider.name) }}</code> in the env file. {% if provider.removable() %}Not named by any <code>[llm]</code> role. <button type="submit" form="remove-{{ provider.name }}" class="danger">Remove provider</button>{% else if provider.built_in %}Built in: declared by the defaults, so it can be edited and left unreferenced but not removed.{% else %}Named by <code>llm.{{ provider.roles() }}</code>; reassign the role before removing it.{% endif %}</p>{% when None %}{% endmatch %}
{% if group.is_weights() %}<p class="meta">Weights need not sum to 1; they are renormalized over the signals present for each article.</p>{% endif %}
{% for group in groups %}<section class="card scroll-mt-4" id="{{ group.id }}">
<div>
<p class="page-eyebrow font-mono normal-case tracking-normal">{{ group.id }}</p>
<h2 class="mt-0.5">{{ group.title }}</h2>
</div>
{% match group.provider %}{% when Some with (provider) %}<p class="meta text-xs">Key: <code>{{ crate::config::ProviderConfig::api_key_env_var(provider.name) }}</code> in the env file. {% if provider.removable() %}Not named by any <code>[llm]</code> role. <button type="submit" form="remove-{{ provider.name }}" class="danger">Remove provider</button>{% else if provider.built_in %}Built in: declared by the defaults, so it can be edited and left unreferenced but not removed.{% else %}Named by <code>llm.{{ provider.roles() }}</code>; reassign the role before removing it.{% endif %}</p>{% when None %}{% endmatch %}
{% if group.is_weights() %}<p class="meta text-xs">Weights need not sum to 1; they are renormalized over the signals present for each article.</p>{% endif %}
{% for field in group.fields %}<div class="setting kind-{{ field.kind_name() }}">
<div class="setting-label"><label for="{{ field.input_id() }}"><code>{{ field.key }}</code></label>{% if field.restart_required %} <span class="badge">restart</span>{% endif %}{% match field.help %}{% when Some with (help) %}<p class="help">{{ help }}</p>{% when None %}{% endmatch %}</div>
<div class="setting-label"><label for="{{ field.input_id() }}"><code>{{ field.key }}</code></label>{% if field.restart_required %} <span class="badge restart">restart</span>{% endif %}{% if field.is_env() %} <span class="badge">locked</span>{% endif %}{% match field.help %}{% when Some with (help) %}<p class="help">{{ help }}</p>{% when None %}{% endmatch %}</div>
<div class="setting-input">
{% if field.is_secret() %}<p class="secret">{% if field.is_set() %}set{% if field.is_env() %} (from <code>{{ field.env_var() }}</code>){% else if field.is_file() %} (in the config file — move it to the env file){% endif %}{% else %}not set — set <code>{{ field.env_var() }}</code> in the env file{% endif %}</p>
{% else if field.is_env() %}{% if field.is_text_list() %}<textarea id="{{ field.input_id() }}" disabled>{{ field.current }}</textarea>{% else %}<input id="{{ field.input_id() }}" type="text" value="{{ field.current }}" disabled>{% endif %}<p class="help">locked by <code>{{ field.env_var() }}</code> in the env file</p>
@@ -25,17 +31,17 @@
{% endfor %}
</section>
{% endfor %}
<div class="actions">{% match config_path %}{% when Some with (_path) %}<button type="submit">Save settings</button>{% when None %}<button type="submit" disabled>Save settings</button>{% endmatch %}</div>
<div class="actions save-bar">{% match config_path %}{% when Some with (path) %}<span class="meta">Writes <code>{{ path }}</code>.</span><button class="btn-primary" type="submit">Save settings</button>{% when None %}<span class="meta">No config file: settings are read-only.</span><button class="btn-primary" type="submit" disabled>Save settings</button>{% endmatch %}</div>
</form>
{% for group in groups %}{% match group.provider %}{% when Some with (provider) %}{% if provider.removable() %}<form method="post" action="/dashboard/settings/providers" id="remove-{{ provider.name }}" class="inline" data-confirm="Remove [providers.{{ provider.name }}] from config.toml?"><input type="hidden" name="action" value="remove"><input type="hidden" name="name" value="{{ provider.name }}"></form>{% endif %}{% when None %}{% endmatch %}{% endfor %}
<section class="card" id="add-provider">
{% for group in groups %}{% match group.provider %}{% when Some with (provider) %}{% if provider.removable() %}<form method="post" action="/dashboard/settings/providers" id="remove-{{ provider.name }}" class="form-inline" data-confirm="Remove [providers.{{ provider.name }}] from config.toml?"><input type="hidden" name="action" value="remove"><input type="hidden" name="name" value="{{ provider.name }}"></form>{% endif %}{% when None %}{% endmatch %}{% endfor %}
<section class="card scroll-mt-4" id="add-provider">
<h2>Add provider</h2>
<form method="post" action="/dashboard/settings/providers" class="add-provider">
<form method="post" action="/dashboard/settings/providers" class="add-provider filters border-y-0 py-0">
<input type="hidden" name="action" value="add">
<label>Name <input name="name" type="text" pattern="[a-z0-9_]+" placeholder="mistral" required{% if config_path.is_none() %} disabled{% endif %}></label>
<label>Kind <select name="kind">{% for kind in provider_kinds %}<option value="{{ kind }}">{{ kind }}</option>{% endfor %}</select></label>
<p class="help">Inserts <code>[providers.&lt;name&gt;]</code> with placeholder <code>base_url</code> and <code>model</code>; edit them above afterwards and put <code>DAILY_EPUB_PROVIDERS__&lt;NAME&gt;__API_KEY</code> in the env file.</p>
<button type="submit"{% if config_path.is_none() %} disabled{% endif %}>Add provider</button>
<div class="filter-actions"><button class="btn" type="submit"{% if config_path.is_none() %} disabled{% endif %}>Add provider</button></div>
</form>
<p class="help">Inserts <code>[providers.&lt;name&gt;]</code> with placeholder <code>base_url</code> and <code>model</code>; edit them above afterwards and put <code>DAILY_EPUB_PROVIDERS__&lt;NAME&gt;__API_KEY</code> in the env file.</p>
</section>
</section>{% endblock %}