|
# retoor <retoor@molodetz.nl>
|
|
|
|
import logging
|
|
|
|
from fastapi import APIRouter, Request
|
|
from fastapi.responses import HTMLResponse
|
|
|
|
from devplacepy.database import db, get_admin_uids, get_int_setting, get_users_by_uids
|
|
from devplacepy.responses import action_result, respond
|
|
from devplacepy.schemas import AdminDeviiTasksOut
|
|
from devplacepy.seo import base_seo_context, site_url, website_schema
|
|
from devplacepy.services.audit import record as audit
|
|
from devplacepy.services.devii import config as devii_config
|
|
from devplacepy.services.devii.tasks import limits
|
|
from devplacepy.services.devii.tasks.guards import DEFAULT_MAX_PER_OWNER
|
|
from devplacepy.services.devii.tasks.schedule import now_utc
|
|
from devplacepy.services.devii.tasks.store import TABLE, TaskStore
|
|
from devplacepy.utils import not_found, require_admin
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter()
|
|
|
|
STATES = ("active", "inactive", "all")
|
|
|
|
|
|
def _schedule_text(row: dict) -> str:
|
|
kind = row.get("kind") or ""
|
|
if kind == "interval":
|
|
return f"every {row.get('every_seconds')}s"
|
|
if kind == "cron":
|
|
return f"cron {row.get('cron')}"
|
|
return f"once {row.get('run_at') or ''}".strip()
|
|
|
|
|
|
def _rows(state: str) -> list[dict]:
|
|
if TABLE not in db.tables:
|
|
return []
|
|
criteria: dict = {"deleted_at": None}
|
|
if state == "active":
|
|
criteria["enabled"] = True
|
|
elif state == "inactive":
|
|
criteria["enabled"] = False
|
|
rows = list(db[TABLE].find(**criteria))
|
|
rows.sort(key=lambda row: int(row.get("run_count") or 0), reverse=True)
|
|
return rows
|
|
|
|
|
|
def _quotas(owner_uids: set[str]) -> dict[str, dict]:
|
|
reference = now_utc()
|
|
quotas = {}
|
|
for owner_uid in owner_uids:
|
|
runs = limits.run_quota(db, "user", owner_uid, reference)
|
|
creations = limits.create_quota(db, "user", owner_uid, reference)
|
|
quotas[owner_uid] = {
|
|
"runs_used": runs.used,
|
|
"runs_limit": runs.limit,
|
|
"creates_used": creations.used,
|
|
"creates_limit": creations.limit,
|
|
}
|
|
return quotas
|
|
|
|
|
|
def _items(rows: list[dict]) -> list[dict]:
|
|
owners = get_users_by_uids([row.get("owner_id") for row in rows if row.get("owner_id")])
|
|
admins = get_admin_uids()
|
|
quotas = _quotas({str(row.get("owner_id") or "") for row in rows if row.get("owner_id")})
|
|
items = []
|
|
for row in rows:
|
|
owner_uid = row.get("owner_id") or ""
|
|
owner = owners.get(owner_uid)
|
|
items.append(
|
|
{
|
|
"uid": row.get("uid"),
|
|
"label": row.get("label") or row.get("uid"),
|
|
"owner_uid": owner_uid,
|
|
"owner": owner["username"] if owner else owner_uid,
|
|
"owner_is_admin": owner_uid in admins,
|
|
"quota": quotas.get(owner_uid, {}),
|
|
"schedule": _schedule_text(row),
|
|
"status": row.get("status") or "",
|
|
"enabled": bool(row.get("enabled")),
|
|
"run_count": int(row.get("run_count") or 0),
|
|
"max_runs": row.get("max_runs"),
|
|
"failure_count": int(row.get("failure_count") or 0),
|
|
"next_run_at": row.get("next_run_at"),
|
|
"expires_at": row.get("expires_at"),
|
|
"last_error": row.get("last_error"),
|
|
}
|
|
)
|
|
return items
|
|
|
|
|
|
def _require_task(uid: str) -> dict:
|
|
row = db[TABLE].find_one(uid=uid, deleted_at=None) if TABLE in db.tables else None
|
|
if row is None:
|
|
raise not_found("Unknown task")
|
|
return row
|
|
|
|
|
|
@router.get("/devii-tasks", response_class=HTMLResponse)
|
|
async def admin_devii_tasks(request: Request, state: str = "active"):
|
|
admin = require_admin(request)
|
|
if state not in STATES:
|
|
state = "active"
|
|
rows = _rows(state)
|
|
items = _items(rows)
|
|
bounds = {
|
|
"max_concurrent": get_int_setting(
|
|
devii_config.FIELD_TASK_MAX_CONCURRENT,
|
|
devii_config.DEFAULT_TASK_MAX_CONCURRENT,
|
|
),
|
|
"max_per_owner": get_int_setting(
|
|
devii_config.FIELD_TASK_MAX_PER_OWNER, DEFAULT_MAX_PER_OWNER
|
|
),
|
|
"max_failures": get_int_setting(
|
|
devii_config.FIELD_TASK_MAX_FAILURES,
|
|
devii_config.DEFAULT_TASK_MAX_FAILURES,
|
|
),
|
|
"idle_days": get_int_setting(
|
|
devii_config.FIELD_TASK_IDLE_DAYS, devii_config.DEFAULT_TASK_IDLE_DAYS
|
|
),
|
|
"member_create_24h": limits.create_limit(False),
|
|
"member_runs_24h": limits.run_limit(False),
|
|
"admin_create_24h": limits.create_limit(True),
|
|
"admin_runs_24h": limits.run_limit(True),
|
|
}
|
|
tabs = [
|
|
{"key": key, "label": key.capitalize(), "active": key == state}
|
|
for key in STATES
|
|
]
|
|
base = site_url(request)
|
|
seo_ctx = base_seo_context(
|
|
request,
|
|
title="Devii tasks - Admin",
|
|
description="Every scheduled Devii task, its owner, and its bounds.",
|
|
breadcrumbs=[
|
|
{"name": "Home", "url": "/feed"},
|
|
{"name": "Admin", "url": "/admin"},
|
|
{"name": "Devii tasks", "url": "/admin/devii-tasks"},
|
|
],
|
|
schemas=[website_schema(base)],
|
|
)
|
|
return respond(
|
|
request,
|
|
"admin_devii_tasks.html",
|
|
{
|
|
**seo_ctx,
|
|
"request": request,
|
|
"user": admin,
|
|
"items": items,
|
|
"state": state,
|
|
"tabs": tabs,
|
|
"limits": bounds,
|
|
"admin_section": "devii-tasks",
|
|
},
|
|
model=AdminDeviiTasksOut,
|
|
)
|
|
|
|
|
|
@router.post("/devii-tasks/{uid}/disable")
|
|
async def admin_devii_task_disable(request: Request, uid: str):
|
|
admin = require_admin(request)
|
|
row = _require_task(uid)
|
|
store = TaskStore(db, str(row.get("owner_kind") or "user"), str(row.get("owner_id") or ""))
|
|
store.update(
|
|
uid,
|
|
{
|
|
"enabled": False,
|
|
"status": "disabled",
|
|
"next_run_at": None,
|
|
"last_error": f"disabled by {admin['username']}",
|
|
},
|
|
)
|
|
logger.info(f"Admin {admin['username']} disabled Devii task {uid}")
|
|
audit.record(
|
|
request,
|
|
"admin.devii_task.disable",
|
|
user=admin,
|
|
target_type="task",
|
|
target_uid=uid,
|
|
target_label=row.get("label"),
|
|
metadata={"owner_id": row.get("owner_id"), "run_count": row.get("run_count")},
|
|
summary=f"{admin['username']} disabled Devii task {uid}",
|
|
)
|
|
return action_result(request, "/admin/devii-tasks")
|
|
|
|
|
|
@router.post("/devii-tasks/{uid}/delete")
|
|
async def admin_devii_task_delete(request: Request, uid: str):
|
|
admin = require_admin(request)
|
|
row = _require_task(uid)
|
|
store = TaskStore(db, str(row.get("owner_kind") or "user"), str(row.get("owner_id") or ""))
|
|
store.delete(uid)
|
|
logger.info(f"Admin {admin['username']} deleted Devii task {uid}")
|
|
audit.record(
|
|
request,
|
|
"admin.devii_task.delete",
|
|
user=admin,
|
|
target_type="task",
|
|
target_uid=uid,
|
|
target_label=row.get("label"),
|
|
metadata={"owner_id": row.get("owner_id"), "run_count": row.get("run_count")},
|
|
summary=f"{admin['username']} deleted Devii task {uid}",
|
|
)
|
|
return action_result(request, "/admin/devii-tasks")
|