All checks were successful
Deploy / deploy (push) Successful in 39s
Replace Pico CSS with a custom dark design system: - Deep navy background (#090b12) with violet-indigo accent (#7b6cf5) - Outfit font for UI, JetBrains Mono for code/URLs - Custom nav with SVG icons and active-state highlighting - Pill badges with colored status dots - Uppercase letter-spaced table headers - Stat cards on replica detail page - h3 section headings with extending rule line - Refined dialog/modal styling with blur backdrop - Terminal-style SSE log stream - Progress bar with accent gradient animation - Page entrance fade-in animation - All Pico CSS variable references replaced with custom tokens Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
153 lines
5.3 KiB
HTML
153 lines
5.3 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Dashboard — pngx-controller{% endblock %}
|
|
{% block content %}
|
|
|
|
<div class="page-hd">
|
|
<div>
|
|
<h2>Dashboard</h2>
|
|
</div>
|
|
<div class="page-actions">
|
|
<button id="sync-btn"
|
|
hx-post="/api/sync"
|
|
hx-swap="none"
|
|
hx-on::after-request="startPolling()"
|
|
class="{% if progress.running %}secondary{% endif %}">
|
|
{% if progress.running %}
|
|
Syncing…
|
|
{% else %}
|
|
<svg width="13" height="13" viewBox="0 0 13 13" fill="none"><path d="M11 6.5A4.5 4.5 0 112.5 4" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/><path d="M2.5 1.5V4H5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
Sync Now
|
|
{% endif %}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{% if flash %}
|
|
<div role="alert">{{ flash }}</div>
|
|
{% endif %}
|
|
|
|
<div id="progress-bar" class="{% if progress.running %}active{% endif %}"
|
|
hx-get="/api/sync/running"
|
|
hx-trigger="every 2s"
|
|
hx-target="#progress-inner"
|
|
hx-swap="innerHTML">
|
|
<article>
|
|
<div id="progress-inner">
|
|
{% if progress.running %}
|
|
<strong style="font-size:.85rem;">{{ progress.phase }}</strong>
|
|
{% if progress.docs_total > 0 %}
|
|
<span style="color:var(--text-3); font-size:.8rem; margin-left:.5rem;">{{ progress.docs_done }} / {{ progress.docs_total }} documents</span>
|
|
<progress value="{{ progress.docs_done }}" max="{{ progress.docs_total }}"></progress>
|
|
{% else %}
|
|
<progress></progress>
|
|
{% endif %}
|
|
{% endif %}
|
|
</div>
|
|
</article>
|
|
</div>
|
|
|
|
{% if last_run %}
|
|
<details style="margin-bottom:1.25rem;">
|
|
<summary>
|
|
Last sync run —
|
|
{% if last_run.finished_at %}
|
|
{{ last_run.finished_at.strftime('%Y-%m-%d %H:%M') }} UTC
|
|
· {{ last_run.docs_synced }} synced, {{ last_run.docs_failed }} failed
|
|
{% if last_run.timed_out %} <span class="badge badge-warning">timed out</span>{% endif %}
|
|
{% else %}
|
|
running…
|
|
{% endif %}
|
|
</summary>
|
|
<p style="font-size:.83rem; color:var(--text-2); padding-top:.5rem;">Triggered by: <code>{{ last_run.triggered_by }}</code> — Run #{{ last_run.id }}</p>
|
|
</details>
|
|
{% endif %}
|
|
|
|
<div id="doc-counts"
|
|
hx-get="/ui/doc-counts"
|
|
hx-trigger="load, every 120s"
|
|
hx-swap="innerHTML">
|
|
<small class="muted">Loading document counts…</small>
|
|
</div>
|
|
|
|
<h3>Replicas</h3>
|
|
|
|
{% if replica_rows %}
|
|
<div class="overflow-auto">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>URL</th>
|
|
<th>Status</th>
|
|
<th>Lag</th>
|
|
<th>Last run</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for row in replica_rows %}
|
|
<tr>
|
|
<td><a href="/replicas/{{ row.replica.id }}" style="font-weight:500;">{{ row.replica.name }}</a></td>
|
|
<td><code>{{ row.replica.url }}</code></td>
|
|
<td>
|
|
<span class="badge badge-{{ row.status }}">{{ row.status }}</span>
|
|
{% if row.replica.suspended_at %}
|
|
<br><small class="muted">{{ row.replica.consecutive_failures }} failures</small>
|
|
{% endif %}
|
|
</td>
|
|
<td style="color:var(--text-2); font-size:.82rem;">{{ row.lag }}</td>
|
|
<td>
|
|
{% if row.last_run %}
|
|
<small>
|
|
<span style="color:var(--ok);">✓</span> {{ row.last_run.docs_synced }}
|
|
{% if row.last_run.docs_failed %}
|
|
<span style="color:var(--err);">✗</span> <a href="/logs?replica_id={{ row.replica.id }}" style="color:var(--err);">{{ row.last_run.docs_failed }}</a>
|
|
{% endif %}
|
|
</small>
|
|
{% else %}
|
|
<small class="muted">never</small>
|
|
{% endif %}
|
|
</td>
|
|
<td class="actions">
|
|
<a href="/replicas/{{ row.replica.id }}" role="button" class="secondary outline" style="padding:.25em .65em; font-size:.78rem;">Details</a>
|
|
{% if row.replica.suspended_at %}
|
|
<button class="contrast outline" style="padding:.25em .65em; font-size:.78rem;"
|
|
hx-post="/api/replicas/{{ row.replica.id }}/unsuspend"
|
|
hx-on::after-request="window.location.reload()">
|
|
Re-enable
|
|
</button>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% else %}
|
|
<p>No replicas configured. <a href="/replicas">Add one →</a></p>
|
|
{% endif %}
|
|
|
|
<script>
|
|
function startPolling() {
|
|
document.getElementById('progress-bar').classList.add('active');
|
|
const btn = document.getElementById('sync-btn');
|
|
btn.textContent = 'Syncing\u2026';
|
|
btn.setAttribute('disabled', true);
|
|
}
|
|
|
|
document.body.addEventListener('htmx:afterSettle', function(evt) {
|
|
if (evt.detail.target && evt.detail.target.id === 'progress-inner') {
|
|
fetch('/api/sync/running').then(r => r.json()).then(data => {
|
|
if (!data.running) {
|
|
document.getElementById('progress-bar').classList.remove('active');
|
|
const btn = document.getElementById('sync-btn');
|
|
btn.innerHTML = '<svg width="13" height="13" viewBox="0 0 13 13" fill="none"><path d="M11 6.5A4.5 4.5 0 112.5 4" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/><path d="M2.5 1.5V4H5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg> Sync Now';
|
|
btn.removeAttribute('disabled');
|
|
}
|
|
});
|
|
}
|
|
});
|
|
</script>
|
|
|
|
{% endblock %}
|