"""Public booking page blueprint (#17): the customer-facing /book/ page, embedded via iframe into a client's landing page. This route only resolves a slug to a client and renders the slot-grid/booking-form page around it -- all booking mutations still go through booking_api.py's JSON API (#16), which the page's own JS calls via fetch. IP rate limiting on these public routes is enforced at the Caddy layer (per #17's acceptance criteria), not in Flask -- see deploy/booking/RATE_LIMIT.md for the Caddy config to apply by hand on the homelab. """ from flask import Blueprint, abort, render_template, request import booking_db as bdb bp = Blueprint("public_booking", __name__) DEFAULT_BRAND_COLOR = "#0f8a7e" @bp.get("/book/") def book_page(slug): client = bdb.get_client_by_slug(slug) if client is None: abort(404) client_id = client["client_id"] locations = bdb.list_active_locations(client_id) active_location_ids = {l["location_id"] for l in locations} # A resource whose Filiale was deactivated shouldn't stay bookable even if # the resource row itself is still active. resources = [r for r in bdb.list_active_resources(client_id) if r["location_id"] in active_location_ids] services = bdb.list_active_services(client_id) if not locations or not resources or not services: # No bookable services/resources configured yet -- nothing to show a # customer rather than a broken/empty booking form. abort(404) return render_template( "book.html", client_id=client_id, business_name=client.get("business_name") or slug, locations=[{"location_id": l["location_id"], "name": l["name"]} for l in locations], resources=[{"resource_id": r["resource_id"], "name": r["name"], "location_id": r["location_id"]} for r in resources], services=[{"service_id": s["service_id"], "name": s["name"], "duration_minutes": s["duration_minutes"], "price": float(s["price"]) if s["price"] is not None else None} for s in services], brand_color=request.args.get("color") or DEFAULT_BRAND_COLOR, )