Files
gandalf/static/style.css
Jared Vititoe 0278dad502 feat: inspector page, link debug enhancements, security hardening
- Add /inspector page: visual model-accurate switch chassis diagrams
  (USF5P, USL8A, US24PRO, USPPDUP, USMINI), clickable port blocks
  with color coding (green=up, amber=PoE, cyan=uplink, grey=down),
  detail panel with stats/PoE/LLDP, LLDP-based path debug side-by-side

- Link Debug: port number badges (#N), LLDP neighbor line, PoE class/max,
  collapsible host/switch panels with sessionStorage persistence

- monitor.py: collect LLDP neighbor map + PoE class/max/mode per switch
  port; PulseClient uses requests.Session() for HTTP keep-alive; add
  shlex.quote() around interface names (defense-in-depth)

- Security: suppress buttons use data-* attrs + delegated click handler
  instead of inline onclick with Jinja2 variable interpolation; remove
  | safe filter from user-controlled fields in suppressions.html;
  setDuration() takes explicit el param instead of implicit event global

- db.py: thread-local connection reuse with ping(reconnect=True) to
  avoid a new TCP handshake per query

- .gitignore: add config.json (contains credentials), __pycache__

- README: full rewrite covering architecture, all 4 pages, alert logic,
  config reference, deployment, troubleshooting, security notes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 15:39:48 -05:00

1149 lines
40 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ══════════════════════════════════════════════════════════════════════
GANDALF Terminal aesthetic (Pulse / TinkerTickets style)
══════════════════════════════════════════════════════════════════════ */
/* ── Variables ────────────────────────────────────────────────────── */
:root {
--bg: #0a0a0a;
--bg2: #1a1a1a;
--bg3: #2a2a2a;
--bg-hover: rgba(0,255,65,.07);
--green: #00ff41;
--green-dim: rgba(0,255,65,.15);
--green-dark: #00cc33;
--green-muted: #008822;
--amber: #ffb000;
--amber-dim: rgba(255,176,0,.15);
--cyan: #00ffff;
--cyan-dim: rgba(0,255,255,.12);
--red: #ff4444;
--red-dim: rgba(255,68,68,.15);
--orange: #ff8c00;
--orange-dim: rgba(255,140,0,.15);
--border: rgba(0,255,65,.35);
--border-hi: #00ff41;
--text: #00ff41;
--text-dim: #00cc33;
--text-muted: #00bb33;
--font: 'Courier New','Consolas','Monaco','Menlo',monospace;
--glow: 0 0 5px #00ff41, 0 0 10px rgba(0,255,65,.4);
--glow-xl: 0 0 8px #00ff41, 0 0 20px rgba(0,255,65,.35);
--glow-amber: 0 0 5px #ffb000, 0 0 10px rgba(255,176,0,.4);
--glow-red: 0 0 5px #ff4444, 0 0 10px rgba(255,68,68,.4);
--glow-cyan: 0 0 5px #00ffff, 0 0 10px rgba(0,255,255,.35);
}
/* ── Reset ────────────────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { scrollbar-color: var(--green-muted) var(--bg2); scrollbar-width: thin; }
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: var(--bg2); }
::-webkit-scrollbar-thumb { background: var(--green-muted); }
::-webkit-scrollbar-thumb:hover { background: var(--green); }
/* ── Body / CRT ───────────────────────────────────────────────────── */
body {
font-family: var(--font);
background: var(--bg);
color: var(--text);
font-size: 14px;
line-height: 1.5;
min-height: 100vh;
position: relative;
animation: flicker .25s ease-in-out 45s infinite;
}
/* CRT scanline overlay */
body::before {
content: '';
position: fixed;
inset: 0;
background: repeating-linear-gradient(
0deg,
rgba(0,0,0,.13) 0px, rgba(0,0,0,.13) 1px,
transparent 1px, transparent 2px
);
pointer-events: none;
z-index: 9999;
animation: scanline 8s linear infinite;
}
/* Binary data stream corner */
body::after {
content: '10101010';
position: fixed;
bottom: 10px; right: 14px;
font-family: var(--font);
font-size: .55rem;
color: var(--green);
opacity: .07;
pointer-events: none;
letter-spacing: 2px;
animation: data-stream 3s steps(1) infinite;
}
@keyframes scanline { to { transform: translateY(4px); } }
@keyframes flicker { 0%,100%{opacity:1} 10%{opacity:.96} 50%{opacity:.98} }
@keyframes data-stream {
0% { content:'10101010'; } 25% { content:'01010101'; }
50% { content:'11001100'; } 75% { content:'00110011'; }
}
@keyframes pulse-glow {
0%,100% { text-shadow: var(--glow); }
50% { text-shadow: var(--glow-xl); }
}
@keyframes pulse-red {
0%,100% { box-shadow: 0 0 0 0 rgba(255,68,68,.5); }
50% { box-shadow: 0 0 6px 3px rgba(255,68,68,.2); }
}
@keyframes blink { 0%,49%{opacity:1} 50%,100%{opacity:0} }
@keyframes slide-in {
from { transform: translateX(110%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
a { color: var(--amber); text-decoration: none; }
a:hover { text-decoration: underline; text-shadow: var(--glow-amber); }
/* ── Header ───────────────────────────────────────────────────────── */
.header {
background: var(--bg2);
border-bottom: 2px solid var(--green);
box-shadow: 0 2px 16px rgba(0,255,65,.12);
padding: 0 28px;
height: 58px;
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
z-index: 100;
}
.header::before { content:'╔'; position:absolute; top:-1px; left:-1px; font-size:1.4rem; color:var(--green); text-shadow:var(--glow); line-height:1; }
.header::after { content:'╗'; position:absolute; top:-1px; right:-1px; font-size:1.4rem; color:var(--green); text-shadow:var(--glow); line-height:1; }
.header-left { display:flex; align-items:center; gap:24px; }
.header-brand { display:flex; flex-direction:column; }
.header-title {
font-size: 1.35em;
font-weight: bold;
color: var(--amber);
text-shadow: var(--glow-amber);
letter-spacing: .08em;
}
.header-title::before { content:'>> '; color:var(--green); text-shadow:var(--glow); }
.header-sub { font-size: .65em; color: var(--text-muted); letter-spacing: .12em; text-transform: uppercase; }
.header-nav { display:flex; gap:3px; }
.nav-link {
color: var(--text-muted);
padding: 5px 12px;
border: 1px solid transparent;
font-size: .8em;
letter-spacing: .06em;
text-transform: uppercase;
transition: all .15s;
}
.nav-link::before { content:'[ '; }
.nav-link::after { content:' ]'; }
.nav-link:hover, .nav-link.active {
color: var(--green);
border-color: var(--border);
background: var(--green-dim);
text-shadow: var(--glow);
text-decoration: none;
}
.header-right { display:flex; align-items:center; gap:10px; }
.header-user { font-size: .78em; color: var(--text-muted); }
.header-user::before { content:'[USER: '; }
.header-user::after { content:']'; }
/* ── Main ─────────────────────────────────────────────────────────── */
.main { max-width: 1500px; margin: 0 auto; padding: 22px 20px; }
/* ── Section ──────────────────────────────────────────────────────── */
.section { margin-bottom: 26px; }
.section-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
border-bottom: 1px solid var(--border);
padding-bottom: 5px;
}
.section-title {
font-size: .9em;
font-weight: bold;
color: var(--amber);
text-shadow: var(--glow-amber);
text-transform: uppercase;
letter-spacing: .1em;
}
.section-title::before { content:'╠══ '; color:var(--green); text-shadow:var(--glow); }
.section-badge {
font-size: .72em;
font-weight: bold;
color: var(--red);
border: 1px solid var(--red);
padding: 0 5px;
text-shadow: var(--glow-red);
}
.section-badge::before { content:'['; }
.section-badge::after { content:']'; }
/* ── Page header ──────────────────────────────────────────────────── */
.page-header { margin-bottom: 18px; }
.page-title {
font-size: 1.05em;
font-weight: bold;
color: var(--amber);
text-shadow: var(--glow-amber);
letter-spacing: .06em;
}
.page-title::before { content:'>> '; color:var(--green); text-shadow:var(--glow); }
.page-sub { font-size: .75em; color: var(--text-muted); margin-top: 3px; }
/* ── Status bar ───────────────────────────────────────────────────── */
.status-bar {
display: flex;
align-items: center;
justify-content: space-between;
background: var(--bg2);
border: 1px solid var(--border);
padding: 9px 16px;
margin-bottom: 18px;
gap: 12px;
flex-wrap: wrap;
position: relative;
}
.status-bar::before { content:'┌'; position:absolute; top:-1px; left:-1px; color:var(--green); font-size:.85rem; line-height:1; }
.status-bar::after { content:'┐'; position:absolute; top:-1px; right:-1px; color:var(--green); font-size:.85rem; line-height:1; }
.status-chips { display:flex; gap:7px; flex-wrap:wrap; align-items:center; }
.chip {
font-size: .78em;
font-weight: bold;
padding: 2px 9px;
border: 1px solid;
letter-spacing: .04em;
}
.chip::before { content:'['; }
.chip::after { content:']'; }
.chip-critical { color:var(--red); border-color:var(--red); text-shadow:var(--glow-red); animation:pulse-glow 2s infinite; }
.chip-warning { color:var(--orange); border-color:var(--orange); }
.chip-ok { color:var(--green); border-color:var(--border); text-shadow:var(--glow); }
.status-meta { display:flex; align-items:center; gap:10px; white-space:nowrap; }
.last-check { font-size: .72em; color: var(--text-muted); }
.btn-refresh {
background: transparent;
border: 1px solid var(--border);
color: var(--text-muted);
padding: 2px 10px;
font-family: var(--font);
font-size: .75em;
cursor: pointer;
transition: all .15s;
}
.btn-refresh:hover { color:var(--green); border-color:var(--green); background:var(--green-dim); text-shadow:var(--glow); }
/* ── Topology ─────────────────────────────────────────────────────── */
.topology {
background: var(--bg2);
border: 1px solid var(--border);
padding: 20px 16px 16px;
margin-bottom: 16px;
text-align: center;
overflow-x: auto;
position: relative;
}
.topology::before { content:'┌'; position:absolute; top:-1px; left:-1px; color:var(--green); font-size:.85rem; line-height:1; }
.topology::after { content:'┐'; position:absolute; top:-1px; right:-1px; color:var(--green); font-size:.85rem; line-height:1; }
.topo-row { display:flex; justify-content:center; gap:16px; flex-wrap:wrap; align-items:center; }
.topo-row-internet { margin-bottom:2px; }
.topo-hosts-row { flex-wrap:wrap; gap:10px; }
.topo-connectors { display:flex; justify-content:center; gap:80px; height:22px; margin:0; }
.topo-connectors.single { gap:0; }
.topo-connectors.wide { gap:44px; }
.topo-line { width:1px; height:100%; background:var(--green); opacity:.4; }
.topo-line-labeled { position:relative; }
.topo-line-labeled::after {
content: attr(data-link-label);
position: absolute;
left: 6px; top: 50%;
transform: translateY(-50%);
font-size: .62em;
color: var(--amber);
text-shadow: var(--glow-amber);
white-space: nowrap;
letter-spacing: .05em;
}
.topo-node {
display: flex;
flex-direction: column;
align-items: center;
gap: 3px;
padding: 7px 12px;
border: 1px solid var(--border);
background: var(--bg3);
min-width: 94px;
font-size: .75em;
position: relative;
transition: border-color .2s;
}
.topo-node::before { content:'┌'; position:absolute; top:-1px; left:-1px; color:var(--green); font-size:.8rem; line-height:1; }
.topo-node::after { content:'┐'; position:absolute; top:-1px; right:-1px; color:var(--green); font-size:.8rem; line-height:1; }
.topo-internet { border-color:var(--cyan); color:var(--cyan); text-shadow:var(--glow-cyan); font-weight:bold; }
.topo-switch { border-color:var(--amber); color:var(--amber); text-shadow:var(--glow-amber); }
.topo-host { cursor:default; }
.topo-icon { font-size:1.1em; }
.topo-label { font-weight:bold; letter-spacing:.03em; }
.topo-badge { font-size:.68em; padding:1px 5px; border:1px solid; letter-spacing:.03em; }
.topo-badge-up { color:var(--green); border-color:var(--green); text-shadow:var(--glow); }
.topo-badge-down { color:var(--red); border-color:var(--red); text-shadow:var(--glow-red); animation:pulse-glow 1.5s infinite; }
.topo-badge-degraded { color:var(--orange); border-color:var(--orange); }
.topo-status-up { border-color:var(--green); box-shadow:0 0 8px rgba(0,255,65,.2); }
.topo-status-down { border-color:var(--red); box-shadow:0 0 8px rgba(255,68,68,.3); }
.topo-status-degraded { border-color:var(--orange); box-shadow:0 0 8px rgba(255,140,0,.2); }
.topo-status-dot { width:7px; height:7px; border:1px solid var(--text-muted); background:transparent; position:absolute; top:5px; right:5px; }
/* ── Host cards ───────────────────────────────────────────────────── */
.host-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(248px, 1fr));
gap: 12px;
}
.host-card {
background: var(--bg2);
border: 1px solid var(--border);
padding: 12px;
position: relative;
transition: border-color .2s, box-shadow .2s;
}
.host-card::before { content:'┌'; position:absolute; top:-1px; left:-1px; color:var(--green); font-size:.85rem; line-height:1; }
.host-card::after { content:'┐'; position:absolute; top:-1px; right:-1px; color:var(--green); font-size:.85rem; line-height:1; }
.host-card:hover { border-color:var(--green); box-shadow:0 0 12px rgba(0,255,65,.12); }
.host-card-up { border-left: 3px solid var(--green); }
.host-card-down { border-left: 3px solid var(--red); box-shadow: inset 3px 0 10px rgba(255,68,68,.08); }
.host-card-degraded { border-left: 3px solid var(--orange); }
.host-card-header { margin-bottom: 8px; }
.host-name-row { display:flex; align-items:center; gap:6px; margin-bottom:3px; }
.host-name {
font-weight: bold;
font-size: .88em;
color: var(--amber);
text-shadow: var(--glow-amber);
letter-spacing: .04em;
}
.host-meta { display:flex; gap:6px; align-items:center; flex-wrap:wrap; }
.host-ip { font-size:.72em; color:var(--text-muted); letter-spacing:.02em; }
.host-source {
font-size: .65em;
padding: 1px 5px;
border: 1px solid;
letter-spacing: .04em;
font-weight: bold;
}
.source-prometheus { color:#e8703a; border-color:rgba(232,112,58,.4); }
.source-ping { color:var(--cyan); border-color:var(--cyan-dim); }
.iface-list { border-top:1px solid var(--border); padding-top:6px; margin-bottom:8px; }
.iface-row { display:flex; align-items:center; gap:6px; padding:2px 0; }
.iface-name { font-size:.78em; flex:1; color:var(--text-dim); letter-spacing:.01em; }
.iface-state { font-size:.72em; font-weight:bold; letter-spacing:.04em; }
.state-up { color:var(--green); text-shadow:var(--glow); }
.state-down { color:var(--red); text-shadow:var(--glow-red); }
.state-initial_down { color:var(--text-muted); }
.host-ping-note { font-size:.72em; color:var(--text-muted); border-top:1px solid var(--border); padding-top:6px; margin-bottom:8px; }
.host-actions { border-top:1px solid var(--border); padding-top:7px; display:flex; gap:5px; flex-wrap:wrap; }
/* ── Status dots ──────────────────────────────────────────────────── */
.host-status-dot, .iface-dot,
.dot-up, .dot-down, .dot-degraded, .dot-unknown, .dot-initial_down {
display: inline-block;
width: 8px; height: 8px;
border: 1px solid;
flex-shrink: 0;
}
.dot-up, .host-status-dot.dot-up { border-color:var(--green); background:var(--green); box-shadow:0 0 4px var(--green); }
.dot-down, .host-status-dot.dot-down { border-color:var(--red); background:var(--red); animation:pulse-red 1.5s infinite; }
.dot-degraded { border-color:var(--orange); background:var(--orange); }
.dot-unknown, .dot-initial_down { border-color:var(--text-muted); background:transparent; }
/* ── Badges ───────────────────────────────────────────────────────── */
.badge {
display: inline-block;
font-size: .7em;
font-weight: bold;
padding: 1px 6px;
border: 1px solid;
letter-spacing: .05em;
text-transform: uppercase;
}
.badge::before { content:'['; }
.badge::after { content:']'; }
.badge-critical { color:var(--red); border-color:var(--red); text-shadow:var(--glow-red); }
.badge-warning { color:var(--orange); border-color:var(--orange); }
.badge-info { color:var(--cyan); border-color:var(--cyan-dim); }
.badge-ok { color:var(--green); border-color:var(--border); text-shadow:var(--glow); }
.badge-neutral { color:var(--text-muted); border-color:var(--text-muted); }
.badge-suppressed{ font-size:.9em; padding:0; border:none; color:var(--text-muted); }
/* ── Tables ───────────────────────────────────────────────────────── */
.table-wrap {
background: var(--bg2);
border: 1px solid var(--border);
overflow: hidden;
position: relative;
}
.table-wrap::before { content:'┌'; position:absolute; top:-1px; left:-1px; color:var(--green); font-size:.85rem; line-height:1; }
.table-wrap::after { content:'┐'; position:absolute; top:-1px; right:-1px; color:var(--green); font-size:.85rem; line-height:1; }
.data-table { width:100%; border-collapse:collapse; }
.data-table th {
background: var(--bg3);
padding: 8px 12px;
text-align: left;
font-size: .7em;
font-weight: bold;
color: var(--amber);
text-transform: uppercase;
letter-spacing: .08em;
border-bottom: 1px solid var(--border);
white-space: nowrap;
text-shadow: var(--glow-amber);
}
.data-table th::before { content:'> '; color:var(--green); }
.data-table td {
padding: 7px 12px;
border-bottom: 1px solid rgba(0,255,65,.08);
vertical-align: middle;
font-size: .83em;
}
.data-table tr:last-child td { border-bottom:none; }
.data-table tr:hover td { background:var(--bg-hover); }
.row-critical td { background:rgba(255,68,68,.03); }
.row-critical td:first-child { border-left:2px solid var(--red); }
.row-warning td { background:rgba(255,140,0,.03); }
.row-warning td:first-child { border-left:2px solid var(--orange); }
.row-resolved td { opacity:.5; }
.data-table-sm td, .data-table-sm th { padding:5px 10px; }
.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; }
.empty-row td{ text-align:center; color:var(--text-muted); }
/* ── Buttons ──────────────────────────────────────────────────────── */
.btn {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 6px 14px;
border: 1px solid;
cursor: pointer;
font-family: var(--font);
font-size: .8em;
font-weight: bold;
letter-spacing: .05em;
text-transform: uppercase;
background: transparent;
transition: all .15s;
}
.btn:hover { transform: translateY(-1px); }
.btn-primary { color:var(--green); border-color:var(--green); text-shadow:var(--glow); }
.btn-primary::before { content:'> '; color:var(--amber); }
.btn-primary:hover { background:var(--green-dim); box-shadow:var(--glow); }
.btn-secondary { color:var(--text-dim); border-color:var(--border); }
.btn-secondary:hover { color:var(--green); border-color:var(--green); background:var(--bg-hover); }
.btn-danger { color:var(--red); border-color:rgba(255,68,68,.35); }
.btn-danger:hover { background:var(--red-dim); border-color:var(--red); text-shadow:var(--glow-red); }
.btn-lg { padding:8px 18px; font-size:.85em; }
.btn-sm {
padding: 2px 8px;
font-family: var(--font);
font-size: .7em;
font-weight: bold;
border: 1px solid;
cursor: pointer;
background: transparent;
letter-spacing: .04em;
transition: all .15s;
}
.btn-suppress { color:var(--text-muted); border-color:var(--text-muted); }
.btn-suppress:hover { color:var(--amber); border-color:var(--amber); }
.btn-danger.btn-sm { color:var(--red); border-color:rgba(255,68,68,.35); }
.btn-danger.btn-sm:hover{ color:var(--red); border-color:var(--red); text-shadow:var(--glow-red); }
/* ── Modal ────────────────────────────────────────────────────────── */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,.8);
z-index: 200;
display: flex;
align-items: center;
justify-content: center;
}
.modal {
background: var(--bg2);
border: 1px solid var(--green);
box-shadow: 0 0 30px rgba(0,255,65,.18);
width: 480px;
max-width: 95vw;
padding: 20px;
position: relative;
}
.modal::before { content:'┌'; position:absolute; top:-1px; left:-1px; color:var(--green); text-shadow:var(--glow); font-size:.9rem; line-height:1; }
.modal::after { content:'┐'; position:absolute; top:-1px; right:-1px; color:var(--green); text-shadow:var(--glow); font-size:.9rem; line-height:1; }
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
border-bottom: 1px solid var(--border);
padding-bottom: 10px;
}
.modal-header h3 { font-size:.88em; color:var(--amber); text-shadow:var(--glow-amber); text-transform:uppercase; letter-spacing:.08em; }
.modal-header h3::before { content:'>> '; color:var(--green); }
.modal-close {
background: none;
border: 1px solid var(--border);
cursor: pointer;
font-size: .82em;
color: var(--text-muted);
padding: 2px 8px;
font-family: var(--font);
transition: all .15s;
}
.modal-close:hover { color:var(--red); border-color:var(--red); }
.modal-actions {
display: flex;
gap: 8px;
justify-content: flex-end;
margin-top: 16px;
padding-top: 12px;
border-top: 1px solid var(--border);
}
/* ── Forms ────────────────────────────────────────────────────────── */
.form-card {
background: var(--bg2);
border: 1px solid var(--border);
padding: 16px;
position: relative;
}
.form-card::before { content:'┌'; position:absolute; top:-1px; left:-1px; color:var(--green); font-size:.85rem; line-height:1; }
.form-card::after { content:'┐'; position:absolute; top:-1px; right:-1px; color:var(--green); font-size:.85rem; line-height:1; }
.form-row { display:flex; gap:12px; flex-wrap:wrap; margin-bottom:10px; }
.form-row-align { align-items:flex-end; }
.form-group { display:flex; flex-direction:column; gap:4px; min-width:150px; flex:1; }
.form-group-wide{ flex:3; }
.form-group-submit { flex:0 0 auto; min-width:unset; }
.form-group label {
font-size: .7em;
font-weight: bold;
color: var(--amber);
text-transform: uppercase;
letter-spacing: .07em;
text-shadow: var(--glow-amber);
}
.form-group input,
.form-group select {
padding: 6px 9px;
border: 1px solid var(--border);
font-family: var(--font);
font-size: .8em;
background: var(--bg3);
color: var(--text);
transition: border-color .15s, box-shadow .15s;
}
.form-group input::placeholder { color: var(--text-muted); }
.form-group input:focus,
.form-group select:focus {
outline: none;
border-color: var(--amber);
box-shadow: 0 0 6px rgba(255,176,0,.18);
}
.form-group select option { background: var(--bg3); color: var(--text); }
.form-hint { font-size:.7em; color:var(--text-muted); margin-top:2px; }
.required { color:var(--red); }
/* ── Duration pills ───────────────────────────────────────────────── */
.duration-pills { display:flex; gap:5px; flex-wrap:wrap; margin-bottom:5px; }
.pill {
padding: 3px 10px;
border: 1px solid var(--border);
background: transparent;
font-family: var(--font);
font-size: .72em;
font-weight: bold;
cursor: pointer;
color: var(--text-muted);
transition: all .15s;
letter-spacing: .04em;
}
.pill:hover { border-color:var(--green); color:var(--green); background:var(--green-dim); }
.pill.active,
.pill-manual.active { border-color:var(--amber); color:var(--amber); background:var(--amber-dim); text-shadow:var(--glow-amber); }
/* ── Targets grid ─────────────────────────────────────────────────── */
.targets-grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(180px, 1fr)); gap:10px; }
.target-card {
background: var(--bg2);
border: 1px solid var(--border);
padding: 10px;
position: relative;
}
.target-card::before { content:'┌'; position:absolute; top:-1px; left:-1px; color:var(--green); font-size:.78rem; line-height:1; }
.target-card::after { content:'┐'; position:absolute; top:-1px; right:-1px; color:var(--green); font-size:.78rem; line-height:1; }
.target-name { font-weight:bold; font-size:.82em; margin-bottom:3px; color:var(--amber); }
.target-type { font-size:.7em; color:var(--text-muted); margin-bottom:6px; }
.target-ifaces{ display:flex; flex-wrap:wrap; gap:3px; }
.iface-chip { font-family:var(--font); font-size:.65em; background:var(--bg3); border:1px solid var(--border); padding:1px 5px; color:var(--text-dim); }
/* ── Toast ────────────────────────────────────────────────────────── */
.toast-container { position:fixed; bottom:20px; right:20px; z-index:300; display:flex; flex-direction:column; gap:7px; }
.toast {
padding: 9px 16px;
border: 1px solid;
font-family: var(--font);
font-size: .8em;
font-weight: bold;
background: var(--bg2);
animation: slide-in .15s ease;
letter-spacing: .04em;
}
.toast::before { content:'>> '; }
.toast-success { color:var(--green); border-color:var(--green); text-shadow:var(--glow); }
.toast-error { color:var(--red); border-color:var(--red); text-shadow:var(--glow-red); }
/* ── Link debug page ──────────────────────────────────────────────── */
.link-host-list { display:flex; flex-direction:column; gap:18px; }
.link-host-panel {
background: var(--bg2);
border: 1px solid var(--border);
position: relative;
}
.link-host-panel::before { content:'╔'; position:absolute; top:-1px; left:-1px; color:var(--green); text-shadow:var(--glow); font-size:1rem; line-height:1; }
.link-host-panel::after { content:'╗'; position:absolute; top:-1px; right:-1px; color:var(--green); text-shadow:var(--glow); font-size:1rem; line-height:1; }
.link-host-title {
display: flex;
align-items: center;
gap: 12px;
padding: 9px 16px;
background: var(--bg3);
border-bottom: 1px solid var(--border);
}
.link-host-name { font-weight:bold; font-size:.88em; color:var(--amber); text-shadow:var(--glow-amber); letter-spacing:.05em; }
.link-host-name::before { content:'>> '; color:var(--green); }
.link-host-ip { font-size:.72em; color:var(--text-muted); }
.link-host-upd { font-size:.65em; color:var(--text-muted); margin-left:auto; }
.link-ifaces-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
}
.link-iface-card {
border-right: 1px solid rgba(0,255,65,.15);
border-bottom: 1px solid rgba(0,255,65,.15);
padding: 12px 14px;
}
.link-iface-card:last-child { border-right:none; }
.link-iface-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
padding-bottom: 6px;
border-bottom: 1px solid rgba(0,255,65,.12);
}
.link-iface-name { font-weight:bold; font-size:.84em; color:var(--amber); text-shadow:var(--glow-amber); flex:1; }
.link-iface-speed { font-size:.75em; color:var(--cyan); text-shadow:var(--glow-cyan); font-weight:bold; }
.link-iface-type { font-size:.65em; color:var(--text-muted); padding:1px 5px; border:1px solid var(--text-muted); letter-spacing:.04em; }
.link-iface-type.type-fibre { color:var(--cyan); border-color:var(--cyan-dim); text-shadow:var(--glow-cyan); }
.link-iface-type.type-copper{ color:var(--green); border-color:var(--border); }
.link-iface-type.type-da { color:var(--amber); border-color:var(--amber-dim); }
/* Link stats 2-column grid */
.link-stats-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px 16px;
margin-bottom: 10px;
}
.link-stat { display:flex; flex-direction:column; gap:1px; }
.link-stat-label { font-size:.6em; color:var(--text-muted); text-transform:uppercase; letter-spacing:.07em; }
.link-stat-value { font-size:.78em; font-weight:bold; color:var(--text-dim); }
.val-good { color:var(--green); text-shadow:var(--glow); }
.val-warn { color:var(--orange); }
.val-crit { color:var(--red); text-shadow:var(--glow-red); }
.val-neutral { color:var(--text-muted); }
.val-cyan { color:var(--cyan); text-shadow:var(--glow-cyan); }
/* Traffic bars */
.traffic-section { margin-top:8px; padding-top:8px; border-top:1px solid rgba(0,255,65,.1); }
.traffic-row { display:flex; align-items:center; gap:8px; margin-bottom:5px; }
.traffic-label { font-size:.62em; color:var(--text-muted); width:20px; text-transform:uppercase; letter-spacing:.04em; flex-shrink:0; }
.traffic-bar-track { flex:1; height:5px; background:var(--bg); border:1px solid rgba(0,255,65,.2); position:relative; overflow:hidden; }
.traffic-bar-fill { height:100%; position:absolute; left:0; top:0; transition:width .4s; }
.traffic-tx { background:var(--cyan); box-shadow:0 0 3px rgba(0,255,255,.4); }
.traffic-rx { background:var(--green); box-shadow:0 0 3px rgba(0,255,65,.4); }
.traffic-value { font-size:.7em; color:var(--text-dim); width:68px; text-align:right; flex-shrink:0; }
/* SFP / optical panel */
.sfp-panel {
margin-top: 10px;
padding: 10px 10px 8px;
background: var(--bg3);
border: 1px solid rgba(0,255,255,.2);
position: relative;
}
.sfp-panel::before {
content: '[ SFP / OPTICAL ]';
position: absolute;
top: -8px; left: 10px;
font-size: .6em;
color: var(--cyan);
text-shadow: var(--glow-cyan);
background: var(--bg3);
padding: 0 4px;
letter-spacing: .09em;
font-weight: bold;
}
.sfp-vendor-row { font-size:.7em; color:var(--text-muted); margin-bottom:8px; }
.sfp-vendor-row span { color:var(--text-dim); }
.sfp-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 6px 12px;
}
.sfp-stat { display:flex; flex-direction:column; gap:1px; }
.sfp-stat-label { font-size:.58em; color:var(--text-muted); text-transform:uppercase; letter-spacing:.07em; }
.sfp-stat-value { font-size:.78em; font-weight:bold; }
/* Power level with bar */
.power-row { display:flex; align-items:center; gap:5px; margin-top:1px; }
.power-track { flex:1; height:3px; background:var(--bg); border:1px solid rgba(0,255,65,.2); position:relative; overflow:hidden; }
.power-fill { height:100%; position:absolute; left:0; top:0; transition:width .4s; }
.power-ok { background:var(--green); box-shadow:0 0 3px var(--green); }
.power-warn { background:var(--orange); }
.power-crit { background:var(--red); box-shadow:0 0 3px var(--red); }
/* Collapsible link panels */
.link-host-title {
cursor: pointer;
user-select: none;
}
.link-host-title:hover { background: rgba(0,255,65,.04); }
.panel-toggle {
font-size: .65em;
color: var(--text-muted);
letter-spacing: .04em;
flex-shrink: 0;
margin-left: 6px;
padding: 0 4px;
border: 1px solid rgba(0,255,65,.2);
}
.link-host-panel.collapsed > .link-ifaces-grid { display: none; }
/* Collapse all / Expand all bar */
.link-collapse-bar {
display: flex;
gap: 8px;
margin-bottom: 10px;
}
/* Link panel states */
.link-no-data { padding:14px; color:var(--text-muted); font-size:.78em; text-align:center; }
.link-loading { padding:20px; text-align:center; color:var(--text-muted); font-size:.8em; }
.link-loading::after { content:' ...'; animation:blink 1s step-end infinite; }
/* Counters (errors/drops) */
.counter-zero { color:var(--green); }
.counter-nonzero { color:var(--red); text-shadow:var(--glow-red); }
/* UniFi switch section divider */
.unifi-section-header {
display: flex;
align-items: center;
gap: 12px;
margin: 24px 0 12px;
color: var(--cyan);
font-size: .75em;
letter-spacing: .1em;
text-shadow: var(--glow-cyan);
}
.unifi-section-header::before,
.unifi-section-header::after {
content: '';
flex: 1;
height: 1px;
background: linear-gradient(90deg, transparent, var(--cyan), transparent);
}
/* Port badges (UPLINK, PoE, #N) */
.port-badge {
font-size: .58em;
padding: 1px 5px;
border: 1px solid;
letter-spacing: .05em;
font-weight: bold;
vertical-align: middle;
}
.port-badge-uplink { color:var(--amber); border-color:var(--amber-dim); }
.port-badge-poe { color:var(--cyan); border-color:var(--cyan-dim); }
.port-badge-num { color:var(--text-muted); border-color:rgba(0,255,65,.2); }
/* LLDP neighbor + PoE info lines on link debug cards */
.port-lldp {
font-size: .68em;
color: var(--cyan);
text-shadow: var(--glow-cyan);
margin: -4px 0 6px;
letter-spacing: .02em;
}
.port-poe-info {
font-size: .68em;
color: var(--amber);
margin: -4px 0 6px;
letter-spacing: .02em;
}
/* Amber value colour used in inspector */
.val-amber { color:var(--amber); text-shadow:var(--glow-amber); }
/* Down port card — dim everything */
.link-iface-card.port-down {
opacity: .42;
filter: saturate(.3);
}
/* ── Inspector page ───────────────────────────────────────────────── */
/* Layout: main chassis area + collapsible right panel */
.inspector-layout {
display: flex;
gap: 16px;
align-items: flex-start;
min-height: 300px;
}
.inspector-main {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 14px;
}
/* Switch chassis card */
.inspector-chassis {
background: var(--bg2);
border: 1px solid var(--border);
position: relative;
}
.inspector-chassis::before { content:'╔'; position:absolute; top:-1px; left:-1px; color:var(--green); text-shadow:var(--glow); font-size:1rem; line-height:1; }
.inspector-chassis::after { content:'╗'; position:absolute; top:-1px; right:-1px; color:var(--green); text-shadow:var(--glow); font-size:1rem; line-height:1; }
.chassis-header {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 16px;
background: var(--bg3);
border-bottom: 1px solid var(--border);
}
.chassis-name { font-weight:bold; font-size:.88em; color:var(--amber); text-shadow:var(--glow-amber); letter-spacing:.05em; }
.chassis-name::before { content:'>> '; color:var(--green); }
.chassis-ip { font-size:.72em; color:var(--text-muted); }
.chassis-meta { font-size:.65em; color:var(--text-muted); margin-left:auto; }
.chassis-body {
padding: 12px 16px 14px;
}
/* Port rows */
.chassis-rows { display:flex; flex-direction:column; gap:5px; margin-bottom:8px; }
.chassis-row { display:flex; flex-wrap:wrap; gap:4px; }
/* SFP section below main rows */
.chassis-sfp-section {
display: flex;
gap: 6px;
padding-top: 8px;
border-top: 1px solid rgba(0,255,255,.15);
margin-top: 4px;
}
/* Individual port block */
.switch-port-block {
width: 34px;
height: 34px;
display: flex;
align-items: center;
justify-content: center;
font-size: .6em;
font-weight: bold;
border: 1px solid;
cursor: pointer;
transition: box-shadow .1s, border-color .1s, background .1s;
user-select: none;
flex-shrink: 0;
letter-spacing: 0;
}
/* SFP port (in rows — slightly narrower to suggest cage) */
.switch-port-block.sfp-port {
width: 28px;
height: 38px;
font-size: .55em;
}
/* SFP section block (standalone cage) */
.switch-port-block.sfp-block {
width: 44px;
height: 30px;
font-size: .55em;
letter-spacing: .04em;
}
/* State colours */
.switch-port-block.down {
background: var(--bg3);
border-color: rgba(0,255,65,.15);
color: rgba(0,255,65,.25);
}
.switch-port-block.up {
background: rgba(0,255,65,.06);
border-color: var(--green-muted);
color: var(--green);
text-shadow: 0 0 4px rgba(0,255,65,.5);
}
.switch-port-block.up:hover {
background: rgba(0,255,65,.13);
border-color: var(--green);
box-shadow: var(--glow);
}
.switch-port-block.poe-active {
background: var(--amber-dim);
border-color: var(--amber);
color: var(--amber);
text-shadow: 0 0 4px rgba(255,176,0,.5);
}
.switch-port-block.poe-active:hover {
box-shadow: var(--glow-amber);
}
.switch-port-block.uplink {
background: var(--cyan-dim);
border-color: var(--cyan);
color: var(--cyan);
text-shadow: 0 0 4px rgba(0,255,255,.5);
}
.switch-port-block.uplink:hover {
box-shadow: var(--glow-cyan);
}
.switch-port-block.selected {
outline: 2px solid #fff;
outline-offset: 1px;
}
/* Right-side detail panel */
.inspector-panel {
width: 0;
overflow: hidden;
flex-shrink: 0;
transition: width .2s ease;
display: flex;
flex-direction: column;
}
.inspector-panel.open {
width: 310px;
}
.inspector-panel-inner {
width: 310px;
background: var(--bg2);
border: 1px solid var(--border);
padding: 14px 14px 18px;
position: relative;
overflow-y: auto;
max-height: calc(100vh - 120px);
}
.inspector-panel-inner::before { content:'╔'; position:absolute; top:-1px; left:-1px; color:var(--green); text-shadow:var(--glow); font-size:1rem; line-height:1; }
.inspector-panel-inner::after { content:'╗'; position:absolute; top:-1px; right:-1px; color:var(--green); text-shadow:var(--glow); font-size:1rem; line-height:1; }
.panel-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
padding-bottom: 10px;
border-bottom: 1px solid var(--border);
}
.panel-port-name { font-weight:bold; font-size:.92em; color:var(--amber); text-shadow:var(--glow-amber); }
.panel-meta { font-size:.68em; color:var(--text-muted); margin-top:2px; }
.panel-close {
background: none;
border: 1px solid var(--border);
color: var(--text-muted);
cursor: pointer;
font-size: .8em;
padding: 1px 7px;
font-family: var(--font);
flex-shrink: 0;
transition: all .15s;
}
.panel-close:hover { color:var(--red); border-color:var(--red); }
.panel-section-title {
font-size: .62em;
font-weight: bold;
color: var(--amber);
text-shadow: var(--glow-amber);
text-transform: uppercase;
letter-spacing: .1em;
margin: 10px 0 5px;
padding-bottom: 3px;
border-bottom: 1px solid rgba(0,255,65,.12);
}
.panel-section-title:first-of-type { margin-top: 0; }
.panel-row {
display: flex;
justify-content: space-between;
align-items: baseline;
padding: 2px 0;
}
.panel-label { font-size:.68em; color:var(--text-muted); text-transform:uppercase; letter-spacing:.05em; flex-shrink:0; }
.panel-val { font-size:.75em; font-weight:bold; color:var(--text-dim); text-align:right; word-break:break-all; }
/* Path debug two-column layout */
.path-conn-type {
font-size: .68em;
color: var(--cyan);
font-weight: normal;
margin-left: 6px;
text-shadow: none;
text-transform: none;
letter-spacing: normal;
}
.path-debug-cols {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin-top: 6px;
}
.path-col {
background: var(--bg3);
border: 1px solid rgba(0,255,65,.18);
padding: 7px 8px;
}
.path-col-header {
font-size: .62em;
font-weight: bold;
color: var(--amber);
margin-bottom: 5px;
padding-bottom: 3px;
border-bottom: 1px solid rgba(0,255,65,.15);
letter-spacing: .04em;
}
.path-row {
display: flex;
justify-content: space-between;
gap: 4px;
font-size: .65em;
padding: 1px 0;
}
.path-row span:first-child { color:var(--text-muted); flex-shrink:0; }
.path-row span:last-child { color:var(--text-dim); font-weight:bold; text-align:right; word-break:break-all; }
.path-dom {
margin-top: 5px;
padding-top: 5px;
border-top: 1px solid rgba(0,255,255,.15);
}
.path-dom-row {
display: flex;
justify-content: space-between;
font-size: .65em;
padding: 1px 0;
color: var(--cyan);
}
.path-dom-row span:first-child { color:var(--text-muted); }
/* ── Responsive ───────────────────────────────────────────────────── */
@media (max-width: 768px) {
.host-grid { grid-template-columns:1fr; }
.topology { display:none; }
.form-row { flex-direction:column; }
.status-bar { flex-direction:column; align-items:flex-start; }
.link-ifaces-grid { grid-template-columns:1fr; }
.sfp-grid { grid-template-columns:1fr 1fr; }
.header-nav { display:none; }
.inspector-layout { flex-direction:column; }
.inspector-panel.open { width:100%; }
.inspector-panel-inner { width:100%; }
}