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
21
app/models/__init__.py
Normal file
21
app/models/__init__.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
from app.models.models import (
|
||||
Service,
|
||||
Incident,
|
||||
IncidentUpdate,
|
||||
Monitor,
|
||||
MonitorResult,
|
||||
Subscriber,
|
||||
NotificationLog,
|
||||
SiteSetting,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"Service",
|
||||
"Incident",
|
||||
"IncidentUpdate",
|
||||
"Monitor",
|
||||
"MonitorResult",
|
||||
"Subscriber",
|
||||
"NotificationLog",
|
||||
"SiteSetting",
|
||||
]
|
||||
151
app/models/models.py
Normal file
151
app/models/models.py
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import Boolean, DateTime, Integer, String, Text, ForeignKey, Float
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
def _uuid_str() -> str:
|
||||
return str(uuid.uuid4())
|
||||
|
||||
|
||||
class Service(Base):
|
||||
__tablename__ = "services"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=_uuid_str)
|
||||
name: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
slug: Mapped[str] = mapped_column(String(50), unique=True, nullable=False, index=True)
|
||||
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
group_name: Mapped[str | None] = mapped_column(String(50), nullable=True)
|
||||
position: Mapped[int] = mapped_column(Integer, default=0)
|
||||
is_visible: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow
|
||||
)
|
||||
|
||||
incidents: Mapped[list["Incident"]] = relationship(back_populates="service")
|
||||
monitors: Mapped[list["Monitor"]] = relationship(back_populates="service")
|
||||
|
||||
|
||||
class Incident(Base):
|
||||
__tablename__ = "incidents"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=_uuid_str)
|
||||
service_id: Mapped[str] = mapped_column(
|
||||
String(36), ForeignKey("services.id"), nullable=False, index=True
|
||||
)
|
||||
title: Mapped[str] = mapped_column(String(200), nullable=False)
|
||||
status: Mapped[str] = mapped_column(String(20), nullable=False, index=True)
|
||||
# investigating | identified | monitoring | resolved
|
||||
severity: Mapped[str] = mapped_column(String(20), nullable=False)
|
||||
# minor | major | outage
|
||||
started_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=datetime.utcnow)
|
||||
resolved_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow
|
||||
)
|
||||
|
||||
service: Mapped["Service"] = relationship(back_populates="incidents")
|
||||
updates: Mapped[list["IncidentUpdate"]] = relationship(
|
||||
back_populates="incident", cascade="all, delete-orphan"
|
||||
)
|
||||
notifications: Mapped[list["NotificationLog"]] = relationship(back_populates="incident")
|
||||
|
||||
|
||||
class IncidentUpdate(Base):
|
||||
__tablename__ = "incident_updates"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=_uuid_str)
|
||||
incident_id: Mapped[str] = mapped_column(
|
||||
String(36), ForeignKey("incidents.id"), nullable=False, index=True
|
||||
)
|
||||
status: Mapped[str] = mapped_column(String(20), nullable=False)
|
||||
body: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||||
|
||||
incident: Mapped["Incident"] = relationship(back_populates="updates")
|
||||
|
||||
|
||||
class Monitor(Base):
|
||||
__tablename__ = "monitors"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=_uuid_str)
|
||||
service_id: Mapped[str] = mapped_column(
|
||||
String(36), ForeignKey("services.id"), nullable=False, index=True
|
||||
)
|
||||
url: Mapped[str] = mapped_column(String(500), nullable=False)
|
||||
method: Mapped[str] = mapped_column(String(10), default="GET")
|
||||
expected_status: Mapped[int] = mapped_column(Integer, default=200)
|
||||
timeout_seconds: Mapped[int] = mapped_column(Integer, default=10)
|
||||
interval_seconds: Mapped[int] = mapped_column(Integer, default=60)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow
|
||||
)
|
||||
|
||||
service: Mapped["Service"] = relationship(back_populates="monitors")
|
||||
results: Mapped[list["MonitorResult"]] = relationship(
|
||||
back_populates="monitor", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
|
||||
class MonitorResult(Base):
|
||||
__tablename__ = "monitor_results"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=_uuid_str)
|
||||
monitor_id: Mapped[str] = mapped_column(
|
||||
String(36), ForeignKey("monitors.id"), nullable=False, index=True
|
||||
)
|
||||
status: Mapped[str] = mapped_column(String(20), nullable=False) # up | down | degraded
|
||||
response_time_ms: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
status_code: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
error_message: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
checked_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=datetime.utcnow)
|
||||
|
||||
monitor: Mapped["Monitor"] = relationship(back_populates="results")
|
||||
|
||||
|
||||
class Subscriber(Base):
|
||||
__tablename__ = "subscribers"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=_uuid_str)
|
||||
email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False, index=True)
|
||||
is_confirmed: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
confirm_token: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||||
|
||||
notifications: Mapped[list["NotificationLog"]] = relationship(back_populates="subscriber")
|
||||
|
||||
|
||||
class NotificationLog(Base):
|
||||
__tablename__ = "notification_logs"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=_uuid_str)
|
||||
incident_id: Mapped[str] = mapped_column(
|
||||
String(36), ForeignKey("incidents.id"), nullable=False, index=True
|
||||
)
|
||||
subscriber_id: Mapped[str] = mapped_column(
|
||||
String(36), ForeignKey("subscribers.id"), nullable=False
|
||||
)
|
||||
channel: Mapped[str] = mapped_column(String(20), nullable=False) # email | webhook
|
||||
status: Mapped[str] = mapped_column(String(20), nullable=False) # sent | failed
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||||
|
||||
incident: Mapped["Incident"] = relationship(back_populates="notifications")
|
||||
subscriber: Mapped["Subscriber"] = relationship(back_populates="notifications")
|
||||
|
||||
|
||||
class SiteSetting(Base):
|
||||
__tablename__ = "site_settings"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=_uuid_str)
|
||||
key: Mapped[str] = mapped_column(String(50), unique=True, nullable=False, index=True)
|
||||
value: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue