Fix fanout gaps: statistics JSON 500, schema drops, undocumented admin routes
- Fix StatisticsOut to match the real admin_statistics page context (active_tab/tabs/window_hours/initial), which was raising a ValidationError and 500ing GET /admin/statistics with Accept: application/json. - Add missing *Out fields dropped from JSON responses: IsslopSourceOut (source_lines, marked_lines, focus_line, report_url), IsslopReportOut (report_url, badge_url, events_url, topic), IssuesOut (viewer_is_admin), GameStateOut/GameFarmViewOut (game_error), QuizAttemptPageOut (answer_max_chars, quiz_error), QuizBuilderOut (quiz_error). - Convert GET /admin/issues/planning to respond() with a new AdminIssuesPlanningOut schema so it serves JSON like every sibling admin dashboard, and document it in docs_api. - Document previously-undocumented admin endpoint families in docs_api: services page routes, gateway provider/model CRUD, admin workspaces (11 routes), and trash list/restore/purge. - Correct stale references to services/devii/actions/catalog.py as a single file; it is the actions/catalog/ package. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VL9Xn57W5UR3HZbbuuzxdK
This commit is contained in:
@@ -265,6 +265,29 @@ four ways to sign requests.
|
||||
),
|
||||
],
|
||||
),
|
||||
endpoint(
|
||||
id="admin-issues-planning",
|
||||
method="GET",
|
||||
path="/admin/issues/planning",
|
||||
title="Ticket planning report",
|
||||
summary=(
|
||||
"Admin page listing every open Gitea ticket so an admin can pick a subset "
|
||||
"and generate a grouped, ordered planning document for a coding agent."
|
||||
),
|
||||
auth="admin",
|
||||
interactive=False,
|
||||
sample_response={
|
||||
"configured": True,
|
||||
"tickets": [
|
||||
{"number": 42, "title": "Fix login redirect", "labels": ["bug"]},
|
||||
],
|
||||
"tickets_error": False,
|
||||
},
|
||||
notes=[
|
||||
"`configured` is false when the issue tracker (Gitea) has not been set up in Services yet - `tickets` is then empty.",
|
||||
"`tickets_error` is true when the tracker is configured but the live fetch failed - retry shortly.",
|
||||
],
|
||||
),
|
||||
endpoint(
|
||||
id="admin-ai-usage",
|
||||
method="GET",
|
||||
@@ -613,6 +636,84 @@ four ways to sign requests.
|
||||
auth="admin",
|
||||
destructive=True,
|
||||
),
|
||||
endpoint(
|
||||
id="admin-gateway-page",
|
||||
method="GET",
|
||||
path="/admin/gateway",
|
||||
title="Gateway routing dashboard",
|
||||
summary="Admin HTML page for managing AI gateway providers and per-model routing.",
|
||||
auth="admin",
|
||||
interactive=False,
|
||||
),
|
||||
endpoint(
|
||||
id="admin-gateway-providers",
|
||||
method="GET",
|
||||
path="/admin/gateway/providers",
|
||||
title="List AI gateway providers",
|
||||
summary="List every configured upstream provider plus the default provider summary.",
|
||||
auth="admin",
|
||||
interactive=True,
|
||||
),
|
||||
endpoint(
|
||||
id="admin-gateway-provider-set",
|
||||
method="POST",
|
||||
path="/admin/gateway/providers",
|
||||
title="Create or update an AI gateway provider",
|
||||
summary="Save an upstream provider (base URL, model, and API key) by name. Pass name to update an existing provider.",
|
||||
auth="admin",
|
||||
params=[
|
||||
field("name", "json", "string", True, "openrouter", "Provider name; existing name updates in place."),
|
||||
field("base_url", "json", "string", True, "https://openrouter.ai/api/v1", "Upstream chat completions base URL."),
|
||||
field("model", "json", "string", True, "x-ai/grok-4.3", "Default model for this provider."),
|
||||
field("api_key", "json", "string", False, "", "Upstream API key. Blank keeps the current key."),
|
||||
],
|
||||
),
|
||||
endpoint(
|
||||
id="admin-gateway-provider-delete",
|
||||
method="DELETE",
|
||||
path="/admin/gateway/providers/{name}",
|
||||
title="Delete an AI gateway provider",
|
||||
summary="Delete a configured upstream provider by name.",
|
||||
auth="admin",
|
||||
destructive=True,
|
||||
params=[
|
||||
field("name", "path", "string", True, "openrouter", "Provider name."),
|
||||
],
|
||||
),
|
||||
endpoint(
|
||||
id="admin-gateway-models",
|
||||
method="GET",
|
||||
path="/admin/gateway/models",
|
||||
title="List AI gateway model routes",
|
||||
summary="List every source-to-target model route plus the configured provider names.",
|
||||
auth="admin",
|
||||
interactive=True,
|
||||
),
|
||||
endpoint(
|
||||
id="admin-gateway-model-set",
|
||||
method="POST",
|
||||
path="/admin/gateway/models",
|
||||
title="Create or update an AI gateway model route",
|
||||
summary="Route a source model name to a target model, optionally on a specific provider. Pass source_model to update an existing route.",
|
||||
auth="admin",
|
||||
params=[
|
||||
field("source_model", "json", "string", True, "gpt-4", "Model name callers request."),
|
||||
field("target_model", "json", "string", True, "x-ai/grok-4.3", "Model actually sent upstream."),
|
||||
field("provider", "json", "string", False, "openrouter", "Provider name to route through. Blank = the default provider."),
|
||||
],
|
||||
),
|
||||
endpoint(
|
||||
id="admin-gateway-model-delete",
|
||||
method="DELETE",
|
||||
path="/admin/gateway/models/{source_model}",
|
||||
title="Delete an AI gateway model route",
|
||||
summary="Delete a source-to-target model route by source model name.",
|
||||
auth="admin",
|
||||
destructive=True,
|
||||
params=[
|
||||
field("source_model", "path", "string", True, "gpt-4", "Source model name."),
|
||||
],
|
||||
),
|
||||
endpoint(
|
||||
id="admin-gateway-quota-rules",
|
||||
method="GET",
|
||||
@@ -975,5 +1076,190 @@ four ways to sign requests.
|
||||
destructive=True,
|
||||
sample_response={"ok": True, "redirect": "/admin/game"},
|
||||
),
|
||||
endpoint(
|
||||
id="admin-workspaces-page",
|
||||
method="GET",
|
||||
path="/admin/workspaces",
|
||||
title="Workspaces dashboard",
|
||||
summary="Admin HTML page listing every dev workspace across all projects, with owner, project, and moderation flags.",
|
||||
auth="admin",
|
||||
interactive=False,
|
||||
),
|
||||
endpoint(
|
||||
id="admin-workspaces-data",
|
||||
method="GET",
|
||||
path="/admin/workspaces/data",
|
||||
title="List workspaces",
|
||||
summary="Every dev workspace across all projects plus open moderation flags, as JSON.",
|
||||
auth="admin",
|
||||
interactive=True,
|
||||
sample_response={"workspaces": [], "flags": []},
|
||||
),
|
||||
endpoint(
|
||||
id="admin-workspaces-suspend",
|
||||
method="POST",
|
||||
path="/admin/workspaces/{uid}/suspend",
|
||||
title="Suspend a workspace",
|
||||
summary="Suspend a workspace with a reason shown to its owner. The owner is notified.",
|
||||
auth="admin",
|
||||
destructive=True,
|
||||
params=[
|
||||
field("uid", "path", "string", True, "INSTANCE_UID", "Workspace instance uid."),
|
||||
field("reason", "form", "string", True, "Excessive resource usage", "Shown to the workspace owner."),
|
||||
],
|
||||
),
|
||||
endpoint(
|
||||
id="admin-workspaces-unsuspend",
|
||||
method="POST",
|
||||
path="/admin/workspaces/{uid}/unsuspend",
|
||||
title="Unsuspend a workspace",
|
||||
summary="Lift a suspension. The owner is notified the workspace is available again.",
|
||||
auth="admin",
|
||||
params=[
|
||||
field("uid", "path", "string", True, "INSTANCE_UID", "Workspace instance uid."),
|
||||
],
|
||||
),
|
||||
endpoint(
|
||||
id="admin-workspaces-stop",
|
||||
method="POST",
|
||||
path="/admin/workspaces/{uid}/stop",
|
||||
title="Stop a workspace",
|
||||
summary="Stop the workspace container. Files and tunnels are kept.",
|
||||
auth="admin",
|
||||
destructive=True,
|
||||
params=[
|
||||
field("uid", "path", "string", True, "INSTANCE_UID", "Workspace instance uid."),
|
||||
],
|
||||
),
|
||||
endpoint(
|
||||
id="admin-workspaces-start",
|
||||
method="POST",
|
||||
path="/admin/workspaces/{uid}/start",
|
||||
title="Start a workspace",
|
||||
summary="Resume a stopped workspace container.",
|
||||
auth="admin",
|
||||
params=[
|
||||
field("uid", "path", "string", True, "INSTANCE_UID", "Workspace instance uid."),
|
||||
],
|
||||
),
|
||||
endpoint(
|
||||
id="admin-workspaces-delete",
|
||||
method="POST",
|
||||
path="/admin/workspaces/{uid}/delete",
|
||||
title="Delete a workspace",
|
||||
summary="Remove a workspace and its tunnels.",
|
||||
auth="admin",
|
||||
destructive=True,
|
||||
params=[
|
||||
field("uid", "path", "string", True, "INSTANCE_UID", "Workspace instance uid."),
|
||||
],
|
||||
),
|
||||
endpoint(
|
||||
id="admin-workspaces-flag",
|
||||
method="POST",
|
||||
path="/admin/workspaces/{uid}/flag",
|
||||
title="Flag a workspace",
|
||||
summary="Raise a moderation flag on a workspace. The owner is notified.",
|
||||
auth="admin",
|
||||
params=[
|
||||
field("uid", "path", "string", True, "INSTANCE_UID", "Workspace instance uid."),
|
||||
field("kind", "form", "string", False, "manual", "Flag kind."),
|
||||
field("severity", "form", "string", False, "warn", "Flag severity."),
|
||||
field("detail", "form", "string", False, "", "Detail shown to the owner."),
|
||||
],
|
||||
),
|
||||
endpoint(
|
||||
id="admin-workspaces-flag-resolve",
|
||||
method="POST",
|
||||
path="/admin/workspaces/flags/{flag_uid}/resolve",
|
||||
title="Resolve or dismiss a workspace flag",
|
||||
summary="Set a moderation flag's status.",
|
||||
auth="admin",
|
||||
params=[
|
||||
field("flag_uid", "path", "string", True, "FLAG_UID", "Flag uid."),
|
||||
field("status", "query", "string", False, "resolved", "resolved or dismissed."),
|
||||
],
|
||||
),
|
||||
endpoint(
|
||||
id="admin-workspaces-editor",
|
||||
method="POST",
|
||||
path="/admin/workspaces/{uid}/editor",
|
||||
title="Set a workspace owner's editor preferences",
|
||||
summary="Change the workspace owner's editor preferences on their behalf, or reset them. Subject to admin seniority.",
|
||||
auth="admin",
|
||||
params=[
|
||||
field("uid", "path", "string", True, "INSTANCE_UID", "Workspace instance uid."),
|
||||
field("theme", "form", "string", False, "devplace-dark", "devplace-dark, devplace-light or system."),
|
||||
field("layout", "form", "string", False, "standard", "standard, terminal-focus or zen."),
|
||||
field("panel_preset", "form", "string", False, "tall", "short, normal, tall or maximized."),
|
||||
field("font_size", "form", "integer", False, "14", "Editor font size in pixels."),
|
||||
field("terminal_font_size", "form", "integer", False, "13", "Terminal font size in pixels."),
|
||||
field("zoom_level", "form", "integer", False, "0", "Window zoom, -5 to 5."),
|
||||
field("boot_agent", "form", "string", False, "dpc", "dpc or none."),
|
||||
field("boot_shell", "form", "integer", False, "1", "1 opens a shell on boot, 0 skips it."),
|
||||
field("window_mode", "form", "string", False, "tab", "tab, window or fullscreen."),
|
||||
field("window_width", "form", "integer", False, "1600", "Editor window width in pixels."),
|
||||
field("window_height", "form", "integer", False, "1000", "Editor window height in pixels."),
|
||||
field("reset", "form", "boolean", False, "false", "Drop every preference for this owner."),
|
||||
],
|
||||
),
|
||||
endpoint(
|
||||
id="admin-workspaces-quota",
|
||||
method="POST",
|
||||
path="/admin/workspaces/quota",
|
||||
title="Set a user's workspace quota override",
|
||||
summary="Set or update a per-user override of the workspace count/disk/egress/idle/retention/CPU/memory limits.",
|
||||
auth="admin",
|
||||
params=[
|
||||
field("owner_id", "form", "string", True, "USER_UID", "Target user uid."),
|
||||
field("label", "form", "string", False, "", "Optional admin-facing note."),
|
||||
field("max_workspaces", "form", "int", False, "0", "0 = use the site default."),
|
||||
field("max_tunnels", "form", "int", False, "0", "0 = use the site default."),
|
||||
field("disk_quota_mb", "form", "int", False, "0", "0 = use the site default."),
|
||||
field("egress_quota_mb", "form", "int", False, "0", "0 = use the site default."),
|
||||
field("idle_stop_minutes", "form", "int", False, "0", "0 = use the site default."),
|
||||
field("retention_days", "form", "int", False, "0", "0 = use the site default."),
|
||||
field("cpu_millicores", "form", "int", False, "0", "0 = use the site default."),
|
||||
field("memory_mb", "form", "int", False, "0", "0 = use the site default."),
|
||||
],
|
||||
),
|
||||
endpoint(
|
||||
id="admin-trash-list",
|
||||
method="GET",
|
||||
path="/admin/trash",
|
||||
title="Trash",
|
||||
summary="Admin HTML page listing soft-deleted rows for one table, with restore/purge controls per row.",
|
||||
auth="admin",
|
||||
interactive=True,
|
||||
params=[
|
||||
field("table", "query", "string", False, "posts", "Trash table key (posts, comments, gists, projects, news, awards, quizzes, project_files, attachments)."),
|
||||
field("page", "query", "int", False, "1", "Page number."),
|
||||
],
|
||||
),
|
||||
endpoint(
|
||||
id="admin-trash-restore",
|
||||
method="POST",
|
||||
path="/admin/trash/{table}/{uid}/restore",
|
||||
title="Restore a soft-deleted row",
|
||||
summary="Restore every row soft-deleted under the same event timestamp as the given row (a whole delete cascade at once).",
|
||||
auth="admin",
|
||||
params=[
|
||||
field("table", "path", "string", True, "posts", "Trash table key."),
|
||||
field("uid", "path", "string", True, "ROW_UID", "Row uid."),
|
||||
],
|
||||
),
|
||||
endpoint(
|
||||
id="admin-trash-purge",
|
||||
method="POST",
|
||||
path="/admin/trash/{table}/{uid}/purge",
|
||||
title="Purge a soft-deleted row",
|
||||
summary="Permanently delete every row soft-deleted under the same event timestamp as the given row, unlinking any attachment/project-file blobs.",
|
||||
auth="admin",
|
||||
destructive=True,
|
||||
params=[
|
||||
field("table", "path", "string", True, "posts", "Trash table key."),
|
||||
field("uid", "path", "string", True, "ROW_UID", "Row uid."),
|
||||
],
|
||||
),
|
||||
],
|
||||
}
|
||||
|
||||
@@ -117,6 +117,25 @@ def build_services_group(services, base):
|
||||
)
|
||||
)
|
||||
control_endpoints = [
|
||||
endpoint(
|
||||
id="services-page",
|
||||
method="GET",
|
||||
path="/admin/services",
|
||||
title="Services dashboard",
|
||||
summary="Admin HTML index of every registered background service, its status, and controls.",
|
||||
auth="admin",
|
||||
interactive=False,
|
||||
),
|
||||
endpoint(
|
||||
id="services-detail-page",
|
||||
method="GET",
|
||||
path="/admin/services/{name}",
|
||||
title="Service detail page",
|
||||
summary="Admin HTML detail page for a single service: overview, configuration form, and logs.",
|
||||
auth="admin",
|
||||
interactive=False,
|
||||
params=[field("name", "path", "string", True, "news", "Service name.")],
|
||||
),
|
||||
endpoint(
|
||||
id="services-data",
|
||||
method="GET",
|
||||
|
||||
@@ -5,11 +5,12 @@ import logging
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
|
||||
from devplacepy.responses import respond
|
||||
from devplacepy.schemas import AdminIssuesPlanningOut
|
||||
from devplacepy.seo import base_seo_context
|
||||
from devplacepy.services.gitea import runtime
|
||||
from devplacepy.services.gitea.config import gitea_config
|
||||
from devplacepy.services.gitea.planning import collect_open_issues
|
||||
from devplacepy.templating import templates
|
||||
from devplacepy.utils import require_admin
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -65,4 +66,4 @@ async def admin_issues_planning(request: Request):
|
||||
"tickets": tickets,
|
||||
"tickets_error": tickets_error,
|
||||
}
|
||||
return templates.TemplateResponse(request, "admin_issues_planning.html", context)
|
||||
return respond(request, "admin_issues_planning.html", context, model=AdminIssuesPlanningOut)
|
||||
|
||||
@@ -62,6 +62,7 @@ from devplacepy.schemas.profile import (
|
||||
TelegramPairOut,
|
||||
)
|
||||
from devplacepy.schemas.issues import (
|
||||
AdminIssuesPlanningOut,
|
||||
IssueAttachmentsOut,
|
||||
IssueCommentOut,
|
||||
IssueDetailOut,
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from devplacepy.schemas.base import _Out
|
||||
|
||||
|
||||
@@ -186,6 +188,7 @@ class GameFarmOut(_Out):
|
||||
class GameStateOut(_Out):
|
||||
ok: bool = True
|
||||
farm: GameFarmOut
|
||||
game_error: Optional[str] = None
|
||||
|
||||
|
||||
class GameFarmViewOut(_Out):
|
||||
@@ -193,6 +196,7 @@ class GameFarmViewOut(_Out):
|
||||
page_title: str = ""
|
||||
meta_description: str = ""
|
||||
stole_coins: int = 0
|
||||
game_error: Optional[str] = None
|
||||
|
||||
|
||||
class GameLeaderboardEntryOut(_Out):
|
||||
|
||||
@@ -67,3 +67,16 @@ class IssuesOut(_Out):
|
||||
state: str = "open"
|
||||
configured: bool = True
|
||||
error_message: Optional[str] = None
|
||||
viewer_is_admin: bool = False
|
||||
|
||||
|
||||
class AdminPlanningTicketOut(_Out):
|
||||
number: int = 0
|
||||
title: str = ""
|
||||
labels: list[str] = []
|
||||
|
||||
|
||||
class AdminIssuesPlanningOut(_Out):
|
||||
configured: bool = True
|
||||
tickets: list[AdminPlanningTicketOut] = []
|
||||
tickets_error: bool = False
|
||||
|
||||
@@ -204,6 +204,10 @@ class IsslopReportOut(_Out):
|
||||
detected_builder: Optional[str] = None
|
||||
dom_slop_score: Optional[float] = None
|
||||
error: Optional[str] = None
|
||||
report_url: Optional[str] = None
|
||||
badge_url: Optional[str] = None
|
||||
events_url: Optional[str] = None
|
||||
topic: Optional[str] = None
|
||||
content_hash: Optional[str] = None
|
||||
markdown: str = ""
|
||||
generator_model: str = ""
|
||||
@@ -226,3 +230,7 @@ class IsslopSourceOut(_Out):
|
||||
source: str = ""
|
||||
truncated: bool = False
|
||||
signals: list = []
|
||||
source_lines: list = []
|
||||
marked_lines: dict = {}
|
||||
focus_line: int = 0
|
||||
report_url: Optional[str] = None
|
||||
|
||||
@@ -184,6 +184,8 @@ class QuizAttemptOut(_Out):
|
||||
class QuizAttemptPageOut(_Out):
|
||||
quiz: QuizOut = QuizOut()
|
||||
attempt: QuizAttemptOut = QuizAttemptOut()
|
||||
answer_max_chars: int = 0
|
||||
quiz_error: Optional[str] = None
|
||||
|
||||
|
||||
class QuizResultOut(_Out):
|
||||
@@ -225,6 +227,7 @@ class QuizBuilderOut(_Out):
|
||||
questions: list[QuizQuestionOut] = []
|
||||
kinds: list[Any] = []
|
||||
validation_errors: list[str] = []
|
||||
quiz_error: Optional[str] = None
|
||||
|
||||
|
||||
class QuizFormPageOut(_Out):
|
||||
|
||||
@@ -39,7 +39,7 @@ class StatisticsHighlightOut(BaseModel):
|
||||
value: Any
|
||||
|
||||
|
||||
class StatisticsOut(BaseModel):
|
||||
class StatisticsPayloadOut(BaseModel):
|
||||
tab: str
|
||||
window_hours: int
|
||||
granularity: str
|
||||
@@ -49,4 +49,18 @@ class StatisticsOut(BaseModel):
|
||||
series: list[StatisticsSeriesOut] = []
|
||||
tables: list[StatisticsTableOut] = []
|
||||
highlights: list[StatisticsHighlightOut] = []
|
||||
notes: dict[str, Any] = {}
|
||||
notes: dict[str, Any] = {}
|
||||
|
||||
|
||||
class StatisticsTabOut(BaseModel):
|
||||
key: str
|
||||
label: str
|
||||
icon: str
|
||||
active: bool
|
||||
|
||||
|
||||
class StatisticsOut(BaseModel):
|
||||
active_tab: str
|
||||
window_hours: int
|
||||
tabs: list[StatisticsTabOut] = []
|
||||
initial: StatisticsPayloadOut
|
||||
@@ -43,7 +43,7 @@ The dispatcher's **authorization guard** (before `_run`) mirrors this with `_aud
|
||||
|
||||
### Devii access (read-only)
|
||||
|
||||
Admins query the same two routes conversationally via the admin-only Devii tools `audit_log` (GET `/admin/audit-log`) and `audit_event` (GET `/admin/audit-log/{uid}`) (`services/devii/actions/catalog.py`, `handler="http"`, `requires_admin=True`) - no new endpoint, since the `respond(...)` routes already serve JSON to Devii's `Accept: application/json` client (like `admin_list_users`). `audit_log` forwards every filter as a query param (`page`, `event_key`, `category`, `actor_role`, `actor_uid`, `origin`, `result`, `q`, `date_from`, `date_to`) and returns `AuditLogOut`, whose `options` object lists the valid values for each filter so the agent can discover them in one call. `audit_event` returns `AuditEventOut` (the row + related links). A system-prompt steer in `agent.py` (the AGGREGATES block) routes audit/history/"who did X" questions to these tools.
|
||||
Admins query the same two routes conversationally via the admin-only Devii tools `audit_log` (GET `/admin/audit-log`) and `audit_event` (GET `/admin/audit-log/{uid}`) (`services/devii/actions/catalog/admin.py`, `handler="http"`, `requires_admin=True`) - no new endpoint, since the `respond(...)` routes already serve JSON to Devii's `Accept: application/json` client (like `admin_list_users`). `audit_log` forwards every filter as a query param (`page`, `event_key`, `category`, `actor_role`, `actor_uid`, `origin`, `result`, `q`, `date_from`, `date_to`) and returns `AuditLogOut`, whose `options` object lists the valid values for each filter so the agent can discover them in one call. `audit_event` returns `AuditEventOut` (the row + related links). A system-prompt steer in `agent.py` (the AGGREGATES block) routes audit/history/"who did X" questions to these tools.
|
||||
|
||||
### Deferred persistence
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ It exposes per-table reads, a validated raw `query()`, a natural-language-to-SQL
|
||||
|
||||
## Devii (primary-admin gated)
|
||||
|
||||
**Read-only** tools `db_list_tables`, `db_table_schema`, `db_list_rows`, `db_get_row`, `db_query` (SELECT-only; surface `suspicious`), and `db_design_query` (NL->SQL), all flagged `requires_primary_admin=True` (alongside `requires_admin=True`) in `actions/catalog.py`. The `Action.requires_primary_admin` flag is filtered by `Catalog.tool_schemas_for(authenticated, is_admin, is_primary_admin)` and enforced again in `Dispatcher.dispatch`, so these tools are **added to the LLM tool list only for the primary administrator** - every other administrator's (and member's/guest's) Devii never receives the schemas and is unaware the database API exists. `is_primary_admin` is threaded WS/Telegram/CLI -> `hub.get_or_create(is_primary_admin=)` -> `DeviiSession` -> `Dispatcher`, mirroring how `is_admin` flows (`routers/devii.py` `_resolve_ws_owner`, `services/telegram/bridge.py`, `services/devii/cli.py` which runs both flags `True` as the trusted local operator, and the headless scheduler in `service.py`). There are **no write tools** - the former `db_insert_row`/`db_update_row`/`db_delete_row` were removed along with the HTTP write routes, so Devii cannot change data through the database API.
|
||||
**Read-only** tools `db_list_tables`, `db_table_schema`, `db_list_rows`, `db_get_row`, `db_query` (SELECT-only; surface `suspicious`), and `db_design_query` (NL->SQL), all flagged `requires_primary_admin=True` (alongside `requires_admin=True`) in `actions/catalog/dbapi.py`. The `Action.requires_primary_admin` flag is filtered by `Catalog.tool_schemas_for(authenticated, is_admin, is_primary_admin)` and enforced again in `Dispatcher.dispatch`, so these tools are **added to the LLM tool list only for the primary administrator** - every other administrator's (and member's/guest's) Devii never receives the schemas and is unaware the database API exists. `is_primary_admin` is threaded WS/Telegram/CLI -> `hub.get_or_create(is_primary_admin=)` -> `DeviiSession` -> `Dispatcher`, mirroring how `is_admin` flows (`routers/devii.py` `_resolve_ws_owner`, `services/telegram/bridge.py`, `services/devii/cli.py` which runs both flags `True` as the trusted local operator, and the headless scheduler in `service.py`). There are **no write tools** - the former `db_insert_row`/`db_update_row`/`db_delete_row` were removed along with the HTTP write routes, so Devii cannot change data through the database API.
|
||||
|
||||
## nginx
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ After any mutation the handler `await`s `_shared.notify_farm(username)` which pu
|
||||
|
||||
## Fan-out
|
||||
|
||||
Devii plays via the `game_*` `http` actions in `actions/catalog.py` (state/leaderboard/view public, the rest `requires_auth`); the API reference has a **Code Farm** group in `docs_api.py`; pages are `noindex,follow` (interactive, user-specific) so they are intentionally not in the sitemap; badges live in `utils.BADGE_CATALOG` under the **Code Farm** group with `harvest`/`water`/`harvest_stolen`/`got_stolen_from` `ACHIEVEMENTS` (the steal pair awards **Cat Burglar** to the thief and **Robbed** to the victim, both threshold 1).
|
||||
Devii plays via the `game_*` `http` actions in `actions/catalog/game.py` (state/leaderboard/view public, the rest `requires_auth`); the API reference has a **Code Farm** group in `docs_api.py`; pages are `noindex,follow` (interactive, user-specific) so they are intentionally not in the sitemap; badges live in `utils.BADGE_CATALOG` under the **Code Farm** group with `harvest`/`water`/`harvest_stolen`/`got_stolen_from` `ACHIEVEMENTS` (the steal pair awards **Cat Burglar** to the thief and **Robbed** to the victim, both threshold 1).
|
||||
|
||||
## Stealing (competitive loop, backwards compatible)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user