diff options
| -rw-r--r-- | makima/frontend/src/components/NavStrip.tsx | 2 | ||||
| -rw-r--r-- | makima/frontend/src/components/contracts/ContractDetail.tsx | 4 | ||||
| -rw-r--r-- | makima/frontend/src/components/directives/DirectiveDetail.tsx | 2 | ||||
| -rw-r--r-- | makima/frontend/src/components/orders/OrderDetail.tsx | 114 | ||||
| -rw-r--r-- | makima/frontend/src/components/orders/OrderList.tsx | 2 | ||||
| -rw-r--r-- | makima/frontend/src/routes/contracts.tsx | 2 | ||||
| -rw-r--r-- | makima/src/db/models.rs | 8 | ||||
| -rw-r--r-- | makima/src/db/repository.rs | 90 | ||||
| -rw-r--r-- | makima/src/orchestration/directive.rs | 119 | ||||
| -rw-r--r-- | makima/src/server/handlers/directives.rs | 68 | ||||
| -rw-r--r-- | makima/src/server/handlers/orders.rs | 26 |
11 files changed, 331 insertions, 106 deletions
diff --git a/makima/frontend/src/components/NavStrip.tsx b/makima/frontend/src/components/NavStrip.tsx index 5aba6a3..117f4e1 100644 --- a/makima/frontend/src/components/NavStrip.tsx +++ b/makima/frontend/src/components/NavStrip.tsx @@ -11,8 +11,8 @@ interface NavLink { const NAV_LINKS: NavLink[] = [ { label: "Listen", href: "/listen" }, { label: "Directives", href: "/directives", requiresAuth: true }, - { label: "Contracts", href: "/contracts", requiresAuth: true }, { label: "Orders", href: "/orders", requiresAuth: true }, + { label: "Contracts", href: "/contracts", requiresAuth: true }, { label: "Mesh", href: "/mesh", requiresAuth: true }, { label: "History", href: "/history", requiresAuth: true }, ]; diff --git a/makima/frontend/src/components/contracts/ContractDetail.tsx b/makima/frontend/src/components/contracts/ContractDetail.tsx index e5e65f6..46b2212 100644 --- a/makima/frontend/src/components/contracts/ContractDetail.tsx +++ b/makima/frontend/src/components/contracts/ContractDetail.tsx @@ -102,7 +102,7 @@ export function ContractDetail({ ]; return ( - <div className="panel h-full flex flex-col"> + <div className="panel h-full flex flex-col min-h-0"> {/* Header */} <div className="p-4 border-b border-dashed border-[rgba(117,170,252,0.35)]"> <div className="flex items-center justify-between mb-3"> @@ -224,7 +224,7 @@ export function ContractDetail({ </div> {/* Tab content */} - <div className="flex-1 overflow-y-auto p-4"> + <div className="flex-1 overflow-y-auto p-4 min-h-0"> {activeTab === "overview" && ( <OverviewTab contract={contract} diff --git a/makima/frontend/src/components/directives/DirectiveDetail.tsx b/makima/frontend/src/components/directives/DirectiveDetail.tsx index c8da7a0..c9dac37 100644 --- a/makima/frontend/src/components/directives/DirectiveDetail.tsx +++ b/makima/frontend/src/components/directives/DirectiveDetail.tsx @@ -355,7 +355,7 @@ export function DirectiveDetail({ disabled={pickingUpOrders} className="text-[10px] font-mono text-[#c084fc] hover:text-[#d8b4fe] border border-[rgba(192,132,252,0.3)] rounded px-2 py-1 disabled:opacity-50" > - {pickingUpOrders ? "Picking up..." : "Pick Up Orders"} + {pickingUpOrders ? "Planning..." : "Plan Orders"} </button> <button type="button" diff --git a/makima/frontend/src/components/orders/OrderDetail.tsx b/makima/frontend/src/components/orders/OrderDetail.tsx index 12c87d1..9c3ac97 100644 --- a/makima/frontend/src/components/orders/OrderDetail.tsx +++ b/makima/frontend/src/components/orders/OrderDetail.tsx @@ -60,6 +60,7 @@ export function OrderDetail({ const [editingLabels, setEditingLabels] = useState(false); const [labelsText, setLabelsText] = useState(order.labels.join(", ")); const [showLinkDirective, setShowLinkDirective] = useState(false); + const [directiveSearch, setDirectiveSearch] = useState(""); const badge = STATUS_BADGE[order.status] || STATUS_BADGE.open; const currentPriority = PRIORITY_OPTIONS.find((p) => p.value === order.priority) || PRIORITY_OPTIONS[4]; @@ -406,31 +407,96 @@ export function OrderDetail({ <div className="flex flex-col gap-2"> {/* Link to Directive */} <div> - <button - type="button" - onClick={() => setShowLinkDirective(!showLinkDirective)} - className="text-[10px] font-mono text-[#75aafc] hover:text-white border border-[rgba(117,170,252,0.3)] rounded px-2 py-1 w-full text-left" - > - Link to Directive - </button> + <div className="flex items-center gap-1.5"> + <button + type="button" + onClick={() => { + setShowLinkDirective(!showLinkDirective); + setDirectiveSearch(""); + }} + className="text-[10px] font-mono text-[#75aafc] hover:text-white border border-[rgba(117,170,252,0.3)] rounded px-2 py-1 flex-1 text-left" + > + {order.directiveId ? "Change Directive" : "Link to Directive"} + </button> + {order.directiveId && ( + <button + type="button" + onClick={() => onUpdate({ directiveId: null, directiveStepId: null })} + className="text-[10px] font-mono text-red-400 hover:text-red-300 border border-red-800 rounded px-2 py-1" + title="Unlink directive" + > + Unlink + </button> + )} + </div> {showLinkDirective && ( - <div className="mt-1 border border-[rgba(117,170,252,0.2)] bg-[#0a1525] max-h-32 overflow-y-auto rounded"> - {directives.length === 0 ? ( - <div className="px-3 py-2 text-[10px] font-mono text-[#556677]"> - No directives available - </div> - ) : ( - directives.map((d) => ( - <button - key={d.id} - type="button" - onClick={() => handleLinkDirective(d.id)} - className="w-full text-left px-3 py-1.5 text-[10px] font-mono text-[#9bc3ff] hover:bg-[rgba(117,170,252,0.1)] border-b border-[rgba(117,170,252,0.1)] last:border-b-0" - > - {d.title} - </button> - )) - )} + <div className="mt-1 border border-[rgba(117,170,252,0.2)] bg-[#0a1525] rounded"> + <div className="px-2 py-1.5 border-b border-[rgba(117,170,252,0.1)]"> + <input + type="text" + value={directiveSearch} + onChange={(e) => setDirectiveSearch(e.target.value)} + placeholder="Search directives..." + autoFocus + className="w-full bg-transparent border-none outline-none text-[10px] font-mono text-[#75aafc] placeholder-[#556677]" + /> + </div> + <div className="max-h-32 overflow-y-auto"> + {directives.length === 0 ? ( + <div className="px-3 py-2 text-[10px] font-mono text-[#556677]"> + No directives available + </div> + ) : ( + (() => { + const filtered = directives.filter((d) => + d.title.toLowerCase().includes(directiveSearch.toLowerCase()) + ); + if (filtered.length === 0) { + return ( + <div className="px-3 py-2 text-[10px] font-mono text-[#556677]"> + No matching directives + </div> + ); + } + return filtered.map((d) => { + const isLinked = d.id === order.directiveId; + const statusColors: Record<string, string> = { + draft: "text-[#556677] border-[#2a3a5a]", + active: "text-emerald-400 border-emerald-800", + idle: "text-[#7788aa] border-[#2a3a5a]", + paused: "text-yellow-400 border-yellow-800", + archived: "text-[#556677] border-[#2a3a5a]", + }; + const sColor = statusColors[d.status] || statusColors.draft; + return ( + <button + key={d.id} + type="button" + onClick={() => handleLinkDirective(d.id)} + className={`w-full text-left px-3 py-1.5 text-[10px] font-mono hover:bg-[rgba(117,170,252,0.1)] border-b border-[rgba(117,170,252,0.1)] last:border-b-0 ${ + isLinked ? "bg-[rgba(117,170,252,0.08)] text-white" : "text-[#9bc3ff]" + }`} + > + <div className="flex items-center gap-1.5"> + <span className={`shrink-0 text-[8px] font-mono ${sColor} border rounded px-1 py-0.5 uppercase`}> + {d.status} + </span> + <span className="truncate">{d.title}</span> + {isLinked && ( + <span className="shrink-0 text-[8px] text-emerald-400">●</span> + )} + </div> + {d.repositoryUrl && ( + <div className="text-[8px] text-[#556677] truncate mt-0.5"> + {d.repositoryUrl} + </div> + )} + </button> + ); + }); + })() + )} + </div> </div> )} </div> diff --git a/makima/frontend/src/components/orders/OrderList.tsx b/makima/frontend/src/components/orders/OrderList.tsx index 213a332..0ebd18d 100644 --- a/makima/frontend/src/components/orders/OrderList.tsx +++ b/makima/frontend/src/components/orders/OrderList.tsx @@ -4,7 +4,7 @@ import type { Order, OrderStatus, OrderPriority, OrderType } from "../../lib/api const STATUS_BADGE: Record<OrderStatus, { color: string; label: string }> = { open: { color: "text-[#75aafc] border-[rgba(117,170,252,0.4)]", label: "OPEN" }, in_progress: { color: "text-yellow-400 border-yellow-800", label: "IN PROGRESS" }, - under_review: { color: "text-purple-400 border-purple-800", label: "UNDER REVIEW" }, + under_review: { color: "text-purple-400 border-purple-800", label: "REVIEW" }, done: { color: "text-emerald-400 border-emerald-800", label: "DONE" }, archived: { color: "text-[#556677] border-[#2a3a5a]", label: "ARCHIVED" }, }; diff --git a/makima/frontend/src/routes/contracts.tsx b/makima/frontend/src/routes/contracts.tsx index 6d838ab..7046f66 100644 --- a/makima/frontend/src/routes/contracts.tsx +++ b/makima/frontend/src/routes/contracts.tsx @@ -542,7 +542,7 @@ function ContractsPageContent() { </div> {/* Right: Detail or Create */} - <div className="flex-1 overflow-hidden flex flex-col"> + <div className="flex-1 overflow-hidden flex flex-col min-h-0"> {error && ( <div className="p-3 bg-red-400/10 border border-red-400/30 text-red-400 font-mono text-sm"> {error} diff --git a/makima/src/db/models.rs b/makima/src/db/models.rs index e53dcce..f9a34b8 100644 --- a/makima/src/db/models.rs +++ b/makima/src/db/models.rs @@ -2860,6 +2860,8 @@ pub struct CreateDirectiveStepRequest { #[serde(default)] pub order_index: i32, pub generation: Option<i32>, + /// Optional order ID to auto-link this step to an order. + #[serde(default)] pub order_id: Option<Uuid>, } @@ -2892,13 +2894,13 @@ pub struct Order { pub description: Option<String>, /// Priority: critical, high, medium, low, none pub priority: String, - /// Status: open, in_progress, done, archived + /// Status: open, in_progress, under_review, done, archived pub status: String, /// Type of work: feature, bug, spike, chore, improvement pub order_type: String, /// Flexible labels as JSON array of strings pub labels: serde_json::Value, - /// Linked directive (optional) + /// Linked directive (required for new orders, nullable for legacy rows) pub directive_id: Option<Uuid>, /// Linked directive step (optional) pub directive_step_id: Option<Uuid>, @@ -2958,7 +2960,7 @@ pub struct OrderListResponse { #[derive(Debug, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct OrderListQuery { - /// Filter by status (e.g., "open", "in_progress", "done", "archived") + /// Filter by status (e.g., "open", "in_progress", "under_review", "done", "archived") pub status: Option<String>, /// Filter by order type (e.g., "feature", "bug", "spike", "chore", "improvement") #[serde(rename = "type")] diff --git a/makima/src/db/repository.rs b/makima/src/db/repository.rs index bc61bee..c7b0a1f 100644 --- a/makima/src/db/repository.rs +++ b/makima/src/db/repository.rs @@ -5400,6 +5400,7 @@ pub async fn create_directive_step( req: CreateDirectiveStepRequest, ) -> Result<DirectiveStep, sqlx::Error> { let generation = req.generation.unwrap_or(1); + let order_id = req.order_id; let step = sqlx::query_as::<_, DirectiveStep>( r#" INSERT INTO directive_steps (directive_id, name, description, task_plan, depends_on, order_index, generation) @@ -5417,15 +5418,15 @@ pub async fn create_directive_step( .fetch_one(pool) .await?; - // Link the order to this step if an order_id was provided - if let Some(order_id) = req.order_id { - let _ = sqlx::query( - "UPDATE orders SET directive_step_id = $1, updated_at = NOW() WHERE id = $2", + // If an order_id was provided, auto-link the order to this step + if let Some(oid) = order_id { + sqlx::query( + r#"UPDATE orders SET directive_step_id = $1, updated_at = NOW() WHERE id = $2"#, ) .bind(step.id) - .bind(order_id) + .bind(oid) .execute(pool) - .await; + .await?; } Ok(step) @@ -6331,19 +6332,21 @@ pub async fn convert_order_to_step( // Order Pickup // ============================================================================= -/// Get available orders for pickup: open orders with no directive assigned, +/// Get available orders for pickup: open orders with no directive assigned +/// OR orders already linked to this specific directive that are not yet done, /// sorted by priority (critical first) then creation date. pub async fn get_available_orders_for_pickup( pool: &PgPool, owner_id: Uuid, + directive_id: Uuid, ) -> Result<Vec<Order>, sqlx::Error> { sqlx::query_as::<_, Order>( r#" SELECT * FROM orders WHERE owner_id = $1 - AND status = 'open' - AND directive_id IS NULL + AND status IN ('open', 'in_progress') + AND (directive_id IS NULL OR directive_id = $2) ORDER BY CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 @@ -6354,6 +6357,7 @@ pub async fn get_available_orders_for_pickup( "#, ) .bind(owner_id) + .bind(directive_id) .fetch_all(pool) .await } @@ -6382,3 +6386,71 @@ pub async fn bulk_link_orders_to_directive( Ok(result.rows_affected() as i64) } +/// Bulk update order status for a set of order IDs. +/// Returns the count of updated rows. +pub async fn bulk_update_order_status( + pool: &PgPool, + owner_id: Uuid, + order_ids: &[Uuid], + status: &str, +) -> Result<i64, sqlx::Error> { + let result = sqlx::query( + r#"UPDATE orders SET status = $1, updated_at = NOW() + WHERE id = ANY($2) AND owner_id = $3"#, + ) + .bind(status) + .bind(order_ids) + .bind(owner_id) + .execute(pool) + .await?; + Ok(result.rows_affected() as i64) +} + +/// Get orders linked to a specific directive step. +pub async fn get_orders_by_step_id( + pool: &PgPool, + step_id: Uuid, +) -> Result<Vec<Order>, sqlx::Error> { + sqlx::query_as::<_, Order>( + r#"SELECT * FROM orders WHERE directive_step_id = $1"#, + ) + .bind(step_id) + .fetch_all(pool) + .await +} + +/// Reconcile directive orders by checking linked step statuses. +/// - Orders linked to completed steps are marked "done" +/// - Orders linked to running/ready steps are marked "under_review" +/// Returns the count of orders updated. +pub async fn reconcile_directive_orders( + pool: &PgPool, + owner_id: Uuid, + directive_id: Uuid, +) -> Result<i64, sqlx::Error> { + let rows: Vec<(Uuid,)> = sqlx::query_as( + r#" + UPDATE orders o + SET status = CASE + WHEN ds.status = 'completed' THEN 'done' + WHEN ds.status IN ('running', 'ready') THEN 'under_review' + ELSE o.status + END, + updated_at = NOW() + FROM directive_steps ds + WHERE o.directive_step_id = ds.id + AND o.directive_id = $1 + AND o.owner_id = $2 + AND o.status NOT IN ('done', 'archived') + AND ds.status IN ('completed', 'running', 'ready') + RETURNING o.id + "#, + ) + .bind(directive_id) + .bind(owner_id) + .fetch_all(pool) + .await?; + + Ok(rows.len() as i64) +} + diff --git a/makima/src/orchestration/directive.rs b/makima/src/orchestration/directive.rs index 0a812ce..eb157a3 100644 --- a/makima/src/orchestration/directive.rs +++ b/makima/src/orchestration/directive.rs @@ -245,6 +245,20 @@ impl DirectiveOrchestrator { ..Default::default() }; repository::update_directive_step(&self.pool, step.step_id, update).await?; + + // Mark linked orders as done + if let Ok(linked_orders) = repository::get_orders_by_step_id(&self.pool, step.step_id).await { + for order in linked_orders { + if order.status != "done" && order.status != "archived" { + let order_update = crate::db::models::UpdateOrderRequest { + status: Some("done".to_string()), + ..Default::default() + }; + let _ = repository::update_order(&self.pool, order.owner_id, order.id, order_update).await; + } + } + } + repository::advance_directive_ready_steps(&self.pool, step.directive_id) .await?; repository::check_directive_idle(&self.pool, step.directive_id).await?; @@ -961,11 +975,7 @@ pub async fn trigger_completion_task( }) .collect(); - let prompt = if directive.pr_url.is_some() { - build_verification_prompt(&directive, &directive_branch, base_branch) - } else { - build_completion_prompt(&directive, &step_tasks, &step_branches, &directive_branch, base_branch) - }; + let prompt = build_completion_prompt(&directive, &step_tasks, &step_branches, &directive_branch, base_branch); let task_name = if directive.pr_url.is_some() { format!("Update PR: {}", directive.title) @@ -1330,12 +1340,71 @@ fn build_completion_prompt( .collect::<Vec<_>>() .join("\n"); - if directive.pr_url.is_some() { - // Re-completion: PR already exists, merge new branches into existing PR branch + if let Some(ref pr_url) = directive.pr_url { + // Re-completion: PR already exists — but it may have been merged or closed. + // We must check the PR state first and handle accordingly. format!( r#"You are updating an existing PR for directive "{title}". -The PR branch `{directive_branch}` already exists. Merge any new step branches into it. +IMPORTANT: The previous PR may have been merged or closed. You MUST check its state first. + +## Step 1: Check PR state + +Run this command to check the PR state: +``` +gh pr view {pr_url} --json state --jq '.state' +``` + +## If the PR state is MERGED or CLOSED: + +The previous PR was already merged/closed. You need to create a NEW PR with a fresh branch. + +Goal: {goal} + +Steps completed: +{step_summary} + +1. Clear the old PR URL: +``` +makima directive update --pr-url "" +``` + +2. Create a fresh branch with a timestamp suffix to avoid collision: +``` +git fetch origin +NEW_BRANCH="{directive_branch}-v$(date +%s)" +git checkout -b "$NEW_BRANCH" origin/{base_branch} +{merge_commands} +git push -u origin "$NEW_BRANCH" +``` + +3. Generate a descriptive PR title and create a new PR: + +Based on the steps completed above, generate a descriptive PR title that summarizes the actual changes (not just the directive name "{title}"). The title should: +- Be concise (under 72 characters) +- Describe WHAT was done, not just the project name +- Use conventional commit style if appropriate (feat:, fix:, refactor:, etc.) + +Then create the PR: +``` +gh pr create --title "<YOUR_GENERATED_TITLE>" --body "{pr_body}" --head "$NEW_BRANCH" --base {base_branch} +``` +Replace <YOUR_GENERATED_TITLE> with the concise descriptive title you generated. + +4. Store the new PR URL: +``` +makima directive update --pr-url "<URL_FROM_GH_PR_CREATE>" +``` +Replace the URL with the actual PR URL from the `gh pr create` output. This step is CRITICAL. + +5. Update the directive pr_branch to the new branch name: +``` +makima directive update --pr-branch "$NEW_BRANCH" +``` + +## If the PR state is OPEN: + +The PR is still open. Merge new step branches into the existing PR branch. Steps completed: {step_summary} @@ -1352,9 +1421,17 @@ git push origin {directive_branch} Already-merged branches will be a no-op. If there are merge conflicts, resolve them sensibly. "#, title = directive.title, + goal = directive.goal, + pr_url = pr_url, directive_branch = directive_branch, + base_branch = base_branch, step_summary = step_summary, merge_commands = merge_commands, + pr_body = format!( + "## Directive\\n\\n{}\\n\\n## Steps\\n\\n{}", + directive.goal.replace('\n', "\\n").replace('"', "\\\""), + step_summary.replace('\n', "\\n").replace('"', "\\\""), + ), ) } else { // First completion: create new PR @@ -1374,10 +1451,18 @@ git checkout -b {directive_branch} origin/{base_branch} git push -u origin {directive_branch} ``` -Then create the PR: +Then generate a descriptive PR title and create the PR: + +Based on the steps completed above, generate a descriptive PR title that summarizes the actual changes (not just the directive name "{title}"). The title should: +- Be concise (under 72 characters) +- Describe WHAT was done, not just the project name +- Use conventional commit style if appropriate (feat:, fix:, refactor:, etc.) +For example, instead of "soryu-co/soryu - makima" use something like "Fix order lifecycle, PR update, and contracts overflow". + ``` -gh pr create --title "{title}" --body "{pr_body}" --head {directive_branch} --base {base_branch} +gh pr create --title "<YOUR_GENERATED_TITLE>" --body "{pr_body}" --head {directive_branch} --base {base_branch} ``` +Replace <YOUR_GENERATED_TITLE> with the concise descriptive title you generated. IMPORTANT: After creating the PR, you MUST store the PR URL so the directive system can track it. @@ -1437,9 +1522,15 @@ Done — the PR already exists. git ls-remote --heads origin {pr_branch} ``` -4. If the branch exists, create the PR: +4. If the branch exists, generate a descriptive PR title and create the PR: + +Based on the branch name and directive "{title}", generate a descriptive PR title that summarizes the actual changes. The title should: +- Be concise (under 72 characters) +- Describe WHAT was done, not just the project name +- Use conventional commit style if appropriate (feat:, fix:, refactor:, etc.) + ``` -gh pr create --title "{title}" --body "Directive PR verification — auto-created" --head {pr_branch} --base {base_branch} +gh pr create --title "<YOUR_GENERATED_TITLE>" --body "Directive PR verification — auto-created" --head {pr_branch} --base {base_branch} ``` Then store the resulting URL: ``` @@ -1508,7 +1599,7 @@ pub fn build_order_pickup_prompt( } // ── Orders being picked up ─────────────────────────────────── - prompt.push_str("== ORDERS AVAILABLE FOR PICKUP ==\n"); + prompt.push_str("== ORDERS AVAILABLE FOR PLANNING ==\n"); prompt.push_str("The following open orders have been linked to this directive. \ Review them and create steps to fulfil them.\n\n"); for (i, order) in orders.iter().enumerate() { @@ -1652,7 +1743,7 @@ Submit steps using generation {generation}: (Use --depends-on "uuid1,uuid2" for dependencies) Or batch: - makima directive batch-add-steps --json '[{{"name":"...","description":"...","taskPlan":"...","dependsOn":[],"orderIndex":0,"generation":{generation},"orderId":"<uuid-of-order>"}}]' + makima directive batch-add-steps --json '[{{"name":"...","description":"...","taskPlan":"...","dependsOn":[],"orderIndex":0,"generation":{generation},"orderId":"<order-uuid>"}}]' DEPENDENCY WORKTREE CONTINUATION: Each step runs in its own git worktree. How that worktree is initialised depends on dependsOn: diff --git a/makima/src/server/handlers/directives.rs b/makima/src/server/handlers/directives.rs index fd7ae81..b4b438a 100644 --- a/makima/src/server/handlers/directives.rs +++ b/makima/src/server/handlers/directives.rs @@ -1063,58 +1063,21 @@ pub async fn pick_up_orders( } }; - // Reconcile existing orders before picking up new ones - if let Ok(existing_orders) = repository::list_orders( - pool, - auth.owner_id, - Some("in_progress"), - None, - None, - Some(id), - None, - ) - .await - { - for order in &existing_orders { - if let Some(step_id) = order.directive_step_id { - if let Ok(Some(step)) = repository::get_directive_step(pool, step_id).await { - match step.status.as_str() { - "completed" => { - let order_update = UpdateOrderRequest { - status: Some("done".to_string()), - ..Default::default() - }; - let _ = repository::update_order( - pool, - auth.owner_id, - order.id, - order_update, - ) - .await; - } - "running" | "ready" | "pending" => { - let order_update = UpdateOrderRequest { - status: Some("under_review".to_string()), - ..Default::default() - }; - let _ = repository::update_order( - pool, - auth.owner_id, - order.id, - order_update, - ) - .await; - } - "failed" => { /* keep as in_progress for re-planning */ } - _ => {} - } - } + // Reconcile existing orders: mark done if step completed, under_review if step in progress + match repository::reconcile_directive_orders(pool, auth.owner_id, id).await { + Ok(count) => { + if count > 0 { + tracing::info!("Reconciled {} orders for directive {}", count, id); } } + Err(e) => { + tracing::warn!("Failed to reconcile directive orders: {}", e); + // Non-fatal: continue with pickup even if reconciliation fails + } } // Fetch available orders - let orders = match repository::get_available_orders_for_pickup(pool, auth.owner_id).await { + let orders = match repository::get_available_orders_for_pickup(pool, auth.owner_id, id).await { Ok(o) => o, Err(e) => { tracing::error!("Failed to fetch available orders: {}", e); @@ -1129,7 +1092,7 @@ pub async fn pick_up_orders( // If no orders available, return early if orders.is_empty() { return Json(PickUpOrdersResponse { - message: "No orders available to pick up".to_string(), + message: "No orders available to plan".to_string(), order_count: 0, task_id: None, }) @@ -1176,6 +1139,13 @@ pub async fn pick_up_orders( .into_response(); } + // Mark picked-up orders as in_progress + if let Err(e) = + repository::bulk_update_order_status(pool, auth.owner_id, &order_ids, "in_progress").await + { + tracing::warn!("Failed to update order status to in_progress: {}", e); + } + // Create the planning task let req = CreateTaskRequest { contract_id: None, @@ -1250,7 +1220,7 @@ pub async fn pick_up_orders( let _ = repository::advance_directive_ready_steps(pool, id).await; Json(PickUpOrdersResponse { - message: format!("Picked up {} orders", order_count), + message: format!("Planning {} orders", order_count), order_count, task_id: Some(task.id), }) diff --git a/makima/src/server/handlers/orders.rs b/makima/src/server/handlers/orders.rs index cddf6a6..1251f79 100644 --- a/makima/src/server/handlers/orders.rs +++ b/makima/src/server/handlers/orders.rs @@ -81,13 +81,14 @@ pub async fn list_orders( } } -/// Create a new order. +/// Create a new order. A valid directive_id is required. #[utoipa::path( post, path = "/api/v1/orders", request_body = CreateOrderRequest, responses( (status = 201, description = "Order created", body = Order), + (status = 400, description = "Invalid directive_id", body = ApiError), (status = 401, description = "Unauthorized", body = ApiError), (status = 503, description = "Database not configured", body = ApiError), ), @@ -107,6 +108,29 @@ pub async fn create_order( .into_response(); }; + // Validate the directive exists and belongs to this owner. + // directive_id is required by the CreateOrderRequest struct (Uuid, not Option<Uuid>). + match repository::get_directive_for_owner(pool, auth.owner_id, req.directive_id).await { + Ok(Some(_)) => {} + Ok(None) => { + return ( + StatusCode::BAD_REQUEST, + Json(ApiError::new( + "INVALID_DIRECTIVE", + "directive_id is required and must reference a valid directive owned by you", + )), + ) + .into_response(); + } + Err(e) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("VALIDATION_FAILED", &e.to_string())), + ) + .into_response(); + } + } + match repository::create_order(pool, auth.owner_id, req).await { Ok(order) => (StatusCode::CREATED, Json(order)).into_response(), Err(e) => { |
