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,56 +2,64 @@
|
||||
{% block title %}Logs — pngx-controller{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<h2>Logs</h2>
|
||||
<div class="page-hd">
|
||||
<h2>Logs</h2>
|
||||
<div class="page-actions">
|
||||
<button class="secondary outline"
|
||||
hx-delete="/api/logs?older_than_days=90"
|
||||
hx-confirm="Delete logs older than 90 days?"
|
||||
hx-on::after-request="window.location.reload()">
|
||||
Clear old logs
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex; gap:1rem; flex-wrap:wrap; align-items:flex-end; margin-bottom:1rem;">
|
||||
<label style="flex:1; min-width:150px;">
|
||||
<div style="display:flex; gap:.85rem; flex-wrap:wrap; align-items:flex-end; margin-bottom:1.25rem;">
|
||||
<label style="flex:1; min-width:140px; margin:0;">
|
||||
Replica
|
||||
<select id="filter-replica" onchange="applyFilters()">
|
||||
<option value="">All</option>
|
||||
<option value="">All replicas</option>
|
||||
{% for r in replicas %}
|
||||
<option value="{{ r.id }}">{{ r.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label style="flex:1; min-width:120px;">
|
||||
<label style="flex:1; min-width:120px; margin:0;">
|
||||
Level
|
||||
<select id="filter-level" onchange="applyFilters()">
|
||||
<option value="">All</option>
|
||||
<option value="">All levels</option>
|
||||
<option value="info">Info</option>
|
||||
<option value="warning">Warning</option>
|
||||
<option value="error">Error</option>
|
||||
</select>
|
||||
</label>
|
||||
<label style="flex:2; min-width:200px;">
|
||||
<label style="flex:2; min-width:180px; margin:0;">
|
||||
Search
|
||||
<input type="text" id="filter-q" placeholder="Full-text search…" oninput="debounceFilter()">
|
||||
<input type="text" id="filter-q" placeholder="Full-text search…" oninput="debounceFilter()">
|
||||
</label>
|
||||
<button class="secondary outline" onclick="applyFilters()">Filter</button>
|
||||
<button class="contrast outline"
|
||||
hx-delete="/api/logs?older_than_days=90"
|
||||
hx-confirm="Delete logs older than 90 days?"
|
||||
hx-on::after-request="window.location.reload()">
|
||||
Clear old logs
|
||||
<button class="secondary outline" onclick="applyFilters()" style="align-self:flex-end; margin-bottom:0;">
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><circle cx="5" cy="5" r="3.5" stroke="currentColor" stroke-width="1.4"/><path d="M8.5 8.5L11 11" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
|
||||
Filter
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Live SSE tail -->
|
||||
<h3>Live stream</h3>
|
||||
|
||||
<details open id="live-section">
|
||||
<summary>Live log stream</summary>
|
||||
<div id="sse-log"
|
||||
hx-ext="sse"
|
||||
sse-connect="/api/logs/stream"
|
||||
style="font-family:monospace; font-size:0.8em; max-height:300px; overflow-y:auto; border:1px solid var(--pico-muted-border-color); padding:0.5rem; border-radius:0.5rem; background:var(--pico-code-background-color);">
|
||||
class="sse-terminal"
|
||||
style="margin-top:.75rem;">
|
||||
<div sse-swap="message" hx-swap="afterbegin">
|
||||
<span class="muted">Connecting to log stream…</span>
|
||||
<span class="muted">Connecting to log stream…</span>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<hr>
|
||||
<h3>Log history</h3>
|
||||
|
||||
<!-- Static log table -->
|
||||
<div id="log-table-area">
|
||||
{% include "partials/log_table.html" %}
|
||||
</div>
|
||||
@@ -76,39 +84,41 @@ function applyFilters() {
|
||||
target: '#log-table-area',
|
||||
swap: 'innerHTML',
|
||||
handler: function(elt, info) {
|
||||
// render JSON as table
|
||||
const data = JSON.parse(info.xhr.responseText);
|
||||
const rows = data.map(l => `
|
||||
<tr class="log-entry-${l.level || 'info'}">
|
||||
<td><small>${l.created_at || ''}</small></td>
|
||||
<td><small style="font-family:var(--mono);">${l.created_at || ''}</small></td>
|
||||
<td><span class="badge badge-${l.level}">${l.level || ''}</span></td>
|
||||
<td>${l.replica_id || ''}</td>
|
||||
<td>${l.doc_id || ''}</td>
|
||||
<td>${l.message || ''}</td>
|
||||
<td style="font-size:.82rem;">${l.replica_id || ''}</td>
|
||||
<td style="font-family:var(--mono); font-size:.8rem;">${l.doc_id || ''}</td>
|
||||
<td style="font-size:.83rem;">${l.message || ''}</td>
|
||||
</tr>`).join('');
|
||||
elt.innerHTML = `<table><thead><tr><th>Time</th><th>Level</th><th>Replica</th><th>Doc</th><th>Message</th></tr></thead><tbody>${rows}</tbody></table>`;
|
||||
elt.innerHTML = `<div class="overflow-auto"><table><thead><tr><th>Time</th><th>Level</th><th>Replica</th><th>Doc</th><th>Message</th></tr></thead><tbody>${rows}</tbody></table></div>`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Pretty-print SSE messages
|
||||
document.body.addEventListener('htmx:sseMessage', function(e) {
|
||||
try {
|
||||
const data = JSON.parse(e.detail.data);
|
||||
const el = document.createElement('div');
|
||||
const color = data.level === 'error' ? '#f87171' : data.level === 'warning' ? '#fbbf24' : '#6ee7b7';
|
||||
const color = data.level === 'error' ? 'var(--err)' : data.level === 'warning' ? 'var(--warn)' : 'var(--ok)';
|
||||
el.style.color = color;
|
||||
el.innerHTML = `[${data.ts}] ${data.level?.toUpperCase()} ${data.replica_id ? '[r:'+data.replica_id+']' : ''} ${data.doc_id ? '[d:'+data.doc_id+']' : ''} ${data.message}`;
|
||||
el.style.marginBottom = '.1rem';
|
||||
const ts = data.ts || '';
|
||||
const lvl = (data.level || 'info').toUpperCase();
|
||||
const rep = data.replica_id ? ' [r:' + data.replica_id + ']' : '';
|
||||
const doc = data.doc_id ? ' [d:' + data.doc_id + ']' : '';
|
||||
el.textContent = '[' + ts + '] ' + lvl + rep + doc + ' ' + (data.message || '');
|
||||
const container = document.querySelector('#sse-log div[sse-swap]');
|
||||
if (container) {
|
||||
const first = container.querySelector('span.muted');
|
||||
if (first) first.remove();
|
||||
container.insertBefore(el, container.firstChild);
|
||||
// Keep max 200 entries
|
||||
while (container.children.length > 200) container.removeChild(container.lastChild);
|
||||
}
|
||||
} catch(err) {}
|
||||
return false; // prevent default HTMX swap
|
||||
return false;
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user