# v0.8.1 (2026-05-16)

## ⚠️ Breaking Changes

- **Telegram webhook secret is now mandatory.** `NOTIFY_BRIDGE_TELEGRAM_WEBHOOK_SECRET` must be set when running Telegram bots in webhook mode — the inbound endpoint returns 401 without it. Polling-mode bots are unaffected ([10d30fc](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/10d30fc))
- **Generic webhooks with `auth_mode="none"` require explicit opt-in.** Existing unauthenticated webhook providers must set `acknowledge_unauthenticated=true` in their config or they will be rejected. Generic webhook ingest is also rate-limited to 60 requests/min per source IP ([10d30fc](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/10d30fc))
- **Every user now gets a `bridge_self` provider auto-seeded.** It is internal-only and excluded from the "create provider" wizard, but appears in the provider list. Wire it to a Telegram/Email/Matrix target to receive bridge health alerts ([10d30fc](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/10d30fc))

## User-facing changes

### Features

- **Home Assistant provider.** New service provider that subscribes to a Home Assistant instance over WebSocket (long-lived connection with auth handshake, exponential-backoff reconnect, area-registry enrichment) and emits 4 event types: `state_changed`, `automation_triggered`, `call_service`, `event_fired`. Trackers support entity-glob, domain-allowlist, and exact-id filters via the new `TagInput` UI control ([22127e2](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/22127e2))
- **Home Assistant bot commands.** `/status`, `/entities [glob]`, `/state <entity_id>`, `/areas` — query your HA instance from chat. Multi-command WS sessions reuse a single handshake; sensitive attributes (camera access tokens, entity pictures, etc.) are blocklisted and `/state` output is capped at 30 attributes to stay within Telegram message limits ([22127e2](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/22127e2))
- **Bridge self-monitoring (`bridge_self` provider).** A new internal provider type emits health events from the bridge itself: `bridge_self_poll_failures` (consecutive tracker poll failures), `bridge_self_deferred_backlog` (pending defers above threshold), `bridge_self_target_failures` (consecutive 5xx/network failures per target). Per-user thresholds default to 3 / 100 / 5 and are configurable. Self-loop guard ensures bridge_self failures never count toward target-failure thresholds ([10d30fc](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/10d30fc))
- **bridge_self bot commands.** `/status`, `/thresholds`, `/reset`, `/health` let operators inspect bridge health and reset counters from chat. Includes Jinja2 templates for both locales ([8651767](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/8651767))
- **On-watch stats scope selector.** New icon toggle on the "On watch" provider deck switches between page-scoped stats (legacy) and full-corpus stats that aggregate across every event matching the active filters ([dec0839](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/dec0839))

### Bug Fixes

- **Immich periodic summary now honors `periodic_interval_days`.** A configured 3-day interval was firing every day because the dispatch path never consulted the interval or start date. The scheduler now gates on `(today - start_date).days % interval == 0` and logs `interval_not_due` skips so operators can distinguish suppressed-by-interval from other skip causes ([90f958b](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/90f958b))
- **Planka webhook crash fixed.** The handler had a `NameError` on every call when reading the request body — webhook ingest from Planka now works ([10d30fc](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/10d30fc))
- **HA quiet-hours dropped events.** `ha_state_changed`, `automation_triggered`, `service_called`, and `event_fired` were missing from the deferrable set and were silently dropped during quiet windows. They now defer like every other event type ([10d30fc](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/10d30fc))
- **Notifier no longer cancels peer sends on a single failure.** Switched the per-receiver fan-out to `asyncio.gather(return_exceptions=True)`; one bad chat won't cancel sends to the rest ([10d30fc](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/10d30fc))
- **Quiet-hours gate now respects event-type-disabled.** When a tracker has the event type disabled, that wins over the deferral path — events are dropped, not stored to be drained later ([10d30fc](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/10d30fc))
- **NUT first poll seeds silently.** No more spurious `ups_on_battery` notification on the very first poll after adding a NUT provider ([10d30fc](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/10d30fc))
- **HA disconnect/reconnect now logged.** Status changes write `ha_status_*` rows to the EventLog so operators can see WS supervisor health in the UI ([10d30fc](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/10d30fc))

