feat: Implement comprehensive project management with new API client, backend routes, and frontend pages for projects and settings.
This commit is contained in:
+100
-19
@@ -1,13 +1,10 @@
|
||||
"""
|
||||
SQLAlchemy ORM models for Foxy Dev Team.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
import enum
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional, List
|
||||
from typing import List
|
||||
|
||||
from sqlalchemy import (
|
||||
String, Text, Integer, DateTime, ForeignKey, Enum, JSON
|
||||
String, Text, Integer, DateTime, ForeignKey, Enum, JSON, Boolean
|
||||
)
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
@@ -43,6 +40,54 @@ class WorkflowType(str, enum.Enum):
|
||||
SYSADMIN_ADJUST = "SYSADMIN_ADJUST"
|
||||
|
||||
|
||||
class ProjectType(str, enum.Enum):
|
||||
SYSTEMD_SERVICE = "SYSTEMD_SERVICE"
|
||||
DOCKER_WEBAPP = "DOCKER_WEBAPP"
|
||||
DOCKER_API = "DOCKER_API"
|
||||
DOCKER_COMPOSE = "DOCKER_COMPOSE"
|
||||
CLI_TOOL = "CLI_TOOL"
|
||||
CRON_JOB = "CRON_JOB"
|
||||
SHELL_SCRIPT = "SHELL_SCRIPT"
|
||||
WORKER_MQ = "WORKER_MQ"
|
||||
SOCKET_IPC = "SOCKET_IPC"
|
||||
WEBHOOK_EVENT = "WEBHOOK_EVENT"
|
||||
LIBRARY_PLUGIN = "LIBRARY_PLUGIN"
|
||||
AGENT_BOT = "AGENT_BOT"
|
||||
TUI_APP = "TUI_APP"
|
||||
MCP_SERVER = "MCP_SERVER"
|
||||
WASM_EDGE = "WASM_EDGE"
|
||||
GRPC_SERVICE = "GRPC_SERVICE"
|
||||
KERNEL_EBPF = "KERNEL_EBPF"
|
||||
SERVERLESS_FAAS = "SERVERLESS_FAAS"
|
||||
DESKTOP_GUI = "DESKTOP_GUI"
|
||||
STATIC_SITE = "STATIC_SITE"
|
||||
|
||||
|
||||
# Project type metadata (emoji, label, description)
|
||||
PROJECT_TYPE_INFO = {
|
||||
ProjectType.SYSTEMD_SERVICE: ("⚙️", "Service système (systemd)", "Démon long-running, démarrage automatique au boot, redémarrage sur erreur."),
|
||||
ProjectType.DOCKER_WEBAPP: ("🌐", "Docker — Web app", "Application frontend ou backend HTTP/HTTPS containerisée."),
|
||||
ProjectType.DOCKER_API: ("🔌", "Docker — API (REST / gRPC)", "Service HTTP headless containerisé, sans interface utilisateur."),
|
||||
ProjectType.DOCKER_COMPOSE: ("🐳", "Docker Compose", "Orchestration de plusieurs conteneurs avec réseaux et volumes partagés."),
|
||||
ProjectType.CLI_TOOL: ("💻", "Outil CLI", "Programme en ligne de commande avec sous-commandes, flags et stdin/stdout."),
|
||||
ProjectType.CRON_JOB: ("⏰", "Jobs planifiés (cron / timer)", "Exécution périodique ou batch, anacron pour machines non 24/7."),
|
||||
ProjectType.SHELL_SCRIPT: ("📜", "Script shell (Bash / Zsh)", "Automatisation légère, glue entre outils, pipelines de commandes."),
|
||||
ProjectType.WORKER_MQ: ("📨", "Worker + Message queue", "Traitement asynchrone via Redis, RabbitMQ ou Kafka."),
|
||||
ProjectType.SOCKET_IPC: ("🔗", "Socket / IPC local", "Communication inter-processus via Unix socket, FIFO ou D-Bus."),
|
||||
ProjectType.WEBHOOK_EVENT: ("🪝", "Webhook / Event-driven", "Réception d'événements HTTP entrants, architecture pub/sub."),
|
||||
ProjectType.LIBRARY_PLUGIN: ("📦", "Bibliothèque / Plugin", "Code réutilisable sous forme de .so, package Python ou module Go."),
|
||||
ProjectType.AGENT_BOT: ("🤖", "Agent / Bot autonome", "Service IA ou bot (Telegram, Discord) tournant en continu."),
|
||||
ProjectType.TUI_APP: ("🖥️", "TUI (interface texte)", "Interface terminal interactive via curses, Textual ou Bubble Tea."),
|
||||
ProjectType.MCP_SERVER: ("🧠", "Serveur MCP", "Exposition d'outils IA via Model Context Protocol (stdio ou HTTP SSE)."),
|
||||
ProjectType.WASM_EDGE: ("🧊", "WebAssembly / Edge", "Modules wasm sandboxés, portables, via wasi et wasmtime."),
|
||||
ProjectType.GRPC_SERVICE: ("⚡", "Microservice gRPC / Thrift", "RPC binaire performant avec contrat IDL (protobuf)."),
|
||||
ProjectType.KERNEL_EBPF: ("🔬", "Module kernel / eBPF", "Driver, hook réseau ou sonde d'observabilité au niveau noyau."),
|
||||
ProjectType.SERVERLESS_FAAS: ("☁️", "Serverless / FaaS", "Fonctions déclenchées à la demande via OpenFaaS ou Knative."),
|
||||
ProjectType.DESKTOP_GUI: ("🖼️", "Application desktop (GUI)", "Interface graphique native via GTK, Qt, ou Tauri/Electron."),
|
||||
ProjectType.STATIC_SITE: ("📄", "Site statique / SSG", "Génération de HTML pur au build time via Hugo, Astro ou Jekyll."),
|
||||
}
|
||||
|
||||
|
||||
class TaskStatus(str, enum.Enum):
|
||||
PENDING = "PENDING"
|
||||
IN_PROGRESS = "IN_PROGRESS"
|
||||
@@ -78,7 +123,35 @@ def _utcnow() -> datetime:
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
# ─── Models ────────────────────────────────────────────────────────────────────
|
||||
# ─── Infrastructure Models ─────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class DeployServer(Base):
|
||||
__tablename__ = "deploy_servers"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
name: Mapped[str] = mapped_column(String(200), unique=True, nullable=False)
|
||||
host: Mapped[str] = mapped_column(String(500), nullable=False)
|
||||
user: Mapped[str] = mapped_column(String(100), default="deploy")
|
||||
password: Mapped[str] = mapped_column(String(500), nullable=True)
|
||||
ssh_port: Mapped[int] = mapped_column(Integer, default=22)
|
||||
description: Mapped[str] = mapped_column(Text, default="")
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow)
|
||||
|
||||
|
||||
class GitServer(Base):
|
||||
__tablename__ = "git_servers"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
name: Mapped[str] = mapped_column(String(200), unique=True, nullable=False)
|
||||
url: Mapped[str] = mapped_column(String(500), nullable=False)
|
||||
token: Mapped[str] = mapped_column(String(500), nullable=True)
|
||||
org: Mapped[str] = mapped_column(String(200), default="openclaw")
|
||||
description: Mapped[str] = mapped_column(Text, default="")
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow)
|
||||
|
||||
|
||||
# ─── Core Models ───────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class Project(Base):
|
||||
@@ -94,9 +167,15 @@ class Project(Base):
|
||||
workflow_type: Mapped[WorkflowType] = mapped_column(
|
||||
Enum(WorkflowType), default=WorkflowType.SOFTWARE_DESIGN, nullable=False
|
||||
)
|
||||
project_type: Mapped[ProjectType] = mapped_column(
|
||||
Enum(ProjectType), default=ProjectType.DOCKER_WEBAPP, nullable=False
|
||||
)
|
||||
test_mode: Mapped[bool] = mapped_column(default=False)
|
||||
gitea_repo: Mapped[Optional[str]] = mapped_column(String(500), nullable=True)
|
||||
deployment_target: Mapped[Optional[str]] = mapped_column(String(200), nullable=True)
|
||||
install_path: Mapped[str] = mapped_column(String(500), nullable=True)
|
||||
gitea_repo: Mapped[str] = mapped_column(String(500), nullable=True)
|
||||
deployment_target: Mapped[str] = mapped_column(String(200), nullable=True)
|
||||
deploy_server_id: Mapped[int] = mapped_column(ForeignKey("deploy_servers.id"), nullable=True)
|
||||
git_server_id: Mapped[int] = mapped_column(ForeignKey("git_servers.id"), nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=_utcnow, onupdate=_utcnow
|
||||
@@ -112,6 +191,8 @@ class Project(Base):
|
||||
agent_executions: Mapped[List["AgentExecution"]] = relationship(
|
||||
back_populates="project", cascade="all, delete-orphan", lazy="selectin"
|
||||
)
|
||||
deploy_server: Mapped["DeployServer"] = relationship(lazy="selectin")
|
||||
git_server: Mapped["GitServer"] = relationship(lazy="selectin")
|
||||
|
||||
|
||||
class Task(Base):
|
||||
@@ -123,13 +204,13 @@ class Task(Base):
|
||||
type: Mapped[TaskType] = mapped_column(Enum(TaskType), default=TaskType.BACKEND)
|
||||
title: Mapped[str] = mapped_column(String(500), nullable=False)
|
||||
priority: Mapped[TaskPriority] = mapped_column(Enum(TaskPriority), default=TaskPriority.P3)
|
||||
assigned_to: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
|
||||
assigned_to: Mapped[str] = mapped_column(String(100), nullable=True)
|
||||
status: Mapped[TaskStatus] = mapped_column(
|
||||
Enum(TaskStatus), default=TaskStatus.PENDING, nullable=False
|
||||
)
|
||||
dependencies: Mapped[Optional[dict]] = mapped_column(JSON, default=list)
|
||||
acceptance_criteria: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||
agent_payloads: Mapped[Optional[dict]] = mapped_column(JSON, default=dict)
|
||||
dependencies: Mapped[dict] = mapped_column(JSON, default=list)
|
||||
acceptance_criteria: Mapped[str] = mapped_column(Text, nullable=True)
|
||||
agent_payloads: Mapped[dict] = mapped_column(JSON, default=dict)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=_utcnow, onupdate=_utcnow
|
||||
@@ -148,11 +229,11 @@ class AgentExecution(Base):
|
||||
status: Mapped[AgentExecutionStatus] = mapped_column(
|
||||
Enum(AgentExecutionStatus), default=AgentExecutionStatus.RUNNING
|
||||
)
|
||||
pid: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
|
||||
pid: Mapped[int] = mapped_column(Integer, nullable=True)
|
||||
started_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow)
|
||||
finished_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
exit_code: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
|
||||
error_output: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||
finished_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
exit_code: Mapped[int] = mapped_column(Integer, nullable=True)
|
||||
error_output: Mapped[str] = mapped_column(Text, nullable=True)
|
||||
|
||||
# Relationships
|
||||
project: Mapped["Project"] = relationship(back_populates="agent_executions")
|
||||
@@ -166,8 +247,8 @@ class AuditLog(Base):
|
||||
timestamp: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow)
|
||||
agent: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
action: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
target: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
|
||||
message: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||
target: Mapped[str] = mapped_column(String(100), nullable=True)
|
||||
message: Mapped[str] = mapped_column(Text, nullable=True)
|
||||
source: Mapped[str] = mapped_column(String(100), default="api")
|
||||
|
||||
# Relationships
|
||||
|
||||
@@ -1,17 +1,28 @@
|
||||
"""
|
||||
Configuration management API endpoint.
|
||||
Configuration management and server CRUD API endpoints.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from fastapi import APIRouter
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.config import settings
|
||||
from app.schemas import ConfigResponse, ConfigUpdate
|
||||
from app.database import get_db
|
||||
from app.models import DeployServer, GitServer
|
||||
from app.schemas import (
|
||||
ConfigResponse, ConfigUpdate,
|
||||
DeployServerCreate, DeployServerResponse,
|
||||
GitServerCreate, GitServerResponse,
|
||||
)
|
||||
|
||||
log = logging.getLogger("foxy.api.config")
|
||||
router = APIRouter(prefix="/api/config", tags=["config"])
|
||||
|
||||
|
||||
# ─── App Configuration ────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@router.get("", response_model=ConfigResponse)
|
||||
async def get_config():
|
||||
"""Get current configuration (secrets masked)."""
|
||||
@@ -43,3 +54,86 @@ async def update_config(body: ConfigUpdate):
|
||||
|
||||
log.info(f"Config updated: {list(updated.keys())}")
|
||||
return {"message": "Configuration updated", "updated_fields": list(updated.keys())}
|
||||
|
||||
|
||||
# ─── Deploy Servers ────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@router.get("/deploy-servers", response_model=list[DeployServerResponse])
|
||||
async def list_deploy_servers(db: AsyncSession = Depends(get_db)):
|
||||
"""List all deployment servers."""
|
||||
result = await db.execute(select(DeployServer).order_by(DeployServer.name))
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
@router.post("/deploy-servers", response_model=DeployServerResponse, status_code=201)
|
||||
async def create_deploy_server(body: DeployServerCreate, db: AsyncSession = Depends(get_db)):
|
||||
"""Add a new deployment server."""
|
||||
server = DeployServer(
|
||||
name=body.name,
|
||||
host=body.host,
|
||||
user=body.user,
|
||||
password=body.password,
|
||||
ssh_port=body.ssh_port,
|
||||
description=body.description,
|
||||
)
|
||||
db.add(server)
|
||||
await db.flush()
|
||||
await db.refresh(server)
|
||||
log.info(f"Deploy server created: {body.name} ({body.host})")
|
||||
return server
|
||||
|
||||
|
||||
@router.delete("/deploy-servers/{server_id}")
|
||||
async def delete_deploy_server(server_id: int, db: AsyncSession = Depends(get_db)):
|
||||
"""Remove a deployment server."""
|
||||
result = await db.execute(select(DeployServer).where(DeployServer.id == server_id))
|
||||
server = result.scalar_one_or_none()
|
||||
if not server:
|
||||
raise HTTPException(status_code=404, detail="Deploy server not found")
|
||||
name = server.name
|
||||
await db.delete(server)
|
||||
await db.flush()
|
||||
log.info(f"Deploy server deleted: {name}")
|
||||
return {"message": f"Deploy server '{name}' deleted"}
|
||||
|
||||
|
||||
# ─── Git Servers ───────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@router.get("/git-servers", response_model=list[GitServerResponse])
|
||||
async def list_git_servers(db: AsyncSession = Depends(get_db)):
|
||||
"""List all Git servers."""
|
||||
result = await db.execute(select(GitServer).order_by(GitServer.name))
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
@router.post("/git-servers", response_model=GitServerResponse, status_code=201)
|
||||
async def create_git_server(body: GitServerCreate, db: AsyncSession = Depends(get_db)):
|
||||
"""Add a new Git server."""
|
||||
server = GitServer(
|
||||
name=body.name,
|
||||
url=body.url,
|
||||
token=body.token,
|
||||
org=body.org,
|
||||
description=body.description,
|
||||
)
|
||||
db.add(server)
|
||||
await db.flush()
|
||||
await db.refresh(server)
|
||||
log.info(f"Git server created: {body.name} ({body.url})")
|
||||
return server
|
||||
|
||||
|
||||
@router.delete("/git-servers/{server_id}")
|
||||
async def delete_git_server(server_id: int, db: AsyncSession = Depends(get_db)):
|
||||
"""Remove a Git server."""
|
||||
result = await db.execute(select(GitServer).where(GitServer.id == server_id))
|
||||
server = result.scalar_one_or_none()
|
||||
if not server:
|
||||
raise HTTPException(status_code=404, detail="Git server not found")
|
||||
name = server.name
|
||||
await db.delete(server)
|
||||
await db.flush()
|
||||
log.info(f"Git server deleted: {name}")
|
||||
return {"message": f"Git server '{name}' deleted"}
|
||||
|
||||
@@ -14,11 +14,12 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.database import get_db
|
||||
from app.models import (
|
||||
Project, Task, AuditLog, AgentExecution,
|
||||
ProjectStatus, WorkflowType, AgentExecutionStatus,
|
||||
ProjectStatus, WorkflowType, ProjectType, AgentExecutionStatus,
|
||||
PROJECT_TYPE_INFO,
|
||||
)
|
||||
from app.schemas import (
|
||||
ProjectCreate, ProjectUpdate, ProjectSummary, ProjectDetail,
|
||||
TaskCreate, TaskResponse,
|
||||
TaskCreate, TaskResponse, ProjectTypeInfo,
|
||||
)
|
||||
from app.workflows import (
|
||||
get_workflow_steps, get_current_step, get_workflow_progress,
|
||||
@@ -40,6 +41,23 @@ def _utcnow() -> datetime:
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
# ─── Project Types ─────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@router.get("/types", response_model=list[ProjectTypeInfo])
|
||||
async def list_project_types():
|
||||
"""Return all available project types with emoji, label and description."""
|
||||
return [
|
||||
ProjectTypeInfo(
|
||||
value=pt.value,
|
||||
emoji=info[0],
|
||||
label=info[1],
|
||||
description=info[2],
|
||||
)
|
||||
for pt, info in PROJECT_TYPE_INFO.items()
|
||||
]
|
||||
|
||||
|
||||
# ─── CRUD ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -54,7 +72,11 @@ async def create_project(body: ProjectCreate, db: AsyncSession = Depends(get_db)
|
||||
description=body.description,
|
||||
status=ProjectStatus.AWAITING_CONDUCTOR,
|
||||
workflow_type=body.workflow_type,
|
||||
project_type=body.project_type,
|
||||
test_mode=body.test_mode,
|
||||
install_path=body.install_path,
|
||||
deploy_server_id=body.deploy_server_id,
|
||||
git_server_id=body.git_server_id,
|
||||
)
|
||||
db.add(project)
|
||||
await db.flush()
|
||||
@@ -64,21 +86,48 @@ async def create_project(body: ProjectCreate, db: AsyncSession = Depends(get_db)
|
||||
agent="system",
|
||||
action="PROJECT_CREATED",
|
||||
target=slug,
|
||||
message=f"Projet créé: {body.name} (workflow: {body.workflow_type.value})",
|
||||
message=f"Projet créé: {body.name} (workflow: {body.workflow_type.value}, type: {body.project_type.value})",
|
||||
source="api",
|
||||
)
|
||||
db.add(audit)
|
||||
await db.flush()
|
||||
|
||||
# Refresh to load eager relationships (tasks, audit_logs, agent_executions)
|
||||
await db.refresh(project, ["tasks", "audit_logs", "agent_executions"])
|
||||
# Refresh to load eager relationships (tasks, audit_logs, agent_executions, servers)
|
||||
await db.refresh(project, ["tasks", "audit_logs", "agent_executions", "deploy_server", "git_server"])
|
||||
|
||||
log.info(f"Project created: {slug} (workflow: {body.workflow_type.value})")
|
||||
log.info(f"Project created: {slug} (workflow: {body.workflow_type.value}, type: {body.project_type.value})")
|
||||
|
||||
await manager.broadcast_project_update(project.id, project.status.value, project.name)
|
||||
await notify_project_event(project.name, "Projet créé", f"Workflow: {body.workflow_type.value}")
|
||||
await notify_project_event(project.name, "Projet créé", f"Workflow: {body.workflow_type.value}, Type: {body.project_type.value}")
|
||||
|
||||
return project
|
||||
# Build response with server names
|
||||
return _project_detail(project)
|
||||
|
||||
|
||||
def _project_detail(p: Project) -> dict:
|
||||
"""Build a ProjectDetail-compatible dict from a Project ORM instance."""
|
||||
return {
|
||||
"id": p.id,
|
||||
"name": p.name,
|
||||
"slug": p.slug,
|
||||
"description": p.description,
|
||||
"status": p.status,
|
||||
"workflow_type": p.workflow_type,
|
||||
"project_type": p.project_type,
|
||||
"test_mode": p.test_mode,
|
||||
"install_path": p.install_path,
|
||||
"gitea_repo": p.gitea_repo,
|
||||
"deployment_target": p.deployment_target,
|
||||
"deploy_server_id": p.deploy_server_id,
|
||||
"git_server_id": p.git_server_id,
|
||||
"deploy_server_name": p.deploy_server.name if p.deploy_server else None,
|
||||
"git_server_name": p.git_server.name if p.git_server else None,
|
||||
"created_at": p.created_at,
|
||||
"updated_at": p.updated_at,
|
||||
"tasks": p.tasks,
|
||||
"audit_logs": p.audit_logs,
|
||||
"agent_executions": p.agent_executions,
|
||||
}
|
||||
|
||||
|
||||
@router.get("", response_model=list[ProjectSummary])
|
||||
@@ -115,9 +164,13 @@ async def list_projects(
|
||||
slug=p.slug,
|
||||
status=p.status,
|
||||
workflow_type=p.workflow_type,
|
||||
project_type=p.project_type,
|
||||
test_mode=p.test_mode,
|
||||
install_path=p.install_path,
|
||||
task_count=total,
|
||||
tasks_done=done,
|
||||
deploy_server_name=p.deploy_server.name if p.deploy_server else None,
|
||||
git_server_name=p.git_server.name if p.git_server else None,
|
||||
created_at=p.created_at,
|
||||
updated_at=p.updated_at,
|
||||
))
|
||||
@@ -131,7 +184,7 @@ async def get_project(project_id: int, db: AsyncSession = Depends(get_db)):
|
||||
project = result.scalar_one_or_none()
|
||||
if not project:
|
||||
raise HTTPException(status_code=404, detail="Project not found")
|
||||
return project
|
||||
return _project_detail(project)
|
||||
|
||||
|
||||
# ─── Workflow Control ──────────────────────────────────────────────────────────
|
||||
|
||||
@@ -20,18 +20,23 @@ async def list_workflows():
|
||||
"""List all available workflow types with their step sequences."""
|
||||
workflows = []
|
||||
for wf_type, steps in WORKFLOW_REGISTRY.items():
|
||||
step_list = [
|
||||
{
|
||||
"agent": s.agent_name,
|
||||
"label": AGENT_LABELS.get(s.agent_name, s.agent_name),
|
||||
"model": AGENT_MODELS.get(s.agent_name, "unknown"),
|
||||
"awaiting_status": s.awaiting_status,
|
||||
"running_status": s.running_status,
|
||||
}
|
||||
for s in steps
|
||||
]
|
||||
# Build visual path string: "Conductor → Architect → Dev → QA → Admin"
|
||||
path = " → ".join(s.agent_name for s in steps)
|
||||
workflows.append({
|
||||
"type": wf_type.value,
|
||||
"label": wf_type.value.replace("_", " ").title(),
|
||||
"steps": [
|
||||
{
|
||||
"agent": s.agent_name,
|
||||
"label": AGENT_LABELS.get(s.agent_name, s.agent_name),
|
||||
"model": AGENT_MODELS.get(s.agent_name, "unknown"),
|
||||
"awaiting_status": s.awaiting_status,
|
||||
"running_status": s.running_status,
|
||||
}
|
||||
for s in steps
|
||||
],
|
||||
"path": path,
|
||||
"steps": step_list,
|
||||
})
|
||||
return workflows
|
||||
|
||||
|
||||
+75
-1
@@ -7,7 +7,7 @@ from typing import Optional, List, Any
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.models import (
|
||||
ProjectStatus, WorkflowType, TaskStatus, TaskType,
|
||||
ProjectStatus, WorkflowType, ProjectType, TaskStatus, TaskType,
|
||||
TaskPriority, AgentExecutionStatus,
|
||||
)
|
||||
|
||||
@@ -76,6 +76,62 @@ class AgentExecutionResponse(BaseModel):
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
# ─── Deploy Server Schemas ─────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class DeployServerCreate(BaseModel):
|
||||
name: str = Field(..., min_length=1, max_length=200)
|
||||
host: str = Field(..., min_length=1)
|
||||
user: str = "deploy"
|
||||
password: Optional[str] = None
|
||||
ssh_port: int = 22
|
||||
description: str = ""
|
||||
|
||||
|
||||
class DeployServerResponse(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
host: str
|
||||
user: str
|
||||
ssh_port: int
|
||||
description: str
|
||||
created_at: datetime
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
# ─── Git Server Schemas ────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class GitServerCreate(BaseModel):
|
||||
name: str = Field(..., min_length=1, max_length=200)
|
||||
url: str = Field(..., min_length=1)
|
||||
token: Optional[str] = None
|
||||
org: str = "openclaw"
|
||||
description: str = ""
|
||||
|
||||
|
||||
class GitServerResponse(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
url: str
|
||||
org: str
|
||||
description: str
|
||||
created_at: datetime
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
# ─── Project Type Info ─────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class ProjectTypeInfo(BaseModel):
|
||||
value: str
|
||||
emoji: str
|
||||
label: str
|
||||
description: str
|
||||
|
||||
|
||||
# ─── Project Schemas ───────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -83,13 +139,21 @@ class ProjectCreate(BaseModel):
|
||||
name: str = Field(..., min_length=1, max_length=200)
|
||||
description: str = ""
|
||||
workflow_type: WorkflowType = WorkflowType.SOFTWARE_DESIGN
|
||||
project_type: ProjectType = ProjectType.DOCKER_WEBAPP
|
||||
test_mode: bool = False
|
||||
install_path: Optional[str] = None
|
||||
deploy_server_id: Optional[int] = None
|
||||
git_server_id: Optional[int] = None
|
||||
|
||||
|
||||
class ProjectUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
workflow_type: Optional[WorkflowType] = None
|
||||
project_type: Optional[ProjectType] = None
|
||||
install_path: Optional[str] = None
|
||||
deploy_server_id: Optional[int] = None
|
||||
git_server_id: Optional[int] = None
|
||||
|
||||
|
||||
class ProjectSummary(BaseModel):
|
||||
@@ -98,9 +162,13 @@ class ProjectSummary(BaseModel):
|
||||
slug: str
|
||||
status: ProjectStatus
|
||||
workflow_type: WorkflowType
|
||||
project_type: ProjectType
|
||||
test_mode: bool
|
||||
install_path: Optional[str]
|
||||
task_count: int = 0
|
||||
tasks_done: int = 0
|
||||
deploy_server_name: Optional[str] = None
|
||||
git_server_name: Optional[str] = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
@@ -114,9 +182,15 @@ class ProjectDetail(BaseModel):
|
||||
description: str
|
||||
status: ProjectStatus
|
||||
workflow_type: WorkflowType
|
||||
project_type: ProjectType
|
||||
test_mode: bool
|
||||
install_path: Optional[str]
|
||||
gitea_repo: Optional[str]
|
||||
deployment_target: Optional[str]
|
||||
deploy_server_id: Optional[int]
|
||||
git_server_id: Optional[int]
|
||||
deploy_server_name: Optional[str] = None
|
||||
git_server_name: Optional[str] = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
tasks: List[TaskResponse] = []
|
||||
|
||||
Reference in New Issue
Block a user