212 lines
7.8 KiB
Markdown
212 lines
7.8 KiB
Markdown
# 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.
|
|
|
|
```sh
|
|
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:
|
|
|
|
```sh
|
|
python -m blackout_notifier check --dry-run
|
|
```
|
|
|
|
Run the live checker with:
|
|
|
|
```sh
|
|
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:
|
|
|
|
```text
|
|
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:
|
|
|
|
```text
|
|
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:
|
|
|
|
```yaml
|
|
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
|
|
|
|
```sh
|
|
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`:
|
|
|
|
```sh
|
|
./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`:
|
|
|
|
```sh
|
|
./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:
|
|
|
|
```sh
|
|
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.
|