9d6583a08a
Lint / Python (flake8) (push) Successful in 1m13s
Lint / JS (eslint) (push) Successful in 9s
Security / Python Security (bandit) (push) Failing after 45s
Test / Python Tests (pytest) (push) Successful in 57s
Lint / Notify on failure (push) Has been skipped
Lint / Deploy (push) Successful in 5s
- Add /api/avatar endpoint querying lldap for user jpegPhoto; disk cache with sentinel pattern avoids repeat LDAP hits for users without photos - Add ldap3 dependency and ldap config block to config.json - Wire lt-avatar img overlay in base.html with capture-phase error fallback (lt-avatar-img-err) to reveal initials when image is absent - Fix lt-avatar CSS shim: position:relative + absolute inset on img (local base.css was missing these; added to style.css) - Replace all empty-state paragraphs with proper lt-empty-state markup (icon + title + body) across index, suppressions, inspector, app.js - Add lt-spinner--cyan next to refresh button; shows during refreshAll() - Replace inspector panel-section-title with lt-divider throughout - Add data-tooltip attributes to SFP DOM metrics, TX/RX/Carrier/Duplex/ Auto-neg/Error labels in links.html and inspector panel - Add tooltips to events table column headers (Sev, First Seen, Failures) - Fix links.html host panel timestamp (was reading sample.updated which is always undefined; now uses data.updated) - Fix UniFi status text casing (Online→ONLINE to match server render) - Remove dead topo-status-* class manipulation from updateTopology() - Always render alert-count-badge; toggle display:none when count is 0 - Fix double UniFi get_devices() call in monitor.py run loop - Fix chip-critical animation (was using green pulse-glow; now red) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
260 lines
11 KiB
HTML
260 lines
11 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en" data-theme="dark">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
|
<meta name="theme-color" content="#030508">
|
|
<meta name="robots" content="noindex, nofollow">
|
|
<title>{% block title %}GANDALF{% endblock %} — GANDALF</title>
|
|
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,400;0,600;0,700;1,400&family=VT323&display=swap" rel="stylesheet">
|
|
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='base.css') }}">
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
|
|
|
<!-- base.js loaded in head so lt.* is available for inline scripts -->
|
|
<script src="{{ url_for('static', filename='base.js') }}"></script>
|
|
</head>
|
|
<body>
|
|
|
|
<a class="lt-skip-link" href="#main-content">Skip to main content</a>
|
|
|
|
<!-- BOOT OVERLAY -->
|
|
<div id="lt-boot" class="lt-boot-overlay" data-app-name="GANDALF" style="display:none" aria-hidden="true">
|
|
<pre id="lt-boot-text" class="lt-boot-text"></pre>
|
|
</div>
|
|
|
|
<!-- MOBILE NAV DRAWER -->
|
|
<div id="lt-nav-drawer" class="lt-nav-drawer" aria-hidden="true" role="dialog" aria-modal="true" aria-label="Navigation menu">
|
|
<div class="lt-nav-drawer-header">
|
|
<span class="lt-brand-title">GANDALF</span>
|
|
<button type="button" class="lt-nav-drawer-close" id="lt-nav-drawer-close" aria-label="Close navigation">✕</button>
|
|
</div>
|
|
<nav class="lt-nav-drawer-links" aria-label="Mobile navigation">
|
|
<a href="{{ url_for('index') }}"
|
|
class="lt-nav-drawer-link{% if request.endpoint == 'index' %} active{% endif %}"
|
|
{% if request.endpoint == 'index' %}aria-current="page"{% endif %}>Dashboard</a>
|
|
<a href="{{ url_for('links_page') }}"
|
|
class="lt-nav-drawer-link{% if request.endpoint == 'links_page' %} active{% endif %}"
|
|
{% if request.endpoint == 'links_page' %}aria-current="page"{% endif %}>Link Debug</a>
|
|
<a href="{{ url_for('inspector') }}"
|
|
class="lt-nav-drawer-link{% if request.endpoint == 'inspector' %} active{% endif %}"
|
|
{% if request.endpoint == 'inspector' %}aria-current="page"{% endif %}>Inspector</a>
|
|
{% if user.groups and 'admin' in user.groups %}
|
|
<a href="{{ url_for('suppressions_page') }}"
|
|
class="lt-nav-drawer-link{% if request.endpoint == 'suppressions_page' %} active{% endif %}"
|
|
{% if request.endpoint == 'suppressions_page' %}aria-current="page"{% endif %}>Suppressions</a>
|
|
{% endif %}
|
|
</nav>
|
|
</div>
|
|
<div id="lt-nav-overlay" class="lt-nav-drawer-overlay"></div>
|
|
|
|
<!-- PRIMARY HEADER -->
|
|
<header class="lt-header" role="banner">
|
|
<div class="lt-header-left">
|
|
|
|
<!-- Hamburger (mobile) -->
|
|
<button type="button"
|
|
class="lt-menu-btn"
|
|
id="lt-menu-btn"
|
|
data-action="open-nav-drawer"
|
|
aria-label="Open navigation menu"
|
|
aria-expanded="false"
|
|
aria-controls="lt-nav-drawer">
|
|
<span class="lt-menu-btn-bar"></span>
|
|
<span class="lt-menu-btn-bar"></span>
|
|
<span class="lt-menu-btn-bar"></span>
|
|
</button>
|
|
|
|
<!-- Brand -->
|
|
<div class="lt-brand">
|
|
<a href="{{ url_for('index') }}"
|
|
class="lt-brand-title lt-glitch"
|
|
data-text="GANDALF"
|
|
style="text-decoration:none"
|
|
aria-label="GANDALF home">GANDALF</a>
|
|
<span class="lt-brand-subtitle">Network Monitor // LotusGuild</span>
|
|
</div>
|
|
|
|
<!-- Desktop nav -->
|
|
<nav class="lt-nav" aria-label="Main navigation">
|
|
<a href="{{ url_for('index') }}"
|
|
class="lt-nav-link{% if request.endpoint == 'index' %} active{% endif %}"
|
|
{% if request.endpoint == 'index' %}aria-current="page"{% endif %}>
|
|
Dashboard
|
|
</a>
|
|
<a href="{{ url_for('links_page') }}"
|
|
class="lt-nav-link{% if request.endpoint == 'links_page' %} active{% endif %}"
|
|
{% if request.endpoint == 'links_page' %}aria-current="page"{% endif %}>
|
|
Link Debug
|
|
</a>
|
|
<a href="{{ url_for('inspector') }}"
|
|
class="lt-nav-link{% if request.endpoint == 'inspector' %} active{% endif %}"
|
|
{% if request.endpoint == 'inspector' %}aria-current="page"{% endif %}>
|
|
Inspector
|
|
</a>
|
|
{% if user.groups and 'admin' in user.groups %}
|
|
<div class="lt-nav-dropdown" data-action="toggle-nav-dropdown">
|
|
<a href="#"
|
|
class="lt-nav-link{% if request.endpoint == 'suppressions_page' %} active{% endif %}"
|
|
role="button"
|
|
aria-haspopup="true"
|
|
aria-expanded="false"
|
|
aria-controls="lt-admin-dropdown-menu">
|
|
Admin ▾
|
|
</a>
|
|
<ul class="lt-nav-dropdown-menu"
|
|
id="lt-admin-dropdown-menu"
|
|
role="menu"
|
|
aria-label="Admin menu">
|
|
<li role="none">
|
|
<a href="{{ url_for('suppressions_page') }}" role="menuitem"
|
|
class="{% if request.endpoint == 'suppressions_page' %}active{% endif %}">
|
|
Suppressions
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
{% else %}
|
|
<a href="{{ url_for('suppressions_page') }}"
|
|
class="lt-nav-link{% if request.endpoint == 'suppressions_page' %} active{% endif %}"
|
|
{% if request.endpoint == 'suppressions_page' %}aria-current="page"{% endif %}>
|
|
Suppressions
|
|
</a>
|
|
{% endif %}
|
|
</nav>
|
|
</div>
|
|
|
|
<div class="lt-header-right">
|
|
{% set _uname = user.name or user.username %}
|
|
{% set _words = _uname.split() %}
|
|
{% set _initials = (_words[0][0] ~ (_words[1][0] if _words|length > 1 else ''))|upper %}
|
|
<div class="lt-avatar lt-avatar--sm {{ _uname | avatar_color }}"
|
|
aria-hidden="true" title="{{ _uname }}">
|
|
<img src="{{ url_for('api_avatar') }}" alt="" class="lt-avatar-img">
|
|
<span class="lt-avatar-initials">{{ _initials }}</span>
|
|
</div>
|
|
<span class="lt-header-user">{{ _uname }}</span>
|
|
{% if user.groups and 'admin' in user.groups %}
|
|
<span class="lt-badge lt-badge-admin" aria-label="Administrator">ADMIN</span>
|
|
{% endif %}
|
|
<button type="button" class="lt-theme-btn" id="lt-theme-btn"
|
|
aria-label="Toggle theme" title="Toggle light/dark mode">☀</button>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- COMMAND PALETTE -->
|
|
<div id="lt-cmd-overlay" class="lt-cmd-overlay" role="dialog" aria-modal="true" aria-label="Command palette" aria-hidden="true">
|
|
<div class="lt-cmd-palette" id="lt-cmd-palette">
|
|
<div class="lt-cmd-input-wrap">
|
|
<span class="lt-cmd-prompt">></span>
|
|
<input id="lt-cmd-input" class="lt-cmd-input" type="text"
|
|
placeholder="Search commands…" autocomplete="off"
|
|
spellcheck="false" aria-label="Search commands">
|
|
</div>
|
|
<div class="lt-cmd-results" id="lt-cmd-results">
|
|
<div class="lt-cmd-empty">Start typing to search…</div>
|
|
</div>
|
|
<div class="lt-cmd-footer">
|
|
<span><kbd>↑</kbd><kbd>↓</kbd> Navigate</span>
|
|
<span><kbd>Enter</kbd> Select</span>
|
|
<span><kbd>Esc</kbd> Close</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- MAIN CONTENT -->
|
|
<main class="lt-main lt-container" id="main-content">
|
|
{% block content %}{% endblock %}
|
|
</main>
|
|
|
|
<!-- FOOTER -->
|
|
<footer class="lt-footer" role="contentinfo">
|
|
<nav class="lt-footer-hints" aria-label="Keyboard shortcuts">
|
|
<span class="lt-footer-hint"><span class="lt-footer-key">[ Ctrl+K ]</span> SEARCH</span>
|
|
<span class="lt-footer-sep">|</span>
|
|
<span class="lt-footer-hint"><span class="lt-footer-key">[ R ]</span> REFRESH</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>
|
|
</nav>
|
|
<span>GANDALF — TDS v1.2</span>
|
|
</footer>
|
|
|
|
<!-- KEYBOARD SHORTCUTS MODAL -->
|
|
<div id="lt-keys-help" class="lt-modal-overlay" aria-hidden="true">
|
|
<div class="lt-modal" role="dialog" aria-modal="true" aria-labelledby="keys-help-title">
|
|
<div class="lt-modal-header">
|
|
<span class="lt-modal-title" id="keys-help-title">Keyboard Shortcuts</span>
|
|
<button type="button" class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
|
|
</div>
|
|
<div class="lt-modal-body">
|
|
<table class="lt-table" style="width:100%">
|
|
<thead><tr><th>Shortcut</th><th>Action</th></tr></thead>
|
|
<tbody>
|
|
<tr><td>Ctrl / ⌘ + K</td><td>Command palette</td></tr>
|
|
<tr><td>R</td><td>Refresh dashboard data</td></tr>
|
|
<tr><td>?</td><td>Show this help</td></tr>
|
|
<tr><td>ESC</td><td>Close modal</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="lt-modal-footer">
|
|
<button type="button" class="lt-btn" data-modal-close>Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const GANDALF_CONFIG = {
|
|
ticket_web_url: "{{ config.get('ticket_api', {}).get('web_url', 'http://t.lotusguild.org/ticket/') }}"
|
|
};
|
|
</script>
|
|
<script src="{{ url_for('static', filename='app.js') }}"></script>
|
|
{% block scripts %}{% endblock %}
|
|
|
|
<script>
|
|
if (window.lt) {
|
|
lt.init({ bootName: 'GANDALF' });
|
|
|
|
// Theme toggle
|
|
var themeBtn = document.getElementById('lt-theme-btn');
|
|
if (themeBtn) themeBtn.addEventListener('click', function() { lt.theme.toggle(); });
|
|
|
|
// Command palette
|
|
lt.cmdPalette.init([
|
|
{ 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-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: 'help-shortcuts', group: 'Help', icon: '?', label: 'Keyboard Shortcuts', action: function() { lt.modal.open('lt-keys-help'); } },
|
|
{ id: 'help-theme', group: 'Help', icon: '*', label: 'Toggle Theme', action: function() { lt.theme.toggle(); } },
|
|
{ id: 'action-refresh', group: 'Actions', icon: '↻', label: 'Refresh Data', kbd: 'R', action: function() { if (typeof refreshAll === 'function') refreshAll(); } },
|
|
]);
|
|
}
|
|
|
|
// Footer hint actions
|
|
document.addEventListener('click', function(e) {
|
|
var btn = e.target.closest('[data-action]');
|
|
if (!btn) return;
|
|
if (btn.getAttribute('data-action') === 'show-keyboard-help' && window.lt) {
|
|
lt.modal.open('lt-keys-help');
|
|
}
|
|
});
|
|
|
|
lt.keys.on('r', function() {
|
|
if (typeof refreshAll === 'function') refreshAll();
|
|
});
|
|
|
|
// Avatar image error fallback — hide broken img, reveal initials beneath
|
|
document.addEventListener('error', function(e) {
|
|
if (e.target.tagName === 'IMG' && e.target.closest('.lt-avatar')) {
|
|
e.target.classList.add('lt-avatar-img-err');
|
|
}
|
|
}, true);
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|