From c0232fdcfe346f4521c0b5aa578b3ac178bcdc6f Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Wed, 27 May 2026 22:29:51 +0200 Subject: [PATCH] refactor(automations): park manual trigger pending Run-now redesign Manual-as-a-standalone-trigger conflates "user clicks Run now" with the trigger model and forces ad-hoc input plumbing on the caller. Remove the unreachable surface so the tree reflects reality (schedule is the only v1 trigger). - Unregister `manual`: drop import from triggers/__init__.py - Delete `app/automations/triggers/manual/` - Drop `RunService.dispatch_manual` (RunService is now read-only) - Drop `POST /automations/{id}/run` and `RunDispatched` schema - Keep `TriggerType.MANUAL` Python + PG enum value (reserved, documented) to avoid an Alembic round-trip when Run-now is redesigned --- surfsense_backend/app/automations/api/run.py | 33 +-------- .../persistence/enums/trigger_type.py | 7 +- .../automations/persistence/models/trigger.py | 2 +- .../app/automations/schemas/api/__init__.py | 3 +- .../app/automations/schemas/api/run.py | 9 +-- .../app/automations/services/run.py | 25 +------ .../app/automations/services/trigger.py | 2 +- .../app/automations/triggers/__init__.py | 4 +- .../automations/triggers/manual/__init__.py | 11 --- .../automations/triggers/manual/definition.py | 15 ---- .../automations/triggers/manual/dispatch.py | 72 ------------------- .../app/automations/triggers/manual/params.py | 9 --- surfsense_backend/app/routes/__init__.py | 2 +- 13 files changed, 18 insertions(+), 176 deletions(-) delete mode 100644 surfsense_backend/app/automations/triggers/manual/__init__.py delete mode 100644 surfsense_backend/app/automations/triggers/manual/definition.py delete mode 100644 surfsense_backend/app/automations/triggers/manual/dispatch.py delete mode 100644 surfsense_backend/app/automations/triggers/manual/params.py diff --git a/surfsense_backend/app/automations/api/run.py b/surfsense_backend/app/automations/api/run.py index d0d4bbfb7..b662a5943 100644 --- a/surfsense_backend/app/automations/api/run.py +++ b/surfsense_backend/app/automations/api/run.py @@ -1,42 +1,15 @@ -"""HTTP routes for automation runs (dispatch + history).""" +"""HTTP routes for automation run history.""" from __future__ import annotations -from typing import Any +from fastapi import APIRouter, Depends, Query -from fastapi import APIRouter, Body, Depends, Query, status - -from app.automations.schemas.api import ( - RunDetail, - RunDispatched, - RunList, - RunSummary, -) +from app.automations.schemas.api import RunDetail, RunList, RunSummary from app.automations.services import RunService, get_run_service router = APIRouter() -@router.post( - "/automations/{automation_id}/run", - response_model=RunDispatched, - status_code=status.HTTP_202_ACCEPTED, -) -async def run_automation_now( - automation_id: int, - inputs: dict[str, Any] | None = Body(default=None), - service: RunService = Depends(get_run_service), -) -> RunDispatched: - """Fire a manual run. - - ``inputs`` is the runtime payload supplied by the caller; it is merged with - the manual trigger's ``static_inputs`` (static wins) and validated against - the automation's input schema. - """ - run = await service.dispatch_manual(automation_id=automation_id, runtime_inputs=inputs) - return RunDispatched(run_id=run.id, status=run.status) - - @router.get( "/automations/{automation_id}/runs", response_model=RunList, diff --git a/surfsense_backend/app/automations/persistence/enums/trigger_type.py b/surfsense_backend/app/automations/persistence/enums/trigger_type.py index 8318bfdee..a583b1bd6 100644 --- a/surfsense_backend/app/automations/persistence/enums/trigger_type.py +++ b/surfsense_backend/app/automations/persistence/enums/trigger_type.py @@ -1,4 +1,9 @@ -"""Trigger-kind discriminator. v1: schedule | manual; webhook/event in Phase 2/3.""" +"""Trigger-kind discriminator. + +v1 only registers ``schedule``. ``manual`` is reserved in the enum (mirrors the +postgres enum) but is intentionally unregistered pending a redesign of the +"Run now" UX. +""" from __future__ import annotations diff --git a/surfsense_backend/app/automations/persistence/models/trigger.py b/surfsense_backend/app/automations/persistence/models/trigger.py index f73a8f350..de1078acf 100644 --- a/surfsense_backend/app/automations/persistence/models/trigger.py +++ b/surfsense_backend/app/automations/persistence/models/trigger.py @@ -56,7 +56,7 @@ class AutomationTrigger(BaseModel, TimestampMixin): # Precomputed next fire moment in UTC; advanced after each fire by the # schedule tick. NULL means the trigger has never been scheduled (the - # tick self-heals on first sight). Manual triggers leave this NULL. + # tick self-heals on first sight). next_fire_at = Column(TIMESTAMP(timezone=True), nullable=True) automation = relationship("Automation", back_populates="triggers") diff --git a/surfsense_backend/app/automations/schemas/api/__init__.py b/surfsense_backend/app/automations/schemas/api/__init__.py index a8a010a2c..f49e5c589 100644 --- a/surfsense_backend/app/automations/schemas/api/__init__.py +++ b/surfsense_backend/app/automations/schemas/api/__init__.py @@ -9,7 +9,7 @@ from .automation import ( AutomationSummary, AutomationUpdate, ) -from .run import RunDetail, RunDispatched, RunList, RunSummary +from .run import RunDetail, RunList, RunSummary from .trigger import TriggerCreate, TriggerDetail, TriggerUpdate __all__ = [ @@ -19,7 +19,6 @@ __all__ = [ "AutomationSummary", "AutomationUpdate", "RunDetail", - "RunDispatched", "RunList", "RunSummary", "TriggerCreate", diff --git a/surfsense_backend/app/automations/schemas/api/run.py b/surfsense_backend/app/automations/schemas/api/run.py index 42ea7ac14..3f6eaab82 100644 --- a/surfsense_backend/app/automations/schemas/api/run.py +++ b/surfsense_backend/app/automations/schemas/api/run.py @@ -1,4 +1,4 @@ -"""Response schemas for run sub-resources and run dispatch.""" +"""Response schemas for run sub-resources.""" from __future__ import annotations @@ -40,10 +40,3 @@ class RunList(BaseModel): items: list[RunSummary] total: int - - -class RunDispatched(BaseModel): - """Response of a successful run dispatch.""" - - run_id: int - status: RunStatus diff --git a/surfsense_backend/app/automations/services/run.py b/surfsense_backend/app/automations/services/run.py index 92d79e9bc..ac9970241 100644 --- a/surfsense_backend/app/automations/services/run.py +++ b/surfsense_backend/app/automations/services/run.py @@ -1,46 +1,25 @@ -"""``RunService`` — dispatch and history of automation runs.""" +"""``RunService`` — read-only access to automation run history.""" from __future__ import annotations -from typing import Any - from fastapi import Depends, HTTPException from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession -from app.automations.dispatch import DispatchError from app.automations.persistence.models.automation import Automation from app.automations.persistence.models.run import AutomationRun -from app.automations.triggers.manual import dispatch_manual_run from app.db import Permission, User, get_async_session from app.users import current_active_user from app.utils.rbac import check_permission class RunService: - """Lifecycle of the ``AutomationRun`` resource.""" + """Read-only access to ``AutomationRun`` history.""" def __init__(self, *, session: AsyncSession, user: User) -> None: self.session = session self.user = user - async def dispatch_manual( - self, - *, - automation_id: int, - runtime_inputs: dict[str, Any] | None, - ) -> AutomationRun: - """Fire a manual run via the registered manual trigger.""" - await self._authorize(automation_id, Permission.AUTOMATIONS_EXECUTE.value) - try: - return await dispatch_manual_run( - session=self.session, - automation_id=automation_id, - runtime_inputs=runtime_inputs, - ) - except DispatchError as exc: - raise HTTPException(status_code=422, detail=str(exc)) from exc - async def list( self, *, diff --git a/surfsense_backend/app/automations/services/trigger.py b/surfsense_backend/app/automations/services/trigger.py index 33e9c1386..c76cc0740 100644 --- a/surfsense_backend/app/automations/services/trigger.py +++ b/surfsense_backend/app/automations/services/trigger.py @@ -69,7 +69,7 @@ class TriggerService: trigger.enabled = data["enabled"] # Recompute next_fire_at when schedule timing changed or the trigger was - # toggled back on. Manual triggers always have NULL next_fire_at. + # toggled back on. if trigger.type == TriggerType.SCHEDULE: trigger.next_fire_at = _initial_next_fire( trigger.type, trigger.params, trigger.enabled diff --git a/surfsense_backend/app/automations/triggers/__init__.py b/surfsense_backend/app/automations/triggers/__init__.py index 258b2fda9..d7abb6b5d 100644 --- a/surfsense_backend/app/automations/triggers/__init__.py +++ b/surfsense_backend/app/automations/triggers/__init__.py @@ -1,6 +1,6 @@ """Triggers domain: registry surface + built-in trigger packages. -Each trigger lives in its own subpackage (``manual/``, ``schedule/``, ...) and +Each trigger lives in its own subpackage (``schedule/``, ...) and self-registers at import time via its ``definition`` module. """ @@ -17,4 +17,4 @@ __all__ = [ ] # Built-in triggers self-register at import time. -from . import manual, schedule # noqa: E402, F401 +from . import schedule # noqa: E402, F401 diff --git a/surfsense_backend/app/automations/triggers/manual/__init__.py b/surfsense_backend/app/automations/triggers/manual/__init__.py deleted file mode 100644 index 65cca9270..000000000 --- a/surfsense_backend/app/automations/triggers/manual/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -"""``manual`` trigger: fired by a user clicking ``Run now``.""" - -from __future__ import annotations - -from .dispatch import dispatch_manual_run -from .params import ManualTriggerParams - -__all__ = ["ManualTriggerParams", "dispatch_manual_run"] - -# Side-effect: register on the triggers store. -from . import definition # noqa: E402, F401 diff --git a/surfsense_backend/app/automations/triggers/manual/definition.py b/surfsense_backend/app/automations/triggers/manual/definition.py deleted file mode 100644 index 5a3529116..000000000 --- a/surfsense_backend/app/automations/triggers/manual/definition.py +++ /dev/null @@ -1,15 +0,0 @@ -"""``manual`` ``TriggerDefinition`` registration.""" - -from __future__ import annotations - -from ..store import register_trigger -from ..types import TriggerDefinition -from .params import ManualTriggerParams - -MANUAL_TRIGGER = TriggerDefinition( - type="manual", - description="Fire on a user-initiated 'Run now' invocation.", - params_model=ManualTriggerParams, -) - -register_trigger(MANUAL_TRIGGER) diff --git a/surfsense_backend/app/automations/triggers/manual/dispatch.py b/surfsense_backend/app/automations/triggers/manual/dispatch.py deleted file mode 100644 index 6c92317d0..000000000 --- a/surfsense_backend/app/automations/triggers/manual/dispatch.py +++ /dev/null @@ -1,72 +0,0 @@ -"""Manual ``Run now`` dispatch adapter: load + guard, then call generic dispatch.""" - -from __future__ import annotations - -from typing import Any - -from sqlalchemy import select -from sqlalchemy.ext.asyncio import AsyncSession - -from app.automations.dispatch import DispatchError, dispatch_run -from app.automations.persistence.enums.automation_status import AutomationStatus -from app.automations.persistence.enums.trigger_type import TriggerType -from app.automations.persistence.models.automation import Automation -from app.automations.persistence.models.run import AutomationRun -from app.automations.persistence.models.trigger import AutomationTrigger - - -async def dispatch_manual_run( - *, - session: AsyncSession, - automation_id: int, - runtime_inputs: dict[str, Any] | None, -) -> AutomationRun: - """Find the automation + its enabled manual trigger, then run the generic dispatch. - - ``runtime_inputs`` is the caller-supplied payload (e.g. an HTTP body for a - "Run now" API call); it is merged with the trigger's ``static_inputs`` by - the generic dispatcher, with static winning on key collision. - """ - automation = await _load_automation(session, automation_id) - if automation is None: - raise DispatchError(f"automation {automation_id} not found") - - if automation.status != AutomationStatus.ACTIVE: - raise DispatchError( - f"automation {automation_id} is {automation.status.value}, not active" - ) - - trigger = await _find_manual_trigger(session, automation_id) - if trigger is None: - raise DispatchError( - f"automation {automation_id} has no enabled manual trigger" - ) - - return await dispatch_run( - session=session, - automation=automation, - trigger=trigger, - runtime_inputs=runtime_inputs, - ) - - -async def _load_automation( - session: AsyncSession, automation_id: int -) -> Automation | None: - stmt = select(Automation).where(Automation.id == automation_id) - return (await session.execute(stmt)).scalar_one_or_none() - - -async def _find_manual_trigger( - session: AsyncSession, automation_id: int -) -> AutomationTrigger | None: - stmt = ( - select(AutomationTrigger) - .where( - AutomationTrigger.automation_id == automation_id, - AutomationTrigger.type == TriggerType.MANUAL, - AutomationTrigger.enabled.is_(True), - ) - .limit(1) - ) - return (await session.execute(stmt)).scalar_one_or_none() diff --git a/surfsense_backend/app/automations/triggers/manual/params.py b/surfsense_backend/app/automations/triggers/manual/params.py deleted file mode 100644 index 577655086..000000000 --- a/surfsense_backend/app/automations/triggers/manual/params.py +++ /dev/null @@ -1,9 +0,0 @@ -"""``ManualTriggerParams`` — params for the ``manual`` trigger (empty in v1).""" - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - - -class ManualTriggerParams(BaseModel): - model_config = ConfigDict(extra="forbid") diff --git a/surfsense_backend/app/routes/__init__.py b/surfsense_backend/app/routes/__init__.py index 64c8c6585..ef1c9312a 100644 --- a/surfsense_backend/app/routes/__init__.py +++ b/surfsense_backend/app/routes/__init__.py @@ -120,4 +120,4 @@ router.include_router(youtube_router) # YouTube playlist resolution router.include_router(prompts_router) router.include_router(memory_router) # User personal memory (memory.md style) router.include_router(team_memory_router) # Search-space team memory -router.include_router(automations_router) # Automations (manual run-now) +router.include_router(automations_router) # Automations CRUD + run history