Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@
- Fix all issues, including pre-existing ones unrelated to your changes. The codebase must always be warning-free.
- **Do not suppress lint warnings with ignore comments by default.** Always try to fix the issue properly first. Only add a suppress comment when the lint rule genuinely does not apply and a proper fix would be worse (e.g. less readable, wrong behavior). Include a clear justification in the comment.

## Audits

- **When fixing an item from any audit in `audits/`**, always mark it as fixed in the audit file (strikethrough title, add ✅ FIXED, add a note with what was done).

## Bug Fixes

- **When fixing a bug, always search the entire codebase for the same pattern** before considering the fix done. Proactively find and fix all similar occurrences.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""add indexes on attribute handler_id enabled and action handler_id

Revision ID: e21044117ef6
Revises: 96db4b158ec4
Create Date: 2026-03-09 09:13:01.929799

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = 'e21044117ef6'
down_revision: Union[str, Sequence[str], None] = '96db4b158ec4'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_index(op.f('ix_actions_handler_id'), 'actions', ['handler_id'], unique=False)
op.create_index(op.f('ix_attributes_enabled'), 'attributes', ['enabled'], unique=False)
op.create_index(op.f('ix_attributes_handler_id'), 'attributes', ['handler_id'], unique=False)
# ### end Alembic commands ###


def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_attributes_handler_id'), table_name='attributes')
op.drop_index(op.f('ix_attributes_enabled'), table_name='attributes')
op.drop_index(op.f('ix_actions_handler_id'), table_name='actions')
# ### end Alembic commands ###
33 changes: 9 additions & 24 deletions backend/app/api/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
from app.dependencies import CurrentUser, DbSession, HandlerManagerDep
from app.models.action import Action
from app.schemas.action import ActionCreate, ActionRead, ActionUpdate, ExecuteActionRequest
from app.socketio_app import sio
from app.socketio_app import emit_mutate
from app.utils.action_params import merge_params
from app.utils.db import get_or_404

router = APIRouter(prefix="/actions", tags=["actions"])

Expand All @@ -18,11 +19,7 @@ async def list_actions(db: DbSession, _current_user: CurrentUser):

@router.get("/{action_id}", response_model=ActionRead)
async def get_action(action_id: int, db: DbSession, _current_user: CurrentUser):
result = await db.execute(select(Action).where(Action.id == action_id))
action = result.scalar_one_or_none()
if not action:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Action not found")
return action
return await get_or_404(db, Action, action_id)


@router.post("/", response_model=ActionRead, status_code=status.HTTP_201_CREATED)
Expand All @@ -31,40 +28,31 @@ async def create_action(body: ActionCreate, db: DbSession, _current_user: Curren
db.add(action)
await db.commit()
await db.refresh(action)
await sio.emit("mutate", {"entity": "actions"})
await sio.emit("mutate", {"entity": "handlers"})
await emit_mutate("actions", "handlers")
return action


@router.patch("/{action_id}", response_model=ActionRead)
async def update_action(
action_id: int, body: ActionUpdate, db: DbSession, manager: HandlerManagerDep, _current_user: CurrentUser
):
result = await db.execute(select(Action).where(Action.id == action_id))
action = result.scalar_one_or_none()
if not action:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Action not found")
action = await get_or_404(db, Action, action_id)
for field, value in body.model_dump(exclude_unset=True).items():
setattr(action, field, value)
await db.commit()
await db.refresh(action)
manager.invalidate_action_cache(action_id)
await sio.emit("mutate", {"entity": "actions"})
await sio.emit("mutate", {"entity": "handlers"})
await emit_mutate("actions", "handlers")
return action


@router.delete("/{action_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_action(action_id: int, db: DbSession, manager: HandlerManagerDep, _current_user: CurrentUser):
result = await db.execute(select(Action).where(Action.id == action_id))
action = result.scalar_one_or_none()
if not action:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Action not found")
action = await get_or_404(db, Action, action_id)
await db.delete(action)
await db.commit()
manager.invalidate_action_cache(action_id)
await sio.emit("mutate", {"entity": "actions"})
await sio.emit("mutate", {"entity": "handlers"})
await emit_mutate("actions", "handlers")


@router.post("/{action_id}/execute")
Expand All @@ -75,10 +63,7 @@ async def execute_action(
_current_user: CurrentUser,
body: ExecuteActionRequest | None = None,
):
result = await db.execute(select(Action).where(Action.id == action_id))
action = result.scalar_one_or_none()
if not action:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Action not found")
action = await get_or_404(db, Action, action_id)
if not action.handler_id:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Action has no handler")
message = action.message
Expand Down
Loading