feat: Actions system — scheduled mutations on external services
Full-stack implementation of provider-scoped Actions with extensible executor architecture. First action type: Immich auto_organize (sort assets into albums by person, CLIP search, date range, favorites). Core: - ActionTypeDefinition registry + ActionExecutor ABC with execute/validate/dry-run - ImmichActionExecutor with multi-album support and client-side filtering - ImmichClient write methods: add/remove assets, create album, paginated search Server: - Action, ActionRule, ActionExecution DB models - Full CRUD API + manual execute + dry-run + execution history endpoints - APScheduler integration (interval + cron) for automated execution - Action type discovery API + provider people endpoint Frontend: - Actions page with CRUD, execute/dry-run buttons, inline rule editor - RuleEditor: person/album MultiEntitySelect pickers, criteria config - ExecutionHistory: expandable per-rule result details - MultiEntitySelect reusable component (searchable multi-pick palette) - Notification tracker album picker migrated to MultiEntitySelect - Fixed MdiIcon race condition (icons missing after cache-clearing reload)
This commit is contained in:
@@ -470,6 +470,60 @@ class EventLog(SQLModel, table=True):
|
||||
created_at: datetime = Field(default_factory=_utcnow)
|
||||
|
||||
|
||||
class Action(SQLModel, table=True):
|
||||
"""A scheduled action that mutates an external service."""
|
||||
|
||||
__tablename__ = "action"
|
||||
|
||||
id: int | None = Field(default=None, primary_key=True)
|
||||
user_id: int = Field(foreign_key="user.id")
|
||||
provider_id: int = Field(foreign_key="service_provider.id")
|
||||
name: str
|
||||
icon: str = Field(default="")
|
||||
action_type: str # e.g. "auto_organize"
|
||||
config: dict[str, Any] = Field(default_factory=dict, sa_column=Column(JSON))
|
||||
schedule_type: str = Field(default="interval") # "interval" or "cron"
|
||||
schedule_interval: int = Field(default=3600) # seconds
|
||||
schedule_cron: str = Field(default="")
|
||||
enabled: bool = Field(default=False) # default disabled for safety
|
||||
last_run_at: datetime | None = Field(default=None)
|
||||
last_run_status: str = Field(default="") # "success", "partial", "failed", ""
|
||||
created_at: datetime = Field(default_factory=_utcnow)
|
||||
|
||||
|
||||
class ActionRule(SQLModel, table=True):
|
||||
"""One rule within an Action. Executed in order."""
|
||||
|
||||
__tablename__ = "action_rule"
|
||||
|
||||
id: int | None = Field(default=None, primary_key=True)
|
||||
action_id: int = Field(foreign_key="action.id", index=True)
|
||||
name: str = Field(default="")
|
||||
rule_config: dict[str, Any] = Field(default_factory=dict, sa_column=Column(JSON))
|
||||
enabled: bool = Field(default=True)
|
||||
order: int = Field(default=0)
|
||||
created_at: datetime = Field(default_factory=_utcnow)
|
||||
|
||||
|
||||
class ActionExecution(SQLModel, table=True):
|
||||
"""Log of an action execution (scheduled, manual, or dry-run)."""
|
||||
|
||||
__tablename__ = "action_execution"
|
||||
|
||||
id: int | None = Field(default=None, primary_key=True)
|
||||
action_id: int = Field(foreign_key="action.id", index=True)
|
||||
started_at: datetime = Field(default_factory=_utcnow)
|
||||
finished_at: datetime | None = Field(default=None)
|
||||
status: str = Field(default="running") # "running", "success", "partial", "failed"
|
||||
rules_processed: int = Field(default=0)
|
||||
rules_succeeded: int = Field(default=0)
|
||||
rules_failed: int = Field(default=0)
|
||||
total_items_affected: int = Field(default=0)
|
||||
summary: dict[str, Any] = Field(default_factory=dict, sa_column=Column(JSON))
|
||||
error: str = Field(default="")
|
||||
trigger: str = Field(default="scheduled") # "scheduled", "manual", "dry_run"
|
||||
|
||||
|
||||
class AppSetting(SQLModel, table=True):
|
||||
"""Key-value app-level settings (admin-configurable)."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user