docs: document server-side rendering pipeline, response timing middleware, and Telegram pairing API

- Add comprehensive documentation for backend content rendering in AGENTS.md, detailing the new `render_content` and `render_title` Jinja globals built on mistune with media processing, emoji shortcodes, and XSS protection
- Document the `X-Response-Time` header and bottom-left render time indicator in README.md
- Update bot token pricing documentation to clarify fallback vs gateway cost headers
- Add `email_accounts` to soft-delete tables and `idx_users_role` composite index in database schema
- Implement `telegram_pairings` and `telegram_links` table creation with column migration and indexes
- Add `/profile/{username}/telegram` endpoint to docs API with request/unpair actions
- Register `TelegramService` in main.py lifespan and add `response_timing` middleware emitting `X-Response-Time` header
- Introduce `TelegramPairForm` model and `guard_public_host_sync` synchronous host validation function
This commit is contained in:
2026-06-18 22:09:34 +00:00
parent 95dca73291
commit 6ceca3d0d4
146 changed files with 6079 additions and 392 deletions
@@ -77,6 +77,13 @@ TASK_ACTIONS: tuple[Action, ...] = (
required=True,
),
field("label", "Optional short human-readable label for the task."),
field(
"notify",
"Set true for a REMINDER: when the task fires, the user is sent an in-app "
"notification and live toast carrying the result, so it reaches them even when "
"the Devii terminal is closed. Leave false for silent background automation.",
kind="boolean",
),
*SCHEDULE_FIELDS,
),
),
@@ -116,6 +123,11 @@ TASK_ACTIONS: tuple[Action, ...] = (
field("prompt", "New prompt."),
field("label", "New label."),
field("enabled", "Enable or disable the task.", kind="boolean"),
field(
"notify",
"Enable or disable the in-app reminder notification when this task fires.",
kind="boolean",
),
field("kind", "New schedule type when rescheduling."),
field("run_at", "New absolute run time for kind=once."),
field("delay_seconds", "New relative delay for kind=once.", kind="integer"),
@@ -51,6 +51,8 @@ def _serialize(row: dict[str, Any], preview: bool) -> dict[str, Any]:
"every_seconds": row.get("every_seconds"),
"cron": row.get("cron"),
"run_at": row.get("run_at"),
"notify": bool(row.get("notify")),
"tz": row.get("tz") or None,
}
last_error = row.get("last_error")
if last_error:
@@ -98,6 +100,8 @@ class TaskController:
"run_count": 0,
"last_result": None,
"last_error": None,
"notify": 1 if _as_bool(arguments.get("notify"), default=False) else 0,
"tz": (arguments.get("tz") or "").strip() or None,
**schedule.columns(),
}
self._store.create(record)
@@ -137,6 +141,10 @@ class TaskController:
changes["label"] = (arguments.get("label") or "").strip() or None
if "enabled" in arguments:
changes["enabled"] = _as_bool(arguments.get("enabled"))
if "notify" in arguments and arguments["notify"] is not None:
changes["notify"] = 1 if _as_bool(arguments.get("notify")) else 0
if "tz" in arguments and arguments["tz"] is not None:
changes["tz"] = (str(arguments.get("tz")) or "").strip() or None
if any(
key in arguments and arguments[key] is not None for key in SCHEDULE_KEYS
+26
View File
@@ -24,6 +24,24 @@ def memory_db() -> Any:
return dataset.connect("sqlite:///:memory:")
ACTIVE_STATUSES = ("pending", "running")
def pending_owner_ids(db: Any) -> list[str]:
if TABLE not in db.tables:
return []
table = db[TABLE]
if "owner_id" not in table.columns:
return []
rows = table.find(owner_kind="user", enabled=True, deleted_at=None)
owners = {
row["owner_id"]
for row in rows
if row.get("owner_id") and row.get("status") in ACTIVE_STATUSES
}
return sorted(owners)
class TaskStore:
def __init__(self, db: Any, owner_kind: str, owner_id: str) -> None:
self._db = db
@@ -39,6 +57,10 @@ class TaskStore:
table.create_column_by_example("deleted_at", "")
if not table.has_column("deleted_by"):
table.create_column_by_example("deleted_by", "")
if not table.has_column("notify"):
table.create_column_by_example("notify", 0)
if not table.has_column("tz"):
table.create_column_by_example("tz", "")
for columns in INDEXED_COLUMNS:
table.create_index(columns)
@@ -109,6 +131,10 @@ class TaskStore:
logger.info("Recovered %d task(s) stuck in running", len(stuck))
return len(stuck)
def has_pending(self) -> bool:
rows = self._table.find(enabled=True, deleted_at=None, **self._scope)
return any(row.get("status") in ACTIVE_STATUSES for row in rows)
def due(self, now_iso: str) -> list[dict[str, Any]]:
rows = self._table.find(
enabled=True, status="pending", deleted_at=None, **self._scope