"""The per-frame HTML pages: Photos (/frames/{id}), Configuration, and Stats tabs, all inside the sidebar app shell. Data loading happens client-side against /api/frames/{id}/... (routers/api_frames.py); these routes just authorize and render the scaffold.""" from __future__ import annotations from fastapi import APIRouter, Depends, HTTPException, Request from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.templating import Jinja2Templates from sqlalchemy import select from sqlalchemy.orm import Session from ..auth import can_view_frame, current_user from ..calendar_render import CALENDAR_VIEW_LABELS from ..db import get_db from ..image_pipeline import ( DEFAULT_PALETTE_RGB, DISPLAY_MODE_LABELS, PALETTE_LABELS, palette_to_hex, ) from ..models import Frame, User, UserFrame from ..quiet_hours import ALL_TIMEZONES from .common import shell_context router = APIRouter() templates = Jinja2Templates(directory="app/templates") def _frame_page(request: Request, db: Session, frame_id: int, template: str, tab: str, **extra): user = current_user(request, db) if user is None: return RedirectResponse(f"/login?next=/frames/{frame_id}", status_code=303) frame = db.get(Frame, frame_id) if frame is None or not can_view_frame(db, user, frame): raise HTTPException(404, "No such frame") ctx = shell_context(request, db, user, active_frame=frame) ctx.update({"frame": frame, "active_tab": tab, **extra}) return templates.TemplateResponse(template, ctx) @router.get("/frames/{frame_id}", response_class=HTMLResponse) def frame_photos_page(frame_id: int, request: Request, db: Session = Depends(get_db)): return _frame_page(request, db, frame_id, "frame_photos.html", "photos") def _calendar_users_for_frame(db: Session, frame_id: int) -> list[dict]: """Every user linked to this frame, their calendar opt-in state, and whether they even have a calendar URL set -- what the Configuration tab's "Included calendars" list needs. Whether a given row is *this* viewer's own (and therefore editable) is decided in the template, using the `user` shell_context already provides.""" rows = db.execute( select(User, UserFrame.calendar_included) .join(UserFrame, UserFrame.user_id == User.id) .where(UserFrame.frame_id == frame_id) .order_by(User.username) ).all() return [ {"user_id": u.id, "display_name": u.display_name or u.username, "has_url": bool(u.calendar_ics_url), "included": included} for u, included in rows ] @router.get("/frames/{frame_id}/config", response_class=HTMLResponse) def frame_config_page(frame_id: int, request: Request, db: Session = Depends(get_db)): return _frame_page( request, db, frame_id, "frame_config.html", "config", timezones=ALL_TIMEZONES, palette_labels=PALETTE_LABELS, default_palette_rgb=DEFAULT_PALETTE_RGB, palette_to_hex=palette_to_hex, display_mode_labels=DISPLAY_MODE_LABELS, calendar_views=CALENDAR_VIEW_LABELS, calendar_users=_calendar_users_for_frame(db, frame_id), ) @router.get("/frames/{frame_id}/stats", response_class=HTMLResponse) def frame_stats_page(frame_id: int, request: Request, db: Session = Depends(get_db)): return _frame_page(request, db, frame_id, "frame_stats.html", "stats")