M13: Veilige Claude-agent-workflow (Scrum4Me-side) (#26)
* feat: add pushed_at field to ClaudeJob schema Nullable DateTime column to record when the agent's feature branch was pushed to origin. Enables the UI to show a 'pushed' state independently of DONE status. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: GitHub-link op DONE-card + pushed_at doorvoer - lib/job-status-url.ts: getBranchUrl(repoUrl, branch) → GitHub tree URL - JobState + ClaudeJobEvent: pushed_at? veld toegevoegd - realtime/solo/route.ts: pushed_at in Prisma-select, JobPayload en mapping - SoloBoardProps + TaskDetailDialog: repoUrl prop doorgevoerd - task-detail-dialog: "Open op GitHub"-link als done + pushed_at + branch + repoUrl - 3 unit-tests voor getBranchUrl; totaal 261 tests groen Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add VerifyResult enum, verify_only on Task, verify_result on ClaudeJob Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add verify_result+pushed_at to JobState, VerifyResultApi type, SSE payload Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: verify_only field on SoloTask, PATCH route saves verify_only Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: TaskDetailDialog — verify_result display + verify_only checkbox Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test: verify_only PATCH + verify_result dialog render + store fix Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: document VerifyResult enum, verify_only task field, pushed_at in architecture Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(M13): cron /api/cron/cleanup-agent-artifacts — hard-delete FAILED/CANCELLED jobs >7 days * feat(M13): add auto_pr field to Product schema + migration * feat(M13): auto_pr toggle in product settings — server action + UI component + tests * feat(M13): add pr_url to ClaudeJob schema + migration * feat(M13): UI — 'Open PR' link on DONE-card; pr_url in JobState + SSE + task-dialog * feat(M13): add retry_count migration + regen erd - Migration ALTER TABLE claude_jobs ADD COLUMN retry_count INT DEFAULT 0 (schema.prisma was reeds bijgewerkt in eerdere commits) - docs/erd.svg geregenereerd voor de complete M13-schema-wijzigingen (verify_result, verify_only, pushed_at, pr_url, auto_pr, retry_count) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
acb591266f
commit
9794a9baef
26 changed files with 725 additions and 20 deletions
|
|
@ -13,6 +13,7 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/comp
|
|||
import { useSoloStore } from '@/stores/solo-store'
|
||||
import { enqueueClaudeJobAction, cancelClaudeJobAction } from '@/actions/claude-jobs'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { getBranchUrl } from '@/lib/job-status-url'
|
||||
import type { SoloTask } from './solo-board'
|
||||
|
||||
const STATUS_COLORS: Record<string, string> = {
|
||||
|
|
@ -33,6 +34,7 @@ interface TaskDetailDialogProps {
|
|||
task: SoloTask | null
|
||||
productId: string
|
||||
isDemo: boolean
|
||||
repoUrl?: string | null
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
|
|
@ -40,19 +42,29 @@ interface TaskDetailContentProps {
|
|||
task: SoloTask
|
||||
productId: string
|
||||
isDemo: boolean
|
||||
repoUrl?: string | null
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const VERIFY_RESULT_CONFIG: Record<string, { label: string; className: string }> = {
|
||||
aligned: { label: 'Aligned', className: 'text-status-done' },
|
||||
partial: { label: 'Gedeeltelijk', className: 'text-warning' },
|
||||
divergent: { label: 'Divergent', className: 'text-error' },
|
||||
empty: { label: 'Geen wijzigingen', className: 'text-muted-foreground' },
|
||||
}
|
||||
|
||||
type SaveState = 'idle' | 'saving' | 'saved'
|
||||
|
||||
function TaskDetailContent({ task, productId, isDemo, onClose }: TaskDetailContentProps) {
|
||||
const { updatePlan } = useSoloStore()
|
||||
function TaskDetailContent({ task, productId, isDemo, repoUrl, onClose }: TaskDetailContentProps) {
|
||||
const { updatePlan, updateVerifyOnly } = useSoloStore()
|
||||
const job = useSoloStore(s => s.claudeJobsByTaskId[task.id])
|
||||
const connectedWorkers = useSoloStore(s => s.connectedWorkers)
|
||||
const [localPlan, setLocalPlan] = useState(task.implementation_plan ?? '')
|
||||
const [localVerifyOnly, setLocalVerifyOnly] = useState(task.verify_only)
|
||||
const [saveState, setSaveState] = useState<SaveState>('idle')
|
||||
const [, startTransition] = useTransition()
|
||||
const [jobPending, startJobTransition] = useTransition()
|
||||
const [verifyOnlyPending, startVerifyOnlyTransition] = useTransition()
|
||||
const fadeTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const savedPlanRef = useRef(task.implementation_plan ?? '')
|
||||
|
||||
|
|
@ -108,6 +120,31 @@ function TaskDetailContent({ task, productId, isDemo, onClose }: TaskDetailConte
|
|||
})
|
||||
}
|
||||
|
||||
function handleVerifyOnlyToggle() {
|
||||
if (isDemo) return
|
||||
const newValue = !localVerifyOnly
|
||||
setLocalVerifyOnly(newValue)
|
||||
startVerifyOnlyTransition(async () => {
|
||||
try {
|
||||
const res = await fetch(`/api/tasks/${task.id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ verify_only: newValue }),
|
||||
})
|
||||
if (!res.ok) {
|
||||
setLocalVerifyOnly(!newValue)
|
||||
toast.error('Verify-only bijwerken mislukt')
|
||||
return
|
||||
}
|
||||
updateVerifyOnly(task.id, newValue)
|
||||
} catch {
|
||||
setLocalVerifyOnly(!newValue)
|
||||
toast.error('Verify-only bijwerken mislukt')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DialogHeader>
|
||||
|
|
@ -159,6 +196,30 @@ function TaskDetailContent({ task, productId, isDemo, onClose }: TaskDetailConte
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<DemoTooltip show={isDemo}>
|
||||
<button
|
||||
type="button"
|
||||
role="checkbox"
|
||||
aria-checked={localVerifyOnly}
|
||||
onClick={handleVerifyOnlyToggle}
|
||||
disabled={isDemo || verifyOnlyPending}
|
||||
className={cn(
|
||||
'h-4 w-4 rounded border border-border flex items-center justify-center shrink-0',
|
||||
'disabled:cursor-not-allowed disabled:opacity-50',
|
||||
localVerifyOnly && 'bg-primary border-primary',
|
||||
)}
|
||||
>
|
||||
{localVerifyOnly && (
|
||||
<svg className="h-3 w-3 text-primary-foreground" viewBox="0 0 12 12" fill="none">
|
||||
<path d="M2 6l3 3 5-5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</DemoTooltip>
|
||||
<span className="text-xs text-muted-foreground">Alleen verifiëren (niet implementeren)</span>
|
||||
</div>
|
||||
|
||||
<div className="-mx-4 -mb-4 flex flex-wrap items-center gap-2 border-t bg-muted/50 px-4 py-3 rounded-b-xl">
|
||||
<Link
|
||||
href={`/products/${productId}/sprint/planning`}
|
||||
|
|
@ -206,8 +267,48 @@ function TaskDetailContent({ task, productId, isDemo, onClose }: TaskDetailConte
|
|||
)}
|
||||
|
||||
{job?.status === 'done' && (
|
||||
<span className="text-xs text-status-done">
|
||||
Klaar{job.branch ? ` — branch ${job.branch}` : ''}
|
||||
<span className="text-xs text-status-done flex items-center gap-2 flex-wrap">
|
||||
Klaar{job.branch && !job.pushed_at ? ` — branch ${job.branch}` : ''}
|
||||
{job.pr_url && (
|
||||
<a
|
||||
href={job.pr_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline text-primary hover:text-primary/80"
|
||||
>
|
||||
Open PR
|
||||
</a>
|
||||
)}
|
||||
{!job.pr_url && job.pushed_at && job.branch && repoUrl && (
|
||||
<a
|
||||
href={getBranchUrl(repoUrl, job.branch)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline text-primary hover:text-primary/80"
|
||||
>
|
||||
Open op GitHub
|
||||
</a>
|
||||
)}
|
||||
{job.verify_result && (() => {
|
||||
const cfg = VERIFY_RESULT_CONFIG[job.verify_result]
|
||||
return cfg ? (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={
|
||||
<span className={cn('font-medium cursor-default', cfg.className)}>
|
||||
▸ {cfg.label}
|
||||
</span>
|
||||
} />
|
||||
<TooltipContent side="top" className="max-w-xs text-xs">
|
||||
{job.verify_result === 'aligned' && 'De implementatie komt overeen met het plan.'}
|
||||
{job.verify_result === 'partial' && 'De implementatie wijkt gedeeltelijk af van het plan.'}
|
||||
{job.verify_result === 'divergent' && 'De implementatie wijkt significant af van het plan.'}
|
||||
{job.verify_result === 'empty' && 'Er zijn geen codewijzigingen gedetecteerd.'}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
) : null
|
||||
})()}
|
||||
</span>
|
||||
)}
|
||||
|
||||
|
|
@ -219,7 +320,7 @@ function TaskDetailContent({ task, productId, isDemo, onClose }: TaskDetailConte
|
|||
)
|
||||
}
|
||||
|
||||
export function TaskDetailDialog({ task, productId, isDemo, onClose }: TaskDetailDialogProps) {
|
||||
export function TaskDetailDialog({ task, productId, isDemo, repoUrl, onClose }: TaskDetailDialogProps) {
|
||||
return (
|
||||
<Dialog open={!!task} onOpenChange={(open) => { if (!open) onClose() }}>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
|
|
@ -229,6 +330,7 @@ export function TaskDetailDialog({ task, productId, isDemo, onClose }: TaskDetai
|
|||
task={task}
|
||||
productId={productId}
|
||||
isDemo={isDemo}
|
||||
repoUrl={repoUrl}
|
||||
onClose={onClose}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue