Add time-based day progress bar to dashboards
Replaces block-count progress with a wall-clock progress bar driven by configurable day start/end hours on each schedule template. - ScheduleTemplate: add day_start_time / day_end_time (TIME, nullable) - Startup migration: idempotent ALTER TABLE for existing DBs - Dashboard snapshot: includes day_start_time / day_end_time from template - Admin → Schedules: time pickers in block editor to set day hours - Dashboard view: time-based progress bar with start/current/end labels - TV view: full-width day progress strip between header and main content Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,8 @@ from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.exc import OperationalError
|
||||
|
||||
from app.config import get_settings
|
||||
from app.database import engine
|
||||
@@ -12,11 +14,23 @@ from app.websocket.manager import manager
|
||||
settings = get_settings()
|
||||
|
||||
|
||||
async def _add_column_if_missing(conn, table: str, column: str, definition: str):
|
||||
"""Add a column to a table, silently ignoring if it already exists (MySQL 1060)."""
|
||||
try:
|
||||
await conn.execute(text(f"ALTER TABLE {table} ADD COLUMN {column} {definition}"))
|
||||
except OperationalError as e:
|
||||
if e.orig.args[0] != 1060: # 1060 = Duplicate column name
|
||||
raise
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
# Create tables on startup (Alembic handles migrations in prod, this is a safety net)
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
# Idempotent column additions for schema migrations
|
||||
await _add_column_if_missing(conn, "schedule_templates", "day_start_time", "TIME NULL")
|
||||
await _add_column_if_missing(conn, "schedule_templates", "day_end_time", "TIME NULL")
|
||||
yield
|
||||
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ class ScheduleTemplate(TimestampMixin, Base):
|
||||
)
|
||||
name: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
is_default: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
day_start_time: Mapped[time | None] = mapped_column(Time, nullable=True)
|
||||
day_end_time: Mapped[time | None] = mapped_column(Time, nullable=True)
|
||||
|
||||
user: Mapped["User"] = relationship("User", back_populates="schedule_templates") # noqa: F821
|
||||
child: Mapped["Child | None"] = relationship("Child") # noqa: F821
|
||||
|
||||
@@ -11,7 +11,7 @@ from sqlalchemy.orm import selectinload
|
||||
|
||||
from app.dependencies import get_db
|
||||
from app.models.child import Child
|
||||
from app.models.schedule import ScheduleBlock
|
||||
from app.models.schedule import ScheduleBlock, ScheduleTemplate
|
||||
from app.models.session import DailySession, TimerEvent
|
||||
from app.schemas.session import DashboardSnapshot
|
||||
|
||||
@@ -41,6 +41,8 @@ async def get_dashboard(child_id: int, db: AsyncSession = Depends(get_db)):
|
||||
blocks = []
|
||||
completed_ids = []
|
||||
block_elapsed_seconds = 0
|
||||
day_start_time = None
|
||||
day_end_time = None
|
||||
|
||||
if session and session.template_id:
|
||||
blocks_result = await db.execute(
|
||||
@@ -50,6 +52,14 @@ async def get_dashboard(child_id: int, db: AsyncSession = Depends(get_db)):
|
||||
)
|
||||
blocks = blocks_result.scalars().all()
|
||||
|
||||
template_result = await db.execute(
|
||||
select(ScheduleTemplate).where(ScheduleTemplate.id == session.template_id)
|
||||
)
|
||||
template = template_result.scalar_one_or_none()
|
||||
if template:
|
||||
day_start_time = template.day_start_time
|
||||
day_end_time = template.day_end_time
|
||||
|
||||
events_result = await db.execute(
|
||||
select(TimerEvent).where(
|
||||
TimerEvent.session_id == session.id,
|
||||
@@ -88,4 +98,6 @@ async def get_dashboard(child_id: int, db: AsyncSession = Depends(get_db)):
|
||||
blocks=blocks,
|
||||
completed_block_ids=completed_ids,
|
||||
block_elapsed_seconds=block_elapsed_seconds,
|
||||
day_start_time=day_start_time,
|
||||
day_end_time=day_end_time,
|
||||
)
|
||||
|
||||
@@ -42,6 +42,8 @@ async def create_template(
|
||||
name=body.name,
|
||||
child_id=body.child_id,
|
||||
is_default=body.is_default,
|
||||
day_start_time=body.day_start_time,
|
||||
day_end_time=body.day_end_time,
|
||||
)
|
||||
db.add(template)
|
||||
await db.flush() # get template.id before adding blocks
|
||||
|
||||
@@ -27,6 +27,8 @@ class ScheduleTemplateCreate(BaseModel):
|
||||
name: str
|
||||
child_id: int | None = None
|
||||
is_default: bool = False
|
||||
day_start_time: time | None = None
|
||||
day_end_time: time | None = None
|
||||
blocks: list[ScheduleBlockCreate] = []
|
||||
|
||||
|
||||
@@ -34,6 +36,8 @@ class ScheduleTemplateUpdate(BaseModel):
|
||||
name: str | None = None
|
||||
child_id: int | None = None
|
||||
is_default: bool | None = None
|
||||
day_start_time: time | None = None
|
||||
day_end_time: time | None = None
|
||||
|
||||
|
||||
class ScheduleTemplateOut(BaseModel):
|
||||
@@ -41,6 +45,8 @@ class ScheduleTemplateOut(BaseModel):
|
||||
name: str
|
||||
child_id: int | None
|
||||
is_default: bool
|
||||
day_start_time: time | None
|
||||
day_end_time: time | None
|
||||
blocks: list[ScheduleBlockOut] = []
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from datetime import date, datetime
|
||||
from datetime import date, datetime, time
|
||||
from pydantic import BaseModel
|
||||
from app.schemas.schedule import ScheduleBlockOut
|
||||
from app.schemas.child import ChildOut
|
||||
@@ -43,3 +43,5 @@ class DashboardSnapshot(BaseModel):
|
||||
blocks: list[ScheduleBlockOut] = []
|
||||
completed_block_ids: list[int] = []
|
||||
block_elapsed_seconds: int = 0 # seconds already elapsed for the current block
|
||||
day_start_time: time | None = None
|
||||
day_end_time: time | None = None
|
||||
|
||||
Reference in New Issue
Block a user