feat: scheduled backups, availability tracking, PoE power metrics (v0.11.2 Beta)

P1 — Scheduled Backups
- BackupService.createBackup() now accepts a `type` param (manual/scheduled)
- PollerService: cronMatchesNow() parser + runScheduledBackups() + Redis 1h gate
- Settings page: backup schedule toggle + cron expression input with examples

P2 — Uptime/Availability Tracking
- DB: device_availability table (went_offline_at, came_back_online_at, duration_seconds)
- PollerService: inserts outage rows on offline detection, closes them on recovery
- GET /api/metrics/device/:id/availability?range=30d endpoint
- /api/metrics/summary now includes fleet 30-day uptime %
- DeviceDetailPage: 30-day availability card (uptime %, outage count, longest outage)
- DashboardPage HealthBar: Uptime 30d mini-stat

P3 — PoE Power Draw Metrics
- DeviceCollector.collectPoePower() queries /interface/ethernet/poe/monitor for switches
- Writes poe_power measurement to InfluxDB (tags: device_id, port; fields: watts, current_ma, voltage_v)
- GET /api/metrics/device/:id/poe endpoint (current per-port + 1h history)
- SwitchPortDiagram: amber watt badge on PoE-active ports
- HardwareTab: PoE budget section showing active ports and total watts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2GT-Rich
2026-05-16 15:19:58 -07:00
co-authored by Claude Sonnet 4.6
parent e692a1d7cd
commit 6a4ab1fc5f
16 changed files with 431 additions and 23 deletions
+9 -1
View File
@@ -158,7 +158,7 @@ function DeviceLocationsMap({ devices }: { devices: Device[] }) {
function HealthBar({
summary, devices, wirelessCount, clientSparkline,
}: {
summary: { devices: { total: number; online: number; offline: number }; clients: { active: number; total: number }; alerts: { critical: number; warning: number } } | undefined;
summary: { devices: { total: number; online: number; offline: number }; clients: { active: number; total: number }; alerts: { critical: number; warning: number }; availability?: { fleetUptimePct30d: number } } | undefined;
devices: Device[];
wirelessCount: number;
clientSparkline: number[];
@@ -234,6 +234,14 @@ function HealthBar({
spark: [] as number[],
color: (summary?.alerts.critical ?? 0) > 0 ? 'var(--bad)' : 'var(--ink-3)',
},
{
label: 'Uptime 30d',
value: summary?.availability != null
? `${summary.availability.fleetUptimePct30d.toFixed(1)}%`
: '—',
spark: [] as number[],
color: (summary?.availability?.fleetUptimePct30d ?? 100) >= 99 ? 'var(--accent)' : (summary?.availability?.fleetUptimePct30d ?? 100) >= 95 ? 'var(--warn)' : 'var(--bad)',
},
].map(({ label, value, spark, color }) => (
<div key={label} style={{ minWidth: 110, paddingLeft: 20, borderLeft: '1px solid var(--line)' }}>
<div className="text-[11px] mb-[2px]" style={{ color: 'var(--ink-3)' }}>{label}</div>