Add unified operations realtime dashboard
- add /portal/ws/operations WebSocket snapshot endpoint
- add shared operations-live.js with reconnect and operations:snapshot events
- update Operations, Jobs, and Workers pages to use realtime snapshots
- keep job detail live logs on dedicated /ws/jobs/{job_id}/logs endpoint
- add Jobs and Workers pagination/filtering
- remove old page auto-refresh behavior from jobs/deployments
- normalize main navigation labels
- move New App CTA from menu to Apps page
- add backup download action for admins
- add Workers pages and operations dashboard data helpers
This commit is contained in:
@@ -2,6 +2,10 @@ from app.db.database import get_connection
|
||||
from app.db.migrations import run_migrations
|
||||
from app.db.workers import is_worker_online
|
||||
|
||||
DEPLOYMENT_SUCCESS_STATUSES = ("ok", "success", "succeeded", "done", "deployed", "completed")
|
||||
DEPLOYMENT_FAILED_STATUSES = ("failed", "failure", "error", "cancelled", "canceled")
|
||||
DEPLOYMENT_RUNNING_STATUSES = ("running", "pending", "queued", "in_progress", "starting")
|
||||
|
||||
|
||||
def _table_columns(con, table_name: str) -> set[str]:
|
||||
rows = con.execute(f"PRAGMA table_info({table_name})").fetchall()
|
||||
@@ -142,3 +146,100 @@ def get_recent_audit_events(limit: int = 20):
|
||||
|
||||
con.close()
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
|
||||
def get_operations_snapshot():
|
||||
run_migrations()
|
||||
con = get_connection()
|
||||
|
||||
workers = con.execute(
|
||||
"""
|
||||
SELECT id, status, started_at, last_seen_at, current_job_id, metadata_json
|
||||
FROM workers
|
||||
ORDER BY last_seen_at DESC, id
|
||||
LIMIT 20
|
||||
"""
|
||||
).fetchall()
|
||||
recent_workers = []
|
||||
workers_online = 0
|
||||
for row in workers:
|
||||
worker = dict(row)
|
||||
worker["online"] = is_worker_online(worker.get("last_seen_at"))
|
||||
workers_online += 1 if worker["online"] else 0
|
||||
recent_workers.append(worker)
|
||||
|
||||
total_workers = con.execute("SELECT COUNT(*) AS count FROM workers").fetchone()["count"]
|
||||
online_all = con.execute("SELECT last_seen_at FROM workers").fetchall()
|
||||
workers_online = sum(1 for worker in online_all if is_worker_online(worker["last_seen_at"]))
|
||||
|
||||
jobs_summary = con.execute(
|
||||
"""
|
||||
SELECT
|
||||
SUM(CASE WHEN status = 'queued' THEN 1 ELSE 0 END) AS queued,
|
||||
SUM(CASE WHEN status = 'running' THEN 1 ELSE 0 END) AS running,
|
||||
SUM(CASE WHEN status = 'failed' AND created_at >= datetime('now', '-24 hours') THEN 1 ELSE 0 END) AS failed_24h,
|
||||
SUM(CASE WHEN status = 'success' AND created_at >= datetime('now', '-24 hours') THEN 1 ELSE 0 END) AS success_24h
|
||||
FROM jobs
|
||||
"""
|
||||
).fetchone()
|
||||
recent_jobs = con.execute(
|
||||
"""
|
||||
SELECT id, type, target_type, target_id, status, source, created_at, started_at, finished_at
|
||||
FROM jobs
|
||||
ORDER BY id DESC
|
||||
LIMIT 20
|
||||
"""
|
||||
).fetchall()
|
||||
|
||||
deployments_summary = con.execute(
|
||||
f"""
|
||||
SELECT
|
||||
SUM(CASE WHEN LOWER(COALESCE(status, '')) IN ({','.join('?' for _ in DEPLOYMENT_RUNNING_STATUSES)}) THEN 1 ELSE 0 END) AS running,
|
||||
SUM(CASE WHEN LOWER(COALESCE(status, '')) IN ({','.join('?' for _ in DEPLOYMENT_FAILED_STATUSES)}) AND started_at >= datetime('now', '-24 hours') THEN 1 ELSE 0 END) AS failed_24h,
|
||||
SUM(CASE WHEN LOWER(COALESCE(status, '')) IN ({','.join('?' for _ in DEPLOYMENT_SUCCESS_STATUSES)}) AND started_at >= datetime('now', '-24 hours') THEN 1 ELSE 0 END) AS success_24h
|
||||
FROM deployments
|
||||
""",
|
||||
(*DEPLOYMENT_RUNNING_STATUSES, *DEPLOYMENT_FAILED_STATUSES, *DEPLOYMENT_SUCCESS_STATUSES),
|
||||
).fetchone()
|
||||
recent_deployments = con.execute(
|
||||
"""
|
||||
SELECT id, app_id, kind, status, started_at, finished_at, trigger_source
|
||||
FROM deployments
|
||||
ORDER BY id DESC
|
||||
LIMIT 20
|
||||
"""
|
||||
).fetchall()
|
||||
recent_audit = con.execute(
|
||||
"""
|
||||
SELECT id, username, action, target_type, target_id, source, created_at
|
||||
FROM audit_events
|
||||
ORDER BY id DESC
|
||||
LIMIT 20
|
||||
"""
|
||||
).fetchall()
|
||||
|
||||
con.close()
|
||||
return {
|
||||
"type": "operations.snapshot",
|
||||
"jobs": {
|
||||
"queued": jobs_summary["queued"] or 0,
|
||||
"running": jobs_summary["running"] or 0,
|
||||
"failed_24h": jobs_summary["failed_24h"] or 0,
|
||||
"success_24h": jobs_summary["success_24h"] or 0,
|
||||
"recent": [dict(row) for row in recent_jobs],
|
||||
},
|
||||
"workers": {
|
||||
"online": workers_online,
|
||||
"offline": max(0, total_workers - workers_online),
|
||||
"recent": recent_workers,
|
||||
},
|
||||
"deployments": {
|
||||
"running": deployments_summary["running"] or 0,
|
||||
"failed_24h": deployments_summary["failed_24h"] or 0,
|
||||
"success_24h": deployments_summary["success_24h"] or 0,
|
||||
"recent": [dict(row) for row in recent_deployments],
|
||||
},
|
||||
"audit": {
|
||||
"recent": [dict(row) for row in recent_audit],
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user