'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' type RestartMode = 'reload' | 'force' const FLOW_KEYS: Record = { reload: 'update_caddy_config', force: 'update_caddy_config_force', } const STEP_LABELS: Record = { reload: [ 'caddy validate (syntax check)', 'caddy reload (zero-downtime via admin API)', 'smoke test: curl -I each hostname (expect 200/301/308/401)', ], force: [ 'caddy validate (syntax check)', 'docker compose up --force-recreate caddy (hard restart)', 'smoke test: curl -I each hostname (expect 200/301/308/401)', ], } function parseCaddyfileHostnames(content: string): string[] { const hostnames: string[] = [] // Match top-level server block labels: hostname { or hostname:port { for (const match of content.matchAll(/^([a-zA-Z0-9][a-zA-Z0-9\-.:]+)\s*\{/gm)) { const candidate = match[1].trim() // Skip obvious non-hostnames like "tls", "log", "handle", etc. if (!candidate.includes('.') && !candidate.includes(':')) continue // Strip port suffix for display const host = candidate.split(':')[0] if (!hostnames.includes(host)) hostnames.push(host) } return hostnames } function CaddyfileDiff({ content, error }: { content: string; error: string | null }) { if (error) { return (
Could not load Caddyfile: {error}
) } if (!content.trim()) { return (
Caddyfile is empty or not found
) } return (
      {content}
    
) } type Props = { caddyfile: string caddyfileError: string | null } export default function FlowPanel({ caddyfile, caddyfileError }: Props) { const [mode, setMode] = useState('reload') const [pendingDryRun, setPendingDryRun] = useState(null) const [completedFlowRunId, setCompletedFlowRunId] = useState(null) const [showConfig, setShowConfig] = useState(false) 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_KEYS[mode], dryRun) }, [pendingDryRun, flowRun, mode]) const handleReset = useCallback(() => { flowRun.reset() setCompletedFlowRunId(null) }, [flowRun]) const hostnames = parseCaddyfileHostnames(caddyfile) const steps = STEP_LABELS[mode] const flowKey = FLOW_KEYS[mode] return (
{/* Description */}

Validates the Caddyfile on disk, then reloads or restarts Caddy, and smoke-tests every public hostname. Edit the config first via the{' '} Caddy editor .

config: /srv/scrum4me/caddy/Caddyfile

{/* Restart mode toggle */}
Restart mode:
{/* Step list */}
    {steps.map((step, i) => (
  1. {i + 1}. {step}
  2. ))}
{/* Detected hostnames */} {hostnames.length > 0 && (

Hostnames detected in Caddyfile (will be smoke-tested):

    {hostnames.map((h) => (
  • • {h}
  • ))}
)}
{/* Config preview (dry-run diff) */}
{showConfig && (

This is the config that will be validated and applied:

{!caddyfileError && (

To change the config before applying,{' '} edit in the Caddy editor {' '} first.

)}
)}
{/* Action buttons */}
{flowRun.status !== 'idle' && flowRun.status !== 'running' && ( )}
{/* Terminal output */} {flowRun.status !== 'idle' && (
Output {completedFlowRunId && ( View in audit log → )}
)} {/* Confirm dialog */} 0 ? `\nHostnames: ${hostnames.join(', ')}` : ''}` : `flow: ${flowKey}\n\nSteps:\n${steps.map((s, i) => ` ${i + 1}. ${s}`).join('\n')}${hostnames.length > 0 ? `\n\nHostnames to smoke-test: ${hostnames.join(', ')}` : ''}` } onConfirm={handleConfirm} onCancel={() => setPendingDryRun(null)} />
) }