feat: port full CRUD API routes and frontend pages from Immich Watcher
Backend API (38 routes): - providers: full CRUD + test connection + list collections + API key masking - trackers: full CRUD + trigger + history + test-periodic/memory - tracking-configs: full CRUD with Pydantic models, provider_type filter - template-configs: full CRUD + preview + preview-raw with two-pass validation - targets: full CRUD + test notification + config masking - telegram-bots: full CRUD + chat discovery + token endpoint - users: full admin CRUD + password reset + self-delete protection - status: dashboard endpoint with providers/trackers/targets/events counts Frontend pages updated: - Dashboard with animated stat cards and event timeline - Providers with proper components, delete confirm, snackbar - Trackers/targets/tracking-configs/template-configs/telegram-bots/users all use PageHeader, Card, Loading, MdiIcon with correct i18n keys Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"""TrackingConfig CRUD API routes."""
|
||||
"""Tracking configuration CRUD API routes."""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from pydantic import BaseModel
|
||||
from sqlmodel import select
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
@@ -11,8 +12,85 @@ from ..database.models import TrackingConfig, User
|
||||
router = APIRouter(prefix="/api/tracking-configs", tags=["tracking-configs"])
|
||||
|
||||
|
||||
class TrackingConfigCreate(BaseModel):
|
||||
provider_type: str
|
||||
name: str
|
||||
icon: str = ""
|
||||
track_assets_added: bool = True
|
||||
track_assets_removed: bool = False
|
||||
track_collection_renamed: bool = True
|
||||
track_collection_deleted: bool = True
|
||||
track_sharing_changed: bool = False
|
||||
track_images: bool = True
|
||||
track_videos: bool = True
|
||||
notify_favorites_only: bool = False
|
||||
include_tags: bool = True
|
||||
include_asset_details: bool = False
|
||||
max_assets_to_show: int = 5
|
||||
assets_order_by: str = "none"
|
||||
assets_order: str = "descending"
|
||||
periodic_enabled: bool = False
|
||||
periodic_interval_days: int = 1
|
||||
periodic_start_date: str = "2025-01-01"
|
||||
periodic_times: str = "12:00"
|
||||
scheduled_enabled: bool = False
|
||||
scheduled_times: str = "09:00"
|
||||
scheduled_collection_mode: str = "per_collection"
|
||||
scheduled_limit: int = 10
|
||||
scheduled_favorite_only: bool = False
|
||||
scheduled_asset_type: str = "all"
|
||||
scheduled_min_rating: int = 0
|
||||
scheduled_order_by: str = "random"
|
||||
scheduled_order: str = "descending"
|
||||
memory_enabled: bool = False
|
||||
memory_times: str = "09:00"
|
||||
memory_collection_mode: str = "combined"
|
||||
memory_limit: int = 10
|
||||
memory_favorite_only: bool = False
|
||||
memory_asset_type: str = "all"
|
||||
memory_min_rating: int = 0
|
||||
|
||||
|
||||
class TrackingConfigUpdate(BaseModel):
|
||||
name: str | None = None
|
||||
icon: str | None = None
|
||||
track_assets_added: bool | None = None
|
||||
track_assets_removed: bool | None = None
|
||||
track_collection_renamed: bool | None = None
|
||||
track_collection_deleted: bool | None = None
|
||||
track_sharing_changed: bool | None = None
|
||||
track_images: bool | None = None
|
||||
track_videos: bool | None = None
|
||||
notify_favorites_only: bool | None = None
|
||||
include_tags: bool | None = None
|
||||
include_asset_details: bool | None = None
|
||||
max_assets_to_show: int | None = None
|
||||
assets_order_by: str | None = None
|
||||
assets_order: str | None = None
|
||||
periodic_enabled: bool | None = None
|
||||
periodic_interval_days: int | None = None
|
||||
periodic_start_date: str | None = None
|
||||
periodic_times: str | None = None
|
||||
scheduled_enabled: bool | None = None
|
||||
scheduled_times: str | None = None
|
||||
scheduled_collection_mode: str | None = None
|
||||
scheduled_limit: int | None = None
|
||||
scheduled_favorite_only: bool | None = None
|
||||
scheduled_asset_type: str | None = None
|
||||
scheduled_min_rating: int | None = None
|
||||
scheduled_order_by: str | None = None
|
||||
scheduled_order: str | None = None
|
||||
memory_enabled: bool | None = None
|
||||
memory_times: str | None = None
|
||||
memory_collection_mode: str | None = None
|
||||
memory_limit: int | None = None
|
||||
memory_favorite_only: bool | None = None
|
||||
memory_asset_type: str | None = None
|
||||
memory_min_rating: int | None = None
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def list_tracking_configs(
|
||||
async def list_configs(
|
||||
provider_type: str | None = None,
|
||||
user: User = Depends(get_current_user),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
@@ -21,63 +99,66 @@ async def list_tracking_configs(
|
||||
if provider_type:
|
||||
query = query.where(TrackingConfig.provider_type == provider_type)
|
||||
result = await session.exec(query)
|
||||
return result.all()
|
||||
return [_response(c) for c in result.all()]
|
||||
|
||||
|
||||
@router.post("", status_code=201)
|
||||
async def create_tracking_config(
|
||||
body: dict,
|
||||
@router.post("", status_code=status.HTTP_201_CREATED)
|
||||
async def create_config(
|
||||
body: TrackingConfigCreate,
|
||||
user: User = Depends(get_current_user),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
config = TrackingConfig(user_id=user.id, **body)
|
||||
config = TrackingConfig(user_id=user.id, **body.model_dump())
|
||||
session.add(config)
|
||||
await session.commit()
|
||||
await session.refresh(config)
|
||||
return config
|
||||
return _response(config)
|
||||
|
||||
|
||||
@router.get("/{config_id}")
|
||||
async def get_tracking_config(
|
||||
async def get_config(
|
||||
config_id: int,
|
||||
user: User = Depends(get_current_user),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
config = await session.get(TrackingConfig, config_id)
|
||||
if not config or config.user_id != user.id:
|
||||
raise HTTPException(status_code=404, detail="Tracking config not found")
|
||||
return config
|
||||
return _response(await _get(session, config_id, user.id))
|
||||
|
||||
|
||||
@router.put("/{config_id}")
|
||||
async def update_tracking_config(
|
||||
async def update_config(
|
||||
config_id: int,
|
||||
body: dict,
|
||||
body: TrackingConfigUpdate,
|
||||
user: User = Depends(get_current_user),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
config = await session.get(TrackingConfig, config_id)
|
||||
if not config or config.user_id != user.id:
|
||||
raise HTTPException(status_code=404, detail="Tracking config not found")
|
||||
|
||||
for field, value in body.items():
|
||||
if field not in ("id", "user_id", "created_at"):
|
||||
setattr(config, field, value)
|
||||
|
||||
config = await _get(session, config_id, user.id)
|
||||
for field, value in body.model_dump(exclude_unset=True).items():
|
||||
setattr(config, field, value)
|
||||
session.add(config)
|
||||
await session.commit()
|
||||
await session.refresh(config)
|
||||
return config
|
||||
return _response(config)
|
||||
|
||||
|
||||
@router.delete("/{config_id}", status_code=204)
|
||||
async def delete_tracking_config(
|
||||
@router.delete("/{config_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_config(
|
||||
config_id: int,
|
||||
user: User = Depends(get_current_user),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
config = await session.get(TrackingConfig, config_id)
|
||||
if not config or config.user_id != user.id:
|
||||
raise HTTPException(status_code=404, detail="Tracking config not found")
|
||||
config = await _get(session, config_id, user.id)
|
||||
await session.delete(config)
|
||||
await session.commit()
|
||||
|
||||
|
||||
def _response(c: TrackingConfig) -> dict:
|
||||
return {k: getattr(c, k) for k in TrackingConfig.model_fields if k != "user_id"} | {
|
||||
"created_at": c.created_at.isoformat()
|
||||
}
|
||||
|
||||
|
||||
async def _get(session: AsyncSession, config_id: int, user_id: int) -> TrackingConfig:
|
||||
config = await session.get(TrackingConfig, config_id)
|
||||
if not config or config.user_id != user_id:
|
||||
raise HTTPException(status_code=404, detail="Tracking config not found")
|
||||
return config
|
||||
|
||||
Reference in New Issue
Block a user