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
Generalize dashboard panel expansion: Stream / On Watch / Pulse / Wires
each get a chevron toggle persisted in localStorage under
dashboard_section_state (migrates legacy dashboard_chart_visible). Drop
the redundant inner border on EventChart so it doesn't double-frame the
panel. Mobile "more" sheet becomes full-height with translucent glass
(rgba(...,0.72) + 28px blur) instead of nearly-solid.
Portal EntitySelect/MultiEntitySelect/Modal/Snackbar/EventChart/Hint to
<body> so they escape backdrop-filter ancestors. Replace translucent
glass on popups (IconPicker, IconGridSelect, SearchPalette, Snackbar)
with solid backgrounds and theme-aware light-mode override.
The hero's right column now spreads vertically — count + label pinned
at the top, action button(s) flushed to the bottom-right corner of
the card via margin-top: auto. Row uses align-items: stretch so the
side column reaches the full hero height regardless of how tall the
left column gets (long descriptions, many pills, etc.).
Single CSS change in the shared component, so it applies to every
page that uses PageHeader (all 14 of them) — no per-page edits.
Big batch — every secondary page now wears the same glass-card hero
that landed on Providers earlier:
- notification-trackers, tracking-configs, template-configs
- command-trackers, command-configs, command-template-configs
- targets (with active-tab title), actions
- bots (telegram / email / matrix tabs)
- settings, settings/backup, users
Each page picks an italic-em emphasis word, an editorial crumb (e.g.
'Routing · Notification', 'Operators · Bots', 'System · Maintenance'),
a count meter, and entity-specific status pills derived from live
data: 'X armed / Y paused' for trackers and actions, 'X types' for
configs/templates, 'X channels' or '$N receivers' for targets.
Other changes in this commit:
- Button.svelte: redesigned. Primary variant becomes a real Aurora
CTA — gradient lavender → orchid pill, 40px tall md / 34px sm,
inset highlight, lift + glow on hover. Secondary, danger, ghost
variants reworked to match. The 'Add <Type>' button on every page
now reads as the page's primary action instead of a flat lavender
rectangle.
- JinjaEditor: overrode oneDark's hardcoded background with
!important so the editor surface picks up var(--color-input-bg).
Gutters / scroller / selection / autocomplete tooltip all match
Aurora glass tokens now. Template editors stop visually clashing
with the surrounding panel.
- Aurora pulse dot: rewritten as a self-contained box-shadow glow
pulse (no transform, no pseudo-element). The dot's bounding box is
now stable so ancestors with overflow:hidden can never clip the
visible dot — only the (decorative) outer glow halo. Fixes the
'half-moon clipping' on the dashboard 'On watch' deck.
- topbar-action.svelte.ts left in tree but unused (topbar CTA was
reverted per your call). Will clean up in a later commit.
- Form input baseline styling moved into app.css (rounded 0.625rem,
glass background, hover/focus rings) so untouched filter inputs
on the per-type pages stop looking out of place.
i18n: emphasis / countLabel / armed / paused / receiver / receivers
/ channelsCount keys added across en + ru.
Build clean: 0 errors, 61 pre-existing a11y warnings unchanged.
Three threads bundled:
- PageHeader.svelte upgraded to a glass-card subpage hero matching
the dashboard hero language, scaled down. New optional props (all
backward-compatible — old callers keep working): emphasis (italic
gradient word appended to title), crumb (uppercase mono kicker),
count + countLabel (right-side mono meter), pills (status chips
with tones: mint / sky / orchid / coral / citrus / primary).
- Providers page wired up first as the test surface: pulls live
online/offline/checking counts from the existing health probe and
shows a type-count pill. Locale keys added (en + ru) for the new
copy ('Service / providers' wordmark, longer description).
- IconPicker dropdown was suffering the same backdrop-filter
containing-block bug as IconGridSelect — repositioned popups
rendered inside any glass form panel got clipped or floated to
the bottom of the page. Now portals to <body> via the shared
$lib/portal action and uses Aurora glass styling end-to-end
(solid surface, gradient-active cell, glass-strong search input).
- Layout gaps tightened to match the mockup:
* sidebar→content horizontal gap is now 18px flat (was 50px:
the 18px shell-gap PLUS another 32px wrapper padding on each
child of main). Dropped px-4/md:px-8 from the topbar wrapper
and the per-page content wrapper — main's children sit flush
at the column boundary.
* topbar→content vertical gap reduced to 12px (was 16px / pt-4).
Build clean, 0 errors.
Topbar wrapper used md:px-0 (edge-to-edge of main column) while the
page content wrapper used md:px-8 (32px side padding). Result was a
32px overshoot on each side of the topbar pill versus the hero, stat
cards, and panels below it.
Match the wrappers — topbar now uses md:px-8 too. Left/right edges of
the search bar pill line up with the hero/stat panels.
Round-up of feedback:
- Brand: drop italic-gradient 'Bridge' wordmark + 'SERVICE
NOTIFICATIONS' tag. Now plain 'Notify Bridge' bold sans + 'v0.5.2'
mono — exactly what the Aurora mockup shows.
- Event rows: replaced [collection_name] [event_tag] [count_tag] with
a sentence form — bold provider, muted verb, bold count, gradient
italic-feeling collection. Matches the mockup's 'Immich added 14
assets to «...»' pattern. Tracker → provider trail kept underneath.
- SearchPalette: re-skinned to Aurora glass — solid surface (was
using the now-translucent --color-card token and was nearly
invisible), backdrop blur, hairline section headers with
letterspaced mono labels, gradient active row, glass-bordered
result icons + detail pills.
- 'On watch' provider deck hidden when a global provider filter is
active (deck would only show that one provider — redundant). Two-col
grid collapses to single full-width column in that mode.
- Layout: dropped the max-w-7xl cap on the topbar and the page
container. Pages now stretch to full available width on wide
displays.
- Section labels in sidebar: dropped :global() wrapper since the
element is in the same component. Added font-mono for tighter
letterspacing match to the mockup.
Build clean.
Three issues addressed:
1. IconGridSelect popup was clipped/mispositioned because the .panel
ancestor has backdrop-filter, which makes it the containing block
for position:fixed descendants (CSS spec gotcha). Added a tiny
portal action ($lib/portal) that re-parents the popup + backdrop
to document.body, and refreshed the popup styling to be Aurora-
native (solid surface, gradient active state, glass-strong search).
2. Always-visible top search bar: a sticky glass strip at the top of
every page with the search trigger (⌘K), theme cycler, locale
toggle, and a primary 'New tracker' gradient CTA — moved out of
the sidebar to free up rail space and make search reachable from
anywhere. The sidebar's inline search button is gone.
3. Navbar snapped to the mockup:
- Sidebar footer redone as a glass user-card (avatar gradient +
username + role + mint live chip + change-password / docs / logout
row beneath a hairline).
- Section labels above each nav group (Overview / Routing /
Operators / System) with a hairline rule that extends to the
edge — same rhythm as the dashboard mockup.
- Active nav link keeps the gradient bar + glass-elev background
+ inset highlight + soft outer glow.
i18n: nav.section* keys (en + ru), dashboard.newTracker reused for
the topbar CTA. Build clean.
Bringing the Aurora dashboard mockup to feature parity in the real app:
- NavIcon component: thin-stroke SVG icon set covering ~30 nav-tier
glyphs (dashboard, server, target, radar, bots, console, email,
matrix, webhook, slack, bullhorn, settings, theme, search, logout,
chevrons, plus/arrow). Falls back to MdiIcon for unknown names so
every existing usage keeps working.
- layout.svelte: sidebar nav swapped from MdiIcon to NavIcon — dropping
the dense filled glyphs in favour of soft 1.6px outlines that match
the mockup. Main container max-width bumped from 5xl (1024px) to
7xl (1280px) so the dashboard finally gets to breathe.
- +page.svelte (dashboard): full rewrite of the markup to project
every mockup section onto live data:
* Editorial Hero — 'Tonight, everything is flowing.' with gradient
italic emphasis, live/attention pill driven by failure count,
and a big mono throughput meter (24h events, armed/total,
providers, targets) on the right.
* Stats — kept the 4-card grid, refined accent palette per card.
* Two-column row — Signal stream (events with full routing trail:
tracker → provider, gradient avatar tile per event-type) on the
left, On-watch provider deck (per-provider activity bar fill +
live/idle pulse, derived from recent_events) on the right.
* Pulse panel wrapping the existing EventChart with the new title
treatment.
* Active wires — Sankey-style 'Tracker → Target' route summary
derived from notificationTrackers.tracker_targets + targetsCache
with event counts per route.
* Compose band — gradient call-to-action strip with new-tracker CTA.
- i18n: en.json + ru.json get the full set of new dashboard keys
(hero copy, panel titles, compose band, wires labels).
Build: 0 errors, 57 pre-existing warnings unchanged.
Foundation pass on the Aurora redesign:
- app.css: full token swap to lavender/orchid/mint/sky pastel palette,
vivid aurora gradient backdrop, frosted glass surface tokens, two
themes (Aurora dark + Pearl light), shadow recipe for floating glass.
- fonts: add Geist Sans / Geist Mono / Newsreader, drop DM Sans usage
(legacy fontsource entries kept in package.json until full migration).
- layout.svelte: sidebar becomes a floating glass rail with a conic-
gradient brand orb, Newsreader italic 'Bridge' wordmark, soft glass
hovers on nav links, gradient active indicator, gradient user avatar.
- Card.svelte: glass surface + inner highlight + soft hover lift.
- PageHeader.svelte: Newsreader serif display title.
- +page.svelte (dashboard): stat cards become glass with colored
accent 'orb', event timeline gets soft glass rows + slide-on-hover.
Build clean (0 errors, pre-existing a11y warnings unchanged).
Other pages still inherit old chrome via shared tokens but will need
component-specific passes; tracked separately.
Three full-fidelity dashboard mockups (Bridge/Console, Aurora/Glass,
Bento/Modular) plus a chooser index and a tracker-detail page in
the chosen Aurora language. Self-contained HTML, no build needed.
Display filters (Immich tracking config):
- favorites_only drops events with no favorited new assets, or filters
added_assets to favorites only
- assets_order_by/assets_order sort the rendered list
(date / name / rating / random / none)
- max_assets_to_show caps rendered+attached media (default 5 -> 10)
- include_tags strips people from event extras and tags from each asset
- include_asset_details strips city/country/state/lat/lon/is_favorite/
rating/description; load-bearing fields (thumbhash, file_size,
playback_size, cache keys) preserved
- New apply_tracking_display_filters helper in dispatch_helpers; wired
into watcher, webhooks, scheduled/periodic/memory, and manual
test-dispatch
- Targets sharing a TrackingConfig dispatch together; targets with
different TCs each see their own shaped event
Adaptive polling:
- Replace NotificationTracker.batch_duration with adaptive_max_skip
- Per-tracker opt-in: NULL/0 disables back-off (every tick runs);
positive N caps the skip factor at (N-1)-in-N after long idle
- Scheduler caches the cap in module state for the tick fast-path
- Migration adds the new column; API schemas/responses, frontend types,
i18n, and the tracker form updated to match
Dispatch: honor {kind}_collection_mode on TrackingConfig — "per_collection"
fans out one event per album; "combined" pools assets as before. Extract
build_immich_dispatch_events shared by cron and test paths.
Assets: collect_scheduled_assets attaches album_name/album_url/album_public_url
to each asset so combined-mode templates can attribute rows to their source
album. Default scheduled_assets templates render a multi-album header with
inline album list and per-row album link; memory_mode follows the same pattern.
UI: "Reset to default" buttons on notification and command template slots
(per-slot and whole-template), backed by new GET /*-template-configs/defaults
endpoints. tracking-configs "Preview template" now opens an inline preview
modal with locale tabs instead of navigating away; Edit button deep-links
with ?edit_slot=<name> so the destination auto-opens the config and scrolls
to the slot. Reset confirmations use ConfirmModal instead of window.confirm.
Fixes:
* NotificationDispatcher._session_ctx infinite recursion when no shared
aiohttp.ClientSession was passed — broke test dispatch for periodic/
scheduled/memory (cron path was unaffected).
* telegram-bots /chats/{id}/test now resolves chat.language_override /
language_code instead of using the raw ?locale query param, matching
the resolution the tracker-target test endpoint already used.
* scheduled_assets default template no longer emits a blank line between
header and the first asset when the multi-album branch is taken.
Introduce a third update_mode option alongside polling/webhook. 'none'
disables both polling and webhook delivery — useful when another instance
owns the listener or when the bot is send-only. Switching into 'none' now
unschedules polling and unregisters any active webhook so Telegram stops
delivering updates.
New bots default to 'none' (safer when multiple bridges share a token).
Existing bots upgraded from a pre-update_mode schema keep 'polling' so
their behavior is unchanged.
The scheduled_enabled / scheduled_times (and the periodic / memory
counterparts) on TrackingConfig had been wired into the model, the
API, and the test-dispatch path — but no production scheduler ever
read them, so users saw the slot in the UI and only ever got fires
through "Test". This adds the missing cron jobs and the dispatch
fan-out, both keyed off the app-level IANA timezone.
* services/scheduled_dispatch.py — production fan-out reusing the
test-path event builders, picking the slot template per kind, and
writing an EventLog row per fire so the dashboard reflects it.
* services/scheduler.py — _load_immich_dispatch_jobs builds one
CronTrigger per (tracker, kind, HH:MM) from the tracker's default
TrackingConfig; reschedule_immich_dispatch_jobs rebuilds them all
on any relevant CRUD or timezone change.
* tracker / link / tracking-config CRUD endpoints now invalidate.
Also: skip dispatch when scheduled/memory yield zero matching assets
(prevents header-only "On this day:" spam), and update the EN/RU
default scheduled_assets templates to surface that the delivery is
a scheduled random selection.
The static-adapter build emits an inline <script> with the hydration
payload; ``script-src 'self'`` alone blocks the SPA from starting
(browser error: "Executing inline script violates the following Content
Security Policy directive").
Mirrors the 'unsafe-inline' already present for style-src. Primary XSS
protection still comes from Svelte's auto-escaping and
frontend/src/lib/sanitize.ts for the {@html} paths that render user
content. CSP still blocks remote scripts (no https: in script-src),
framing (frame-ancestors 'none'), base-uri hijacking, and form
exfiltration.
Stage 3's `pip install /tmp/wheels/*.whl` re-resolved and re-downloaded
~50 transitive deps on every build, because the wheel filenames (and
thus the layer hash) changed on every version bump.
Two changes:
1. Emit /wheels/deps.txt in the build stage — external deps only,
notify-bridge-* siblings filtered out. The runtime stage installs
from this file first, so the cache key is the pyproject.toml deps
(stable across releases) instead of the local wheel filenames
(changes every release). Registry buildcache now serves the whole
install layer on version-only bumps.
2. Swap pip for uv (ghcr.io/astral-sh/uv:0.11.7). uv resolves and
downloads ~10x faster than pip; combined with a BuildKit cache
mount on /root/.cache/uv and UV_COMPILE_BYTECODE=1, a cold install
drops from ~60-90s to a few seconds.
Local wheels are now installed with --no-deps since externals are
already satisfied.