- Actually load configs/config.yaml at startup (init_config was commented out) - Add timeouts and error handling to all HTTP calls; run blocking requests in threads; serialize processing with a lock; run initial check in background - URL-encode Telegram messages and HTML-escape scraped data - Fix GPV crash on empty schedule, 00:00 end time, date sorting and fallback format - Parse PO table per row; fix daily reminder check and change detection - Use per-instance timestamps in dataclasses; robust config value parsing - Matrix: use v3 PUT send endpoint; strip secrets from logged errors - Resolve templates/static/test paths relative to the project; use lifespan - Move debug GPV data to test/data_gpv.json; add CLAUDE.md Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4.4 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
What this is
FastAPI service that polls the Ivano-Frankivsk energy company (oe.if.ua) for power outages at a configured address/account and pushes notifications to Telegram and (optionally) Matrix. Two independent data sources:
- PO (planned outages) — HTML table scraped from
https://oe.if.ua/uk/shutdowns_table, parsed with lxml XPath per row (div[@class='table-row flex']). - GPV (hourly outage schedule) — JSON from
https://be-svitlo.oe.if.ua/schedule-by-searchby account number (requested withverify=False).
Messages and log text are in Ukrainian; keep that.
Commands
.venv/bin/pip install -r requirements.txt
.venv/bin/python main.py # runs uvicorn with host/port from configs/config.yaml
There is no test suite or linter configured. test/ holds fixtures only (data_po_2days_*.html, data_gpv.json). To exercise the app without contacting real sites or messengers, set application.debug: "true" in configs/config.yaml: fetches then read the fixtures, and senders print() instead of sending. Note fixtures are dated April 2026, so entries are pruned as expired unless dates are shifted.
Configuration
configs/config.yamlis loaded at import ofmain.pyviacfg.init_config(); values use!secret <key>resolved fromconfigs/secrets.yaml(gitignored, must exist for a real run). The!secrettag is registered on a private loader subclass inconfig_file.read_yaml.app_config.pyexposes settings as module-level globals (cfg.TG_TOKEN,cfg.IS_DEBUG, …) populated byparse_config(). Always access them ascfg.Xat call time —from app_config import Xwould capture the pre-config default.- Booleans in YAML may be strings (
"true") — parse withas_bool. configs/log.yamlis the uvicorn log config. All modules log through the singlecfg.LOGlogger, which also feeds the in-memoryLOG_LISTserved by/api/v1/logs(unauthenticated — never log secrets; useutils.safe_error(e)for request exceptions, since TG URLs contain the token).
Architecture
main.py— FastAPIappwith a sub-appapimounted at/api/v1(the root HTML page intemplates/info.htmllistsapiroutes).lifespancreates anAsyncIOSchedulerwith one cron job per (hour in[start_hour, stop_hour), minute inPROCESSING_MINUTES) — every 2 minutes in debug — and runs an initial check as a background task.check_and_notify()runsprocess_gpvthenprocess_pounderPROCESS_LOCKso scheduled runs and/updatenever overlap (prevents duplicate notifications).utils.py— fetching, parsing, state and notification. State is in-memory only (PARSED_PO_DATAS,PARSED_GPV_DATAS); lost on restart, so everything current is re-notified after a restart. Blockingrequestscalls are run viaasyncio.to_thread; every request hascfg.HTTP_TIMEOUT;process_*catch network/parse errors and return status codes instead of raising.model.py—POData/GPVDatadataclasses. Service fields (notified_at,updated_at,need_to_send,start_ts) arecompare=False, so==means "same outage content"; this drives change detection.
Notification lifecycle (per poll)
- Parse fresh entries →
update_*_datamerges into the list bykey(PO: start ts + address; GPV: event date + queue).- not yet sent → replaced with fresh data;
- already sent and content changed (PO
!=, GPVis_same_schedule) → replaced, so it is re-sent; - PO only: also re-sent by
is_reminder_needed(a day since last notification, or the first processing slot of the day passed).
remove_old_*_dataprunes expired entries (PO: 1h after start; GPV: 30 min after the last power-on / end of day). GPVstart_tsholds power-on timestamps despite the name;00:00end rolls to the next day.check_and_send_*sends entries withneed_to_sendvianotify(), which sends to TG and, if enabled, Matrix (v3PUTwith txn id); an entry is marked sent if at least one channel succeeded.
Messages use Telegram HTML parse_mode — escape any scraped text with html.escape.
Constraints
- Don't change the public API (paths, params, response shapes):
/,/api/v1/status,/update,/health,/test_message/{tg|mx},/logs. Dataclasses in/statusare serialized as-is, so renaming their fields changes the API.