From b3de779d87450033f1e0361144c621a1d5f1dbf8 Mon Sep 17 00:00:00 2001 From: soryu Date: Mon, 16 Feb 2026 17:59:38 +0000 Subject: Fix contracts page overflow, remove contract link from orders, add directive name (#65) * feat: soryu-co/soryu - makima: Add frontend pick-up-orders button and API integration * WIP: heartbeat checkpoint * feat: soryu-co/soryu - makima: Remove contract link from orders and add directive name to order metadata (frontend) * fix: contracts page overflow - use contained scrolling layout Changed the contracts page to use contained scrolling matching the orders/directives pages, preventing the page from growing beyond viewport height. Co-Authored-By: Claude Opus 4.6 * fix: resolve completion_task_id FK violation and duplicate button The completion_task_id column has an FK to tasks(id), but claim_directive_for_completion was being called with a placeholder UUID that did not exist in the tasks table, causing FK constraint violations. Fix: Create the task FIRST via create_task_for_owner, then use the real task.id when calling claim_directive_for_completion. Applied in all three locations: phase_completion Part 1 (idle directives), Part 3 (verification tasks), and trigger_completion_task (manual PR creation). Also removes a duplicate "Pick Up Orders" button in DirectiveDetail.tsx. * fix: restore Order type changes lost during rebase conflict resolution Re-apply changes from the orders-refactor commit that were dropped when resolving rebase conflicts with --ours: - Replace contractId with directiveName in Order interface - Make directiveId required in CreateOrderRequest - Remove contractId from UpdateOrderRequest - Change listOrders parameter from contractId to search - Remove linkOrderToContract function - Simplify convertOrderToStep to single argument --------- Co-authored-by: Claude Opus 4.6 --- .../frontend/src/components/orders/OrderDetail.tsx | 89 +++----------------- .../frontend/src/components/orders/OrderList.tsx | 8 +- makima/frontend/src/hooks/useOrders.ts | 14 +--- makima/frontend/src/lib/api.ts | 24 ++---- makima/frontend/src/routes/contracts.tsx | 23 +++--- makima/frontend/src/routes/orders.tsx | 34 +++++--- ...0_orders_remove_contract_add_directive_name.sql | 32 ++++++++ makima/src/db/models.rs | 30 ++----- makima/src/db/repository.rs | 60 +++++--------- makima/src/orchestration/directive.rs | 94 ++++++++++++---------- makima/src/server/handlers/orders.rs | 85 +++---------------- makima/src/server/mod.rs | 1 - makima/src/server/openapi.rs | 7 +- 13 files changed, 185 insertions(+), 316 deletions(-) create mode 100644 makima/migrations/20260216100000_orders_remove_contract_add_directive_name.sql diff --git a/makima/frontend/src/components/orders/OrderDetail.tsx b/makima/frontend/src/components/orders/OrderDetail.tsx index 7f8a95d..9d4c00c 100644 --- a/makima/frontend/src/components/orders/OrderDetail.tsx +++ b/makima/frontend/src/components/orders/OrderDetail.tsx @@ -39,8 +39,7 @@ interface OrderDetailProps { onUpdate: (req: UpdateOrderRequest) => Promise; onDelete: () => void; onLinkDirective: (directiveId: string) => Promise; - onLinkContract: (contractId: string) => Promise; - onConvertToStep: (directiveId: string) => Promise; + onConvertToStep: () => Promise; onRefresh: () => void; } @@ -50,7 +49,6 @@ export function OrderDetail({ onUpdate, onDelete, onLinkDirective, - onLinkContract, onConvertToStep, onRefresh, }: OrderDetailProps) { @@ -61,9 +59,6 @@ export function OrderDetail({ const [editingLabels, setEditingLabels] = useState(false); const [labelsText, setLabelsText] = useState(order.labels.join(", ")); const [showLinkDirective, setShowLinkDirective] = useState(false); - const [showLinkContract, setShowLinkContract] = useState(false); - const [contractIdInput, setContractIdInput] = useState(""); - const [showConvertToStep, setShowConvertToStep] = useState(false); const badge = STATUS_BADGE[order.status] || STATUS_BADGE.open; const currentPriority = PRIORITY_OPTIONS.find((p) => p.value === order.priority) || PRIORITY_OPTIONS[4]; @@ -110,17 +105,6 @@ export function OrderDetail({ setShowLinkDirective(false); }; - const handleLinkContract = async () => { - if (!contractIdInput.trim()) return; - await onLinkContract(contractIdInput.trim()); - setContractIdInput(""); - setShowLinkContract(false); - }; - - const handleConvertToStep = async (directiveId: string) => { - await onConvertToStep(directiveId); - setShowConvertToStep(false); - }; return (
@@ -196,12 +180,9 @@ export function OrderDetail({ {/* Linked entities */} {order.directiveId && ( - )} - {order.contractId && ( - )} {order.directiveStepId && ( @@ -453,67 +434,15 @@ export function OrderDetail({ )}
- {/* Link to Contract */} -
+ {/* Convert to Directive Step */} + {!order.directiveStepId && order.directiveId && ( - {showLinkContract && ( -
- setContractIdInput(e.target.value)} - placeholder="Contract ID..." - className="flex-1 bg-[#0a1628] border border-[rgba(117,170,252,0.2)] rounded px-2 py-1 text-[10px] font-mono text-white" - autoFocus - /> - -
- )} -
- - {/* Convert to Directive Step */} - {!order.directiveStepId && ( -
- - {showConvertToStep && ( -
- {directives.length === 0 ? ( -
- No directives available -
- ) : ( - directives.map((d) => ( - - )) - )} -
- )} -
)} diff --git a/makima/frontend/src/components/orders/OrderList.tsx b/makima/frontend/src/components/orders/OrderList.tsx index 76ac7a7..1d279f7 100644 --- a/makima/frontend/src/components/orders/OrderList.tsx +++ b/makima/frontend/src/components/orders/OrderList.tsx @@ -57,7 +57,8 @@ export function OrderList({ (o) => o.title.toLowerCase().includes(q) || (o.description && o.description.toLowerCase().includes(q)) || - o.labels.some((l) => l.toLowerCase().includes(q)), + o.labels.some((l) => l.toLowerCase().includes(q)) || + (o.directiveName && o.directiveName.toLowerCase().includes(q)), ); }, [orders, search]); @@ -158,6 +159,11 @@ export function OrderList({ /> {o.title} + {o.directiveName && ( + + {o.directiveName} + + )}
diff --git a/makima/frontend/src/hooks/useOrders.ts b/makima/frontend/src/hooks/useOrders.ts index 2dd20bb..9380080 100644 --- a/makima/frontend/src/hooks/useOrders.ts +++ b/makima/frontend/src/hooks/useOrders.ts @@ -12,7 +12,6 @@ import { updateOrder, deleteOrder, linkOrderToDirective, - linkOrderToContract, convertOrderToStep, } from "../lib/api"; @@ -101,16 +100,9 @@ export function useOrder(id: string | undefined) { return o; }, [id]); - const linkContract = useCallback(async (contractId: string) => { + const convertToStep = useCallback(async () => { if (!id) return; - const o = await linkOrderToContract(id, contractId); - setOrder(o); - return o; - }, [id]); - - const convertToStep = useCallback(async (directiveId: string) => { - if (!id) return; - const step = await convertOrderToStep(id, directiveId); + const step = await convertOrderToStep(id); await refresh(); return step; }, [id, refresh]); @@ -118,6 +110,6 @@ export function useOrder(id: string | undefined) { return { order, loading, error, refresh, update, remove, - linkDirective, linkContract, convertToStep, + linkDirective, convertToStep, }; } diff --git a/makima/frontend/src/lib/api.ts b/makima/frontend/src/lib/api.ts index a496412..17bc20f 100644 --- a/makima/frontend/src/lib/api.ts +++ b/makima/frontend/src/lib/api.ts @@ -3295,7 +3295,7 @@ export interface Order { labels: string[]; directiveId: string | null; directiveStepId: string | null; - contractId: string | null; + directiveName: string | null; repositoryUrl: string | null; createdAt: string; updatedAt: string; @@ -3313,8 +3313,7 @@ export interface CreateOrderRequest { status?: OrderStatus; orderType?: OrderType; labels?: string[]; - directiveId?: string | null; - contractId?: string | null; + directiveId: string; repositoryUrl?: string | null; } @@ -3327,7 +3326,6 @@ export interface UpdateOrderRequest { labels?: string[]; directiveId?: string | null; directiveStepId?: string | null; - contractId?: string | null; repositoryUrl?: string | null; } @@ -3336,14 +3334,14 @@ export async function listOrders( type?: OrderType, priority?: OrderPriority, directiveId?: string, - contractId?: string, + search?: string, ): Promise { const params = new URLSearchParams(); if (status) params.set("status", status); if (type) params.set("type", type); if (priority) params.set("priority", priority); if (directiveId) params.set("directiveId", directiveId); - if (contractId) params.set("contractId", contractId); + if (search) params.set("search", search); const qs = params.toString(); const res = await authFetch(`${API_BASE}/api/v1/orders${qs ? `?${qs}` : ""}`); if (!res.ok) throw new Error(`Failed to list orders: ${res.statusText}`); @@ -3391,21 +3389,9 @@ export async function linkOrderToDirective(orderId: string, directiveId: string) return res.json(); } -export async function linkOrderToContract(orderId: string, contractId: string): Promise { - const res = await authFetch(`${API_BASE}/api/v1/orders/${orderId}/link-contract`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ contractId }), - }); - if (!res.ok) throw new Error(`Failed to link order to contract: ${res.statusText}`); - return res.json(); -} - -export async function convertOrderToStep(orderId: string, directiveId: string): Promise { +export async function convertOrderToStep(orderId: string): Promise { const res = await authFetch(`${API_BASE}/api/v1/orders/${orderId}/convert-to-step`, { method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ directiveId }), }); if (!res.ok) throw new Error(`Failed to convert order to step: ${res.statusText}`); return res.json(); diff --git a/makima/frontend/src/routes/contracts.tsx b/makima/frontend/src/routes/contracts.tsx index acb6789..6d838ab 100644 --- a/makima/frontend/src/routes/contracts.tsx +++ b/makima/frontend/src/routes/contracts.tsx @@ -524,15 +524,9 @@ function ContractsPageContent() { return (
-
- {error && ( -
- {error} -
- )} - -
- {/* Contract list */} +
+ {/* Left: Contract list */} +
+
+ + {/* Right: Detail or Create */} +
+ {error && ( +
+ {error} +
+ )} {/* Contract detail, creation form, or empty state */} +
{isCreating ? (

@@ -873,6 +877,7 @@ function ContractsPageContent() {

)} +
diff --git a/makima/frontend/src/routes/orders.tsx b/makima/frontend/src/routes/orders.tsx index 735c557..deca77f 100644 --- a/makima/frontend/src/routes/orders.tsx +++ b/makima/frontend/src/routes/orders.tsx @@ -16,7 +16,7 @@ export default function OrdersPage() { const [statusFilter, setStatusFilter] = useState(undefined); const [typeFilter, setTypeFilter] = useState(undefined); const { orders, loading: listLoading, create, refresh: refreshList } = useOrders(statusFilter, typeFilter); - const { order, refresh: refreshDetail, update, remove: removeOrder, linkDirective, linkContract, convertToStep } = useOrder(selectedId); + const { order, refresh: refreshDetail, update, remove: removeOrder, linkDirective, convertToStep } = useOrder(selectedId); const { directives } = useDirectives(); const [showCreate, setShowCreate] = useState(false); @@ -24,6 +24,7 @@ export default function OrdersPage() { const [newDesc, setNewDesc] = useState(""); const [newPriority, setNewPriority] = useState("medium"); const [newType, setNewType] = useState("feature"); + const [newDirectiveId, setNewDirectiveId] = useState(""); useEffect(() => { if (!authLoading && isAuthConfigured && !isAuthenticated) { @@ -43,19 +44,21 @@ export default function OrdersPage() { } const handleCreate = async () => { - if (!newTitle.trim()) return; + if (!newTitle.trim() || !newDirectiveId) return; try { const o = await create({ title: newTitle.trim(), description: newDesc.trim() || undefined, priority: newPriority, orderType: newType, + directiveId: newDirectiveId, }); setShowCreate(false); setNewTitle(""); setNewDesc(""); setNewPriority("medium"); setNewType("feature"); + setNewDirectiveId(""); navigate(`/orders/${o.id}`); } catch (e) { console.error("Failed to create order:", e); @@ -84,13 +87,8 @@ export default function OrdersPage() { await refreshList(); }; - const handleLinkContract = async (contractId: string) => { - await linkContract(contractId); - await refreshList(); - }; - - const handleConvertToStep = async (directiveId: string) => { - await convertToStep(directiveId); + const handleConvertToStep = async () => { + await convertToStep(); await refreshList(); }; @@ -162,6 +160,21 @@ export default function OrdersPage() { className="w-full bg-[#0a1628] border border-[rgba(117,170,252,0.2)] rounded px-2 py-1.5 text-[12px] font-mono text-white resize-y" />
+
+ + +