# Daily Blackout Check

Checks the official SAAPA planned-outage API and sends Persian notifications to
the Eitaa chats configured for each bill 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.
- Routes each bill's notifications to all of its configured Eitaa chats.
- 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 every configured chat accepts it. This
  favors delivery over perfect deduplication: if one chat fails, successful
  destinations may receive a retry on the next run.
- 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 |
| `BILL_CHAT_MAP` | Yes | — | Comma-separated `BILL_ID:CHAT_ID` routes |
| `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`.

For example, this sends bill `7356609804429` to chats `10251670` and `20873456`,
while bill `7619314604429` is sent only to chat `30984567`:

```dotenv
BILL_CHAT_MAP=7356609804429:10251670,7356609804429:20873456,7619314604429:30984567
```

Repeat a bill ID with another chat ID to add destinations; duplicate identical
routes are ignored. The old `CHAT_ID` plus `BILL_IDS` configuration is still
accepted when `BILL_CHAT_MAP` is absent, but it sends every bill to the same chat
and is intended only for backward compatibility.

## 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 |
| `BILL_CHAT_MAP` | Comma-separated `BILL_ID:CHAT_ID` routes |

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
```

On macOS, install the modern Bash and GNU `date` implementations with Homebrew:

```sh
brew install bash coreutils
```

`main.sh` automatically switches from Apple's Bash 3.2 to Homebrew Bash and
adds Homebrew's GNU utilities to `PATH`, so both `./main.sh` and
`bash ./main.sh` work after installation.

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
```

Select one bill for the large countdown with `-b`/`--bill-id`:

```sh
./main.sh --bill-id 7356609804429
```

The dashboard uses Tehran time to show a large `HH : mm : ss` countdown for the
selected bill's ongoing or nearest upcoming outage. The compact schedule below
continues to list active current and future events for every bill, including
emergency outages. If the selected bill has no qualifying outage, the countdown
area says so while the full schedule remains visible. Without `--bill-id`, the
countdown retains the original behavior and selects across all bills.

The terminal output uses English-only labels and omits the stored address and
reason text. When there are no qualifying events for any bill 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.
