Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ed19838a4e |
@@ -334,6 +334,10 @@ async function submitSuppress(e) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Suppress form – wired here so the modal works from any page ──────
|
||||
document.getElementById('suppress-form')?.addEventListener('submit', submitSuppress);
|
||||
document.getElementById('sup-type')?.addEventListener('change', updateSuppressForm);
|
||||
|
||||
// ── Global click delegation ───────────────────────────────────────────
|
||||
document.addEventListener('click', e => {
|
||||
// Refresh button
|
||||
|
||||
+39
-8
@@ -96,6 +96,45 @@
|
||||
content: '…';
|
||||
}
|
||||
|
||||
/* ── Secondary button – dark-mode definition ─────────────────────────
|
||||
base.css only defines .lt-btn-secondary in its light-theme block,
|
||||
so dark mode falls back to the default cyan primary appearance.
|
||||
This restores a visually distinct secondary look in dark mode. */
|
||||
.lt-btn-secondary {
|
||||
background: var(--cyan-dim);
|
||||
border-color: rgba(0,212,255,.28);
|
||||
color: var(--cyan);
|
||||
}
|
||||
.lt-btn-secondary:hover {
|
||||
background: rgba(0,212,255,.18);
|
||||
border-color: rgba(0,212,255,.5);
|
||||
}
|
||||
|
||||
/* ── ⌘K hint button in header ────────────────────────────────────── */
|
||||
.lt-cmd-hint-btn {
|
||||
font-size: 0.65rem;
|
||||
opacity: 0.55;
|
||||
letter-spacing: 0.03em;
|
||||
padding: 0.2rem 0.45rem;
|
||||
}
|
||||
|
||||
/* ── Topology section collapse toggle ────────────────────────────── */
|
||||
.topo-collapse-btn {
|
||||
margin-left: auto;
|
||||
font-size: .7em;
|
||||
color: var(--text-muted);
|
||||
background: transparent;
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 2px 8px;
|
||||
cursor: pointer;
|
||||
font-family: var(--font);
|
||||
letter-spacing: .04em;
|
||||
transition: border-color .15s, color .15s;
|
||||
}
|
||||
.topo-collapse-btn:hover { border-color: var(--amber); color: var(--amber); }
|
||||
.topo-collapsible { overflow: hidden; transition: max-height .25s ease; }
|
||||
.topo-collapsible.is-collapsed { display: none; }
|
||||
|
||||
/* ── Animations used by custom components ─────────────────────────── */
|
||||
@keyframes pulse-red {
|
||||
0%,100% { box-shadow: 0 0 0 0 rgba(255,45,85,.5); }
|
||||
@@ -145,13 +184,6 @@
|
||||
.events-filter-bar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||||
.events-filter-bar .lt-input-sm { width: 220px; }
|
||||
.sev-pills { display: flex; gap: 4px; }
|
||||
.g-page-header { margin-bottom: 20px; }
|
||||
.g-page-title {
|
||||
font-size: 1em;
|
||||
font-weight: bold;
|
||||
color: var(--text-accent);
|
||||
letter-spacing: .06em;
|
||||
}
|
||||
.g-page-sub { font-size: .78em; color: var(--text-muted); margin-top: 4px; }
|
||||
|
||||
/* ── Badge severity color variants (used with lt-badge) ───────────── */
|
||||
@@ -180,7 +212,6 @@
|
||||
.ts-cell { color: var(--text-muted); font-size: .75em; white-space: nowrap; }
|
||||
.desc-cell { max-width: 280px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.ticket-link{ color: var(--amber); text-shadow: var(--glow-amber); font-weight: bold; }
|
||||
.empty-state { padding: 28px; text-align: center; color: var(--text-muted); font-size: .82em; }
|
||||
.pagination-notice { font-size: .8em; color: var(--text-muted); padding: 6px 0 8px; }
|
||||
.pagination-notice a { color: var(--amber); }
|
||||
|
||||
|
||||
+59
-34
@@ -73,7 +73,6 @@
|
||||
<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>
|
||||
@@ -96,28 +95,6 @@
|
||||
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 %}>
|
||||
@@ -166,11 +143,10 @@
|
||||
|
||||
<!-- ⌘K affordance -->
|
||||
<button type="button"
|
||||
class="lt-btn lt-btn-ghost lt-btn-sm"
|
||||
class="lt-btn lt-btn-ghost lt-btn-sm lt-cmd-hint-btn"
|
||||
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>
|
||||
onclick="if(window.lt&<.cmdPalette)lt.cmdPalette.open()">⌕ K</button>
|
||||
|
||||
<button type="button" class="lt-theme-btn" id="lt-theme-btn"
|
||||
aria-label="Toggle theme" title="Toggle light/dark mode">☀</button>
|
||||
@@ -221,6 +197,59 @@
|
||||
<span>GANDALF — TDS v1.2</span>
|
||||
</footer>
|
||||
|
||||
<!-- QUICK-SUPPRESS MODAL — available on all pages via [S] shortcut -->
|
||||
<div id="suppress-modal" class="lt-modal-overlay"
|
||||
role="dialog" aria-modal="true" aria-labelledby="suppress-modal-title" aria-hidden="true">
|
||||
<div class="lt-modal">
|
||||
<div class="lt-modal-header">
|
||||
<h3 class="lt-modal-title" id="suppress-modal-title">Suppress Alert</h3>
|
||||
<button type="button" class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
|
||||
</div>
|
||||
<form id="suppress-form">
|
||||
<div class="lt-modal-body">
|
||||
<div class="lt-form-group" style="margin-bottom:12px">
|
||||
<label class="lt-label" for="sup-type">Target Type</label>
|
||||
<select class="lt-select" id="sup-type" name="target_type">
|
||||
<option value="host">Host (all interfaces)</option>
|
||||
<option value="interface">Specific Interface</option>
|
||||
<option value="unifi_device">UniFi Device</option>
|
||||
<option value="all">Global Maintenance</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="lt-form-group" id="sup-name-group" style="margin-bottom:12px">
|
||||
<label class="lt-label" for="sup-name">Target Name</label>
|
||||
<input type="text" class="lt-input" id="sup-name" name="target_name" placeholder="e.g. large1">
|
||||
</div>
|
||||
<div class="lt-form-group" id="sup-detail-group" style="margin-bottom:12px;display:none">
|
||||
<label class="lt-label" for="sup-detail">Interface <span class="lt-field-hint">(interface type only)</span></label>
|
||||
<input type="text" class="lt-input" id="sup-detail" name="target_detail" placeholder="e.g. enp35s0">
|
||||
</div>
|
||||
<div class="lt-form-group" style="margin-bottom:12px">
|
||||
<label class="lt-label" for="sup-reason">Reason <span class="required">*</span></label>
|
||||
<input type="text" class="lt-input" id="sup-reason" name="reason"
|
||||
placeholder="e.g. Planned switch reboot" required>
|
||||
</div>
|
||||
<div class="lt-form-group" style="margin-bottom:0">
|
||||
<label class="lt-label">Duration</label>
|
||||
<div class="duration-pills">
|
||||
<button type="button" class="pill" data-duration="30">30 min</button>
|
||||
<button type="button" class="pill" data-duration="60">1 hr</button>
|
||||
<button type="button" class="pill" data-duration="240">4 hr</button>
|
||||
<button type="button" class="pill" data-duration="480">8 hr</button>
|
||||
<button type="button" class="pill pill-manual active" data-duration="">Manual ∞</button>
|
||||
</div>
|
||||
<input type="hidden" id="sup-expires" name="expires_minutes" value="">
|
||||
<div class="lt-field-hint" id="duration-hint">Persists until manually removed.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lt-modal-footer">
|
||||
<button type="button" class="lt-btn lt-btn-secondary" data-modal-close>Cancel</button>
|
||||
<button type="submit" class="lt-btn lt-btn-primary">Apply</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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">
|
||||
@@ -268,15 +297,11 @@
|
||||
</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>
|
||||
<span class="lt-kv-key">User</span>
|
||||
<span class="lt-kv-val lt-kv-val--cyan">{{ user.name or user.username }}</span>
|
||||
{% 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>
|
||||
<span class="lt-kv-key">Groups</span>
|
||||
<span class="lt-kv-val">{{ user.groups | join(', ') }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+126
-155
@@ -62,11 +62,112 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Active alerts ─────────────────────────────── (above the fold) -->
|
||||
<section class="g-section">
|
||||
<div class="g-section-header">
|
||||
<h2 class="g-section-title">Active Alerts</h2>
|
||||
<span class="g-section-badge" id="alert-count-badge"
|
||||
{% if not summary.critical and not summary.warning %}style="display:none"{% endif %}>{{ (summary.critical or 0) + (summary.warning or 0) }}</span>
|
||||
</div>
|
||||
<div class="lt-toolbar">
|
||||
<div class="lt-toolbar-left">
|
||||
<div class="lt-search">
|
||||
<input type="search" class="lt-input lt-search-input" id="events-search"
|
||||
placeholder="Filter by target, type, description…" autocomplete="off">
|
||||
</div>
|
||||
<div class="sev-pills">
|
||||
<button type="button" class="pill active" data-sev="">All</button>
|
||||
<button type="button" class="pill" data-sev="critical">Critical</button>
|
||||
<button type="button" class="pill" data-sev="warning">Warning</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lt-frame">
|
||||
<span class="lt-frame-bl">╚</span>
|
||||
<span class="lt-frame-br">╝</span>
|
||||
<div class="lt-section-header">Alert Queue</div>
|
||||
<div id="events-table-wrap">
|
||||
{% if events %}
|
||||
{% if total_active is defined and total_active > events|length %}
|
||||
<div class="pagination-notice">Showing {{ events|length }} of {{ total_active }} active alerts — <a href="/api/events?limit=1000">view all via API</a></div>
|
||||
{% endif %}
|
||||
<div class="lt-table-wrap">
|
||||
<table class="lt-table" id="events-table">
|
||||
<caption class="lt-sr-only">Active network alerts</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-tooltip="Alert severity level" data-tooltip-pos="bottom">Sev</th>
|
||||
<th>Type</th>
|
||||
<th>Target</th>
|
||||
<th>Detail</th>
|
||||
<th>Description</th>
|
||||
<th data-tooltip="When this alert was first raised" data-tooltip-pos="bottom">First Seen</th>
|
||||
<th data-tooltip="Most recent check failure" data-tooltip-pos="bottom">Last Seen</th>
|
||||
<th data-tooltip="Consecutive check failures since first seen" data-tooltip-pos="bottom">Failures</th>
|
||||
<th>Ticket</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for e in events %}
|
||||
{% if e.severity != 'info' %}
|
||||
<tr class="row-{{ e.severity }}">
|
||||
<td><span class="lt-badge badge-{{ e.severity }}">{{ e.severity }}</span></td>
|
||||
<td>{{ e.event_type | replace('_', ' ') }}</td>
|
||||
<td><strong>{{ e.target_name }}</strong></td>
|
||||
<td>{{ e.target_detail or '–' }}</td>
|
||||
<td class="desc-cell" title="{{ e.description | e }}">{{ e.description | truncate(60) }}</td>
|
||||
<td class="ts-cell" title="{{ e.first_seen }}">
|
||||
<span class="event-age" data-ts="{{ e.first_seen }}">{{ e.first_seen }}</span>
|
||||
</td>
|
||||
<td class="ts-cell" title="{{ e.last_seen }}">
|
||||
<span class="event-age" data-ts="{{ e.last_seen }}">{{ e.last_seen }}</span>
|
||||
</td>
|
||||
<td>{{ e.consecutive_failures }}</td>
|
||||
<td>
|
||||
{% if e.ticket_id %}
|
||||
<a href="{{ config.ticket_api.web_url }}{{ e.ticket_id }}" target="_blank"
|
||||
class="ticket-link">#{{ e.ticket_id }}</a>
|
||||
{% else %}–{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<button class="lt-btn lt-btn-ghost lt-btn-sm btn-suppress"
|
||||
data-sup-type="{{ 'unifi_device' if e.event_type == 'unifi_device_offline' else 'interface' if e.event_type == 'interface_down' else 'host' }}"
|
||||
data-sup-name="{{ e.target_name }}"
|
||||
data-sup-detail="{{ e.target_detail or '' }}"
|
||||
title="Suppress" aria-label="Suppress alert for {{ e.target_name }}">🔕</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<tr><td colspan="10">
|
||||
<div class="lt-empty-state lt-empty-state--sm">
|
||||
<div class="lt-empty-state-icon">✔</div>
|
||||
<div class="lt-empty-state-title">No active alerts</div>
|
||||
</div>
|
||||
</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="lt-empty-state lt-empty-state--sm">
|
||||
<div class="lt-empty-state-icon">✔</div>
|
||||
<div class="lt-empty-state-title">No active alerts</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div><!-- /#events-table-wrap -->
|
||||
</div><!-- /.lt-frame -->
|
||||
</section>
|
||||
|
||||
<!-- ── Network topology + host grid ───────────────────────────────── -->
|
||||
<section class="g-section">
|
||||
<div class="g-section-header">
|
||||
<h2 class="g-section-title">Network Hosts</h2>
|
||||
<button type="button" class="topo-collapse-btn" id="topo-toggle-btn"
|
||||
aria-expanded="true" aria-controls="topo-collapsible-wrap">▴ Collapse</button>
|
||||
</div>
|
||||
<div class="topo-collapsible" id="topo-collapsible-wrap">
|
||||
|
||||
<div class="topology" id="topology-diagram">
|
||||
<div class="topo-v2">
|
||||
@@ -208,7 +309,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Host cards -->
|
||||
<div class="lt-toolbar" style="margin-bottom:10px" id="host-toolbar">
|
||||
<div class="lt-toolbar" id="host-toolbar">
|
||||
<div class="lt-toolbar-left">
|
||||
<div class="lt-search">
|
||||
<input type="search" class="lt-input lt-search-input" id="host-search"
|
||||
@@ -257,7 +358,7 @@
|
||||
🔕 Suppress
|
||||
</button>
|
||||
<a href="{{ url_for('links_page') }}#{{ name }}"
|
||||
class="lt-btn lt-btn-secondary lt-btn-sm" style="text-decoration:none">
|
||||
class="lt-btn lt-btn-secondary lt-btn-sm">
|
||||
↗ Links
|
||||
</a>
|
||||
</div>
|
||||
@@ -270,6 +371,7 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div><!-- /#topo-collapsible-wrap -->
|
||||
</section>
|
||||
|
||||
<!-- ── UniFi devices ────────────────────────────────────────────────── -->
|
||||
@@ -325,104 +427,6 @@
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<!-- ── Active alerts ───────────────────────────────────────────────── -->
|
||||
<section class="g-section">
|
||||
<div class="g-section-header">
|
||||
<h2 class="g-section-title">Active Alerts</h2>
|
||||
<span class="g-section-badge" id="alert-count-badge"
|
||||
{% if not summary.critical and not summary.warning %}style="display:none"{% endif %}>{{ (summary.critical or 0) + (summary.warning or 0) }}</span>
|
||||
</div>
|
||||
<div class="lt-toolbar">
|
||||
<div class="lt-toolbar-left">
|
||||
<div class="lt-search">
|
||||
<input type="search" class="lt-input lt-search-input" id="events-search"
|
||||
placeholder="Filter by target, type, description…" autocomplete="off">
|
||||
</div>
|
||||
<div class="sev-pills">
|
||||
<button type="button" class="pill active" data-sev="">All</button>
|
||||
<button type="button" class="pill" data-sev="critical">Critical</button>
|
||||
<button type="button" class="pill" data-sev="warning">Warning</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lt-frame">
|
||||
<span class="lt-frame-bl">╚</span>
|
||||
<span class="lt-frame-br">╝</span>
|
||||
<div class="lt-section-header">Alert Queue</div>
|
||||
<div id="events-table-wrap">
|
||||
{% if events %}
|
||||
{% if total_active is defined and total_active > events|length %}
|
||||
<div class="pagination-notice">Showing {{ events|length }} of {{ total_active }} active alerts — <a href="/api/events?limit=1000">view all via API</a></div>
|
||||
{% endif %}
|
||||
<div class="lt-table-wrap">
|
||||
<table class="lt-table" id="events-table">
|
||||
<caption class="lt-sr-only">Active network alerts</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-tooltip="Alert severity level" data-tooltip-pos="bottom">Sev</th>
|
||||
<th>Type</th>
|
||||
<th>Target</th>
|
||||
<th>Detail</th>
|
||||
<th>Description</th>
|
||||
<th data-tooltip="When this alert was first raised" data-tooltip-pos="bottom">First Seen</th>
|
||||
<th data-tooltip="Most recent check failure" data-tooltip-pos="bottom">Last Seen</th>
|
||||
<th data-tooltip="Consecutive check failures since first seen" data-tooltip-pos="bottom">Failures</th>
|
||||
<th>Ticket</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for e in events %}
|
||||
{% if e.severity != 'info' %}
|
||||
<tr class="row-{{ e.severity }}">
|
||||
<td><span class="lt-badge badge-{{ e.severity }}">{{ e.severity }}</span></td>
|
||||
<td>{{ e.event_type | replace('_', ' ') }}</td>
|
||||
<td><strong>{{ e.target_name }}</strong></td>
|
||||
<td>{{ e.target_detail or '–' }}</td>
|
||||
<td class="desc-cell" title="{{ e.description | e }}">{{ e.description | truncate(60) }}</td>
|
||||
<td class="ts-cell" title="{{ e.first_seen }}">
|
||||
<span class="event-age" data-ts="{{ e.first_seen }}">{{ e.first_seen }}</span>
|
||||
</td>
|
||||
<td class="ts-cell" title="{{ e.last_seen }}">
|
||||
<span class="event-age" data-ts="{{ e.last_seen }}">{{ e.last_seen }}</span>
|
||||
</td>
|
||||
<td>{{ e.consecutive_failures }}</td>
|
||||
<td>
|
||||
{% if e.ticket_id %}
|
||||
<a href="{{ config.ticket_api.web_url }}{{ e.ticket_id }}" target="_blank"
|
||||
class="ticket-link">#{{ e.ticket_id }}</a>
|
||||
{% else %}–{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<button class="lt-btn lt-btn-ghost lt-btn-sm btn-suppress"
|
||||
data-sup-type="{{ 'unifi_device' if e.event_type == 'unifi_device_offline' else 'interface' if e.event_type == 'interface_down' else 'host' }}"
|
||||
data-sup-name="{{ e.target_name }}"
|
||||
data-sup-detail="{{ e.target_detail or '' }}"
|
||||
title="Suppress" aria-label="Suppress alert for {{ e.target_name }}">🔕</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<tr><td colspan="10">
|
||||
<div class="lt-empty-state lt-empty-state--sm">
|
||||
<div class="lt-empty-state-icon">✔</div>
|
||||
<div class="lt-empty-state-title">No active alerts</div>
|
||||
</div>
|
||||
</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="lt-empty-state lt-empty-state--sm">
|
||||
<div class="lt-empty-state-icon">✔</div>
|
||||
<div class="lt-empty-state-title">No active alerts</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div><!-- /#events-table-wrap -->
|
||||
</div><!-- /.lt-frame -->
|
||||
</section>
|
||||
|
||||
<!-- ── Recently Resolved (last 24h) ───────────────────────────────── -->
|
||||
{% if recent_resolved %}
|
||||
<section class="g-section">
|
||||
@@ -452,59 +456,6 @@
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<!-- ── Quick-suppress modal ─────────────────────────────────────────── -->
|
||||
<div id="suppress-modal" class="lt-modal-overlay"
|
||||
role="dialog" aria-modal="true" aria-labelledby="suppress-modal-title" aria-hidden="true">
|
||||
<div class="lt-modal">
|
||||
<div class="lt-modal-header">
|
||||
<h3 class="lt-modal-title" id="suppress-modal-title">Suppress Alert</h3>
|
||||
<button type="button" class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
|
||||
</div>
|
||||
<form id="suppress-form">
|
||||
<div class="lt-modal-body">
|
||||
<div class="lt-form-group" style="margin-bottom:12px">
|
||||
<label class="lt-label" for="sup-type">Target Type</label>
|
||||
<select class="lt-select" id="sup-type" name="target_type">
|
||||
<option value="host">Host (all interfaces)</option>
|
||||
<option value="interface">Specific Interface</option>
|
||||
<option value="unifi_device">UniFi Device</option>
|
||||
<option value="all">Global Maintenance</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="lt-form-group" id="sup-name-group" style="margin-bottom:12px">
|
||||
<label class="lt-label" for="sup-name">Target Name</label>
|
||||
<input type="text" class="lt-input" id="sup-name" name="target_name" placeholder="e.g. large1">
|
||||
</div>
|
||||
<div class="lt-form-group" id="sup-detail-group" style="margin-bottom:12px;display:none">
|
||||
<label class="lt-label" for="sup-detail">Interface <span class="lt-field-hint">(interface type only)</span></label>
|
||||
<input type="text" class="lt-input" id="sup-detail" name="target_detail" placeholder="e.g. enp35s0">
|
||||
</div>
|
||||
<div class="lt-form-group" style="margin-bottom:12px">
|
||||
<label class="lt-label" for="sup-reason">Reason <span class="required">*</span></label>
|
||||
<input type="text" class="lt-input" id="sup-reason" name="reason"
|
||||
placeholder="e.g. Planned switch reboot" required>
|
||||
</div>
|
||||
<div class="lt-form-group" style="margin-bottom:0">
|
||||
<label class="lt-label">Duration</label>
|
||||
<div class="duration-pills">
|
||||
<button type="button" class="pill" data-duration="30">30 min</button>
|
||||
<button type="button" class="pill" data-duration="60">1 hr</button>
|
||||
<button type="button" class="pill" data-duration="240">4 hr</button>
|
||||
<button type="button" class="pill" data-duration="480">8 hr</button>
|
||||
<button type="button" class="pill pill-manual active" data-duration="">Manual ∞</button>
|
||||
</div>
|
||||
<input type="hidden" id="sup-expires" name="expires_minutes" value="">
|
||||
<div class="lt-field-hint" id="duration-hint">Persists until manually removed.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lt-modal-footer">
|
||||
<button type="button" class="lt-btn lt-btn-secondary" data-modal-close>Cancel</button>
|
||||
<button type="submit" class="lt-btn lt-btn-primary">Apply</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
@@ -519,8 +470,28 @@
|
||||
if (s.refreshInterval > 0) lt.autoRefresh.start(refreshAll, s.refreshInterval * 1000);
|
||||
};
|
||||
|
||||
document.getElementById('suppress-form')?.addEventListener('submit', submitSuppress);
|
||||
document.getElementById('sup-type')?.addEventListener('change', updateSuppressForm);
|
||||
// ── Topology collapse toggle ───────────────────────────────────
|
||||
(function() {
|
||||
var LS_KEY = 'gandalf_topo_collapsed';
|
||||
var btn = document.getElementById('topo-toggle-btn');
|
||||
var wrap = document.getElementById('topo-collapsible-wrap');
|
||||
if (!btn || !wrap) return;
|
||||
|
||||
function setCollapsed(v) {
|
||||
wrap.classList.toggle('is-collapsed', v);
|
||||
btn.setAttribute('aria-expanded', v ? 'false' : 'true');
|
||||
btn.textContent = v ? '▾ Expand' : '▴ Collapse';
|
||||
try { localStorage.setItem(LS_KEY, v ? '1' : '0'); } catch(_) {}
|
||||
}
|
||||
|
||||
var saved = false;
|
||||
try { saved = localStorage.getItem(LS_KEY) === '1'; } catch(_) {}
|
||||
setCollapsed(saved);
|
||||
|
||||
btn.addEventListener('click', function() {
|
||||
setCollapsed(!wrap.classList.contains('is-collapsed'));
|
||||
});
|
||||
})();
|
||||
|
||||
function updateEventAges() {
|
||||
document.querySelectorAll('.event-age[data-ts]').forEach(el => {
|
||||
|
||||
Reference in New Issue
Block a user