"""The per-frame HTML pages: Photos (/frames/{id}), Configuration, Calendar, 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, FrameCalendar, 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 _user_available_calendars(user: User) -> list[dict]: """This user's full set of calendars available to add to any frame: the single ICS subscription (if set) plus every CalDAV calendar last discovered from Settings' "Discover calendars" button. Doesn't hit the network -- reads the cached list a user refreshes themselves.""" calendars = [] if user.calendar_ics_url: calendars.append({"key": "ics", "label": "My calendar (ICS)"}) for c in (user.calendar_caldav_calendars or []): calendars.append({"key": f"caldav:{c['href']}", "label": c.get("display_name") or "Calendar"}) return calendars def _calendar_users_for_frame(db: Session, frame_id: int, viewer_id: int | None) -> list[dict]: """Per-linked-user calendar list for the Calendar tab's "Included calendars" section. The viewer's own row lists EVERY calendar they have available, each with a full add/remove toggle; every other linked user's row lists ONLY the calendars they've already included (mute-only for the viewer -- see api_frames.py's api_calendar_select: only a calendar's owner may turn it on, but anyone linked to the frame may turn one off).""" users = db.execute( select(User).join(UserFrame, UserFrame.user_id == User.id) .where(UserFrame.frame_id == frame_id).order_by(User.username) ).scalars().all() included_by_user: dict[int, list[FrameCalendar]] = {} for fc in db.execute(select(FrameCalendar).where(FrameCalendar.frame_id == frame_id)).scalars().all(): included_by_user.setdefault(fc.user_id, []).append(fc) result = [] for u in users: is_self = u.id == viewer_id if is_self: own_rows = {fc.calendar_key: fc for fc in included_by_user.get(u.id, [])} calendars = [ {**c, "included": own_rows[c["key"]].included if c["key"] in own_rows else False, "color_index": own_rows[c["key"]].color_index if c["key"] in own_rows else None} for c in _user_available_calendars(u) ] else: calendars = [ {"key": fc.calendar_key, "label": fc.calendar_label, "included": True} for fc in included_by_user.get(u.id, []) if fc.included ] result.append({ "user_id": u.id, "display_name": u.display_name or u.username, "is_self": is_self, "calendars": calendars, }) return result @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, ) WEEK_START_LABELS = {0: "Monday", 1: "Tuesday", 2: "Wednesday", 3: "Thursday", 4: "Friday", 5: "Saturday", 6: "Sunday"} @router.get("/frames/{frame_id}/calendar", response_class=HTMLResponse) def frame_calendar_page(frame_id: int, request: Request, db: Session = Depends(get_db)): viewer = current_user(request, db) return _frame_page( request, db, frame_id, "frame_calendar.html", "calendar", calendar_views=CALENDAR_VIEW_LABELS, calendar_users=_calendar_users_for_frame(db, frame_id, viewer.id if viewer else None), week_start_labels=WEEK_START_LABELS, calendar_color_labels=PALETTE_LABELS, default_palette_rgb=DEFAULT_PALETTE_RGB, palette_to_hex=palette_to_hex, ) @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")