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)):

Detail

Nasazení

-
- + +
@@ -120,14 +150,40 @@ def index(request: Request, user=Depends(require_user)): """ if not rows: - rows = 'Zatím nejsou nasazené žádné aplikace.' + rows = 'Zatím nejsou nasazené žádné služby.' + status_options = [''] + for value in status_values: + selected = " selected" if selected_status == value else "" + escaped_value = html.escape(value) + status_options.append(f'') + first_item = offset + 1 if total_apps else 0 + last_item = min(offset + len(apps), total_apps) + + def page_url(page: int) -> str: + params = {"page": page} + if query: + params["q"] = query + if selected_status: + params["status"] = selected_status + return f"/portal/apps?{urlencode(params)}" + + pagination = f""" + + """ return page( - "Aplikace", + "Služby", f"""
-

Aplikace

+

Služby

Vytváření, nasazení, klonování, nastavení prostředků a mazání služeb.

@@ -143,11 +199,19 @@ def index(request: Request, user=Depends(require_user)):
-

Nasazené aplikace

-

+ Nová aplikace

+

Nasazené služby

+

+ Nová služba

+ + + + + + Reset + + {pagination} - + @@ -156,6 +220,7 @@ def index(request: Request, user=Depends(require_user)): {rows}
AplikaceSlužba Status Dokumentace Prostředky
+ {pagination}
""", user=user, @@ -197,20 +262,39 @@ def app_detail(app_id: str, request: Request, user=Depends(require_user)): """ if not rows: - rows = 'Zatím nejsou evidovaná žádná nasazení této aplikace.' + rows = 'Zatím nejsou evidovaná žádná nasazení této služby.' + + job_rows = "" + for job in get_jobs(limit=100, target=app.get("id", "")): + if job.get("target_type") != "app" or job.get("target_id") != app.get("id", ""): + continue + + job_id = html.escape(str(job.get("id", ""))) + job_rows += f""" + + #{job_id} + {render_status_pill(job.get("status"))} + {html.escape(job.get("type", "") or "")} + {html.escape(job.get("created_at", "") or "")} + + """ + + if not job_rows: + job_rows = 'Zatím nejsou evidované žádné úlohy této služby.' return page( - f"Aplikace {escaped_app_id}", + f"Služba {escaped_app_id}", f"""

{escaped_app_id}

{name}

- ← Zpět na aplikace - Nasazení aplikace + ← Zpět na služby + Nasazení služby + Úlohy služby

-
- + +
@@ -225,21 +309,35 @@ def app_detail(app_id: str, request: Request, user=Depends(require_user)): Paměť{memory} CPU{cpus} Upraveno{updated_at} + LogyZobrazit úlohy a logy
-

Poslední deploymenty

+

Historie nasazení

- - + + {rows}
ID StatusTimestampTriggered byČasSpustil
+ +
+

Historie úloh

+ + + + + + + + {job_rows} +
IDStatusTypVytvořeno
+
""", user=user, ) @@ -277,20 +375,20 @@ def redeploy_app(app_id: str, user=Depends(require_user)): @router.get("/new-app", response_class=HTMLResponse) def new_app_form(request: Request, user=Depends(require_user)): return page( - "Nová aplikace", + "Nová služba", """
-

Vytvořit novou aplikaci

+

Vytvořit novou službu

Vytvoří Gitea repozitář, webhook, lokální workspace, první commit a nasadí službu.

-
+

-
+

@@ -301,10 +399,10 @@ def new_app_form(request: Request, user=Depends(require_user)):

- +
-

← Zpět

+

← Zpět

""", user=user, @@ -341,8 +439,8 @@ def create_app( ) return render_result( - title=f"Vytvoření aplikace: {status}", - back_url="/portal", + title=f"Vytvoření služby: {status}", + back_url="/portal/apps", sections=[ ("Výstup vytvoření", create_result.stdout), ("Chyba vytvoření", create_result.stderr), @@ -372,8 +470,8 @@ def delete_app(app_id: str = Form(...), user=Depends(require_user)): ) return render_result( - title=f"Smazání aplikace: {status}", - back_url="/portal", + title=f"Smazání služby: {status}", + back_url="/portal/apps", sections=[("Výstup", result.stdout), ("Chyba", result.stderr)], user=user, ) @@ -418,7 +516,7 @@ def update_resources( return render_result( title=f"Úprava prostředků: {status}", - back_url="/portal", + back_url="/portal/apps", sections=[ ("Výstup katalogu", catalog_result.stdout), ("Chyba katalogu", catalog_result.stderr), diff --git a/app/routes/auth.py b/app/routes/auth.py index d8c59a1..abca15b 100644 --- a/app/routes/auth.py +++ b/app/routes/auth.py @@ -13,7 +13,7 @@ router = APIRouter() @router.get("/login", response_class=HTMLResponse) def login_form(request: Request): if current_user(request): - return RedirectResponse(url="/portal", status_code=303) + return RedirectResponse(url="/portal/operations", status_code=303) return _render_login() @@ -35,7 +35,7 @@ def login(request: Request, username: str = Form(...), password: str = Form(...) metadata={"username": user.get("username")}, ) - return RedirectResponse(url="/portal", status_code=303) + return RedirectResponse(url="/portal/operations", status_code=303) @router.post("/logout") @@ -61,8 +61,8 @@ def _render_login(error: str | None = None) -> str: "Přihlášení", f"""
-

Přihlášení do portálu

-

Přihlaste se interním účtem AppFactory.

+

CSBot Services Portal

+

Přihlaste se interním účtem.

{error_html}
diff --git a/app/routes/deployments.py b/app/routes/deployments.py index c328417..f125f80 100644 --- a/app/routes/deployments.py +++ b/app/routes/deployments.py @@ -49,7 +49,25 @@ def render_status_pill(status: str | None) -> str: else: class_name = "pill pill-muted" - return f'{html.escape(status_value)}' + labels = { + "running": "běží", + "pending": "čeká", + "queued": "ve frontě", + "in_progress": "probíhá", + "starting": "startuje", + "success": "úspěšné", + "ok": "úspěšné", + "succeeded": "úspěšné", + "done": "hotovo", + "deployed": "nasazeno", + "completed": "dokončeno", + "failed": "selhalo", + "failure": "selhalo", + "error": "chyba", + "cancelled": "zrušeno", + "canceled": "zrušeno", + } + return f'{html.escape(labels.get(normalized, status_value))}' def parse_timestamp(value: str | None) -> datetime | None: @@ -229,14 +247,14 @@ async def deployments_page(

Historie nasazení

Přehled posledních běhů nasazení a jejich výsledků.

- ← Zpět na portál + ← Zpět na přehled
-
Total deploys{stats.get("total") or 0}
-
Failed deploys{stats.get("failed") or 0}
-
Running deploys{stats.get("running") or 0}
-
Success rate{stats.get("success_rate") or 0}%
+
Celkem nasazení{stats.get("total") or 0}
+
Selhaná nasazení{stats.get("failed") or 0}
+
Běžící nasazení{stats.get("running") or 0}
+
Úspěšnost{stats.get("success_rate") or 0}%
@@ -335,19 +353,19 @@ async def deployment_detail_page( {failed_notice}

← Zpět na nasazení - Detail aplikace - Raw logy + Detail služby + Surové logy

Souhrn

- + - - + + diff --git a/app/routes/jobs.py b/app/routes/jobs.py index a0a695a..1755ad3 100644 --- a/app/routes/jobs.py +++ b/app/routes/jobs.py @@ -39,7 +39,15 @@ def render_job_status(status: str | None) -> str: else: class_name = "pill pill-muted" - return f'{html.escape(status_value)}' + labels = { + "queued": "čeká", + "running": "běží", + "cancelled_requested": "žádost o zrušení", + "cancelled": "zrušeno", + "success": "úspěšné", + "failed": "selhalo", + } + return f'{html.escape(labels.get(normalized, status_value))}' def pretty_json(value: str | None) -> str: @@ -195,7 +203,7 @@ def jobs_page( """ if not rows: - rows = '' + rows = '' status_options = render_options(JOB_FILTER_STATUSES, selected_status, "Všechny statusy") type_options = render_options(JOB_FILTER_TYPES, selected_type, "Všechny typy") @@ -225,19 +233,19 @@ def jobs_page( """ return page( - "Joby", + "Úlohy", f""" {refresh}
-

Joby

+

Úlohy

Fronta portálových a webhook úloh připravená pro centrální worker.

-
Queued{stats.get("queued") or 0}
-
Running{stats.get("running") or 0}
-
Failed (24h){stats.get("failed_24h") or 0}
-
Success (24h){stats.get("success_24h") or 0}
+
Čekající{stats.get("queued") or 0}
+
Běžící{stats.get("running") or 0}
+
Selhané (24 h){stats.get("failed_24h") or 0}
+
Úspěšné (24 h){stats.get("success_24h") or 0}
@@ -245,7 +253,7 @@ def jobs_page( - + Reset @@ -253,15 +261,15 @@ def jobs_page(
-

Recent Jobs

+

Poslední úlohy

Aplikace{app_id}
Služba{app_id}
Typ{kind}
Status{status}
Trigger source{trigger_source}
Triggered by{triggered_by}
Zdroj spuštění{trigger_source}
Spustil{triggered_by}
Commit author{commit_author}
Pusher{pusher}
Spuštěno{started_at}
Zatím nejsou evidované žádné joby.
Zatím nejsou evidované žádné úlohy.
- - - - + + + +
ID StatusTypeTargetSourceCreated AtTypCílZdrojVytvořeno
@@ -274,10 +282,10 @@ def jobs_page( ID Status - Source - Target - Created by - Created at + Zdroj + Cíl + Vytvořil + Vytvořeno Started at Finished at Akce @@ -300,7 +308,7 @@ def job_detail_page(job_id: int, request: Request, user=Depends(require_user)): status_value = (job.get("status") or "").lower() refresh = "" - title = f"Job #{html.escape(str(job.get('id', job_id)))}" + title = f"Úloha #{html.escape(str(job.get('id', job_id)))}" status = render_job_status(job.get("status")) target_type = job.get("target_type", "") or "" target_id = job.get("target_id", "") or "" @@ -315,7 +323,7 @@ def job_detail_page(job_id: int, request: Request, user=Depends(require_user)): if status_value == "failed" and can_retry_job(job): actions += f""" - + """ elif status_value == "failed" and (job.get("target_id") or "") in IGNORED_RETRY_REPOSITORIES: @@ -323,7 +331,7 @@ def job_detail_page(job_id: int, request: Request, user=Depends(require_user)): if status_value in {"queued", "running"}: actions += f"""
- +
""" if actions: @@ -420,8 +428,8 @@ def job_detail_page(job_id: int, request: Request, user=Depends(require_user)):

{title}

- ← Zpět na joby - Detail targetu + ← Zpět na úlohy + Detail cíle

{retry_blocked_notice} {actions} @@ -432,10 +440,10 @@ def job_detail_page(job_id: int, request: Request, user=Depends(require_user)): - - - - + + + + @@ -456,8 +464,8 @@ def job_detail_page(job_id: int, request: Request, user=Depends(require_user)):
-

Live logy

-

WebSocket: Connecting

+

Živé logy

+

WebSocket: Připojování

diff --git a/app/routes/operations.py b/app/routes/operations.py index 0c3b30f..9e6e4e8 100644 --- a/app/routes/operations.py +++ b/app/routes/operations.py @@ -135,55 +135,55 @@ def operations_dashboard(request: Request, user=Depends(require_user)): ) return page( - "Operations", + "Přehled", f"""
-

Operations Dashboard

-

Provozní přehled workerů, jobů, deploymentů a auditních událostí.

+

Přehled systému

+

Rychlá odpověď na otázku, zda jsou služby, úlohy a nasazení v pořádku.

-
Workers Online{snapshot["workers"]["online"]}
-
Running Jobs{snapshot["jobs"]["running"]}
-
Queued Jobs{snapshot["jobs"]["queued"]}
-
Failed Jobs (24h){snapshot["jobs"]["failed_24h"]}
-
Running Deployments{snapshot["deployments"]["running"]}
-
Failed Deployments (24h){snapshot["deployments"]["failed_24h"]}
+
Běžící služby{snapshot["apps"]["active"]}
+
Problémové služby{snapshot["apps"]["problematic"]}
+
Aktivní úlohy{snapshot["jobs"]["running"]}
+
Nasazení dnes{snapshot["deployments"]["today"]}
+
Online workery{snapshot["workers"]["online"]}
+
Selhané úlohy (24 h){snapshot["jobs"]["failed_24h"]}
-

Recent Jobs

+

Poslední incidenty

Typ{html.escape(job.get("type", "") or "")}
Status{status}
Source{html.escape(job.get("source", "") or "")}
Target{html.escape(target_type)}: {target_id_html}
Created by{html.escape(job.get("created_by_display_name") or job.get("created_by_username") or "")}
Created at{html.escape(job.get("created_at", "") or "")}
Zdroj{html.escape(job.get("source", "") or "")}
Cíl{html.escape(target_type)}: {target_id_html}
Vytvořil{html.escape(job.get("created_by_display_name") or job.get("created_by_username") or "")}
Vytvořeno{html.escape(job.get("created_at", "") or "")}
Started at{html.escape(job.get("started_at", "") or "")}
Finished at{html.escape(job.get("finished_at", "") or "")}
Duration{duration}
- - - - + + + + - {render_recent_jobs(snapshot["jobs"]["recent"])} + {render_recent_jobs(snapshot["jobs"]["recent_failed"])}
ID StatusTypeTargetSourceCreated AtTypCílZdrojVytvořeno
-

Recent Deployments

+

Poslední neúspěšná nasazení

- + - {render_recent_deployments(snapshot["deployments"]["recent"])} + {render_recent_deployments(snapshot["deployments"]["recent_failed"])}
IDAplikaceSlužba Typ Status Zdroj Spuštěno Dokončeno
-

Recent Audit Events

+

Poslední auditní události

diff --git a/app/routes/workers.py b/app/routes/workers.py index 80d99b0..2d6f532 100644 --- a/app/routes/workers.py +++ b/app/routes/workers.py @@ -17,8 +17,8 @@ DEFAULT_PAGE_SIZE = 20 def render_worker_badge(online: bool) -> str: if online: - return 'ONLINE' - return 'OFFLINE' + return 'online' + return 'offline' def pretty_json(value: str | None) -> str: @@ -131,24 +131,24 @@ def workers_page( ) return page( - "Workers", + "Workery", f"""
-

Workers

-

Přehled worker procesů obsluhujících AppFactory joby.

+

Workery

+

Přehled worker procesů obsluhujících úlohy portálu.

-
Online Workers{online_count}
-
Offline Workers{offline_count}
-
Total Workers{total_count}
+
Online workery{online_count}
+
Offline workery{offline_count}
+
Celkem workerů{total_count}

Filtry

- + Reset @@ -156,15 +156,15 @@ def workers_page(
-

Workers

+

Workery

{pagination}
Čas
- + - - - + + + {rows} @@ -217,18 +217,18 @@ def worker_detail_page(worker_id: str, request: Request, user=Depends(require_us

Worker {worker_id_html}

- ← Zpět na workers + ← Zpět na workery

Souhrn

Worker IDID workeru StatusLast SeenCurrent JobStarted AtNaposledy viděnAktuální úlohaSpuštěn Akce
- + - - - + + +
Worker ID{worker_id_html}
ID workeru{worker_id_html}
Status{badge}
{status}
Last Seen{last_seen_at}
Current Job{current_job}
Started At{started_at}
Naposledy viděn{last_seen_at}
Aktuální úloha{current_job}
Spuštěn{started_at}
diff --git a/app/static/operations-live.js b/app/static/operations-live.js index c4c866a..6a2f5b3 100644 --- a/app/static/operations-live.js +++ b/app/static/operations-live.js @@ -1,5 +1,5 @@ (() => { - if (window.AppFactoryOperationsLive) { + if (window.CSBotOperationsLive) { return; } @@ -19,6 +19,16 @@ const statusBadge = (status) => { const value = String(status || ""); const normalized = value.toLowerCase(); + const labels = { + queued: "čeká", + running: "běží", + cancelled_requested: "žádost o zrušení", + cancelled: "zrušeno", + success: "úspěšné", + failed: "selhalo", + ok: "úspěšné", + error: "chyba", + }; let className = "pill pill-muted"; if (["success", "ok", "succeeded", "done", "deployed", "completed"].includes(normalized)) { className = "pill pill-success"; @@ -27,12 +37,12 @@ } else if (["failed", "failure", "error", "cancelled", "canceled"].includes(normalized)) { className = "pill pill-danger"; } - return `${escapeHtml(value)}`; + return `${escapeHtml(labels[normalized] || value)}`; }; const workerBadge = (online) => online - ? 'ONLINE' - : 'OFFLINE'; + ? 'online' + : 'offline'; const setText = (selector, value) => { document.querySelectorAll(selector).forEach((el) => { @@ -51,7 +61,7 @@ ${escapeHtml(job.created_at)} `).join(""); - return rows || 'Zatím nejsou evidované žádné joby.'; + return rows || 'Zatím nejsou evidované žádné úlohy.'; }; const renderWorkers = (workers) => { @@ -107,16 +117,21 @@ setText("[data-live-count='jobs.running']", snapshot.jobs?.running); setText("[data-live-count='jobs.failed_24h']", snapshot.jobs?.failed_24h); setText("[data-live-count='jobs.success_24h']", snapshot.jobs?.success_24h); + setText("[data-live-count='apps.active']", snapshot.apps?.active); + setText("[data-live-count='apps.problematic']", snapshot.apps?.problematic); setText("[data-live-count='workers.online']", snapshot.workers?.online); setText("[data-live-count='workers.offline']", snapshot.workers?.offline); setText("[data-live-count='workers.total']", (snapshot.workers?.online || 0) + (snapshot.workers?.offline || 0)); setText("[data-live-count='deployments.running']", snapshot.deployments?.running); setText("[data-live-count='deployments.failed_24h']", snapshot.deployments?.failed_24h); setText("[data-live-count='deployments.success_24h']", snapshot.deployments?.success_24h); + setText("[data-live-count='deployments.today']", snapshot.deployments?.today); document.querySelectorAll("[data-live-table='jobs.recent']").forEach((el) => { el.innerHTML = renderJobs(snapshot.jobs?.recent); }); + document.querySelectorAll("[data-live-table='jobs.failed']").forEach((el) => { el.innerHTML = renderJobs(snapshot.jobs?.recent_failed); }); document.querySelectorAll("[data-live-table='workers.recent']").forEach((el) => { el.innerHTML = renderWorkers(snapshot.workers?.recent); }); document.querySelectorAll("[data-live-table='deployments.recent']").forEach((el) => { el.innerHTML = renderDeployments(snapshot.deployments?.recent); }); + document.querySelectorAll("[data-live-table='deployments.failed']").forEach((el) => { el.innerHTML = renderDeployments(snapshot.deployments?.recent_failed); }); document.querySelectorAll("[data-live-table='audit.recent']").forEach((el) => { el.innerHTML = renderAudit(snapshot.audit?.recent); }); (snapshot.workers?.recent || []).forEach((worker) => { @@ -159,6 +174,6 @@ }); }; - window.AppFactoryOperationsLive = { connect }; + window.CSBotOperationsLive = { connect }; connect(); })(); diff --git a/app/static/styles.css b/app/static/styles.css index 73dff27..e9e0b6e 100644 --- a/app/static/styles.css +++ b/app/static/styles.css @@ -76,6 +76,14 @@ nav a { border-radius: 8px; } +.nav-group { + color: rgba(255, 255, 255, 0.62); + margin-left: 14px; + font-size: 11px; + font-weight: 800; + text-transform: uppercase; +} + .user-menu { display: flex; align-items: center; diff --git a/app/templates/layout.py b/app/templates/layout.py index 15c7e00..a1a94bc 100644 --- a/app/templates/layout.py +++ b/app/templates/layout.py @@ -2,6 +2,8 @@ import html from ..config import PORTAL_PREFIX +APP_NAME = "CSBot Services Portal" + def page(title: str, body: str, user=None) -> str: nav = "" @@ -12,13 +14,17 @@ def page(title: str, body: str, user=None) -> str: display_name = html.escape(user.get("display_name") or user.get("username", "")) nav = """ """ user_panel = f""" @@ -34,15 +40,15 @@ def page(title: str, body: str, user=None) -> str: return f""" - {html.escape(title)} + {html.escape(title)} | {APP_NAME}
- + CSBOT - AppFactory + {APP_NAME} {nav} {user_panel}