# Linkki Handoff Brief ## Purpose This directory is the extracted Linkki Telegram broadcast editor. The active product is a FastAPI web app in a Docker container. Users draft Finnish, Swedish, and English channel broadcasts, preview Telegram formatting, schedule publishing, update published text, map channel messages to linked discussion-group messages, manage pinning, and delete or unlink Telegram posts. The source is intended to be absorbed into a larger project. Read `README.md` for setup and operation; this document is implementation context for the next coding agent. ## Important entry points | Path | Role | | --- | --- | | `webapp_main.py` | Container and local web-server entry point. | | `webapp/app.py` | FastAPI routes, async scheduler, publish/update/delete orchestration. | | `webapp/storage.py` | On-disk post schema, optimistic field revisions, state transitions, title/directory renames. | | `webapp/static/app.js` | Stateful editor client. Large single IIFE; also handles editor-only sidebar navigation without a full page load. | | `webapp/static/app.css` | Existing desktop-first editor/sidebar layout. | | `webapp/templates/` | Base shell, editor, login, and instructions markup. | | `webapp/translations/catalog.json` | All visible UI strings in Finnish, Swedish, and English. | | `linkki_poster/` | Telegram API wrapper, formatting parser, directory scanning, publishing, updates, deletes, and pinning. | | `tests/` | Pytest suite. | ## Data model Runtime data is outside the source tree under `LINKKI_DATA_DIR` (`/data` in Docker): ```text config/ app_config.json users.json telegram_config.json posts/ slug/ post.json Suomi.md Svenska.md English.md images named Kuva*, Suomi*, Svenska*, English* ``` `post.json` is canonical metadata. `webapp.storage.load_metadata()` merges it with current defaults and normalizes fields. Current schema is `METADATA_SCHEMA_VERSION = 3`. Scheduled publishes retain a retry count and next-attempt timestamp; they retry after 1, 5, 15, and then 30 minutes until success. A one-shot migration previously normalized production posts, and old binding-format fallback support was deliberately removed afterward. Do not casually reintroduce old schema handling; if a new schema change is needed, use a deliberate migration. Every editable persistent field has a revision in `metadata.field_revisions`. Browser saves include a base revision; storage returns a conflict if another editor changed the field. When adding a metadata field, update all of: - `default_metadata()` - `default_field_revisions()` when the field is independently editable - `merge_metadata_with_defaults()` - the browser editable snapshot and save payload in `app.js` - storage tests The title is also the directory slug. `apply_title_change()` renames the directory after checking the target path does not exist. ## Publish state and Telegram bindings The central state values are `draft`, `scheduled`, `published`, `modified`, and `failed`. - A channel broadcast stores target selection as `telegram_bot_id` and `telegram_chat_id`. - `telegram_binding` stores channel message IDs and corresponding discussion-group message IDs. - Linked broadcasts lock target/language/image setup, but title and text remain editable. - Editing a linked text marks `dirty_text_languages`; editing pinning marks `dirty_discussion_pinning`. `perform_publish_action()` sends the required Telegram update. - A successful scheduled unpin clears its schedule and sets pin mode to `none`. Keep channel-broadcast bindings (`PublishBinding`, `LanguageBinding`, `TextSegmentBinding`) separate from any future direct-message or group-management feature. The current binding model assumes channel posts with optional discussion messages. Discussion mapping is a Telegram limitation, not merely a UI concern. The bot must receive fresh automatic-forward updates from the linked discussion group. Privacy mode/admin visibility and competing `getUpdates` consumers matter. The automatic historical repair path was removed because it could only help while relevant updates were still arriving. Manual channel/group ID mapping is the intended recovery path. `Delete from Telegram` deletes discussion messages first, retains partial bindings after failures, then deletes channel messages. Telegram may reject bot deletions after about 48 hours. Pin-service-message deletion is separate from pinning and defaults to enabled for new broadcasts. ## Frontend notes `app.js` owns a `state` object containing the current and last-saved broadcast, permissions, operation polling, sidebar data, and active tab. Re-rendering rebuilds language panels and Telegram mapping cells. Existing helpers keep textarea and preview heights synchronized. Sidebar navigation between editor pages is deliberately partial: 1. Sidebar announcement clicks are intercepted. 2. The client fetches `GET /api/announcements/{directory}`. 3. It replaces editor state, re-renders, and calls `history.pushState`. 4. Browser back/forward uses `popstate` and refetches editor state. Do not turn ordinary sidebar forms (creation, reordering, sign-out, language change) into partial navigation without designing their return/state behavior. The base template still owns sidebar scroll persistence. The Telegram link tab uses an aligned table. A group link cannot exist without its channel link. The Pinning tab is disabled when the chosen channel target lacks a discussion group. Preserve those constraints in both UI and backend routes. Text formatting is conservative by design. Inline styles are parsed/re-serialized to avoid crossing tags. The parser in `linkki_poster/formatting.py` is authoritative. Text-node HTML escaping intentionally uses `quote=False`: escaping apostrophes as `'` caused Telegram heading rendering bugs. ## Permissions and security Permissions are in `webapp/auth.py` and normalized into `users.json`. New actions need both backend enforcement and corresponding browser disabled state. Never commit real Telegram tokens, user data, password hashes, or `/data` contents. ## Operations and testing The FastAPI scheduler runs in-process and checks persisted schedules every `scheduler_poll_seconds`. Its first pass posts every overdue scheduled broadcast immediately after startup, then follows the persisted retry delay. In-memory operation progress (`app.state.operations`) disappears on container restart; the persisted logs and schedules remain. Run: ```text python -m pip install -r requirements-dev.txt python -m pytest -q python -m compileall webapp linkki_poster tests node --check webapp/static/app.js ``` Use a virtual environment if the host Python is externally managed. ## Non-goals and recent decisions - The application only supports channel broadcasts today. - A proposed direct/mass-message feature was not implemented because Telegram Bot API bots cannot reliably resolve arbitrary private-user `@usernames` to chat IDs or initiate chats with them. Treat any future direct-message feature as a separate design with numeric chat IDs or a local known-contact store. - A proposed group-creation feature was also deferred; Bot API does not provide general create-group/add-arbitrary-user-by-username behavior. - The root standalone CLI scripts (`post_broadcast.py`, `main.py`, and `manage_broadcast.py`) remain as legacy/manual tools. They are not part of the container workflow.