266 lines
8.3 KiB
Python
266 lines
8.3 KiB
Python
from fastapi import FastAPI, Depends, HTTPException, Request, Response, Cookie
|
|
from fastapi.staticfiles import StaticFiles
|
|
from fastapi.responses import FileResponse, RedirectResponse, JSONResponse
|
|
from sqlalchemy.orm import Session
|
|
from pydantic import BaseModel
|
|
from datetime import datetime, timezone
|
|
from typing import Optional
|
|
import hashlib
|
|
import hmac
|
|
import json
|
|
import os
|
|
import secrets
|
|
|
|
from database import get_db, init_db
|
|
from models import Agent, Run
|
|
|
|
app = FastAPI(title="Agent Command Center", version="1.0.0")
|
|
|
|
|
|
# --- Auth ---
|
|
|
|
AUTH_USER = os.environ.get("AUTH_USER", "eric")
|
|
AUTH_PASS = os.environ.get("AUTH_PASS", "Kj8#mPx2vQ!nR4wL")
|
|
SESSION_SECRET = os.environ.get("SESSION_SECRET", secrets.token_hex(32))
|
|
|
|
# In-memory session store
|
|
_sessions: dict[str, str] = {}
|
|
|
|
|
|
def create_session(username: str) -> str:
|
|
token = secrets.token_urlsafe(32)
|
|
_sessions[token] = username
|
|
return token
|
|
|
|
|
|
def get_current_user(session: Optional[str] = Cookie(None)) -> Optional[str]:
|
|
if session and session in _sessions:
|
|
return _sessions[session]
|
|
return None
|
|
|
|
|
|
def require_auth(session: Optional[str] = Cookie(None)):
|
|
user = get_current_user(session)
|
|
if not user:
|
|
raise HTTPException(status_code=401, detail="Not authenticated")
|
|
return user
|
|
|
|
|
|
class LoginRequest(BaseModel):
|
|
username: str
|
|
password: str
|
|
|
|
|
|
@app.post("/api/login")
|
|
def login(creds: LoginRequest, response: Response):
|
|
if creds.username == AUTH_USER and creds.password == AUTH_PASS:
|
|
token = create_session(creds.username)
|
|
response.set_cookie("session", token, httponly=True, samesite="lax", max_age=86400 * 7)
|
|
return {"status": "ok", "user": creds.username}
|
|
raise HTTPException(status_code=401, detail="Invalid credentials")
|
|
|
|
|
|
@app.post("/api/logout")
|
|
def logout(response: Response, session: Optional[str] = Cookie(None)):
|
|
if session and session in _sessions:
|
|
del _sessions[session]
|
|
response.delete_cookie("session")
|
|
return {"status": "ok"}
|
|
|
|
|
|
@app.get("/login")
|
|
def login_page():
|
|
return FileResponse("static/login.html")
|
|
|
|
|
|
# --- Pydantic schemas ---
|
|
|
|
class AgentCreate(BaseModel):
|
|
id: str
|
|
name: str
|
|
description: str = ""
|
|
schedule: str = "manual"
|
|
|
|
class AgentUpdate(BaseModel):
|
|
name: Optional[str] = None
|
|
description: Optional[str] = None
|
|
schedule: Optional[str] = None
|
|
status: Optional[str] = None
|
|
|
|
class RunCreate(BaseModel):
|
|
status: str = "running"
|
|
output: str = ""
|
|
error: str = ""
|
|
metadata: dict = {}
|
|
|
|
class RunUpdate(BaseModel):
|
|
status: Optional[str] = None
|
|
output: Optional[str] = None
|
|
error: Optional[str] = None
|
|
finished_at: Optional[str] = None
|
|
metadata: Optional[dict] = None
|
|
|
|
|
|
# --- API routes ---
|
|
|
|
@app.get("/api/health")
|
|
def health():
|
|
return {"status": "ok", "service": "agent-command-center"}
|
|
|
|
|
|
@app.get("/api/agents")
|
|
def list_agents(user: str = Depends(require_auth), db: Session = Depends(get_db)):
|
|
agents = db.query(Agent).all()
|
|
result = []
|
|
for a in agents:
|
|
last_run = db.query(Run).filter(Run.agent_id == a.id).order_by(Run.started_at.desc()).first()
|
|
recent_runs = db.query(Run).filter(Run.agent_id == a.id).order_by(Run.started_at.desc()).limit(10).all()
|
|
success_streak = 0
|
|
for r in recent_runs:
|
|
if r.status == "success":
|
|
success_streak += 1
|
|
else:
|
|
break
|
|
result.append({
|
|
"id": a.id,
|
|
"name": a.name,
|
|
"description": a.description,
|
|
"schedule": a.schedule,
|
|
"status": a.status,
|
|
"created_at": a.created_at.isoformat() if a.created_at else None,
|
|
"last_run": {
|
|
"status": last_run.status,
|
|
"started_at": last_run.started_at.isoformat() if last_run.started_at else None,
|
|
"finished_at": last_run.finished_at.isoformat() if last_run.finished_at else None,
|
|
} if last_run else None,
|
|
"success_streak": success_streak,
|
|
"total_runs": db.query(Run).filter(Run.agent_id == a.id).count(),
|
|
})
|
|
return result
|
|
|
|
|
|
@app.post("/api/agents")
|
|
def create_agent(agent: AgentCreate, db: Session = Depends(get_db)):
|
|
existing = db.query(Agent).filter(Agent.id == agent.id).first()
|
|
if existing:
|
|
raise HTTPException(status_code=409, detail="Agent already exists")
|
|
new_agent = Agent(
|
|
id=agent.id,
|
|
name=agent.name,
|
|
description=agent.description,
|
|
schedule=agent.schedule,
|
|
)
|
|
db.add(new_agent)
|
|
db.commit()
|
|
return {"id": new_agent.id, "status": "created"}
|
|
|
|
|
|
@app.get("/api/agents/{agent_id}")
|
|
def get_agent(agent_id: str, user: str = Depends(require_auth), db: Session = Depends(get_db)):
|
|
agent = db.query(Agent).filter(Agent.id == agent_id).first()
|
|
if not agent:
|
|
raise HTTPException(status_code=404, detail="Agent not found")
|
|
runs = db.query(Run).filter(Run.agent_id == agent_id).order_by(Run.started_at.desc()).limit(50).all()
|
|
return {
|
|
"id": agent.id,
|
|
"name": agent.name,
|
|
"description": agent.description,
|
|
"schedule": agent.schedule,
|
|
"status": agent.status,
|
|
"created_at": agent.created_at.isoformat() if agent.created_at else None,
|
|
"runs": [{
|
|
"id": r.id,
|
|
"started_at": r.started_at.isoformat() if r.started_at else None,
|
|
"finished_at": r.finished_at.isoformat() if r.finished_at else None,
|
|
"status": r.status,
|
|
"output": r.output,
|
|
"error": r.error,
|
|
"metadata": r.metadata_,
|
|
} for r in runs],
|
|
}
|
|
|
|
|
|
@app.put("/api/agents/{agent_id}")
|
|
def update_agent(agent_id: str, update: AgentUpdate, db: Session = Depends(get_db)):
|
|
agent = db.query(Agent).filter(Agent.id == agent_id).first()
|
|
if not agent:
|
|
raise HTTPException(status_code=404, detail="Agent not found")
|
|
for field, value in update.model_dump(exclude_none=True).items():
|
|
setattr(agent, field, value)
|
|
db.commit()
|
|
return {"id": agent.id, "status": "updated"}
|
|
|
|
|
|
@app.post("/api/agents/{agent_id}/runs")
|
|
def create_run(agent_id: str, run: RunCreate, db: Session = Depends(get_db)):
|
|
agent = db.query(Agent).filter(Agent.id == agent_id).first()
|
|
if not agent:
|
|
raise HTTPException(status_code=404, detail="Agent not found")
|
|
new_run = Run(
|
|
agent_id=agent_id,
|
|
status=run.status,
|
|
output=run.output,
|
|
error=run.error,
|
|
metadata_=run.metadata,
|
|
)
|
|
if run.status in ("success", "failed"):
|
|
new_run.finished_at = datetime.now(timezone.utc)
|
|
db.add(new_run)
|
|
db.commit()
|
|
return {"id": new_run.id, "status": new_run.status}
|
|
|
|
|
|
@app.put("/api/runs/{run_id}")
|
|
def update_run(run_id: int, update: RunUpdate, db: Session = Depends(get_db)):
|
|
run = db.query(Run).filter(Run.id == run_id).first()
|
|
if not run:
|
|
raise HTTPException(status_code=404, detail="Run not found")
|
|
if update.status is not None:
|
|
run.status = update.status
|
|
if update.output is not None:
|
|
run.output = update.output
|
|
if update.error is not None:
|
|
run.error = update.error
|
|
if update.metadata is not None:
|
|
run.metadata_ = update.metadata
|
|
if update.finished_at is not None:
|
|
run.finished_at = datetime.fromisoformat(update.finished_at)
|
|
elif update.status in ("success", "failed"):
|
|
run.finished_at = datetime.now(timezone.utc)
|
|
db.commit()
|
|
return {"id": run.id, "status": run.status}
|
|
|
|
|
|
@app.get("/api/runs")
|
|
def list_runs(limit: int = 50, user: str = Depends(require_auth), db: Session = Depends(get_db)):
|
|
runs = db.query(Run).order_by(Run.started_at.desc()).limit(limit).all()
|
|
return [{
|
|
"id": r.id,
|
|
"agent_id": r.agent_id,
|
|
"started_at": r.started_at.isoformat() if r.started_at else None,
|
|
"finished_at": r.finished_at.isoformat() if r.finished_at else None,
|
|
"status": r.status,
|
|
"output": r.output,
|
|
"error": r.error,
|
|
"metadata": r.metadata_,
|
|
} for r in runs]
|
|
|
|
|
|
# --- Static files (frontend) ---
|
|
|
|
app.mount("/static", StaticFiles(directory="static"), name="static")
|
|
|
|
@app.get("/")
|
|
def root(session: Optional[str] = Cookie(None)):
|
|
user = get_current_user(session)
|
|
if not user:
|
|
return RedirectResponse("/login", status_code=302)
|
|
return FileResponse("static/index.html")
|
|
|
|
|
|
# --- Startup ---
|
|
|
|
@app.on_event("startup")
|
|
def startup():
|
|
init_db()
|