Files
Anry DasandClaude Opus 5.5 22fc8b38bf Load config, harden processing and notifications
- 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>
2026-10-13 14:04:18 +03:00

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-search by account number (requested with verify=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.yaml is loaded at import of main.py via cfg.init_config(); values use !secret <key> resolved from configs/secrets.yaml (gitignored, must exist for a real run). The !secret tag is registered on a private loader subclass in config_file.read_yaml.
  • app_config.py exposes settings as module-level globals (cfg.TG_TOKEN, cfg.IS_DEBUG, …) populated by parse_config(). Always access them as cfg.X at call time — from app_config import X would capture the pre-config default.
  • Booleans in YAML may be strings ("true") — parse with as_bool.
  • configs/log.yaml is the uvicorn log config. All modules log through the single cfg.LOG logger, which also feeds the in-memory LOG_LIST served by /api/v1/logs (unauthenticated — never log secrets; use utils.safe_error(e) for request exceptions, since TG URLs contain the token).

Architecture

  • main.py — FastAPI app with a sub-app api mounted at /api/v1 (the root HTML page in templates/info.html lists api routes). lifespan creates an AsyncIOScheduler with one cron job per (hour in [start_hour, stop_hour), minute in PROCESSING_MINUTES) — every 2 minutes in debug — and runs an initial check as a background task. check_and_notify() runs process_gpv then process_po under PROCESS_LOCK so scheduled runs and /update never 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. Blocking requests calls are run via asyncio.to_thread; every request has cfg.HTTP_TIMEOUT; process_* catch network/parse errors and return status codes instead of raising.
  • model.py — POData / GPVData dataclasses. Service fields (notified_at, updated_at, need_to_send, start_ts) are compare=False, so == means "same outage content"; this drives change detection.

Notification lifecycle (per poll)

  1. Parse fresh entries → update_*_data merges into the list by key (PO: start ts + address; GPV: event date + queue).
    • not yet sent → replaced with fresh data;
    • already sent and content changed (PO !=, GPV is_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).
  2. remove_old_*_data prunes expired entries (PO: 1h after start; GPV: 30 min after the last power-on / end of day). GPV start_ts holds power-on timestamps despite the name; 00:00 end rolls to the next day.
  3. check_and_send_* sends entries with need_to_send via notify(), which sends to TG and, if enabled, Matrix (v3 PUT with 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 /status are serialized as-is, so renaming their fields changes the API.