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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user