37ccaa4fdc
Service organization (19 files moved into 6 subpackages): - services/instance/ — event_bus, health_monitor, lifecycle_hooks - services/config/ — config_profile_resolver - services/git/ — clone, git_operations, git_service - services/build/ — docker_build, manifest_compiler - services/terminal/ — terminal_manager, terminal_session - services/shared/ — correlation, file_service, notification_service, permission_fixer, readiness_probe, ssh_keys, tunnel, workspace_manager API router organization (16 files moved into 6 subpackages): - api/tool/ — tool_instances, tool_types, tool_definitions, tool_types_validation, sessions (extracted from tool_instances) - api/config/ — config_profiles, user_config - api/workspace/ — workspaces, workspace_files, workspace_git, workspace_instances - api/user/ — users, auth, ssh_keys - api/project/ — projects, git_repositories - api/system/ — health, events, notifications, dashboard, terminal, instance_proxy Updated main.py imports and all __init__.py re-exports. Sessions router extracted from tool_instances.py into api/tool/sessions.py. Quality gates: py_compile passed, ruff passed.
162 lines
5.0 KiB
Python
162 lines
5.0 KiB
Python
"""Notification API endpoints."""
|
|
|
|
import uuid
|
|
from datetime import datetime
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
|
from pydantic import BaseModel, ConfigDict, Field
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from src.auth.dependencies import get_current_user, get_db_session
|
|
from src.models.user import User
|
|
from src.models import UserConfig
|
|
from src.services.shared.notification_service import notification_service
|
|
|
|
router = APIRouter(prefix="/notifications", tags=["notifications"])
|
|
|
|
|
|
class NotificationItem(BaseModel):
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: uuid.UUID
|
|
user_id: uuid.UUID
|
|
category: str
|
|
severity: str
|
|
title: str
|
|
message: str | None
|
|
source_type: str | None
|
|
source_id: uuid.UUID | None
|
|
notification_metadata: dict = Field(serialization_alias="metadata")
|
|
read_at: datetime | None
|
|
dismissed_at: datetime | None
|
|
created_at: datetime
|
|
|
|
|
|
class NotificationListResponse(BaseModel):
|
|
items: list[NotificationItem]
|
|
total: int
|
|
limit: int
|
|
offset: int
|
|
|
|
|
|
class UnreadCountResponse(BaseModel):
|
|
count: int
|
|
|
|
|
|
class MarkAllReadResponse(BaseModel):
|
|
marked_count: int
|
|
|
|
|
|
class ClearAllResponse(BaseModel):
|
|
cleared_count: int
|
|
|
|
|
|
async def _get_mute_categories(
|
|
session: AsyncSession,
|
|
user_id: uuid.UUID,
|
|
) -> list[str]:
|
|
"""Read notification mute categories from user config."""
|
|
from sqlalchemy import select
|
|
|
|
result = await session.execute(
|
|
select(UserConfig).where(UserConfig.user_id == user_id)
|
|
)
|
|
config = result.scalar_one_or_none()
|
|
if config is None:
|
|
return []
|
|
mute_categories = config.config.get("notification_mute_categories", [])
|
|
if isinstance(mute_categories, list):
|
|
return mute_categories
|
|
return []
|
|
|
|
|
|
@router.get("", response_model=NotificationListResponse)
|
|
async def list_notifications(
|
|
limit: int = Query(20, ge=1, le=100),
|
|
offset: int = Query(0, ge=0),
|
|
unread_only: bool = Query(False),
|
|
user: User = Depends(get_current_user),
|
|
session: AsyncSession = Depends(get_db_session),
|
|
) -> NotificationListResponse:
|
|
"""List notifications for the authenticated user."""
|
|
mute_categories = await _get_mute_categories(session, user.id)
|
|
items, total = await notification_service.list_notifications(
|
|
session,
|
|
user.id,
|
|
limit=limit,
|
|
offset=offset,
|
|
unread_only=unread_only,
|
|
mute_categories=mute_categories,
|
|
)
|
|
return NotificationListResponse(
|
|
items=[NotificationItem.model_validate(item) for item in items],
|
|
total=total,
|
|
limit=limit,
|
|
offset=offset,
|
|
)
|
|
|
|
|
|
@router.get("/unread", response_model=UnreadCountResponse)
|
|
async def get_unread_count(
|
|
user: User = Depends(get_current_user),
|
|
session: AsyncSession = Depends(get_db_session),
|
|
) -> UnreadCountResponse:
|
|
"""Get unread notification count for the authenticated user."""
|
|
count = await notification_service.get_unread_count(session, user.id)
|
|
return UnreadCountResponse(count=count)
|
|
|
|
|
|
@router.patch("/{notification_id}/read", response_model=NotificationItem)
|
|
async def mark_notification_read(
|
|
notification_id: uuid.UUID,
|
|
user: User = Depends(get_current_user),
|
|
session: AsyncSession = Depends(get_db_session),
|
|
) -> NotificationItem:
|
|
"""Mark a single notification as read."""
|
|
try:
|
|
notification = await notification_service.mark_read(
|
|
session, notification_id, user.id
|
|
)
|
|
except ValueError as exc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Notification not found",
|
|
) from exc
|
|
return NotificationItem.model_validate(notification)
|
|
|
|
|
|
@router.post("/mark-all-read", response_model=MarkAllReadResponse)
|
|
async def mark_all_read(
|
|
user: User = Depends(get_current_user),
|
|
session: AsyncSession = Depends(get_db_session),
|
|
) -> MarkAllReadResponse:
|
|
"""Mark all unread notifications as read."""
|
|
marked = await notification_service.mark_all_read(session, user.id)
|
|
return MarkAllReadResponse(marked_count=marked)
|
|
|
|
|
|
@router.delete("", status_code=status.HTTP_200_OK)
|
|
async def clear_all_notifications(
|
|
user: User = Depends(get_current_user),
|
|
session: AsyncSession = Depends(get_db_session),
|
|
) -> ClearAllResponse:
|
|
"""Dismiss all notifications for the authenticated user."""
|
|
cleared = await notification_service.dismiss_all(session, user.id)
|
|
return ClearAllResponse(cleared_count=cleared)
|
|
|
|
|
|
@router.delete("/{notification_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
async def dismiss_notification(
|
|
notification_id: uuid.UUID,
|
|
user: User = Depends(get_current_user),
|
|
session: AsyncSession = Depends(get_db_session),
|
|
) -> None:
|
|
"""Soft-delete (dismiss) a single notification."""
|
|
try:
|
|
await notification_service.dismiss(session, notification_id, user.id)
|
|
except ValueError as exc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Notification not found",
|
|
) from exc
|