2026-06-12 01:58:46 +02:00
|
|
|
# retoor <retoor@molodetz.nl>
|
|
|
|
|
|
2026-06-16 05:32:19 +02:00
|
|
|
from datetime import datetime, timezone
|
2026-05-10 09:08:12 +02:00
|
|
|
from fastapi.templating import Jinja2Templates
|
2026-06-16 05:32:19 +02:00
|
|
|
from markupsafe import Markup, escape
|
2026-05-23 06:20:27 +02:00
|
|
|
from devplacepy.cache import TTLCache
|
2026-06-14 16:46:36 +02:00
|
|
|
from devplacepy.config import STATIC_VERSION, TEMPLATES_DIR, TEMPLATE_AUTO_RELOAD
|
2026-06-06 16:31:42 +02:00
|
|
|
from devplacepy.constants import TOPICS, REACTION_EMOJI
|
2026-06-09 20:02:50 +02:00
|
|
|
from devplacepy.database import get_int_setting, get_setting, get_table
|
2026-05-11 03:14:43 +02:00
|
|
|
from devplacepy.avatar import avatar_url
|
2026-05-12 12:45:52 +02:00
|
|
|
from devplacepy.utils import format_date as _format_date
|
2026-06-16 05:32:19 +02:00
|
|
|
from devplacepy.utils import time_ago as _time_ago
|
2026-06-12 08:30:17 +02:00
|
|
|
from devplacepy.utils import get_badge, is_admin, pretty_json
|
2026-06-09 20:02:50 +02:00
|
|
|
from devplacepy.attachments import format_file_size, file_icon_emoji
|
feat: add api key auth, devii agent, openai gateway, and admin service management
This commit introduces a comprehensive set of new features including API key authentication with CLI management commands (get, reset, backfill), a Devii agentic assistant with WebSocket terminal and session bootstrap, an OpenAI-compatible LLM gateway service, and an admin service management panel. It also adds Playwright browser automation for bot support, configures internal gateway URLs, refactors content editing/deletion to support JSON API responses, and updates documentation across AGENTS.md, README.md, and the developer docs site.
2026-06-08 17:38:33 +02:00
|
|
|
from devplacepy.content import is_owner as _owns
|
2026-06-09 20:02:50 +02:00
|
|
|
from devplacepy.customization import custom_css_tag, custom_js_tag, page_type_for
|
2026-06-09 18:48:08 +02:00
|
|
|
|
feat: add admin/internal database API with CRUD, read-only query, and natural-language SQL endpoints
Add a new `/dbapi` router package providing a generic database API over `dataset`, restricted to admin sessions, admin API keys, and the internal gateway key. Includes:
- `tables.py`: list all tables and inspect table schemas
- `crud.py`: full CRUD operations (GET, POST, PATCH, DELETE) with soft-delete awareness, born-live inserts, `?include_deleted`, `.../restore`, and `?hard=true` purge
- `query.py`: validated read-only SELECT execution via sqlglot parsing, classification, and EXPLAIN dry-run; async query jobs with WebSocket streaming via `DbApiJobService`
- `nl.py`: natural-language-to-SQL conversion using the platform AI gateway with re-prompting until validation passes
Also register `DbApiJobService` and `PubSubService` in the service manager, add `DBAPI_DIR` to config data paths, and force cleartext `http://` connections to HTTP/1.1 in `curl_transport` to fix large request failures against uvicorn's HTTP/1.1-only internal gateway.
2026-06-15 01:00:30 +02:00
|
|
|
templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
|
|
|
|
|
templates.env.auto_reload = TEMPLATE_AUTO_RELOAD
|
2026-05-10 09:08:12 +02:00
|
|
|
|
feat: add api key auth, devii agent, openai gateway, and admin service management
This commit introduces a comprehensive set of new features including API key authentication with CLI management commands (get, reset, backfill), a Devii agentic assistant with WebSocket terminal and session bootstrap, an OpenAI-compatible LLM gateway service, and an admin service management panel. It also adds Playwright browser automation for bot support, configures internal gateway URLs, refactors content editing/deletion to support JSON API responses, and updates documentation across AGENTS.md, README.md, and the developer docs site.
2026-06-08 17:38:33 +02:00
|
|
|
|
|
|
|
|
def is_self(user, uid) -> bool:
|
|
|
|
|
return bool(user) and user["uid"] == uid
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def guest_disabled(user):
|
|
|
|
|
if user:
|
|
|
|
|
return Markup("")
|
|
|
|
|
return Markup(' disabled aria-disabled="true" title="Log in to participate"')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def login_hint(user):
|
|
|
|
|
if user:
|
|
|
|
|
return Markup("")
|
|
|
|
|
return Markup('<a href="/auth/login" class="login-hint">Log in to participate</a>')
|
|
|
|
|
|
|
|
|
|
|
2026-06-14 02:16:22 +02:00
|
|
|
def static_url(path):
|
|
|
|
|
if not isinstance(path, str):
|
|
|
|
|
return path
|
|
|
|
|
if not path.startswith("/static/") or path.startswith("/static/uploads/"):
|
|
|
|
|
return path
|
|
|
|
|
return f"/static/v{STATIC_VERSION}{path[len('/static'):]}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
templates.env.globals["static_url"] = static_url
|
|
|
|
|
templates.env.globals["static_version"] = STATIC_VERSION
|
feat: add api key auth, devii agent, openai gateway, and admin service management
This commit introduces a comprehensive set of new features including API key authentication with CLI management commands (get, reset, backfill), a Devii agentic assistant with WebSocket terminal and session bootstrap, an OpenAI-compatible LLM gateway service, and an admin service management panel. It also adds Playwright browser automation for bot support, configures internal gateway URLs, refactors content editing/deletion to support JSON API responses, and updates documentation across AGENTS.md, README.md, and the developer docs site.
2026-06-08 17:38:33 +02:00
|
|
|
templates.env.globals["is_admin"] = is_admin
|
|
|
|
|
templates.env.globals["owns"] = _owns
|
|
|
|
|
templates.env.globals["is_self"] = is_self
|
|
|
|
|
templates.env.globals["guest_disabled"] = guest_disabled
|
|
|
|
|
templates.env.globals["login_hint"] = login_hint
|
|
|
|
|
|
2026-06-14 09:48:10 +02:00
|
|
|
from devplacepy.docs_devrant import devrant_endpoints
|
|
|
|
|
|
|
|
|
|
templates.env.globals["devrant_endpoints"] = devrant_endpoints
|
|
|
|
|
|
2026-06-09 18:48:08 +02:00
|
|
|
_unread_cache = TTLCache(ttl=10, max_size=1000)
|
|
|
|
|
_messages_cache = TTLCache(ttl=10, max_size=1000)
|
|
|
|
|
|
2026-05-11 03:14:43 +02:00
|
|
|
|
2026-05-16 02:49:53 +02:00
|
|
|
def clear_unread_cache(user_uid: str) -> None:
|
2026-05-23 06:20:27 +02:00
|
|
|
_unread_cache.pop(user_uid)
|
2026-05-16 02:49:53 +02:00
|
|
|
|
|
|
|
|
|
2026-06-06 16:31:42 +02:00
|
|
|
def clear_messages_cache(user_uid: str) -> None:
|
|
|
|
|
_messages_cache.pop(user_uid)
|
|
|
|
|
|
|
|
|
|
|
2026-06-09 20:02:50 +02:00
|
|
|
def _cached_count(cache: TTLCache, table_name: str, cache_key: str, **filters) -> int:
|
|
|
|
|
cached = cache.get(cache_key)
|
2026-05-23 06:20:27 +02:00
|
|
|
if cached is not None:
|
|
|
|
|
return cached
|
2026-06-09 20:02:50 +02:00
|
|
|
table = get_table(table_name)
|
|
|
|
|
count = table.count(**filters)
|
|
|
|
|
cache.set(cache_key, count)
|
2026-05-11 03:14:43 +02:00
|
|
|
return count
|
2026-05-10 09:08:12 +02:00
|
|
|
|
2026-06-06 16:31:42 +02:00
|
|
|
|
2026-06-09 20:02:50 +02:00
|
|
|
def jinja_unread_count(user_uid: str) -> int:
|
|
|
|
|
return _cached_count(_unread_cache, "notifications", user_uid, user_uid=user_uid, read=False)
|
|
|
|
|
|
|
|
|
|
|
2026-06-06 16:31:42 +02:00
|
|
|
def jinja_unread_messages(user_uid: str) -> int:
|
2026-06-09 20:02:50 +02:00
|
|
|
return _cached_count(_messages_cache, "messages", user_uid, receiver_uid=user_uid, read=False)
|
2026-06-06 16:31:42 +02:00
|
|
|
|
2026-06-09 18:48:08 +02:00
|
|
|
|
2026-05-10 09:08:12 +02:00
|
|
|
def jinja_user_projects(user_uid: str) -> list:
|
|
|
|
|
projects = get_table("projects")
|
|
|
|
|
return list(projects.find(user_uid=user_uid))
|
|
|
|
|
|
2026-06-09 18:48:08 +02:00
|
|
|
|
2026-05-10 09:08:12 +02:00
|
|
|
templates.env.globals["get_unread_count"] = jinja_unread_count
|
2026-06-06 16:31:42 +02:00
|
|
|
templates.env.globals["get_unread_messages"] = jinja_unread_messages
|
2026-05-10 09:08:12 +02:00
|
|
|
templates.env.globals["get_user_projects"] = jinja_user_projects
|
2026-05-10 21:33:53 +02:00
|
|
|
templates.env.globals["avatar_url"] = avatar_url
|
2026-05-12 12:45:52 +02:00
|
|
|
templates.env.globals["format_date"] = _format_date
|
2026-06-16 05:32:19 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def _normalize_iso(dt_str: str) -> str | None:
|
|
|
|
|
try:
|
|
|
|
|
parsed = datetime.fromisoformat(dt_str)
|
|
|
|
|
except (ValueError, TypeError):
|
|
|
|
|
return None
|
|
|
|
|
if parsed.tzinfo is None:
|
|
|
|
|
parsed = parsed.replace(tzinfo=timezone.utc)
|
|
|
|
|
return parsed.isoformat()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def local_dt(dt_str: str, mode: str = "datetime") -> Markup:
|
|
|
|
|
if not dt_str:
|
|
|
|
|
return Markup("")
|
|
|
|
|
iso = _normalize_iso(dt_str)
|
|
|
|
|
if iso is None:
|
|
|
|
|
return Markup(escape(dt_str))
|
|
|
|
|
if mode == "ago":
|
|
|
|
|
fallback = _time_ago(dt_str)
|
|
|
|
|
elif mode == "date":
|
|
|
|
|
fallback = _format_date(dt_str)
|
|
|
|
|
else:
|
|
|
|
|
fallback = _format_date(dt_str, include_time=True)
|
|
|
|
|
return Markup(
|
|
|
|
|
f'<time datetime="{escape(iso)}" data-dt data-dt-mode="{escape(mode)}">'
|
|
|
|
|
f"{escape(fallback)}</time>"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
templates.env.globals["local_dt"] = local_dt
|
|
|
|
|
templates.env.globals["dt_ago"] = lambda dt_str: local_dt(dt_str, "ago")
|
2026-06-12 05:37:12 +02:00
|
|
|
templates.env.globals["badge_info"] = get_badge
|
2026-05-13 21:17:57 +02:00
|
|
|
templates.env.globals["TOPICS"] = TOPICS
|
2026-06-06 16:31:42 +02:00
|
|
|
templates.env.globals["REACTION_EMOJI"] = REACTION_EMOJI
|
2026-06-09 18:48:08 +02:00
|
|
|
|
|
|
|
|
|
2026-05-12 15:07:34 +02:00
|
|
|
def jinja_max_upload_size_mb():
|
2026-05-23 06:55:11 +02:00
|
|
|
return get_int_setting("max_upload_size_mb", 10)
|
2026-05-12 15:07:34 +02:00
|
|
|
|
2026-06-09 18:48:08 +02:00
|
|
|
|
2026-05-12 15:07:34 +02:00
|
|
|
def jinja_max_attachments():
|
2026-05-23 06:55:11 +02:00
|
|
|
return get_int_setting("max_attachments_per_resource", 10)
|
2026-05-12 15:07:34 +02:00
|
|
|
|
2026-06-09 18:48:08 +02:00
|
|
|
|
2026-05-12 15:07:34 +02:00
|
|
|
def jinja_allowed_file_types():
|
|
|
|
|
return get_setting("allowed_file_types", "")
|
|
|
|
|
|
2026-06-09 18:48:08 +02:00
|
|
|
|
2026-05-12 15:07:34 +02:00
|
|
|
templates.env.globals["max_upload_size_mb"] = jinja_max_upload_size_mb
|
|
|
|
|
templates.env.globals["max_attachments_per_resource"] = jinja_max_attachments
|
|
|
|
|
templates.env.globals["allowed_file_types"] = jinja_allowed_file_types
|
2026-05-13 21:17:57 +02:00
|
|
|
|
|
|
|
|
_LANGUAGE_NAMES = {
|
2026-06-09 18:48:08 +02:00
|
|
|
"python": "Python",
|
|
|
|
|
"javascript": "JavaScript",
|
|
|
|
|
"typescript": "TypeScript",
|
|
|
|
|
"html": "HTML",
|
|
|
|
|
"css": "CSS",
|
|
|
|
|
"c": "C",
|
|
|
|
|
"cpp": "C++",
|
|
|
|
|
"java": "Java",
|
|
|
|
|
"go": "Go",
|
|
|
|
|
"rust": "Rust",
|
|
|
|
|
"sql": "SQL",
|
|
|
|
|
"bash": "Bash",
|
|
|
|
|
"yaml": "YAML",
|
|
|
|
|
"json": "JSON",
|
|
|
|
|
"markdown": "Markdown",
|
|
|
|
|
"swift": "Swift",
|
|
|
|
|
"php": "PHP",
|
|
|
|
|
"ruby": "Ruby",
|
|
|
|
|
"kotlin": "Kotlin",
|
|
|
|
|
"lua": "Lua",
|
|
|
|
|
"perl": "Perl",
|
|
|
|
|
"haskell": "Haskell",
|
|
|
|
|
"elixir": "Elixir",
|
|
|
|
|
"r": "R",
|
|
|
|
|
"dart": "Dart",
|
|
|
|
|
"scala": "Scala",
|
|
|
|
|
"plaintext": "Plain Text",
|
2026-05-13 21:17:57 +02:00
|
|
|
}
|
|
|
|
|
|
2026-06-09 18:48:08 +02:00
|
|
|
|
2026-05-13 21:17:57 +02:00
|
|
|
def jinja_language_name(code: str) -> str:
|
|
|
|
|
return _LANGUAGE_NAMES.get(code, code)
|
|
|
|
|
|
2026-06-09 18:48:08 +02:00
|
|
|
|
2026-05-13 21:17:57 +02:00
|
|
|
templates.env.globals["language_name"] = jinja_language_name
|
2026-06-12 08:30:17 +02:00
|
|
|
templates.env.globals["pretty_json"] = pretty_json
|
2026-05-13 21:17:57 +02:00
|
|
|
|
|
|
|
|
templates.env.globals["format_file_size"] = format_file_size
|
|
|
|
|
templates.env.globals["file_icon_emoji"] = file_icon_emoji
|
2026-06-09 16:06:02 +02:00
|
|
|
|
|
|
|
|
templates.env.globals["custom_css_tag"] = custom_css_tag
|
|
|
|
|
templates.env.globals["custom_js_tag"] = custom_js_tag
|
|
|
|
|
templates.env.globals["page_type_for"] = page_type_for
|