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,84 +2,107 @@
|
||||
{% block title %}Settings — pngx-controller{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<h2>Settings</h2>
|
||||
<div class="page-hd">
|
||||
<h2>Settings</h2>
|
||||
</div>
|
||||
|
||||
<form onsubmit="saveSettings(event)">
|
||||
|
||||
<h3>Master Instance</h3>
|
||||
<div style="display:grid; grid-template-columns:1fr 1fr; gap:1rem;">
|
||||
<label>
|
||||
Master URL
|
||||
<input type="url" name="master_url" value="{{ settings.get('master_url', '') }}" placeholder="http://100.x.x.x:8000">
|
||||
</label>
|
||||
<label>
|
||||
Master API Token
|
||||
<input type="password" name="master_token" value="{{ env_master_token }}" placeholder="leave blank to keep current">
|
||||
</label>
|
||||
</div>
|
||||
<article style="margin-bottom:1.5rem;">
|
||||
<div class="grid-2">
|
||||
<label>
|
||||
Master URL
|
||||
<input type="url" name="master_url" value="{{ settings.get('master_url', '') }}" placeholder="http://100.x.x.x:8000">
|
||||
<small>Paperless-ngx instance that acts as the source of truth.</small>
|
||||
</label>
|
||||
<label>
|
||||
Master API Token
|
||||
<input type="password" name="master_token" value="{{ env_master_token }}" placeholder="leave blank to keep current">
|
||||
<small>From Paperless → Settings → API → Auth Tokens.</small>
|
||||
</label>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<h3>Sync Engine</h3>
|
||||
<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(200px, 1fr)); gap:1rem;">
|
||||
<label>
|
||||
Sync Interval (seconds)
|
||||
<input type="number" name="sync_interval_seconds" value="{{ settings.get('sync_interval_seconds', '900') }}" min="60">
|
||||
</label>
|
||||
<label>
|
||||
Cycle Timeout (seconds)
|
||||
<input type="number" name="sync_cycle_timeout_seconds" value="{{ settings.get('sync_cycle_timeout_seconds', '1800') }}" min="60">
|
||||
</label>
|
||||
<label>
|
||||
Task Poll Timeout (seconds)
|
||||
<input type="number" name="task_poll_timeout_seconds" value="{{ settings.get('task_poll_timeout_seconds', '600') }}" min="30">
|
||||
</label>
|
||||
<label>
|
||||
Max Concurrent Requests
|
||||
<input type="number" name="max_concurrent_requests" value="{{ settings.get('max_concurrent_requests', '4') }}" min="1" max="20">
|
||||
</label>
|
||||
<label>
|
||||
Suspend Threshold (consecutive failures)
|
||||
<input type="number" name="replica_suspend_threshold" value="{{ settings.get('replica_suspend_threshold', '5') }}" min="1">
|
||||
</label>
|
||||
<label>
|
||||
Log Retention (days)
|
||||
<input type="number" name="log_retention_days" value="{{ settings.get('log_retention_days', '90') }}" min="1">
|
||||
</label>
|
||||
</div>
|
||||
<article style="margin-bottom:1.5rem;">
|
||||
<div class="grid-auto">
|
||||
<label>
|
||||
Sync Interval (seconds)
|
||||
<input type="number" name="sync_interval_seconds" value="{{ settings.get('sync_interval_seconds', '900') }}" min="60">
|
||||
<small>How often the scheduler runs a sync cycle.</small>
|
||||
</label>
|
||||
<label>
|
||||
Cycle Timeout (seconds)
|
||||
<input type="number" name="sync_cycle_timeout_seconds" value="{{ settings.get('sync_cycle_timeout_seconds', '1800') }}" min="60">
|
||||
<small>Maximum wall-clock time for a full sync cycle.</small>
|
||||
</label>
|
||||
<label>
|
||||
Task Poll Timeout (seconds)
|
||||
<input type="number" name="task_poll_timeout_seconds" value="{{ settings.get('task_poll_timeout_seconds', '600') }}" min="30">
|
||||
<small>Timeout waiting for Paperless task completion.</small>
|
||||
</label>
|
||||
<label>
|
||||
Max Concurrent Requests
|
||||
<input type="number" name="max_concurrent_requests" value="{{ settings.get('max_concurrent_requests', '4') }}" min="1" max="20">
|
||||
<small>Semaphore limit for parallel API calls.</small>
|
||||
</label>
|
||||
<label>
|
||||
Suspend Threshold
|
||||
<input type="number" name="replica_suspend_threshold" value="{{ settings.get('replica_suspend_threshold', '5') }}" min="1">
|
||||
<small>Consecutive failures before a replica is suspended.</small>
|
||||
</label>
|
||||
<label>
|
||||
Log Retention (days)
|
||||
<input type="number" name="log_retention_days" value="{{ settings.get('log_retention_days', '90') }}" min="1">
|
||||
<small>Automatically prune log entries older than this.</small>
|
||||
</label>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<h3>Notifications</h3>
|
||||
<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(200px, 1fr)); gap:1rem;">
|
||||
<label>
|
||||
Alert Target
|
||||
<select name="alert_target_type">
|
||||
<option value="" {% if not settings.get('alert_target_type') %}selected{% endif %}>Disabled</option>
|
||||
<option value="gotify" {% if settings.get('alert_target_type') == 'gotify' %}selected{% endif %}>Gotify</option>
|
||||
<option value="webhook" {% if settings.get('alert_target_type') == 'webhook' %}selected{% endif %}>Webhook</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Alert URL
|
||||
<input type="url" name="alert_target_url" value="{{ settings.get('alert_target_url', '') }}" placeholder="https://…">
|
||||
</label>
|
||||
<label>
|
||||
Alert Token / Auth Header
|
||||
<input type="password" name="alert_target_token" value="{{ env_alert_token }}" placeholder="leave blank to keep current">
|
||||
</label>
|
||||
<label>
|
||||
Alert Error Threshold (docs failed)
|
||||
<input type="number" name="alert_error_threshold" value="{{ settings.get('alert_error_threshold', '5') }}" min="1">
|
||||
</label>
|
||||
<label>
|
||||
Alert Cooldown (seconds)
|
||||
<input type="number" name="alert_cooldown_seconds" value="{{ settings.get('alert_cooldown_seconds', '3600') }}" min="60">
|
||||
</label>
|
||||
</div>
|
||||
<article style="margin-bottom:1.5rem;">
|
||||
<div class="grid-auto">
|
||||
<label>
|
||||
Alert Target
|
||||
<select name="alert_target_type">
|
||||
<option value="" {% if not settings.get('alert_target_type') %}selected{% endif %}>Disabled</option>
|
||||
<option value="gotify" {% if settings.get('alert_target_type') == 'gotify' %}selected{% endif %}>Gotify</option>
|
||||
<option value="webhook" {% if settings.get('alert_target_type') == 'webhook' %}selected{% endif %}>Webhook</option>
|
||||
</select>
|
||||
<small>Where to send sync failure alerts.</small>
|
||||
</label>
|
||||
<label>
|
||||
Alert URL
|
||||
<input type="url" name="alert_target_url" value="{{ settings.get('alert_target_url', '') }}" placeholder="https://…">
|
||||
<small>Gotify server URL or webhook endpoint.</small>
|
||||
</label>
|
||||
<label>
|
||||
Alert Token / Auth Header
|
||||
<input type="password" name="alert_target_token" value="{{ env_alert_token }}" placeholder="leave blank to keep current">
|
||||
<small>Gotify token or Authorization header value.</small>
|
||||
</label>
|
||||
<label>
|
||||
Alert Error Threshold
|
||||
<input type="number" name="alert_error_threshold" value="{{ settings.get('alert_error_threshold', '5') }}" min="1">
|
||||
<small>Number of failed docs before an alert is sent.</small>
|
||||
</label>
|
||||
<label>
|
||||
Alert Cooldown (seconds)
|
||||
<input type="number" name="alert_cooldown_seconds" value="{{ settings.get('alert_cooldown_seconds', '3600') }}" min="60">
|
||||
<small>Minimum time between repeated alerts.</small>
|
||||
</label>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<div style="margin-top:1.5rem; display:flex; gap:0.5rem; align-items:center; flex-wrap:wrap;">
|
||||
<div style="display:flex; gap:.5rem; align-items:center; flex-wrap:wrap;">
|
||||
<button type="submit" id="save-btn">Save Settings</button>
|
||||
<button type="button" id="test-btn" class="secondary outline" onclick="testConnection()">
|
||||
Test Connection
|
||||
</button>
|
||||
<span id="settings-result"></span>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<script>
|
||||
@@ -97,9 +120,7 @@ function formBody() {
|
||||
return body;
|
||||
}
|
||||
|
||||
function setResult(html) {
|
||||
document.getElementById('settings-result').innerHTML = html;
|
||||
}
|
||||
function setResult(html) { document.getElementById('settings-result').innerHTML = html; }
|
||||
|
||||
function setLoading(btn, label) {
|
||||
btn.setAttribute('aria-busy', 'true');
|
||||
@@ -117,7 +138,7 @@ function clearLoading(btn) {
|
||||
async function saveSettings(e) {
|
||||
e.preventDefault();
|
||||
const btn = document.getElementById('save-btn');
|
||||
setLoading(btn, 'Saving…');
|
||||
setLoading(btn, 'Saving\u2026');
|
||||
setResult('');
|
||||
try {
|
||||
const r = await fetch('/api/settings', {
|
||||
@@ -127,13 +148,13 @@ async function saveSettings(e) {
|
||||
});
|
||||
const json = await r.json();
|
||||
if (r.ok) {
|
||||
setResult('<span class="badge badge-ok">✓ Settings saved</span>');
|
||||
setResult('<span class="badge badge-ok">\u2713 Settings saved</span>');
|
||||
} else {
|
||||
const detail = typeof json.detail === 'string' ? json.detail : JSON.stringify(json.detail);
|
||||
setResult('<span class="badge badge-error">✗ ' + detail + '</span>');
|
||||
setResult('<span class="badge badge-error">\u2717 ' + detail + '</span>');
|
||||
}
|
||||
} catch (err) {
|
||||
setResult('<span class="badge badge-error">✗ Network error: ' + err.message + '</span>');
|
||||
setResult('<span class="badge badge-error">\u2717 Network error: ' + err.message + '</span>');
|
||||
} finally {
|
||||
clearLoading(btn);
|
||||
}
|
||||
@@ -141,7 +162,7 @@ async function saveSettings(e) {
|
||||
|
||||
async function testConnection() {
|
||||
const btn = document.getElementById('test-btn');
|
||||
setLoading(btn, 'Testing…');
|
||||
setLoading(btn, 'Testing\u2026');
|
||||
setResult('');
|
||||
try {
|
||||
const form = document.querySelector('form');
|
||||
@@ -154,12 +175,12 @@ async function testConnection() {
|
||||
});
|
||||
const data = await r.json();
|
||||
if (data.ok) {
|
||||
setResult('<span class="badge badge-ok">✓ Connected — ' + data.doc_count + ' docs · ' + data.latency_ms + 'ms</span>');
|
||||
setResult('<span class="badge badge-ok">\u2713 Connected — ' + data.doc_count + ' docs · ' + data.latency_ms + 'ms</span>');
|
||||
} else {
|
||||
setResult('<span class="badge badge-error">✗ ' + (data.error || 'Connection failed') + '</span>');
|
||||
setResult('<span class="badge badge-error">\u2717 ' + (data.error || 'Connection failed') + '</span>');
|
||||
}
|
||||
} catch (err) {
|
||||
setResult('<span class="badge badge-error">✗ ' + err.message + '</span>');
|
||||
setResult('<span class="badge badge-error">\u2717 ' + err.message + '</span>');
|
||||
} finally {
|
||||
clearLoading(btn);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user