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:
+46
-20
@@ -1,16 +1,14 @@
|
||||
import asyncio
|
||||
import html
|
||||
from urllib.parse import quote
|
||||
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from fastapi import APIRouter, Depends, Request, WebSocket, WebSocketDisconnect
|
||||
from fastapi.responses import HTMLResponse
|
||||
|
||||
from app.auth import require_user
|
||||
from app.auth import current_user, require_user
|
||||
from app.db.audit import log_audit_event
|
||||
from app.db.operations import (
|
||||
get_operations_summary,
|
||||
get_recent_audit_events,
|
||||
get_recent_deployments,
|
||||
get_recent_jobs,
|
||||
get_operations_snapshot,
|
||||
)
|
||||
from app.routes.deployments import render_status_pill
|
||||
from app.routes.jobs import render_job_status
|
||||
@@ -19,6 +17,22 @@ from app.templates.layout import page
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.websocket("/ws/operations")
|
||||
async def operations_websocket(websocket: WebSocket):
|
||||
user = current_user(websocket)
|
||||
if not user:
|
||||
await websocket.close(code=1008)
|
||||
return
|
||||
|
||||
await websocket.accept()
|
||||
try:
|
||||
while True:
|
||||
await websocket.send_json(get_operations_snapshot())
|
||||
await asyncio.sleep(2)
|
||||
except WebSocketDisconnect:
|
||||
return
|
||||
|
||||
|
||||
def render_recent_jobs(jobs: list[dict]) -> str:
|
||||
rows = ""
|
||||
for job in jobs:
|
||||
@@ -98,16 +112,27 @@ def render_recent_audit_events(events: list[dict]) -> str:
|
||||
|
||||
@router.get("/operations", response_class=HTMLResponse)
|
||||
def operations_dashboard(request: Request, user=Depends(require_user)):
|
||||
summary = get_operations_summary()
|
||||
snapshot = get_operations_snapshot()
|
||||
log_audit_event(
|
||||
user,
|
||||
action="operations.dashboard.view",
|
||||
target_type="operations",
|
||||
metadata=summary,
|
||||
metadata={
|
||||
"jobs": {
|
||||
"queued": snapshot["jobs"]["queued"],
|
||||
"running": snapshot["jobs"]["running"],
|
||||
"failed_24h": snapshot["jobs"]["failed_24h"],
|
||||
},
|
||||
"workers": {
|
||||
"online": snapshot["workers"]["online"],
|
||||
"offline": snapshot["workers"]["offline"],
|
||||
},
|
||||
"deployments": {
|
||||
"running": snapshot["deployments"]["running"],
|
||||
"failed_24h": snapshot["deployments"]["failed_24h"],
|
||||
},
|
||||
},
|
||||
)
|
||||
recent_jobs = get_recent_jobs()
|
||||
recent_deployments = get_recent_deployments()
|
||||
recent_audit_events = get_recent_audit_events()
|
||||
|
||||
return page(
|
||||
"Operations",
|
||||
@@ -118,12 +143,12 @@ def operations_dashboard(request: Request, user=Depends(require_user)):
|
||||
</div>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card stat-success"><span>Workers Online</span><strong>{summary.get("workers_online") or 0}</strong></div>
|
||||
<div class="stat-card stat-warning"><span>Running Jobs</span><strong>{summary.get("running_jobs") or 0}</strong></div>
|
||||
<div class="stat-card stat-warning"><span>Queued Jobs</span><strong>{summary.get("queued_jobs") or 0}</strong></div>
|
||||
<div class="stat-card stat-danger"><span>Failed Jobs (24h)</span><strong>{summary.get("failed_jobs_24h") or 0}</strong></div>
|
||||
<div class="stat-card stat-total"><span>Deployments Today</span><strong>{summary.get("deployments_today") or 0}</strong></div>
|
||||
<div class="stat-card stat-total"><span>Active Applications</span><strong>{summary.get("active_applications") or 0}</strong></div>
|
||||
<div class="stat-card stat-success"><span>Workers Online</span><strong data-live-count="workers.online">{snapshot["workers"]["online"]}</strong></div>
|
||||
<div class="stat-card stat-warning"><span>Running Jobs</span><strong data-live-count="jobs.running">{snapshot["jobs"]["running"]}</strong></div>
|
||||
<div class="stat-card stat-warning"><span>Queued Jobs</span><strong data-live-count="jobs.queued">{snapshot["jobs"]["queued"]}</strong></div>
|
||||
<div class="stat-card stat-danger"><span>Failed Jobs (24h)</span><strong data-live-count="jobs.failed_24h">{snapshot["jobs"]["failed_24h"]}</strong></div>
|
||||
<div class="stat-card stat-warning"><span>Running Deployments</span><strong data-live-count="deployments.running">{snapshot["deployments"]["running"]}</strong></div>
|
||||
<div class="stat-card stat-danger"><span>Failed Deployments (24h)</span><strong data-live-count="deployments.failed_24h">{snapshot["deployments"]["failed_24h"]}</strong></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
@@ -137,7 +162,7 @@ def operations_dashboard(request: Request, user=Depends(require_user)):
|
||||
<th>Source</th>
|
||||
<th>Created At</th>
|
||||
</tr>
|
||||
{render_recent_jobs(recent_jobs)}
|
||||
<tbody data-live-table="jobs.recent">{render_recent_jobs(snapshot["jobs"]["recent"])}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -153,7 +178,7 @@ def operations_dashboard(request: Request, user=Depends(require_user)):
|
||||
<th>Spuštěno</th>
|
||||
<th>Dokončeno</th>
|
||||
</tr>
|
||||
{render_recent_deployments(recent_deployments)}
|
||||
<tbody data-live-table="deployments.recent">{render_recent_deployments(snapshot["deployments"]["recent"])}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -168,9 +193,10 @@ def operations_dashboard(request: Request, user=Depends(require_user)):
|
||||
<th>Cíl</th>
|
||||
<th>Zdroj</th>
|
||||
</tr>
|
||||
{render_recent_audit_events(recent_audit_events)}
|
||||
<tbody data-live-table="audit.recent">{render_recent_audit_events(snapshot["audit"]["recent"])}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<script src="/portal/static/operations-live.js"></script>
|
||||
""",
|
||||
user=user,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user