134 lines
5.1 KiB
Python
134 lines
5.1 KiB
Python
"""
|
|
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:
|
|
now_utc = datetime.utcnow()
|
|
age_minutes = int((now_utc - last_seen).total_seconds() // 60)
|
|
if age_minutes > 60:
|
|
status = "missing"
|
|
else:
|
|
status = "ok"
|
|
else:
|
|
age_minutes = None
|
|
status = "missing"
|
|
|
|
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": 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,
|
|
})
|