### Performance

- **Jinja2 template compilation cached** with `lru_cache(maxsize=512)` — repeated renders of the same template no longer reparse the source ([10d30fc](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/10d30fc))
- **Per-locale render cache in `NotificationDispatcher`** skips re-rendering identical content for receivers sharing a locale ([10d30fc](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/10d30fc))
- **Tracker list cached per `provider_id`** with a 5s TTL plus explicit invalidation on tracker CRUD — relieves the HA chat-bus rate-query pressure ([10d30fc](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/10d30fc))
- **Nav-counts collapsed from 16 round-trips to a single `UNION ALL`** ([10d30fc](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/10d30fc))
- **HA event_log skips empty events** — `assets_added`/`assets_removed` events with empty payloads are no longer persisted ([10d30fc](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/10d30fc))

### Security

- **DNS-rebinding SSRF protection.** `PinnedResolver` is now wired into the shared aiohttp session — outbound URL validation pins the resolved IP for the lifetime of the request ([10d30fc](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/10d30fc))
- **Mass-assignment guards** added on Action create/update; cron expressions with sub-minute granularity are rejected ([10d30fc](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/10d30fc))
- **Backup import hardening.** JSON depth capped at 10, node count at 100k; `_sanitize_config` now extends to all JSON-typed fields on import ([10d30fc](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/10d30fc))
- **Telegram `_safe_get` walks redirects manually** with SSRF revalidation at every hop ([10d30fc](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/10d30fc))
- **Bcrypt 72-byte password length cap** with a clear `422` response (was silently truncating before) ([10d30fc](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/10d30fc))
- **Webhook payload body redaction.** Sensitive substring set extended with `oauth`, `client_secret`, `webhook_secret`, `csrf` in both header filter and template-extras filter ([10d30fc](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/10d30fc))

### Operations

- **Deep healthcheck at `/api/ready`** — checks DB `SELECT 1`, scheduler running, HA supervisor presence; returns `{ready, checks, errors, version}` ([10d30fc](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/10d30fc))
- **Prometheus metrics at `/api/metrics`** — `deferred_pending`, `event_log_total`, `dispatch_duration`, `poll_failures`, `send_failures` ([10d30fc](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/10d30fc))
- **New `OPERATIONS.md`** covering deploy, healthchecks, metrics, backup/restore procedures, log handling, common scenarios, and upgrade flow ([10d30fc](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/10d30fc))

---

## Development / Internal

### Architecture

- **Shared dispatch pipeline.** Extracted webhook ingest's event-log + deferred-dispatch + quiet-hours code path from `api/webhooks.py` into `services/event_dispatch.py` so HA subscription and webhook ingest now share the same pipeline ([22127e2](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/22127e2))
- **`ServiceProvider` ABC gains optional `subscribe()`** for push-style providers; `HomeAssistantServiceProvider` uses it via a per-provider supervisor task started in the FastAPI lifespan ([22127e2](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/22127e2))
- **Provider construction switched from if/elif ladder to factory registry** ([10d30fc](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/10d30fc))
- **Provider credential resolution unified** across all 5 dispatch sites ([10d30fc](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/10d30fc))
- **`NotificationDispatcher` hoisted out of the per-tracker loop** ([10d30fc](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/10d30fc))

### Database

