Build and push server image / build-and-push (push) Successful in 42s
calendar_feed.py/calendar_render.py: fetch/merge per-user ICS feeds, render agenda/week/month views. manage_overlay.py: composites the manage-button overlay server-side (QR, battery, location/date, share-QR, face labels), reused by every render mode. device.py/common.py wire both together: mode dispatch for /frame/image+advance+back, and the &manage=1 flag. Plus UI (frame_config.html Calendar card, settings calendar URL field) and the icalendar/recurring-ical-events deps.
83 lines
3.3 KiB
Python
83 lines
3.3 KiB
Python
"""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") |