fix(panel): align task actions to server contract + collect required audit notes

The panel's human action buttons had drifted from the server request
schemas: wrong field names (qa_notes/reason vs notes), missing bodies
(cancel/complete/submit-pm-review), and a bare-string docs-complete body —
so cancel/pass-qa/fail-qa/escalate-to-ceo 4xx'd and decisions recorded no
audit note. (Agents were unaffected — they go through the gateway.)

- tasks.ts: pass-qa/fail-qa -> {notes}; escalate-to-ceo -> {notes:reason};
  cancel -> {reason}; complete -> {justification}; docs-complete -> {notes};
  submit-pm-review -> {notes}.
- New reusable RequiredNotesDialog (generalizes CeoApproveDialog). Every
  decision action now collects a substantive note before POSTing: cancel
  (>=10), pass-qa/fail-qa/docs-complete/submit-pm-review/complete (>=20),
  matching the server gates. Wired in the task detail page, the actions
  dropdown, and the kanban board.

Verified: pnpm tsc --noEmit and eslint both clean.
This commit is contained in:
Renn F
2026-05-24 07:10:34 +02:00
parent 5120b5ce81
commit bc5e016d6d
6 changed files with 429 additions and 51 deletions
@@ -205,6 +205,84 @@ export function CeoApproveDialog({
);
}
// Required Notes Dialog — a generalized version of CeoApproveDialog. Collects a
// substantive audit note (>= minChars) before confirming a decision action.
interface RequiredNotesDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onConfirm: (text: string) => void;
isPending?: boolean;
title: string;
description: string;
label: string;
placeholder: string;
minChars: number;
confirmLabel: string;
destructive?: boolean;
}
export function RequiredNotesDialog({
open,
onOpenChange,
onConfirm,
isPending,
title,
description,
label,
placeholder,
minChars,
confirmLabel,
destructive,
}: RequiredNotesDialogProps) {
const [text, setText] = useState("");
const tooShort = text.trim().length < minChars;
const handleConfirm = () => {
if (!tooShort) {
onConfirm(text.trim());
setText("");
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="required-notes">{label}</Label>
<Textarea
id="required-notes"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder={placeholder}
rows={4}
/>
<p className="text-xs text-muted-foreground">
{text.trim().length}/{minChars} characters minimum
</p>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button
variant={destructive ? "destructive" : "default"}
onClick={handleConfirm}
disabled={tooShort || isPending}
>
{isPending ? "Working..." : confirmLabel}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
// Create Branch Dialog
interface CreateBranchDialogProps {
open: boolean;