Ops-dashboard/app/flows/update-scrum4me-web/_components/flow-panel.tsx
Scrum4Me Agent 9f590f1732 feat(flows): add update_scrum4me_web flow and UI page
- Update ops-agent/flows.example/update_scrum4me_web.yml with full
  deployment steps: git_status, git_fetch, git_log_ahead, git_pull,
  npm_ci, prisma_migrate_deploy, npm_run_build, systemctl_restart,
  and smoke test against thuis.jp-visser.nl/api/products
- Add npm_ci, prisma_migrate_deploy, npm_run_build, and
  curl_smoke_scrum4me_thuis to commands.yml.example
- Add /flows/update-scrum4me-web UI page with Run and Dry Run buttons,
  streaming terminal output, and link to audit log on completion

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 19:42:39 +02:00

128 lines
4.3 KiB
TypeScript

'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'
const FLOW_KEY = 'update_scrum4me_web'
const STEPS = [
'git status (show current state)',
'git fetch (fetch remote refs)',
'git log (commits ahead of upstream)',
'git pull --ff-only (aborts if dirty)',
'npm ci (install dependencies)',
'prisma migrate deploy (apply migrations)',
'npm run build (build application)',
'systemctl restart scrum4me-web',
'smoke test: curl /api/products (expect 200 or 401)',
]
export default function FlowPanel() {
const [pendingDryRun, setPendingDryRun] = useState<boolean | null>(null)
const [completedFlowRunId, setCompletedFlowRunId] = useState<string | null>(null)
const handleComplete = useCallback((flowRunId: string) => {
setCompletedFlowRunId(flowRunId)
}, [])
const flowRun = useFlowRun(handleComplete)
const handleConfirm = useCallback(() => {
if (pendingDryRun === null) return
const dryRun = pendingDryRun
setPendingDryRun(null)
setCompletedFlowRunId(null)
flowRun.startFlow(FLOW_KEY, dryRun)
}, [pendingDryRun, flowRun])
const handleReset = useCallback(() => {
flowRun.reset()
setCompletedFlowRunId(null)
}, [flowRun])
return (
<div className="space-y-6">
<div className="rounded-lg border border-border p-5 space-y-4">
<div>
<p className="text-sm text-muted-foreground">
Reproduces the Scrum4Me website update: pulls latest code, installs
dependencies, applies migrations, builds, restarts the service, and
verifies the endpoint is responding.
</p>
<p className="mt-1 text-xs text-muted-foreground font-mono">
repo: /srv/scrum4me/repos/Scrum4Me
</p>
</div>
<ol className="space-y-1">
{STEPS.map((step, i) => (
<li key={i} className="flex gap-2 text-xs font-mono text-muted-foreground">
<span className="text-border min-w-[1.5rem]">{i + 1}.</span>
<span>{step}</span>
</li>
))}
</ol>
</div>
<div className="flex items-center gap-3">
<button
onClick={() => setPendingDryRun(false)}
disabled={flowRun.status === 'running'}
className="rounded-lg bg-foreground text-background px-4 py-2 text-sm font-medium hover:opacity-90 disabled:opacity-50 transition-opacity"
>
Run
</button>
<button
onClick={() => setPendingDryRun(true)}
disabled={flowRun.status === 'running'}
className="rounded-lg border border-border px-4 py-2 text-sm hover:bg-muted/50 disabled:opacity-50 transition-colors"
>
Dry Run
</button>
{flowRun.status !== 'idle' && flowRun.status !== 'running' && (
<button
onClick={handleReset}
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
>
Reset
</button>
)}
</div>
{flowRun.status !== 'idle' && (
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">Output</span>
{completedFlowRunId && (
<Link
href={`/audit/${completedFlowRunId}`}
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
>
View in audit log
</Link>
)}
</div>
<StreamingTerminal
lines={flowRun.lines}
status={flowRun.status}
error={flowRun.error}
/>
</div>
)}
<ConfirmDialog
open={pendingDryRun !== null}
title={pendingDryRun ? 'Dry Run: Update Scrum4Me Web' : 'Run: Update Scrum4Me Web'}
commandPreview={
pendingDryRun
? `[DRY RUN] flow: ${FLOW_KEY}\n\nAll steps will be shown without executing.`
: `flow: ${FLOW_KEY}\n\nSteps:\n${STEPS.map((s, i) => ` ${i + 1}. ${s}`).join('\n')}`
}
onConfirm={handleConfirm}
onCancel={() => setPendingDryRun(null)}
/>
</div>
)
}