Add standalone FastAPI server backend (Phase 3)
Some checks failed
Validate / Hassfest (push) Has been cancelled
Some checks failed
Validate / Hassfest (push) Has been cancelled
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>
This commit is contained in:
112
packages/server/src/immich_watcher_server/database/models.py
Normal file
112
packages/server/src/immich_watcher_server/database/models.py
Normal file
@@ -0,0 +1,112 @@
|
||||
"""SQLModel database table definitions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
from sqlmodel import JSON, Column, Field, SQLModel
|
||||
|
||||
|
||||
def _utcnow() -> datetime:
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
class User(SQLModel, table=True):
|
||||
"""Application user."""
|
||||
|
||||
id: int | None = Field(default=None, primary_key=True)
|
||||
username: str = Field(index=True, unique=True)
|
||||
hashed_password: str
|
||||
role: str = Field(default="user") # "admin" or "user"
|
||||
created_at: datetime = Field(default_factory=_utcnow)
|
||||
|
||||
|
||||
class ImmichServer(SQLModel, table=True):
|
||||
"""Immich server connection."""
|
||||
|
||||
__tablename__ = "immich_server"
|
||||
|
||||
id: int | None = Field(default=None, primary_key=True)
|
||||
user_id: int = Field(foreign_key="user.id")
|
||||
name: str = Field(default="Immich")
|
||||
url: str
|
||||
api_key: str
|
||||
external_domain: str | None = None
|
||||
created_at: datetime = Field(default_factory=_utcnow)
|
||||
|
||||
|
||||
class NotificationTarget(SQLModel, table=True):
|
||||
"""Notification destination (Telegram chat, webhook URL)."""
|
||||
|
||||
__tablename__ = "notification_target"
|
||||
|
||||
id: int | None = Field(default=None, primary_key=True)
|
||||
user_id: int = Field(foreign_key="user.id")
|
||||
type: str # "telegram" or "webhook"
|
||||
name: str
|
||||
config: dict[str, Any] = Field(default_factory=dict, sa_column=Column(JSON))
|
||||
created_at: datetime = Field(default_factory=_utcnow)
|
||||
|
||||
|
||||
class MessageTemplate(SQLModel, table=True):
|
||||
"""Jinja2 message template."""
|
||||
|
||||
__tablename__ = "message_template"
|
||||
|
||||
id: int | None = Field(default=None, primary_key=True)
|
||||
user_id: int = Field(foreign_key="user.id")
|
||||
name: str
|
||||
body: str = Field(default="")
|
||||
is_default: bool = Field(default=False)
|
||||
created_at: datetime = Field(default_factory=_utcnow)
|
||||
|
||||
|
||||
class AlbumTracker(SQLModel, table=True):
|
||||
"""Album change tracker configuration."""
|
||||
|
||||
__tablename__ = "album_tracker"
|
||||
|
||||
id: int | None = Field(default=None, primary_key=True)
|
||||
user_id: int = Field(foreign_key="user.id")
|
||||
server_id: int = Field(foreign_key="immich_server.id")
|
||||
name: str
|
||||
album_ids: list[str] = Field(default_factory=list, sa_column=Column(JSON))
|
||||
event_types: list[str] = Field(
|
||||
default_factory=lambda: ["assets_added"],
|
||||
sa_column=Column(JSON),
|
||||
)
|
||||
target_ids: list[int] = Field(default_factory=list, sa_column=Column(JSON))
|
||||
template_id: int | None = Field(default=None, foreign_key="message_template.id")
|
||||
scan_interval: int = Field(default=60) # seconds
|
||||
enabled: bool = Field(default=True)
|
||||
quiet_hours_start: str | None = None # "HH:MM"
|
||||
quiet_hours_end: str | None = None # "HH:MM"
|
||||
created_at: datetime = Field(default_factory=_utcnow)
|
||||
|
||||
|
||||
class AlbumState(SQLModel, table=True):
|
||||
"""Persisted album state for change detection across restarts."""
|
||||
|
||||
__tablename__ = "album_state"
|
||||
|
||||
id: int | None = Field(default=None, primary_key=True)
|
||||
tracker_id: int = Field(foreign_key="album_tracker.id")
|
||||
album_id: str
|
||||
asset_ids: list[str] = Field(default_factory=list, sa_column=Column(JSON))
|
||||
pending_asset_ids: list[str] = Field(default_factory=list, sa_column=Column(JSON))
|
||||
last_updated: datetime = Field(default_factory=_utcnow)
|
||||
|
||||
|
||||
class EventLog(SQLModel, table=True):
|
||||
"""Log of detected album change events."""
|
||||
|
||||
__tablename__ = "event_log"
|
||||
|
||||
id: int | None = Field(default=None, primary_key=True)
|
||||
tracker_id: int = Field(foreign_key="album_tracker.id")
|
||||
event_type: str
|
||||
album_id: str
|
||||
album_name: str
|
||||
details: dict[str, Any] = Field(default_factory=dict, sa_column=Column(JSON))
|
||||
created_at: datetime = Field(default_factory=_utcnow)
|
||||
Reference in New Issue
Block a user