Telegram targets now require a registered bot (no manual token
fallback). Shows link to /telegram-bots page when no bots exist.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New entity + API:
- TelegramBot model (name, token, bot_username, bot_id)
- CRUD at /api/telegram-bots with token validation via getMe
- GET /{id}/chats: discover active chats via getUpdates
- GET /{id}/token: retrieve full token (for target config)
Frontend:
- /telegram-bots page: register bots, view discovered chats
per bot (expandable), refresh on demand
- Targets page: select from registered bots (dropdown) instead
of raw token input. Chat selector shows discovered chats
when bot is selected, falls back to manual input.
Bot token resolved server-side on save.
Nav icon uses monochrome symbol for consistency.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The locale defaulted to 'en' until onMount ran initLocale(), causing
all server-side and initial client renders to show English even when
Russian was saved in localStorage.
Fix: read localStorage synchronously at module import time so t()
returns the correct locale from the first render.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Modal: use inline styles instead of Tailwind classes for position:fixed
overlay (Tailwind v4 classes weren't applying correctly inside flex)
- Move password modal outside the flex container to top level
- Replace logout text button with SVG logout icon (16x16, Lucide style)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- New Modal.svelte component: overlay with backdrop click to close,
title bar, reusable via children snippet
- Layout: password change moved from inline sidebar form to modal
dialog. Clean UX with current + new password fields.
- Users page: 🔑 button per user opens modal for admin to set a
new password (no current password required for admin reset)
- Backend: PUT /api/users/{id}/password (admin only, min 6 chars)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
5 features implemented:
1. Server health indicator: green/red/yellow dot on each server card.
Pings Immich in background on page load. New GET /api/servers/{id}/ping.
2. Album selector filter: search input above album list in tracker form.
Filters by name as you type (case-insensitive). Shows total count.
3. Album last update time: each album in the selector shows its
updatedAt date. Backend now returns updatedAt from Immich API.
4. Full Jinja2 template engine in notifier:
- build_full_context() assembles all ~40 variables from blueprint
- Common date/location detection across assets
- Per-asset date/location when they differ
- Favorite indicator, people formatting, asset list with truncation
- Video warning for Telegram
- All template slots from TemplateConfig used contextually
5. Password change: PUT /api/auth/password endpoint (validates current
password, min 6 chars). UI in sidebar footer with inline form.
Also: Phase 9 plan (Telegram bot management) added.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
5 tasks:
1. Server health indicator on cards
2. Album selector filter-by-name
3. Album last update time display
4. Review/improve template engine (Jinja2 with full variable context)
5. Change user password (self + admin reset)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Core library:
- New combine_album_assets() in asset_utils.py: smart redistribution
of unused quota when albums return fewer assets than their share.
Two-pass algorithm: even split then redistribute remainder.
- 6 new tests (56 total passing).
Frontend:
- Fix "leave empty to keep current" not localized in server edit.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New pages:
- /tracking-configs: Full CRUD with event tracking, asset display,
periodic summary, scheduled assets, and memory mode sections.
Collapsible sub-sections that show/hide based on enabled state.
- /template-configs: Full CRUD with all 21 template slots organized
into 5 fieldsets (event messages, asset formatting, date/location,
scheduled messages, telegram). Preview support per slot.
Updated pages:
- Targets: added tracking_config_id + template_config_id selectors
(dropdowns populated from configs). Configs are reusable.
- Trackers: simplified to album selection + scan interval + targets.
Added Test, Test Periodic, Test Memory buttons per tracker.
- Nav: replaced Templates with Tracking + Templates config links
Other fixes:
- Language button: now triggers window.location.reload() to force
all child pages to re-evaluate t() calls
- Dark theme buttons: changed primary color to dark gray in dark mode
- Removed old /templates page (replaced by /template-configs)
- Added .gitignore for __pycache__ in server package
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Major model restructuring for clean separation of concerns:
New entities:
- TrackingConfig: What to react to (event types, asset filters,
periodic/scheduled/memory mode config) - reusable across targets
- TemplateConfig: All ~15 template slots from blueprint (event
messages, asset formatting, date/location, scheduled messages)
with full defaults - separate entities per locale
Changed entities:
- AlbumTracker: Simplified to album selection + polling + target_ids
(removed event_types, template_id, all filter fields)
- NotificationTarget: Extended with tracking_config_id and
template_config_id FKs (many-to-one, reusable configs)
Removed entities:
- MessageTemplate (replaced by TemplateConfig)
- ScheduledJob (absorbed into TrackingConfig)
Updated services:
- watcher.py: Each target checked against its own tracking_config
for event filtering before sending notification
- notifier.py: Uses target's template_config to select the right
template slot based on event type
New API routes:
- /api/tracking-configs/* (CRUD)
- /api/template-configs/* (CRUD + per-slot preview)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix i18n: remove $state rune (SSR incompatible in .ts files),
use reactive localeVersion counter in layout to trigger re-render
on locale change. Language switching now works immediately.
- Fix dark theme: add global CSS rules for input/select/textarea to
use theme colors, override browser autofill in dark mode, set
color-scheme for native controls (scrollbars, checkboxes)
- Collapsible sidebar: toggle button (▶/◀) with persistent state,
icons-only mode when collapsed. Theme/language buttons moved to
bottom above user info.
- Loading skeletons: all pages show animated pulse placeholders
while data loads, eliminating content flicker on tab switch
- Edit support: Servers, Trackers, and Targets now have Edit buttons
that open the form pre-filled with current values. Save calls PUT.
Sensitive fields (API key, bot token) can be left empty to keep
current value when editing.
- CLAUDE.md: add dev server restart rules
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Backend changes for full blueprint feature parity:
Database models:
- MessageTemplate: add body_ru (locale variant), event_type field
- AlbumTracker: add track_images, track_videos, notify_favorites_only,
include_people, include_asset_details, max_assets_to_show,
assets_order_by, assets_order fields
- New ScheduledJob model: supports periodic_summary, scheduled_assets,
and memory (On This Day) notification modes with full config
(times, interval, album_mode, limit, filters, sorting)
API:
- New /api/scheduled/* CRUD endpoints for scheduled jobs
- Tracker create/update accept all new filtering fields
- Tracker response includes new fields
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Frontend enhancements:
- i18n: Full Russian and English translations (~170 keys each),
language switcher in sidebar and login page, auto-detect from
browser, persists to localStorage
- Themes: Light/dark mode with CSS custom properties, system
preference detection, toggle in sidebar header, smooth transitions
- Dark theme: Full color palette (background, card, muted, border,
success, warning, error variants)
Enhanced forms:
- Tracker creation: asset type filtering (images/videos), favorites
only, include people/details toggles, sort by/order selects,
max assets to show
- Target creation: Telegram media settings (collapsible) with
max media, group size, chunk delay, max asset size, URL preview
disable, large photos as documents
- Template creation: event_type selector (all/added/removed/renamed/deleted)
All pages use t() for translations, var(--color-*) for theme-safe
colors, and proper label-for-input associations.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix jinja2.sandbox import: use `from jinja2.sandbox import
SandboxedEnvironment` (dotted attribute access doesn't work)
in templates.py, sync.py, and notifier.py
- Fix greenlet crash in tracker trigger: SQLAlchemy async sessions
can't survive across aiohttp.ClientSession context managers.
Eagerly load all tracker/server data before entering HTTP context.
Split check_tracker into check_tracker (scheduler, own session)
and check_tracker_with_session (API, reuses route session).
- Fix _check_album to accept pre-loaded params instead of tracker
object (avoids lazy-load access after greenlet context break)
Tested end-to-end against live Immich server:
- Server connection + album browsing: OK (39 albums)
- Template creation + preview: OK
- Webhook target creation: OK
- Tracker creation + trigger: OK (initialized 4 assets)
- Second trigger: OK (no_changes detected)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fixes 7 issues identified by code-reviewer agent:
1. (Critical) Webhook endpoint now validates X-Telegram-Bot-Api-
Secret-Token header against configured secret, and verifies
bot_token matches a stored NotificationTarget
2. (Critical) register/unregister webhook endpoints now require
JWT auth via Depends(get_current_user); register passes
secret_token to Telegram setWebhook
3. (Critical) Conversation dict now uses OrderedDict with LRU
eviction (max 100 chats); trim happens BEFORE API call
4. (Important) Tool-use responses no longer stored in conversation
history (avoids corrupted multi-turn state)
5. (Important) Singleton AsyncAnthropic client (module-level,
lazily initialized once) - no more connection pool leaks
6. (Important) Markdown retry now uses payload.pop("parse_mode")
instead of setting empty string
7. (Important) All user-controlled data wrapped in <data> tags
with _sanitize() helper (truncation + newline stripping)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Integrate Claude AI into the notification system for intelligent
conversational interactions and AI-powered captions.
New modules:
- ai/service.py: Claude API client with conversation history,
caption generation, and album activity summarization
- ai/telegram_webhook.py: Telegram webhook handler for incoming
bot messages, routes to AI service for responses
Features:
- Conversational bot: users chat with the bot about albums
- AI captions: intelligent notification messages based on album
context (people, locations, dates) - enabled per target via
"ai_captions" config flag
- Album summaries: "what's new?" triggers AI-generated overview
- /start command with welcome message
- Webhook register/unregister endpoints
Architecture:
- Per-chat conversation history (in-memory, capped at 20 messages)
- Graceful degradation: AI features completely disabled without
IMMICH_WATCHER_ANTHROPIC_API_KEY env var (zero impact)
- AI caption failure falls back to Jinja2 template rendering
- Health endpoint reports ai_enabled status
Config: IMMICH_WATCHER_ANTHROPIC_API_KEY, IMMICH_WATCHER_AI_MODEL,
IMMICH_WATCHER_AI_MAX_TOKENS
Server now has 45 API routes (was 42 after Phase 5).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fixes 5 issues identified by code-reviewer agent:
1. (Critical) EventLog.tracker_id now nullable - use None instead
of 0 when tracker name doesn't match, avoiding FK constraint
violations on PostgreSQL
2. (Critical) Replace jinja2.Environment with SandboxedEnvironment
in all 3 server template rendering locations to prevent SSTI
3. (Important) Rebuild sync_client in _async_update_listener when
server URL/key options change, propagate to all coordinators
4. (Important) Validate partial server config - require both URL
and API key or neither, with clear error message
5. (Important) Name fire-and-forget sync task for debugging
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Enable the HAOS integration to optionally connect to the standalone
Immich Watcher server for config sync and event reporting.
Server-side:
- New /api/sync/* endpoints: GET trackers, POST template render,
POST event report
- API key auth via X-API-Key header (accepts JWT access tokens)
Integration-side:
- New sync.py: ServerSyncClient with graceful error handling
(all methods return defaults on connection failure)
- Options flow: optional server_url and server_api_key fields
with connection validation
- Coordinator: fire-and-forget event reporting to server when
album changes are detected
- Translations: en.json and ru.json updated with new fields
The connection is fully additive -- the integration works identically
without a server URL configured. Server failures never break HA.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fixes 7 issues identified by code-reviewer agent:
1. (Critical) Move JSON.parse inside try/catch in targets page to
handle malformed webhook headers input gracefully
2. (Low) Add window guard to refreshAccessToken for SSR safety
3. (Important) Show loading indicator instead of page content while
auth state is being resolved (prevents flash of protected content)
4. (Important) Add try/catch to trackers load() Promise.all
5. (Important) Add error handling to remove() in servers, trackers,
and templates pages
6. (Important) Track preview per-template with previewId, show
inline instead of global bottom block
7. (Important) Use finally block for loading state in auth
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Build a modern, calm web UI using SvelteKit 5 + Tailwind CSS v4.
Pages:
- Setup wizard (first-run admin account creation)
- Login with JWT token management and auto-refresh
- Dashboard with stats cards and recent events timeline
- Servers: add/delete Immich server connections with validation
- Trackers: create album trackers with album picker, event type
selection, target assignment, and scan interval config
- Templates: Jinja2 message template editor with live preview
- Targets: Telegram and webhook notification targets with test
- Users: admin-only user management (create/delete)
Architecture:
- Reactive auth state with Svelte 5 runes
- API client with JWT auth, auto-refresh on 401
- Static adapter builds to 153KB for embedding in FastAPI
- Vite proxy config for dev server -> backend API
- Sidebar layout with navigation and user info
Also adds Rule 2 to primary plan: perform detailed code review
after completing each phase.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Build a complete standalone web server for Immich album change
notifications, independent of Home Assistant. Uses the shared
core library from Phase 1.
Server features:
- FastAPI with async SQLite (SQLModel + aiosqlite)
- Multi-user auth with JWT (admin/user roles, setup wizard)
- CRUD APIs: Immich servers, album trackers, message templates,
notification targets (Telegram + webhook), user management
- APScheduler background polling per tracker
- Jinja2 template rendering with live preview
- Album browser proxied from Immich API
- Event logging and dashboard status endpoint
- Docker deployment (single container, SQLite in volume)
39 API routes, 14 integration tests passing.
Also adds Phase 6 (Claude AI Telegram bot enhancement) to the
primary plan as an optional future phase.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Wire the integration to delegate all HA-independent logic to
immich-watcher-core, eliminating ~2300 lines of duplicated code.
Changes:
- const.py: Import shared constants from core, keep HA-specific ones
- storage.py: Create HAStorageBackend adapter wrapping HA's Store,
use core TelegramFileCache and NotificationQueue via adapter
- coordinator.py: Delegate to core ImmichClient for API calls,
detect_album_changes() for change detection, and asset_utils
for filtering/sorting/URL building. Keep HA-specific event firing.
- sensor.py: Replace ~1300 lines of Telegram code with 15-line
delegation to core TelegramClient. Keep entity classes unchanged.
- __init__.py: Use factory functions for creating core instances
with HA storage backends
- manifest.json: Add immich-watcher-core dependency
Integration line count: 3600 -> 1295 lines (-64%)
Zero behavior changes for end users.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract HA-independent logic from the integration into packages/core/
as a standalone Python library (immich-watcher-core). This is the first
phase of restructuring the project to support a standalone web app
alongside the existing HAOS integration.
Core library modules:
- models: SharedLinkInfo, AssetInfo, AlbumData, AlbumChange dataclasses
- immich_client: Async Immich API client (aiohttp, session-injected)
- change_detector: Pure function for album change detection
- asset_utils: Filtering, sorting, URL building utilities
- telegram/client: Full Telegram Bot API (text, photo, video, media groups)
- telegram/cache: File ID cache with pluggable storage backend
- telegram/media: Media size checks, URL extraction, group splitting
- notifications/queue: Persistent notification queue
- storage: StorageBackend protocol + JSON file implementation
All modules have zero Home Assistant imports. 50 unit tests passing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Quiet hours are now specified per send_telegram_notification call via
quiet_hours_start/quiet_hours_end params instead of being a hub-wide
integration option. This allows different automations to use different
quiet hours windows (or none at all).
- Remove quiet_hours_start/end from config options UI and const.py
- Add quiet_hours_start/end as optional HH:MM params on the service
- Remove ignore_quiet_hours param (omit quiet hours params to send immediately)
- Queue stores quiet_hours_end per item; each unique end time gets its
own async_track_time_change timer for replay
- On startup, items whose quiet hours have passed are sent immediately
- Add async_remove_indices() to NotificationQueue for selective removal
- Timers are cleaned up when no more items need them
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add quiet hours support to queue notifications during configured time windows
- Fix UnboundLocalError when single-item document chunk exceeds max_asset_data_size
- Fix document-only multi-item chunks being silently dropped (missing skip guard)
- Fix notification queue entity lookup by storing entity_id in queued params
- Fix quiet hours using OS timezone instead of HA-configured timezone (dt_util.now)
- Fix chat_action schema rejecting empty string from "Disabled" selector
- Fix stale thumbhash cache entries not being removed on mismatch
- Fix translation descriptions for send_large_photos_as_documents
- Add batch async_set_many() to TelegramFileCache to reduce disk writes
- Add max-entries eviction (2000) for thumbhash cache to prevent unbounded growth
- Eliminate redundant _is_asset_id/get_asset_thumbhash lookups in media group loop
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Asset cache now validates entries by comparing stored thumbhash with current
Immich thumbhash instead of using TTL expiration. This makes cache invalidation
precise (only when content actually changes) and eliminates unnecessary re-uploads.
URL-based cache retains TTL for non-Immich URLs.
- Add TELEGRAM_MAX_VIDEO_SIZE (50 MB) check to skip oversized videos in both
single-video and media-group paths, preventing entire groups from failing.
- Split media groups into sub-groups by cumulative upload size to ensure each
sendMediaGroup request stays under Telegram's 50 MB upload limit.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add type: document as default media type (instead of photo)
- Add optional content_type field for explicit MIME type specification
- Documents are sent separately (Telegram API limitation for media groups)
- Default content types: image/jpeg (photo), video/mp4 (video), auto-detect (document)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Shows typing/upload indicator while processing media. Supports:
typing, upload_photo, upload_video, upload_document actions.
Set to empty string to disable. Default: typing.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Renamed the date filter parameter and changed default behavior to match
Google Photos memories - now excludes assets from the same year as the
reference date, returning only photos from previous years on that day.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add offset parameter for pagination support
- Add on_this_day parameter for memories filtering (match month and day)
- Add city, state, country parameters for geolocation filtering
- Update README with new parameters and examples
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add asset_count, last_updated, and created_at attributes to the
Album ID sensor for convenient access to album metadata.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add _pending_asset_ids to track assets detected but not yet processed
- Fire events when pending assets become processed (thumbhash available)
- Fixes issue where videos added during transcoding never triggered events
- Add debug logging for change detection and pending asset tracking
- Document external domain feature in README
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fetch externalDomain from Immich server config on startup
- Use external domain for user-facing URLs (share links, asset URLs)
- Keep internal connection URL for API calls
- Add get_internal_download_url() to convert external URLs back to
internal for faster local network downloads (Telegram notifications)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Use thumbhash for all assets instead of encodedVideoPath for videos
(encodedVideoPath is not exposed in Immich API response)
- Add isTrashed check to exclude trashed assets from events
- Simplify processing status logic for both photos and videos
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implement caching for Telegram file_ids to avoid re-uploading the same media.
Cached IDs are reused for subsequent sends, improving performance significantly.
Added configurable cache TTL option (1-168 hours, default 48).
Also added city, state, and country fields from Immich reverse geocoding
to asset data in events and get_assets service.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Expose GPS coordinates from EXIF data in asset responses. The latitude
and longitude fields are included in get_assets service responses and
event data when available.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Replace filter parameter with independent favorite_only boolean
- Add order_by parameter supporting date, rating, and name sorting
- Rename count to limit for clarity
- Add date range filtering with min_date and max_date parameters
- Add asset_type filtering for photos and videos
- Update README with language support section and fixed sensor list
- Add translations for all new parameters
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Remove photo downscaling logic in favor of cleaner error handling
- Add intelligent Telegram API error logging with diagnostics and suggestions
- Define Telegram photo limits as global constants (TELEGRAM_MAX_PHOTO_SIZE, TELEGRAM_MAX_DIMENSION_SUM)
- Add photo_url support for image assets (matching video_url for videos)
- Unify asset detail building with shared _build_asset_detail() helper method
- Enhance get_assets service to return complete asset data matching events
- Simplify attribute naming by removing redundant asset_ prefix from values
BREAKING CHANGE: Asset attribute keys changed from "asset_type", "asset_filename"
to simpler "type", "filename" for consistency and cleaner JSON responses
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>