From f01bd555d1c141acc21117669888fcff9ee5c27b Mon Sep 17 00:00:00 2001 From: Janpeter Visser <30029041+madhura68@users.noreply.github.com> Date: Fri, 15 May 2026 20:52:52 +0200 Subject: [PATCH] feat(flows): add /flows/server-backup page (full + restore-test) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tweede entry point voor de server-backup feature die in ab87c0f + 20de584 is opgezet. Geeft de bestaande server_backup_full en server_backup_restore_test flows een eigen plek in de /flows/-index, naast redeploy-all / update-caddy / update-scrum4me-web. Eén panel met twee knoppen ('Backup now' / 'Run restore test'); de description- en step-lijst wisselen mee met de actief gekozen flow. Bevestig- dialog en confirm-body hergebruiken de wording uit app/settings/backups/_components/server-backup-section.tsx zodat beide ingangen consistent blijven. SSE-stream via dezelfde useFlowRun hook; audit-link na afloop net als redeploy-all. De settings/backups-page (status + config) raakt niet aangepast — bewust behouden als tweede ingang voor wie al op die pagina is. Files: - app/flows/server-backup/page.tsx (new, breadcrumb + panel) - app/flows/server-backup/_components/flow-panel.tsx (new, dual-action UI) - app/flows/page.tsx (+1 entry in FLOWS array) Server-side commands.yml en /etc/ops-agent/flows/*.yml moeten nog gedeployed worden — zonder die geeft ops-agent 'flow_key not found' terug. Deployment-script komt los. Co-Authored-By: Claude Opus 4.7 (1M context) --- app/flows/page.tsx | 5 + .../server-backup/_components/flow-panel.tsx | 178 ++++++++++++++++++ app/flows/server-backup/page.tsx | 31 +++ 3 files changed, 214 insertions(+) create mode 100644 app/flows/server-backup/_components/flow-panel.tsx create mode 100644 app/flows/server-backup/page.tsx diff --git a/app/flows/page.tsx b/app/flows/page.tsx index ee16447..e2790a7 100644 --- a/app/flows/page.tsx +++ b/app/flows/page.tsx @@ -20,6 +20,11 @@ const FLOWS = [ title: 'Update Caddy config', desc: 'Reload Caddy met nieuwe Caddyfile + cert renewal check', }, + { + href: '/flows/server-backup', + title: 'Server backup', + desc: 'pg_dumpall + restic naar NAS én B2 — handmatige backup of restore-test', + }, ] export default async function FlowsIndex() { diff --git a/app/flows/server-backup/_components/flow-panel.tsx b/app/flows/server-backup/_components/flow-panel.tsx new file mode 100644 index 0000000..e61c25a --- /dev/null +++ b/app/flows/server-backup/_components/flow-panel.tsx @@ -0,0 +1,178 @@ +'use client' + +import { useState, useCallback } from 'react' +import Link from 'next/link' +import { useFlowRun } from '@/hooks/useFlowRun' +import StreamingTerminal from '@/components/StreamingTerminal' +import ConfirmDialog from '@/components/ConfirmDialog' + +// One panel runs either flow; we switch step-list + description based on the +// currently active or last-triggered action. Wording mirrors the existing +// /settings/backups → server-backup-section so the two entry points stay +// consistent. The actual work in both flows runs out-of-band via systemd +// (server-backup.service) — the ops-agent flow just kicks it off and tails +// the resulting log / status file. + +type Kind = 'backup' | 'restore' + +type FlowSpec = { + flowKey: string + buttonLabel: string + shortDescription: string + steps: string[] + confirmTitle: string + confirmBody: string +} + +const FLOWS: Record = { + backup: { + flowKey: 'server_backup_full', + buttonLabel: 'Backup now', + shortDescription: + 'Volledige server-backup: pg_dumpall van alle databases + restic snapshot naar NAS én Backblaze B2 (Object Lock). Draait dagelijks via timer; deze knop triggert handmatig.', + steps: [ + 'trigger_server_backup (systemctl start server-backup.service)', + 'tail_backup_log_today (live log mee-stream)', + 'read_backup_status (lees status.json met repo-totalen + duur)', + ], + confirmTitle: 'Trigger server backup', + confirmBody: + 'flow: server_backup_full\n\nSteps:\n 1. trigger_server_backup (systemctl start server-backup.service)\n 2. tail_backup_log_today\n 3. read_backup_status\n\nThe actual work happens in systemd; this flow kicks it off and tails the log.', + }, + restore: { + flowKey: 'server_backup_restore_test', + buttonLabel: 'Run restore test', + shortDescription: + 'Non-destructieve restore-test: haalt de laatste snapshot uit de NAS-repo terug naar /tmp/restore-test en verifieert dat kritieke files er zijn. Raakt niets in de live stack.', + steps: [ + 'trigger_restore_test (restore latest NAS snapshot to /tmp/restore-test/)', + 'read_backup_status (lees assertions + per-file outcome)', + ], + confirmTitle: 'Run restore test (NAS)', + confirmBody: + 'flow: server_backup_restore_test\n\nSteps:\n 1. trigger_restore_test (restore latest NAS snapshot to /tmp/restore-test/)\n 2. read_backup_status\n\nNon-destructive — restores into /tmp only and asserts critical files exist.', + }, +} + +export default function FlowPanel() { + // `displayKind` drives the steps/description card; updated optimistically + // when the user presses a button so the displayed flow matches the pending + // confirm. `activeKind` only flips once the flow actually starts. + const [displayKind, setDisplayKind] = useState('backup') + const [pendingKind, setPendingKind] = useState(null) + const [activeKind, setActiveKind] = useState(null) + const [completedFlowRunId, setCompletedFlowRunId] = useState(null) + + const handleComplete = useCallback((flowRunId: string) => { + setCompletedFlowRunId(flowRunId) + }, []) + + const flowRun = useFlowRun(handleComplete) + + const handleClickKind = useCallback((kind: Kind) => { + setDisplayKind(kind) + setPendingKind(kind) + }, []) + + const handleConfirm = useCallback(() => { + if (pendingKind === null) return + const kind = pendingKind + setPendingKind(null) + setCompletedFlowRunId(null) + setActiveKind(kind) + flowRun.startFlow(FLOWS[kind].flowKey, false) + }, [pendingKind, flowRun]) + + const handleReset = useCallback(() => { + flowRun.reset() + setCompletedFlowRunId(null) + setActiveKind(null) + }, [flowRun]) + + const spec = FLOWS[displayKind] + + return ( +
+
+
+

{spec.shortDescription}

+

+ flow: {spec.flowKey} +

+
+
    + {spec.steps.map((step, i) => ( +
  1. + {i + 1}. + {step} +
  2. + ))} +
+
+ +
+ + + {flowRun.status !== 'idle' && flowRun.status !== 'running' && ( + + )} +
+ + {flowRun.status !== 'idle' && ( +
+
+ + Output{activeKind ? ` — ${FLOWS[activeKind].buttonLabel}` : ''} + + {completedFlowRunId && ( + + View in audit log → + + )} +
+ +
+ )} + + setPendingKind(null)} + /> +
+ ) +} diff --git a/app/flows/server-backup/page.tsx b/app/flows/server-backup/page.tsx new file mode 100644 index 0000000..7568e16 --- /dev/null +++ b/app/flows/server-backup/page.tsx @@ -0,0 +1,31 @@ +import Link from 'next/link' +import { redirect } from 'next/navigation' +import { getCurrentUser } from '@/lib/session' +import FlowPanel from './_components/flow-panel' + +export const dynamic = 'force-dynamic' + +export default async function ServerBackupPage() { + const user = await getCurrentUser() + if (!user) redirect('/login') + + return ( +
+
+
+ + ← Home + + / + + Flows + + / +

Server backup

+
+ + +
+
+ ) +}