Files
vacuum-wall/webui/static/app.js
T
mteehan e2f56b8cc8 Initial commit: SSL proxy / firewall appliance
Flask WebUI behind nginx reverse proxy with zone-based firewall, DHCP,
WireGuard, and ACME certificate management.
2026-05-07 22:24:24 +00:00

148 lines
4.7 KiB
JavaScript

// Toast notification system
function showToast(message, type = "info") {
const container = document.querySelector(".toast") || createToastContainer();
const toast = document.createElement("div");
toast.className = `toast-message toast-${type}`;
toast.textContent = message;
container.appendChild(toast);
setTimeout(() => {
toast.style.opacity = "0";
toast.style.transform = "translateX(40px)";
toast.style.transition = "all 0.3s ease";
setTimeout(() => toast.remove(), 300);
}, 5000);
}
function createToastContainer() {
const el = document.createElement("div");
el.className = "toast";
document.body.appendChild(el);
return el;
}
// Modal helpers
function openModal(id) {
const modal = document.getElementById(id);
if (modal) modal.classList.add("show");
}
function closeModal(id) {
const modal = document.getElementById(id);
if (modal) modal.classList.remove("show");
}
// Confirm dialog
function confirmAction(message, onConfirm) {
const existing = document.getElementById("confirm-modal");
if (existing) existing.remove();
const modal = document.createElement("div");
modal.id = "confirm-modal";
modal.className = "modal";
modal.innerHTML = `
<div class="modal-content">
<p class="mb-2">${message}</p>
<div style="display:flex; gap:0.75rem; justify-content:flex-end;">
<button class="btn btn-outline" id="confirm-cancel">Cancel</button>
<button class="btn btn-danger" id="confirm-ok">Confirm</button>
</div>
</div>`;
document.body.appendChild(modal);
openModal("confirm-modal");
document.getElementById("confirm-cancel").onclick = () => closeModal("confirm-modal");
modal.addEventListener("click", (e) => {
if (e.target === modal) closeModal("confirm-modal");
});
}
function setupConfirmCallback(callback) {
document.getElementById("confirm-ok")?.addEventListener("click", () => {
closeModal("confirm-modal");
callback();
});
}
// Auto-refresh with HTMX
function startAutoRefresh(endpoint, target, interval) {
const el = document.createElement("div");
el.setAttribute("hx-get", endpoint);
el.setAttribute("hx-target", `#${target}`);
el.setAttribute("hx-swap", "innerHTML");
el.setAttribute("hx-trigger", `every ${interval}s`);
el.setAttribute("hx-swap-oob", "true");
document.body.appendChild(el);
}
// Time formatting
function formatTime(seconds) {
if (seconds < 60) return `${seconds}s`;
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m`;
return `${Math.floor(seconds / 86400)}d ${Math.floor((seconds % 86400) / 3600)}h`;
}
// Bytes formatting
function formatBytes(bytes) {
if (bytes === 0) return "0 B";
const units = ["B", "KB", "MB", "GB", "TB"];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return `${(bytes / Math.pow(1024, i)).toFixed(i > 0 ? 1 : 0)} ${units[i]}`;
}
// Form helpers
function resetForm(formId) {
const form = document.getElementById(formId);
if (form) form.reset();
}
function fillForm(formId, data) {
const form = document.getElementById(formId);
if (!form) return;
for (const [key, value] of Object.entries(data)) {
const input = form.querySelector(`[name="${key}"]`);
if (input) input.value = value;
}
}
// HTMX event handlers
document.body.addEventListener("htmx:afterSwap", (evt) => {
const toastHeader = evt.detail.xhr?.getResponseHeader("X-Toast");
if (toastHeader) {
const parts = toastHeader.split(":");
const msg = parts.slice(1).join(":").trim();
showToast(msg, parts[0]?.trim() || "info");
}
});
document.body.addEventListener("htmx:responseError", (evt) => {
const status = evt.detail.xhr?.status || 0;
showToast(`Request failed (${status})`, "error");
});
document.body.addEventListener("htmx:beforeRequest", (evt) => {
const target = evt.target;
const btn = target.closest(".btn");
if (btn) {
btn.dataset.originalText = btn.textContent;
btn.disabled = true;
btn.textContent = "Loading...";
}
});
document.body.addEventListener("htmx:afterRequest", (evt) => {
const target = evt.target;
const btn = target.closest(".btn");
if (btn && btn.dataset.originalText !== undefined) {
btn.disabled = false;
btn.textContent = btn.dataset.originalText;
delete btn.dataset.originalText;
}
});
// Close on escape
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
document.querySelectorAll(".modal.show").forEach((m) => m.classList.remove("show"));
}
});