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
@@ -71,6 +71,9 @@ export class DeviceCollector {
if (this.device.device_type === 'wireless_ap') {
await this.collectWirelessStats();
}
if (this.device.device_type === 'switch') {
await this.collectPoePower();
}
await this.updateDeviceStatus('online');
}
@@ -1727,6 +1730,37 @@ export class DeviceCollector {
return this.client.execute('/interface/ethernet/poe/print', { detail: '' }).catch(() => []);
}
async collectPoePower(): Promise<void> {
try {
const ports = await this.client
.execute('/interface/ethernet/poe/monitor', { once: '' })
.catch(() => [] as Record<string, string>[]);
if (ports.length === 0) return;
const writeApi = getWriteApi();
for (const port of ports) {
const name = port['name'] || port['port-name'] || '';
if (!name) continue;
const watts = parseFloat(port['poe-out-voltage'] || '0') * parseFloat(port['poe-out-current'] || '0') / 1000;
const currentMa = parseFloat(port['poe-out-current'] || '0');
const voltageV = parseFloat(port['poe-out-voltage'] || '0');
writeApi.writePoint(
new Point('poe_power')
.tag('device_id', String(this.device.id))
.tag('device_name', this.device.name)
.tag('port', name)
.floatField('watts', parseFloat(watts.toFixed(2)))
.floatField('current_ma', currentMa)
.floatField('voltage_v', voltageV)
.timestamp(new Date())
);
}
await writeApi.flush().catch((e) => console.error('InfluxDB PoE flush error:', e));
} catch (err) {
console.error(`[${this.device.name}] Failed to collect PoE power:`, err);
}
}
// ─── Port VLAN config ─────────────────────────────────────────────────────
async setPortVlanConfig(