feat: watcher agent management system implemented.
This commit is contained in:
129
backend/routers/watcher_manager.py
Normal file
129
backend/routers/watcher_manager.py
Normal file
@@ -0,0 +1,129 @@
|
||||
"""
|
||||
Watcher Manager — admin API for series3-watcher and thor-watcher agents.
|
||||
|
||||
Endpoints:
|
||||
GET /api/admin/watchers — list all watcher agents
|
||||
GET /api/admin/watchers/{agent_id} — get single agent detail
|
||||
POST /api/admin/watchers/{agent_id}/trigger-update — flag agent for update
|
||||
POST /api/admin/watchers/{agent_id}/clear-update — clear update flag
|
||||
GET /api/admin/watchers/{agent_id}/update-check — polled by watcher on heartbeat
|
||||
|
||||
Page:
|
||||
GET /admin/watchers — HTML admin page
|
||||
"""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Optional
|
||||
|
||||
from backend.database import get_db
|
||||
from backend.models import WatcherAgent
|
||||
from backend.templates_config import templates
|
||||
|
||||
router = APIRouter(tags=["admin"])
|
||||
|
||||
|
||||
# ── helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
def _agent_to_dict(agent: WatcherAgent) -> dict:
|
||||
last_seen = agent.last_seen
|
||||
if last_seen:
|
||||
# Compute age in minutes (last_seen stored as UTC naive)
|
||||
now_utc = datetime.utcnow()
|
||||
age_minutes = int((now_utc - last_seen).total_seconds() // 60)
|
||||
else:
|
||||
age_minutes = None
|
||||
|
||||
return {
|
||||
"id": agent.id,
|
||||
"source_type": agent.source_type,
|
||||
"version": agent.version,
|
||||
"last_seen": last_seen.isoformat() if last_seen else None,
|
||||
"age_minutes": age_minutes,
|
||||
"status": agent.status,
|
||||
"ip_address": agent.ip_address,
|
||||
"log_tail": agent.log_tail,
|
||||
"update_pending": bool(agent.update_pending),
|
||||
"update_version": agent.update_version,
|
||||
}
|
||||
|
||||
|
||||
# ── API routes ────────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/api/admin/watchers")
|
||||
def list_watchers(db: Session = Depends(get_db)):
|
||||
agents = db.query(WatcherAgent).order_by(WatcherAgent.last_seen.desc()).all()
|
||||
return [_agent_to_dict(a) for a in agents]
|
||||
|
||||
|
||||
@router.get("/api/admin/watchers/{agent_id}")
|
||||
def get_watcher(agent_id: str, db: Session = Depends(get_db)):
|
||||
agent = db.query(WatcherAgent).filter(WatcherAgent.id == agent_id).first()
|
||||
if not agent:
|
||||
raise HTTPException(status_code=404, detail="Watcher agent not found")
|
||||
return _agent_to_dict(agent)
|
||||
|
||||
|
||||
class TriggerUpdateRequest(BaseModel):
|
||||
version: Optional[str] = None # target version label (informational)
|
||||
|
||||
|
||||
@router.post("/api/admin/watchers/{agent_id}/trigger-update")
|
||||
def trigger_update(agent_id: str, body: TriggerUpdateRequest, db: Session = Depends(get_db)):
|
||||
agent = db.query(WatcherAgent).filter(WatcherAgent.id == agent_id).first()
|
||||
if not agent:
|
||||
raise HTTPException(status_code=404, detail="Watcher agent not found")
|
||||
agent.update_pending = True
|
||||
agent.update_version = body.version
|
||||
db.commit()
|
||||
return {"ok": True, "agent_id": agent_id, "update_pending": True}
|
||||
|
||||
|
||||
@router.post("/api/admin/watchers/{agent_id}/clear-update")
|
||||
def clear_update(agent_id: str, db: Session = Depends(get_db)):
|
||||
agent = db.query(WatcherAgent).filter(WatcherAgent.id == agent_id).first()
|
||||
if not agent:
|
||||
raise HTTPException(status_code=404, detail="Watcher agent not found")
|
||||
agent.update_pending = False
|
||||
agent.update_version = None
|
||||
db.commit()
|
||||
return {"ok": True, "agent_id": agent_id, "update_pending": False}
|
||||
|
||||
|
||||
@router.get("/api/admin/watchers/{agent_id}/update-check")
|
||||
def update_check(agent_id: str, db: Session = Depends(get_db)):
|
||||
"""
|
||||
Polled by watcher agents on each heartbeat cycle.
|
||||
Returns update_available=True when an update has been triggered via the UI.
|
||||
Automatically clears the flag after the watcher acknowledges it.
|
||||
"""
|
||||
agent = db.query(WatcherAgent).filter(WatcherAgent.id == agent_id).first()
|
||||
if not agent:
|
||||
return {"update_available": False}
|
||||
|
||||
pending = bool(agent.update_pending)
|
||||
|
||||
if pending:
|
||||
# Clear the flag — the watcher will now self-update
|
||||
agent.update_pending = False
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"update_available": pending,
|
||||
"version": agent.update_version,
|
||||
}
|
||||
|
||||
|
||||
# ── HTML page ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/admin/watchers", response_class=HTMLResponse)
|
||||
def admin_watchers_page(request: Request, db: Session = Depends(get_db)):
|
||||
agents = db.query(WatcherAgent).order_by(WatcherAgent.last_seen.desc()).all()
|
||||
agents_data = [_agent_to_dict(a) for a in agents]
|
||||
return templates.TemplateResponse("admin_watchers.html", {
|
||||
"request": request,
|
||||
"agents": agents_data,
|
||||
})
|
||||
Reference in New Issue
Block a user