aioresponses 0.7.8 (latest) does not pass the stream_writer keyword
argument that aiohttp 3.14 made required on ClientResponse, so every
aioresponses-mocked HTTP test fails to construct a response. core
declares aiohttp>=3.9 with no upper bound, so CI floated to 3.14.0 and
the backend job would break on the next run. Pin <3.14 in the test
install in both build.yml and release.yml until aioresponses ships an
aiohttp-3.14-compatible release.
Replace the single comma-separated text box with an add/remove list of
native HH:MM pickers for the Immich scheduled-assets, periodic-summary,
and memory slots. The backend already stored comma-separated *_times and
scheduled one cron job per time; this makes entering several times per
day discoverable and hardens the read/write path.
Backend:
- services/time_list.py: normalize_time_list (validate / dedup / sort /
cap at 24, raising TimeListError) + lenient parse_hhmm_list; the
scheduler now uses the shared parser (drops its private copy).
- tracking_configs API normalizes *_times on every write (422 on bad
input) and rejects enabling a slot whose times list is empty.
- scheduler warns when an enabled slot has zero or dropped fire times,
restoring the observability lost with the old per-call warning.
Frontend:
- TimeListEditor.svelte: add/remove native time rows, dedupe + sort on
emit, per-day cap, collapses on-screen duplicates, aria-labelled rows;
syncs from the value prop only on external changes (untrack guard) so
keyboard entry isn't clobbered mid-edit.
- Descriptor-driven save guard: an enabled feature section must have at
least one time.
- i18n (en/ru) keys; refreshed help text; removed dead invalidTimeList.
Tests: time_list normalization/parsing (incl. non-ASCII/odd shapes) and
the enabled-implies-times validation.
test_fallback_task_retained_until_fire asserts len(_bg_tasks) == 1, but
the set carries pending tasks from earlier tests' fallback schedules, so
the assertion saw the accumulated count instead. Drop the references (no
.cancel() — the tasks belong to closed loops, cross-loop cancel raises
RuntimeError on the next test's setup).
Operability:
- Correlation IDs end-to-end: shared dispatch_id between log lines and
EventLog rows (event/watcher/scheduled/deferred/action/HA/command paths)
and a new X-Request-Id middleware that normalizes inbound ids and binds
request_id into log context.
- dispatch_summary block merged into EventLog.details: per-target
success/failure counts plus Telegram media delivered/skipped/failed and
truncated error lists, so partial outcomes surface in the UI.
- Diagnostic mode: admin can flip one module to DEBUG for a bounded
window with auto-revert (in-memory only; setup_logging() resets on
boot, lifespan reverts on shutdown). New /diagnostic-mode endpoints
plus DiagnosticsCassette UI on the settings page.
Telegram:
- Per-receiver options: disable_notification (silent send) and
message_thread_id (forum-topic routing), wired through the dispatcher
via a ContextVar so all four send sites (sendMessage / sendPhoto-Video-
Document / sendMediaGroup / cache-hit POST) pick them up.
- send_large_videos_as_documents target setting: bypass the 50 MB
sendVideo cap by falling back to sendDocument for oversized videos.
- sendMediaGroup byte-budget enforcement (TELEGRAM_MAX_GROUP_TOTAL_BYTES,
45 MB) with per-item fallback on chunk failure so a stale file_id no
longer silently drops a cached asset.
Tests:
- New: diagnostic_mode, dispatch_summary, request_correlation,
telegram_media_group_partial, telegram_per_send_options.
Docs:
- .claude/reviews/: six-axis production-readiness review of v0.8.1.
- .claude/docs/functional-review-2026-05-28.md: focused review of
Telegram/Immich/logging subsystems.
Apply six isolated, low-risk fixes surfaced by the parallel
production-readiness review (backend, frontend, security, perf,
UI/UX, bugs+features).
Backend
- Mask access_token in provider GET responses and drop it on edit
when carrying the *** placeholder — fixes plaintext leak of HA
long-lived tokens (security H-1). Centralized via
PROVIDER_SECRET_FIELDS so all call sites stay in sync (C-5).
- Hold HA status-change tasks in a module-level set with a
done_callback — asyncio.create_task only keeps weak refs and
the task could be GC'd before its row was written (C-1).
- Roll back the request session in the Telegram-webhook catch-all
so a handler exception cannot leak uncommitted writes into the
next request (C-2).
- Bail before reading the 1 MiB webhook body when the Gitea
provider has no secret configured or the request has no
signature header. For the generic webhook with bearer_token
auth, verify the Authorization header before the body read.
Closes the pre-auth resource-exhaustion amplifier (C-3).
Frontend
- Add supportsAutoOrganize capability to ProviderDescriptor and
consume it from RuleEditor instead of `provider.type !== 'immich'`,
bringing the last action-rule editor under CLAUDE.md rule 8
(no provider-type hardcoding in components).
- Snackbar: add role="region" + per-toast role/aria-live/aria-atomic
so screen readers announce success/error toasts.
- Sidebar nav: add aria-current="page" on the active link so the
active state has an accessible name.
- New snackbar.region key in en + ru (locale parity preserved).
Out of scope for this commit (tracked in .claude/reviews/README.md
ship-blocker list): secret encryption at rest, JWT cookie move,
Alembic adoption, webhook idempotency, deferred-dispatch crash
window, persisted Telegram update watermark, bridge_self counter
lock — each needs more than a mechanical edit.
Both pending suggestions (venv install for monorepos + hatchling
METADATA workaround) were applied directly to the facts repo in
commit cfdafa9. Queue file removed since nothing pending.
The persistent Gitea runner caches the setup-python toolcache between
runs. A previous run that produced wheels with broken metadata (no
Version field in METADATA) left a notify-bridge-server install with
no RECORD file in site-packages. The next run hits:
Found existing installation: notify-bridge-server None
error: uninstall-no-record-file
pip refuses to uninstall (no RECORD) and refuses to overlay (it tries
to uninstall first). Switching from a system-pip install into the
toolcache to an isolated /tmp/venv per run sidesteps the leak — each
CI run starts with empty site-packages.
Same change to build.yml and release.yml so the pre-merge gate and
the release-gate both run the same setup.
Four root causes blocked the CI test gate; all fixed minimally:
1. test_release_provider._allow_private_urls used setenv +
importlib.reload(ssrf_mod). The reload permanently rebound
_ALLOW_PRIVATE=True in the module; monkeypatch.setenv undid the
env var on teardown but the module attribute stayed True for the
rest of the session, masking every test_ssrf*/test_ssrf_hardening
case (16 failures). Switched to monkeypatch.setattr on the module
attribute directly — restored cleanly on teardown.
2. _FakeResponse in test_release_provider lacked the content_length
attribute and a top-level read() method that the new size-cap
guards in gitea.py consult before parsing (5 failures).
3. test_gate_quiet_hours_wins_over_event_type_flag was asserting the
pre-refactor gate order. evaluate_event_gate now intentionally
reports EVENT_TYPE_DISABLED before QUIET_HOURS so deferrable
events with the event-type flag off get dropped immediately
instead of being deferred and then silently discarded at drain
time. Renamed the test and inverted the expectation.
4. resolve_version() returned 0.0.0+unknown in CI because
pip-wheel-built hatchling distributions ended up with METADATA
missing the Version field — importlib.metadata returned None.
Added __version__ = "0.8.1" to notify_bridge_server/__init__.py
as a third (always-available) candidate; resolve_version() now
picks the max of (installed, package, source).
Adds bot commands for the bridge_self provider so operators can inspect
and manage bridge health from chat: /status, /thresholds, /reset, /health.
Includes Jinja2 templates for both locales, seed data, capability slots,
and a handler that exposes pending deferred backlog plus per-counter
reset. Also adds .claude/skills/ for project-scoped graph-aware skills.
Comprehensive multi-area pass driven by a parallel 8-agent production
review. Frontend, backend, database, security, performance, operational,
plus a new self-monitoring feature.
## Critical fixes
- Planka webhook: reads bounded raw body (was NameError on every call)
- HA quiet hours: ha_state_changed/automation_triggered/service_called/
event_fired added to deferrable set (were silently dropped)
- DNS-rebinding SSRF: PinnedResolver wired into shared aiohttp session
- Telegram inbound webhook: secret now mandatory (401 without)
- Generic webhook: auth_mode="none" requires explicit
acknowledge_unauthenticated=true; per-IP rate limit 60/min
- svelte-check: 5 null-narrowing errors in EventDetailModal fixed
- Provider hardcoding: Immich-only block extracted to descriptor
featureDiscoveryHint
- command_sync: snapshot+expunge bot before exiting AsyncSession
## Bug fixes
- notifier asyncio.gather(return_exceptions=True) — one bad chat no longer
cancels peer sends
- NotificationDispatcher hoisted out of per-tracker loop
- Provider credential resolution unified across all 5 dispatch sites
- HA asyncio.shield now drains inner task on cancellation
- Provider construction switched from if/elif ladder to factory registry
- NUT first poll seeds silently (no spurious ups_on_battery)
- Quiet-hours gate: event-type-disabled now wins over deferral
- APScheduler drain job ID resolution upgraded to seconds
- HA on_status_change wired through to EventLog
- Webhook payload rollback failures now logged (not swallowed)
- Batched receivers/chats/bots in load_link_data (was per-target N+1)
- flag_modified on JSON column reassignments in deferred_dispatch
## Database
- UNIQUE indexes on 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
- Composite ix_event_log_user_event_type_created index
- save_chat_from_webhook switched to ON CONFLICT DO UPDATE
- ondelete=CASCADE on user-id FKs (model annotation; app-side cascade
delete added for existing data)
- delete_notification_tracker converted from N+1 to bulk DELETE/UPDATE
- Module-level asyncio.Lock replaced with lazy _get_lock() pattern
- VACUUM INTO snapshot now PRAGMA integrity_check verified
## Performance
- Jinja2 template compilation LRU cached (lru_cache maxsize=512)
- Per-locale render cache in NotificationDispatcher (skips re-rendering
identical content for receivers sharing a locale)
- Tracker list cached per provider_id with 5s TTL + explicit invalidation
on tracker CRUD (relieves HA chat-bus rate query pressure)
- Nav-counts collapsed from 16 round-trips to single UNION ALL
- HA event_log: skip persisting empty assets_added/removed events
## Security hardening
- Mass-assignment guard on Action create/update; cron sub-minute reject
- Backup JSON depth/node-count cap (depth ≤ 10, nodes ≤ 100k)
- _sanitize_config extended to all JSON-typed fields on backup import
- Telegram _safe_get walks redirects manually with SSRF revalidation
- Bcrypt 72-byte password length cap with clear 422
- Webhook payload body redaction; sensitive substring set extended with
oauth/client_secret/webhook_secret/csrf in both header filter and
template extras filter
## Frontend
- 76 catch (err: any) sites converted to errMsg(err) helper
- globalProviderFilter: pure getter; reconciliation moved to one-time
$effect in +layout
- Provider-filter binding: removed paired $effects + _syncingFilter flag,
now one-way derived
- entity-cache: separate _refreshing flag for background re-fetches
- api.ts 401 handling: AuthRedirectError class + dedup _redirecting flag,
goto() instead of window.location.href
- a11y: aria-expanded on mobile More, role=switch + aria-checked on
Telegram bot toggles
## Tests & operations
- CI pytest gate added to .gitea/workflows/build.yml + release.yml
(wheel-built install to dodge editable-install slowness)
- /api/ready upgraded to deep healthcheck (db SELECT 1, scheduler.running,
HA supervisor presence) returning {ready, checks, errors, version}
- /api/metrics endpoint with prometheus_client (deferred_pending,
event_log_total, dispatch_duration, poll_failures, send_failures)
- New OPERATIONS.md covering deploy, healthchecks, metrics, backup/restore
procedures, log handling, common scenarios, upgrade flow
- New tests: test_bridge_self (11), test_gitea_parser (9),
test_planka_parser (6), test_immich_change_detector (6),
test_backup_roundtrip (1)
## New feature: bridge self-monitoring
- New bridge_self provider type — internal sink for bridge health events
- Three event types: bridge_self_poll_failures (consecutive tracker poll
failures), bridge_self_deferred_backlog (pending count crosses
threshold), bridge_self_target_failures (consecutive 5xx/network
failures per target)
- Per-user thresholds (defaults: 3 / 100 / 5) configurable via the
provider config form
- Auto-seeded on user create + /setup + boot backfill for existing users
- Anti-spam: counters reset after emission; backlog uses transition latch
- Self-loop guard: bridge_self failures don't count toward target-failure
thresholds (logged only) — wire to your own Telegram/Email/Matrix to
get notified when polls/dispatches/sends fail
- 6 default templates (3 events × 2 locales), tracking config columns
with backfill migration, frontend descriptor (excluded from "create
provider" wizard since auto-managed)
Operator-visible behavior changes (call out in release notes):
- NOTIFY_BRIDGE_TELEGRAM_WEBHOOK_SECRET now REQUIRED for webhook mode
- Existing webhook providers with auth_mode="none" need explicit opt-in
- Generic webhook endpoint rate-limited 60/min per source IP
- HA disconnect/reconnect writes ha_status_* EventLog rows
- Every user gets a bridge_self provider — wire it to a target to
receive failure alerts
Pre-existing test failures (test_ssrf, test_release_provider) on
Python 3.13 are unrelated; CI runs on 3.12.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds Home Assistant as a service provider with two coordinated surfaces:
Notifications (subscription):
- Long-lived WebSocket client (aiohttp ws_connect) with auth handshake,
exponential-backoff reconnect, bounded event queue, and area-registry
enrichment cached per (re)connect
- ServiceProvider ABC gains an optional `subscribe()` method for push-style
providers; HomeAssistantServiceProvider uses it via a per-provider
supervisor task started in the FastAPI lifespan
- 4 event types (state_changed, automation_triggered, call_service,
event_fired), 4 default Jinja templates (en + ru), HA-specific
tracker filters (entity_glob, domain_allowlist, exact entity ids)
- Extracted shared dispatch pipeline (api/webhooks.py → services/
event_dispatch.py) so subscription and webhook ingest share the same
event_log + deferred-dispatch + quiet-hours code path
Bot commands:
- /status, /entities [glob], /state <entity_id>, /areas
- Multi-command WS session so /status and /areas cost one handshake
- Sensitive-attribute blocklist (camera access_token, entity_picture, etc.)
and 30-attribute cap to keep /state output safe and within Telegram's
message size
- Error-message redaction strips URL userinfo before surfacing to chat
Frontend:
- HA descriptor with toggle ConfigField type (new) and tag-input filter
mode for free-text glob/domain lists (new TagInput component)
- 15 command slots + 4 notification slots wired into the existing
template-config UI
The APScheduler cron job fires daily at every entry in `periodic_times`,
and nothing in the dispatch path consulted `periodic_interval_days` or
`periodic_start_date` — so a configured 3-day interval still produced a
daily summary.
Gate the dispatch in `dispatch_scheduled_for_tracker` for kind=periodic
via `(today_in_app_tz - start_date).days % interval == 0`. Log a skip
with reason `interval_not_due` on non-firing days so operators can tell
suppressed-by-interval apart from other skip causes.
Adds an icon selector to the "On watch" provider deck letting users
choose between page-scoped stats (legacy) and full-corpus stats that
aggregate across every event matching the current filters. Backend
returns a new provider_event_counts map alongside the paginated events.
- Defer quiet-hours dispatches into new deferred_dispatch table; drain
job + periodic catch-up scan re-fire at window end with coalescing on
(link, event_type, collection_id).
- Add ON DELETE SET NULL migration on event_log_id and partial unique
index on (link_id, collection_id, event_type) WHERE status='pending'.
- Add release-check provider abstraction (Gitea/GitHub) with SSRF-safe
URL validation, settings UI cassette, and scheduled polling.
- Replace importlib-only version lookup with version.py helper that
prefers the higher of installed metadata vs source pyproject so stale
editable dev installs stop misreporting.
- Aurora frontend polish: MetaStrip component, ReleaseCassette,
EventDetailModal expansion, and i18n additions.
Fix CORS default (was incorrectly listed as `*`, which is rejected on
startup) and document the env vars exposed by config.py and
docker-compose.yml — proxy/SSRF, auth, logging, retention, and
integration settings. Sync the Docker Compose example with the
hardened compose file at the repo root.
Splits the monolithic settings page into 6 focused glass components matching
the polish of the recently redesigned settings/backup page.
- SettingsHero: PageHeader with 4 live status pills (URL host, timezone +
ticking clock, locale codes, log severity tinted by level)
- IdentityCassette: groups External URL + Timezone + Locales as numbered
rows; URL field gains copy + open chips and a mint border when valid
- TelegramCassette: webhook secret with show/hide toggle and verified
status chip; cache TTL/max as oversized mono numerals with humanized
previews ("720 hrs -> 30d")
- CacheLedger: mirrors BackupLedger -- big total, gradient capacity meter,
tone-edged URL/Asset bucket rows colored by oldest entry age
- LoggingCassette: per-module overrides become tone-edged chips with
severity-colored level borders; raw-text fallback behind toggle; live
ACTIVE preview line
- SaveBar: sticky dirty-aware footer with citrus pulse, italic message,
and Discard/Save (only renders when settings differ from baseline)
No backend changes -- same /settings and /settings/telegram-cache/* endpoints.
Targets page: collapse targets under a per-bot header (BotGroupHeader)
with a count chip and an "Open bot" cross-link. Receivers are hidden
by default and expand per group; non-bot types fall back to a "Direct
delivery" group. Telegram "Add receiver" now opens the EntitySelect
chat palette directly instead of an inline form — EntitySelect grew a
bindable `open` flag, `showTrigger`, and an `onclose` cancel signal.
Backup settings page: split the monolithic +page into focused panels
(BackupHero, BackupLedger, ExportPanel, ImportPanel, PendingStrip,
ScheduleCassette) and introduce a stepwise export/import flow with
category groups, secrets handling, conflict policy, and validation
gating. New i18n keys in both locales cover the bot grouping labels
and the backup step copy.
The generic-webhook provider has no upstream API, so /status reports
DB-derived stats: active/total trackers, provider name, and last event
timestamp (formatted via the shared get_last_event_str helper).
Includes pytest coverage for handler registration, populated stats with
a recent event, the empty-state dash sentinel, and unknown-command
fall-through. Template variable docs in command_template_configs.py
extended with the new trackers_active/trackers_total keys.
Pagination/filter reloads were collapsing the panel into a "Loading
events…" placeholder and then replaying the stagger entry animation,
which read as the whole section being reconstructed. Keep the existing
rows + paginator mounted during reload (with a soft dim) and only run
the aurora-rise cascade on the very first non-empty render.
The legacy ``@fontsource/geist-sans`` (v5.2.5) ships latin only and
``@fontsource/geist-mono`` was imported with the default subset only —
so Russian text in the sidebar (nav links, section labels, badges) fell
back to system fonts (Segoe UI / Cascadia / Consolas) and visibly
clashed with the Latin glyphs around it.
- Switched the sans family to ``@fontsource-variable/geist`` (single
variable woff2 with latin + latin-ext + cyrillic). Updated
``--font-sans`` to lead with ``'Geist Variable'`` then keep the old
``'Geist Sans'`` and system fallbacks for safety.
- Added ``@fontsource/geist-mono/cyrillic-{400,500,600}.css`` imports
so Geist Mono renders Russian glyphs in the section labels and
monospace badges instead of falling back.
Newsreader (display serif) still has no Cyrillic on fontsource, so
italic page-hero emphasis in Russian still falls back to Georgia.
That's a separate, less-prominent concern and a future swap.
- Auto-refresh ticker is now silent: skips ``eventsLoading`` so the
loading placeholder no longer flashes, uses ``(event.id)`` key on
the events ``{#each}`` so unchanged rows reuse their DOM nodes, and
short-circuits the array reassignment when the visible page is
identical to what we already rendered. No-op refreshes leave the
list completely untouched.
- ``PageHeader`` crumbs (Routing · Notification, Operators · Bots, …)
were hard-coded literals. Moved to a new ``crumbs`` i18n namespace
with 9 keys; updated all 15 call sites to ``t('crumbs.*')`` so they
switch with the language.
- Tracker form's Immich feature-discovery banner now exposes both
``Open Tracking Config`` and ``Open Template Config``. Added the
``?edit=<id>`` auto-open hook to ``/template-configs`` (mirrors the
existing one on ``/tracking-configs``) so the new link lands users
directly on the editor.
Bot commands were the only user-initiated path that didn't surface in
the dashboard. They now produce ``command_handled`` /
``command_rate_limited`` / ``command_failed`` rows in ``EventLog``
alongside tracker and action events.
Backend
- ``EventLog`` gains nullable ``command_tracker_id`` / ``telegram_bot_id``
FKs plus deletion-snapshot name columns (idempotent migration).
- New ``_log_command_event`` helper emits one row per invocation at the
three branches in ``handle_command``. Logging failures are swallowed
so they cannot block the user-visible reply.
- Telegram ``from`` is captured in poller and webhook, whitelisted to
identity fields by ``_normalize_issuer`` (drops ``language_code`` and
any future PII), persisted under ``details.issuer``.
- ``/api/status`` resolves live ``CommandTracker`` / ``TelegramBot``
names (mirroring the action pattern) and exposes ``tracker_id``,
``command_tracker_id``, ``telegram_bot_id`` so the frontend can
deep-link.
Frontend
- Event rows are now clickable and open a detail modal with full
provenance (bot → chat → issuer → provider), raw ``details`` JSON,
and per-entity action buttons.
- Buttons use the existing ``requestHighlight`` + ``goto`` crosslink
pattern, so clicking lands on the entity's list page with that
specific card scrolled into view and pulsing.
- Auto-refresh dropdown (Off / 10s / 30s / 1m / 5m) persisted in
``localStorage``; ticker pauses while the tab is hidden.
- Event-type filter, dashboard verb labels, and gradients extended for
the three new ``command_*`` types.
- Filled in pre-existing missing i18n keys (``common.hide`` /
``common.show`` / ``commandConfig.noCommandsForProvider``).
Tests
- New ``test_command_event_logging.py`` covers subject formatting,
issuer normalization, the three event branches, and graceful failure
when the DB is unreachable. ``pytest packages/server/tests/`` → 96/96.
Notifications:
- Add shared http_base, redact, and SSRF hardening modules
- Refactor dispatcher, queue, receiver and per-provider clients
(telegram, discord, email, matrix, ntfy, slack, webhook) to use
the shared base, with bounded queue and redacted error logs
- Tests for ssrf, redact, http_base, queue bounds, dispatcher
aggregation, telegram media partition, email and matrix clients
Frontend:
- Settings: log level / log format selectors now use IconGridSelect
with per-option icons and i18n descriptions
- Minor providers page and entity-cache store updates
Tooling:
- Document code-review-graph MCP usage in CLAUDE.md
- Ignore .code-review-graph/, register .mcp.json
Mirror the providers form pattern (defaultName tied to type) across
bots, targets, trackers, actions, and configs. Each form now derives
form.name from the selected type or provider while the user hasn't
manually edited it; switching to edit-mode flips the manualEdited
flag so existing names are preserved.
Defaults: bots → "<Type> Bot"; targets → type label; notification
trackers → "<provider> Tracker"; command trackers → "<provider>
Commands"; actions → "<provider> <Action Type>"; tracking/template/
command/command-template configs → "<descriptor.defaultName>
<Suffix>". TargetForm and TrackerForm grew an optional onnameinput
prop so parents can flag manual edits in subform inputs.
Mirrors the notification-template page's group layout. Command slots
now split by name prefix into Command Responses, Error Messages
(rate_limited/no_results), Command Descriptions (desc_*), and Usage
Examples (usage_*). Language picker, reset-all, and slot filter are
hoisted above the groups so they apply across all fieldsets, and
empty groups are hidden so providers without usage_* don't render
empty headers.
Drops the orphan cmdTemplateConfig.commandResponsesHint i18n key —
hints.commandResponses replaces it.
- Add overscroll-behavior: contain to all in-modal/popup scroll
containers (Modal body, EntitySelect, MultiEntitySelect, IconPicker,
IconGridSelect, SearchPalette, TimezoneSelector) so reaching the
inner scroll boundary no longer scrolls the page underneath.
- Telegram bot Discover Chats no longer collapses the existing chat
list into a "Loading…" placeholder. Split chatsLoading (initial)
from chatsRefreshing (Discover); rows are keyed by chat.id with
flip+fade animations; the list dims with a sweeping shimmer bar
while the Discover button shows a spinning icon and "Discovering
chats…" label. Honors prefers-reduced-motion.
chat_action was stored in two places — the model column and config JSON —
and dispatch_helpers unconditionally overrode the config value with the
column. The frontend only ever wrote the JSON path, so the UI choice
silently had no effect on outgoing chat actions.
Make the column the single source of truth: frontend sends chat_action
top-level, dispatch_helpers reads from the column, and a one-time
backfill migrates existing config values to the column and strips the
legacy key.
Also fix a long-standing race where the keepalive's bare sleep(4) +
finally cancel could fire one last sendChatAction after the response
already arrived, leaving a phantom indicator for ~5s. Replace with a
stop event + wait_for so callers can signal stop cleanly via the new
stop_keepalive helper.
- vite.config.ts: read package.json and expose its version as a
build-time global via Vite's `define`.
- app.d.ts: add ambient declaration so the layout's brand version
badge (`v{__APP_VERSION__}`) type-checks.
- app.html: inline blocking script resolves the theme from localStorage
(or prefers-color-scheme) and sets data-theme on <html> before first
paint, eliminating the dark→light transition users saw when the light
theme was selected.
- +layout.svelte: hydrate sidebar collapsed state and expanded nav groups
synchronously in their $state initializers instead of inside onMount,
so the sidebar no longer snaps from expanded→collapsed and groups no
longer slide open after mount.
- +layout.svelte: keep the global provider-filter row rendered while
providersCache.fetchedAt === 0, so the row doesn't pop in mid-paint
and push the nav down once the cache resolves.
- Gitea: NotificationTracker now exposes sender allowlist / blocklist filters
via MultiEntitySelect, populated from Gitea /users/search merged with past
EventLog senders so the picker is useful before the first webhook arrives.
- Webhook providers (gitea, planka, webhook): stop scheduling interval polling
jobs on tracker create/update/startup; hide the "every Xs" indicator in the
tracker list since there is no polling.
- Dashboard: stat cards are now <a> links that route to providers, trackers,
targets, command-trackers, or scroll to the events panel. Provider deck
rows highlight the target provider on click.
- Command trackers / command configs: auto-reselect the right config when the
provider type changes (matches notification-tracker behavior).
- Migration: drop legacy batch_duration column from notification_tracker —
the field is gone from the model but its NOT NULL constraint blocked
inserts on older DBs.
- Docs: refresh entity-relationships.md with current NotificationTracker
fields (filters, adaptive_max_skip, default_*_config_id).
- Template editors (notification & command) now use EntitySelect for
locale switching and default to the configured primary locale
instead of always 'en' when opening, editing, or cloning a config.
- LocaleSelector's add-flow uses EntitySelect for catalog pick;
custom BCP-47 codes (e.g. de-CH) keep a small dedicated input.
- TimezoneSelector dropdown was being clipped by Card's overflow:hidden
and backdrop-filter; portalled to <body> with an overlay backdrop and
styled as a centered modal palette (same pattern as EntitySelect).
- Removed top padding on the timezone scroll list so sticky region
group headers no longer leak rows above them.
- Extracted shared locale catalog to lib/locales.ts.
Wire column was content-width (min 100px) so the line vanished between
two wide endpoint blocks. Bumped to minmax(220px, 1.6fr) so the pipe
takes ~60% more space than either side, thickened the line 2→3px,
faded both ends via color-mix transparency stops, added a soft
primary-glow halo plus a 1px specular sheen, and beefed up the count
badge with a rule-strong border / inset highlight / drop shadow so it
reads as a node on the wire. Stacks to a single column below 880px.
Two related Telegram changes:
1. Per-chat command localization. setMyCommands now accepts a scope
(BotCommandScopeChat) and deleteMyCommands clears scoped bindings.
Command registration runs three tiers: default → per-language
(Telegram client language) → per-chat (UI override). Saving a
chat's language_override or commands_enabled toggle pushes the
binding to Telegram inline rather than waiting on the 30s
debounced bot-wide sync.
2. Unified Telegram locale resolution. Three test paths (bot test_chat,
target receiver test, target-level fan-out) used to disagree on
locale priority — the target receiver test in particular only
consulted receiver.locale and ignored the chat's language_override.
Introduced pick_telegram_locale (pure) and
resolve_telegram_chat_locale (async DB lookup) in services/notifier
so all three paths share one priority order:
receiver.locale → chat.language_override → chat.language_code → fallback
Fan-out keeps batch-loading TelegramChat rows for efficiency, just
runs them through the same priority function now.
Comprehensive pre-production sweep across the Aurora redesign — drives
svelte-check to 0 errors / 0 warnings (was 61) without changing visual
intent. Highlights:
- Mobile: hero title shrinks at 480px, signal-list stacks timestamp
under sentence below 640px, sidebar icon buttons bumped to 40x40
- Light theme: muted-foreground darkened to #3a3560 to clear WCAG AA
on glass surfaces and the modal close button
- Perf: topbar backdrop-filter 28→14px, mobile-more sheet 28→12px to
cut concurrent blur layers on mid-tier mobile
- a11y: prefers-reduced-motion mute for aurora drift / pulses /
shimmer / stagger; aria-label on every icon-only button;
aria-describedby on Hint; combobox/listbox/aria-activedescendant on
SearchPalette; modal dialog tabindex; 47 label-without-control
warnings across 14 form pages cleaned up via for=/id= or label→div
- Dashboard derived state split into topology- vs status-bound layers
so polling no longer re-runs the full provider/wires computation
- Mobile bottom nav derived from baseNavEntries by key lookup so
adding a top-level nav entry keeps the two trees in sync
- Bug: template-configs page now respects the global provider filter
for both the count meter and the type pill (was reading the
unfiltered cache)
- Misc: portal EventChart tooltip and switch its swatches to Aurora
tokens; CollapsibleSlot warning state uses warning-fg/-bg tokens
instead of #d97706; Hint z-index 99999→9999; element refs across
Modal/EntitySelect/MultiEntitySelect/SearchPalette/IconGridSelect/
Hint/targets converted to \$state for reactivity; 4 dead
.topbar-cta selectors removed