Add Meeting system subject and notification system
- Auto-create a locked "Meeting" subject for every user on registration and seed it for all existing users on startup - Meeting subject cannot be deleted or renamed (is_system flag) - 5-minute corner toast warning on Dashboard and TV with live countdown, dismiss button, and 1-minute re-notify if dismissed - At start time: full-screen TV overlay with 30-second auto-dismiss, automatic pause of running block, switch to Meeting block, and auto-start of Meeting timer - Web Audio API chimes: rising on warnings, falling at meeting start - Update README with Meeting subject and notification system docs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,12 +2,15 @@ from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy import select, text
|
||||
from sqlalchemy.exc import OperationalError
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.config import get_settings
|
||||
from app.database import engine
|
||||
from app.models import Base
|
||||
from app.models.subject import Subject
|
||||
from app.models.user import User
|
||||
from app.routers import auth, users, children, subjects, schedules, sessions, logs, dashboard
|
||||
from app.routers import morning_routine, break_activity, admin
|
||||
from app.websocket.manager import manager
|
||||
@@ -35,6 +38,27 @@ async def lifespan(app: FastAPI):
|
||||
await _add_column_if_missing(conn, "schedule_blocks", "break_time_minutes", "INT NULL")
|
||||
await _add_column_if_missing(conn, "children", "strikes", "INT NOT NULL DEFAULT 0")
|
||||
await _add_column_if_missing(conn, "users", "timezone", "VARCHAR(64) NOT NULL DEFAULT 'UTC'")
|
||||
await _add_column_if_missing(conn, "subjects", "is_system", "TINYINT(1) NOT NULL DEFAULT 0")
|
||||
|
||||
# Seed Meeting system subject for any existing users who don't have one
|
||||
from app.database import AsyncSessionLocal
|
||||
async with AsyncSessionLocal() as db:
|
||||
users_result = await db.execute(select(User).where(User.is_active == True))
|
||||
all_users = users_result.scalars().all()
|
||||
for user in all_users:
|
||||
existing = await db.execute(
|
||||
select(Subject).where(Subject.user_id == user.id, Subject.is_system == True)
|
||||
)
|
||||
if not existing.scalar_one_or_none():
|
||||
db.add(Subject(
|
||||
user_id=user.id,
|
||||
name="Meeting",
|
||||
icon="📅",
|
||||
color="#6366f1",
|
||||
is_system=True,
|
||||
))
|
||||
await db.commit()
|
||||
|
||||
yield
|
||||
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ class Subject(TimestampMixin, Base):
|
||||
color: Mapped[str] = mapped_column(String(7), default="#10B981") # hex color
|
||||
icon: Mapped[str] = mapped_column(String(10), default="📚") # emoji
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
is_system: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||
|
||||
user: Mapped["User"] = relationship("User", back_populates="subjects") # noqa: F821
|
||||
schedule_blocks: Mapped[list["ScheduleBlock"]] = relationship( # noqa: F821
|
||||
|
||||
@@ -11,6 +11,7 @@ from app.auth.jwt import (
|
||||
)
|
||||
from app.dependencies import get_db
|
||||
from app.models.user import User
|
||||
from app.models.subject import Subject
|
||||
from app.schemas.auth import LoginRequest, RegisterRequest, TokenResponse
|
||||
from app.schemas.user import UserOut
|
||||
|
||||
@@ -39,6 +40,16 @@ async def register(body: RegisterRequest, response: Response, db: AsyncSession =
|
||||
await db.commit()
|
||||
await db.refresh(user)
|
||||
|
||||
meeting = Subject(
|
||||
user_id=user.id,
|
||||
name="Meeting",
|
||||
icon="📅",
|
||||
color="#6366f1",
|
||||
is_system=True,
|
||||
)
|
||||
db.add(meeting)
|
||||
await db.commit()
|
||||
|
||||
access = create_access_token({"sub": str(user.id)})
|
||||
refresh = create_refresh_token({"sub": str(user.id)})
|
||||
response.set_cookie(REFRESH_COOKIE, refresh, **COOKIE_OPTS)
|
||||
|
||||
@@ -101,6 +101,8 @@ async def delete_subject(
|
||||
subject = result.scalar_one_or_none()
|
||||
if not subject:
|
||||
raise HTTPException(status_code=404, detail="Subject not found")
|
||||
if subject.is_system:
|
||||
raise HTTPException(status_code=403, detail="System subjects cannot be deleted")
|
||||
await db.delete(subject)
|
||||
await db.commit()
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ class SubjectOut(BaseModel):
|
||||
color: str
|
||||
icon: str
|
||||
is_active: bool
|
||||
is_system: bool = False
|
||||
options: list[SubjectOptionOut] = []
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
Reference in New Issue
Block a user