feat: status page enhancements

This commit is contained in:
Ubuntu 2026-04-25 12:14:06 +00:00
parent 158a6ee716
commit 44d353a30f
5 changed files with 561 additions and 14 deletions

View file

@ -1,4 +1,9 @@
"""Monitors API endpoints."""
"""Monitors API endpoints with tier enforcement.
When creating a monitor for an organization, tier enforcement checks:
- monitors_per_service: max number of monitors per service
- check_interval_min: minimum allowed check interval
"""
from uuid import UUID
@ -8,7 +13,13 @@ from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.dependencies import get_db, verify_api_key
from app.models.models import Monitor, MonitorResult
from app.models.models import Monitor, MonitorResult, Service
from app.models.saas_models import Organization
from app.services.tier_enforcement import (
enforce_monitor_limit,
validate_check_interval,
)
from app.services.tier_limits import TierLimitExceeded, get_tier_limits
router = APIRouter()
@ -20,6 +31,7 @@ class MonitorCreate(BaseModel):
expected_status: int = 200
timeout_seconds: int = Field(10, ge=1, le=60)
interval_seconds: int = Field(60, ge=30, le=3600)
organization_id: str | None = None # Optional: for tier enforcement
class MonitorUpdate(BaseModel):
@ -41,6 +53,7 @@ def serialize_monitor(m: Monitor) -> dict:
"timeout_seconds": m.timeout_seconds,
"interval_seconds": m.interval_seconds,
"is_active": m.is_active,
"organization_id": m.organization_id,
"created_at": m.created_at.isoformat() if m.created_at else None,
"updated_at": m.updated_at.isoformat() if m.updated_at else None,
}
@ -60,7 +73,43 @@ async def create_monitor(
db: AsyncSession = Depends(get_db),
api_key: str = Depends(verify_api_key),
):
"""Create a new monitor."""
"""Create a new monitor.
If organization_id is provided, tier enforcement is applied:
- check monitors_per_service limit
- validate check_interval against minimum for the tier
"""
# Look up org if provided
org = None
if data.organization_id:
result = await db.execute(
select(Organization).where(Organization.id == data.organization_id)
)
org = result.scalar_one_or_none()
if org is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Organization '{data.organization_id}' not found",
)
# Tier enforcement when org context is provided
if org is not None:
# Check monitors_per_service limit
await enforce_monitor_limit(db, org, str(data.service_id))
# Validate check interval
validate_check_interval(org, data.interval_seconds)
# Verify the service exists
service_result = await db.execute(
select(Service).where(Service.id == str(data.service_id))
)
service = service_result.scalar_one_or_none()
if not service:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Service '{data.service_id}' not found",
)
monitor = Monitor(
service_id=str(data.service_id),
url=data.url,
@ -68,6 +117,7 @@ async def create_monitor(
expected_status=data.expected_status,
timeout_seconds=data.timeout_seconds,
interval_seconds=data.interval_seconds,
organization_id=data.organization_id or (service.organization_id if service else None),
)
db.add(monitor)
await db.flush()
@ -115,13 +165,26 @@ async def update_monitor(
db: AsyncSession = Depends(get_db),
api_key: str = Depends(verify_api_key),
):
"""Update a monitor."""
"""Update a monitor.
If interval_seconds is being changed, validate against the org's tier minimum.
"""
result = await db.execute(select(Monitor).where(Monitor.id == str(monitor_id)))
monitor = result.scalar_one_or_none()
if not monitor:
raise HTTPException(status_code=404, detail="Monitor not found")
update_data = data.model_dump(exclude_unset=True)
# If updating interval_seconds and org context exists, validate
if data.interval_seconds is not None and monitor.organization_id:
org_result = await db.execute(
select(Organization).where(Organization.id == monitor.organization_id)
)
org = org_result.scalar_one_or_none()
if org:
validate_check_interval(org, data.interval_seconds)
for field, value in update_data.items():
setattr(monitor, field, value)