c1c3905179
Lint / Python (flake8) (push) Failing after 1m7s
Lint / JS (eslint) (push) Successful in 10s
Security / Python Security (bandit) (push) Failing after 1m31s
Test / Python Tests (pytest) (push) Successful in 1m8s
Lint / Notify on failure (push) Successful in 2s
Lint / Deploy (push) Has been skipped
- app.py: avatar_color Jinja filter using deterministic hash → lt-avatar--orange/green/purple - base.html: proper lt-avatar--sm with lt-avatar-initials span and color class; multi-word initials support - base.html: admin users get lt-nav-dropdown for Suppressions; non-admins see flat link; mobile drawer hides Suppressions for non-admins Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
252 lines
11 KiB
HTML
252 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 }}">
|
|
<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();
|
|
});
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|