diff --git a/README.md b/README.md index 03a1123..74ca784 100644 --- a/README.md +++ b/README.md @@ -124,9 +124,15 @@ generated from `config/alertmanager/alertmanager.yml.template` whenever The custom alert UI lives at `/alerts/`. It signs users in with Grafana username/password credentials and sends those credentials to `alert-api` as -HTTP Basic auth. The API validates credentials against Grafana, then uses the -installer-managed Grafana service account token to create, delete, disable, and -route alert rules. +HTTP Basic auth. The browser keeps that auth header in memory only for the +current page session (it is not persisted to browser storage). Use an Editor +account for alert-ui access and reserve admin login for direct Grafana admin +tasks. + +The API validates credentials against Grafana, then uses the installer-managed +Grafana service account token to create, delete, disable, and route alert +rules. If this project scope grows, migrate alert-ui auth to backend-managed +sessions (cookie + server-side session ID) instead of browser-held Basic auth. `install_alert_ui.sh` maintains the required Grafana service account: diff --git a/alert-ui/app.js b/alert-ui/app.js index 568b5b5..adc648c 100644 --- a/alert-ui/app.js +++ b/alert-ui/app.js @@ -1,8 +1,8 @@ /* Fridge Alert Manager — app.js * - * Auth: Grafana Basic auth credentials stored in sessionStorage. + * Auth: Grafana Basic auth header is held in-memory only (never browser storage). * All /alerts/api/* calls include "Authorization: Basic " automatically. - * On 401, the login modal is shown and the credential is cleared. + * On 401 or sign-out, in-memory auth is cleared and login is shown. * * All fetch paths are absolute from the site root so they work behind Caddy. * @@ -11,7 +11,7 @@ */ // ── State ────────────────────────────────────────────────────────────────── -let authHeader = sessionStorage.getItem('fridge_auth') || ''; +let authHeader = ''; let metricsData = { metrics: [], fridges: [], operators: [] }; let refreshTimer = null; let alertSort = { key: 'status', dir: 'desc' }; @@ -36,26 +36,13 @@ function hideLogin() { document.getElementById('login-modal').classList.remove('visible'); } -function setAuth(username, password) { - authHeader = 'Basic ' + btoa(username + ':' + password); - sessionStorage.setItem('fridge_auth', authHeader); +function setAuth(username, authorizationHeader) { + authHeader = authorizationHeader; document.getElementById('header-username').textContent = username; } function clearAuth() { authHeader = ''; - sessionStorage.removeItem('fridge_auth'); -} - -// Decode stored auth to get username (for display only) -function storedUsername() { - if (!authHeader.startsWith('Basic ')) return ''; - try { - const decoded = atob(authHeader.slice(6)); - return decoded.split(':')[0]; - } catch (_) { - return ''; - } } // ── Fetch wrapper ─────────────────────────────────────────────────────────── @@ -108,7 +95,7 @@ document.getElementById('login-form').addEventListener('submit', async (e) => { if (resp.status === 401) { document.getElementById('login-error').textContent = 'Incorrect username or password.'; } else { - setAuth(username, password); + setAuth(username, testHeader); hideLogin(); document.getElementById('login-username').value = ''; document.getElementById('login-password').value = ''; @@ -206,6 +193,32 @@ document.getElementById('f-fridge').addEventListener('change', (e) => { populateMetricDropdown(e.target.value); }); +document.getElementById('alerts-body').addEventListener('click', (e) => { + const actionBtn = e.target.closest('button[data-action]'); + if (actionBtn) { + e.stopPropagation(); + const uid = actionBtn.dataset.uid || ''; + if (actionBtn.dataset.action === 'toggle-enabled') { + toggleAlert(uid, actionBtn.dataset.enabled === 'true', actionBtn); + return; + } + if (actionBtn.dataset.action === 'toggle-no-data') { + toggleNoDataState(uid, actionBtn.dataset.targetState || 'OK', actionBtn); + return; + } + if (actionBtn.dataset.action === 'delete-alert') { + deleteAlert(uid, actionBtn); + } + return; + } + + const row = e.target.closest('tr[data-alert-uid]'); + if (!row) return; + const uid = row.dataset.alertUid; + const alert = (window._alertsCache || []).find((a) => a.uid === uid); + if (alert) editAlert(alert); +}); + // Load notification policy (public) and render repeat_interval as hours function parseRepeatIntervalToHours(s) { if (!s) return null; @@ -373,14 +386,17 @@ function renderAlerts(alerts) { const condition = a.operator ? `${escHtml(a.operator)} ${a.threshold}` : '—'; - const toggleBtn = ``; + const toggleBtn = ``; const noDataState = a.no_data_state || 'OK'; - const noDataBtn = ``; + const noDataClass = noDataState === 'Alerting' ? 'btn-warn' : 'btn-secondary'; + const noDataTarget = noDataState === 'Alerting' ? 'OK' : 'Alerting'; + const noDataLabel = noDataState === 'Alerting' ? 'Fire' : 'Ignore'; + const noDataBtn = ``; const deleteBtn = a.provisioned ? '' - : ``; + : ``; const pencilIcon = ``; const titleWithIcon = `${escHtml(a.title)}${pencilIcon}`; @@ -389,7 +405,7 @@ function renderAlerts(alerts) { ? `${a.recipient_count}` : ''; - return ` + return ` ${titleWithIcon} ${stateBadge} ${escHtml(a.fridge)} @@ -648,6 +664,10 @@ function toggleNoDataForm() { setNoDataFormState(current === 'Alerting' ? 'OK' : 'Alerting'); } +document.getElementById('btn-no-data-state').addEventListener('click', toggleNoDataForm); +document.getElementById('sort-status').addEventListener('click', () => setAlertSort('status')); +document.getElementById('sort-fridge').addEventListener('click', () => setAlertSort('fridge')); + function editAlert(alert) { // Populate form fields with this alert's values document.getElementById('f-name').value = alert.title || ''; @@ -1147,15 +1167,14 @@ function escHtml(str) { await loadPolicy(); await loadMetrics(); + // Wire up fridge-level quick-toggle buttons + const btnManny = document.getElementById('btn-manny-all'); + if (btnManny) btnManny.addEventListener('click', () => toggleAllForFridge('Manny', btnManny)); + const btnDodo = document.getElementById('btn-dodo-all'); + if (btnDodo) btnDodo.addEventListener('click', () => toggleAllForFridge('Dodo', btnDodo)); + if (authHeader) { - // Show username from stored token - document.getElementById('header-username').textContent = storedUsername(); await loadAll(); - // Wire up fridge-level quick-toggle buttons - const btnManny = document.getElementById('btn-manny-all'); - if (btnManny) btnManny.addEventListener('click', () => toggleAllForFridge('Manny', btnManny)); - const btnDodo = document.getElementById('btn-dodo-all'); - if (btnDodo) btnDodo.addEventListener('click', () => toggleAllForFridge('Dodo', btnDodo)); // Ensure buttons reflect current alert state updateFridgeButtons(); } else { diff --git a/alert-ui/index.html b/alert-ui/index.html index 3201b07..e33e338 100644 --- a/alert-ui/index.html +++ b/alert-ui/index.html @@ -12,7 +12,7 @@