- **New UNIQUE indexes:** `service_provider.webhook_token`, `telegram_bot.webhook_path_id`, partial UNIQUE on `telegram_bot.bot_id`, `telegram_chat(bot_id, chat_id)`, `notification_tracker_target` unique link, partial UNIQUE on `bridge_self` provider per user ([10d30fc](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/10d30fc))
- **Composite `ix_event_log_user_event_type_created` index** ([10d30fc](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/10d30fc))
- **`save_chat_from_webhook` switched to `ON CONFLICT DO UPDATE`** (was racy under concurrent webhook delivery) ([10d30fc](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/10d30fc))
- **`ondelete=CASCADE` on user-id FKs** (model annotation; app-side cascade delete added for existing data) ([10d30fc](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/10d30fc))
- **`delete_notification_tracker` converted from N+1 to bulk DELETE/UPDATE** ([10d30fc](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/10d30fc))
- **Module-level `asyncio.Lock` replaced with lazy `_get_lock()` pattern** (avoids cross-event-loop binding) ([10d30fc](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/10d30fc))
- **VACUUM INTO snapshot now `PRAGMA integrity_check`-verified** before being returned ([10d30fc](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/10d30fc))
- **Batched `receivers/chats/bots` in `load_link_data`** (was per-target N+1) ([10d30fc](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/10d30fc))
- **`flag_modified` on JSON column reassignments** in deferred_dispatch ([10d30fc](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/10d30fc))

### Frontend

- **76 `catch (err: any)` sites converted to `errMsg(err)` helper** ([10d30fc](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/10d30fc))
- **`globalProviderFilter` made a pure getter**; reconciliation moved to a one-time `$effect` in `+layout` ([10d30fc](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/10d30fc))
- **Provider-filter binding simplified** — paired `$effect`s and the `_syncingFilter` flag removed; now a one-way derived value ([10d30fc](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/10d30fc))
- **`entity-cache` got a separate `_refreshing` flag** for background re-fetches so loading spinners don't appear on revalidation ([10d30fc](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/10d30fc))
- **`api.ts` 401 handling rewritten:** `AuthRedirectError` class + dedup `_redirecting` flag, `goto()` instead of `window.location.href` ([10d30fc](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/10d30fc))
- **Accessibility:** `aria-expanded` on mobile More menu, `role=switch` + `aria-checked` on Telegram bot toggles ([10d30fc](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/10d30fc))
- **Provider-specific hardcoding removed:** Immich-only block extracted to descriptor `featureDiscoveryHint` ([10d30fc](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/10d30fc))
- **5 `svelte-check` null-narrowing errors fixed** in `EventDetailModal` ([10d30fc](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/10d30fc))
- **New `TagInput` component** for free-text glob/domain lists; new toggle `ConfigField` type for HA descriptor ([22127e2](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/22127e2))

### CI/Build

- **CI pytest gate added** to `.gitea/workflows/build.yml` and `release.yml` (wheel-built install to dodge editable-install slowness on the hosted runner) ([10d30fc](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/10d30fc))

### Tests

- **New test suites:** `test_bridge_self` (11), `test_gitea_parser` (9), `test_planka_parser` (6), `test_immich_change_detector` (6), `test_backup_roundtrip` (1) ([10d30fc](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/10d30fc))

### Other

- **`command_sync` snapshot+expunge bot before exiting `AsyncSession`** (was raising on detached-instance access) ([10d30fc](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/10d30fc))
- **HA `asyncio.shield` now drains inner task on cancellation** (was leaking tasks on supervisor restart) ([10d30fc](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/10d30fc))
- **APScheduler drain job ID resolution upgraded to seconds** (was minute-bucketed; collisions possible) ([10d30fc](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/10d30fc))
- **Webhook payload rollback failures now logged** (were swallowed) ([10d30fc](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/10d30fc))

---

<details>
<summary>All Commits</summary>

| Hash | Message | Author |
| ---- | ------- | ------ |
| [8651767](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/8651767) | feat: bridge_self bot commands — status, thresholds, reset, health | alexei.dolgolyov |
| [10d30fc](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/10d30fc) | feat: production readiness — security, perf, bug fixes, bridge self-monitoring | alexei.dolgolyov |
| [22127e2](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/22127e2) | feat: Home Assistant provider — WebSocket subscription + bot commands | alexei.dolgolyov |
| [90f958b](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/90f958b) | fix(server): honor periodic_interval_days for Immich periodic summary | alexei.dolgolyov |
| [dec0839](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/dec0839) | feat: on-watch stats scope selector (page vs all) | alexei.dolgolyov |

</details>
