feat: indie status page MVP -- FastAPI + SQLite
- 8 DB models (services, incidents, monitors, subscribers, etc.) - Full CRUD API for services, incidents, monitors - Public status page with live data - Incident detail page with timeline - API key authentication - Uptime monitoring scheduler - 13 tests passing - TECHNICAL_DESIGN.md with full spec
This commit is contained in:
commit
902133edd3
4655 changed files with 1342691 additions and 0 deletions
120
app/api/services.py
Normal file
120
app/api/services.py
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
"""Services API endpoints."""
|
||||
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.dependencies import get_db, verify_api_key
|
||||
from app.models.models import Service
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class ServiceCreate(BaseModel):
|
||||
name: str = Field(..., max_length=100)
|
||||
slug: str = Field(..., max_length=50, pattern=r"^[a-z0-9-]+$")
|
||||
description: str | None = None
|
||||
group_name: str | None = Field(None, max_length=50)
|
||||
position: int = 0
|
||||
is_visible: bool = True
|
||||
|
||||
|
||||
class ServiceUpdate(BaseModel):
|
||||
name: str | None = None
|
||||
slug: str | None = Field(None, max_length=50, pattern=r"^[a-z0-9-]+$")
|
||||
description: str | None = None
|
||||
group_name: str | None = None
|
||||
position: int | None = None
|
||||
is_visible: bool | None = None
|
||||
|
||||
|
||||
def serialize_service(s: Service) -> dict:
|
||||
return {
|
||||
"id": s.id,
|
||||
"name": s.name,
|
||||
"slug": s.slug,
|
||||
"description": s.description,
|
||||
"group_name": s.group_name,
|
||||
"position": s.position,
|
||||
"is_visible": s.is_visible,
|
||||
"created_at": s.created_at.isoformat() if s.created_at else None,
|
||||
"updated_at": s.updated_at.isoformat() if s.updated_at else None,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def list_services(db: AsyncSession = Depends(get_db)):
|
||||
"""List all services."""
|
||||
result = await db.execute(select(Service).order_by(Service.position, Service.name))
|
||||
services = result.scalars().all()
|
||||
return [serialize_service(s) for s in services]
|
||||
|
||||
|
||||
@router.post("/", status_code=status.HTTP_201_CREATED)
|
||||
async def create_service(
|
||||
data: ServiceCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
api_key: str = Depends(verify_api_key),
|
||||
):
|
||||
"""Create a new service."""
|
||||
service = Service(
|
||||
name=data.name,
|
||||
slug=data.slug,
|
||||
description=data.description,
|
||||
group_name=data.group_name,
|
||||
position=data.position,
|
||||
is_visible=data.is_visible,
|
||||
)
|
||||
db.add(service)
|
||||
await db.flush()
|
||||
await db.refresh(service)
|
||||
return serialize_service(service)
|
||||
|
||||
|
||||
@router.get("/{service_id}")
|
||||
async def get_service(service_id: UUID, db: AsyncSession = Depends(get_db)):
|
||||
"""Get a service by ID."""
|
||||
result = await db.execute(select(Service).where(Service.id == str(service_id)))
|
||||
service = result.scalar_one_or_none()
|
||||
if not service:
|
||||
raise HTTPException(status_code=404, detail="Service not found")
|
||||
return serialize_service(service)
|
||||
|
||||
|
||||
@router.patch("/{service_id}")
|
||||
async def update_service(
|
||||
service_id: UUID,
|
||||
data: ServiceUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
api_key: str = Depends(verify_api_key),
|
||||
):
|
||||
"""Update a service."""
|
||||
result = await db.execute(select(Service).where(Service.id == str(service_id)))
|
||||
service = result.scalar_one_or_none()
|
||||
if not service:
|
||||
raise HTTPException(status_code=404, detail="Service not found")
|
||||
|
||||
update_data = data.model_dump(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(service, field, value)
|
||||
|
||||
await db.flush()
|
||||
await db.refresh(service)
|
||||
return serialize_service(service)
|
||||
|
||||
|
||||
@router.delete("/{service_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_service(
|
||||
service_id: UUID,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
api_key: str = Depends(verify_api_key),
|
||||
):
|
||||
"""Delete a service."""
|
||||
result = await db.execute(select(Service).where(Service.id == str(service_id)))
|
||||
service = result.scalar_one_or_none()
|
||||
if not service:
|
||||
raise HTTPException(status_code=404, detail="Service not found")
|
||||
await db.delete(service)
|
||||
Loading…
Add table
Add a link
Reference in a new issue