Take a consistent, atomic copy of the DB at lifespan startup BEFORE
migrations run, so a botched future upgrade is recoverable by restoring
a single file instead of a data-loss incident.
Uses SQLite's VACUUM INTO — safe under WAL, cannot tear against
concurrent writes. Best-effort: failures are logged, never raised —
the main DB remains the source of truth.
Configurable via NOTIFY_BRIDGE_PRE_MIGRATE_SNAPSHOT_KEEP (default 5;
0 disables). Snapshots land in ``data_dir/backups/pre-migrate-<ts>.db``
and the N oldest are pruned each boot.
Security
- SSRF: async DNS resolver; allow_redirects=False on all outbound clients;
matrix homeserver_url validated on create/update/test; update_provider
and email_bot merge incoming config and reject ***-masked secrets.
- Auth: bcrypt offloaded to asyncio.to_thread; JWT now carries iss/aud +
leeway and rejects missing claims; setup TOCTOU closed inside a
transaction; rate limits extended (default 600/min, 10/min on password
change, 30/min on needs-setup); constant-time login to prevent username
enumeration.
- Config: rejects known dev secret keys; validates CORS origin schemes,
port range, token lifetimes.
- Webhook handlers stream-read body with a 1 MiB cap; Discord 429 retries
bounded (3 attempts, Retry-After capped at 60 s).
- CSP + HSTS added to SecurityHeadersMiddleware.
Async / runtime
- SQLite engine: WAL, synchronous=NORMAL, foreign_keys=ON, busy_timeout,
pool_pre_ping, dispose on shutdown.
- Lifespan shutdown now stops scheduler before closing HTTP session and
disposing the engine.
- Shared aiohttp session locked against concurrent first-caller races;
core NotificationDispatcher accepts and reuses it.
- Storage and scheduled backup writes wrapped in asyncio.to_thread.
- NUT client writes bounded by asyncio.wait_for.
- Telegram poller switched from 3 s short-poll to 30 s interval + 25 s
long-poll (~10x fewer API calls).
Database
- New performance-indexes migration covers every FK/owner column and
hot-path composite (notification_tracker(provider_id, enabled);
event_log(user_id, created_at DESC); webhook_payload_log(provider_id,
created_at DESC); action_execution(action_id, started_at DESC)).
- New schema_version table for future upgrade gating.
- __system__ placeholder user (id=0) seeded so user_id=0 system defaults
satisfy the newly enforced FK; filtered out of /auth/needs-setup,
/api/users, and setup.
- list_notification_trackers rewritten to batched loads (was 1+N+N*M).
- Retention job extended to event_log, webhook_payload_log, and
action_execution; retention days exposed as a setting.
Scheduler
- AsyncIOScheduler job_defaults: coalesce, misfire_grace_time=300,
max_instances=1.
Ops
- uvicorn runs with proxy_headers, forwarded_allow_ips,
timeout_graceful_shutdown; access log suppressed in non-debug.
- FastAPI version string now reads from importlib.metadata.
- New /api/ready endpoint separate from /api/health.
- docker-compose drops the ALLOW_PRIVATE_URLS=1 default, adds mem/cpu/pid
limits, read_only + tmpfs, cap_drop:ALL, no-new-privileges; healthcheck
targets /api/ready.
- CI now runs on push/PR with backend pytest, frontend svelte-check +
build, and a non-push image build; release workflow gated on tests,
publishes immutable sha-<commit> image tag, adds Trivy scan.
Tests
- New packages/server/tests/ with 29 passing tests: config validation,
JWT round-trip + aud/alg=none rejection, SSRF scheme and private-range
enforcement (sync + async), Discord bounded retry, and a lifespan-level
/api/health + /api/ready smoke check.
- Renamed the misnamed services/test_dispatch.py to manual_dispatch.py so
pytest never auto-collects production code.
Frontend
- /login now redirects already-authenticated users to /, shows a distinct
'backend unreachable' banner (en/ru) when /auth/needs-setup fails.
Boot-time logging was a three-line basicConfig stub with no timestamps, no
correlation, and silent drops at every layer of the Telegram send path — a
/random command that delivered text but no media left zero evidence in the
log. This replaces the setup and closes every silent drop encountered end-to-end.
New infrastructure:
- notify_bridge_core.log_context: request_id/command/chat_id/bot_id/dispatch_id
ContextVars with a bind_log_context() context manager so deep call sites
(TelegramClient, NotificationDispatcher) inherit the correlation tag without
threading args through.
- notify_bridge_server.logging_setup: dictConfig-based setup with a
LogRecordFactory that tags every record, a SecretMaskingFilter that redacts
/botN:TOKEN plus Authorization/x-api-key/password/secret in messages AND
tracebacks, a JSON formatter for aggregators, text formatter with grep-friendly
[req=... cmd=... bot=... chat=... disp=...] prefix, and default dampening
for sqlalchemy/aiohttp/apscheduler/urllib3/PIL.
Runtime control:
- NOTIFY_BRIDGE_LOG_LEVEL / _FORMAT / _LEVELS env vars (boot).
- DB-backed log_level / log_format / log_levels AppSettings, applied on
boot after migrations and live via apply_log_levels() when edited in
the settings UI (format still requires restart, logs a WARN).
- Frontend settings page gains a Logging card (level dropdown, format
dropdown, per-module overrides); en/ru i18n keys added.
Call-site fixes (/random media-group blind spot and adjacent):
- TelegramClient._fetch_asset: every silent drop now WARN-logs with reason
(missing url, HTTP non-200, size/dimension limits, ClientError).
- TelegramClient._send_media_group: WARN on "chunk had N items but 0 usable",
ERROR on sendMediaGroup non-ok/transport with full context; returns
success=False + "no_items_delivered" instead of success=True with an empty
message_ids list so callers can distinguish.
- TelegramClient.send_message / _upload_media / _send_from_cache: ERROR on
non-ok + transport failures with status/code/desc; DEBUG for cache-hit
fallbacks.
- NotificationDispatcher.dispatch: generates a dispatch_id, binds it, logs
start/finish with failure count, uses exc_info for target failures.
- commands/handler: missing/failed templates -> ERROR + exc_info; send_reply
and send_media_group errors upgraded WARNING -> ERROR with chat/error_code
context; rate-limit and truncation cases logged with full context.
- commands/webhook and services/telegram_poller: bind_log_context(request_id
=tg:<update_id>, command, chat_id, bot_id), INFO on receive/dispatch/
completion with duration, exc_info on raise, INFO when commands disabled.
- commands/immich: INFO when album scope is empty; WARN per asset dropped
from media payload and a summary WARN when "N assets in, 0 out".
Backend
- Per-chat album scope for Immich commands (search/latest/memory/...): new
allowed_album_ids on CommandTrackerListener, threaded listener/page kwargs
through ProviderCommandHandler.handle; PATCH listener-scope endpoint.
- /search and /find accept a trailing page number; Immich client search_smart
/ search_metadata take a page param.
- Immich person-asset lookup switched from removed GET /api/people/{id}/assets
to POST /api/search/metadata with personIds (fixes /person command and
auto_organize rules silently returning zero candidates on Immich 1.106+).
- Auto_organize rule now sets the target album's thumbnail to the first added
image when missing (falls back to any asset type); failures do not fail the
rule. add_assets_to_album surfaces the Immich error body on non-2xx.
- EventLog.user_id / action_id / action_name columns with defensive migration
+ backfill. Status query filters by user_id directly; Immich/webhook paths
emit user_id explicitly. action_runner writes an action_success/partial/
failed event on each non-dry-run.
- Dashboard DELETE /api/status/events (scoped to user_id) + rendering live
tracker/provider/action names via FK join with snapshot fallback.
- PATCH /api/users/{id} for username/role change with last-admin guard.
- Deletion protection returns structured {message, entity, blocked_by}
(ApiError carries .blockedBy; frontend opens BlockedByModal).
- Backup prepare-restore → AppSetting markers + atomic write of
pending_restore.json; lifespan hook applies on next startup and archives
under data/applied_restores/. apply-restart sends SIGTERM so the lifespan
shutdown runs; NOTIFY_BRIDGE_SUPERVISED env override gates the button.
Manual POST /api/backup/files (same format as scheduled).
- New periodic-summary test path reuses shared collect_scheduled_assets
(limit=0) so test and future production code go through one primitive.
- Per-receiver locale for Telegram test messages (resolves
TelegramChat.language_override per chat instead of applying the first
receiver's locale to everyone).
- Bounded concurrency (semaphores) in NotificationDispatcher._preload_asset_data
and _refresh_telegram_chat_titles; chat title sweep extended to 24h since
save_chat_from_webhook covers active chats opportunistically.
- Telegram poller detects the \"webhook is active\" 409 and auto-calls
deleteWebhook for bots whose DB update_mode is polling (throttled per bot).
- TelegramClient.get_chat added (CLAUDE.md rule 6); set_album_thumbnail added.
- Seeds: rename \"Default Commands\" → \"Default Immich Commands\";
track_assets_removed default False.
Frontend
- Global provider selector visible when there is only one provider.
- Clear-events button + i18n + ConfirmModal on the dashboard; new icons/
labels/filters/colors for action_success / action_partial / action_failed.
- Auto-select first available tracking/template/command/config + bot on
create forms (trackers, command-trackers, targets, template/command
configs).
- Telegram target disable_url_preview defaults to true.
- BlockedByModal wired into 8 deletion flows; fetchAuth helper for
multipart/binary calls (reuses api()'s refresh + ApiError mapping).
- Immich tracker 'Checking links' parallelised (concurrency cap 6).
- Backup page: pending-restore banner + Apply-now / Apply-later modal,
restarting overlay polling /api/health, manual 'Create backup' button.
- Command-trackers listener row gets an 'Edit album scope' modal with
inherit/explicit multiselect.
- Users page: Edit user modal (username + role).
- parseDate helper for consistent UTC date rendering.
Migrations / schema
- event_log: + user_id, action_id, action_name (+ backfill user_id from
notification_tracker).
- command_tracker_listener: + allowed_album_ids.
Deep-linking to non-root URLs like /settings or /notification-trackers
returned {"detail":"Not Found"} because StaticFiles(html=True) only
serves index.html for directory roots, not for arbitrary SPA routes.
Subclass StaticFiles with an SPA fallback: any 404 on a non-/api path
serves the root index.html, letting the SvelteKit router hydrate the
correct view on the client. Real /api/* 404s still bubble up as JSON
from FastAPI.
Add person exclude criteria to Immich auto-organize — assets containing
excluded persons are filtered out after candidate gathering. Also adds
full backup/restore system with export, import, scheduled backups, and
retention management.
- Add locale support to notification templates (matching command template
pattern): TemplateSlot now has locale field with (config_id, slot_name,
locale) uniqueness, nested API format {slot: {locale: template}}
- Migration merges separate EN/RU system configs into unified per-provider
configs; seeds create one config per provider with multi-locale slots
- Locale-aware dispatch with EN fallback in NotificationDispatcher
- Frontend locale tabs (EN/RU) on template config editor
- Fix tracking config cards not showing default provider icons
- Global provider filter, search palette, and various UX polish
Scheduler provider:
- Virtual provider (no external service) that emits SCHEDULED_MESSAGE
events on user-defined intervals or cron expressions
- Custom variables stored in tracker filters, flattened into template context
- fire_count persists across triggers via tracker state
- APScheduler CronTrigger support for cron-mode schedules
- Default templates (EN+RU), seeded on startup
Multi-provider UX fixes:
- Tracking config hides Immich-specific sections (periodic, scheduled,
memory, asset display) for non-Immich providers
- Command config driven by provider capabilities — hides commands/settings
for providers without bot commands
- Template config hides empty "Scheduled Messages" group
- Test menu on tracker targets is provider-aware (Immich shows all 4 test
types, others show only basic)
- Removed redundant Test button from tracker card
- System-owned tracking configs (user_id=0) seeded for Gitea + Scheduler
- Fixed ownership checks to allow system configs in tracker-target links
- Capabilities cache shared across template-configs and command-configs
- Command tracker bot selector uses EntitySelect instead of raw select
- Sample context includes Gitea + Scheduler variables for template preview
First webhook-based provider integration (Immich uses polling).
Gitea pushes events via POST /api/webhooks/gitea/{provider_id} with
HMAC-SHA256 signature validation.
- 9 event types: push, issue opened/closed/commented, PR opened/closed/merged/commented, release published
- Generic filters system on NotificationTracker (collections, senders, exclude_senders)
- Provider capabilities include supported_filters and webhook_based flag
- Gitea API client for connection testing and repository listing
- 18 default Jinja2 notification templates (EN + RU)
- Frontend: conditional provider forms, Gitea event toggles in tracking config
- Auto-migration for filters column and Gitea tracking flags
- Locale-aware templates: CommandTemplateSlot now has a locale column,
allowing each slot to have per-language variants (EN/RU). Templates
are resolved at runtime from the Telegram user's language_code.
- Merged system configs: "Default Commands (EN)" and "(RU)" merged
into a single "Default Commands" config with locale-aware slots.
Migration handles existing data automatically.
- Configurable command descriptions: hardcoded COMMAND_DESCRIPTIONS
replaced with desc_* template slots (desc_status, desc_help, etc.)
that users can customize per locale. setMyCommands registers all
locales explicitly.
- Removed locale from CommandConfig: no longer needed since locale
is derived from the Telegram user's language at runtime.
- Debounced command auto-sync: after command config/tracker changes,
affected bots are marked dirty and synced after a 30s debounce
window. Manual "Sync with Telegram" button still works.
- Entity pickers in LinkedTargetsSection: replaced 6 plain <select>
elements with EntitySelect components (search, icons, keyboard nav).
Added onselect callback and size="sm" props to EntitySelect.
- Template seed now re-creates missing system templates on every startup
(not just first boot), using raw SQL to handle legacy NOT NULL columns
- Tracking configs: add provider type selector (was missing)
- All config forms: provider type uses IconGridSelect during creation,
read-only text during editing (immutable after creation)
- Pages: tracking-configs, command-configs, command-template-configs,
template-configs
- Discord, Slack, ntfy, Matrix notification target types with clients and dispatch
- MatrixBot model + API + frontend in Bots tab
- Command template system fully wired into all handler commands
- Default command templates seeded (EN/RU, 14 slots each)
- Command template editor with variables reference including child fields
- Delete protection on all 10 entity types (409 with consumer details)
- Provider type selector on template config forms
- Target type selector as dropdown with all 7 types
- Response template selector on command config form
- CLAUDE.md: mandatory server restart rule, child properties rule
- Fix sanitizePreview regex to match literal quotes instead of " entities
- Default telegram chat_action to "typing" in model and frontend
- Change "photo(s)" to "file(s)" in default templates (EN/RU)
- Remove redundant album URL line from assets_added templates
- Auto-refresh system-owned templates from files on server startup
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rework entity schema: rename Tracker→NotificationTracker, add CommandConfig/
CommandTracker/CommandTrackerListener entities for decoupled command handling.
Commands now resolve through CommandTracker→CommandConfig instead of
TelegramBot.commands_config. Smart ref-counted bot polling based on active
listeners. Add chat_action to telegram targets. Full frontend CRUD pages
for command configs and command trackers. Idempotent SQLite migrations.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds telegram bot command system with 13 commands (search, latest, random, etc.),
webhook/polling handlers, rate limiting, app settings page, and various UI/UX
improvements across all entity pages.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Show entity icons on all cards with fallback defaults (providers, trackers, targets, bots)
- Enrich EventLog with provider_name, tracker_name, assets_count; add DB migration
- Dashboard events: filtering (type, provider, search), sorting, pagination, dynamic page size
- Friendly chat names on telegram target cards (resolve from TelegramChat table)
- Test message button on bot chat items with locale-aware messages
- Album public link validation on tracker save with auto-create dialog
- Support albums without public links: conditional <a href> in templates
- Fetch shared links during poll, enrich events with public_url/protected_url
- Per-asset public_url in template context ({share_url}/photos/{asset_id})
- Common date/location detection: common_date + common_location context vars
- Dual date formats: date_format (datetime) + date_only_format (date only)
- Template clone button, HTML link rendering in template preview
- Fix Telegram asset download 401: pass x-api-key headers through client
- Fix provider external_url matching for API key scoping
- Fix event timestamp timezone (append Z suffix for UTC)
- Localize event filter controls, test messages (EN/RU)
- Template variable UI helpers updated with all new fields
- CLAUDE.md: template system sync rules documentation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Backend API (38 routes):
- providers: full CRUD + test connection + list collections + API key masking
- trackers: full CRUD + trigger + history + test-periodic/memory
- tracking-configs: full CRUD with Pydantic models, provider_type filter
- template-configs: full CRUD + preview + preview-raw with two-pass validation
- targets: full CRUD + test notification + config masking
- telegram-bots: full CRUD + chat discovery + token endpoint
- users: full admin CRUD + password reset + self-delete protection
- status: dashboard endpoint with providers/trackers/targets/events counts
Frontend pages updated:
- Dashboard with animated stat cards and event timeline
- Providers with proper components, delete confirm, snackbar
- Trackers/targets/tracking-configs/template-configs/telegram-bots/users
all use PageHeader, Card, Loading, MdiIcon with correct i18n keys
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Wire all components into a working application:
- Scheduler service: APScheduler loads enabled trackers, polls at intervals
- Watcher service: orchestrates poll -> detect -> notify flow
- Eagerly loads DB data, then creates aiohttp session for provider
- Saves tracker state after each poll
- Logs events to EventLog table
- Dispatches notifications to targets with template rendering
- Manual trigger endpoint: POST /api/trackers/{id}/trigger
- Scheduler starts on app lifespan startup
- Full end-to-end flow verified: server starts cleanly
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Set up the Notify Bridge project structure:
- packages/core (notify_bridge_core) with provider, model, notification, template packages
- packages/server (notify_bridge_server) with FastAPI skeleton and health endpoint
- frontend with SvelteKit 2, Svelte 5, Tailwind CSS v4, static adapter
- Root configs: .gitignore, README.md, CLAUDE.md
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>