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
+47
View File
@@ -496,6 +496,53 @@ export default function SettingsPage() {
))}
</div>
</div>
{/* Scheduled Backups */}
<div className="card p-5">
<h3 className="font-semibold text-gray-900 dark:text-white mb-1">Scheduled Backups</h3>
<p className="text-xs text-gray-400 dark:text-slate-500 mb-4">
Automatically backs up all online devices on a cron schedule via SSH export.
Backups appear in the Backups section with type <code className="font-mono">scheduled</code>.
</p>
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="text-sm font-medium text-gray-700 dark:text-slate-300">Enable scheduled backups</div>
<button
onClick={() => isAdmin && updateSettingsMutation.mutate({ backup_schedule_enabled: !settings['backup_schedule_enabled'] })}
disabled={!isAdmin}
className={clsx(
'relative inline-flex h-6 w-11 flex-shrink-0 rounded-full border-2 border-transparent transition-colors duration-200',
isAdmin ? 'cursor-pointer' : 'cursor-not-allowed opacity-50',
settings['backup_schedule_enabled'] ? 'bg-blue-600' : 'bg-gray-300 dark:bg-slate-600'
)}
>
<span className={clsx(
'inline-block h-5 w-5 transform rounded-full bg-white shadow transition duration-200',
settings['backup_schedule_enabled'] ? 'translate-x-5' : 'translate-x-0'
)} />
</button>
</div>
<div className="flex items-center justify-between gap-4">
<div>
<div className="text-sm font-medium text-gray-700 dark:text-slate-300">Cron schedule</div>
<div className="text-xs text-gray-400">Standard 5-part cron expression (minute hour day month weekday)</div>
</div>
<input
type="text"
className="input w-36 text-center font-mono text-sm disabled:opacity-50 disabled:cursor-not-allowed"
value={(settings['backup_schedule_cron'] as string) ?? '0 2 * * *'}
onChange={(e) => updateSettingsMutation.mutate({ backup_schedule_cron: e.target.value })}
placeholder="0 2 * * *"
disabled={!isAdmin || !settings['backup_schedule_enabled']}
/>
</div>
<p className="text-xs text-gray-400 dark:text-slate-500">
Examples: <code className="font-mono">0 2 * * *</code> = daily at 2:00 AM &nbsp;·&nbsp;
<code className="font-mono">0 3 * * 0</code> = weekly Sunday at 3:00 AM &nbsp;·&nbsp;
<code className="font-mono">0 */6 * * *</code> = every 6 hours
</p>
</div>
</div>
</div>
)}