2026-11-04 23:40:17 -05:00
'use strict' ;
2026-11-05 10:11:11 -05:00
/* global DEVICE_NAME, deviceUrl, refreshModem, _toIso, fmtRate, fmtErrors, renderPortInspector, selectPortByIdx, highlightSelectedPort */
2026-11-04 23:40:17 -05:00
2026-11-05 10:11:11 -05:00
// Per-device page: overview, alerts, and a compact interface/port table. Host
// rows expand in place; switch and router rows open the port inspector panel.
2026-11-04 23:40:17 -05:00
const esc = s => lt . escHtml ( String ( s === null || s === undefined ? '' : s ));
const _openRows = new Set (); // survives auto-refresh
let _filter = 'all' ;
let _query = '' ;
let _device = null ;
// ── Formatting ───────────────────────────────────────────────────────
function fmtSpeed ( mbps ) {
if ( ! mbps ) return '– ' ;
return mbps >= 1000 ? ( mbps / 1000 ). toFixed ( 0 ) + ' Gbps' : mbps + ' Mbps' ;
}
function fmtUnit ( value , unit , digits ) {
return value === null || value === undefined ? '– ' : ` ${ value . toFixed ( digits ) } ${ unit } ` ;
}
function fmtUptime ( seconds ) {
if ( seconds === null || seconds === undefined ) return '– ' ;
const d = Math . floor ( seconds / 86400 ), h = Math . floor (( seconds % 86400 ) / 3600 );
const m = Math . floor (( seconds % 3600 ) / 60 );
return d ? ` ${ d } d ${ h } h` : ` ${ h } h ${ m } m` ;
}
function ratePct ( bytesPerSec , mbps ) {
if ( ! mbps || ! bytesPerSec ) return 0 ;
return Math . min ( 100 , ( bytesPerSec * 8 / 1e6 / mbps ) * 100 );
}
function barClass ( pct , isTx ) {
if ( pct > 85 ) return 'lt-progress--red' ;
if ( pct > 65 ) return 'lt-progress--amber' ;
return isTx ? '' : 'lt-progress--cyan' ;
}
// ── SFP/DOM value classification ─────────────────────────────────────
function rxClass ( dbm ) {
if ( dbm === null || dbm === undefined ) return 'val-neutral' ;
if ( dbm < - 15 ) return 'val-crit' ;
return dbm < - 10 ? 'val-warn' : 'val-good' ;
}
function txClass ( dbm ) {
if ( dbm === null || dbm === undefined ) return 'val-neutral' ;
return dbm < - 5 ? 'val-crit' : 'val-good' ;
}
function tempClass ( c ) {
if ( c === null || c === undefined ) return 'val-neutral' ;
if ( c > 80 ) return 'val-crit' ;
return c > 70 ? 'val-warn' : 'val-good' ;
}
function voltClass ( v ) {
if ( v === null || v === undefined ) return 'val-neutral' ;
if ( v < 3.0 || v > 3.6 ) return 'val-crit' ;
return ( v < 3.1 || v > 3.5 ) ? 'val-warn' : 'val-good' ;
}
function powerFill ( cls ) {
return cls === 'val-good' ? 'power-ok' : cls === 'val-warn' ? 'power-warn' : 'power-crit' ;
}
function badges ( d ) {
const out = [];
if (( d . tx_errs_rate || 0 ) > 0.001 || ( d . rx_errs_rate || 0 ) > 0.001 ) out . push ( '<span class="link-alert-badge">ERR</span>' );
if (( d . tx_drops_rate || 0 ) > 0.001 || ( d . rx_drops_rate || 0 ) > 0.001 ) out . push ( '<span class="link-alert-badge link-alert-amber">DROP</span>' );
if (( d . carrier_changes || 0 ) > 3 ) out . push ( '<span class="link-alert-badge link-alert-amber">FLAP</span>' );
return out . join ( '' );
}
function stat ( label , valueHtml , tooltip ) {
const tip = tooltip ? ` data-tooltip=" ${ esc ( tooltip ) } "` : '' ;
return `<div class="link-stat"><span class="link-stat-label" ${ tip } > ${ esc ( label ) } </span>` +
`<span class="link-stat-value"> ${ valueHtml } </span></div>` ;
}
function trafficBars ( d ) {
const tx = ratePct ( d . tx_bytes_rate , d . speed_mbps ), rx = ratePct ( d . rx_bytes_rate , d . speed_mbps );
const row = ( label , pct , rate , isTx ) => `
<div class="traffic-row">
<span class="traffic-label"> ${ label } </span>
<div class="lt-progress ${ barClass ( pct , isTx ) } "><div class="lt-progress-bar" style="width: ${ pct } %"></div></div>
<span class="traffic-value"> ${ fmtRate ( rate ) } </span>
</div>` ;
return `<div class="traffic-section"> ${ row ( 'TX' , tx , d . tx_bytes_rate , true ) }${ row ( 'RX' , rx , d . rx_bytes_rate , false ) } </div>` ;
}
function sfpPanel ( s ) {
if ( ! s || ! Object . keys ( s ). length ) return '' ;
const rx = rxClass ( s . rx_power_dbm ), tx = txClass ( s . tx_power_dbm );
const has = v => v !== null && v !== undefined ;
const rxPct = has ( s . rx_power_dbm ) ? Math . min ( 100 , Math . max ( 0 , ( s . rx_power_dbm + 20 ) / 15 * 100 )) : 0 ;
const txPct = has ( s . tx_power_dbm ) ? Math . min ( 100 , Math . max ( 0 , ( s . tx_power_dbm + 10 ) / 8 * 100 )) : 0 ;
const item = ( label , cls , value , tip , pct ) => `
<div class="sfp-stat">
<span class="sfp-stat-label" data-tooltip=" ${ esc ( tip ) } "> ${ label } </span>
<span class="sfp-stat-value ${ cls } "> ${ value } </span>
${ pct === undefined ? '' : `<div class="power-row"><div class="power-track"><div class="power-fill ${ powerFill ( cls ) } " style="width: ${ pct } %"></div></div></div>` }
</div>` ;
const vendor = [ s . vendor , s . part_no , s . sfp_type , s . connector , s . wavelength_nm && ` ${ s . wavelength_nm } nm` ]
. filter ( Boolean ). map ( esc ). join ( ' / ' );
return `
<div class="sfp-panel">
<div class="sfp-vendor-row"> ${ vendor } </div>
<div class="sfp-grid">
${ item ( 'Temp' , tempClass ( s . temp_c ), fmtUnit ( s . temp_c , '°C' , 1 ), 'Module temperature. Normal: below 70°C.' ) }
${ item ( 'Voltage' , voltClass ( s . voltage_v ), fmtUnit ( s . voltage_v , 'V' , 2 ), 'Supply voltage. Normal: 3.1– 3.5 V.' ) }
${ item ( 'Bias' , '' , fmtUnit ( s . bias_ma , 'mA' , 2 ), 'Laser bias current. High values may mean an aging laser.' ) }
${ item ( 'TX Power' , tx , fmtUnit ( s . tx_power_dbm , 'dBm' , 2 ), 'Optical transmit power.' , txPct ) }
${ item ( 'RX Power' , rx , fmtUnit ( s . rx_power_dbm , 'dBm' , 2 ), 'Optical receive power. Very low values may mean dirty or damaged fiber.' , rxPct ) }
${ has ( s . rx_power_dbm ) && has ( s . tx_power_dbm ) ? item ( 'RX− TX Δ' , '' , ` ${ ( s . rx_power_dbm - s . tx_power_dbm ). toFixed ( 2 ) } dBm` , 'Difference between receive and transmit power.' ) : '' }
</div>
</div>` ;
}
// ── Normalized rows, so hosts and switches share one table ───────────
function hostRows ( interfaces ) {
return Object . entries ( interfaces )
. sort (([ a ], [ b ]) => a . localeCompare ( b , undefined , { numeric : true }))
. map (([ name , d ]) => {
const up = d . state ? d . state === 'up' : d . link_detected !== false ;
return {
id : name , label : name , up , data : d , sub : d . port_type || '' ,
detail : `
<div class="link-stats-grid">
${ stat ( 'Duplex' , esc ( d . duplex || '– ' ), 'Full = both directions at once. Half can cause collisions.' ) }
${ stat ( 'Auto-neg' , d . auto_neg === null || d . auto_neg === undefined ? '– ' : d . auto_neg ? 'On' : 'Off' ) }
${ stat ( 'Carrier Δ' , d . carrier_changes === null || d . carrier_changes === undefined ? '– ' : esc ( d . carrier_changes ), 'Times the link went up or down. High values mean flapping.' ) }
${ stat ( 'TX Err/s' , fmtErrors ( d . tx_errs_rate )) }
${ stat ( 'RX Err/s' , fmtErrors ( d . rx_errs_rate )) }
${ stat ( 'TX Drop/s' , fmtErrors ( d . tx_drops_rate )) }
${ stat ( 'RX Drop/s' , fmtErrors ( d . rx_drops_rate )) }
</div>
${ trafficBars ( d ) }
${ sfpPanel ( d . sfp ) } ` ,
};
});
}
function portRows ( ports ) {
return Object . entries ( ports )
. sort (([, a ], [, b ]) => ( a . port_idx || 0 ) - ( b . port_idx || 0 ))
. map (([ name , d ]) => {
return {
id : name , label : name , num : d . port_idx , up : !! d . up , data : d ,
2026-11-05 10:11:11 -05:00
neighbor : d . lldp ? { name : d . lldp . system_name , port : d . lldp . port_id , chassis : d . lldp . chassis_id } : null ,
2026-11-04 23:40:17 -05:00
sub : [ d . is_uplink ? 'uplink' : '' , d . media || '' , d . poe_power ? `PoE ${ d . poe_power . toFixed ( 1 ) } W` : '' ]. filter ( Boolean ). join ( ' · ' ),
};
});
}
function rowHasProblem ( r ) {
return !! badges ( r . data );
}
function renderTable ( rows ) {
const q = _query . trim (). toLowerCase ();
const shown = rows . filter ( r => {
if ( _filter === 'up' && ! r . up ) return false ;
if ( _filter === 'problems' && ! rowHasProblem ( r )) return false ;
return ! q || ` ${ r . label } ${ r . num || '' } ${ r . sub } ${ ( r . neighbor && r . neighbor . name ) || '' } ` . toLowerCase (). includes ( q );
});
if ( ! shown . length ) return '<div class="lt-empty-state lt-empty-state--sm"><div class="lt-empty-state-title">Nothing matches this filter</div></div>' ;
const isPorts = rows . some ( r => r . num !== undefined );
const body = shown . map ( r => {
2026-11-05 10:11:11 -05:00
const open = ! isPorts && _openRows . has ( r . id );
2026-11-04 23:40:17 -05:00
const d = r . data ;
const neighbor = r . neighbor
2026-11-05 10:11:11 -05:00
? ( r . neighbor . name ? `<a class="device-link" href=" ${ esc ( deviceUrl ( r . neighbor . name )) } "> ${ esc ( r . neighbor . name ) } </a>` : esc ( r . neighbor . chassis || r . neighbor . port || '' ))
2026-11-04 23:40:17 -05:00
: '– ' ;
return `
2026-11-05 10:11:11 -05:00
<tr class="device-row ${ r . up ? '' : ' device-row--down' } " ${ isPorts ? ` data-port-idx=" ${ esc ( r . num ) } "` : '' } >
<td><button type="button" class="device-row-toggle" data-row=" ${ esc ( r . id ) } " ${ isPorts ? '' : ` aria-expanded=" ${ open } "` } aria-label="Details for ${ esc ( r . label ) } "> ${ open ? '▾' : '▸' } </button></td>
2026-11-04 23:40:17 -05:00
<td><span class="dot- ${ r . up ? 'up' : 'down' } "></span></td>
<td class="device-row-name"> ${ r . num !== undefined ? `<span class="port-badge port-badge-num"># ${ esc ( r . num ) } </span> ` : '' }${ esc ( r . label ) } <div class="device-row-sub"> ${ esc ( r . sub ) } </div></td>
<td class="device-num"> ${ r . up ? fmtSpeed ( d . speed_mbps ) : '– ' } </td>
<td class="device-num device-col-wide"> ${ fmtRate ( d . rx_bytes_rate ) } </td>
<td class="device-num device-col-wide"> ${ fmtRate ( d . tx_bytes_rate ) } </td>
${ isPorts ? `<td class="device-col-wide"> ${ neighbor } </td>` : '' }
<td> ${ badges ( d ) || ( r . up ? '<span class="val-good">OK</span>' : '<span class="device-row-sub">down</span>' ) } </td>
</tr>
2026-11-05 10:11:11 -05:00
${ open ? `<tr class="device-row-detail"><td colspan="7"> ${ r . detail } </td></tr>` : '' } ` ;
2026-11-04 23:40:17 -05:00
}). join ( '' );
return `
<div class="lt-table-wrap">
<table class="lt-table device-table">
<caption class="lt-sr-only">Interfaces</caption>
<thead><tr>
<th><span class="lt-sr-only">Expand</span></th><th><span class="lt-sr-only">State</span></th>
<th> ${ isPorts ? 'Port' : 'Interface' } </th><th>Speed</th><th class="device-col-wide">RX</th><th class="device-col-wide">TX</th>
${ isPorts ? '<th class="device-col-wide">Neighbor</th>' : '' } <th>Health</th>
</tr></thead>
<tbody> ${ body } </tbody>
</table>
</div>` ;
}
function section ( title , inner , id ) {
return `
<section class="g-section" ${ id ? ` id=" ${ id } "` : '' } >
<div class="g-section-header"><h2 class="g-section-title"> ${ esc ( title ) } </h2></div>
<div class="lt-frame">
<span class="lt-frame-bl">╚</span><span class="lt-frame-br">╝</span>
${ inner }
</div>
</section>` ;
}
function kvGrid ( pairs ) {
return `<div class="modem-stats"> ${ pairs . filter (([, v ]) => v !== null && v !== undefined && v !== '' ). map (([ k , v ]) => `
<div class="modem-stat"><span class="modem-stat-label"> ${ esc ( k ) } </span><span class="modem-stat-value"> ${ esc ( v ) } </span></div>` ). join ( '' ) } </div>` ;
}
function renderOverview ( d ) {
const info = d . info || {};
const pct = v => ( v === null || v === undefined ? null : ` ${ v . toFixed ( 0 ) } %` );
const temps = ( info . temps || []). filter ( t => t . value_c !== null ). map ( t => ` ${ t . name } ${ t . value_c . toFixed ( 0 ) } °C` ). join ( ', ' );
return section ( 'Overview' , kvGrid ([
[ 'Status' , ( d . status || 'unknown' ). toUpperCase ()],
[ 'Type' , d . kind ],
[ 'IP' , d . ip ],
[ 'MAC' , d . mac ],
[ 'Model' , d . model ],
[ 'Firmware' , info . firmware ],
[ 'Uptime' , info . uptime_s === undefined ? null : fmtUptime ( info . uptime_s )],
[ 'CPU' , pct ( info . cpu_pct )],
[ 'Memory' , pct ( info . mem_pct )],
[ 'Clients' , info . clients ],
[ 'Temperature' , temps ],
[ 'Power draw' , info . power_w === null || info . power_w === undefined ? null : ` ${ info . power_w . toFixed ( 0 ) } W` ],
[ 'Data source' , d . source ],
]));
}
function renderUplinks ( d ) {
if ( ! ( d . uplinks || []). length ) return '' ;
const rows = d . uplinks . map ( u => `
<tr>
<td><span class="dot- ${ u . up ? 'up' : 'down' } "></span></td>
<td> ${ esc ( u . host_interface || '– ' ) } </td>
<td><a class="device-link" href=" ${ esc ( deviceUrl ( u . switch )) } "> ${ esc ( u . switch ) } </a></td>
2026-11-05 10:11:11 -05:00
<td><a class="device-link" href=" ${ esc ( deviceUrl ( u . switch )) } #port- ${ esc ( u . port_idx ) } "># ${ esc ( u . port_idx ) } ${ esc ( u . port ) } </a></td>
2026-11-04 23:40:17 -05:00
<td> ${ fmtSpeed ( u . speed_mbps ) } </td>
</tr>` ). join ( '' );
return section ( 'Connected to' , `<div class="lt-table-wrap"><table class="lt-table">
<caption class="lt-sr-only">Switch ports this host connects to, from LLDP</caption>
<thead><tr><th><span class="lt-sr-only">State</span></th><th>Interface</th><th>Switch</th><th>Port</th><th>Speed</th></tr></thead>
<tbody> ${ rows } </tbody></table></div>` );
}
function renderWan ( d ) {
const wan = ( d . info && d . info . wan ) || {};
const names = Object . keys ( wan );
if ( ! names . length ) return '' ;
const rows = names . map ( n => {
const w = wan [ n ];
return `<tr><td><span class="dot- ${ w . up ? 'up' : 'down' } "></span></td><td> ${ esc ( n . toUpperCase ()) } </td>
<td> ${ fmtSpeed ( w . speed_mbps ) } </td><td> ${ w . latency_ms === null || w . latency_ms === undefined ? '– ' : ` ${ esc ( w . latency_ms ) } ms` } </td>
<td> ${ w . availability === null || w . availability === undefined ? '– ' : ` ${ esc ( w . availability ) } %` } </td></tr>` ;
}). join ( '' );
return section ( 'WAN' , `<div class="lt-table-wrap"><table class="lt-table">
<caption class="lt-sr-only">WAN connections</caption>
<thead><tr><th><span class="lt-sr-only">State</span></th><th>Connection</th><th>Speed</th><th>Latency</th><th>Availability</th></tr></thead>
<tbody> ${ rows } </tbody></table></div>` );
}
function renderOutlets ( d ) {
const outlets = ( d . info && d . info . outlets ) || [];
if ( ! outlets . length ) return '' ;
const rows = outlets . map ( o => `
<tr>
<td><span class="dot- ${ o . on ? 'up' : 'down' } "></span></td>
<td># ${ esc ( o . index ) } </td>
<td> ${ esc ( o . name || '– ' ) } </td>
<td class="device-num"> ${ o . power_w === null || o . power_w === undefined ? '– ' : ` ${ o . power_w . toFixed ( 1 ) } W` } </td>
<td> ${ o . auto_power_cycle ? '<span class="lt-badge">auto power-cycle</span>' : '' } </td>
</tr>` ). join ( '' );
return section ( 'Outlets' , `<div class="lt-table-wrap"><table class="lt-table">
<caption class="lt-sr-only">PDU outlets</caption>
<thead><tr><th><span class="lt-sr-only">State</span></th><th>Outlet</th><th>Name</th><th>Power</th><th></th></tr></thead>
<tbody> ${ rows } </tbody></table></div>` );
}
function renderEvents ( events ) {
const body = ! ( events || []). length
? '<div class="lt-empty-state lt-empty-state--sm"><div class="lt-empty-state-icon">✔</div><div class="lt-empty-state-title">No recorded alerts</div></div>'
: `<div class="lt-table-wrap"><table class="lt-table">
<caption class="lt-sr-only">Alerts for this device</caption>
<thead><tr><th>Sev</th><th>Type</th><th>Detail</th><th>Description</th><th>First seen</th><th>State</th></tr></thead>
<tbody> ${ events . map ( e => `
<tr class=" ${ e . resolved_at ? '' : `row- ${ esc ( e . severity ) } ` } ">
<td><span class="lt-badge badge- ${ e . resolved_at ? 'resolved' : esc ( e . severity ) } "> ${ esc ( e . severity ) } </span> ${ e . is_suppressed && ! e . resolved_at ? '<span class="lt-badge badge-suppressed">🔕 sup</span>' : '' } </td>
<td> ${ esc ( e . event_type . replace ( /_/g , ' ' )) } </td>
<td> ${ esc ( e . target_detail || '– ' ) } </td>
<td class="desc-cell"> ${ esc ( e . description || '' ) } </td>
<td class="ts-cell" title=" ${ esc ( e . first_seen || '' ) } "> ${ esc ( lt . time . ago ( _toIso ( e . first_seen ))) } </td>
<td> ${ e . resolved_at ? `resolved ${ esc ( lt . time . ago ( _toIso ( e . resolved_at ))) } ` : '<strong>active</strong>' } </td>
</tr>` ). join ( '' ) } </tbody></table></div>` ;
return section ( 'Alerts' , body );
}
function currentRows () {
if ( ! _device ) return [];
if ( _device . ports ) return portRows ( _device . ports );
if ( _device . interfaces ) return hostRows ( _device . interfaces );
return [];
}
function renderInterfaces () {
const wrap = document . getElementById ( 'device-table-wrap' );
2026-11-05 10:11:11 -05:00
if ( ! wrap ) return ;
wrap . innerHTML = renderTable ( currentRows ());
highlightSelectedPort ();
2026-11-04 23:40:17 -05:00
}
2026-11-05 10:11:11 -05:00
function updateInterfaceSection ( rows , title ) {
const section = document . getElementById ( 'device-ports' );
section . hidden = ! rows . length ;
if ( ! rows . length ) return ;
document . getElementById ( 'device-ports-title' ). textContent =
` ${ title } · ${ rows . filter ( r => r . up ). length } of ${ rows . length } up` ;
document . getElementById ( 'device-table-search' ). setAttribute ( 'aria-label' , `Filter ${ title . toLowerCase () } ` );
document . getElementById ( 'device-error-count' ). textContent = rows . filter ( rowHasProblem ). length ;
2026-11-04 23:40:17 -05:00
}
function render ( d ) {
_device = d ;
document . getElementById ( 'device-dot' ). className = `dot- ${ d . status || 'unknown' } ` ;
document . getElementById ( 'device-subtitle' ). textContent =
[ d . kind , d . ip , d . model ]. filter ( Boolean ). join ( ' · ' ) +
( d . updated ? ` · updated ${ new Date ( _toIso ( d . updated )). toLocaleTimeString () } ` : '' );
const unifi = d . source === 'unifi' ;
document . getElementById ( 'device-actions' ). innerHTML = `
<button class="lt-btn lt-btn-ghost lt-btn-sm btn-suppress" data-sup-type=" ${ unifi ? 'unifi_device' : 'host' } "
data-sup-name=" ${ esc ( d . name ) } " data-sup-detail="" aria-label="Suppress alerts for ${ esc ( d . name ) } ">🔕 Suppress</button>` ;
document . getElementById ( 'device-root' ). innerHTML =
renderOverview ( d ) +
renderEvents ( d . events ) +
renderWan ( d ) +
renderUplinks ( d ) +
2026-11-05 10:11:11 -05:00
renderOutlets ( d );
const hasPorts = !! ( d . ports && Object . keys ( d . ports ). length );
document . getElementById ( 'device-inspector' ). classList . toggle ( 'has-chassis' , hasPorts );
updateInterfaceSection ( currentRows (), d . ports ? 'Ports' : 'Interfaces' );
if ( hasPorts ) renderPortInspector ( d );
2026-11-04 23:40:17 -05:00
renderInterfaces ();
if ( d . kind === 'modem' ) refreshModem ();
}
async function loadDevice () {
try {
render ( await lt . api . get ( `/api/device/ ${ encodeURIComponent ( DEVICE_NAME ) } ` ));
} catch ( err ) {
if ( _device ) {
lt . toast . error ( 'Could not refresh device data' );
return ;
}
const notFound = /404|No device/ . test ( err . message || '' );
document . getElementById ( 'device-root' ). innerHTML = `
<div class="lt-empty-state">
<div class="lt-empty-state-icon">✕</div>
<div class="lt-empty-state-title"> ${ notFound ? 'No device with this name' : 'Could not load this device' } </div>
<div class="lt-empty-state-body"> ${ notFound ? 'It may have been renamed, or the monitor may not have seen it yet.' : 'Check that the Gandalf service is healthy, then refresh.' } </div>
</div>` ;
}
}
document . addEventListener ( 'click' , e => {
2026-11-05 10:11:11 -05:00
// Switch and router rows open the shared port panel, except when following a link
const portRow = e . target . closest ( 'tr.device-row[data-port-idx]' );
if ( portRow && ! e . target . closest ( 'a' )) {
selectPortByIdx ( _device . name , parseInt ( portRow . dataset . portIdx , 10 ));
return ;
}
2026-11-04 23:40:17 -05:00
const toggle = e . target . closest ( '.device-row-toggle' );
if ( toggle ) {
const id = toggle . dataset . row ;
if ( _openRows . has ( id )) _openRows . delete ( id ); else _openRows . add ( id );
renderInterfaces ();
return ;
}
const pill = e . target . closest ( '[data-device-filter]' );
if ( pill ) {
_filter = pill . dataset . deviceFilter ;
document . querySelectorAll ( '[data-device-filter]' ). forEach ( p => {
const on = p === pill ;
p . classList . toggle ( 'active' , on );
p . setAttribute ( 'aria-pressed' , String ( on ));
});
renderInterfaces ();
}
});
document . addEventListener ( 'input' , e => {
if ( e . target . id === 'device-table-search' ) {
_query = e . target . value ;
renderInterfaces ();
}
});
loadDevice ();
const _deviceInterval = window . gandalfSettings ? . refreshInterval ?? 60 ;
if ( _deviceInterval > 0 ) lt . autoRefresh . start ( loadDevice , Math . max ( _deviceInterval , 15 ) * 1000 );
window . onGandalfSettingsChanged = function ( s ) {
lt . autoRefresh . stop ();
if ( s . refreshInterval > 0 ) lt . autoRefresh . start ( loadDevice , Math . max ( s . refreshInterval , 15 ) * 1000 );
};