diff --git a/src/dashboard/app.py b/src/dashboard/app.py
index 2e3b074a..1f2084a7 100644
--- a/src/dashboard/app.py
+++ b/src/dashboard/app.py
@@ -38,6 +38,8 @@ from dashboard.routes.chat_api import router as chat_api_router
from dashboard.routes.thinking import router as thinking_router
from dashboard.routes.calm import router as calm_router
from dashboard.routes.swarm import router as swarm_router
+from dashboard.routes.tasks import router as tasks_router
+from dashboard.routes.work_orders import router as work_orders_router
from dashboard.routes.system import router as system_router
from dashboard.routes.paperclip import router as paperclip_router
from infrastructure.router.api import router as cascade_router
@@ -333,6 +335,8 @@ app.include_router(chat_api_router)
app.include_router(thinking_router)
app.include_router(calm_router)
app.include_router(swarm_router)
+app.include_router(tasks_router)
+app.include_router(work_orders_router)
app.include_router(system_router)
app.include_router(paperclip_router)
app.include_router(cascade_router)
diff --git a/src/dashboard/models/database.py b/src/dashboard/models/database.py
index 10877b05..0994996f 100644
--- a/src/dashboard/models/database.py
+++ b/src/dashboard/models/database.py
@@ -12,6 +12,11 @@ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
+def create_tables():
+ """Create all tables defined by models that have imported Base."""
+ Base.metadata.create_all(bind=engine)
+
+
def get_db():
db = SessionLocal()
try:
diff --git a/src/dashboard/routes/calm.py b/src/dashboard/routes/calm.py
index ff842e5c..46ebe077 100644
--- a/src/dashboard/routes/calm.py
+++ b/src/dashboard/routes/calm.py
@@ -8,9 +8,12 @@ from fastapi.responses import HTMLResponse
from sqlalchemy.orm import Session
from dashboard.models.calm import JournalEntry, Task, TaskCertainty, TaskState
-from dashboard.models.database import SessionLocal, engine, get_db
+from dashboard.models.database import SessionLocal, engine, get_db, create_tables
from dashboard.templating import templates
+# Ensure CALM tables exist (safe to call multiple times)
+create_tables()
+
logger = logging.getLogger(__name__)
router = APIRouter(tags=["calm"])
diff --git a/src/dashboard/routes/system.py b/src/dashboard/routes/system.py
index 3651bc5b..b4a3cdca 100644
--- a/src/dashboard/routes/system.py
+++ b/src/dashboard/routes/system.py
@@ -77,11 +77,6 @@ async def self_modify_queue(request: Request):
)
-@router.get("/tasks", response_class=HTMLResponse)
-async def tasks_page(request: Request):
- return templates.TemplateResponse(request, "tasks.html", {"tasks": []})
-
-
@router.get("/swarm/mission-control", response_class=HTMLResponse)
async def mission_control(request: Request):
return templates.TemplateResponse(request, "mission_control.html", {})
@@ -104,11 +99,6 @@ async def hands_page(request: Request):
return templates.TemplateResponse(request, "hands.html", {"executions": []})
-@router.get("/work-orders/queue", response_class=HTMLResponse)
-async def work_orders(request: Request):
- return templates.TemplateResponse(request, "work_orders.html", {"orders": []})
-
-
@router.get("/creative/ui", response_class=HTMLResponse)
async def creative_ui(request: Request):
return templates.TemplateResponse(request, "creative.html", {})
diff --git a/src/dashboard/routes/tasks.py b/src/dashboard/routes/tasks.py
new file mode 100644
index 00000000..0b1f9403
--- /dev/null
+++ b/src/dashboard/routes/tasks.py
@@ -0,0 +1,375 @@
+"""Task Queue routes — SQLite-backed CRUD for the task management dashboard."""
+
+import logging
+import sqlite3
+import uuid
+from datetime import datetime
+from pathlib import Path
+from typing import Optional
+
+from fastapi import APIRouter, HTTPException, Request, Form
+from fastapi.responses import HTMLResponse, JSONResponse
+
+from dashboard.templating import templates
+
+logger = logging.getLogger(__name__)
+
+router = APIRouter(tags=["tasks"])
+
+# ---------------------------------------------------------------------------
+# Database helpers
+# ---------------------------------------------------------------------------
+
+DB_PATH = Path("data/tasks.db")
+
+VALID_STATUSES = {
+ "pending_approval", "approved", "running", "paused",
+ "completed", "vetoed", "failed", "backlogged",
+}
+VALID_PRIORITIES = {"low", "normal", "high", "urgent"}
+
+
+def _get_db() -> sqlite3.Connection:
+ DB_PATH.parent.mkdir(parents=True, exist_ok=True)
+ conn = sqlite3.connect(str(DB_PATH))
+ conn.row_factory = sqlite3.Row
+ conn.execute("""
+ CREATE TABLE IF NOT EXISTS tasks (
+ id TEXT PRIMARY KEY,
+ title TEXT NOT NULL,
+ description TEXT DEFAULT '',
+ status TEXT DEFAULT 'pending_approval',
+ priority TEXT DEFAULT 'normal',
+ assigned_to TEXT DEFAULT '',
+ created_by TEXT DEFAULT 'operator',
+ result TEXT DEFAULT '',
+ created_at TEXT DEFAULT (datetime('now')),
+ completed_at TEXT
+ )
+ """)
+ conn.commit()
+ return conn
+
+
+def _row_to_dict(row: sqlite3.Row) -> dict:
+ return dict(row)
+
+
+class _EnumLike:
+ """Thin wrapper so Jinja templates can use task.status.value."""
+
+ def __init__(self, v: str):
+ self.value = v
+
+ def __str__(self):
+ return self.value
+
+ def __eq__(self, other):
+ if isinstance(other, str):
+ return self.value == other
+ return NotImplemented
+
+
+class _TaskView:
+ """Lightweight view object for Jinja template rendering."""
+
+ def __init__(self, row: dict):
+ self.id = row["id"]
+ self.title = row.get("title", "")
+ self.description = row.get("description", "")
+ self.status = _EnumLike(row.get("status", "pending_approval"))
+ self.priority = _EnumLike(row.get("priority", "normal"))
+ self.assigned_to = row.get("assigned_to", "")
+ self.created_by = row.get("created_by", "operator")
+ self.result = row.get("result", "")
+ self.created_at = row.get("created_at", "")
+ self.completed_at = row.get("completed_at")
+ self.steps = [] # reserved for future use
+
+
+# ---------------------------------------------------------------------------
+# Page routes
+# ---------------------------------------------------------------------------
+
+@router.get("/tasks", response_class=HTMLResponse)
+async def tasks_page(request: Request):
+ """Render the main task queue page with 3-column layout."""
+ db = _get_db()
+ try:
+ pending = [_TaskView(_row_to_dict(r)) for r in db.execute(
+ "SELECT * FROM tasks WHERE status IN ('pending_approval') ORDER BY created_at DESC"
+ ).fetchall()]
+ active = [_TaskView(_row_to_dict(r)) for r in db.execute(
+ "SELECT * FROM tasks WHERE status IN ('approved','running','paused') ORDER BY created_at DESC"
+ ).fetchall()]
+ completed = [_TaskView(_row_to_dict(r)) for r in db.execute(
+ "SELECT * FROM tasks WHERE status IN ('completed','vetoed','failed') ORDER BY completed_at DESC LIMIT 50"
+ ).fetchall()]
+ finally:
+ db.close()
+
+ return templates.TemplateResponse(request, "tasks.html", {
+ "pending_count": len(pending),
+ "pending": pending,
+ "active": active,
+ "completed": completed,
+ "agents": [], # no agent roster wired yet
+ "pre_assign": "",
+ })
+
+
+# ---------------------------------------------------------------------------
+# HTMX partials (polled by the template)
+# ---------------------------------------------------------------------------
+
+@router.get("/tasks/pending", response_class=HTMLResponse)
+async def tasks_pending(request: Request):
+ db = _get_db()
+ try:
+ rows = db.execute(
+ "SELECT * FROM tasks WHERE status='pending_approval' ORDER BY created_at DESC"
+ ).fetchall()
+ finally:
+ db.close()
+ tasks = [_TaskView(_row_to_dict(r)) for r in rows]
+ parts = []
+ for task in tasks:
+ parts.append(templates.TemplateResponse(
+ request, "partials/task_card.html", {"task": task}
+ ).body.decode())
+ if not parts:
+ return HTMLResponse('
No pending tasks
')
+ return HTMLResponse("".join(parts))
+
+
+@router.get("/tasks/active", response_class=HTMLResponse)
+async def tasks_active(request: Request):
+ db = _get_db()
+ try:
+ rows = db.execute(
+ "SELECT * FROM tasks WHERE status IN ('approved','running','paused') ORDER BY created_at DESC"
+ ).fetchall()
+ finally:
+ db.close()
+ tasks = [_TaskView(_row_to_dict(r)) for r in rows]
+ parts = []
+ for task in tasks:
+ parts.append(templates.TemplateResponse(
+ request, "partials/task_card.html", {"task": task}
+ ).body.decode())
+ if not parts:
+ return HTMLResponse('
No active tasks
')
+ return HTMLResponse("".join(parts))
+
+
+@router.get("/tasks/completed", response_class=HTMLResponse)
+async def tasks_completed(request: Request):
+ db = _get_db()
+ try:
+ rows = db.execute(
+ "SELECT * FROM tasks WHERE status IN ('completed','vetoed','failed') ORDER BY completed_at DESC LIMIT 50"
+ ).fetchall()
+ finally:
+ db.close()
+ tasks = [_TaskView(_row_to_dict(r)) for r in rows]
+ parts = []
+ for task in tasks:
+ parts.append(templates.TemplateResponse(
+ request, "partials/task_card.html", {"task": task}
+ ).body.decode())
+ if not parts:
+ return HTMLResponse('
No completed tasks yet
')
+ return HTMLResponse("".join(parts))
+
+
+# ---------------------------------------------------------------------------
+# Form-based create (used by the modal in tasks.html)
+# ---------------------------------------------------------------------------
+
+@router.post("/tasks/create", response_class=HTMLResponse)
+async def create_task_form(
+ request: Request,
+ title: str = Form(...),
+ description: str = Form(""),
+ priority: str = Form("normal"),
+ assigned_to: str = Form(""),
+):
+ """Create a task from the modal form and return a task card partial."""
+ task_id = str(uuid.uuid4())
+ now = datetime.utcnow().isoformat()
+ priority = priority if priority in VALID_PRIORITIES else "normal"
+
+ db = _get_db()
+ try:
+ db.execute(
+ "INSERT INTO tasks (id, title, description, priority, assigned_to, created_at) VALUES (?, ?, ?, ?, ?, ?)",
+ (task_id, title, description, priority, assigned_to, now),
+ )
+ db.commit()
+ row = db.execute("SELECT * FROM tasks WHERE id=?", (task_id,)).fetchone()
+ finally:
+ db.close()
+
+ task = _TaskView(_row_to_dict(row))
+ return templates.TemplateResponse(request, "partials/task_card.html", {"task": task})
+
+
+# ---------------------------------------------------------------------------
+# Task action endpoints (approve, veto, modify, pause, cancel, retry)
+# ---------------------------------------------------------------------------
+
+@router.post("/tasks/{task_id}/approve", response_class=HTMLResponse)
+async def approve_task(request: Request, task_id: str):
+ return await _set_status(request, task_id, "approved")
+
+
+@router.post("/tasks/{task_id}/veto", response_class=HTMLResponse)
+async def veto_task(request: Request, task_id: str):
+ return await _set_status(request, task_id, "vetoed")
+
+
+@router.post("/tasks/{task_id}/pause", response_class=HTMLResponse)
+async def pause_task(request: Request, task_id: str):
+ return await _set_status(request, task_id, "paused")
+
+
+@router.post("/tasks/{task_id}/cancel", response_class=HTMLResponse)
+async def cancel_task(request: Request, task_id: str):
+ return await _set_status(request, task_id, "vetoed")
+
+
+@router.post("/tasks/{task_id}/retry", response_class=HTMLResponse)
+async def retry_task(request: Request, task_id: str):
+ return await _set_status(request, task_id, "approved")
+
+
+@router.post("/tasks/{task_id}/modify", response_class=HTMLResponse)
+async def modify_task(
+ request: Request,
+ task_id: str,
+ title: str = Form(...),
+ description: str = Form(""),
+):
+ db = _get_db()
+ try:
+ db.execute(
+ "UPDATE tasks SET title=?, description=? WHERE id=?",
+ (title, description, task_id),
+ )
+ db.commit()
+ row = db.execute("SELECT * FROM tasks WHERE id=?", (task_id,)).fetchone()
+ finally:
+ db.close()
+ if not row:
+ raise HTTPException(404, "Task not found")
+ task = _TaskView(_row_to_dict(row))
+ return templates.TemplateResponse(request, "partials/task_card.html", {"task": task})
+
+
+async def _set_status(request: Request, task_id: str, new_status: str):
+ """Helper to update status and return refreshed task card."""
+ completed_at = datetime.utcnow().isoformat() if new_status in ("completed", "vetoed", "failed") else None
+ db = _get_db()
+ try:
+ db.execute(
+ "UPDATE tasks SET status=?, completed_at=COALESCE(?, completed_at) WHERE id=?",
+ (new_status, completed_at, task_id),
+ )
+ db.commit()
+ row = db.execute("SELECT * FROM tasks WHERE id=?", (task_id,)).fetchone()
+ finally:
+ db.close()
+ if not row:
+ raise HTTPException(404, "Task not found")
+ task = _TaskView(_row_to_dict(row))
+ return templates.TemplateResponse(request, "partials/task_card.html", {"task": task})
+
+
+# ---------------------------------------------------------------------------
+# JSON API (for programmatic access / Timmy's tool calls)
+# ---------------------------------------------------------------------------
+
+@router.post("/api/tasks", response_class=JSONResponse, status_code=201)
+async def api_create_task(request: Request):
+ """Create a task via JSON API."""
+ body = await request.json()
+ title = body.get("title")
+ if not title:
+ raise HTTPException(422, "title is required")
+
+ task_id = str(uuid.uuid4())
+ now = datetime.utcnow().isoformat()
+ priority = body.get("priority", "normal")
+ if priority not in VALID_PRIORITIES:
+ priority = "normal"
+
+ db = _get_db()
+ try:
+ db.execute(
+ "INSERT INTO tasks (id, title, description, priority, assigned_to, created_by, created_at) "
+ "VALUES (?, ?, ?, ?, ?, ?, ?)",
+ (
+ task_id,
+ title,
+ body.get("description", ""),
+ priority,
+ body.get("assigned_to", ""),
+ body.get("created_by", "operator"),
+ now,
+ ),
+ )
+ db.commit()
+ row = db.execute("SELECT * FROM tasks WHERE id=?", (task_id,)).fetchone()
+ finally:
+ db.close()
+
+ return JSONResponse(_row_to_dict(row), status_code=201)
+
+
+@router.get("/api/tasks", response_class=JSONResponse)
+async def api_list_tasks():
+ """List all tasks as JSON."""
+ db = _get_db()
+ try:
+ rows = db.execute("SELECT * FROM tasks ORDER BY created_at DESC").fetchall()
+ finally:
+ db.close()
+ return JSONResponse([_row_to_dict(r) for r in rows])
+
+
+@router.patch("/api/tasks/{task_id}/status", response_class=JSONResponse)
+async def api_update_status(task_id: str, request: Request):
+ """Update task status via JSON API."""
+ body = await request.json()
+ new_status = body.get("status")
+ if not new_status or new_status not in VALID_STATUSES:
+ raise HTTPException(422, f"Invalid status. Must be one of: {VALID_STATUSES}")
+
+ completed_at = datetime.utcnow().isoformat() if new_status in ("completed", "vetoed", "failed") else None
+ db = _get_db()
+ try:
+ db.execute(
+ "UPDATE tasks SET status=?, completed_at=COALESCE(?, completed_at) WHERE id=?",
+ (new_status, completed_at, task_id),
+ )
+ db.commit()
+ row = db.execute("SELECT * FROM tasks WHERE id=?", (task_id,)).fetchone()
+ finally:
+ db.close()
+ if not row:
+ raise HTTPException(404, "Task not found")
+ return JSONResponse(_row_to_dict(row))
+
+
+@router.delete("/api/tasks/{task_id}", response_class=JSONResponse)
+async def api_delete_task(task_id: str):
+ """Delete a task."""
+ db = _get_db()
+ try:
+ cursor = db.execute("DELETE FROM tasks WHERE id=?", (task_id,))
+ db.commit()
+ finally:
+ db.close()
+ if cursor.rowcount == 0:
+ raise HTTPException(404, "Task not found")
+ return JSONResponse({"success": True, "id": task_id})
diff --git a/src/dashboard/routes/work_orders.py b/src/dashboard/routes/work_orders.py
new file mode 100644
index 00000000..1365f3e4
--- /dev/null
+++ b/src/dashboard/routes/work_orders.py
@@ -0,0 +1,239 @@
+"""Work Orders routes — SQLite-backed submit/review/execute pipeline."""
+
+import logging
+import sqlite3
+import uuid
+from datetime import datetime
+from pathlib import Path
+
+from fastapi import APIRouter, HTTPException, Request, Form
+from fastapi.responses import HTMLResponse, JSONResponse
+
+from dashboard.templating import templates
+
+logger = logging.getLogger(__name__)
+
+router = APIRouter(tags=["work-orders"])
+
+DB_PATH = Path("data/work_orders.db")
+
+PRIORITIES = ["low", "medium", "high", "critical"]
+CATEGORIES = ["bug", "feature", "suggestion", "maintenance", "security"]
+VALID_STATUSES = {"submitted", "triaged", "approved", "in_progress", "completed", "rejected"}
+
+
+def _get_db() -> sqlite3.Connection:
+ DB_PATH.parent.mkdir(parents=True, exist_ok=True)
+ conn = sqlite3.connect(str(DB_PATH))
+ conn.row_factory = sqlite3.Row
+ conn.execute("""
+ CREATE TABLE IF NOT EXISTS work_orders (
+ id TEXT PRIMARY KEY,
+ title TEXT NOT NULL,
+ description TEXT DEFAULT '',
+ priority TEXT DEFAULT 'medium',
+ category TEXT DEFAULT 'suggestion',
+ submitter TEXT DEFAULT 'dashboard',
+ related_files TEXT DEFAULT '',
+ status TEXT DEFAULT 'submitted',
+ result TEXT DEFAULT '',
+ rejection_reason TEXT DEFAULT '',
+ created_at TEXT DEFAULT (datetime('now')),
+ completed_at TEXT
+ )
+ """)
+ conn.commit()
+ return conn
+
+
+class _EnumLike:
+ def __init__(self, v: str):
+ self.value = v
+
+ def __str__(self):
+ return self.value
+
+ def __eq__(self, other):
+ if isinstance(other, str):
+ return self.value == other
+ return NotImplemented
+
+
+class _WOView:
+ """View object for Jinja template rendering."""
+
+ def __init__(self, row: dict):
+ self.id = row["id"]
+ self.title = row.get("title", "")
+ self.description = row.get("description", "")
+ self.priority = _EnumLike(row.get("priority", "medium"))
+ self.category = _EnumLike(row.get("category", "suggestion"))
+ self.submitter = row.get("submitter", "dashboard")
+ self.status = _EnumLike(row.get("status", "submitted"))
+ raw_files = row.get("related_files", "")
+ self.related_files = [f.strip() for f in raw_files.split(",") if f.strip()] if raw_files else []
+ self.result = row.get("result", "")
+ self.rejection_reason = row.get("rejection_reason", "")
+ self.created_at = row.get("created_at", "")
+ self.completed_at = row.get("completed_at")
+ self.execution_mode = None
+
+
+def _row_to_dict(row: sqlite3.Row) -> dict:
+ return dict(row)
+
+
+def _query_wos(db, statuses):
+ placeholders = ",".join("?" for _ in statuses)
+ return [
+ _WOView(_row_to_dict(r))
+ for r in db.execute(
+ f"SELECT * FROM work_orders WHERE status IN ({placeholders}) ORDER BY created_at DESC",
+ statuses,
+ ).fetchall()
+ ]
+
+
+# ---------------------------------------------------------------------------
+# Page route
+# ---------------------------------------------------------------------------
+
+@router.get("/work-orders/queue", response_class=HTMLResponse)
+async def work_orders_page(request: Request):
+ db = _get_db()
+ try:
+ pending = _query_wos(db, ["submitted", "triaged"])
+ active = _query_wos(db, ["approved", "in_progress"])
+ completed = _query_wos(db, ["completed"])
+ rejected = _query_wos(db, ["rejected"])
+ finally:
+ db.close()
+
+ return templates.TemplateResponse(request, "work_orders.html", {
+ "pending_count": len(pending),
+ "pending": pending,
+ "active": active,
+ "completed": completed,
+ "rejected": rejected,
+ "priorities": PRIORITIES,
+ "categories": CATEGORIES,
+ })
+
+
+# ---------------------------------------------------------------------------
+# Form submit
+# ---------------------------------------------------------------------------
+
+@router.post("/work-orders/submit", response_class=HTMLResponse)
+async def submit_work_order(
+ request: Request,
+ title: str = Form(...),
+ description: str = Form(""),
+ priority: str = Form("medium"),
+ category: str = Form("suggestion"),
+ submitter: str = Form("dashboard"),
+ related_files: str = Form(""),
+):
+ wo_id = str(uuid.uuid4())
+ now = datetime.utcnow().isoformat()
+ priority = priority if priority in PRIORITIES else "medium"
+ category = category if category in CATEGORIES else "suggestion"
+
+ db = _get_db()
+ try:
+ db.execute(
+ "INSERT INTO work_orders (id, title, description, priority, category, submitter, related_files, created_at) "
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
+ (wo_id, title, description, priority, category, submitter, related_files, now),
+ )
+ db.commit()
+ row = db.execute("SELECT * FROM work_orders WHERE id=?", (wo_id,)).fetchone()
+ finally:
+ db.close()
+
+ wo = _WOView(_row_to_dict(row))
+ return templates.TemplateResponse(request, "partials/work_order_card.html", {"wo": wo})
+
+
+# ---------------------------------------------------------------------------
+# HTMX partials
+# ---------------------------------------------------------------------------
+
+@router.get("/work-orders/queue/pending", response_class=HTMLResponse)
+async def pending_partial(request: Request):
+ db = _get_db()
+ try:
+ wos = _query_wos(db, ["submitted", "triaged"])
+ finally:
+ db.close()
+ if not wos:
+ return HTMLResponse(
+ '
'
+ "No pending work orders.
"
+ )
+ parts = []
+ for wo in wos:
+ parts.append(
+ templates.TemplateResponse(request, "partials/work_order_card.html", {"wo": wo}).body.decode()
+ )
+ return HTMLResponse("".join(parts))
+
+
+@router.get("/work-orders/queue/active", response_class=HTMLResponse)
+async def active_partial(request: Request):
+ db = _get_db()
+ try:
+ wos = _query_wos(db, ["approved", "in_progress"])
+ finally:
+ db.close()
+ if not wos:
+ return HTMLResponse(
+ '
'
+ "No work orders currently in progress.
"
+ )
+ parts = []
+ for wo in wos:
+ parts.append(
+ templates.TemplateResponse(request, "partials/work_order_card.html", {"wo": wo}).body.decode()
+ )
+ return HTMLResponse("".join(parts))
+
+
+# ---------------------------------------------------------------------------
+# Action endpoints
+# ---------------------------------------------------------------------------
+
+async def _update_status(request: Request, wo_id: str, new_status: str, **extra):
+ completed_at = datetime.utcnow().isoformat() if new_status in ("completed", "rejected") else None
+ db = _get_db()
+ try:
+ sets = ["status=?", "completed_at=COALESCE(?, completed_at)"]
+ vals = [new_status, completed_at]
+ for col, val in extra.items():
+ sets.append(f"{col}=?")
+ vals.append(val)
+ vals.append(wo_id)
+ db.execute(f"UPDATE work_orders SET {', '.join(sets)} WHERE id=?", vals)
+ db.commit()
+ row = db.execute("SELECT * FROM work_orders WHERE id=?", (wo_id,)).fetchone()
+ finally:
+ db.close()
+ if not row:
+ raise HTTPException(404, "Work order not found")
+ wo = _WOView(_row_to_dict(row))
+ return templates.TemplateResponse(request, "partials/work_order_card.html", {"wo": wo})
+
+
+@router.post("/work-orders/{wo_id}/approve", response_class=HTMLResponse)
+async def approve_wo(request: Request, wo_id: str):
+ return await _update_status(request, wo_id, "approved")
+
+
+@router.post("/work-orders/{wo_id}/reject", response_class=HTMLResponse)
+async def reject_wo(request: Request, wo_id: str):
+ return await _update_status(request, wo_id, "rejected")
+
+
+@router.post("/work-orders/{wo_id}/execute", response_class=HTMLResponse)
+async def execute_wo(request: Request, wo_id: str):
+ return await _update_status(request, wo_id, "in_progress")
diff --git a/src/dashboard/templates/partials/memory_facts.html b/src/dashboard/templates/partials/memory_facts.html
new file mode 100644
index 00000000..9b659131
--- /dev/null
+++ b/src/dashboard/templates/partials/memory_facts.html
@@ -0,0 +1,13 @@
+{% if facts %}
+