feat: add persistence and audit trails
This commit is contained in:
1 parent
d381c02946
commit
2f2e9ab3e3
9 files changed
+633
-151
No files matched your search
+63
-5
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user