2026-03-01 23:03:18 -05:00
'use strict' ;
2026-03-14 21:40:20 -04:00
// ── Toast notifications — delegates to lt.toast from base.js ─────────
2026-03-01 23:03:18 -05:00
function showToast ( msg , type = 'success' ) {
2026-03-14 21:40:20 -04:00
if ( type === 'error' ) return lt . toast . error ( msg ) ;
if ( type === 'warning' ) return lt . toast . warning ( msg ) ;
if ( type === 'info' ) return lt . toast . info ( msg ) ;
return lt . toast . success ( msg ) ;
2025-02-07 21:03:31 -05:00
}
2026-03-01 23:03:18 -05:00
// ── Dashboard auto-refresh ────────────────────────────────────────────
async function refreshAll ( ) {
try {
const [ netResp , statusResp ] = await Promise . all ( [
fetch ( '/api/network' ) ,
fetch ( '/api/status' ) ,
] ) ;
if ( ! netResp . ok || ! statusResp . ok ) return ;
const net = await netResp . json ( ) ;
const status = await statusResp . json ( ) ;
updateHostGrid ( net . hosts || { } ) ;
updateUnifiTable ( net . unifi || [ ] ) ;
2026-03-17 20:32:32 -04:00
updateEventsTable ( status . events || [ ] , status . total _active ) ;
2026-03-01 23:03:18 -05:00
updateStatusBar ( status . summary || { } , status . last _check || '' ) ;
updateTopology ( net . hosts || { } ) ;
} catch ( e ) {
console . warn ( 'Refresh failed:' , e ) ;
}
2025-02-08 00:32:25 -05:00
}
2026-03-01 23:03:18 -05:00
function updateStatusBar ( summary , lastCheck ) {
const bar = document . querySelector ( '.status-chips' ) ;
if ( ! bar ) return ;
const chips = [ ] ;
feat: terminal aesthetic rewrite + link debug page
- Full dark terminal aesthetic (Pulse/TinkerTickets style):
- #0a0a0a background, #00ff41 green, #ffb000 amber, #00ffff cyan
- CRT scanline overlay, phosphor glow, ASCII corner pseudoelements
- Bracket-notation badges [CRITICAL], monospace font throughout
- style.css, base.html, index.html, suppressions.html all rewritten
- New Link Debug page (/links, /api/links):
- Per-host, per-interface cards with speed/duplex/port type/auto-neg
- Traffic bars (TX cyan, RX green) with rate labels
- Error/drop counters, carrier change history
- SFP/DOM optical panel: vendor, temp, voltage, bias, TX/RX power dBm bars
- RX-TX delta shown; color-coded warn/crit thresholds
- Auto-refresh every 60s, anchor-jump to #hostname
- LinkStatsCollector in monitor.py:
- SSHes to each host (one connection, all ifaces batched)
- Parses ethtool + ethtool -m (SFP DOM) output
- Merges with Prometheus traffic/error/carrier metrics
- Stores as link_stats in monitor_state table
- config.json: added ssh section for ethtool collection
- app.js: terminal chip style consistency (uppercase, ● bullet)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 12:43:11 -05:00
if ( summary . critical ) chips . push ( ` <span class="chip chip-critical">● ${ summary . critical } CRITICAL</span> ` ) ;
if ( summary . warning ) chips . push ( ` <span class="chip chip-warning">● ${ summary . warning } WARNING</span> ` ) ;
if ( ! summary . critical && ! summary . warning ) chips . push ( '<span class="chip chip-ok">✔ ALL SYSTEMS NOMINAL</span>' ) ;
2026-03-01 23:03:18 -05:00
bar . innerHTML = chips . join ( '' ) ;
const lc = document . getElementById ( 'last-check' ) ;
feat: terminal aesthetic rewrite + link debug page
- Full dark terminal aesthetic (Pulse/TinkerTickets style):
- #0a0a0a background, #00ff41 green, #ffb000 amber, #00ffff cyan
- CRT scanline overlay, phosphor glow, ASCII corner pseudoelements
- Bracket-notation badges [CRITICAL], monospace font throughout
- style.css, base.html, index.html, suppressions.html all rewritten
- New Link Debug page (/links, /api/links):
- Per-host, per-interface cards with speed/duplex/port type/auto-neg
- Traffic bars (TX cyan, RX green) with rate labels
- Error/drop counters, carrier change history
- SFP/DOM optical panel: vendor, temp, voltage, bias, TX/RX power dBm bars
- RX-TX delta shown; color-coded warn/crit thresholds
- Auto-refresh every 60s, anchor-jump to #hostname
- LinkStatsCollector in monitor.py:
- SSHes to each host (one connection, all ifaces batched)
- Parses ethtool + ethtool -m (SFP DOM) output
- Merges with Prometheus traffic/error/carrier metrics
- Stores as link_stats in monitor_state table
- config.json: added ssh section for ethtool collection
- app.js: terminal chip style consistency (uppercase, ● bullet)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 12:43:11 -05:00
if ( lc && lastCheck ) lc . textContent = lastCheck ;
2026-03-14 21:35:32 -04:00
// Update browser tab title with alert count
const critCount = summary . critical || 0 ;
const warnCount = summary . warning || 0 ;
if ( critCount ) {
document . title = ` ( ${ critCount } CRIT) GANDALF ` ;
} else if ( warnCount ) {
document . title = ` ( ${ warnCount } WARN) GANDALF ` ;
} else {
document . title = 'GANDALF' ;
}
// Stale data banner: warn if last_check is older than 15 minutes
let staleBanner = document . getElementById ( 'stale-banner' ) ;
if ( lastCheck ) {
// last_check format: "2026-03-14 14:14:21 UTC"
const checkAge = ( Date . now ( ) - new Date ( lastCheck . replace ( ' UTC' , 'Z' ) . replace ( ' ' , 'T' ) ) ) / 1000 ;
if ( checkAge > 900 ) { // 15 minutes
if ( ! staleBanner ) {
staleBanner = document . createElement ( 'div' ) ;
staleBanner . id = 'stale-banner' ;
staleBanner . className = 'stale-banner' ;
document . querySelector ( '.main' ) . prepend ( staleBanner ) ;
}
const mins = Math . floor ( checkAge / 60 ) ;
staleBanner . textContent = ` ⚠ Monitoring data is stale — last check was ${ mins } minute ${ mins !== 1 ? 's' : '' } ago. The monitor daemon may be down. ` ;
staleBanner . style . display = '' ;
} else if ( staleBanner ) {
staleBanner . style . display = 'none' ;
}
}
2025-02-07 23:54:28 -05:00
}
2026-03-01 23:03:18 -05:00
function updateHostGrid ( hosts ) {
for ( const [ name , host ] of Object . entries ( hosts ) ) {
const card = document . querySelector ( ` .host-card[data-host=" ${ CSS . escape ( name ) } "] ` ) ;
if ( ! card ) continue ;
// Update card border class
card . className = card . className . replace ( /host-card-(up|down|degraded|unknown)/g , '' ) ;
card . classList . add ( ` host-card- ${ host . status } ` ) ;
// Update status dot in header
const dot = card . querySelector ( '.host-status-dot' ) ;
if ( dot ) dot . className = ` host-status-dot dot- ${ host . status } ` ;
2025-02-07 23:54:28 -05:00
2026-03-01 23:03:18 -05:00
// Update interface rows
const ifaceList = card . querySelector ( '.iface-list' ) ;
if ( ifaceList && host . interfaces && Object . keys ( host . interfaces ) . length > 0 ) {
ifaceList . innerHTML = Object . entries ( host . interfaces )
. sort ( ( [ a ] , [ b ] ) => a . localeCompare ( b ) )
. map ( ( [ iface , state ] ) => `
< div class = "iface-row" >
< span class = "iface-dot dot-${state}" > < / s p a n >
< span class = "iface-name" > $ { escHtml ( iface ) } < / s p a n >
< span class = "iface-state state-${state}" > $ { state } < / s p a n >
< / d i v >
` ).join('');
2025-02-07 23:54:28 -05:00
}
2026-03-01 23:03:18 -05:00
}
2025-02-07 23:54:28 -05:00
}
2026-03-01 23:03:18 -05:00
function updateTopology ( hosts ) {
document . querySelectorAll ( '.topo-host' ) . forEach ( node => {
const name = node . dataset . host ;
const host = hosts [ name ] ;
if ( ! host ) return ;
2026-03-14 22:22:19 -04:00
node . className = node . className . replace ( /topo-v2-status-(up|down|degraded|unknown)/g , '' ) ;
2026-03-01 23:03:18 -05:00
node . className = node . className . replace ( /topo-status-(up|down|degraded|unknown)/g , '' ) ;
2026-03-14 22:22:19 -04:00
node . classList . add ( ` topo-v2-status- ${ host . status } ` ) ;
2026-03-01 23:03:18 -05:00
node . classList . add ( ` topo-status- ${ host . status } ` ) ;
const badge = node . querySelector ( '.topo-badge' ) ;
if ( badge ) {
badge . className = ` topo-badge topo-badge- ${ host . status } ` ;
badge . textContent = host . status ;
}
} ) ;
2025-02-07 21:28:54 -05:00
}
2026-03-01 23:03:18 -05:00
function updateUnifiTable ( devices ) {
const tbody = document . querySelector ( '#unifi-table tbody' ) ;
if ( ! tbody || ! devices . length ) return ;
tbody . innerHTML = devices . map ( d => {
const statusClass = d . connected ? '' : 'row-critical' ;
const dotClass = d . connected ? 'dot-up' : 'dot-down' ;
const statusText = d . connected ? 'Online' : 'Offline' ;
const suppressBtn = ! d . connected
? ` <button class="btn-sm btn-suppress"
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
data - sup - type = "unifi_device"
data - sup - name = "${escHtml(d.name)}"
data - sup - detail = "" > 🔕 Suppress < / b u t t o n > `
2026-03-01 23:03:18 -05:00
: '' ;
return `
< tr class = "${statusClass}" >
< td > < span class = "${dotClass}" > < / s p a n > $ { s t a t u s T e x t } < / t d >
< td > < strong > $ { escHtml ( d . name ) } < / s t r o n g > < / t d >
< td > $ { escHtml ( d . type ) } < / t d >
< td > $ { escHtml ( d . model ) } < / t d >
< td > $ { escHtml ( d . ip ) } < / t d >
< td > $ { suppressBtn } < / t d >
< / t r > ` ;
} ) . join ( '' ) ;
2025-01-04 01:42:16 -05:00
}
2026-03-17 20:32:32 -04:00
function updateEventsTable ( events , totalActive ) {
2026-03-01 23:03:18 -05:00
const wrap = document . getElementById ( 'events-table-wrap' ) ;
if ( ! wrap ) return ;
const active = events . filter ( e => e . severity !== 'info' ) ;
if ( ! active . length ) {
wrap . innerHTML = '<p class="empty-state">No active alerts ✔</p>' ;
return ;
}
2026-03-17 20:32:32 -04:00
const truncated = totalActive != null && totalActive > active . length ;
const countNotice = truncated
? ` <div class="pagination-notice">Showing ${ active . length } of ${ totalActive } active alerts — <a href="/api/events?limit=1000">view all via API</a></div> `
: '' ;
2026-03-01 23:03:18 -05:00
const rows = active . map ( e => {
const supType = e . event _type === 'unifi_device_offline' ? 'unifi_device'
: e . event _type === 'interface_down' ? 'interface'
: 'host' ;
2026-03-14 14:31:57 -04:00
const ticketBase = ( typeof GANDALF _CONFIG !== 'undefined' && GANDALF _CONFIG . ticket _web _url )
? GANDALF _CONFIG . ticket _web _url : 'http://t.lotusguild.org/ticket/' ;
2026-03-01 23:03:18 -05:00
const ticket = e . ticket _id
2026-03-14 14:31:57 -04:00
? ` <a href=" ${ ticketBase } ${ e . ticket _id } " target="_blank"
2026-03-01 23:03:18 -05:00
class = "ticket-link" > # $ { e . ticket _id } < / a > `
: '– ' ;
return `
< tr class = "row-${e.severity}" >
< td > < span class = "badge badge-${e.severity}" > $ { e . severity } < / s p a n > < / t d >
< td > $ { escHtml ( e . event _type . replace ( /_/g , ' ' ) ) } < / t d >
< td > < strong > $ { escHtml ( e . target _name ) } < / s t r o n g > < / t d >
< td > $ { escHtml ( e . target _detail || '– ' ) } < / t d >
< td class = "desc-cell" title = "${escHtml(e.description || '')}" > $ { escHtml ( ( e . description || '' ) . substring ( 0 , 60 ) ) } $ { ( e . description || '' ) . length > 60 ? '…' : '' } < / t d >
2026-03-14 21:46:11 -04:00
< td class = "ts-cell" title = "${escHtml(e.first_seen||'')}" > $ { fmtRelTime ( e . first _seen ) } < / t d >
< td class = "ts-cell" title = "${escHtml(e.last_seen||'')}" > $ { fmtRelTime ( e . last _seen ) } < / t d >
2026-03-01 23:03:18 -05:00
< td > $ { e . consecutive _failures } < / t d >
< td > $ { ticket } < / t d >
< td >
< button class = "btn-sm btn-suppress"
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
data - sup - type = "${escHtml(supType)}"
data - sup - name = "${escHtml(e.target_name)}"
data - sup - detail = "${escHtml(e.target_detail||'')}" > 🔕 < / b u t t o n >
2026-03-01 23:03:18 -05:00
< / t d >
< / t r > ` ;
} ) . join ( '' ) ;
wrap . innerHTML = `
2026-03-17 20:32:32 -04:00
$ { countNotice }
feat: terminal aesthetic rewrite + link debug page
- Full dark terminal aesthetic (Pulse/TinkerTickets style):
- #0a0a0a background, #00ff41 green, #ffb000 amber, #00ffff cyan
- CRT scanline overlay, phosphor glow, ASCII corner pseudoelements
- Bracket-notation badges [CRITICAL], monospace font throughout
- style.css, base.html, index.html, suppressions.html all rewritten
- New Link Debug page (/links, /api/links):
- Per-host, per-interface cards with speed/duplex/port type/auto-neg
- Traffic bars (TX cyan, RX green) with rate labels
- Error/drop counters, carrier change history
- SFP/DOM optical panel: vendor, temp, voltage, bias, TX/RX power dBm bars
- RX-TX delta shown; color-coded warn/crit thresholds
- Auto-refresh every 60s, anchor-jump to #hostname
- LinkStatsCollector in monitor.py:
- SSHes to each host (one connection, all ifaces batched)
- Parses ethtool + ethtool -m (SFP DOM) output
- Merges with Prometheus traffic/error/carrier metrics
- Stores as link_stats in monitor_state table
- config.json: added ssh section for ethtool collection
- app.js: terminal chip style consistency (uppercase, ● bullet)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 12:43:11 -05:00
< div class = "table-wrap" >
< table class = "data-table" id = "events-table" >
< thead >
< tr >
< th > Sev < / t h > < t h > T y p e < / t h > < t h > T a r g e t < / t h > < t h > D e t a i l < / t h >
2026-03-14 21:46:11 -04:00
< th > Description < / t h > < t h > F i r s t S e e n < / t h > < t h > L a s t S e e n < / t h > < t h > F a i l u r e s < / t h > < t h > T i c k e t < / t h > < t h > A c t i o n s < / t h >
feat: terminal aesthetic rewrite + link debug page
- Full dark terminal aesthetic (Pulse/TinkerTickets style):
- #0a0a0a background, #00ff41 green, #ffb000 amber, #00ffff cyan
- CRT scanline overlay, phosphor glow, ASCII corner pseudoelements
- Bracket-notation badges [CRITICAL], monospace font throughout
- style.css, base.html, index.html, suppressions.html all rewritten
- New Link Debug page (/links, /api/links):
- Per-host, per-interface cards with speed/duplex/port type/auto-neg
- Traffic bars (TX cyan, RX green) with rate labels
- Error/drop counters, carrier change history
- SFP/DOM optical panel: vendor, temp, voltage, bias, TX/RX power dBm bars
- RX-TX delta shown; color-coded warn/crit thresholds
- Auto-refresh every 60s, anchor-jump to #hostname
- LinkStatsCollector in monitor.py:
- SSHes to each host (one connection, all ifaces batched)
- Parses ethtool + ethtool -m (SFP DOM) output
- Merges with Prometheus traffic/error/carrier metrics
- Stores as link_stats in monitor_state table
- config.json: added ssh section for ethtool collection
- app.js: terminal chip style consistency (uppercase, ● bullet)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 12:43:11 -05:00
< / t r >
< / t h e a d >
< tbody > $ { rows } < / t b o d y >
< / t a b l e >
< / d i v > ` ;
2025-02-07 20:55:38 -05:00
}
2026-03-01 23:03:18 -05:00
// ── Suppression modal (dashboard) ────────────────────────────────────
function openSuppressModal ( type , name , detail ) {
const modal = document . getElementById ( 'suppress-modal' ) ;
if ( ! modal ) return ;
document . getElementById ( 'sup-type' ) . value = type ;
document . getElementById ( 'sup-name' ) . value = name ;
document . getElementById ( 'sup-detail' ) . value = detail ;
document . getElementById ( 'sup-reason' ) . value = '' ;
document . getElementById ( 'sup-expires' ) . value = '' ;
updateSuppressForm ( ) ;
modal . style . display = 'flex' ;
document . querySelectorAll ( '#suppress-modal .pill' ) . forEach ( p => p . classList . remove ( 'active' ) ) ;
const manualPill = document . querySelector ( '#suppress-modal .pill-manual' ) ;
if ( manualPill ) manualPill . classList . add ( 'active' ) ;
const hint = document . getElementById ( 'duration-hint' ) ;
if ( hint ) hint . textContent = 'Suppression will persist until manually removed.' ;
}
function closeSuppressModal ( ) {
const modal = document . getElementById ( 'suppress-modal' ) ;
if ( modal ) modal . style . display = 'none' ;
}
function updateSuppressForm ( ) {
const type = document . getElementById ( 'sup-type' ) . value ;
const nameGrp = document . getElementById ( 'sup-name-group' ) ;
const detailGrp = document . getElementById ( 'sup-detail-group' ) ;
if ( nameGrp ) nameGrp . style . display = ( type === 'all' ) ? 'none' : '' ;
if ( detailGrp ) detailGrp . style . display = ( type === 'interface' ) ? '' : 'none' ;
}
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
function setDuration ( mins , el ) {
2026-03-01 23:03:18 -05:00
document . getElementById ( 'sup-expires' ) . value = mins || '' ;
document . querySelectorAll ( '#suppress-modal .pill' ) . forEach ( p => p . classList . remove ( 'active' ) ) ;
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
if ( el ) el . classList . add ( 'active' ) ;
2026-03-01 23:03:18 -05:00
const hint = document . getElementById ( 'duration-hint' ) ;
if ( hint ) {
if ( mins ) {
const h = Math . floor ( mins / 60 ) , m = mins % 60 ;
hint . textContent = ` Expires in ${ h ? h + 'h ' : '' } ${ m ? m + 'm' : '' } . ` ;
} else {
hint . textContent = 'Suppression will persist until manually removed.' ;
}
}
2025-02-07 20:55:38 -05:00
}
2026-03-01 23:03:18 -05:00
async function submitSuppress ( e ) {
e . preventDefault ( ) ;
const type = document . getElementById ( 'sup-type' ) . value ;
const name = document . getElementById ( 'sup-name' ) . value ;
const detail = document . getElementById ( 'sup-detail' ) . value ;
const reason = document . getElementById ( 'sup-reason' ) . value ;
const expires = document . getElementById ( 'sup-expires' ) . value ;
if ( ! reason . trim ( ) ) { showToast ( 'Reason is required' , 'error' ) ; return ; }
if ( type !== 'all' && ! name . trim ( ) ) { showToast ( 'Target name is required' , 'error' ) ; return ; }
2025-02-07 20:55:38 -05:00
2026-03-01 23:03:18 -05:00
try {
const resp = await fetch ( '/api/suppressions' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON . stringify ( {
target _type : type ,
target _name : name ,
target _detail : detail ,
reason : reason ,
expires _minutes : expires ? parseInt ( expires ) : null ,
} ) ,
} ) ;
const data = await resp . json ( ) ;
if ( data . success ) {
closeSuppressModal ( ) ;
showToast ( 'Suppression applied ✔' , 'success' ) ;
setTimeout ( refreshAll , 500 ) ;
} else {
showToast ( data . error || 'Failed to apply suppression' , 'error' ) ;
}
} catch ( err ) {
showToast ( 'Network error' , 'error' ) ;
}
2025-02-07 21:03:31 -05:00
}
2025-02-07 20:55:38 -05:00
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
// ── Global click handler: modal backdrop + suppress button delegation ─
2026-03-01 23:03:18 -05:00
document . addEventListener ( 'click' , e => {
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
// Close modal when clicking backdrop
2026-03-01 23:03:18 -05:00
const modal = document . getElementById ( 'suppress-modal' ) ;
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
if ( modal && e . target === modal ) { closeSuppressModal ( ) ; return ; }
// Suppress button via data attributes (avoids inline onclick XSS)
const btn = e . target . closest ( '.btn-suppress[data-sup-type]' ) ;
if ( btn ) {
openSuppressModal (
btn . dataset . supType || '' ,
btn . dataset . supName || '' ,
btn . dataset . supDetail || '' ,
) ;
}
2026-03-01 23:03:18 -05:00
} ) ;
2026-03-14 21:46:11 -04:00
// ── Relative time ─────────────────────────────────────────────────────
function fmtRelTime ( tsStr ) {
if ( ! tsStr ) return '– ' ;
const d = new Date ( tsStr . replace ( ' UTC' , 'Z' ) . replace ( ' ' , 'T' ) ) ;
if ( isNaN ( d ) ) return tsStr ;
const secs = Math . floor ( ( Date . now ( ) - d ) / 1000 ) ;
if ( secs < 60 ) return ` ${ secs } s ago ` ;
if ( secs < 3600 ) return ` ${ Math . floor ( secs / 60 ) } m ago ` ;
if ( secs < 86400 ) return ` ${ Math . floor ( secs / 3600 ) } h ago ` ;
return ` ${ Math . floor ( secs / 86400 ) } d ago ` ;
}
2026-03-01 23:03:18 -05:00
// ── Utility ───────────────────────────────────────────────────────────
function escHtml ( str ) {
if ( str === null || str === undefined ) return '' ;
return String ( str )
. replace ( /&/g , '&' )
. replace ( /</g , '<' )
. replace ( />/g , '>' )
. replace ( /"/g , '"' ) ;
}