|
|
@@ -140,6 +140,38 @@
|
|
|
|
{% if user.groups and 'admin' in user.groups %}
|
|
|
|
{% if user.groups and 'admin' in user.groups %}
|
|
|
|
<span class="lt-badge lt-badge-admin" aria-label="Administrator">ADMIN</span>
|
|
|
|
<span class="lt-badge lt-badge-admin" aria-label="Administrator">ADMIN</span>
|
|
|
|
{% endif %}
|
|
|
|
{% endif %}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Notification bell — shows active monitoring alerts -->
|
|
|
|
|
|
|
|
<div class="lt-notif-dropdown-wrap" id="lt-notif-wrap">
|
|
|
|
|
|
|
|
<button type="button"
|
|
|
|
|
|
|
|
class="lt-btn lt-btn-ghost lt-btn-sm lt-notif-bell-btn"
|
|
|
|
|
|
|
|
id="lt-notif-bell"
|
|
|
|
|
|
|
|
aria-label="Active alerts"
|
|
|
|
|
|
|
|
aria-expanded="false"
|
|
|
|
|
|
|
|
aria-controls="lt-notif-panel"
|
|
|
|
|
|
|
|
title="Active alerts">🔔</button>
|
|
|
|
|
|
|
|
<div class="lt-notif-panel" id="lt-notif-panel" aria-hidden="true" role="dialog" aria-label="Active alerts">
|
|
|
|
|
|
|
|
<div class="lt-notif-panel-header">
|
|
|
|
|
|
|
|
<span>Active Alerts</span>
|
|
|
|
|
|
|
|
<button type="button" class="lt-notif-panel-clear" id="lt-notif-clear-btn">Mark all read</button>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="lt-notif-panel-list" id="lt-notif-list">
|
|
|
|
|
|
|
|
<div style="padding:0.75rem;font-size:0.75rem;color:var(--text-muted);text-align:center">Loading…</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="lt-notif-panel-footer">
|
|
|
|
|
|
|
|
<a href="{{ url_for('index') }}" class="lt-btn lt-btn-ghost lt-btn-sm" style="width:100%;text-align:center;display:block;font-size:0.72rem">View dashboard</a>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<!-- ⌘K affordance -->
|
|
|
|
|
|
|
|
<button type="button"
|
|
|
|
|
|
|
|
class="lt-btn lt-btn-ghost lt-btn-sm"
|
|
|
|
|
|
|
|
title="Command palette (Ctrl+K)"
|
|
|
|
|
|
|
|
aria-label="Open command palette"
|
|
|
|
|
|
|
|
onclick="if(window.lt&<.cmdPalette)lt.cmdPalette.open()"
|
|
|
|
|
|
|
|
style="font-size:0.65rem;opacity:0.55;letter-spacing:0.03em;padding:0.2rem 0.45rem">⌕ K</button>
|
|
|
|
|
|
|
|
|
|
|
|
<button type="button" class="lt-theme-btn" id="lt-theme-btn"
|
|
|
|
<button type="button" class="lt-theme-btn" id="lt-theme-btn"
|
|
|
|
aria-label="Toggle theme" title="Toggle light/dark mode">☀</button>
|
|
|
|
aria-label="Toggle theme" title="Toggle light/dark mode">☀</button>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
@@ -170,12 +202,19 @@
|
|
|
|
{% block content %}{% endblock %}
|
|
|
|
{% block content %}{% endblock %}
|
|
|
|
</main>
|
|
|
|
</main>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- FOOTER -->
|
|
|
|
<!-- FOOTER — context-sensitive per page -->
|
|
|
|
<footer class="lt-footer" role="contentinfo">
|
|
|
|
<footer class="lt-footer" role="contentinfo">
|
|
|
|
<nav class="lt-footer-hints" aria-label="Keyboard shortcuts">
|
|
|
|
<nav class="lt-footer-hints" aria-label="Keyboard shortcuts">
|
|
|
|
<span class="lt-footer-hint"><span class="lt-footer-key">[ Ctrl+K ]</span> SEARCH</span>
|
|
|
|
{% if request.endpoint == 'index' %}
|
|
|
|
<span class="lt-footer-sep">|</span>
|
|
|
|
<span class="lt-footer-hint"><span class="lt-footer-key">[ R ]</span> REFRESH</span>
|
|
|
|
<span class="lt-footer-hint"><span class="lt-footer-key">[ R ]</span> REFRESH</span>
|
|
|
|
<span class="lt-footer-sep">|</span>
|
|
|
|
|
|
|
|
<span class="lt-footer-hint"><span class="lt-footer-key">[ S ]</span> SUPPRESS</span>
|
|
|
|
|
|
|
|
<span class="lt-footer-sep">|</span>
|
|
|
|
|
|
|
|
{% elif request.endpoint == 'links_page' %}
|
|
|
|
|
|
|
|
<span class="lt-footer-hint"><span class="lt-footer-key">[ R ]</span> REFRESH</span>
|
|
|
|
|
|
|
|
<span class="lt-footer-sep">|</span>
|
|
|
|
|
|
|
|
{% endif %}
|
|
|
|
|
|
|
|
<button type="button" class="lt-footer-hint" data-action="open-settings"><span class="lt-footer-key">[ * ]</span> CFG</button>
|
|
|
|
<span class="lt-footer-sep">|</span>
|
|
|
|
<span class="lt-footer-sep">|</span>
|
|
|
|
<button type="button" class="lt-footer-hint" data-action="show-keyboard-help"><span class="lt-footer-key">[ ? ]</span> HELP</button>
|
|
|
|
<button type="button" class="lt-footer-hint" data-action="show-keyboard-help"><span class="lt-footer-key">[ ? ]</span> HELP</button>
|
|
|
|
</nav>
|
|
|
|
</nav>
|
|
|
@@ -194,9 +233,11 @@
|
|
|
|
<thead><tr><th>Shortcut</th><th>Action</th></tr></thead>
|
|
|
|
<thead><tr><th>Shortcut</th><th>Action</th></tr></thead>
|
|
|
|
<tbody>
|
|
|
|
<tbody>
|
|
|
|
<tr><td>Ctrl / ⌘ + K</td><td>Command palette</td></tr>
|
|
|
|
<tr><td>Ctrl / ⌘ + K</td><td>Command palette</td></tr>
|
|
|
|
<tr><td>R</td><td>Refresh dashboard data</td></tr>
|
|
|
|
<tr><td>R</td><td>Refresh data (Dashboard / Link Debug)</td></tr>
|
|
|
|
|
|
|
|
<tr><td>S</td><td>Quick-suppress alert (Dashboard)</td></tr>
|
|
|
|
|
|
|
|
<tr><td>*</td><td>Open settings</td></tr>
|
|
|
|
<tr><td>?</td><td>Show this help</td></tr>
|
|
|
|
<tr><td>?</td><td>Show this help</td></tr>
|
|
|
|
<tr><td>ESC</td><td>Close modal</td></tr>
|
|
|
|
<tr><td>ESC</td><td>Close modal / panel</td></tr>
|
|
|
|
</tbody>
|
|
|
|
</tbody>
|
|
|
|
</table>
|
|
|
|
</table>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
@@ -206,6 +247,45 @@
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<!-- SETTINGS MODAL -->
|
|
|
|
|
|
|
|
<div id="lt-settings-modal" class="lt-modal-overlay" aria-hidden="true">
|
|
|
|
|
|
|
|
<div class="lt-modal" role="dialog" aria-modal="true" aria-labelledby="settings-modal-title">
|
|
|
|
|
|
|
|
<div class="lt-modal-header">
|
|
|
|
|
|
|
|
<span class="lt-modal-title" id="settings-modal-title">Settings</span>
|
|
|
|
|
|
|
|
<button type="button" class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="lt-modal-body">
|
|
|
|
|
|
|
|
<div class="lt-form-group">
|
|
|
|
|
|
|
|
<label class="lt-label">Auto-refresh interval</label>
|
|
|
|
|
|
|
|
<div class="duration-pills" id="settings-refresh-pills">
|
|
|
|
|
|
|
|
<button type="button" class="pill" data-refresh-interval="15">15 s</button>
|
|
|
|
|
|
|
|
<button type="button" class="pill" data-refresh-interval="30">30 s</button>
|
|
|
|
|
|
|
|
<button type="button" class="pill" data-refresh-interval="60">1 min</button>
|
|
|
|
|
|
|
|
<button type="button" class="pill" data-refresh-interval="300">5 min</button>
|
|
|
|
|
|
|
|
<button type="button" class="pill" data-refresh-interval="0">Off</button>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="lt-field-hint" id="settings-refresh-hint"></div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="lt-divider" style="margin:1rem 0 0.75rem"></div>
|
|
|
|
|
|
|
|
<div class="lt-kv-grid">
|
|
|
|
|
|
|
|
<div class="lt-kv-row">
|
|
|
|
|
|
|
|
<span class="lt-kv-label">User</span>
|
|
|
|
|
|
|
|
<span class="lt-kv-value lt-text-cyan">{{ user.name or user.username }}</span>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
{% if user.groups %}
|
|
|
|
|
|
|
|
<div class="lt-kv-row">
|
|
|
|
|
|
|
|
<span class="lt-kv-label">Groups</span>
|
|
|
|
|
|
|
|
<span class="lt-kv-value">{{ user.groups | join(', ') }}</span>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
{% endif %}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="lt-modal-footer">
|
|
|
|
|
|
|
|
<button type="button" class="lt-btn" data-modal-close>Close</button>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
<script>
|
|
|
|
const GANDALF_CONFIG = {
|
|
|
|
const GANDALF_CONFIG = {
|
|
|
|
ticket_web_url: "{{ config.get('ticket_api', {}).get('web_url', 'http://t.lotusguild.org/ticket/') }}"
|
|
|
|
ticket_web_url: "{{ config.get('ticket_api', {}).get('web_url', 'http://t.lotusguild.org/ticket/') }}"
|
|
|
@@ -224,35 +304,189 @@
|
|
|
|
|
|
|
|
|
|
|
|
// Command palette
|
|
|
|
// Command palette
|
|
|
|
lt.cmdPalette.init([
|
|
|
|
lt.cmdPalette.init([
|
|
|
|
{ id: 'nav-dashboard', group: 'Navigate', icon: '~', label: 'Dashboard', action: function() { window.location.href = '/'; } },
|
|
|
|
{ id: 'nav-dashboard', group: 'Navigate', icon: '~', label: 'Dashboard', action: function() { window.location.href = '/'; } },
|
|
|
|
{ id: 'nav-links', group: 'Navigate', icon: '↗', label: 'Link Debug', action: function() { window.location.href = '/links'; } },
|
|
|
|
{ id: 'nav-links', group: 'Navigate', icon: '↗', label: 'Link Debug', action: function() { window.location.href = '/links'; } },
|
|
|
|
{ id: 'nav-inspector', group: 'Navigate', icon: '⬡', label: 'Inspector', action: function() { window.location.href = '/inspector'; } },
|
|
|
|
{ id: 'nav-inspector', group: 'Navigate', icon: '⬡', label: 'Inspector', action: function() { window.location.href = '/inspector'; } },
|
|
|
|
{ id: 'nav-suppressions', group: 'Navigate', icon: '🔕', label: 'Suppressions', action: function() { window.location.href = '/suppressions'; } },
|
|
|
|
{ id: 'nav-suppressions', group: 'Navigate', icon: '🔕', label: 'Suppressions', action: function() { window.location.href = '/suppressions'; } },
|
|
|
|
{ id: 'help-shortcuts', group: 'Help', icon: '?', label: 'Keyboard Shortcuts', action: function() { lt.modal.open('lt-keys-help'); } },
|
|
|
|
{ id: 'action-refresh', group: 'Actions', icon: '↻', label: 'Refresh Data', kbd: 'R', action: function() { lt.autoRefresh.now(); } },
|
|
|
|
{ id: 'help-theme', group: 'Help', icon: '*', label: 'Toggle Theme', action: function() { lt.theme.toggle(); } },
|
|
|
|
{ id: 'action-suppress', group: 'Actions', icon: '🔕', label: 'New Suppression', kbd: 'S', action: function() { if (typeof openSuppressModal === 'function') openSuppressModal('host','',''); else window.location.href='/suppressions'; } },
|
|
|
|
{ id: 'action-refresh', group: 'Actions', icon: '↻', label: 'Refresh Data', kbd: 'R', action: function() { if (typeof refreshAll === 'function') refreshAll(); } },
|
|
|
|
{ id: 'help-shortcuts', group: 'Help', icon: '?', label: 'Keyboard Shortcuts', kbd: '?', action: function() { lt.modal.open('lt-keys-help'); } },
|
|
|
|
|
|
|
|
{ id: 'help-settings', group: 'Help', icon: '*', label: 'Settings', kbd: '*', action: function() { lt.modal.open('lt-settings-modal'); } },
|
|
|
|
|
|
|
|
{ id: 'help-theme', group: 'Help', icon: '☀', label: 'Toggle Theme', action: function() { lt.theme.toggle(); } },
|
|
|
|
]);
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Footer hint actions
|
|
|
|
// ── Global footer + key actions ───────────────────────────────────────
|
|
|
|
document.addEventListener('click', function(e) {
|
|
|
|
document.addEventListener('click', function(e) {
|
|
|
|
var btn = e.target.closest('[data-action]');
|
|
|
|
var btn = e.target.closest('[data-action]');
|
|
|
|
if (!btn) return;
|
|
|
|
if (!btn) return;
|
|
|
|
if (btn.getAttribute('data-action') === 'show-keyboard-help' && window.lt) {
|
|
|
|
var action = btn.getAttribute('data-action');
|
|
|
|
lt.modal.open('lt-keys-help');
|
|
|
|
if (action === 'show-keyboard-help' && window.lt) lt.modal.open('lt-keys-help');
|
|
|
|
}
|
|
|
|
if (action === 'open-settings' && window.lt) lt.modal.open('lt-settings-modal');
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
lt.keys.on('r', function() {
|
|
|
|
lt.keys.on('r', function() { lt.autoRefresh.now(); });
|
|
|
|
if (typeof refreshAll === 'function') refreshAll();
|
|
|
|
lt.keys.on('?', function() { if (window.lt) lt.modal.open('lt-keys-help'); });
|
|
|
|
|
|
|
|
lt.keys.on('*', function() { if (window.lt) lt.modal.open('lt-settings-modal'); });
|
|
|
|
|
|
|
|
lt.keys.on('s', function() {
|
|
|
|
|
|
|
|
if (typeof openSuppressModal === 'function') openSuppressModal('host', '', '');
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Avatar image error fallback — hide broken img, reveal initials beneath
|
|
|
|
// ── Avatar image error fallback ───────────────────────────────────────
|
|
|
|
document.addEventListener('error', function(e) {
|
|
|
|
document.addEventListener('error', function(e) {
|
|
|
|
if (e.target.tagName === 'IMG' && e.target.closest('.lt-avatar')) {
|
|
|
|
if (e.target.tagName === 'IMG' && e.target.closest('.lt-avatar')) {
|
|
|
|
e.target.classList.add('lt-avatar-img-err');
|
|
|
|
e.target.classList.add('lt-avatar-img-err');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}, true);
|
|
|
|
}, true);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ── Settings modal ────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
(function() {
|
|
|
|
|
|
|
|
var LS_KEY = 'gandalf_settings';
|
|
|
|
|
|
|
|
var DEFAULT = { refreshInterval: 30 };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function loadSettings() {
|
|
|
|
|
|
|
|
try { return Object.assign({}, DEFAULT, JSON.parse(localStorage.getItem(LS_KEY) || '{}')); }
|
|
|
|
|
|
|
|
catch(_) { return Object.assign({}, DEFAULT); }
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function saveSettings(s) {
|
|
|
|
|
|
|
|
try { localStorage.setItem(LS_KEY, JSON.stringify(s)); } catch(_) {}
|
|
|
|
|
|
|
|
if (typeof window.onGandalfSettingsChanged === 'function') window.onGandalfSettingsChanged(s);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function applyRefreshPillUI(interval) {
|
|
|
|
|
|
|
|
document.querySelectorAll('#settings-refresh-pills .pill').forEach(function(p) {
|
|
|
|
|
|
|
|
p.classList.toggle('active', parseInt(p.dataset.refreshInterval) === interval);
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
var hint = document.getElementById('settings-refresh-hint');
|
|
|
|
|
|
|
|
if (hint) {
|
|
|
|
|
|
|
|
if (interval === 0) hint.textContent = 'Auto-refresh disabled.';
|
|
|
|
|
|
|
|
else if (interval < 60) hint.textContent = 'Refreshes every ' + interval + ' seconds.';
|
|
|
|
|
|
|
|
else hint.textContent = 'Refreshes every ' + Math.floor(interval/60) + ' minute' + (interval > 60 ? 's' : '') + '.';
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Init pill UI from saved settings
|
|
|
|
|
|
|
|
var _settings = loadSettings();
|
|
|
|
|
|
|
|
applyRefreshPillUI(_settings.refreshInterval);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Expose for pages that need to read it (e.g. index.html for autoRefresh)
|
|
|
|
|
|
|
|
window.gandalfSettings = _settings;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
document.addEventListener('click', function(e) {
|
|
|
|
|
|
|
|
var pill = e.target.closest('#settings-refresh-pills .pill[data-refresh-interval]');
|
|
|
|
|
|
|
|
if (!pill) return;
|
|
|
|
|
|
|
|
var interval = parseInt(pill.dataset.refreshInterval);
|
|
|
|
|
|
|
|
_settings.refreshInterval = interval;
|
|
|
|
|
|
|
|
saveSettings(_settings);
|
|
|
|
|
|
|
|
applyRefreshPillUI(interval);
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ── Notification Bell — shows active monitoring alerts ────────────────
|
|
|
|
|
|
|
|
(function() {
|
|
|
|
|
|
|
|
var bell = document.getElementById('lt-notif-bell');
|
|
|
|
|
|
|
|
var panel = document.getElementById('lt-notif-panel');
|
|
|
|
|
|
|
|
var list = document.getElementById('lt-notif-list');
|
|
|
|
|
|
|
|
var clearBtn = document.getElementById('lt-notif-clear-btn');
|
|
|
|
|
|
|
|
var wrapEl = document.getElementById('lt-notif-wrap');
|
|
|
|
|
|
|
|
if (!bell || !panel) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var _open = false;
|
|
|
|
|
|
|
|
var _lastEvents = [];
|
|
|
|
|
|
|
|
var LS_READ_KEY = 'gandalf_notif_read_before';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function getReadBefore() {
|
|
|
|
|
|
|
|
try { return parseInt(localStorage.getItem(LS_READ_KEY) || '0'); } catch(_) { return 0; }
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
function setReadBefore(ts) {
|
|
|
|
|
|
|
|
try { localStorage.setItem(LS_READ_KEY, String(ts)); } catch(_) {}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function esc(s) {
|
|
|
|
|
|
|
|
return (window.lt && lt.escHtml) ? lt.escHtml(String(s)) : String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function toMs(dateStr) {
|
|
|
|
|
|
|
|
if (!dateStr) return 0;
|
|
|
|
|
|
|
|
return new Date(dateStr.replace(' UTC','Z').replace(' ','T')).getTime();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function fmtAgo(dateStr) {
|
|
|
|
|
|
|
|
var diff = Math.floor((Date.now() - toMs(dateStr)) / 1000);
|
|
|
|
|
|
|
|
if (diff < 60) return diff + 's ago';
|
|
|
|
|
|
|
|
if (diff < 3600) return Math.floor(diff/60) + 'm ago';
|
|
|
|
|
|
|
|
if (diff < 86400) return Math.floor(diff/3600) + 'h ago';
|
|
|
|
|
|
|
|
return Math.floor(diff/86400) + 'd ago';
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var SEV_DOT = { critical: 'var(--red)', warning: 'var(--amber)' };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function renderAlerts(events) {
|
|
|
|
|
|
|
|
_lastEvents = events || [];
|
|
|
|
|
|
|
|
var readBefore = getReadBefore();
|
|
|
|
|
|
|
|
var active = _lastEvents.filter(function(e) { return e.severity !== 'info'; });
|
|
|
|
|
|
|
|
var unreadCount = active.filter(function(e) { return toMs(e.last_seen) > readBefore; }).length;
|
|
|
|
|
|
|
|
lt.notif.set(bell, unreadCount);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!active.length) {
|
|
|
|
|
|
|
|
list.innerHTML = '<div style="padding:1rem;font-size:0.75rem;color:var(--text-muted);text-align:center">✔ No active alerts</div>';
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
list.innerHTML = active.slice(0, 25).map(function(e) {
|
|
|
|
|
|
|
|
var isUnread = toMs(e.last_seen) > readBefore;
|
|
|
|
|
|
|
|
var dotColor = SEV_DOT[e.severity] || 'var(--text-muted)';
|
|
|
|
|
|
|
|
return '<div class="lt-notif-item' + (isUnread ? ' lt-notif-item--unread' : '') + '">' +
|
|
|
|
|
|
|
|
'<div class="lt-notif-dot' + (isUnread ? '' : ' lt-notif-dot--read') + '" style="background:' + dotColor + ';border-radius:50%;margin-top:4px"></div>' +
|
|
|
|
|
|
|
|
'<div class="lt-notif-item-body">' +
|
|
|
|
|
|
|
|
'<div class="lt-notif-item-title">' + esc(e.target_name) + (e.target_detail ? ' · ' + esc(e.target_detail) : '') + '</div>' +
|
|
|
|
|
|
|
|
'<div class="lt-notif-item-time">' + esc(e.event_type.replace(/_/g,' ')) + ' · ' + fmtAgo(e.last_seen) + '</div>' +
|
|
|
|
|
|
|
|
'</div></div>';
|
|
|
|
|
|
|
|
}).join('');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function fetchAlerts(andRender) {
|
|
|
|
|
|
|
|
fetch('/api/status', { credentials: 'same-origin' })
|
|
|
|
|
|
|
|
.then(function(r) { return r.json(); })
|
|
|
|
|
|
|
|
.then(function(data) {
|
|
|
|
|
|
|
|
var events = data.events || [];
|
|
|
|
|
|
|
|
if (andRender) {
|
|
|
|
|
|
|
|
renderAlerts(events);
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
_lastEvents = events;
|
|
|
|
|
|
|
|
var readBefore = getReadBefore();
|
|
|
|
|
|
|
|
var active = events.filter(function(e) { return e.severity !== 'info'; });
|
|
|
|
|
|
|
|
var unread = active.filter(function(e) { return toMs(e.last_seen) > readBefore; }).length;
|
|
|
|
|
|
|
|
lt.notif.set(bell, unread);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
.catch(function() {
|
|
|
|
|
|
|
|
if (andRender) list.innerHTML = '<div style="padding:0.75rem;font-size:0.75rem;color:var(--text-muted);text-align:center">Could not load</div>';
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function openPanel() { _open = true; panel.removeAttribute('aria-hidden'); bell.setAttribute('aria-expanded','true'); fetchAlerts(true); }
|
|
|
|
|
|
|
|
function closePanel() { _open = false; panel.setAttribute('aria-hidden','true'); bell.setAttribute('aria-expanded','false'); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
bell.addEventListener('click', function(e) { e.stopPropagation(); _open ? closePanel() : openPanel(); });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (clearBtn) {
|
|
|
|
|
|
|
|
clearBtn.addEventListener('click', function() {
|
|
|
|
|
|
|
|
setReadBefore(Date.now());
|
|
|
|
|
|
|
|
renderAlerts(_lastEvents);
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
document.addEventListener('click', function(e) { if (_open && wrapEl && !wrapEl.contains(e.target)) closePanel(); });
|
|
|
|
|
|
|
|
document.addEventListener('keydown', function(e) { if (e.key === 'Escape' && _open) closePanel(); });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Initial badge load + poll every 60 s
|
|
|
|
|
|
|
|
fetchAlerts(false);
|
|
|
|
|
|
|
|
setInterval(function() { fetchAlerts(_open); }, 60000);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Allow refreshAll() to also push fresh events to the bell
|
|
|
|
|
|
|
|
window.gandalfNotifUpdate = function(events) { renderAlerts(events); };
|
|
|
|
|
|
|
|
})();
|
|
|
|
</script>
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
</body>
|
|
|
|
</body>
|
|
|
|