--- paths: - "roboco/services/telegram*.py" - "roboco/api/routes/telegram.py" - "roboco/utils/telegram_initdata.py" - "panel/src/app/(tg)/**" - "panel/src/components/tg/**" --- # Telegram bridge (V1-V6) **Telegram notifications bridge V1+V2+V3 (default-off `ROBOCO_TELEGRAM_ENABLED`).** V1: best-effort, outbound-only Telegram DMs to the CEO on escalation and completion. Mirrors the `x_credentials` pattern: a singleton Fernet-encrypted `telegram_credentials` row (migration 074, bot token + chat id; the API returns `has_credentials` only) behind CEO-only `/telegram/credentials` routes and a panel credentials card. `_notify_telegram` (`roboco/services/notification_delivery.py`) fans out from `notify_ceo_of_escalation` / `notify_ceo_of_completion`, sending only the notification's subject plus an optional panel deep-link (`panel_base_url`) — never the body — via a deferred, best-effort send that never raises into the producer (`NullTelegramClient` when unconfigured or the flag is off, `LiveTelegramClient` posting to the Bot API otherwise). V2 (`ROBOCO_TELEGRAM_INBOUND_ENABLED`, sub-switch on top of V1's flag — both plus stored credentials are required, otherwise the bot only sends and never listens) makes the bridge two-way: `TelegramInboundEngine` (`roboco/services/telegram_inbound.py`) long-polls `getUpdates` from a dedicated orchestrator loop (`_telegram_poll_loop`), authorizing every update by BOTH chat id and sender id, and routes `/status` / `/queue` / `/task` commands plus `Approve`/`Reject`/`Open` inline-keyboard taps (a compact `apv|rej::` callback codec; a reject reason or a task-approve note is collected via a force_reply prompt held in a TTL'd in-memory pending-action map) through the SAME CEO-gated service calls the HTTP routes make (task/release/xpost/video/roadmap), stamping a `via=telegram` audit row on each. Escalation DMs (not completion DMs) carry the actionable keyboard when V2 is armed. All bot/bridge messages are HTML-styled (`parse_mode=HTML` with mandatory `_esc`/`_esc_attr` escaping at every dynamic interpolation and balance-aware 4096 truncation — the injection posture moved from no-parse_mode to escaping discipline), and every held-draft origination (release proposal, X post, video post, roadmap item via `propose_roadmap`) pushes a styled DM with its Approve/Reject keyboard the moment it materializes (`notify_ceo_of_queue_item`, best-effort, sharing `/queue`'s renderer). Closing the loop exposed a real hole: a stale Approve/Reject button targets its item by id regardless of current status, so `ReleaseProposalService.approve`/`.reject`, `XPostService.approve`, and `VideoPostService.approve` now all refuse an already-CANCELLED (rejected) or already-COMPLETED (published/posted) target instead of silently re-executing — a fix that also closes the identical hole via a replayed HTTP call, not just Telegram. V3 adds a Telegram **Mini App** sign-in: `POST /api/telegram/webapp-auth` (`roboco/api/routes/telegram.py`, mounted only when `telegram_miniapp_enabled` AND `cloud_auth_enabled` are both armed — `telegram_miniapp_enabled` is env-only like `cloud_auth_enabled`, deliberately off the panel feature-flags card, and fails loud at startup if armed without cloud auth on) validates Telegram's signed `initData` (`roboco/utils/telegram_initdata.py` — pure HMAC-SHA256 `WebAppData`-keyed validation, constant-time compare, a `telegram_initdata_max_age_seconds` freshness window with 60s clock-skew tolerance) against the stored bot token and the CEO's own `chat_id`, then mints the same cloud-auth session cookie `/api/auth/login` issues — turning the CEO's phone into a real panel client at the new `(tg)` route group (`/tg`: Approvals/Inbox/Board/Chat tabs, outside the normal dashboard shell; `proxy.ts`'s matcher excludes `tg(?:/|$)` so a phone session is never bounced to the password `/login` page it can't reach). Requires a public HTTPS origin (the cookie is secure-only) and BotFather's `/setmenubutton` pointed at `https:///tg`. **V4 (Mini App V4)** rebuilds the cockpit and the command tier on both sides. Panel: the `(tg)` surface opens on a "Today" brief (`GET /api/telegram/today`, CEO-gated, one DB-only round trip via `TgCockpitService` — needs-you items, held-draft counts, fleet with per-agent task titles, day-rollup spend, ship state), the Approvals tab is a native card stack over all four held-draft queues (MainButton/BackButton/haptics with visible fallbacks; X 280-counter editing, blob-fetched video player, per-AC release view; a failed queue source is surfaced, never rendered as "queue is clear"), Chat/Today ride the shared `/ws/system` socket (invalidate-on-frame, poll fallback), theme adopts the user's Telegram `themeParams` scoped to `#tg-shell`, a dev-only mock bridge + `/tg?demo=1` fixtures make the whole surface workable in a plain browser, and shared primitives (`panel/src/components/tg/ui.tsx`) carry the visual language. Bot: `BOT_COMMANDS` is the single registry driving `/help` AND a once-per-process Bot API `setMyCommands` sync; `/agents` `/usage` `/blocked` join the read tier, and `/secretary` + `/newtask` bridge the chat into the SAME in-process live runtimes the panel drives (`roboco/services/telegram_bridge.py`): a per-chat consumer task drains the `PrompterLiveRegistry` stream (sole consumer — no sync reply seam exists) and pushes one Telegram message per `turn_end`; free text routes into the live session; a `draft` event becomes a Send-to-Board/Discard keyboard whose confirm runs `PrompterService.confirm_live_draft(route="board")` and PARKS the session so board feedback streams back into the thread; `/end` reaps; the bridge sweeps its own idle TTL (the held stream arms the registry keepalive, so the registry's reap never fires), parked sessions exempt. Intake/secretary containers are process-wide singletons, so a bridged session preempts a live panel session of the same kind by construction; MegaTask batches still confirm in the panel only. **V6 (Mini App V6)** is the premium overhaul: a native-type design system on the `#tg-shell` tokens (borderless elevated cards, wallet-style tabular-numeral heroes, floating dock; Share Tech Mono demoted to the `ROBOCO_` wordmark only) with Telegram window-chrome painting riding the theme bridge (`setHeaderColor`/`setBackgroundColor`/`setBottomBarColor`); Inbox moves behind a header bell as a pushed sub-page with humanized notifications (UUIDs resolve to task names via the Board's shared task index, `tg-format.tsx`); a new Metrics tab (period-segmented spend hero + by-agent/team/model + delivery/efficiency; tapping an agent pushes a drilldown over the previously-untapped `/usage/time-series?agent_slug` plus the member scorecard); Chat is rebuilt with honest scopes — Mine rides the participant-scoped `/a2a/chat/conversations` (resolved peer, real unread counts, mark-read on open, plain CEO send) while Fleet rides the admin list (task-linked threads interject via `replyAsCeo` with a recipient chip, task-less threads are watch-only), both with markdown transcripts, live pulse flashes, and a pinned **Secretary** live chat on the same `secretary_live` SSE session runtime the panel drives; and the Board task sheet carries the CEO's own decide verbs (approve / request-changes / unblock) instead of being read-only. The `/api/dashboard/*` router is now `require_panel_token`-gated at router level (mirroring `/api/usage`), closing the unauthenticated metrics/scorecard exposure.