sparc_phase: A
project: pediatric-news
created: 2026-05-11
author: claude-vps
status: draft
spec_link: ./01-specification.md
pseudocode_link: ./02-pseudocode.md
Architecture — Pediatric News (Telegram + Threads + Infographic)
Стек
| Компонент |
Технологія |
Чому |
| Runtime |
Python 3.11 |
Уже на VPS, поточний bot.py — Python |
| RSS-fetch |
feedparser |
Stateless, працює на 5 feed'ах |
| HTTP |
requests |
Поточний код, без async-міграції на старті |
| AI-prompt |
gemini CLI (subprocess) |
Google AI Ultra ключ, no extra deps |
| Image render |
Pillow (PIL) + DejaVuSans |
Детермінований, без AI-typo ризиків |
| Telegram API |
requests POST sendPhoto / sendMessage |
Поточна реалізація |
| Threads API |
requests POST Graph API v1.0 |
Сертифікований endpoint Meta |
| State |
seen_ids.json (atomic write) + JSONL log |
Спрощено, без БД |
| Scheduling |
cron 0 5,11,17 * * * |
Існує, не міняємо |
| Secrets |
/srv/passepartout/ |
Стандарт правило #4 |
| Logs |
/var/log/pediatric-news.log + .jsonl |
journalctl + structured metrics |
Файлова структура
/srv/projects/pediatric-news/
├── bot.py # main entry, cron target (рефактор поточного)
├── pipeline/
│ ├── __init__.py
│ ├── rss.py # fetch_rss(hours)
│ ├── gemini_select.py # select_and_format → SelectedPost
│ ├── infographic.py # build_spec + render_png (PIL)
│ ├── threads_publisher.py # build_thread + publish_chain
│ ├── telegram_publisher.py # sendPhoto/sendMessage + fallback
│ └── state.py # load_seen / save_seen / log_metric
├── prompts/
│ ├── telegram_post.md # поточний промпт (з bot.py)
│ ├── infographic_spec.md # з infographic-builder skill
│ └── threads_thread.md # з threads-news skill
├── tests/
│ ├── test_rss.py
│ ├── test_infographic.py
│ └── test_threads_publisher.py
├── seen_ids.json # state, поточний
└── AGENTS.md # оновити з Node.js → Python factsheet
Компоненти і відповідальності
- bot.py — orchestrator. cron-entry. Викликає pipeline стадії, fan-out, persist. Без бізнес-логіки.
- pipeline/rss.py — pure: feedparser → list[Article]. Обробка помилок feed-level.
- pipeline/gemini_select.py — subprocess до
gemini, sanitize_post (HEADER_EMOJI fix), retry-on-timeout.
- pipeline/infographic.py — два кроки: (1) build_spec (Gemini, prompt з skill), (2) render_png (PIL, deterministic). PNG_v2-renderer мігрує з
/tmp/infographic_render_v2.py.
- pipeline/threads_publisher.py — Gemini-build thread (3 пости) + 2-stage Graph API publish, sleep 30-60 сек, reply_to chain.
- pipeline/telegram_publisher.py — sendPhoto з caption (1024-char trim) або sendMessage fallback; HTML parse_mode + Markdown fallback.
- pipeline/state.py — atomic seen_ids.json, log_metric у JSONL.
Ключові інтерфейси
# pipeline/rss.py
def fetch_rss(max_age_hours: int = 90) -> list[Article]: ...
# pipeline/gemini_select.py
def select_and_format(articles: list[Article]) -> SelectedPost | None: ...
# pipeline/infographic.py
def build_spec(post: SelectedPost) -> InfographicSpec: ...
def render_png(spec: InfographicSpec, out_path: Path) -> Path: ...
# pipeline/threads_publisher.py
def build_thread(post: SelectedPost) -> list[ThreadPost]: ... # 3 elements
def publish_thread(thread: list[ThreadPost]) -> int: # returns depth published
# pipeline/telegram_publisher.py
def publish(html: str, png: Path | None) -> bool: ...
Потоки даних (Data flow)
[cron tick]
│
▼
[bot.main()] ──── load_seen() ──── seen_ids.json
│
▼
[fetch_rss] ──── 5 RSS feeds ────► list[Article]
│
▼
[gemini_select] ──── Gemini API ──► SelectedPost (HTML + machine fields)
│
▼ FAN-OUT
├─► [build_spec] → [render_png] → /tmp/png ──┐
│ ▼
├─► [telegram_publish (HTML + PNG)] ──► Telegram channel
│
└─► [build_thread] → [publish_thread] ──► Threads (root + 2 reply)
│
▼
[save_seen] + [log_metric → JSONL]
Безпека
- Authentication: Telegram bot token + Threads long-lived user-token, обидва з passepartout
- Authorization: bot — admin каналу
@smart_pediatric_news (chat_id -1003892663092)
- Secrets:
/srv/passepartout/telegram/pediatric-news-bot.token, /srv/passepartout/meta/threads/pediatric-news-token.txt, /srv/passepartout/google/gemini.key
- Логи: НЕ логуємо повний токен, лише prefix; логуємо request_id, status, latency
- Compliance: джерело-link у кожному пості; PNG не містить brand-claims
Деплой і інфраструктура
- systemd-unit: НЕ daemon, cron-driven (
/etc/cron.d/pediatric-news)
- Cron:
0 5,11,17 * * * cd /srv/projects/pediatric-news && python3 bot.py >> /var/log/pediatric-news.log 2>&1
- Logs:
/var/log/pediatric-news.log (текстові) + /var/log/pediatric-news.jsonl (metrics)
- Healthcheck: окремий
check.py (TODO Phase 2) — перевіряє Telegram + Threads token, останній post-час, exit 0/1
- Rollback:
git checkout на попередній commit /srv/projects/pediatric-news/, cron підхопить наступний tick
Спостережуваність
- Метрики (JSONL у
/var/log/pediatric-news.jsonl):
- per-tick: timestamp, article_id, tg_ok (bool), threads_depth (0..3), png_ok, gemini_ms, total_ms
- Daily summary (cron 23:00 UTC) → Telegram DM Сергію: success-rate, threads depth-avg, fail-reasons
- Алерти: token-expiry за 7 днів (Threads); Gemini >2 fail у рядок (Telegram alert)
Ризики
- Gemini timeout 60s — retry з зменшеним batch; досвід: ~10% fail-rate зараз
- Threads rate-limit — 200 публікацій/24h на user-token; ми робимо 3 публікації × 1 на день = з запасом
- Threads token expiry (60 днів long-lived) — алерт за 7 днів + manual refresh
- PIL шрифт missing — DejaVuSans-Bold перевірений
fc-list, fallback на default font
- Caption 1024 chars limit — Gemini-промпт явно обмежує summary 800 chars
Відкриті питання
- Зберігати PNG локально (
/srv/projects/pediatric-news/img/YYYY-MM-DD/) чи /tmp + cleanup?
- Окрема Caddy-endpoint для health-check (
/health через невеликий FastAPI sidecar) чи cron-only?
- Чи мігрувати на systemd-timer замість cron — кращий journalctl?
Sign-off
- [ ] Сергій schválив архітектуру
- [ ] Дата approval: 2026-05-11
- [ ] Перехід на наступну фазу: R (Refinement)
🤖 PM Changelog
- 2026-05-11: повне переписання з bootstrap-template на основі реальної pipeline та нового scope (Threads + infographic)