/* ============================================================ Website Checker — app.js Global utilities: modals, tabs, auto-dismiss alerts, helpers ============================================================ */ 'use strict'; /* ── Modal helpers ─────────────────────────────────────────── */ function openModal(id) { const el = document.getElementById(id); if (el) { el.classList.add('open'); document.body.style.overflow = 'hidden'; const firstInput = el.querySelector('input:not([type=hidden]), select, textarea'); if (firstInput) setTimeout(() => firstInput.focus(), 120); } } function closeModal(id) { const el = document.getElementById(id); if (el) { el.classList.remove('open'); document.body.style.overflow = ''; } } // Close modal when clicking the backdrop document.addEventListener('click', (e) => { if (e.target.classList.contains('modal-overlay')) { e.target.classList.remove('open'); document.body.style.overflow = ''; } }); // Close modal on Escape key document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { document.querySelectorAll('.modal-overlay.open').forEach(m => { m.classList.remove('open'); document.body.style.overflow = ''; }); } }); /* ── Tab switching ─────────────────────────────────────────── */ function switchTab(tabId, groupId) { const group = groupId ? document.getElementById(groupId) : document.body; group.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); group.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active')); const btn = group.querySelector(`[data-tab="${tabId}"]`); const panel = document.getElementById(tabId); if (btn) btn.classList.add('active'); if (panel) panel.classList.add('active'); } // Wire up tab buttons declared in HTML document.addEventListener('DOMContentLoaded', () => { document.querySelectorAll('.tab-btn[data-tab]').forEach(btn => { btn.addEventListener('click', () => { const tabId = btn.dataset.tab; const groupId = btn.dataset.group || null; switchTab(tabId, groupId); }); }); }); /* ── Auto-dismiss flash messages ───────────────────────────── */ document.addEventListener('DOMContentLoaded', () => { document.querySelectorAll('.alert').forEach(alert => { setTimeout(() => { alert.style.transition = 'opacity 0.5s'; alert.style.opacity = '0'; setTimeout(() => alert.remove(), 500); }, 5000); }); }); /* ── Confirm-delete helper ─────────────────────────────────── */ function confirmDelete(message, formOrUrl) { if (!confirm(message || 'Are you sure you want to delete this item?')) return false; if (typeof formOrUrl === 'string') { window.location.href = formOrUrl; } else if (formOrUrl && formOrUrl.submit) { formOrUrl.submit(); } return true; } /* ── Password visibility toggle ────────────────────────────── */ function togglePasswordVisibility(inputId, btn) { const input = document.getElementById(inputId); if (!input) return; if (input.type === 'password') { input.type = 'text'; if (btn) btn.textContent = '🙈'; } else { input.type = 'password'; if (btn) btn.textContent = '👁'; } } /* ── Dynamic credential rows (website form) ────────────────── */ function addCredentialRow(containerId) { const container = document.getElementById(containerId); if (!container) return; const index = container.querySelectorAll('.credential-entry').length; const row = document.createElement('div'); row.className = 'credential-entry'; row.innerHTML = `
`; container.appendChild(row); } /* ── Copy to clipboard ─────────────────────────────────────── */ function copyToClipboard(text, btn) { navigator.clipboard.writeText(text).then(() => { const original = btn ? btn.textContent : null; if (btn) { btn.textContent = '✓ Copied'; btn.disabled = true; } setTimeout(() => { if (btn) { btn.textContent = original; btn.disabled = false; } }, 2000); }).catch(() => { alert('Copy failed. Please copy manually.'); }); } /* ── Progress bar color logic ──────────────────────────────── */ function colorProgressBar(fill, pct) { fill.classList.remove('success', 'warning', 'danger'); if (pct >= 100) fill.classList.add('success'); else if (pct >= 50) fill.classList.add('warning'); else fill.classList.add('danger'); } document.addEventListener('DOMContentLoaded', () => { document.querySelectorAll('.progress-bar-fill[data-pct]').forEach(fill => { const pct = parseInt(fill.dataset.pct, 10); fill.style.width = pct + '%'; colorProgressBar(fill, pct); }); }); /* ── Session timeout warning ───────────────────────────────── */ (function sessionWarning() { const WARN_BEFORE_MS = 5 * 60 * 1000; // warn 5 min before expiry const SESSION_MS = 30 * 60 * 1000; // match Flask SESSION_LIFETIME let warningTimer = null; let expireTimer = null; function resetTimers() { clearTimeout(warningTimer); clearTimeout(expireTimer); warningTimer = setTimeout(() => { if (confirm('Your session will expire in 5 minutes. Click OK to stay logged in.')) { fetch('/ping', { credentials: 'same-origin' }).catch(() => {}); resetTimers(); } }, SESSION_MS - WARN_BEFORE_MS); expireTimer = setTimeout(() => { alert('Your session has expired. You will be redirected to login.'); window.location.href = '/auth/login'; }, SESSION_MS); } ['click', 'keydown', 'mousemove', 'scroll', 'touchstart'].forEach(evt => { document.addEventListener(evt, () => resetTimers(), { passive: true }); }); resetTimers(); })(); /* ── CSRF helper (reads meta tag set by Flask) ─────────────── */ function getCsrfToken() { const meta = document.querySelector('meta[name="csrf-token"]'); return meta ? meta.content : ''; } /* ── Auto-inject CSRF token into every static POST form ─────── */ /* Called immediately — app.js is at the end of so all */ /* forms are already in the DOM when this executes. */ (function injectCsrf() { const token = getCsrfToken(); if (!token) return; document.querySelectorAll('form').forEach(function(form) { if ((form.getAttribute('method') || '').toLowerCase() !== 'post') return; if (form.querySelector('input[name="csrf_token"]')) return; const input = document.createElement('input'); input.type = 'hidden'; input.name = 'csrf_token'; input.value = token; form.appendChild(input); }); }()); /* ── Mobile sidebar toggle ─────────────────────────────────── */ (function() { var toggle = document.getElementById('sidebar-toggle'); var backdrop = document.getElementById('sidebar-backdrop'); var sidebar = document.getElementById('sidebar'); if (!toggle || !sidebar) return; function openSidebar() { sidebar.classList.add('open'); backdrop.classList.add('open'); document.body.style.overflow = 'hidden'; } function closeSidebar() { sidebar.classList.remove('open'); backdrop.classList.remove('open'); document.body.style.overflow = ''; } toggle.addEventListener('click', openSidebar); backdrop.addEventListener('click', closeSidebar); sidebar.querySelectorAll('.nav-item').forEach(function(link) { link.addEventListener('click', closeSidebar); }); }()); /* ── Relative timestamps ───────────────────────────────────── */ function timeAgo(dateStr) { if (!dateStr) return '—'; var d = new Date(dateStr.replace(' ', 'T')); var diff = Math.round((Date.now() - d.getTime()) / 1000); if (diff < 60) return 'just now'; if (diff < 3600) return Math.floor(diff / 60) + ' min ago'; if (diff < 86400) return Math.floor(diff / 3600) + ' hr ago'; if (diff < 604800) return Math.floor(diff / 86400) + ' days ago'; return d.toLocaleDateString(); } document.addEventListener('DOMContentLoaded', function() { document.querySelectorAll('[data-ts]').forEach(function(el) { var iso = el.dataset.ts; if (!iso) return; el.title = el.textContent.trim(); el.textContent = timeAgo(iso); }); }); /* ── Generic fetch-based form submit (JSON response) ───────── */ async function submitJson(url, data, method = 'POST') { const res = await fetch(url, { method, headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest', 'X-CSRFToken': getCsrfToken(), }, body: JSON.stringify(data), credentials: 'same-origin', }); return res.json(); }