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>
This commit is contained in:
Scrum4Me Agent 2026-05-13 19:42:39 +02:00
parent bdc24b57ba
commit 9f590f1732
4 changed files with 211 additions and 15 deletions

View file

@ -0,0 +1,128 @@
'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>
)
}

View file

@ -0,0 +1,27 @@
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 UpdateScrum4MeWebPage() {
const user = await getCurrentUser()
if (!user) redirect('/login')
return (
<div className="min-h-screen bg-background p-6">
<div className="mx-auto max-w-4xl space-y-6">
<div className="flex items-center gap-3">
<Link href="/" className="text-sm text-muted-foreground hover:text-foreground">
Home
</Link>
<span className="text-muted-foreground">/</span>
<h1 className="text-2xl font-semibold tracking-tight">Update Scrum4Me Web</h1>
</div>
<FlowPanel />
</div>
</div>
)
}

View file

@ -162,3 +162,27 @@ commands:
cmd: ["docker", "compose", "ps", "--filter", "status=running", "worker-idea"] cmd: ["docker", "compose", "ps", "--filter", "status=running", "worker-idea"]
cwd: "/srv/scrum4me/compose" cwd: "/srv/scrum4me/compose"
description: "Verify worker-idea container is in the running state" description: "Verify worker-idea container is in the running state"
# ── Scrum4Me web deployment steps ────────────────────────────────────────
npm_ci:
cmd: ["npm", "ci"]
cwd: "/srv/scrum4me/repos/Scrum4Me"
description: "Install production dependencies for Scrum4Me web (npm ci)"
prisma_migrate_deploy:
cmd: ["npx", "prisma", "migrate", "deploy"]
cwd: "/srv/scrum4me/repos/Scrum4Me"
description: "Apply pending Prisma migrations for Scrum4Me web"
npm_run_build:
cmd: ["npm", "run", "build"]
cwd: "/srv/scrum4me/repos/Scrum4Me"
description: "Build the Scrum4Me web application (next build)"
curl_smoke_scrum4me_thuis:
cmd:
- sh
- -c
- "code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 15 https://thuis.jp-visser.nl/api/products); echo \"HTTP $code\"; [ \"$code\" = \"200\" ] || [ \"$code\" = \"401\" ]"
description: "Smoke test: /api/products must return 200 or 401"

View file

@ -1,31 +1,48 @@
# Deploy the latest Scrum4Me web image. # Deploy the latest Scrum4Me web application from source.
# Copy to /etc/ops-agent/flows/update_scrum4me_web.yml on the host. # Copy to /etc/ops-agent/flows/update_scrum4me_web.yml on the host.
# #
# Steps: # Steps:
# 1. Fetch remote refs # 1. Show current git status (dirty tree aborts later at git_pull)
# 2. Fast-forward pull (aborts if working tree is dirty) # 2. Fetch remote refs
# 3. Rebuild the Docker image # 3. Show commits ahead of upstream
# 4. Recreate the container in detached mode # 4. Fast-forward pull (aborts if working tree is dirty)
# 5. Smoke-test the public endpoint # 5. Install dependencies
# 6. Apply database migrations
# 7. Build the application
# 8. Restart the systemd service
# 9. Smoke-test the public endpoint (200 or 401 = pass)
name: Update Scrum4Me Web name: Update Scrum4Me Web
description: Pull latest code, rebuild Docker image, and restart the Scrum4Me web service description: Pull latest code, install deps, run migrations, build, and restart scrum4me-web.service
steps: steps:
- command_key: git_status
args: ["/srv/scrum4me/repos/Scrum4Me"]
on_failure: continue
- command_key: git_fetch - command_key: git_fetch
args: ["/srv/scrum4me"] args: ["/srv/scrum4me/repos/Scrum4Me"]
on_failure: abort on_failure: abort
- command_key: git_log_ahead
args: ["/srv/scrum4me/repos/Scrum4Me"]
on_failure: continue
- command_key: git_pull - command_key: git_pull
args: ["/srv/scrum4me"] args: ["/srv/scrum4me/repos/Scrum4Me"]
on_failure: abort on_failure: abort
- command_key: docker_compose_build - command_key: npm_ci
on_failure: abort
- command_key: prisma_migrate_deploy
on_failure: abort
- command_key: npm_run_build
on_failure: abort
- command_key: systemctl_restart
args: ["scrum4me-web"] args: ["scrum4me-web"]
on_failure: abort on_failure: abort
- command_key: docker_compose_up - command_key: curl_smoke_scrum4me_thuis
args: ["scrum4me-web"]
on_failure: abort
- command_key: curl_smoke_scrum4me_web
on_failure: continue on_failure: continue