diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index ae97f34..e70a2cf 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,116 +1,56 @@ -# v0.8.1 (2026-05-16) +# v0.8.2 (2026-05-22) -## ⚠️ 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)) +A production-readiness hardening release that follows up on v0.8.1 with six isolated, low-risk fixes surfaced by a parallel full-codebase review (backend, frontend, security, performance, UI/UX, bugs+features). No breaking changes; no migrations required. ## User-facing changes -### Features +### Security -- **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 `, `/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)) +- **Provider `access_token` masked in API responses.** The provider GET endpoints were leaking plaintext credentials — most importantly Home Assistant long-lived tokens — in their JSON payloads. The field is now masked on read and dropped on edit when the `***` placeholder is sent back, so the UI can show "set" / "unset" without ever round-tripping the secret. Centralized through `PROVIDER_SECRET_FIELDS` so every call site stays in sync ([2d59a5b](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/2d59a5b)) +- **Pre-auth resource-exhaustion amplifier closed on webhook ingest.** The Gitea provider used to read the 1 MiB request body before checking whether a secret was even configured or whether the request had a signature header — an unauthenticated client could force a body read on every hit. The generic-webhook bearer-token path had the same shape: body read before Authorization check. Both now bail out before consuming the body when the auth precondition fails ([2d59a5b](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/2d59a5b)) ### 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)) +- **Home Assistant status-change events no longer silently lost.** `ha_status_changed` rows are written from `asyncio.create_task(...)`, but `create_task` only keeps a weak reference — the task was being garbage-collected before the row landed, so connection-flap events disappeared. The task handles are now held in a module-level set with a `done_callback` to release them on completion ([2d59a5b](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/2d59a5b)) +- **Telegram-webhook handler exceptions can no longer leak writes.** The catch-all error path in the Telegram inbound endpoint now rolls back the request's SQLAlchemy session before returning, so a handler crash mid-transaction cannot bleed uncommitted state into the next request on the same connection ([2d59a5b](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/2d59a5b)) -### Performance +### Accessibility -- **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)) +- **Toast notifications now announced by screen readers.** Added `role="region"` on the snackbar container plus per-toast `role` / `aria-live` / `aria-atomic` attributes, with a localized region name (`snackbar.region`) in both `en` and `ru` ([2d59a5b](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/2d59a5b)) +- **Active sidebar link now has an accessible state.** `aria-current="page"` is now set on the matching nav item, so assistive tech can announce the active route ([2d59a5b](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/2d59a5b)) --- ## Development / Internal -### Architecture +### Refactoring -- **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)) +- **Last `provider.type === 'immich'` check removed from components.** The action-rule editor's "Auto-organize" affordance now consumes a `supportsAutoOrganize` capability on `ProviderDescriptor` instead of branching on the provider type — bringing the rule editor under CLAUDE.md rule 8 (no provider-specific hardcoding in components) ([2d59a5b](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/2d59a5b)) -### Database +### Chores -- **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)) +- **Synced `.facts-sync.json` with `claude-code-facts@cfdafa9`.** Both previously pending suggestions (venv install for monorepos + hatchling METADATA workaround) were applied upstream; the local queue is empty ([a20635a](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/a20635a)) -### 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)) +### Known gaps (tracked for follow-up) -### CI/Build +The full-codebase review surfaced more ship-blockers than this release fixes. Each of the items below needs more than a mechanical edit and is tracked in `.claude/reviews/README.md`: -- **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)) +- Secret encryption at rest +- JWT moved into an HTTP-only cookie +- Alembic adoption (currently `create_all`) +- Webhook delivery idempotency +- Deferred-dispatch crash window +- Persisted Telegram update watermark +- `bridge_self` counter lock ---
All Commits -| 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 | +- [2d59a5b](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/2d59a5b) — `fix: production-readiness hardening from full-codebase review` (alexei.dolgolyov) +- [a20635a](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/a20635a) — `chore: sync .facts-sync.json with claude-code-facts@cfdafa9` (alexei.dolgolyov)
diff --git a/frontend/package.json b/frontend/package.json index 351c30f..1e326a9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "notify-bridge-frontend", "private": true, - "version": "0.8.1", + "version": "0.8.2", "type": "module", "scripts": { "dev": "vite dev", diff --git a/packages/core/pyproject.toml b/packages/core/pyproject.toml index b34382a..3a3ce6e 100644 --- a/packages/core/pyproject.toml +++ b/packages/core/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "notify-bridge-core" -version = "0.8.1" +version = "0.8.2" description = "Core library for Notify Bridge — service provider abstractions, models, notifications, and templates" requires-python = ">=3.12" dependencies = [ diff --git a/packages/server/pyproject.toml b/packages/server/pyproject.toml index 3f35dd4..fe42991 100644 --- a/packages/server/pyproject.toml +++ b/packages/server/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "notify-bridge-server" -version = "0.8.1" +version = "0.8.2" description = "Standalone Notify Bridge server — FastAPI REST API with SQLite database" requires-python = ">=3.12" dependencies = [