Files
vacuum-wall/webui/templates/logs.html
T
mteehan dc96e15643 feat: pre-computed state store and async ACME issuance (fixes timeout mismatch)
- Add lib/state.py: in-memory state store with subsystem collectors
  (firewall, dnsmasq, nginx, acme, wireguard)
- Refactor all handlers: read from state on GET, call refresh_state()
  after mutations instead of invoking subprocesses per request
- daemon/server.py: add refresh_state(), /status/all, /status/refresh;
  populate state at startup
- webui/api/certs.py: async step-by-step ACME issuance (validate,
  issue with request_id, poll status) replacing blocking endpoint
- webui/server.py: render pages from state instead of direct lib calls
- Update templates, JS for async cert issuance with polling UI
- Update tests for state-based mocking; add test_state.py
- Fix SIM105 lint issue (contextlib.suppress)
- Add TODO.md with certificate issuance issue tracking

Resolves: WebUI 30s timeout freeze during cert issuance (Problem 1)
2026-05-30 05:46:09 +00:00

152 lines
4.9 KiB
HTML

{% extends "base.html" %}
{% block title %}Logs - Vacuum Wall{% endblock %}
{% block content %}
<div class="page-header">
<div>
<h1>System Logs</h1>
<div class="subtitle">Service logs and journal output</div>
</div>
<div class="flex items-center gap-2">
<span class="text-sm text-muted">Auto-refresh</span>
<label class="switch">
<input type="checkbox" id="auto-refresh-toggle" onchange="toggleAutoRefresh()">
<span class="slider"></span>
</label>
<span class="htmx-indicator text-sm" style="color:var(--accent);">Refreshing...</span>
</div>
</div>
<div class="tabs">
<button class="tab active" data-tab="journal" onclick="switchTab('journal')">Journal</button>
<button class="tab" data-tab="nginx-access" onclick="switchTab('nginx-access')">Nginx Access</button>
<button class="tab" data-tab="nginx-error" onclick="switchTab('nginx-error')">Nginx Error</button>
<button class="tab" data-tab="dnsmasq" onclick="switchTab('dnsmasq')">Dnsmasq</button>
<button class="tab" data-tab="app" onclick="switchTab('app')">App</button>
</div>
<div id="tab-journal" class="tab-content active">
<div class="card" style="padding:0;overflow:hidden;">
<div class="log-viewer" id="log-journal"
hx-get="/api/logs/journal"
hx-trigger="none"
hx-swap="innerHTML"
hx-indicator=".page-header .htmx-indicator">
Loading journal entries...
</div>
</div>
</div>
<div id="tab-nginx-access" class="tab-content">
<div class="card" style="padding:0;overflow:hidden;">
<div class="log-viewer" id="log-nginx-access"
hx-get="/api/logs/nginx/access"
hx-trigger="none"
hx-swap="innerHTML"
hx-indicator=".page-header .htmx-indicator">
Loading Nginx access log...
</div>
</div>
</div>
<div id="tab-nginx-error" class="tab-content">
<div class="card" style="padding:0;overflow:hidden;">
<div class="log-viewer" id="log-nginx-error"
hx-get="/api/logs/nginx/error"
hx-trigger="none"
hx-swap="innerHTML"
hx-indicator=".page-header .htmx-indicator">
Loading Nginx error log...
</div>
</div>
</div>
<div id="tab-dnsmasq" class="tab-content">
<div class="card" style="padding:0;overflow:hidden;">
<div class="log-viewer" id="log-dnsmasq"
hx-get="/api/logs/dnsmasq"
hx-trigger="none"
hx-swap="innerHTML"
hx-indicator=".page-header .htmx-indicator">
Loading dnsmasq log...
</div>
</div>
</div>
<div id="tab-app" class="tab-content">
<div class="card" style="padding:0;overflow:hidden;">
<div class="log-viewer" id="log-app"
hx-get="/api/logs/app"
hx-trigger="none"
hx-swap="innerHTML"
hx-indicator=".page-header .htmx-indicator">
Loading app log...
</div>
</div>
</div>
<script>
var refreshInterval = {{ (refresh_interval | default(15)) }};
var currentTab = 'journal';
function loadTabEl(el) {
var url = el.getAttribute('hx-get');
if (!url) return;
el.textContent = 'Loading...';
fetch(url).then(function(r) { return r.text(); })
.then(function(html) { el.innerHTML = html; })
.catch(function() { el.innerHTML = '<div class="log-line">(failed to load log)</div>'; });
}
function setActivePolling() {
document.querySelectorAll('.log-viewer').forEach(function(el) {
if (typeof htmx !== 'undefined') htmx.abort(el);
el.setAttribute('hx-trigger', 'none');
});
var activeEl = document.getElementById('log-' + currentTab);
if (activeEl) {
activeEl.setAttribute('hx-trigger', 'every ' + refreshInterval + 's');
}
if (typeof htmx !== 'undefined') htmx.process(document.body);
}
function loadActiveTab() {
var activeEl = document.getElementById('log-' + currentTab);
if (activeEl) {
loadTabEl(activeEl);
}
}
function toggleAutoRefresh() {
var toggle = document.getElementById('auto-refresh-toggle');
if (toggle.checked) {
setActivePolling();
loadActiveTab();
} else {
document.querySelectorAll('.log-viewer').forEach(function(el) {
if (typeof htmx !== 'undefined') htmx.abort(el);
el.setAttribute('hx-trigger', 'none');
});
}
}
document.addEventListener('DOMContentLoaded', function() {
loadActiveTab();
});
window.addEventListener('load', function() {
var origSwitchTab = typeof switchTab === 'function' ? switchTab : null;
switchTab = function(tabName) {
currentTab = tabName;
if (origSwitchTab) {
origSwitchTab(tabName);
}
if (document.getElementById('auto-refresh-toggle').checked) {
setActivePolling();
}
loadActiveTab();
};
});
</script>
{% endblock %}