diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index e70a2cf..93a17d7 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,56 +1,52 @@ -# v0.8.2 (2026-05-22) +# v0.9.0 (2026-05-28) -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. +A feature + observability release. The headline additions are per-receiver Telegram options (silent send and forum-topic routing), an oversized-video fallback that bypasses Telegram's 50 MB `sendVideo` cap by switching to `sendDocument`, partial-failure visibility on the dashboard via a new `dispatch_summary` block on every `EventLog` row, and an admin diagnostic-mode panel for temporary per-module DEBUG logging with auto-revert. End-to-end correlation IDs (`dispatch_id`, `X-Request-Id`) now tie log lines to the database rows they produced. No breaking changes; no migrations required. ## User-facing changes -### Security +### Features -- **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)) +- **Per-receiver Telegram options.** Each Telegram chat receiver can now be configured to send silently (`disable_notification` — no sound or vibration on the recipient) and to route into a specific forum topic (`message_thread_id`) on supergroups with topics enabled. A new cog-icon button on the receiver row opens an inline editor; active options surface as a bell-off icon and a `#thread-id` chip on the receiver header. The plumbing uses a `ContextVar` bound at the public send entry points, so every internal payload builder (`sendMessage`, `sendPhoto`/`Video`/`Document`, `sendMediaGroup`, cache-hit POST) picks them up without a signature change ([6a8f374](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/6a8f374)) +- **Send oversized videos as documents.** A new per-target toggle, `send_large_videos_as_documents`, falls back to `sendDocument` when a video would exceed Telegram's 50 MB `sendVideo` limit. Useful for archival use cases (Immich library shares with users on free Telegram accounts) where the video would otherwise be silently dropped or noisily 413'd. Pairs with the existing `send_large_photos_as_documents` toggle. Translated copy lives under `targets.sendLargeVideosAsDocuments` ([6a8f374](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/6a8f374)) +- **Diagnostic mode for temporary DEBUG logging.** A new Diagnostics cassette on the Settings page lets an admin flip one module to DEBUG for a bounded window (1 minute to 4 hours) with an automatic revert. Useful for investigating a specific dispatch failure without flooding stderr; the revert reads the current DB-configured `log_levels` at expiry so a setting change made *during* the window is honored. State is in-memory only — a restart wipes overrides, and `setup_logging()` re-applies the DB baseline at boot, so a forgotten override cannot silently survive a deploy ([6a8f374](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/6a8f374)) +- **Partial-delivery visibility in the dashboard.** Every event-, watcher-, scheduled-, deferred-, action-, and command-dispatch path now writes a `dispatch_summary` block onto `EventLog.details`: per-target succeeded/failed counts, Telegram media `delivered_count` / `skipped_count` / `failed_count`, and a truncated list of error strings. Partial outcomes (one target out of three failed, two of ten assets dropped) are no longer indistinguishable from a clean success ([6a8f374](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/6a8f374)) +- **Inbound request correlation IDs.** A new middleware accepts `X-Request-Id` from the inbound request (so an upstream proxy with its own correlation system can propagate its id) and echoes the value back on the response. Values are sanitised to a bounded charset to prevent CR/LF injection into log lines. The id is bound into log context for every request and copied onto any `EventLog` row written during the same request, so the SPA can surface it for bug reports ([6a8f374](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/6a8f374)) ### Bug Fixes -- **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)) - -### Accessibility - -- **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)) +- **sendMediaGroup byte-budget enforcement.** Telegram's `sendMediaGroup` envelope tops out near 50 MB total (multipart bytes including form overhead). Previously the per-item budget admitted items that, when summed, busted Telegram's request cap and 413'd. A new 45 MB total-bytes budget (`TELEGRAM_MAX_GROUP_TOTAL_BYTES`) splits groups before the overhead pushes us over, with safety margin ([6a8f374](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/6a8f374)) +- **Per-item fallback inside `sendMediaGroup`.** A stale `file_id` reference (cache poisoning by Telegram's GC, or a video that's no longer downloadable from the cached URL) used to silently lose the cached asset because the failed chunk had no re-download path. Each cached item now retains its `source_url` + `download_headers` so the per-item fallback can re-fetch and retry as a single send when its `file_id` POST returns transient errors ([6a8f374](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/6a8f374)) --- ## Development / Internal -### Refactoring +### Observability -- **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)) +- **Shared `dispatch_id` across log lines and `EventLog` rows.** A `disp:<12 hex>` correlation id is bound at the top of every dispatch entry point (`dispatch_provider_event`, `check_tracker`, `dispatch_scheduled_for_tracker`, `_process_row` in deferred dispatch, `run_action`, command handler, HA status logger) via `ContextVar`. Nested dispatcher calls reuse the bound id instead of generating their own, so a single dispatch's log lines and the `EventLog.details.dispatch_id` field share one searchable id ([6a8f374](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/6a8f374)) +- **`enrich_details_with_correlation()` helper.** New helper in `notify_bridge_core.log_context` merges the bound `dispatch_id` and `request_id` onto `EventLog.details` dicts at write time without overwriting caller-provided keys. Every `EventLog` insertion site has been migrated to use it ([6a8f374](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/6a8f374)) -### Chores +### Architecture -- **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)) +- **`split_media_by_upload_size` retired.** Per-item upload accounting moved onto the new `_MediaItem` dataclass (`upload_bytes` property) and the splitter logic moved into `_send_media_group`, where the byte budget and per-item fallback live ([6a8f374](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/6a8f374)) +- **API endpoints for diagnostic mode.** New routes under `/api/app-settings/diagnostic-mode` (`GET` list, `POST` activate, `DELETE /{module:path}` revert) with admin-auth requirement and a curated module allowlist that blocks the root logger ([6a8f374](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/6a8f374)) ---- +### Tests -### Known gaps (tracked for follow-up) +- **New suites:** `test_diagnostic_mode.py`, `test_dispatch_summary.py`, `test_request_correlation.py`, `test_telegram_media_group_partial.py`, `test_telegram_per_send_options.py`. Total: 294 tests passing (up from 283 in v0.8.2) ([6a8f374](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/6a8f374)) +- **Test scaffolding fix:** `_reset_state()` in `test_diagnostic_mode.py` now also clears the `_bg_tasks` set so a long-window schedule from one test doesn't pollute `len(_bg_tasks)` in the next test's assertion. Cross-loop `.cancel()` is intentionally skipped — the prior test's loop is closed and cancelling there raises `RuntimeError` on the next test's setup ([9aada75](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/9aada75)) -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`: +### Documentation -- 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 +- **Six-axis production-readiness review** (`/.claude/reviews/`) covering backend, frontend, security, performance/DB, UI/UX, and bugs+features. Snapshots the v0.8.1 codebase; informed several items shipped in v0.8.2 and this release ([6a8f374](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/6a8f374)) +- **Functional review of Telegram / Immich / Logging subsystems** (`.claude/docs/functional-review-2026-05-28.md`). Captures what's in place, in-flight work, and ranked gaps for each subsystem; pairs with the existing feature backlog ([6a8f374](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/6a8f374)) ---
All Commits -- [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) +- [6a8f374](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/6a8f374) — `feat: observability, per-receiver Telegram options, oversized-video fallback` (alexei.dolgolyov) +- [9aada75](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/9aada75) — `fix(tests): clear diagnostic_mode _bg_tasks between cases` (alexei.dolgolyov)
diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5b73549..e1629bd 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "notify-bridge-frontend", - "version": "0.8.0", + "version": "0.9.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "notify-bridge-frontend", - "version": "0.8.0", + "version": "0.9.0", "dependencies": { "@codemirror/autocomplete": "^6.18.0", "@codemirror/lang-html": "^6.4.11", diff --git a/frontend/package.json b/frontend/package.json index 1e326a9..f02df97 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "notify-bridge-frontend", "private": true, - "version": "0.8.2", + "version": "0.9.0", "type": "module", "scripts": { "dev": "vite dev", diff --git a/packages/core/pyproject.toml b/packages/core/pyproject.toml index 3a3ce6e..d24ebef 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.2" +version = "0.9.0" 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 fe42991..f3445a5 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.2" +version = "0.9.0" description = "Standalone Notify Bridge server — FastAPI REST API with SQLite database" requires-python = ">=3.12" dependencies = [ diff --git a/packages/server/src/notify_bridge_server/__init__.py b/packages/server/src/notify_bridge_server/__init__.py index eab9098..9a98e06 100644 --- a/packages/server/src/notify_bridge_server/__init__.py +++ b/packages/server/src/notify_bridge_server/__init__.py @@ -3,4 +3,4 @@ # Source of truth for the running app's reported version. Synced with # pyproject.toml's [project].version on every release; see version.py for # the resolution rules and CLAUDE.md for the bump checklist. -__version__ = "0.8.1" +__version__ = "0.9.0"