feat: add persistence and audit trails

This commit is contained in:
truetype committed 2026-02-05 11:07:20 +01:00
1 parent d381c02946
commit 2f2e9ab3e3
9 files changed
+633 -151

No files matched your search

+63 -5
View File
@@ -2,12 +2,12 @@ const portsTable = document.getElementById("ports-table");
const tunnelsTable = document.getElementById("tunnels-table");
const refreshPortsButton = document.getElementById("refresh-ports");
const refreshTunnelsButton = document.getElementById("refresh-tunnels");
const refreshEventsButton = document.getElementById("refresh-events");
const debugPanel = document.getElementById("debug-panel");
const debugTitle = document.getElementById("debug-title");
const debugContent = document.getElementById("debug-content");
const REFRESH_INTERVAL_MS = 4000;
let latestTunnels = [];
async function fetchJson(url, options) {
const response = await fetch(url, options);
@@ -50,6 +50,17 @@ function hideDebugPanel() {
debugContent.textContent = "";
}
function formatTimestamp(value) {
if (!value) {
return "-";
}
try {
return new Date(value * 1000).toLocaleString();
} catch {
return value;
}
}
function renderPorts(ports, tunnels) {
if (!ports.length) {
portsTable.innerHTML = "<p>Nessuna porta in ascolto trovata.</p>";
@@ -65,7 +76,7 @@ function renderPorts(ports, tunnels) {
.map((port) => {
const tunnel = tunnelMap.get(port.port);
const busy = Boolean(tunnel && tunnel.status !== "stopped");
const statusLabel = busy ? (tunnel.status || "running") : "libero";
const statusLabel = busy ? escapeHtml(tunnel.status || "running") : "libero";
const hasCommand = Boolean(port.command);
const commandCell = hasCommand
? `<span class="mono">${escapeHtml(port.command)}</span>`
@@ -76,6 +87,12 @@ function renderPorts(ports, tunnels) {
const local = escapeHtml(port.local_address || "-");
const service = escapeHtml(port.service || "-");
const protocol = escapeHtml((port.protocol || "tcp").toUpperCase());
const policyText = port.allowed
? "consentita"
: escapeHtml(port.policy_reason || "non autorizzata");
const policyClass = port.allowed ? "status-free" : "status-busy";
const startDisabled = busy || !port.allowed;
const startTitle = !port.allowed && port.policy_reason ? escapeHtml(port.policy_reason) : "";
return `
<div class="row">
@@ -90,7 +107,12 @@ function renderPorts(ports, tunnels) {
<span class="status-pill ${busy ? "status-busy" : "status-free"}">${statusLabel}</span>
</div>
<div class="cell">
<button data-port="${port.port}" class="start-button" ${busy ? "disabled" : ""}>Avvia tunnel</button>
<span class="status-pill ${policyClass}">${policyText}</span>
</div>
<div class="cell">
<button data-port="${port.port}" class="start-button" ${startDisabled ? "disabled" : ""} ${
startTitle ? `title="${startTitle}"` : ""
}>Avvia tunnel</button>
</div>
</div>
`;
@@ -107,6 +129,7 @@ function renderPorts(ports, tunnels) {
<div class="cell">Container</div>
<div class="cell">Indirizzo locale</div>
<div class="cell">Tunnel</div>
<div class="cell">Policy</div>
<div class="cell">Azioni</div>
</div>
${rows}
@@ -206,15 +229,49 @@ function renderTunnels(tunnels) {
});
}
function renderEvents(events) {
if (!events.length) {
eventsTable.innerHTML = "<p>Nessun evento registrato.</p>";
return;
}
const rows = events
.map((event) => {
const status = escapeHtml(event.status || "-");
const message = escapeHtml(event.message || "-");
const timestamp = formatTimestamp(event.created_at);
return `
<div class="row">
<div class="cell">${event.port || "-"}</div>
<div class="cell">${status}</div>
<div class="cell">${message}</div>
<div class="cell">${timestamp}</div>
</div>
`;
})
.join("");
eventsTable.innerHTML = `
<div class="row header">
<div class="cell">Porta</div>
<div class="cell">Evento</div>
<div class="cell">Messaggio</div>
<div class="cell">Data</div>
</div>
${rows}
`;
}
async function refreshState() {
try {
const [ports, tunnels] = await Promise.all([
const [ports, tunnels, events] = await Promise.all([
fetchJson("/api/ports"),
fetchJson("/api/tunnels"),
fetchJson("/api/history/events"),
]);
latestTunnels = tunnels;
renderPorts(ports, tunnels);
renderTunnels(tunnels);
renderEvents(events);
} catch (error) {
portsTable.innerHTML = `<p>Errore: ${error.message}</p>`;
tunnelsTable.innerHTML = `<p>Errore: ${error.message}</p>`;
@@ -223,6 +280,7 @@ async function refreshState() {
refreshPortsButton.addEventListener("click", refreshState);
refreshTunnelsButton.addEventListener("click", refreshState);
refreshEventsButton.addEventListener("click", refreshState);
document.getElementById("debug-close").addEventListener("click", hideDebugPanel);
refreshState();
+6 -1
View File
@@ -91,7 +91,7 @@ button:disabled {
}
.table.ports .row {
grid-template-columns: 0.6fr 0.7fr 1.3fr 0.9fr 2fr 1fr 1fr 0.8fr 0.9fr;
grid-template-columns: 0.6fr 0.7fr 1.3fr 0.9fr 2fr 1fr 1fr 0.8fr 0.9fr 0.8fr;
align-items: center;
}
@@ -100,6 +100,11 @@ button:disabled {
align-items: center;
}
.table.events .row {
grid-template-columns: 0.7fr 1fr 2fr 1fr;
align-items: center;
}
.status-pill {
padding: 4px 10px;
border-radius: 999px;