MeghdadFadaee ded914a6fd
Some checks failed
Test blackout notifier / test (push) Failing after 2s
Hourly blackout check / check (push) Successful in 3s
feat: add live outage countdown cli dashboard
2026-07-18 08:11:55 +03:30
2024-11-16 20:19:23 +03:30

Daily Blackout Check

Checks the official SAAPA planned-outage API and sends Persian notifications to an Eitaa chat when an outage is added, changed, cancelled, or restored.

The checker is designed for an hourly Gitea Actions workflow. Its deduplication state is stored in state/outages.json and committed back to this repository.

Behavior

  • Queries each configured bill ID from today through five days ahead.
  • Uses Tehran time regardless of the runner container's timezone.
  • Sends all upcoming outages when starting with empty state.
  • Treats an existing outage number with changed time, address, date, reason, or planned status as an update.
  • Reports a future outage as cancelled only after two consecutive successful checks omit it.
  • Records a notification only after Eitaa accepts it. This favors delivery over perfect deduplication: a failed state push can cause a duplicate next hour.
  • Rejects malformed responses and snapshots larger than the configured safety limit without changing that bill's state.

Local setup

Python 3.11 or newer is required. The Gitea runner image uses Debian's Python 3.11.

python -m venv .venv
source .venv/bin/activate
python -m pip install -e '.[test]'
cp .env.example .env

Fill in the required values in .env, then preview the next run without sending messages or changing state:

python -m blackout_notifier check --dry-run

Run the live checker with:

python -m blackout_notifier check

Configuration

Variable Required Default Purpose
BARGHEMAN_TOKEN Yes SAAPA bearer token
EITAAYAR_TOKEN Yes EitaaYar bot token
CHAT_ID Yes Destination Eitaa chat
BILL_IDS Yes Comma-separated decimal bill IDs
STATE_FILE No state/outages.json Durable notification state
LOOKAHEAD_DAYS No 5 Query horizon, from 1 to 30 days
REQUEST_TIMEOUT_SECONDS No 15 HTTP timeout, from 1 to 120 seconds
MAX_EVENTS_PER_BILL No 25 Response safety limit, from 1 to 100

Shell and Gitea-provided variables take precedence over .env.

Gitea deployment

This repository targets Gitea 1.25.x and a Docker-based runner advertising the ubuntu-latest label. The test and hourly workflows pull this private image from the Gitea Container Registry:

mahgit.ir/meghdadfadaee/daily-blackout-check-runner:latest

It contains Node for the checkout action plus Python, Git, timezone data, application dependencies, pytest, and Ruff. Scheduled jobs therefore do not install Python or download Python packages.

The Node base image is also mirrored in Gitea, so image builds do not contact Docker Hub:

mahgit.ir/meghdadfadaee/build-runner-image-base:node-20-bookworm

Registry credentials

Create a Gitea personal access token for MeghdadFadaee with package permission set to Read and Write. Add these repository Actions secrets:

Secret Value
REGISTRY_USERNAME Gitea username that owns the package
REGISTRY_TOKEN Personal access token with package Read and Write permission
BARGHEMAN_TOKEN SAAPA bearer token
EITAAYAR_TOKEN EitaaYar bot token
CHAT_ID Destination chat ID
BILL_IDS Comma-separated bill IDs

The registry token is used to publish the image and as container.credentials when Gitea Runner pulls the private image before starting a job.

Runner configuration

The image-build workflow needs access to the Docker daemon used by act_runner. In the runner's generated config.yaml, update the existing container section:

container:
  force_pull: true
  valid_volumes:
    - /var/run/docker.sock

The runner container itself must already mount the same socket, as in the normal Docker-based act_runner setup. Restart the runner after changing its config.

Docker socket access is equivalent to control of the Docker host. Use a trusted, preferably repository-level runner. The build workflow runs only for changes to the root image lock, .dockerignore, or its own workflow on main; it never runs for pull requests.

Image publishing flow

The Build runner image workflow:

  1. Starts in the shared Gitea-hosted build-runner-image-base:node-20-bookworm image.
  2. Calls the shared Gitea action MeghdadFadaee/build-runner-image-action@v1 to install the Docker client, authenticate, build, verify, and publish.
  3. Uses the action's shared Dockerfile with the root requirements.lock. .dockerignore restricts the build context to that lock, so .env, source, and outage state are never sent to Docker.
  4. Runs import, pytest, and Ruff smoke checks inside the candidate image.
  5. Pushes an immutable 12-character Git revision tag and updates latest only after the candidate passes verification.

It runs automatically when requirements.lock, .dockerignore, or its workflow changes on main, and can also be started manually. Image builds require Debian mirrors, PyPI, Gitea, and access to the Docker socket. Normal test and hourly runs only need Gitea, gitea.com, SAAPA, and EitaaYar. The mirrored Node image is intentionally static; refresh it from the upstream node:20-bookworm image when adopting Node security updates.

Deployment order:

  1. Enable Packages and Actions for the repository/owner.
  2. Configure the runner socket permission and restart it.
  3. Add all repository Actions secrets listed above.
  4. Run Build runner image manually and confirm both registry tags appear.
  5. Run Test blackout notifier manually.
  6. Run Hourly blackout check manually once and inspect the notification and resulting state commit.
  7. Leave the @hourly schedule enabled.

The workflow uses fully qualified https://gitea.com/actions/... actions rather than resolving actions through GitHub. Gitea 1.25 ignores workflow permissions and concurrency, so the notifier workflow deliberately does not rely on them. Its checkout credential must retain the default ability to push to the current repository. Scheduled jobs should not be manually overlapped; in the rare event of a push race, the job fails and unsaved notices may be repeated on the next run.

Development

ruff check .
pytest

Tests mock both external services and never send real messages.

Historical state from the previous implementation is retained at archive/blackouts-1404.json for reference only and is not imported.

Standalone outage dashboard

main.sh is a read-only terminal dashboard for the outage state committed to this public repository. It downloads the latest state/outages.json directly from Gitea, so it can be copied and run without cloning the project or installing the Python application.

It requires Bash, curl, jq, and GNU date:

./main.sh

The display uses a flicker-free alternate terminal screen and runs until q is pressed (or it is interrupted with Ctrl+C). It updates the countdown every second and refetches the committed state every 10 minutes. Set a different whole minute interval with -i/--interval:

./main.sh --interval 5

The dashboard uses Tehran time to show a large HH : mm : ss countdown for the outage currently in progress or the nearest upcoming outage. A compact schedule below it lists all active current and future events, including emergency outages. The terminal output uses English-only labels and omits the stored address and reason text. When there are no qualifying events it prints an ALL CLEAR screen and exits successfully.

Colors are enabled only when output is attached to a terminal. Set NO_COLOR to disable them explicitly, or set COLUMNS to control the rendered width:

NO_COLOR=1 COLUMNS=80 ./main.sh

When pyfiglet or figlet provides the univers font, the countdown uses it automatically. Otherwise the bundled matching glyphs are used, keeping the same appearance without making Figlet a dependency.

Description
No description provided
Readme MIT 1.6 MiB
Languages
Python 55.7%
Shell 44.3%