feat(web): dark Gitea UI, systemd installer, WEB-UI docs (W7)

Restyles every template with a hand-written dark Gitea-style theme
(web/static/app.css, ~270 lines: badges, buttons, cards, focus rings,
prefers-reduced-motion) and completes the htmx polling wiring for all
four dashboard cards. Adds the repeatable setup-web-linux.sh installer
(idempotent: pip install '[web]' non-editable, render ci-web.service,
template [web]/[web.oidc] into config.toml, prompt for keyring secrets,
print Gitea OIDC registration steps, enable/restart the unit) plus
deploy/systemd/ci-web.service and docs/WEB-UI.md.
This commit is contained in:
2026-07-23 18:36:15 +02:00
parent 43925e6746
commit 60cee5418e
14 changed files with 953 additions and 128 deletions
@@ -1,48 +1,59 @@
<section class="card" id="disk-card">
<h2>Disk</h2>
{% if disk %}
<dl>
<dt>Path</dt><dd><code>{{ disk.path }}</code></dd>
<dt>Total</dt><dd>{{ "%.1f"|format(disk.total_gb) }} GB</dd>
<dt>Free</dt><dd>{{ "%.1f"|format(disk.free_gb) }} GB</dd>
<dt>Used</dt><dd>{{ "%.1f"|format(disk.used_pct) }}%</dd>
</dl>
{% else %}
<p class="notice notice-warn">Disk status unavailable.</p>
{% endif %}
</section>
<div class="contents" id="host-partial" hx-get="/host" hx-trigger="every 5s" hx-swap="outerHTML">
<section class="card" id="disk-card">
<h2>Disk</h2>
{% if disk %}
<dl>
<dt>Path</dt><dd><code>{{ disk.path }}</code></dd>
<dt>Total</dt><dd>{{ "%.1f"|format(disk.total_gb) }} GB</dd>
<dt>Free</dt><dd>{{ "%.1f"|format(disk.free_gb) }} GB</dd>
<dt>Used</dt><dd>{{ "%.1f"|format(disk.used_pct) }}%</dd>
</dl>
{% else %}
<p class="notice notice-warn" role="alert">Disk status unavailable.</p>
{% endif %}
</section>
<section class="card" id="runner-card">
<h2>Runners</h2>
{% if runners %}
<table>
<thead>
<tr>
<th>Service</th>
<th>Active</th>
<th>Sub-state</th>
</tr>
</thead>
<tbody>
{% for runner in runners %}
<tr class="{{ 'row-ok' if runner.active else 'row-alert' }}">
<td>{{ runner.name }}</td>
<td>{{ "yes" if runner.active else "no" }}</td>
<td>{{ runner.sub_state }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="notice notice-warn">Runner status unavailable.</p>
{% endif %}
</section>
<section class="card" id="runner-card">
<h2>Runners</h2>
{% if runners %}
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Service</th>
<th>Status</th>
<th>Sub-state</th>
</tr>
</thead>
<tbody>
{% for runner in runners %}
<tr class="{{ 'row-alert' if not runner.active else '' }}">
<td>{{ runner.name }}</td>
<td>
{% if runner.active %}
<span class="badge badge-success">active</span>
{% else %}
<span class="badge badge-danger">down</span>
{% endif %}
</td>
<td>{{ runner.sub_state }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="notice notice-warn" role="alert">Runner status unavailable.</p>
{% endif %}
</section>
<section class="card" id="backup-card">
<h2>Template backup</h2>
<form method="post" action="/host/backup" id="host-backup-form">
<input type="hidden" name="csrf" value="{{ csrf_token }}">
<label>Template: <input type="text" name="template" required></label>
<button type="submit" class="btn btn-danger">Backup template</button>
</form>
</section>
<section class="card" id="backup-card">
<h2>Template backup</h2>
<form method="post" action="/host/backup" id="host-backup-form" aria-label="Backup template">
<input type="hidden" name="csrf" value="{{ csrf_token }}">
<label for="host-backup-template">Template</label>
<input type="text" id="host-backup-template" name="template" required>
<button type="submit" class="btn btn-danger" aria-label="Backup template">Backup template</button>
</form>
</section>
</div>