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:
@@ -2,10 +2,10 @@
|
||||
{% block title %}Replicas — pngx-controller{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div style="display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:1rem; margin-bottom:1.5rem;">
|
||||
<div class="page-hd">
|
||||
<div>
|
||||
<h2 style="margin:0;">Replicas</h2>
|
||||
<small class="muted">Paperless-ngx instances that receive documents from the master.</small>
|
||||
<h2>Replicas</h2>
|
||||
<div class="page-hd-sub">Paperless-ngx instances that receive documents from the master.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -25,8 +25,8 @@
|
||||
<tbody>
|
||||
{% for r in replicas %}
|
||||
<tr>
|
||||
<td><a href="/replicas/{{ r.id }}">{{ r.name }}</a></td>
|
||||
<td><small><code>{{ r.url }}</code></small></td>
|
||||
<td><a href="/replicas/{{ r.id }}" style="font-weight:500;">{{ r.name }}</a></td>
|
||||
<td><code>{{ r.url }}</code></td>
|
||||
<td>
|
||||
{% if r.suspended_at %}
|
||||
<span class="badge badge-suspended">suspended</span>
|
||||
@@ -37,38 +37,38 @@
|
||||
<span class="badge badge-ok">enabled</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<td style="color:var(--text-2); font-size:.82rem;">
|
||||
{% if r.sync_interval_seconds %}{{ r.sync_interval_seconds }}s{% else %}<small class="muted">global</small>{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<td style="color:var(--text-2); font-size:.82rem;">
|
||||
{% if r.last_sync_ts %}{{ r.last_sync_ts.strftime('%Y-%m-%d %H:%M') }} UTC{% else %}<small class="muted">never</small>{% endif %}
|
||||
</td>
|
||||
<td class="actions">
|
||||
<a href="/replicas/{{ r.id }}" role="button" class="secondary outline" style="padding:0.2em 0.6em; font-size:0.8em;">Detail</a>
|
||||
<button class="outline" style="padding:0.2em 0.6em; font-size:0.8em;"
|
||||
<a href="/replicas/{{ r.id }}" role="button" class="secondary outline" style="padding:.25em .65em; font-size:.78rem;">Detail</a>
|
||||
<button class="outline" style="padding:.25em .65em; font-size:.78rem;"
|
||||
hx-post="/api/replicas/{{ r.id }}/test"
|
||||
hx-target="#test-result-{{ r.id }}"
|
||||
hx-swap="innerHTML">
|
||||
Test
|
||||
</button>
|
||||
{% if r.suspended_at %}
|
||||
<button class="contrast outline" style="padding:0.2em 0.6em; font-size:0.8em;"
|
||||
<button class="contrast outline" style="padding:.25em .65em; font-size:.78rem;"
|
||||
hx-post="/api/replicas/{{ r.id }}/unsuspend"
|
||||
hx-on::after-request="window.location.reload()">
|
||||
Re-enable
|
||||
</button>
|
||||
{% endif %}
|
||||
<button class="secondary outline" style="padding:0.2em 0.6em; font-size:0.8em;"
|
||||
<button class="secondary outline" style="padding:.25em .65em; font-size:.78rem;"
|
||||
onclick="openEditModal({{ r.id }}, '{{ r.name }}', '{{ r.url }}', {{ r.enabled|lower }}, {{ r.sync_interval_seconds or 'null' }})">
|
||||
Edit
|
||||
</button>
|
||||
<button class="contrast outline" style="padding:0.2em 0.6em; font-size:0.8em;"
|
||||
<button class="contrast outline" style="padding:.25em .65em; font-size:.78rem;"
|
||||
hx-delete="/api/replicas/{{ r.id }}"
|
||||
hx-confirm="Delete replica {{ r.name }}? This also removes its sync map."
|
||||
hx-on::after-request="window.location.reload()">
|
||||
Delete
|
||||
</button>
|
||||
<span id="test-result-{{ r.id }}" style="font-size:0.8em;"></span>
|
||||
<span id="test-result-{{ r.id }}" style="font-size:.78rem;"></span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@@ -79,102 +79,94 @@
|
||||
<p class="muted" style="margin-bottom:1.5rem;">No replicas configured yet.</p>
|
||||
{% endif %}
|
||||
|
||||
<hr style="margin:2rem 0;">
|
||||
|
||||
<h3 style="margin-bottom:0.25rem;">Add Replica</h3>
|
||||
<p style="margin-top:0; margin-bottom:1.5rem;"><small class="muted">The controller will test the connection before saving.</small></p>
|
||||
<h3>Add Replica</h3>
|
||||
|
||||
{% if env_replicas %}
|
||||
<div style="margin-bottom:1.25rem; padding:0.75rem 1rem; background:var(--pico-card-background-color); border:1px solid var(--pico-muted-border-color); border-radius:var(--pico-border-radius);">
|
||||
<small style="font-weight:600; text-transform:uppercase; letter-spacing:0.05em; color:var(--pico-muted-color);">Detected in pngx.env — not yet added</small>
|
||||
<div style="display:flex; flex-wrap:wrap; gap:0.5rem; margin-top:0.5rem;">
|
||||
<div class="env-detected">
|
||||
<div class="env-detected-label">Detected in pngx.env — not yet added</div>
|
||||
<div style="display:flex; flex-wrap:wrap; gap:.4rem; margin-top:.4rem;">
|
||||
{% for er in env_replicas %}
|
||||
{% set er_name = er.safe | lower | replace('_', '-') %}
|
||||
<button type="button" class="outline" style="font-size:0.85em; padding:0.3em 0.8em;"
|
||||
<button type="button" class="outline" style="font-size:.8rem; padding:.3em .75em;"
|
||||
onclick="prefillAdd('{{ er_name }}', '{{ er.url }}', '{{ er.token }}')">
|
||||
+ {{ er.safe }} <small style="opacity:0.6; margin-left:0.25em;">{{ er.url }}</small>
|
||||
+ {{ er.safe }} <span style="opacity:.5; margin-left:.2em; font-size:.75em;">{{ er.url }}</span>
|
||||
</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div style="display:grid; grid-template-columns:1fr 360px; gap:2rem; align-items:start;">
|
||||
<div class="grid-form-sidebar">
|
||||
|
||||
<!-- Add form -->
|
||||
<article style="margin:0; padding:1.5rem;">
|
||||
<article style="margin:0;">
|
||||
<p style="margin-bottom:1rem; font-size:.8rem; color:var(--text-3);">The controller will test the connection before saving.</p>
|
||||
<form id="add-replica-form" onsubmit="submitAddReplica(event)">
|
||||
<div style="display:grid; grid-template-columns:1fr 1fr; gap:1rem;">
|
||||
<div class="grid-2">
|
||||
<label>
|
||||
Display name
|
||||
<input type="text" name="name" required placeholder="e.g. berlin-office">
|
||||
<small class="muted">Short identifier shown in the dashboard.</small>
|
||||
<small>Short identifier shown in the dashboard.</small>
|
||||
</label>
|
||||
<label>
|
||||
Instance URL
|
||||
<input type="url" name="url" required placeholder="http://100.x.x.x:8000">
|
||||
<small class="muted">Must be reachable from this controller.</small>
|
||||
<small>Must be reachable from this controller.</small>
|
||||
</label>
|
||||
</div>
|
||||
<div style="display:grid; grid-template-columns:1fr 1fr; gap:1rem; margin-top:0.5rem;">
|
||||
<div class="grid-2" style="margin-top:.75rem;">
|
||||
<label>
|
||||
API token
|
||||
<input type="password" name="api_token" required autocomplete="off">
|
||||
<small class="muted">From Paperless → Settings → API → Auth Tokens.</small>
|
||||
<small>From Paperless → Settings → API → Auth Tokens.</small>
|
||||
</label>
|
||||
<label>
|
||||
Sync interval
|
||||
<input type="number" name="sync_interval_seconds" placeholder="Leave blank to use global" min="60">
|
||||
<small class="muted">Seconds between syncs. Overrides the global setting.</small>
|
||||
<small>Seconds between syncs. Overrides the global setting.</small>
|
||||
</label>
|
||||
</div>
|
||||
<div style="margin-top:1rem;">
|
||||
<label style="display:flex; align-items:center; gap:0.5rem; cursor:pointer;">
|
||||
<input type="checkbox" name="enabled" checked style="margin:0;">
|
||||
<div style="margin-top:.9rem;">
|
||||
<label style="display:flex; align-items:center; gap:.5rem; cursor:pointer; font-size:.85rem;">
|
||||
<input type="checkbox" name="enabled" checked>
|
||||
Enable immediately after adding
|
||||
</label>
|
||||
</div>
|
||||
<div id="add-test-result" style="margin:0.75rem 0; min-height:1.5em;"></div>
|
||||
<div id="add-test-result" style="margin:.75rem 0; min-height:1.5em;"></div>
|
||||
<button type="submit" style="margin-bottom:0;">Add Replica</button>
|
||||
</form>
|
||||
</article>
|
||||
|
||||
<!-- Help sidebar -->
|
||||
<aside>
|
||||
<article style="margin:0; padding:1.25rem; background:var(--pico-card-sectionning-background-color);">
|
||||
<p style="margin:0 0 1rem; font-weight:600; font-size:0.95em;">Before you add a replica</p>
|
||||
|
||||
<ol style="margin:0 0 1.25rem; padding-left:1.25rem; display:flex; flex-direction:column; gap:0.75rem;">
|
||||
<li>
|
||||
<strong>Verify reachability</strong><br>
|
||||
<small class="muted">The replica's URL must be reachable from this controller. On Tailscale, use the replica's Tailscale IP and port.</small>
|
||||
<article style="margin:0; padding:1.1rem;">
|
||||
<p style="margin:0 0 .9rem; font-weight:600; font-size:.875rem; color:var(--text);">Before you add a replica</p>
|
||||
<ol style="margin:0 0 1.1rem; padding-left:1.25rem; display:flex; flex-direction:column; gap:.65rem;">
|
||||
<li style="font-size:.83rem; color:var(--text-2);">
|
||||
<strong style="color:var(--text); display:block;">Verify reachability</strong>
|
||||
The replica’s URL must be reachable from this controller. On Tailscale, use the Tailscale IP and port.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Generate an API token</strong><br>
|
||||
<small class="muted">In the replica's Paperless UI go to <strong>Settings → API → Auth Tokens</strong> and create a token for a superuser account.</small>
|
||||
<li style="font-size:.83rem; color:var(--text-2);">
|
||||
<strong style="color:var(--text); display:block;">Generate an API token</strong>
|
||||
In the replica’s Paperless UI go to <strong>Settings → API → Auth Tokens</strong> and create a token for a superuser account.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Decide on sync interval</strong><br>
|
||||
<small class="muted">Leave blank to use the global interval (set in Settings). Override per-replica if this instance needs a different cadence.</small>
|
||||
<li style="font-size:.83rem; color:var(--text-2);">
|
||||
<strong style="color:var(--text); display:block;">Decide on sync interval</strong>
|
||||
Leave blank to use the global interval (set in Settings). Override per-replica if this instance needs a different cadence.
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<hr style="margin:1rem 0;">
|
||||
|
||||
<p style="margin:0 0 0.5rem; font-weight:600; font-size:0.9em;">Replica already has documents?</p>
|
||||
<p style="margin:0; font-size:0.875em; color:var(--pico-muted-color);">
|
||||
Add it normally — then open its <strong>Detail</strong> page and run
|
||||
<hr style="margin:.85rem 0;">
|
||||
<p style="margin:0 0 .4rem; font-weight:600; font-size:.83rem; color:var(--text);">Replica already has documents?</p>
|
||||
<p style="margin:0; font-size:.8rem; color:var(--text-2);">
|
||||
Add it normally — then open its <strong>Detail</strong> page and run
|
||||
<strong>Reconcile</strong> before the first sync. Reconcile matches
|
||||
existing documents to the master without re-uploading them, preventing
|
||||
duplicates.
|
||||
existing documents to the master without re-uploading them, preventing duplicates.
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article style="margin:0.75rem 0 0; padding:1rem 1.25rem; border-left:3px solid var(--pico-primary);">
|
||||
<p style="margin:0; font-size:0.875em;">
|
||||
<strong>What gets synced?</strong><br>
|
||||
<span class="muted">Documents, tags, correspondents, document types, and custom fields.
|
||||
The master always wins — replicas are read-only by convention.</span>
|
||||
<article style="margin:.75rem 0 0; padding:.9rem 1.1rem; border-left:3px solid var(--accent);">
|
||||
<p style="margin:0; font-size:.8rem; color:var(--text-2);">
|
||||
<strong style="color:var(--text); display:block; margin-bottom:.2rem;">What gets synced?</strong>
|
||||
Documents, tags, correspondents, document types, and custom fields.
|
||||
The master always wins — replicas are read-only by convention.
|
||||
</p>
|
||||
</article>
|
||||
</aside>
|
||||
@@ -185,26 +177,29 @@
|
||||
<dialog id="edit-modal">
|
||||
<article>
|
||||
<header>
|
||||
<h3 style="margin:0;">Edit Replica</h3>
|
||||
<button aria-label="Close" onclick="document.getElementById('edit-modal').close()"></button>
|
||||
<h3>Edit Replica</h3>
|
||||
</header>
|
||||
<form id="edit-replica-form" onsubmit="submitEditReplica(event)">
|
||||
<input type="hidden" name="id">
|
||||
<label>Display name <input type="text" name="name" required></label>
|
||||
<label>Instance URL <input type="url" name="url" required></label>
|
||||
<label>
|
||||
API Token
|
||||
<input type="password" name="api_token" placeholder="Leave blank to keep current" autocomplete="off">
|
||||
<small class="muted">Only fill this if you want to update the token.</small>
|
||||
</label>
|
||||
<label>
|
||||
Sync interval (seconds)
|
||||
<input type="number" name="sync_interval_seconds" min="60" placeholder="Leave blank to use global">
|
||||
</label>
|
||||
<label style="display:flex; align-items:center; gap:0.5rem; cursor:pointer;">
|
||||
<input type="checkbox" name="enabled" style="margin:0;"> Enabled
|
||||
</label>
|
||||
<div id="edit-test-result" style="margin:0.5rem 0; min-height:1.5em;"></div>
|
||||
<footer style="display:flex; justify-content:flex-end; gap:0.5rem; padding:0; margin-top:1rem;">
|
||||
<form id="edit-replica-form" onsubmit="submitEditReplica(event)" style="padding:0;">
|
||||
<div style="padding:1.15rem 1.4rem; display:flex; flex-direction:column; gap:.75rem;">
|
||||
<input type="hidden" name="id">
|
||||
<label>Display name <input type="text" name="name" required></label>
|
||||
<label>Instance URL <input type="url" name="url" required></label>
|
||||
<label>
|
||||
API Token
|
||||
<input type="password" name="api_token" placeholder="Leave blank to keep current" autocomplete="off">
|
||||
<small>Only fill this if you want to update the token.</small>
|
||||
</label>
|
||||
<label>
|
||||
Sync interval (seconds)
|
||||
<input type="number" name="sync_interval_seconds" min="60" placeholder="Leave blank to use global">
|
||||
</label>
|
||||
<label style="display:flex; align-items:center; gap:.5rem; cursor:pointer; font-size:.85rem; margin-top:.1rem;">
|
||||
<input type="checkbox" name="enabled"> Enabled
|
||||
</label>
|
||||
<div id="edit-test-result" style="min-height:1.5em;"></div>
|
||||
</div>
|
||||
<footer style="display:flex; justify-content:flex-end; gap:.5rem;">
|
||||
<button type="button" class="secondary outline" onclick="document.getElementById('edit-modal').close()">Cancel</button>
|
||||
<button type="submit">Save Changes</button>
|
||||
</footer>
|
||||
@@ -233,7 +228,7 @@ async function submitAddReplica(e) {
|
||||
const resultEl = document.getElementById('add-test-result');
|
||||
const btn = form.querySelector('button[type=submit]');
|
||||
btn.setAttribute('aria-busy', 'true');
|
||||
btn.textContent = 'Testing connection…';
|
||||
btn.textContent = 'Testing connection\u2026';
|
||||
const r = await fetch('/api/replicas', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body)});
|
||||
const json = await r.json();
|
||||
btn.removeAttribute('aria-busy');
|
||||
@@ -271,7 +266,7 @@ async function submitEditReplica(e) {
|
||||
const resultEl = document.getElementById('edit-test-result');
|
||||
const btn = form.querySelector('button[type=submit]');
|
||||
btn.setAttribute('aria-busy', 'true');
|
||||
btn.textContent = 'Saving…';
|
||||
btn.textContent = 'Saving\u2026';
|
||||
const r = await fetch('/api/replicas/' + id, {method:'PUT', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body)});
|
||||
const json = await r.json();
|
||||
btn.removeAttribute('aria-busy');
|
||||
|
||||
Reference in New Issue
Block a user