# src/atm/notifier — Alerting & Live Commands

## Purpose

Tot ce iese din proces către exterior: alerte Discord/Telegram + poller-ul de
comenzi live Telegram. Fan-out trimite același eveniment pe mai multe canale.

## Ownership

`discord.py`, `telegram.py`, `fanout.py` și poller-ul de comenzi
(`../commands.py` definește dataclass-ul `Command` + long-poll `getUpdates`).

## Local Contracts

- **Secretele** vin DOAR din env (`ATM_DISCORD_URL`, `ATM_TG_TOKEN`,
  `ATM_TG_CHAT`), încărcate din `.env` de `config.py`. Nu hardcoda token-uri.
- **`commands.py`** e singurul consumator de **httpx** (async long-poll).
  `TelegramNotifier` (sync) rămâne pe **requests**. Nu amesteca cele două.
- **Comenzi live:** `/ss` `/status` `/pause` `/resume` `/rebase` `/3` (interval
  min) `/stop` `/window`. **Doar Telegram** primește comenzi — Discord e webhook
  outbound, fără poller, deci **nu poate avea slash commands** (ar necesita un
  bot application + interactions endpoint, care nu există).
- **Meniul slash din clientul Telegram** (autocomplete pe `/`) vine din
  `setMyCommands`. Sursa unică e `commands.TELEGRAM_COMMANDS`; poller-ul îl
  înregistrează **automat la startup** (`TelegramPoller._register_commands`,
  best-effort — un eșec nu blochează polling-ul). `scripts/register_telegram_commands.py`
  refolosește aceeași listă pentru (re)set/list/clear manual fără să pornești app-ul.
  `TELEGRAM_COMMANDS` trebuie să oglindească handler-ul `/help` din `main.py`
  (aceleași comenzi, aceeași ordine) — modifici una, actualizezi cealaltă în același commit.

### Contracte pe comenzi (nu le slăbi fără update aici)

- **`/rebase`** — propune `baseline_phash` nou: capture → crop pe `canary.roi`
  → phash → screenshot adnotat (cerc roșu pe ROI) cu old/new hash + distance.
  `/rebase confirm` în ≤180s rescrie `baseline_phash` în TOML-ul activ (păstrează
  comentariile), mirror în `cfg` la runtime, clear `user_paused` + `drift_paused`.
  Fără confirm, nimic nu se schimbă.
- **`/ss`** — top-3 buline din `dot_roi`: cerc roșu gros pe pick-ul FSM, cercuri
  colorate subțiri pe vecini; caption cu nume/RGB/distanță/confidence +
  `config: {version}`. Culoarea cercului = `cfg.colors[name].rgb` (DRY cu paleta).
- **`/resume`** — clear ȘI user-pause ȘI drift-pause într-un shot (`/resume force`
  = alias legacy). Capture rulează **înainte** de clear (zero race cu FSM tick).
  Dacă capture pică, title conține `⚠️ captură eșuată` și resume se execută oricum.
- **Drift-pause** — un singur alert Telegram pe tranziție. Cât e pauzat,
  `/set_interval` e refuzat, caption-ul `/ss` avertizează că detecția e oprită,
  heartbeat arată `⚠️ pauzat (drift)` în loc de `activ`.
- **`/window HH:MM-HH:MM`** (sau `HH:MM HH:MM`) — fereastră de monitorizare în
  **ora locală**, **recurentă zilnic**, stocată în `LifecycleState.session_window`.
  Se combină prin **AND** cu `operating_hours` (vezi `../AGENTS.md` → scheduler):
  în afara intervalului `_should_skip` întoarce `out_of_window_hours` ⇒ pauză
  automată (alertă „Piața închisă" o dată + scheduler oprit + FSM reset).
  `/window off` (sau `clear`) șterge fereastra. Format invalid e ignorat.

## Work Guidance

- Orice mesaj nou de alertă → trece prin `fanout.py`, nu chema notifier-ele direct
  din `main.py`.
- Schimbi semantica unei comenzi → actualizează contractul de mai sus în ACELAȘI
  commit (DOX pass).

## Verification

```bash
pytest tests/test_notifier.py tests/test_commands.py -v
.\.venv\Scripts\python.exe scripts\repro_ss_resume.py   # repro /ss + /resume
```

## Child DOX Index

(none — leaf)
