Files
ajp_anton 8ec3cde4f2
Build custom container images / build (map[base_image:php:8-fpm-alpine build_args:PHP_VERSION=8 context:php8-pgsql fingerprint_command:{ apk info -v | LC_ALL=C sort; find /usr/local/lib/php/extensions /usr/local/etc/php/conf.d -type f -exec sha256sum {} + | LC_ALL=C sort; } name:ph… (push) Successful in 48s
Build custom container images / build (map[base_image:postgres:18 build_args:PG_VERSION=18 POSTGIS_VERSION=3 VCHORD_VERSION=0.5.3 context:postgres fingerprint_command:{ dpkg-query -W -f='${binary:Package}=${Version}\n' | LC_ALL=C sort; find /usr/lib/postgresql -type f -exec sha256su… (push) Successful in 55s
Build custom container images / build (map[base_image:python:3 build_args:PYTHON_VERSION=3 context:python-tools fingerprint_command:{ dpkg-query -W -f='${binary:Package}=${Version}\n' | LC_ALL=C sort; pip freeze | LC_ALL=C sort; } name:python-tools oci_labels:org.opencontainers.ima… (push) Successful in 54s
Build custom container images / build (map[base_image:python:3.12-slim build_args:PYTHON_VERSION=3.12-slim context:linkki-tiedotus fingerprint_command:{ dpkg-query -W -f='${binary:Package}=${Version}\n' | LC_ALL=C sort; pip freeze | LC_ALL=C sort; } name:linkki-tiedotus oci_labels:… (push) Successful in 39s
Add Linkki broadcast image
2026-08-19 20:51:26 +00:00

113 lines
7.3 KiB
Markdown

# 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.