Replace the port blocks on device pages with SVG faceplates traced from
reference images: Dream Machine Pro, USW Pro 24 PoE, USW Aggregation,
USW Flex, USW Flex Mini, USP PDU Pro and UPS 2U Pro. Port LEDs use each
model's documented colors, and ports show a cable or module when up.
Remove SWITCH_LAYOUTS, which had wrong data: the aggregation switch was
numbered across the rows instead of odd over even, and the USW Flex had
an SFP cage it doesn't have. Models without a faceplate use plain blocks.
Show PDU and UPS outlets with on/off state and auto power-cycle, and let
the UPS page show its panel even though it reports no ports.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replace the all-switches Inspector page with a per-device front panel,
port table and detail panel. /inspector now redirects to the dashboard,
and /device/<name>#port-<n> links directly to a port.
Restore neighbor names after UniFi's LLDP change: read local_port_idx
and resolve neighbors by MAC using Prometheus NIC data, UniFi devices
and the wired client table. This also brings back Run Diagnostics on
server ports, which depends on a named neighbor.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Modem and WAN:
- Poll the BGW320 status pages read-only for light levels, PON and WAN
state, with a WAN / Fiber panel, 7-day Rx trend, and alerts on WAN down,
optical limits, and Rx drift
- Read WAN outages and Smart Power restarts from the UniFi system log,
since outages can be over between 5 minute polls
Device pages:
- Add device search to the dashboard and a page per host, switch, router,
AP, PDU, or modem
- Move all Link Debug data into expandable rows there; /links redirects
- Collect UDM ports and UniFi device details (firmware, load, WAN, outlets)
- Reorder the dashboard
Monitoring fixes:
- Alert when a configured host stops reporting: its series disappear
rather than reporting down, so a dead host never raised an alert
- Alert on Prometheus outages instead of blanking the dashboard
- Treat a failed Pulse ping as unknown rather than host unreachable
- Stop new switch ports reporting lifetime bytes as one interval's rate
- Run database sessions in UTC: other server time zones shifted displayed
ages and timed suppression expiries
Also log every avatar 404 reason, add a favicon, and correct the WAN link
labels (1G on Port 9, not 10G SFP+).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Using || 30 / || 60 as a fallback treats refreshInterval=0 (Off) as
falsy and replaces it with the default, causing auto-refresh to start
even when the user saved 'Off'. Replace with nullish coalescing (??)
so only null/undefined triggers the default.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Global suppressions (target_type='all') have an empty target_name, so
the selectattr filter never matched them, leaving no visual indicator
when a global maintenance window was active. Pre-compute has_global_sup
before the host loop and OR it into the badge condition.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
monitor.py: _ticket_interface/_ticket_unifi/_ticket_unreachable all used
`if tid and is_new` to guard db.set_ticket_id(). Since is_new is True only
on the first upsert (consec=1) but tickets are created at consec>=fail_thresh
(default 2), is_new is always False when the ticket is created, so the
ticket link never appeared in the UI. Changed to `if tid:`.
links.html: JSON.parse(sessionStorage.getItem(...)) in togglePanel and
restoreCollapseState had no try-catch. Corrupt/stale session storage would
throw an uncaught SyntaxError. Also wrapped all sessionStorage.setItem
calls in try-catch to defend against storage-full / private-browsing errors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously switching ports while a diagnostic was running left the
setInterval timer active, causing the result to be written into the
old (now detached) DOM elements and never shown to the user.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The check `!data.hosts && !data.unifi_switches` never caught empty
objects `{}`, which are truthy. Replace with Object.keys length checks
so the friendly "no data yet" banner renders when both collections
are empty.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The reason input had `required` for browser validation but was missing
`aria-required="true"`, so screen readers did not announce it as required.
Matches the fix already applied to the equivalent field in base.html.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Same bug as was just fixed in links.html: data.updated is stored as
"YYYY-MM-DD HH:MM:SS UTC" by monitor.py, so appending 'Z' produced
"…UTCZ" — an invalid date. The stale-data warning and Updated timestamp
in Inspector were silently showing "Invalid Date" and the stale overlay
never fired. Fixed to use _toIso() (already global via app.js).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Security: add require_admin decorator; apply to POST/DELETE /api/suppressions
and /suppressions page. Previously any user in allowed_groups could create or
delete suppressions even though the nav restricts the UI to admins.
Bug: links.html "Updated:" timestamp and stale-warning both produced
Invalid Date because the raw "YYYY-MM-DD HH:MM:SS UTC" string was appended
with 'Z' instead of being normalised through _toIso(). Fix both call sites to
use _toIso(), and remove the now-redundant local _toIso redefinition.
Style: use `with open(sentinel, 'w'): pass` consistently (was open().close()
at avatar JPEG validation path).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- updateSuppressForm() now sets required + aria-required on sup-name/sup-detail
when target type changes; sup-reason gets static aria-required="true"
- onTypeChange() in suppressions page syncs aria-required on s-name
- s-name in suppressions.html gets initial required/aria-required (default type=host)
- Duration pills in both modal and suppressions page now have descriptive
aria-label ("30 minutes", "1 hour", etc.) alongside the group aria-label
- setDuration() in app.js accepts optional {expiresId,pillSel,hintId} opts so
logic lives in one place; suppressions.html setDur() delegates to it
- Post-create form reset uses setDur() instead of manually patching DOM
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- inspector.html: show orange '⚠ Stale: HH:MM' with tooltip when link_stats data is >15 min old (previously just showed the time with no visual warning)
- style.css: add .g-stale-warn helper class (orange, bold) for the stale indicator
- diagnose.py: remove supported_modes accumulation from parse_ethtool() — field was collected but never consumed by analyze() or displayed anywhere
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- inspector.html: collapsible section hint text now toggles between [expand]/[collapse] when clicked
- inspector.html: timeout and connection-loss during diagnostic poll now show a Retry button instead of a dead end
- inspector.html: 429 rate-limit response shows a clear human-readable message instead of generic error
- app.py: empty link_stats fallback now includes unifi_switches:{} for schema consistency with real data shape
- index.html: pagination overflow notice now says "export all as JSON" (opens in new tab) instead of misleadingly linking to raw API as navigation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- inspector.html: fix LLDP neighbor label in port blocks — port.lldp_table never exists; data is at port.lldp (dict with system_name/chassis_id); both port block renderers corrected
- db.py: remove dead 'target_detail IS NULL' branch in suppression check — target_detail is always stored as '' not NULL; query simplified to target_detail=''
- app.py: resolve cache_dir/cache_file/sentinel to absolute paths; guard against path escape before use
- app.py: wrap sentinel os.path.getmtime() in try/except OSError to handle TOCTOU deletion race
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- app.py: wrap int(cache_ttl) in try/except so a misconfigured non-integer value falls back to 3600 instead of raising ValueError
- base.html: use Jinja2 tojson filter for ticket_web_url to ensure proper JS string escaping regardless of URL contents
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- app.py: validate server_name from LLDP with fullmatch before use in logs/lookups (prevents log injection)
- app.py: validate each mgmt_ip candidate before assigning host_ip (avoids assigning non-IP string that then fails later check)
- app.py: log actual exception in link_stats JSON parse error
- inspector.html: clear _diagPollTimer in closePanel() so timer doesn't orphan when panel is closed mid-poll
- monitor.py: sleep 30s after a monitor loop exception before resuming normal poll interval
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
security:
- Fix bare open(sentinel, 'w').close() file descriptor leak; use
context manager instead
- Store requesting username in _diag_jobs at creation time; return 403
from api_diagnose_poll if the polling user does not match the job owner
accessibility:
- Add aria-live="polite" aria-atomic="true" to .status-chips container
so screen readers announce critical/warning count changes on refresh
- Add aria-controls="events-table-wrap" to critical and warning stat
cards so assistive tech knows these buttons control the events table
- Add aria-hidden sync to topology setCollapsed() — hidden topology
content is now removed from the accessibility tree when collapsed,
preventing keyboard focus from entering invisible elements
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The command palette button used an inline onclick handler while every
other interactive element in base.html uses data-action + event
delegation. Now consistent: data-action="open-cmdpalette" handled in
the global footer click listener.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add role="button" tabindex="0" aria-expanded to .link-host-title
in both static and JS-rendered panels (host panels + UniFi switches)
- Sync aria-expanded in togglePanel(), restoreCollapseState(),
collapseAll(), and expandAll()
- Add keydown handler (Enter/Space) so panel headers are keyboard-operable
- Add role="region" aria-label to inspector main chassis area
- Add role="complementary" aria-label to inspector port detail panel
- Replace last inline date-parse in renderLinks() with _toIso() helper
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace all var declarations in base.html, index.html scripts with
const/let (const for bindings that are never reassigned, let otherwise)
- Add _toIso() helper to links.html script block and replace the two
inline .replace(' UTC','Z').replace(' ','T') patterns with it
- Replace var with const in links.html _linksInterval
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add role="group" + aria-label to duration-pills and sev-pills containers
- Add aria-pressed to severity filter, duration, and refresh-interval pills
- Keep aria-pressed in sync with JS (setDuration, applyRefreshPillUI, modal reset)
- Add aria-label to events-search, host-search, links-search inputs
- Add aria-label to host and UniFi device suppress buttons in templates
- Replace dynamic style color strings in links.html stat cards with TDS
utility classes (lt-text-red/green/amber) via downCls/errCls variables
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace .empty-state (removed class) with TDS lt-empty-state--sm in
both error branches of renderInspector() and loadInspector()
- Diagnostic run button: add aria-label, apply lt-btn TDS classes for
consistent styling instead of custom btn-diag-only styling
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- lt-divider--unifi / lt-divider-label--unifi: replace hardcoded margin
and cyan label color on the UniFi switch section divider
- lt-text-amber / lt-text-cyan on stat card icons and values (matches
same migration done in index.html)
- lt-stats-grid--mb: margin-bottom:16px on the summary stats grid
- g-page-sub-aside: replaces margin-left:8px on the updated timestamp
span in links and inspector page subtitle
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- lt-notif-empty: replaces all hardcoded padding/font/color/align on
the empty-state and loading/error text in the notification bell panel
- lt-notif-view-all: replaces width/text-align/display/font-size inline
style on the 'View dashboard' footer link
- lt-notif-dot: moves border-radius:50%;margin-top from inline style
(only background color remains inline, which is dynamic per-severity)
- Initial 'Loading…' text in the panel HTML uses lt-notif-empty
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Stat card icons and values: style="color:var(--red)" etc replaced with
lt-text-red, lt-text-amber, lt-text-cyan, lt-text-green (defined in
base.css with both color and glow-shadow)
- Host search input: style="width:180px" extracted to .lt-search-input--sm
- base.html: suppress modal form groups use lt-form-group--last for last
item (already committed); lt-divider--compact applied to settings divider
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Suppress modal form groups: strip margin-bottom:12px (lt-form-group
already has margin-bottom in TDS); use lt-form-group--last for the
final group where zero margin is needed
- Keyboard shortcuts table: remove width:100% (lt-table is already full-
width in base.css)
- Settings divider: replace style=margin override with .lt-divider--compact
- Topology bus section: move max-width:860px into .topo-bus-section rule
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Active events now carry an is_suppressed boolean (added in api_status()
and the index() route via check_suppressed() against the pre-loaded
suppression list). The events table renders a muted '🔕 sup' badge next
to the severity and dims the entire row (.row-suppressed) so operators
can immediately see which firing alerts are silenced.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove style="margin-top:4px" from .g-page-sub in all three secondary
pages (the value is already defined in .g-page-sub rule in style.css)
- suppressions.html: replace inline style="padding:12px 14px" with TDS
lt-section-body class
- index.html topology legend: replace inline dashed-border style with
.topo-legend-item--offrack modifier class in style.css
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Dashboard: reorder sections so Active Alerts appear immediately below
stat cards rather than after 200+ lines of topology — primary content
is now above the fold on all screen sizes
- Network Hosts section gains a collapse toggle (persisted to localStorage)
so the topology diagram can be hidden when not needed
- Admin nav corrected: admin now gets a direct Suppressions link;
non-admin no longer sees the page at all (it was always admin-only)
- Suppress modal moved from index.html into base.html so [S] keyboard
shortcut works on every page, not just the dashboard; form listeners
wired in app.js accordingly
- Settings modal KV grid: replaced lt-kv-row/lt-kv-label/lt-kv-value
(light-mode only) with lt-kv-key/lt-kv-val (dark-mode defined)
- style.css: add lt-btn-secondary dark-mode definition, lt-cmd-hint-btn
class for ⌘K button, topology collapse styles; remove dead .g-page-header,
.g-page-title, .empty-state classes; strip redundant inline styles
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- app.js: replace != with !== (eqeqeq rule) on resolved_24h null check
and totalActive pagination check
- style.css: extend loading state to all .lt-btn.is-loading (not just
refresh button), so suppressions form submit shows disabled feedback;
remove dead .link-collapse-bar rule
- base.html: add inspector page to footer R=REFRESH hint; update
keyboard shortcut table to include Inspector in refresh shortcut doc
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- db.py: add resolved_24h to get_status_summary() so each /api/status
poll carries the fresh 24h resolved count
- app.js: wire stat-resolved-val to update from summary.resolved_24h
so the Resolved 24h card stays accurate after auto-refresh
- index.html: add lt-toolbar/lt-search above host grid for quick
client-side host filtering by name
- links.html: replace custom unifi-section-header div with lt-divider
- style.css: remove unused .unifi-section-header rules
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- index.html: wrap UniFi devices table in lt-frame with section header
- links.html: add static lt-toolbar with lt-search filter and collapse
controls above the dynamic container; remove collapse bar from
renderLinks() since it's now static; add applyLinksSearch() to
filter host/switch panels by name as user types
- suppressions.html: wrap Available Targets section in lt-frame
- style.css: remove unused .link-summary-panel and related rules
(replaced by lt-stats-grid in previous commit)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- links.html: replace custom link-summary-panel with lt-stats-grid/lt-stat-card
showing total interfaces, ports down, errors, and PoE load
- suppressions.html: wrap active suppressions and history tables in lt-frame
with lt-section-header labels
- inspector.html: wire auto-refresh to gandalfSettings (respects interval pill),
fix updated timestamp to use locale time
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Four lt-stat-card widgets (Critical, Warning, Hosts, Resolved 24h)
below the status bar; Critical card pulses red when count > 0
- Clicking Critical or Warning card filters the events table by severity
- Events table wrapped in lt-frame with ASCII corner ornaments and
lt-section-header; filter bar moved to lt-toolbar with lt-search icon
- Recently Resolved table replaced with lt-timeline component
- updateStatusBar() and updateHostGrid() keep stat card values live
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Notification bell in header polls /api/status and shows active alerts
with severity-colored dots; badge counts unread items via localStorage
- Settings modal ([ * ] CFG) controls auto-refresh interval (15s/30s/1m/5m/off)
persisted to localStorage and wired into lt.autoRefresh on all pages
- Context-sensitive footer hints: Dashboard shows REFRESH + SUPPRESS,
Link Debug shows REFRESH, all pages show CFG + HELP
- Added S key (quick suppress) and * key (settings) shortcuts
- ⌘K affordance button added to header-right
- R key now uses lt.autoRefresh.now() so it works on any page
- refreshAll() pushes fresh events to notification bell on each poll
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add /api/avatar endpoint querying lldap for user jpegPhoto; disk cache
with sentinel pattern avoids repeat LDAP hits for users without photos
- Add ldap3 dependency and ldap config block to config.json
- Wire lt-avatar img overlay in base.html with capture-phase error
fallback (lt-avatar-img-err) to reveal initials when image is absent
- Fix lt-avatar CSS shim: position:relative + absolute inset on img
(local base.css was missing these; added to style.css)
- Replace all empty-state paragraphs with proper lt-empty-state markup
(icon + title + body) across index, suppressions, inspector, app.js
- Add lt-spinner--cyan next to refresh button; shows during refreshAll()
- Replace inspector panel-section-title with lt-divider throughout
- Add data-tooltip attributes to SFP DOM metrics, TX/RX/Carrier/Duplex/
Auto-neg/Error labels in links.html and inspector panel
- Add tooltips to events table column headers (Sev, First Seen, Failures)
- Fix links.html host panel timestamp (was reading sample.updated which
is always undefined; now uses data.updated)
- Fix UniFi status text casing (Online→ONLINE to match server render)
- Remove dead topo-status-* class manipulation from updateTopology()
- Always render alert-count-badge; toggle display:none when count is 0
- Fix double UniFi get_devices() call in monitor.py run loop
- Fix chip-critical animation (was using green pulse-glow; now red)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
lt-alert:
- Replace custom .stale-banner with lt-alert lt-alert--warning in app.js
and links.html; remove stale-banner CSS, reuse lt-alert margin rule
lt-progress:
- Replace custom .traffic-bar-track/.traffic-bar-fill in links.html with
lt-progress from base.css; TX uses default (orange), RX uses --cyan,
both flip to --red when utilisation >85% (trafficBarClass helper)
- Keep traffic layout classes (.traffic-section/.traffic-row etc.) for structure
Suppression type badges:
- Map target_type to distinct badge colors: host→badge-warning (orange),
interface→badge-info (cyan), unifi_device→badge-purple (new alias using
--accent-purple from base.css), all→badge-critical (red)
- Applied in both server-rendered table (Jinja2 dict lookup) and
renderActiveRows() JS
Topology animated down-wire:
- Add data-host attribute to .topo-v2-wire-10g/.topo-v2-wire-1g elements
- updateTopology() toggles .wire-down class on the 10G drop-wire when
host.status === 'down'
- .wire-down CSS: animated repeating-linear-gradient dashed red line
via wire-dash-anim @keyframes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- inspector.html: onclick on port blocks, close button, run-diagnostic button,
and diag-toggle sections all converted to data-action attributes; single
delegated click listener handles all cases + Escape key closes panel
- links.html: onclick on panel title headers, Collapse All, Expand All
converted to data-action with delegated listener
- suppressions.html: onsubmit/onchange wired via addEventListener at init
- index.html: onsubmit/onchange on suppress modal form wired at init
No behavioural changes — pure event-handling refactor for TDS compliance.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- app.js: guard events array with || [] before .filter() to prevent crash on null
- app.js: show warning toast when /api/network or /api/status fail (was silent)
- app.js: add aria-label to all dynamically-generated suppress buttons
- index.html: add aria-label to server-rendered suppress buttons
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add escHtml alias (lt.escHtml) to both pages so existing template strings work without touching 40+ call sites
- Replace raw fetch() with lt.api.get/post in loadInspector, loadLinks, runDiagnostic, pollDiagnostic
- Replace setInterval(load*, 60000) with lt.autoRefresh.start() for intelligent polling
- Add lt.toast.error() to catch blocks for user-visible error feedback
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- app.py: avatar_color Jinja filter using deterministic hash → lt-avatar--orange/green/purple
- base.html: proper lt-avatar--sm with lt-avatar-initials span and color class; multi-word initials support
- base.html: admin users get lt-nav-dropdown for Suppressions; non-admins see flat link; mobile drawer hides Suppressions for non-admins
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>