feat: full UI overhaul — dark theme with custom design system
All checks were successful
Deploy / deploy (push) Successful in 39s
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>
This commit is contained in:
@@ -9,9 +9,9 @@
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<div style="display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:1rem;">
|
||||
<h2 style="margin:0;">{{ replica.name }}</h2>
|
||||
<div style="display:flex; gap:0.5rem; flex-wrap:wrap;">
|
||||
<div class="page-hd">
|
||||
<h2>{{ replica.name }}</h2>
|
||||
<div class="page-actions">
|
||||
<button class="secondary outline"
|
||||
hx-post="/api/replicas/{{ replica.id }}/test"
|
||||
hx-target="#conn-result" hx-swap="innerHTML">
|
||||
@@ -21,7 +21,8 @@
|
||||
hx-post="/api/sync?replica_id={{ replica.id }}"
|
||||
hx-swap="none"
|
||||
hx-on::after-request="window.location.reload()">
|
||||
⟳ Sync Now
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M10 6A4 4 0 112 3.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/><path d="M2 1.5V3.5H4" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
Sync Now
|
||||
</button>
|
||||
{% if replica.suspended_at %}
|
||||
<button class="contrast"
|
||||
@@ -31,44 +32,48 @@
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if replica.enabled and not replica.suspended_at %}
|
||||
<button class="outline" style="margin-left:auto;" onclick="openPromoteDialog()">
|
||||
<button class="secondary outline" onclick="openPromoteDialog()">
|
||||
Promote to Master
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span id="conn-result" style="margin-left:0.5rem;"></span>
|
||||
<span id="conn-result"></span>
|
||||
|
||||
<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(180px, 1fr)); gap:1rem; margin: 1.5rem 0;">
|
||||
<article style="padding:1rem;">
|
||||
<header style="margin-bottom:0.25rem;"><small class="muted">URL</small></header>
|
||||
<code>{{ replica.url }}</code>
|
||||
</article>
|
||||
<article style="padding:1rem;">
|
||||
<header style="margin-bottom:0.25rem;"><small class="muted">Status</small></header>
|
||||
{% if replica.suspended_at %}
|
||||
<span class="badge badge-suspended">suspended</span>
|
||||
<br><small class="muted">{{ replica.consecutive_failures }} consecutive failures</small>
|
||||
{% elif replica.last_sync_ts %}
|
||||
<span class="badge badge-synced">synced</span>
|
||||
{% else %}
|
||||
<span class="badge badge-pending">pending</span>
|
||||
{% endif %}
|
||||
</article>
|
||||
<article style="padding:1rem;">
|
||||
<header style="margin-bottom:0.25rem;"><small class="muted">Last sync</small></header>
|
||||
{% if replica.last_sync_ts %}
|
||||
{{ replica.last_sync_ts.strftime('%Y-%m-%d %H:%M:%S') }} UTC
|
||||
<br><small class="muted">{{ lag }}</small>
|
||||
{% else %}
|
||||
<small class="muted">never</small>
|
||||
{% endif %}
|
||||
</article>
|
||||
<article style="padding:1rem;">
|
||||
<header style="margin-bottom:0.25rem;"><small class="muted">Interval</small></header>
|
||||
{% if replica.sync_interval_seconds %}{{ replica.sync_interval_seconds }}s{% else %}global{% endif %}
|
||||
</article>
|
||||
<div class="stat-cards">
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-label">URL</div>
|
||||
<div class="stat-card-value"><code style="font-size:.78rem;">{{ replica.url }}</code></div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-label">Status</div>
|
||||
<div class="stat-card-value">
|
||||
{% if replica.suspended_at %}
|
||||
<span class="badge badge-suspended">suspended</span>
|
||||
<div class="stat-card-sub">{{ replica.consecutive_failures }} consecutive failures</div>
|
||||
{% elif replica.last_sync_ts %}
|
||||
<span class="badge badge-synced">synced</span>
|
||||
{% else %}
|
||||
<span class="badge badge-pending">pending</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-label">Last sync</div>
|
||||
<div class="stat-card-value" style="font-size:.88rem;">
|
||||
{% if replica.last_sync_ts %}
|
||||
{{ replica.last_sync_ts.strftime('%Y-%m-%d %H:%M:%S') }} UTC
|
||||
<div class="stat-card-sub">{{ lag }}</div>
|
||||
{% else %}
|
||||
<span class="muted">never</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-label">Interval</div>
|
||||
<div class="stat-card-value">{% if replica.sync_interval_seconds %}{{ replica.sync_interval_seconds }}s{% else %}<span style="color:var(--text-2);">global</span>{% endif %}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Sync Run History (last 20)</h3>
|
||||
@@ -81,22 +86,22 @@
|
||||
<tbody>
|
||||
{% for run in recent_runs %}
|
||||
<tr>
|
||||
<td>{{ run.id }}</td>
|
||||
<td>{{ run.started_at.strftime('%Y-%m-%d %H:%M:%S') if run.started_at else '—' }}</td>
|
||||
<td>
|
||||
<td style="color:var(--text-3); font-size:.8rem;">{{ run.id }}</td>
|
||||
<td style="font-size:.82rem;">{{ run.started_at.strftime('%Y-%m-%d %H:%M:%S') if run.started_at else '—' }}</td>
|
||||
<td style="font-size:.82rem; color:var(--text-2);">
|
||||
{% if run.started_at and run.finished_at %}
|
||||
{% set dur = (run.finished_at - run.started_at).total_seconds()|int %}
|
||||
{% if dur < 60 %}{{ dur }}s
|
||||
{% else %}{{ dur // 60 }}m {{ dur % 60 }}s{% endif %}
|
||||
{% elif run.started_at %}
|
||||
running…
|
||||
<span class="badge badge-syncing">running</span>
|
||||
{% else %}—{% endif %}
|
||||
</td>
|
||||
<td>{{ run.docs_synced }}</td>
|
||||
<td style="color:var(--ok);">{{ run.docs_synced }}</td>
|
||||
<td>
|
||||
{% if run.docs_failed %}
|
||||
<a href="/logs?replica_id={{ replica.id }}" class="badge badge-error">{{ run.docs_failed }}</a>
|
||||
{% else %}0{% endif %}
|
||||
{% else %}<span style="color:var(--text-3);">0</span>{% endif %}
|
||||
</td>
|
||||
<td><code>{{ run.triggered_by }}</code></td>
|
||||
<td>{% if run.timed_out %}<span class="badge badge-warning">timed out</span>{% endif %}</td>
|
||||
@@ -119,12 +124,12 @@
|
||||
<tbody>
|
||||
{% for entry in sync_map_page %}
|
||||
<tr>
|
||||
<td>{{ entry.master_doc_id }}</td>
|
||||
<td>{{ entry.replica_doc_id or '—' }}</td>
|
||||
<td style="font-family:var(--mono); font-size:.82rem;">{{ entry.master_doc_id }}</td>
|
||||
<td style="font-family:var(--mono); font-size:.82rem;">{{ entry.replica_doc_id or '—' }}</td>
|
||||
<td><span class="badge badge-{{ entry.status }}">{{ entry.status }}</span></td>
|
||||
<td>{{ entry.last_synced.strftime('%Y-%m-%d %H:%M') if entry.last_synced else '—' }}</td>
|
||||
<td>{{ entry.retry_count }}</td>
|
||||
<td><small>{{ entry.error_msg or '' }}</small></td>
|
||||
<td style="font-size:.82rem; color:var(--text-2);">{{ entry.last_synced.strftime('%Y-%m-%d %H:%M') if entry.last_synced else '—' }}</td>
|
||||
<td style="color:var(--text-3);">{{ entry.retry_count }}</td>
|
||||
<td><small style="color:var(--text-2);">{{ entry.error_msg or '' }}</small></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
@@ -134,23 +139,25 @@
|
||||
<p><small class="muted">No sync map entries yet.</small></p>
|
||||
{% endif %}
|
||||
|
||||
<details style="margin-top:2rem;">
|
||||
<details>
|
||||
<summary>Danger Zone</summary>
|
||||
<div style="padding:1rem; border:1px solid var(--pico-del-color); border-radius:0.5rem; margin-top:0.5rem;">
|
||||
<div class="danger-zone" style="margin-top:.75rem;">
|
||||
<p>Full resync wipes the sync map for this replica and re-syncs everything from scratch.</p>
|
||||
<button class="contrast"
|
||||
hx-post="/api/replicas/{{ replica.id }}/resync"
|
||||
hx-confirm="Wipe sync map and trigger full resync for {{ replica.name }}?"
|
||||
hx-on::after-request="window.location.reload()">
|
||||
Full Resync
|
||||
</button>
|
||||
<button class="secondary"
|
||||
hx-post="/api/replicas/{{ replica.id }}/reconcile"
|
||||
hx-confirm="Run reconcile for {{ replica.name }}? This matches existing documents without re-uploading."
|
||||
hx-target="#reconcile-result" hx-swap="innerHTML">
|
||||
Reconcile
|
||||
</button>
|
||||
<span id="reconcile-result" style="margin-left:0.5rem;"></span>
|
||||
<div style="display:flex; gap:.5rem; flex-wrap:wrap; align-items:center;">
|
||||
<button class="contrast"
|
||||
hx-post="/api/replicas/{{ replica.id }}/resync"
|
||||
hx-confirm="Wipe sync map and trigger full resync for {{ replica.name }}?"
|
||||
hx-on::after-request="window.location.reload()">
|
||||
Full Resync
|
||||
</button>
|
||||
<button class="secondary"
|
||||
hx-post="/api/replicas/{{ replica.id }}/reconcile"
|
||||
hx-confirm="Run reconcile for {{ replica.name }}? This matches existing documents without re-uploading."
|
||||
hx-target="#reconcile-result" hx-swap="innerHTML">
|
||||
Reconcile
|
||||
</button>
|
||||
<span id="reconcile-result"></span>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
@@ -158,19 +165,17 @@
|
||||
<dialog id="promote-dialog">
|
||||
<article>
|
||||
<header>
|
||||
<button aria-label="Close" rel="prev" onclick="document.getElementById('promote-dialog').close()"></button>
|
||||
<h3 style="margin:0;">Promote <strong>{{ replica.name }}</strong> to Master</h3>
|
||||
<p style="margin:0.25rem 0 0; font-size:0.875em; color:var(--pico-muted-color);">
|
||||
{{ replica.url }}
|
||||
</p>
|
||||
<button aria-label="Close" onclick="document.getElementById('promote-dialog').close()"></button>
|
||||
<h3>Promote <strong>{{ replica.name }}</strong> to Master</h3>
|
||||
<p style="margin:.2rem 0 0; font-size:.78rem; color:var(--text-3);">{{ replica.url }}</p>
|
||||
</header>
|
||||
|
||||
<div id="promote-preflight-content" style="min-height:6rem;">
|
||||
<div id="promote-preflight-content" style="min-height:6rem; padding:1.15rem 1.4rem;">
|
||||
<progress></progress>
|
||||
<p class="muted">Checking pre-flight conditions…</p>
|
||||
<p class="muted" style="font-size:.83rem;">Checking pre-flight conditions…</p>
|
||||
</div>
|
||||
|
||||
<footer style="display:flex; justify-content:flex-end; gap:0.5rem; padding:0; margin-top:1.25rem;">
|
||||
<footer style="display:flex; justify-content:flex-end; gap:.5rem;">
|
||||
<button class="secondary outline" onclick="document.getElementById('promote-dialog').close()">Cancel</button>
|
||||
<button id="promote-confirm-btn" disabled onclick="doPromote()">Promote to Master</button>
|
||||
</footer>
|
||||
@@ -186,7 +191,7 @@ async function openPromoteDialog() {
|
||||
const dialog = document.getElementById('promote-dialog');
|
||||
const content = document.getElementById('promote-preflight-content');
|
||||
const btn = document.getElementById('promote-confirm-btn');
|
||||
content.innerHTML = '<progress></progress><p class="muted">Checking pre-flight conditions\u2026</p>';
|
||||
content.innerHTML = '<progress></progress><p class="muted" style="font-size:.83rem;">Checking pre-flight conditions\u2026</p>';
|
||||
btn.disabled = true;
|
||||
_preflightData = null;
|
||||
dialog.showModal();
|
||||
@@ -206,9 +211,9 @@ function renderPreflight(data) {
|
||||
let html = '';
|
||||
|
||||
if (!data.can_promote) {
|
||||
html += '<div style="padding:0.75rem 1rem; background:#fff5f5; border-radius:0.4rem; border-left:3px solid var(--pico-del-color);">';
|
||||
html += '<div style="padding:.75rem 1rem; background:rgba(244,112,103,.08); border-radius:4px; border-left:3px solid #f47067;">';
|
||||
html += '<strong>' + (data.error || 'Cannot promote') + '</strong><br>';
|
||||
html += '<small>' + (data.detail || '') + '</small>';
|
||||
html += '<small style="color:var(--text-2);">' + (data.detail || '') + '</small>';
|
||||
html += '</div>';
|
||||
content.innerHTML = html;
|
||||
btn.disabled = true;
|
||||
@@ -216,33 +221,36 @@ function renderPreflight(data) {
|
||||
}
|
||||
|
||||
// Summary cards
|
||||
html += '<div style="display:grid; grid-template-columns:repeat(3,1fr); gap:0.75rem; margin-bottom:1rem;">';
|
||||
html += '<div style="padding:0.6rem 0.75rem; background:var(--pico-card-sectionning-background-color); border-radius:0.4rem; text-align:center;">';
|
||||
html += '<small class="muted">Master docs</small><br><strong>' + (data.master_doc_count ?? '?') + '</strong></div>';
|
||||
html += '<div style="padding:0.6rem 0.75rem; background:var(--pico-card-sectionning-background-color); border-radius:0.4rem; text-align:center;">';
|
||||
html += '<small class="muted">Replica docs</small><br><strong>' + (data.replica_doc_count ?? '?') + '</strong></div>';
|
||||
html += '<div style="padding:0.6rem 0.75rem; background:var(--pico-card-sectionning-background-color); border-radius:0.4rem; text-align:center;">';
|
||||
html += '<small class="muted">Mappings to rebuild</small><br><strong>' + data.ok_entries + '</strong></div>';
|
||||
html += '<div style="display:grid; grid-template-columns:repeat(3,1fr); gap:.65rem; margin-bottom:1rem;">';
|
||||
[
|
||||
['Master docs', data.master_doc_count ?? '?'],
|
||||
['Replica docs', data.replica_doc_count ?? '?'],
|
||||
['Mappings to rebuild', data.ok_entries],
|
||||
].forEach(function([label, val]) {
|
||||
html += '<div style="padding:.55rem .75rem; background:#1d2235; border-radius:4px; text-align:center;">';
|
||||
html += '<div style="font-size:.68rem; text-transform:uppercase; letter-spacing:.07em; color:var(--text-3); margin-bottom:.25rem;">' + label + '</div>';
|
||||
html += '<strong style="font-size:1.1rem;">' + val + '</strong></div>';
|
||||
});
|
||||
html += '</div>';
|
||||
|
||||
// Old master name field
|
||||
html += '<label style="margin-bottom:0.75rem; display:block;">Name for the current master (it becomes a new replica)<input type="text" id="old-master-name" value="old-master" required oninput="updateConfirmBtn()" style="margin-top:0.25rem;"></label>';
|
||||
// Old master name
|
||||
html += '<label style="display:block; margin-bottom:.75rem;">Name for the current master (it becomes a new replica)<input type="text" id="old-master-name" value="old-master" required oninput="updateConfirmBtn()"></label>';
|
||||
|
||||
// Warnings with ack checkboxes
|
||||
// Ack checkboxes
|
||||
if (data.pending_entries > 0) {
|
||||
html += '<label style="display:flex; gap:0.5rem; align-items:flex-start; margin-bottom:0.5rem; cursor:pointer;">';
|
||||
html += '<input type="checkbox" id="ack-pending" onchange="updateConfirmBtn()" style="margin-top:0.1rem; flex-shrink:0;">';
|
||||
html += '<label style="display:flex; gap:.5rem; align-items:flex-start; margin-bottom:.5rem; cursor:pointer; font-size:.85rem;">';
|
||||
html += '<input type="checkbox" id="ack-pending" onchange="updateConfirmBtn()" style="margin-top:.15rem; flex-shrink:0;">';
|
||||
html += '<span><strong>' + data.pending_entries + ' pending entries</strong> \u2014 these documents may be re-uploaded after promotion.</span>';
|
||||
html += '</label>';
|
||||
}
|
||||
if (data.error_entries > 0) {
|
||||
html += '<label style="display:flex; gap:0.5rem; align-items:flex-start; margin-bottom:0.5rem; cursor:pointer;">';
|
||||
html += '<input type="checkbox" id="ack-errors" onchange="updateConfirmBtn()" style="margin-top:0.1rem; flex-shrink:0;">';
|
||||
html += '<label style="display:flex; gap:.5rem; align-items:flex-start; margin-bottom:.5rem; cursor:pointer; font-size:.85rem;">';
|
||||
html += '<input type="checkbox" id="ack-errors" onchange="updateConfirmBtn()" style="margin-top:.15rem; flex-shrink:0;">';
|
||||
html += '<span><strong>' + data.error_entries + ' error entries</strong> \u2014 these documents will be re-synced after promotion.</span>';
|
||||
html += '</label>';
|
||||
}
|
||||
|
||||
html += '<p style="margin:0.75rem 0 0; font-size:0.875em; color:var(--pico-muted-color);">The current master will be added as a new replica and will sync from <strong>' + REPLICA_NAME + '</strong> going forward.</p>';
|
||||
html += '<p style="margin:.75rem 0 0; font-size:.8rem; color:var(--text-3);">The current master will be added as a new replica and will sync from <strong style="color:var(--text-2);">' + REPLICA_NAME + '</strong> going forward.</p>';
|
||||
|
||||
content.innerHTML = html;
|
||||
updateConfirmBtn();
|
||||
@@ -287,15 +295,15 @@ async function doPromote() {
|
||||
} else {
|
||||
const err = await resp.json();
|
||||
const detail = err.detail?.detail || err.detail || 'Unknown error';
|
||||
const content = document.getElementById('promote-preflight-content');
|
||||
content.innerHTML += '<p style="margin-top:0.75rem;"><span class="badge badge-error">Error</span> ' + detail + '</p>';
|
||||
document.getElementById('promote-preflight-content').innerHTML +=
|
||||
'<p style="margin-top:.75rem;"><span class="badge badge-error">Error</span> ' + detail + '</p>';
|
||||
btn.disabled = false;
|
||||
btn.removeAttribute('aria-busy');
|
||||
btn.textContent = 'Promote to Master';
|
||||
}
|
||||
} catch (err) {
|
||||
const content = document.getElementById('promote-preflight-content');
|
||||
content.innerHTML += '<p style="margin-top:0.75rem;"><span class="badge badge-error">Network error</span> ' + err + '</p>';
|
||||
document.getElementById('promote-preflight-content').innerHTML +=
|
||||
'<p style="margin-top:.75rem;"><span class="badge badge-error">Network error</span> ' + err + '</p>';
|
||||
btn.disabled = false;
|
||||
btn.removeAttribute('aria-busy');
|
||||
btn.textContent = 'Promote to Master';
|
||||
|
||||
Reference in New Issue
Block a user