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:
IndieStatusBot 2026-04-25 05:00:00 +00:00
commit 902133edd3
4655 changed files with 1342691 additions and 0 deletions

84
app/api/subscribers.py Normal file
View file

@ -0,0 +1,84 @@
"""Subscribers API endpoints."""
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.dependencies import get_db, verify_api_key
from app.models.models import Subscriber
router = APIRouter()
@router.get("/")
async def list_subscribers(db: AsyncSession = Depends(get_db)):
"""List all subscribers."""
result = await db.execute(select(Subscriber))
subscribers = result.scalars().all()
return [
{
"id": s.id,
"email": s.email,
"is_confirmed": s.is_confirmed,
"created_at": s.created_at.isoformat() if s.created_at else None,
}
for s in subscribers
]
@router.post("/", status_code=status.HTTP_201_CREATED)
async def create_subscriber(
email: str,
db: AsyncSession = Depends(get_db),
api_key: str = Depends(verify_api_key),
):
"""Add a new subscriber."""
import uuid
subscriber = Subscriber(
email=email,
confirm_token=str(uuid.uuid4()),
)
db.add(subscriber)
await db.flush()
await db.refresh(subscriber)
return {
"id": subscriber.id,
"email": subscriber.email,
"confirm_token": subscriber.confirm_token,
}
@router.delete("/{subscriber_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_subscriber(
subscriber_id: UUID,
db: AsyncSession = Depends(get_db),
api_key: str = Depends(verify_api_key),
):
"""Remove a subscriber."""
result = await db.execute(select(Subscriber).where(Subscriber.id == str(subscriber_id)))
subscriber = result.scalar_one_or_none()
if not subscriber:
raise HTTPException(status_code=404, detail="Subscriber not found")
await db.delete(subscriber)
@router.post("/{subscriber_id}/confirm")
async def confirm_subscriber(
subscriber_id: UUID,
token: str,
db: AsyncSession = Depends(get_db),
):
"""Confirm a subscriber's email address."""
result = await db.execute(select(Subscriber).where(Subscriber.id == str(subscriber_id)))
subscriber = result.scalar_one_or_none()
if not subscriber:
raise HTTPException(status_code=404, detail="Subscriber not found")
if subscriber.confirm_token != token:
raise HTTPException(status_code=400, detail="Invalid confirmation token")
subscriber.is_confirmed = True
subscriber.confirm_token = None
await db.flush()
return {"message": "Subscriber confirmed", "email": subscriber.email}