feat: Implement comprehensive project management with new API client, backend routes, and frontend pages for projects and settings.
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user