Three-level zoom control in sidebar footer. Uses CSS zoom property,
persisted to localStorage via Zustand store. Applied on mount via
AppLayout useEffect.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Hostname is now a Link to the device detail page
- MapPin icon shown for devices with coordinates, links to /map
- Hover accent color on both links
- Also fixes tenant-switch query bug and VPN tab colors
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Dashboard fleet query now uses selected tenant ID for super_admin
instead of always fetching all tenants. Needs Attention, metrics
strip, and all widgets update when switching tenants.
- VPN tab: replace hardcoded purple/blue/green hex with token colors
- Add Certificates and VPN back to sidebar low-frequency section
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
These were removed during the Operate/Act restructure but are
standalone management pages that need direct nav access.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
InterfacesPanel: replace Tailwind palette hex (#3B82F6 blue, #8B5CF6
purple, etc.) with token references (accent, info, warning, success,
error). No more blue or purple interface badges.
DiffViewer: replace raw blue/green/red Tailwind classes with token
classes (info, success, error).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Wraps setActiveTab to also scroll #main-content to top. Prevents
stale scroll position when navigating from a long tab (e.g. Firewall)
to a short one (e.g. SNMP).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add .list-item-interactive CSS class: 2px left border on hover/focus,
bg-elevated on active press, 50ms transitions
- FleetTable: update hover to bg-elevated/30 with 50ms transition
- Class available for div-based list rows (alerts, events, nav items)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two-row layout: top row is identity (breadcrumb › dot hostname status),
bottom row is metadata left + actions right. Hostname truncates instead
of wrapping. Metadata truncates. Actions stay on one line and push
against the right edge. No overlap at any width.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Full-text badge ("Plain-Text (Insecure)") replaced with a colored
shield icon — green/yellow/red by TLS mode. Label available on
hover via title attribute. Universal lock convention, no label needed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- WinBox/RemoteWinBox/SSH: replace big accent CTAs with compact
bordered tool buttons (text-[10px], h-3 icons, border-default)
- SimpleModeToggle: shrink from pill-button group to inline segmented
control (text-[10px], accent-soft active state)
- Edit/Delete already icon-only ghost from previous commit
- All tool buttons now visually consistent — small, bordered, receding
- Result: header reads as a compact control strip, not a CTA row
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All ContextStrip features (tenant selector, theme toggle, connection
status, user menu, logout) are now in the sidebar. The top bar is
removed — content area gets the full vertical space.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- KpiCards: remove gradient/glow, flatten to data-oriented panels
- BandwidthChart: replace hardcoded blue (#38BDF8) with accent token,
use token colors for axis text and cursor
- QuickActions: replace icon grid with command-style list rows
with left-border hover interaction
- EventsTimeline: remove timeline/skeleton, tighten to log-stream
layout with divide separators and monospace timestamps
- Light mode: bump border-default opacity 0.12→0.14, darken
text-secondary for dense readability
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Changed "analytics pixel to count page views" to "analytics to measure
page views and engagement" across all 22 site pages to accurately
describe the updated telemetry script.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
API container was OOM-killed under 500-device mock load due to debug
logging, single worker, and 512MB limit. Bumped to info logging,
2 workers, and 1GB. New blog post documents the incident.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add LICENSE_DEVICES env var (default 250, matches BSL 1.1 free tier)
- Add /api/settings/license endpoint returning device count vs limit
- Header shows flashing red "502/500 licensed" badge when over limit
- About page shows license tier, device count, and over-limit warning
- Nothing is crippled — all features work regardless of device count
- Bump version to 9.7.1
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- CONFIGURATION.md: fix database name (mikrotik → tod), add 5 missing
env vars, update NATS memory to 256MB
- API.md: add 8 missing endpoint groups (sites, sectors, wireless links,
signal history, site alerts, config backups, remote access, winbox)
- ARCHITECTURE.md: update subscriber count from 3 to 10, add v9.7
components (sites, sectors, link discovery, signal trending, site
alerts), add background service loops, update router count to 33
- USER-GUIDE.md: add tower/site management, wireless links, signal
history, site alerts, and fleet map documentation
- README.md: add v9.7 features to feature list
- DEPLOYMENT.md: add winbox-worker, openbao, wireguard to service list
- SECURITY.md: add WinBox session security details
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reverted from MapLibre/PMTiles to Leaflet with nginx-proxied OSM raster
tiles — the MapLibre approach had unresolvable CSP and theme compat
issues. The proxy keeps all browser requests local (no third-party).
Also:
- Add CPE signal strength and parent AP name to fleet summary SQL
and map popup cards (e.g. "Signal: -62 dBm to ap-shady-north")
- Add .dockerignore to exclude 8GB PMTiles and node_modules from
Docker build context (was causing 10+ minute builds)
- Configure mailpit SMTP in dev compose
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace Leaflet + OSM raster tiles with MapLibre GL JS + PMTiles:
- Full continental US vector tiles (8GB PMTiles, zoom 0-14 with overzoom)
- Dark theme via @protomaps/basemaps (official supported path)
- Clustered device markers with status colors (green/yellow/red)
- Popup cards show CPU, memory, wireless client count + avg signal
- Font glyphs proxied through nginx, sprites served locally
- Zero third-party requests from the browser
- Fleet summary SQL now includes wireless client count and avg signal
via LEFT JOIN LATERAL on wireless_links
Also removes alert toast spam and fixes map container height.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace OpenStreetMap CDN with self-hosted Protomaps PMTiles
(Wisconsin + Florida regional extracts, served from nginx)
- Add protomaps-leaflet for vector tile rendering in dark theme
- Update CSP to remove openstreetmap.org, add blob: for vector workers
- Add nginx location block for /tiles/ with byte range support
- Mount tiles directory as volume (not baked into image)
- Remove alert_fired/alert_resolved toast notifications that spammed
"undefined" at fleet scale — dashboard still updates via query invalidation
- Add *.pmtiles to .gitignore (large binaries)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SSE connections previously created regular push consumers without durable
names. When browsers disconnected uncleanly or the API restarted, these
orphaned consumers persisted on the NATS server and continued draining
messages — each restart added more, eventually saturating the API at
100% CPU.
Switched to ordered_consumer=True which:
- Creates ephemeral consumers with no server-side ack state
- Auto-cleans on disconnect (no orphans)
- Still delivers new messages in real-time for SSE
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause: stale NATS JetStream consumers accumulated across API
restarts, causing 13+ consumers to fight over messages in a single
Python async event loop (100% CPU).
Fixes:
- Add performance indexes on devices(tenant_id, hostname),
devices(tenant_id, status), key_access_log(tenant_id, created_at)
— drops devices seq_scans from 402k to 6 per interval
- Remove redundant ORDER BY t.name from fleet summary SQL
(tenant name sort is client-side, was forcing a cross-table sort)
- Bump NATS memory limit from 128MB to 256MB (was at 118/128)
- Increase dev poll interval from 60s to 120s for 400+ device fleet
The stream purge + restart brought API CPU from 100% to 0.3%.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Migrations 030 (sites), 032 (device_interfaces), 033 (wireless_links),
and 034 (sectors) were missing GRANT statements for app_user and
poller_user. Without these, fresh deploys crash on site/sector CRUD
with permission denied errors. Also added poller_user SELECT grants
to migration 035 (site_alert_rules/events).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix AttributeError in sites router: CurrentUser has `user_id` not `id`
(create/update/delete all crashed with 500)
- Add onError handlers with toast notifications to SiteFormDialog
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds CA, VPN, About, Alerts, Topology, Map, Batch Config, Bulk Commands,
Alert Rules, Maintenance, Reports, and Transparency to the sidebar
navigation. These routes existed but had no menu entries.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>