feat: deep link diagnostics via Pulse SSH

Adds comprehensive per-port link troubleshooting triggered from the
Inspector panel when a port has an LLDP-identified server counterpart.

- diagnose.py: DiagnosticsRunner with 15-section SSH command (carrier,
  operstate, sysfs counters, ethtool, ethtool -i/-a/-g/-S/-m, ip link,
  ip addr, ip route, dmesg, lldpctl); parsers for all sections; health
  analyzer with 14 check codes (NO_CARRIER, HALF_DUPLEX, SPEED_MISMATCH,
  SFP_RX_CRITICAL, CARRIER_FLAPPING, CRC_ERRORS_HIGH, LLDP_MISMATCH, etc.)
- monitor.py: PulseClient now tracks last_execution_id so callers can
  link back to the raw Pulse execution URL
- app.py: POST /api/diagnose + GET /api/diagnose/<job_id> with daemon
  thread background execution and 10-minute in-memory job store
- inspector.html: "Run Link Diagnostics" button (shown only when LLDP
  host is resolvable); full results panel: health banner, physical layer,
  SFP/DOM with power bars, NIC error counters, collapsible ethtool -S,
  flow control/ring buffers, driver info, LLDP 2-col validation,
  collapsible dmesg, switch port summary, "View in Pulse" link
- style.css: all .diag-* CSS classes with terminal aesthetic

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-03 16:03:54 -05:00
parent 0278dad502
commit b1dd5f9cad
5 changed files with 1272 additions and 0 deletions

View File

@@ -1133,6 +1133,283 @@ a:hover { text-decoration: underline; text-shadow: var(--glow-amber); }
}
.path-dom-row span:first-child { color:var(--text-muted); }
/* ── Link Diagnostics ─────────────────────────────────────────────── */
.diag-bar {
display: flex;
align-items: center;
gap: 10px;
margin-top: 14px;
padding-top: 10px;
border-top: 1px solid var(--border);
}
.btn-diag {
font-family: var(--font);
font-size: .65em;
color: var(--cyan);
background: transparent;
border: 1px solid var(--cyan);
padding: 4px 10px;
cursor: pointer;
letter-spacing: .04em;
transition: background .15s, box-shadow .15s;
animation: diag-pulse 2.5s ease-in-out infinite;
}
.btn-diag:hover {
background: var(--cyan-dim);
box-shadow: var(--glow-cyan);
}
@keyframes diag-pulse {
0%, 100% { box-shadow: none; }
50% { box-shadow: 0 0 6px rgba(0,255,255,.4); }
}
.diag-status {
font-size: .6em;
color: var(--text-muted);
font-style: italic;
}
.diag-error {
color: var(--red);
font-size: .65em;
margin-top: 8px;
}
.diag-results {
margin-top: 4px;
}
.diag-results-inner {
display: flex;
flex-direction: column;
gap: 6px;
}
/* Health banner */
.diag-health-banner {
display: flex;
gap: 8px;
padding: 6px 0 4px;
margin-bottom: 2px;
}
.diag-health-critical {
background: var(--red-dim);
color: var(--red);
border: 1px solid var(--red);
padding: 2px 8px;
font-size: .62em;
font-weight: bold;
letter-spacing: .05em;
}
.diag-health-warning {
background: var(--amber-dim);
color: var(--amber);
border: 1px solid var(--amber);
padding: 2px 8px;
font-size: .62em;
font-weight: bold;
letter-spacing: .05em;
}
.diag-health-ok {
background: var(--green-dim);
color: var(--green);
border: 1px solid var(--green);
padding: 2px 8px;
font-size: .62em;
font-weight: bold;
letter-spacing: .05em;
}
/* Issue list */
.diag-issue-list {
display: flex;
flex-direction: column;
gap: 3px;
}
.diag-issue-row {
font-size: .62em;
padding: 3px 6px;
background: var(--bg2);
border-left: 2px solid var(--border);
line-height: 1.4;
}
.diag-code {
font-weight: bold;
color: var(--amber);
}
/* Sections */
.diag-section {
background: var(--bg2);
border: 1px solid rgba(0,255,65,.12);
}
.diag-section-header {
font-size: .62em;
font-weight: bold;
color: var(--amber);
padding: 4px 8px;
letter-spacing: .04em;
border-bottom: 1px solid rgba(0,255,65,.12);
background: rgba(255,176,0,.04);
}
/* Collapsible sections */
.diag-collapsible .diag-section-body {
display: none;
}
.diag-collapsible.diag-open .diag-section-body {
display: block;
}
.diag-toggle {
cursor: pointer;
user-select: none;
}
.diag-toggle-hint {
font-weight: normal;
color: var(--text-muted);
font-size: .9em;
}
.diag-collapsible.diag-open .diag-toggle-hint::after {
content: '';
}
/* Data tables */
.diag-table {
width: 100%;
border-collapse: collapse;
font-size: .62em;
}
.diag-table td {
padding: 3px 8px;
vertical-align: top;
}
.diag-table td:first-child {
color: var(--text-muted);
width: 40%;
white-space: nowrap;
}
.diag-table td:last-child {
color: var(--text-dim);
font-weight: bold;
word-break: break-all;
}
.diag-table tr:nth-child(even) {
background: rgba(0,255,65,.025);
}
/* Value colour classes */
.diag-val-good { color: var(--green); }
.diag-val-warn { color: var(--amber); }
.diag-val-bad { color: var(--red); }
/* SFP power bar */
.diag-power-bar-wrap {
position: relative;
display: inline-block;
width: 60px;
height: 7px;
background: var(--bg3);
border: 1px solid var(--border);
vertical-align: middle;
margin-left: 6px;
overflow: visible;
}
.diag-power-bar {
display: inline-block;
position: absolute;
left: 0;
top: 0;
height: 100%;
}
.diag-power-bar.diag-val-good { background: var(--green); }
.diag-power-bar.diag-val-warn { background: var(--amber); }
.diag-power-bar.diag-val-bad { background: var(--red); }
.diag-power-zone-warn,
.diag-power-zone-crit {
position: absolute;
top: -2px;
width: 1px;
height: calc(100% + 4px);
pointer-events: none;
}
.diag-power-zone-warn { background: var(--amber); opacity: .7; }
.diag-power-zone-crit { background: var(--red); opacity: .7; }
/* ethtool -S stat table */
.diag-stat-table {
width: 100%;
border-collapse: collapse;
font-size: .58em;
}
.diag-stat-table td {
padding: 2px 8px;
}
.diag-stat-table td:first-child { color: var(--text-muted); }
.diag-stat-table td:last-child { color: var(--text-dim); text-align: right; }
.diag-stat-nonzero-warn {
background: var(--amber-dim);
}
.diag-stat-nonzero-warn td { color: var(--amber); }
/* dmesg */
.diag-dmesg-wrap {
max-height: 200px;
overflow-y: auto;
padding: 6px 8px;
}
.diag-dmesg-line {
font-family: var(--font);
font-size: .58em;
white-space: pre-wrap;
word-break: break-all;
padding: 1px 0;
color: var(--text-dim);
}
.diag-dmesg-warn { color: var(--amber); }
.diag-dmesg-err { color: var(--red); }
/* Pulse link */
.diag-pulse-link {
font-size: .62em;
padding: 4px 0;
text-align: right;
}
.diag-pulse-link a {
color: var(--cyan);
text-decoration: none;
}
.diag-pulse-link a:hover {
text-shadow: var(--glow-cyan);
}
/* ── Responsive ───────────────────────────────────────────────────── */
@media (max-width: 768px) {
.host-grid { grid-template-columns:1fr; }