Comprehensive review fixes: security, performance, code quality, and UI polish
Some checks failed
Validate / Hassfest (push) Has been cancelled
Some checks failed
Validate / Hassfest (push) Has been cancelled
Backend: Fix CORS wildcard+credentials, add secret key warning, remove raw API keys from sync endpoint, fix N+1 queries in watcher/sync, fix AttributeError on event_types, delete dead scheduled.py/templates.py, add limit cap on history, re-validate server on URL/key update, apply tracking/template config IDs in update_target. HA Integration: Replace datetime.now() with dt_util.now(), fix notification queue to only remove successfully sent items, use album UUID for entity unique IDs, add shared links dirty flag and users cache hourly refresh, deduplicate _is_quiet_hours, add HTTP timeouts, cache albums in config flow, change iot_class to local_polling. Frontend: Make i18n reactive via $state (remove window.location.reload), add Modal transitions/a11y/Escape key, create ConfirmModal replacing all confirm() calls, add error handling to all pages, replace Unicode nav icons with MDI SVGs, add card hover effects, dashboard stat icons, global focus-visible styles, form slide transitions, mobile responsive bottom nav, fix password error color, add ~20 i18n keys (EN/RU). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -164,7 +164,7 @@ async def _build_context(session: AsyncSession, chat_id: str) -> str:
|
||||
if trackers:
|
||||
parts.append(f"Active trackers: {len(trackers)}")
|
||||
for t in trackers[:5]:
|
||||
parts.append(f" - {t.name}: {len(t.album_ids)} album(s), events: {', '.join(t.event_types)}")
|
||||
parts.append(f" - {t.name}: {len(t.album_ids)} album(s)")
|
||||
|
||||
result = await session.exec(
|
||||
select(EventLog).order_by(EventLog.created_at.desc()).limit(5)
|
||||
@@ -184,15 +184,19 @@ async def _get_summary_data(
|
||||
"""Fetch data for album summary."""
|
||||
albums_data: list[dict[str, Any]] = []
|
||||
servers_result = await session.exec(select(ImmichServer).limit(5))
|
||||
for server in servers_result.all():
|
||||
try:
|
||||
from immich_watcher_core.immich_client import ImmichClient
|
||||
async with aiohttp.ClientSession() as http_session:
|
||||
client = ImmichClient(http_session, server.url, server.api_key)
|
||||
albums = await client.get_albums()
|
||||
albums_data.extend(albums[:20])
|
||||
except Exception:
|
||||
_LOGGER.debug("Failed to fetch albums from %s for summary", server.url)
|
||||
servers = servers_result.all()
|
||||
try:
|
||||
from immich_watcher_core.immich_client import ImmichClient
|
||||
async with aiohttp.ClientSession() as http_session:
|
||||
for server in servers:
|
||||
try:
|
||||
client = ImmichClient(http_session, server.url, server.api_key)
|
||||
albums = await client.get_albums()
|
||||
albums_data.extend(albums[:20])
|
||||
except Exception:
|
||||
_LOGGER.debug("Failed to fetch albums from %s for summary", server.url)
|
||||
except Exception:
|
||||
_LOGGER.debug("Failed to create HTTP session for summary")
|
||||
|
||||
events_result = await session.exec(
|
||||
select(EventLog).order_by(EventLog.created_at.desc()).limit(20)
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
"""Scheduled notification job API routes."""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from pydantic import BaseModel
|
||||
from sqlmodel import select
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
from ..auth.dependencies import get_current_user
|
||||
from ..database.engine import get_session
|
||||
from ..database.models import AlbumTracker, ScheduledJob, User
|
||||
|
||||
router = APIRouter(prefix="/api/scheduled", tags=["scheduled"])
|
||||
|
||||
|
||||
class ScheduledJobCreate(BaseModel):
|
||||
tracker_id: int
|
||||
job_type: str # periodic_summary, scheduled_assets, memory
|
||||
enabled: bool = True
|
||||
times: str = "09:00"
|
||||
interval_days: int = 1
|
||||
start_date: str = "2025-01-01"
|
||||
album_mode: str = "per_album"
|
||||
limit: int = 10
|
||||
favorite_only: bool = False
|
||||
asset_type: str = "all"
|
||||
min_rating: int = 0
|
||||
order_by: str = "random"
|
||||
order: str = "descending"
|
||||
min_date: str | None = None
|
||||
max_date: str | None = None
|
||||
message_template: str = ""
|
||||
|
||||
|
||||
class ScheduledJobUpdate(BaseModel):
|
||||
enabled: bool | None = None
|
||||
times: str | None = None
|
||||
interval_days: int | None = None
|
||||
album_mode: str | None = None
|
||||
limit: int | None = None
|
||||
favorite_only: bool | None = None
|
||||
asset_type: str | None = None
|
||||
min_rating: int | None = None
|
||||
order_by: str | None = None
|
||||
order: str | None = None
|
||||
min_date: str | None = None
|
||||
max_date: str | None = None
|
||||
message_template: str | None = None
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def list_jobs(
|
||||
user: User = Depends(get_current_user),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
"""List all scheduled jobs for the current user's trackers."""
|
||||
trackers = await session.exec(
|
||||
select(AlbumTracker).where(AlbumTracker.user_id == user.id)
|
||||
)
|
||||
tracker_ids = [t.id for t in trackers.all()]
|
||||
if not tracker_ids:
|
||||
return []
|
||||
|
||||
result = await session.exec(
|
||||
select(ScheduledJob).where(ScheduledJob.tracker_id.in_(tracker_ids))
|
||||
)
|
||||
return [_job_response(j) for j in result.all()]
|
||||
|
||||
|
||||
@router.post("", status_code=status.HTTP_201_CREATED)
|
||||
async def create_job(
|
||||
body: ScheduledJobCreate,
|
||||
user: User = Depends(get_current_user),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
"""Create a scheduled notification job."""
|
||||
# Verify tracker ownership
|
||||
tracker = await session.get(AlbumTracker, body.tracker_id)
|
||||
if not tracker or tracker.user_id != user.id:
|
||||
raise HTTPException(status_code=404, detail="Tracker not found")
|
||||
|
||||
if body.job_type not in ("periodic_summary", "scheduled_assets", "memory"):
|
||||
raise HTTPException(status_code=400, detail="Invalid job_type")
|
||||
|
||||
job = ScheduledJob(**body.model_dump())
|
||||
session.add(job)
|
||||
await session.commit()
|
||||
await session.refresh(job)
|
||||
return _job_response(job)
|
||||
|
||||
|
||||
@router.put("/{job_id}")
|
||||
async def update_job(
|
||||
job_id: int,
|
||||
body: ScheduledJobUpdate,
|
||||
user: User = Depends(get_current_user),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
"""Update a scheduled job."""
|
||||
job = await _get_user_job(session, job_id, user.id)
|
||||
for field, value in body.model_dump(exclude_unset=True).items():
|
||||
setattr(job, field, value)
|
||||
session.add(job)
|
||||
await session.commit()
|
||||
await session.refresh(job)
|
||||
return _job_response(job)
|
||||
|
||||
|
||||
@router.delete("/{job_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_job(
|
||||
job_id: int,
|
||||
user: User = Depends(get_current_user),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
"""Delete a scheduled job."""
|
||||
job = await _get_user_job(session, job_id, user.id)
|
||||
await session.delete(job)
|
||||
await session.commit()
|
||||
|
||||
|
||||
def _job_response(j: ScheduledJob) -> dict:
|
||||
return {
|
||||
"id": j.id,
|
||||
"tracker_id": j.tracker_id,
|
||||
"job_type": j.job_type,
|
||||
"enabled": j.enabled,
|
||||
"times": j.times,
|
||||
"interval_days": j.interval_days,
|
||||
"start_date": j.start_date,
|
||||
"album_mode": j.album_mode,
|
||||
"limit": j.limit,
|
||||
"favorite_only": j.favorite_only,
|
||||
"asset_type": j.asset_type,
|
||||
"min_rating": j.min_rating,
|
||||
"order_by": j.order_by,
|
||||
"order": j.order,
|
||||
"min_date": j.min_date,
|
||||
"max_date": j.max_date,
|
||||
"message_template": j.message_template,
|
||||
"created_at": j.created_at.isoformat(),
|
||||
}
|
||||
|
||||
|
||||
async def _get_user_job(
|
||||
session: AsyncSession, job_id: int, user_id: int
|
||||
) -> ScheduledJob:
|
||||
job = await session.get(ScheduledJob, job_id)
|
||||
if not job:
|
||||
raise HTTPException(status_code=404, detail="Job not found")
|
||||
tracker = await session.get(AlbumTracker, job.tracker_id)
|
||||
if not tracker or tracker.user_id != user_id:
|
||||
raise HTTPException(status_code=404, detail="Job not found")
|
||||
return job
|
||||
@@ -104,10 +104,28 @@ async def update_server(
|
||||
server = await _get_user_server(session, server_id, user.id)
|
||||
if body.name is not None:
|
||||
server.name = body.name
|
||||
url_changed = body.url is not None and body.url != server.url
|
||||
key_changed = body.api_key is not None and body.api_key != server.api_key
|
||||
if body.url is not None:
|
||||
server.url = body.url
|
||||
if body.api_key is not None:
|
||||
server.api_key = body.api_key
|
||||
# Re-validate and refresh external_domain when URL or API key changes
|
||||
if url_changed or key_changed:
|
||||
try:
|
||||
async with aiohttp.ClientSession() as http_session:
|
||||
client = ImmichClient(http_session, server.url, server.api_key)
|
||||
if not await client.ping():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Cannot connect to Immich server at {server.url}",
|
||||
)
|
||||
server.external_domain = await client.get_server_config()
|
||||
except aiohttp.ClientError as err:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Connection error: {err}",
|
||||
)
|
||||
session.add(server)
|
||||
await session.commit()
|
||||
await session.refresh(server)
|
||||
|
||||
@@ -52,12 +52,9 @@ class SyncTrackerResponse(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
server_url: str
|
||||
server_api_key: str
|
||||
album_ids: list[str]
|
||||
event_types: list[str]
|
||||
scan_interval: int
|
||||
enabled: bool
|
||||
template_body: str | None = None
|
||||
targets: list[dict] = []
|
||||
|
||||
|
||||
@@ -84,40 +81,60 @@ async def get_sync_trackers(
|
||||
)
|
||||
trackers = result.all()
|
||||
|
||||
# Batch-load servers and targets to avoid N+1 queries
|
||||
server_ids = {t.server_id for t in trackers}
|
||||
all_target_ids = {tid for t in trackers for tid in t.target_ids}
|
||||
|
||||
servers_result = await session.exec(
|
||||
select(ImmichServer).where(ImmichServer.id.in_(server_ids))
|
||||
)
|
||||
servers_map = {s.id: s for s in servers_result.all()}
|
||||
|
||||
targets_result = await session.exec(
|
||||
select(NotificationTarget).where(NotificationTarget.id.in_(all_target_ids))
|
||||
)
|
||||
targets_map = {t.id: t for t in targets_result.all()}
|
||||
|
||||
responses = []
|
||||
for tracker in trackers:
|
||||
# Fetch server details
|
||||
server = await session.get(ImmichServer, tracker.server_id)
|
||||
server = servers_map.get(tracker.server_id)
|
||||
if not server:
|
||||
continue
|
||||
|
||||
# Fetch target configs
|
||||
targets = []
|
||||
for target_id in tracker.target_ids:
|
||||
target = await session.get(NotificationTarget, target_id)
|
||||
target = targets_map.get(target_id)
|
||||
if target:
|
||||
targets.append({
|
||||
"type": target.type,
|
||||
"name": target.name,
|
||||
"config": target.config,
|
||||
"config": _safe_target_config(target),
|
||||
})
|
||||
|
||||
responses.append(SyncTrackerResponse(
|
||||
id=tracker.id,
|
||||
name=tracker.name,
|
||||
server_url=server.url,
|
||||
server_api_key=server.api_key,
|
||||
album_ids=tracker.album_ids,
|
||||
event_types=[], # Event types now on tracking configs
|
||||
scan_interval=tracker.scan_interval,
|
||||
enabled=tracker.enabled,
|
||||
template_body=None,
|
||||
targets=targets,
|
||||
))
|
||||
|
||||
return responses
|
||||
|
||||
|
||||
def _safe_target_config(target: NotificationTarget) -> dict:
|
||||
"""Return config with sensitive fields masked."""
|
||||
config = dict(target.config)
|
||||
if "bot_token" in config:
|
||||
token = config["bot_token"]
|
||||
config["bot_token"] = f"{token[:8]}...{token[-4:]}" if len(token) > 12 else "***"
|
||||
if "api_key" in config:
|
||||
config["api_key"] = "***"
|
||||
return config
|
||||
|
||||
|
||||
@router.post("/templates/{template_id}/render")
|
||||
async def render_template(
|
||||
template_id: int,
|
||||
|
||||
@@ -92,6 +92,10 @@ async def update_target(
|
||||
target.name = body.name
|
||||
if body.config is not None:
|
||||
target.config = body.config
|
||||
if body.tracking_config_id is not None:
|
||||
target.tracking_config_id = body.tracking_config_id
|
||||
if body.template_config_id is not None:
|
||||
target.template_config_id = body.template_config_id
|
||||
session.add(target)
|
||||
await session.commit()
|
||||
await session.refresh(target)
|
||||
|
||||
@@ -97,8 +97,9 @@ async def get_bot_token(
|
||||
user: User = Depends(get_current_user),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
"""Get the full bot token (for internal use by targets)."""
|
||||
"""Get the full bot token (used by frontend to construct target config)."""
|
||||
bot = await _get_user_bot(session, bot_id, user.id)
|
||||
# Token is returned only to the authenticated owner
|
||||
return {"token": bot.token}
|
||||
|
||||
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
"""Message template management API routes."""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from pydantic import BaseModel
|
||||
from sqlmodel import select
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
import jinja2
|
||||
from jinja2.sandbox import SandboxedEnvironment
|
||||
|
||||
from ..auth.dependencies import get_current_user
|
||||
from ..database.engine import get_session
|
||||
from ..database.models import MessageTemplate, User
|
||||
|
||||
router = APIRouter(prefix="/api/templates", tags=["templates"])
|
||||
|
||||
# Sample data for template preview
|
||||
_SAMPLE_CONTEXT = {
|
||||
"album_name": "Family Photos",
|
||||
"album_url": "https://immich.example.com/share/abc123",
|
||||
"added_count": 3,
|
||||
"removed_count": 0,
|
||||
"change_type": "assets_added",
|
||||
"people": ["Alice", "Bob"],
|
||||
"added_assets": [
|
||||
{"filename": "IMG_001.jpg", "type": "IMAGE", "owner": "Alice", "created_at": "2024-03-19T10:30:00Z"},
|
||||
{"filename": "IMG_002.jpg", "type": "IMAGE", "owner": "Bob", "created_at": "2024-03-19T11:00:00Z"},
|
||||
{"filename": "VID_003.mp4", "type": "VIDEO", "owner": "Alice", "created_at": "2024-03-19T11:30:00Z"},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
class TemplateCreate(BaseModel):
|
||||
name: str
|
||||
body: str
|
||||
is_default: bool = False
|
||||
|
||||
|
||||
class TemplateUpdate(BaseModel):
|
||||
name: str | None = None
|
||||
body: str | None = None
|
||||
is_default: bool | None = None
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def list_templates(
|
||||
user: User = Depends(get_current_user),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
"""List all templates for the current user."""
|
||||
result = await session.exec(
|
||||
select(MessageTemplate).where(MessageTemplate.user_id == user.id)
|
||||
)
|
||||
return [
|
||||
{"id": t.id, "name": t.name, "body": t.body, "is_default": t.is_default, "created_at": t.created_at.isoformat()}
|
||||
for t in result.all()
|
||||
]
|
||||
|
||||
|
||||
@router.post("", status_code=status.HTTP_201_CREATED)
|
||||
async def create_template(
|
||||
body: TemplateCreate,
|
||||
user: User = Depends(get_current_user),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
"""Create a new message template."""
|
||||
template = MessageTemplate(
|
||||
user_id=user.id,
|
||||
name=body.name,
|
||||
body=body.body,
|
||||
is_default=body.is_default,
|
||||
)
|
||||
session.add(template)
|
||||
await session.commit()
|
||||
await session.refresh(template)
|
||||
return {"id": template.id, "name": template.name}
|
||||
|
||||
|
||||
@router.get("/{template_id}")
|
||||
async def get_template(
|
||||
template_id: int,
|
||||
user: User = Depends(get_current_user),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
"""Get a specific template."""
|
||||
template = await _get_user_template(session, template_id, user.id)
|
||||
return {"id": template.id, "name": template.name, "body": template.body, "is_default": template.is_default}
|
||||
|
||||
|
||||
@router.put("/{template_id}")
|
||||
async def update_template(
|
||||
template_id: int,
|
||||
body: TemplateUpdate,
|
||||
user: User = Depends(get_current_user),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
"""Update a template."""
|
||||
template = await _get_user_template(session, template_id, user.id)
|
||||
for field, value in body.model_dump(exclude_unset=True).items():
|
||||
setattr(template, field, value)
|
||||
session.add(template)
|
||||
await session.commit()
|
||||
await session.refresh(template)
|
||||
return {"id": template.id, "name": template.name}
|
||||
|
||||
|
||||
@router.delete("/{template_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_template(
|
||||
template_id: int,
|
||||
user: User = Depends(get_current_user),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
"""Delete a template."""
|
||||
template = await _get_user_template(session, template_id, user.id)
|
||||
await session.delete(template)
|
||||
await session.commit()
|
||||
|
||||
|
||||
@router.post("/{template_id}/preview")
|
||||
async def preview_template(
|
||||
template_id: int,
|
||||
user: User = Depends(get_current_user),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
"""Render a template with sample data."""
|
||||
template = await _get_user_template(session, template_id, user.id)
|
||||
try:
|
||||
env = SandboxedEnvironment(autoescape=False)
|
||||
tmpl = env.from_string(template.body)
|
||||
rendered = tmpl.render(**_SAMPLE_CONTEXT)
|
||||
return {"rendered": rendered}
|
||||
except jinja2.TemplateError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Template error: {e}",
|
||||
)
|
||||
|
||||
|
||||
async def _get_user_template(
|
||||
session: AsyncSession, template_id: int, user_id: int
|
||||
) -> MessageTemplate:
|
||||
template = await session.get(MessageTemplate, template_id)
|
||||
if not template or template.user_id != user_id:
|
||||
raise HTTPException(status_code=404, detail="Template not found")
|
||||
return template
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Album tracker management API routes."""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from pydantic import BaseModel
|
||||
from sqlmodel import select
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
@@ -150,7 +150,7 @@ async def test_memory(
|
||||
@router.get("/{tracker_id}/history")
|
||||
async def tracker_history(
|
||||
tracker_id: int,
|
||||
limit: int = 20,
|
||||
limit: int = Query(default=20, ge=1, le=500),
|
||||
user: User = Depends(get_current_user),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Server configuration from environment variables."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
@@ -11,8 +12,16 @@ class Settings(BaseSettings):
|
||||
data_dir: Path = Path("/data")
|
||||
database_url: str = "" # Computed from data_dir if empty
|
||||
|
||||
# JWT
|
||||
# JWT (MUST be overridden via IMMICH_WATCHER_SECRET_KEY env var)
|
||||
secret_key: str = "change-me-in-production"
|
||||
|
||||
def model_post_init(self, __context: Any) -> None:
|
||||
if self.secret_key == "change-me-in-production" and not self.debug:
|
||||
import logging
|
||||
logging.getLogger(__name__).critical(
|
||||
"SECURITY: Using default secret_key in non-debug mode! "
|
||||
"Set IMMICH_WATCHER_SECRET_KEY environment variable."
|
||||
)
|
||||
access_token_expire_minutes: int = 60
|
||||
refresh_token_expire_days: int = 30
|
||||
|
||||
|
||||
@@ -56,11 +56,11 @@ app = FastAPI(
|
||||
lifespan=lifespan,
|
||||
)
|
||||
|
||||
# CORS for frontend dev server
|
||||
# CORS: restrict to same-origin in production, allow all in debug mode
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_origins=["*"] if settings.debug else [],
|
||||
allow_credentials=False,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
@@ -166,16 +166,28 @@ async def _check_album(
|
||||
)
|
||||
session.add(event_log)
|
||||
|
||||
# Send notifications to each target, filtered by its tracking config
|
||||
for target_id in target_ids:
|
||||
target = await session.get(NotificationTarget, target_id)
|
||||
if not target:
|
||||
continue
|
||||
# Batch-load targets, tracking configs, and template configs
|
||||
targets_result = await session.exec(
|
||||
select(NotificationTarget).where(NotificationTarget.id.in_(target_ids))
|
||||
)
|
||||
targets = targets_result.all()
|
||||
|
||||
# Check target's tracking config for event filtering
|
||||
tracking_config = None
|
||||
if target.tracking_config_id:
|
||||
tracking_config = await session.get(TrackingConfig, target.tracking_config_id)
|
||||
tc_ids = {t.tracking_config_id for t in targets if t.tracking_config_id}
|
||||
tmpl_ids = {t.template_config_id for t in targets if t.template_config_id}
|
||||
|
||||
tracking_configs_map: dict[int, TrackingConfig] = {}
|
||||
if tc_ids:
|
||||
tc_result = await session.exec(select(TrackingConfig).where(TrackingConfig.id.in_(tc_ids)))
|
||||
tracking_configs_map = {tc.id: tc for tc in tc_result.all()}
|
||||
|
||||
template_configs_map: dict[int, TemplateConfig] = {}
|
||||
if tmpl_ids:
|
||||
tmpl_result = await session.exec(select(TemplateConfig).where(TemplateConfig.id.in_(tmpl_ids)))
|
||||
template_configs_map = {tc.id: tc for tc in tmpl_result.all()}
|
||||
|
||||
# Send notifications to each target, filtered by its tracking config
|
||||
for target in targets:
|
||||
tracking_config = tracking_configs_map.get(target.tracking_config_id) if target.tracking_config_id else None
|
||||
|
||||
if tracking_config:
|
||||
# Filter by event type
|
||||
@@ -193,16 +205,13 @@ async def _check_album(
|
||||
if not should_notify:
|
||||
continue
|
||||
|
||||
# Get target's template config
|
||||
template_config = None
|
||||
if target.template_config_id:
|
||||
template_config = await session.get(TemplateConfig, target.template_config_id)
|
||||
template_config = template_configs_map.get(target.template_config_id) if target.template_config_id else None
|
||||
|
||||
try:
|
||||
use_ai = target.config.get("ai_captions", False)
|
||||
await send_notification(target, event_data, template_config, use_ai_caption=use_ai)
|
||||
except Exception:
|
||||
_LOGGER.exception("Failed to send notification to target %d", target_id)
|
||||
_LOGGER.exception("Failed to send notification to target %d", target.id)
|
||||
|
||||
return {
|
||||
"album_id": album_id,
|
||||
|
||||
Reference in New Issue
Block a user