diff --git a/app/db/operations.py b/app/db/operations.py index e2a6ffd..323dc72 100644 --- a/app/db/operations.py +++ b/app/db/operations.py @@ -172,6 +172,32 @@ def get_operations_snapshot(): 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"])) + app_columns = _table_columns(con, "apps") + if "enabled" in app_columns: + active_applications = con.execute( + """ + SELECT COUNT(*) AS count + FROM apps + WHERE enabled = 1 + OR LOWER(CAST(enabled AS TEXT)) IN ('1', 'true', 'yes', 'enabled') + """ + ).fetchone()["count"] + else: + active_applications = con.execute( + """ + SELECT COUNT(*) AS count + FROM apps + WHERE LOWER(COALESCE(status, '')) NOT IN ('disabled', 'deleted', 'archived') + """ + ).fetchone()["count"] + problematic_applications = con.execute( + """ + SELECT COUNT(*) AS count + FROM apps + WHERE LOWER(COALESCE(status, '')) IN ('failed', 'error', 'disabled', 'deleted', 'archived') + """ + ).fetchone()["count"] + jobs_summary = con.execute( """ SELECT @@ -190,6 +216,15 @@ def get_operations_snapshot(): LIMIT 20 """ ).fetchall() + recent_failed_jobs = con.execute( + """ + SELECT id, type, target_type, target_id, status, source, created_at, started_at, finished_at + FROM jobs + WHERE status = 'failed' + ORDER BY id DESC + LIMIT 10 + """ + ).fetchall() deployments_summary = con.execute( f""" @@ -201,6 +236,24 @@ def get_operations_snapshot(): """, (*DEPLOYMENT_RUNNING_STATUSES, *DEPLOYMENT_FAILED_STATUSES, *DEPLOYMENT_SUCCESS_STATUSES), ).fetchone() + deployment_columns = _table_columns(con, "deployments") + deployments_today = 0 + if "created_at" in deployment_columns: + deployments_today = con.execute( + """ + SELECT COUNT(*) AS count + FROM deployments + WHERE date(created_at) = date('now') + """ + ).fetchone()["count"] + elif "started_at" in deployment_columns: + deployments_today = con.execute( + """ + SELECT COUNT(*) AS count + FROM deployments + WHERE date(started_at) = date('now') + """ + ).fetchone()["count"] recent_deployments = con.execute( """ SELECT id, app_id, kind, status, started_at, finished_at, trigger_source @@ -217,6 +270,16 @@ def get_operations_snapshot(): LIMIT 20 """ ).fetchall() + recent_failed_deployments = con.execute( + f""" + SELECT id, app_id, kind, status, started_at, finished_at, trigger_source + FROM deployments + WHERE LOWER(COALESCE(status, '')) IN ({','.join('?' for _ in DEPLOYMENT_FAILED_STATUSES)}) + ORDER BY id DESC + LIMIT 10 + """, + DEPLOYMENT_FAILED_STATUSES, + ).fetchall() con.close() return { @@ -227,6 +290,11 @@ def get_operations_snapshot(): "failed_24h": jobs_summary["failed_24h"] or 0, "success_24h": jobs_summary["success_24h"] or 0, "recent": [dict(row) for row in recent_jobs], + "recent_failed": [dict(row) for row in recent_failed_jobs], + }, + "apps": { + "active": active_applications, + "problematic": problematic_applications, }, "workers": { "online": workers_online, @@ -237,7 +305,9 @@ def get_operations_snapshot(): "running": deployments_summary["running"] or 0, "failed_24h": deployments_summary["failed_24h"] or 0, "success_24h": deployments_summary["success_24h"] or 0, + "today": deployments_today, "recent": [dict(row) for row in recent_deployments], + "recent_failed": [dict(row) for row in recent_failed_deployments], }, "audit": { "recent": [dict(row) for row in recent_audit], diff --git a/app/main.py b/app/main.py index c9a66e0..4aa05e4 100644 --- a/app/main.py +++ b/app/main.py @@ -9,7 +9,7 @@ from .routes import apps, audit, auth, backups, deployments, health, jobs, opera def create_app() -> FastAPI: - app = FastAPI(title="AppFactory Portal") + app = FastAPI(title="CSBot Services Portal") session_secret = read_env_value("PORTAL_SESSION_SECRET", "") or "dev-only-appfactory-session-secret" app.add_middleware( diff --git a/app/routes/apps.py b/app/routes/apps.py index 023223d..1faa56c 100644 --- a/app/routes/apps.py +++ b/app/routes/apps.py @@ -1,7 +1,8 @@ import html -from urllib.parse import quote +import math +from urllib.parse import quote, urlencode -from fastapi import APIRouter, Depends, Form, HTTPException, Request +from fastapi import APIRouter, Depends, Form, HTTPException, Query, Request from fastapi.responses import HTMLResponse, RedirectResponse from ..auth import require_user @@ -16,17 +17,46 @@ from ..config import ( ) from ..db.apps import get_app, get_app_deployments, get_apps, update_app_resources from ..db.audit import log_audit_event -from ..db.jobs import create_job, has_active_deploy_job +from ..db.jobs import create_job, get_jobs, has_active_deploy_job from ..routes.deployments import render_status_pill from ..shell import run_command from ..templates.layout import page, render_result router = APIRouter() +DEFAULT_PAGE_SIZE = 20 -@router.get("/", response_class=HTMLResponse) -def index(request: Request, user=Depends(require_user)): +@router.get("/") +def portal_home(user=Depends(require_user)): + return RedirectResponse(url="/portal/operations", status_code=303) + + +@router.get("/apps", response_class=HTMLResponse) +def apps_page( + request: Request, + q: str = Query(""), + status: str = Query(""), + page_number: int = Query(1, alias="page", ge=1), + user=Depends(require_user), +): apps = get_apps() + query = q.strip() + selected_status = status.strip() + if query: + apps = [ + item for item in apps + if query.lower() in (item.get("id", "") or "").lower() + or query.lower() in (item.get("name", "") or "").lower() + ] + if selected_status: + apps = [item for item in apps if (item.get("status", "") or "") == selected_status] + status_values = sorted({item.get("status", "") for item in get_apps() if item.get("status")}) + total_apps = len(apps) + total_pages = max(1, math.ceil(total_apps / DEFAULT_PAGE_SIZE)) + if page_number > total_pages: + page_number = total_pages + offset = (page_number - 1) * DEFAULT_PAGE_SIZE + apps = apps[offset : offset + DEFAULT_PAGE_SIZE] gitea_url = read_env_value("GITEA_URL", "") gitea_org = read_env_value("GITEA_ORG", DEFAULT_GITEA_ORG) @@ -108,8 +138,8 @@ def index(request: Request, user=Depends(require_user)):