# retoor <retoor@molodetz.nl>
from __future__ import annotations
from ..spec import Action
from ._shared import body, confirm, path, query
ADMIN_ACTIONS: tuple[Action, ...] = (
Action(
name="admin_overview",
method="GET",
path="/admin",
summary="View the admin overview",
requires_admin=True,
),
Action(
name="site_analytics",
method="GET",
path="/admin/analytics",
summary="Site-wide aggregate analytics in one call (admin only)",
description=(
"Returns JSON: total members, active users in the last 24h/7d/30d, signed_in_now, "
"new signups (24h/7d/30d), content totals (posts, comments, gists, projects, "
"news), and top authors. Use this for any 'how many'/'how active'/count question "
"instead of paging through admin_list_users. signed_in_now counts members holding an "
"unexpired session (logged in within the session lifetime, default 7-30 days), NOT a "
"real-time online count - see signed_in_definition in the response and never report it "
"as 'currently online' or 'logged in right now'."
),
params=(query("top_n", "How many top authors to include (1-50)."),),
requires_admin=True,
),
Action(
name="ai_usage",
method="GET",
path="/admin/ai-usage/data",
summary="AI gateway usage, cost, latency, and reliability metrics (admin only)",
description=(
"Returns JSON for a bounded window: request volume and throughput, token usage with "
"averages and percentiles, latency (upstream, gateway overhead, queue wait, connect), "
"error rates by category, cost in USD (per model, per caller, input vs output, projected "
"monthly burn, caching savings), caller behavior, and an hourly breakdown. Use this for "
"any question about AI spend, token consumption, gateway performance, or errors."
),
params=(
query("hours", "Lookback window in hours (1-168, default 48)."),
query("top_n", "How many rows in each top-N breakdown (default 10)."),
),
requires_admin=True,
),
Action(
name="audit_log",
method="GET",
path="/admin/audit-log",
summary="Query the platform audit trail with filters (admin only)",
description=(
"Returns JSON: a paginated, newest-first list of audit events (every state-changing "
"action on the platform, with actor, origin, target, old/new value, and result), plus "
"pagination, the active filters, and an 'options' object listing every available value "
"for each filter (category, event_key, actor_role, origin, result) so you can both "
"filter and discover valid filter values from a single call. Use this for any audit, "
"history, 'who did X', 'what changed', or 'show denied/failed actions' question. Filter "
"by event_key (exact), category, actor_role, actor_uid, origin (web, api, devii, cli, "
"service, scheduler), or result (success, failure, denied); narrow by date_from/date_to "
"(ISO date bounds, inclusive) or free-text q over summary, event key, and target."
),
params=(
query("page", "Page number (1-based)."),
query("event_key", "Filter by exact event key (e.g. admin.setting.update)."),
query("category", "Filter by category (auth, content, admin, container, ...)."),
query("actor_role", "Filter by actor role at action time."),
query("actor_uid", "Filter by acting user uid."),
query("origin", "Filter by origin (web, api, devii, cli, service, scheduler)."),
query("result", "Filter by result (success, failure, denied)."),
query("q", "Free-text search over summary, event key, and target."),
query("date_from", "ISO date lower bound (inclusive)."),
query("date_to", "ISO date upper bound (inclusive)."),
),
requires_admin=True,
),
Action(
name="audit_event",
method="GET",
path="/admin/audit-log/{uid}",
summary="Get one audit event with its related-object links (admin only)",
description=(
"Returns JSON: the full audit event row plus every related-object link (actor, target, "
"parent, project, instance, setting, ...). Use after audit_log to inspect a single event "
"in detail."
),
params=(path("uid", "Audit event uid."),),
requires_admin=True,
),
Action(
name="bot_monitor",
method="GET",
path="/admin/bots/data",
summary="Live screenshot monitor of every running bot persona (admin only)",
description=(
"Returns JSON: one entry per running bot slot with its username, persona, current "
"action and status text, last page url, frame age in seconds, whether it is active, "
"and a frame_url to the latest low-quality screenshot. Use this to see what the bot "
"fleet is doing right now. Frames live only in the worker running the Bots service."
),
requires_admin=True,
),
Action(
name="admin_list_users",
method="GET",
path="/admin/users",
summary="List users for administration",
params=(query("page", "Page number."),),
requires_admin=True,
),
Action(
name="admin_set_user_role",
method="POST",
path="/admin/users/{uid}/role",
summary="Set a user's role",
params=(
path("uid", "User uid."),
body("role", "New role.", required=True),
),
requires_admin=True,
),
Action(
name="admin_set_user_password",
method="POST",
path="/admin/users/{uid}/password",
summary="Set a user's password",
params=(
path("uid", "User uid."),
body("password", "New password.", required=True),
),
requires_admin=True,
),
Action(
name="admin_toggle_user",
method="POST",
path="/admin/users/{uid}/toggle",
summary="Toggle a user's active state",
params=(path("uid", "User uid."),),
requires_admin=True,
),
Action(
name="admin_get_settings",
method="GET",
path="/admin/settings",
summary="View site settings",
requires_admin=True,
),
Action(
name="admin_save_settings",
method="POST",
path="/admin/settings",
summary="Save site settings",
params=(
body("site_name", "Site name."),
body("site_description", "Site description."),
body("site_tagline", "Site tagline."),
body("max_upload_size_mb", "Maximum upload size in megabytes."),
body("allowed_file_types", "Allowed file types."),
body("max_attachments_per_resource", "Maximum attachments per resource."),
body("rate_limit_per_minute", "Rate limit per minute."),
body("rate_limit_window_seconds", "Rate limit window in seconds."),
body("session_max_age_days", "Session maximum age in days."),
body("session_remember_days", "Remember-me duration in days."),
body("registration_open", "Whether registration is open."),
body("maintenance_mode", "Whether maintenance mode is on."),
body("maintenance_message", "Maintenance message."),
),
requires_admin=True,
),
Action(
name="admin_list_news",
method="GET",
path="/admin/news",
summary="List news for administration",
params=(query("page", "Page number."),),
requires_admin=True,
),
Action(
name="admin_toggle_news",
method="POST",
path="/admin/news/{uid}/toggle",
summary="Toggle a news article",
params=(path("uid", "News uid."),),
requires_admin=True,
),
Action(
name="admin_publish_news",
method="POST",
path="/admin/news/{uid}/publish",
summary="Publish a news article",
params=(path("uid", "News uid."),),
requires_admin=True,
),
Action(
name="admin_landing_news",
method="POST",
path="/admin/news/{uid}/landing",
summary="Set a news article as landing content",
params=(path("uid", "News uid."),),
requires_admin=True,
),
Action(
name="admin_delete_news",
method="POST",
path="/admin/news/{uid}/delete",
summary="Delete a news article (admin only). Soft delete, confirmation required",
params=(path("uid", "News uid."), confirm()),
requires_admin=True,
),
Action(
name="admin_list_services",
method="GET",
path="/admin/services",
summary="View managed services",
requires_admin=True,
),
Action(
name="admin_services_data",
method="GET",
path="/admin/services/data",
summary="Get live status, metrics, and log tail for every background service",
requires_admin=True,
),
Action(
name="admin_service_status",
method="GET",
path="/admin/services/{name}/data",
summary="Get live status, metrics, and log tail for one background service",
params=(path("name", "Service name (e.g. news, bots, openai)."),),
requires_admin=True,
),
Action(
name="admin_start_service",
method="POST",
path="/admin/services/{name}/start",
summary="Start a managed service",
params=(path("name", "Service name."),),
requires_admin=True,
),
Action(
name="admin_stop_service",
method="POST",
path="/admin/services/{name}/stop",
summary="Stop a managed service",
params=(path("name", "Service name."),),
requires_admin=True,
),
Action(
name="admin_run_service",
method="POST",
path="/admin/services/{name}/run",
summary="Run a managed service once",
params=(path("name", "Service name."),),
requires_admin=True,
),
Action(
name="admin_clear_service_logs",
method="POST",
path="/admin/services/{name}/clear-logs",
summary="Clear a managed service's logs",
params=(path("name", "Service name."),),
requires_admin=True,
),
Action(
name="admin_config_service",
method="POST",
path="/admin/services/{name}/config",
summary="Update a managed service's configuration",
params=(path("name", "Service name."),),
freeform_body=True,
requires_admin=True,
),
Action(
name="admin_user_ai_usage",
method="GET",
path="/admin/users/{uid}/ai-usage",
summary="Get a user's AI gateway usage (admin only)",
description="Returns per-user AI usage data including token counts, cost, and request volume for the given lookback window.",
params=(
path("uid", "User uid."),
query("hours", "Lookback window in hours (default 24)."),
),
requires_admin=True,
),
Action(
name="admin_reset_user_ai_quota",
method="POST",
path="/admin/users/{uid}/reset-ai-quota",
summary="Reset a user's AI quota (admin only)",
description="Clears the AI quota ledger for a specific user, allowing them to use AI features again.",
params=(path("uid", "User uid."), confirm()),
requires_admin=True,
),
Action(
name="admin_media_purge",
method="POST",
path="/admin/media/{uid}/purge",
summary="Permanently remove soft-deleted media (admin only)",
description="Permanently deletes a soft-deleted attachment from the database and filesystem.",
params=(path("uid", "Attachment uid to purge."), confirm()),
requires_admin=True,
),
Action(
name="admin_reset_guest_ai_quota",
method="POST",
path="/admin/ai-quota/reset-guests",
summary="Reset all guest AI quotas (admin only)",
description="Clears the AI quota ledger for all guest sessions.",
params=(confirm(),),
requires_admin=True,
),
Action(
name="admin_reset_all_ai_quota",
method="POST",
path="/admin/ai-quota/reset-all",
summary="Reset ALL AI quotas including member quotas (admin only)",
description="Clears the AI quota ledger for every user and guest. Use with caution.",
params=(confirm(),),
requires_admin=True,
),
Action(
name="backups_overview",
method="GET",
path="/admin/backups/data",
summary="Backup dashboard: storage usage, backups, and schedules (admin only)",
description=(
"Returns JSON: per-path storage usage (database, uploads, attachments, project files, "
"keys, zips, deepsearch, container workspaces, backups), total data-directory size, total "
"stored-backup size and count, disk usage (total/used/free/percent), every backup archive "
"(target, status, size, file count, sha256 checksum, created/completed time, download_url), "
"and every configured backup schedule. Use this for any question about backups, data "
"footprint, disk space, or what is scheduled."
),
requires_admin=True,
),
Action(
name="backup_run",
method="POST",
path="/admin/backups/run",
summary="Start a backup of a data target (admin only)",
description=(
"Enqueues an async backup job and returns its job uid and status_url. Target is one of: "
"database (consistent SQLite snapshot plus Devii databases), uploads (attachments and "
"project files), keys (VAPID keys and config), or full (database, uploads, and keys in one "
"archive). Poll backup_status with the returned uid until status is done, then read the "
"download_url. The job runs off the request path and never blocks the server."
),
params=(
body("target", "One of database, uploads, keys, full.", required=True),
),
requires_admin=True,
),
Action(
name="backup_status",
method="GET",
path="/admin/backups/jobs/{uid}",
summary="Check the status of a backup job (admin only)",
description=(
"Returns JSON for one backup job: status (pending, running, done, failed), target, "
"backup_uid, download_url (when done), sha256, archive size, file count, and timestamps. "
"Poll this after backup_run."
),
params=(path("uid", "Backup job uid returned by backup_run."),),
requires_admin=True,
),
Action(
name="backup_delete",
method="POST",
path="/admin/backups/{uid}/delete",
summary="Permanently delete a backup archive (admin only)",
description=(
"Removes a backup archive file and its record. This is irreversible and reclaims disk "
"space. The uid is a backup uid (from backups_overview), not a job uid."
),
params=(path("uid", "Backup uid to delete."), confirm()),
requires_admin=True,
),
Action(
name="backup_schedule_create",
method="POST",
path="/admin/backups/schedules/create",
summary="Create a recurring backup schedule (admin only)",
description=(
"Creates a schedule that fires backups automatically. kind is 'interval' (use every_seconds, "
"minimum 60) or 'cron' (use a 5-field cron expression in 'minute hour dom month dow'). "
"keep_last rotates older backups of this schedule, keeping only the newest N (0 keeps all). "
"target is one of database, uploads, keys, full."
),
params=(
body("name", "Human-readable schedule name.", required=True),
body("target", "One of database, uploads, keys, full.", required=True),
body("kind", "interval or cron.", required=True),
body("every_seconds", "Seconds between runs when kind=interval (min 60)."),
body("cron", "Cron expression when kind=cron, e.g. '0 3 * * *'."),
body("keep_last", "Keep only the newest N backups of this schedule (0 = all)."),
),
requires_admin=True,
),
Action(
name="backup_schedule_delete",
method="POST",
path="/admin/backups/schedules/{uid}/delete",
summary="Delete a backup schedule (admin only)",
description=(
"Removes a backup schedule so it stops firing. Existing backup archives are kept; only the "
"schedule is deleted."
),
params=(path("uid", "Backup schedule uid."), confirm()),
requires_admin=True,
),
Action(
name="restore_media",
method="POST",
path="/media/{uid}/restore",
summary="Restore a soft-deleted media attachment (admin only)",
description="Restores a previously soft-deleted attachment, making it visible again.",
params=(path("uid", "Attachment uid to restore."),),
requires_admin=True,
),
)