Prima Get an API key

Module kit / Calendar and booking

Embed Widget Delivery

calendar-booking/embed-widget-delivery

Kind backend
Domain calendar-booking
Maturity prod
Reusable extract
Stack fastapi-0.118, sqlalchemy-2.0-sync, postgres, pydantic-2.12, starlette-0.48
Depends on _shared/fastapi-project-layout, _shared/auth-dependency-pattern, _shared/test-layout
Frontends calendar-booking/ui/calendar-settings-panel
Reference implementation
  • alchemy_labs/backend/api/routers/calendar_router.py
  • alchemy_labs/backend/models/calendar_integration_model.py
  • alchemy_labs/backend/services/forms/style_presets.py
  • alchemy_labs/backend/services/calendar_integration_service.py
  • alchemy_labs/backend/api/middleware/security.py
  • alchemy_labs/backend/api/dependencies.py
  • alchemy_labs/backend/tests/v2/coverage/test_booking_g4_onboarding_workshop_booking_embed_widget_style_presets_calendar_router_.py
Measured on alchemy_labs main 62ba4fc41009b454e1c033e5036c7e8577db2061 2026-09-12
Version 1

Purpose

GET /api/calendar/embed/{slug} server-renders a complete, self-contained HTML page (inline <style>, an inline window.BOOKING_CONFIG script tag, a <script> load of /static/booking.js) that a customer drops into an <iframe> or loads directly, so a booking widget for one meeting type works without the customer's site shipping any JS of its own (get_embed_widget, calendar_router.py 1943 to 2176). The invariant that makes it trustworthy is that styling is resolved from the workspace's own brand colors (BrandEssence) through one shared preset generator (generate_brand_style_presets, style_presets.py 151 to 229) rather than from anything the embedding page supplies, so a widget on an arbitrary third-party domain cannot be made to render brand colors, fonts or copy it wasn't configured with; the only per-request levers are ?theme=, ?preset= and ?integration_id=, all read server-side, never trusted HTML or CSS from the caller.

When to use

Read it before touching get_embed_widget, _get_calendar_style_preset, _embed_error_page, or get_calendar_style_presets; before changing what embed_settings.allowed_domains blocks or ?preset=/?theme= resolve to; when someone asks "why does the widget show the wrong colors", "why is embedding blocked on this domain", "why does the widget 403 in an iframe", or "where do the four design presets come from". Trigger phrases: "embed widget", "booking widget", "BOOKING_CONFIG", "style preset", "allowed_domains", "allow_embed", "minimal_dark", "frame-ancestors".

Not this recipe: POST /public/book/{slug} (calendar_router.py 1487 to 1656), the booking-submission endpoint the widget's injected JS eventually calls, a neighbouring recipe; GET /public/meeting-type/{slug} and GET /public/availability/{slug} (1351 to 1452), the JSON APIs the same public surface exposes, this recipe only covers the server-rendered HTML shell and its pre-fetch of the first 8 weeks of availability; the onboarding-workshop booking flow (/onboarding/workshop-info/{token}, /onboarding/availability/{token}, /onboarding/book/{token}, 2249 to 2485), which shares this router and this recipe's own reference test file but is a fixed, token-gated, non-embeddable flow, not this recipe's subject; booking.js, the static client script window.BOOKING_CONFIG is built for, not in this reference commit's diff and not read for this recipe; Meta CAPI conversion firing on a completed booking (fire_booking_conversion, 1585 to 1610), downstream of the widget, not part of serving it.

Stack assumptions

At 62ba4fc: fastapi==0.118.2, synchronous handlers on sqlalchemy.orm.Session (SQLAlchemy==2.0.43), starlette==0.48.0 (the ASGI middleware this recipe's CORS/framing behavior runs on), pydantic==2.12.0, psycopg2-binary==2.9.10 (alchemy_labs/backend/requirements.txt). Foundations assumed: S4 (fastapi-project-layout) for mount_router and the app's dependency-injection style; S-auth (auth-dependency-pattern) for the CurrentUser/AdminUser/DatabaseSession typed dependencies this router imports (calendar_router.py 25 to 27) and that /admin/style-presets is gated on; S-tests (test-layout) for the pytest fixture layout (client, authenticated_client, admin_client, db_session) the reference test reuses from tests/conftest.py rather than redefining.

Not assumed: _shared/alembic-migrations - this recipe adds no migration of its own, booking_embed_analytics and booking_meeting_types.embed_settings both already exist in baseline_prod_schema_001.py; _shared/workspace-scoping - the public embed path does not derive a workspace from a session at all (Security & tenancy), and the one admin route in this recipe (/admin/style-presets) is platform-wide, not per-workspace, so the usual workspace-derivation pattern doesn't apply here; _shared/error-envelope - deliberately not followed on the friendly-error branches (Gotchas).

Data model

No table or column is added, changed, or migrated by this recipe. Two existing structures are read or written:

booking_meeting_types.embed_settings (JSONB, calendar_integration_model.py 229 to 237), the only per-meeting-type config this recipe reads:

embed_settings DEFAULT {
  "allow_embed": true,
  "allowed_domains": [],
  "theme": "light",
  "accent_color": "#776ADD",
  "show_branding": true,
  "redirect_url": null,
  "custom_css": null
}

get_embed_widget reads allow_embed, allowed_domains, accent_color, and an undocumented style_config key (2016 to 2017) as a fallback style source; show_branding, redirect_url and custom_css are stored on every meeting type but this reference commit's embed HTML never reads any of the three (grep -n "show_branding\|redirect_url\|custom_css" calendar_router.py matches only the default dict above), so the widget always shows branding, never redirects after booking, and never applies custom_css - dead config, not wired to a rendering effect.

booking_embed_analytics (BookingEmbedAnalytics, calendar_integration_model.py 379 to 407), one row per widget session, the full funnel this recipe's scope names:

booking_embed_analytics (
  id                    uuid PRIMARY KEY,
  meeting_type_id       uuid NOT NULL REFERENCES booking_meeting_types(id) ON DELETE CASCADE,
  referrer_domain       varchar(255),
  referrer_url          text,
  session_id            varchar(100),
  viewed_at             timestamptz DEFAULT now(),
  slots_loaded_at       timestamptz,
  slot_selected_at      timestamptz,
  booking_completed_at  timestamptz,
  booking_id            uuid REFERENCES calendar_bookings(id),
  device_type           varchar(50),
  browser               varchar(100),
  ip_address            inet
)

What is genuinely not determinable from this commit, stated plainly rather than invented: nothing writes to this table. git grep -n "BookingEmbedAnalytics" across backend/ at 62ba4fc returns only the relationship declaration (BookingMeetingType.embed_analytics, 249) and the class body itself; get_embed_widget never inserts a row on view, and create_public_booking never inserts or updates one on completion. The schema for a view-to-booking funnel exists and is part of the prod baseline schema (baseline_prod_schema_001.py); no code path in this repo populates it. The only funnel signal that is actually persisted is coarser and lives on the booking itself: CalendarBooking.source is set to BookingSource.EMBED.value when "embed" in data.referrer_url.lower() (calendar_router.py 1537 to 1541), a substring match on whatever URL the client-side JS sent as referrer_url, not a structured event stream and not gated by this recipe's own widget-serving code.

API surface

RouteAuthTenancyStatus codesNotes
GET /api/calendar/embed/{slug} (get_embed_widget, 1943 to 1951)none (public)integration_id query param if given, else the single platform-level integration (calendar_service.get_platform_integration)200 always for "not found"/"not allowed" (friendly HTML, see below); 403 "Domain not allowed" (plain HTTPException, 1995) when allowed_domains is set and the request's Origin/Referer host isn't in itQuery params: theme (light|dark, default light), preset (default|inverted|minimal_bright|minimal_dark, optional), integration_id (optional UUID)
GET /api/calendar/admin/style-presets (get_calendar_style_presets, 795 to 799)AdminUser (Annotated[User, Depends(get_current_admin_user)], dependencies.py 568)platform-wide, no workspace scoping (one BrandEssence row queried with no filter, 816)200 {default, inverted, minimal_bright, minimal_dark}; 401 no session; 403 authenticated but not adminSame generate_brand_style_presets call get_embed_widget uses internally, exposed so an admin UI can preview all four before picking one

Two response shapes on get_embed_widget, deliberately different: an unknown/disallowed slug or a missing platform integration returns 200 with a friendly-HTML error page (_embed_error_page, 1870 to 1936, or a bespoke "Buchung nicht verfügbar" page at 1964 to 1967) meant to render inside the customer's iframe without looking broken; an origin the meeting type's allowed_domains list rejects returns a plain 403 (1995), because that case is a security boundary, not a content-availability one, and the reference test (test_embed_origin_not_allowed_403) asserts the distinction explicitly. Neither path uses the platform's own JSON error envelope (_shared/error-envelope); this router returns hand-built HTML strings or a bare HTTPException(status_code=403, detail=...).

Application logic

get_embed_widget, in the order the function runs it (1943 to 2176):

  1. Resolve the meeting type: by (integration_id, slug) via calendar_service.get_meeting_type_by_slug if integration_id is given (1959 to 1960, service.py 500 to 511, itself filtered to is_active == True), else by the single platform-level integration via calendar_service.get_platform_integration (1962, service.py 413 to 419, filtered to workspace_id IS NULL, provider == 'google', is_active == True) then the same slug lookup against it (1968). No integration configured, or no meeting type at that slug, returns the 200 friendly-error page (1963 to 1974).
  2. embed_settings.allow_embed gate: False returns the 200 "Einbettung nicht erlaubt" page (1976 to 1981); default is True when the key is absent.
  3. embed_settings.allowed_domains gate, only when the list is non-empty: takes request.headers["origin"] or falls back to request.headers["referer"] (1986 to 1988), parses it with urllib.parse.urlparse and compares parsed.netloc against the list verbatim (1990 to 1995). No entry in the list, or neither header present while the list is non-empty, raises HTTPException(403, "Domain not allowed"). An empty allowed_domains list (the default) skips this check entirely, any origin may embed.
  4. Resolve style_config, in this exact priority order (1997 to 2024, the function's own comment states the priority): (a) ?preset= query param, or theme=dark mapped to the minimal_dark preset name for backward compatibility (2003 to 2006), generated via _get_calendar_style_preset; (b) failing that, a style_config object stored directly in embed_settings (2016 to 2017); (c) failing that, the default preset auto-generated from the workspace's BrandEssence colors, wrapped in its own try/except so a brand-lookup failure never blocks the widget from rendering (2019 to 2024, only logger.debug, not warning or error, on that path).
  5. Pre-fetch availability for the next 56 days (start_date = today in FALLBACK_CLIENT_TIMEZONE, default Europe/Zurich, end_date = start_date + 56) via calendar_service.get_available_slots (2031 to 2057, service.py 517 onward), wrapped in try/except so a Google Calendar failure degrades to an empty availability object rather than a 500; keeps only the first 5 dates that have at least one slot (2059 to 2066).
  6. Render the HTML: colors/fonts/text come from style_config when resolved, else a theme-keyed light/dark fallback pair hardcoded in the function (2079 to 2088); window.BOOKING_CONFIG is built as an f-string-interpolated JS object literal containing baseUrl, slug, integrationId, the meeting type's name/duration/booking_questions, theme, accentColor, the full styleConfig, and preloadedAvailability (2151 to 2165); a <script> tag appends /static/booking.js?v=<epoch-ms> for cache-busting (2167 to 2170).

_get_calendar_style_preset (2179 to 2224): tries to load the single BrandEssence row and pull primary_colors/secondary_colors/typography; no row, or any exception reading it, falls back to [meeting_type.embed_settings.accent_color or meeting_type.color] as the sole primary color (2202 to 2214) - the meeting type's own accent, not a brand color, becomes the entire palette basis in that fallback. Calls generate_brand_style_presets (style_presets.py 151 to 229) once and returns presets.get(preset_name), so an unrecognized preset_name returns None silently rather than raising, which sends get_embed_widget down to its next fallback in the priority chain above.

generate_brand_style_presets (style_presets.py 151 to 229): derives brand_white/brand_black from the lightest/darkest secondary_colors when their luminance clears 0.85/is under 0.3 respectively (_find_brand_white_black, 119 to 148), else #FFFFFF/#111827; builds four fixed presets (default, inverted, minimal_bright, minimal_dark) by calling _base_style (53 to 109) with different primary/background/surface/text combinations per preset (171 to 214), then overrides shadowEnabled/shadow per preset afterward (217 to 227). Each preset is a ~30-key dict (fontFamily, primaryColor, inputBorderRadius, buttonPadding, shadow, etc., 75 to 109) meant to be applied as CSS custom properties by the client-side widget script, not by this server-rendered shell itself, which only reads backgroundColor/textColor/textSecondaryColor/fontFamily/fontUrl (2080 to 2083, 2028 to 2029) for its own inline <style> block.

Invariants

  • A caller never supplies HTML, CSS, or a style object directly: every color/font value that reaches the response comes from a named preset (generate_brand_style_presets) or from BrandEssence/meeting_type columns the platform admin set, never from a query string or request body (_get_calendar_style_preset, get_embed_widget 1997 to 2024; no test asserts this directly, inferred from the absence of any request-supplied style path in the source).
  • allow_embed: False and an unmatched allowed_domains entry are answered differently on purpose: the former is a 200 styled page, the latter a bare 403 (test_embed_disallowed_returns_html_error vs test_embed_origin_not_allowed_403).
  • Style resolution never raises past the widget: both the preset branch (2010 to 2013) and the brand-fallback branch (2021 to 2024) catch their own exceptions and fall through, so a broken BrandEssence row degrades styling, it does not break the embed (no dedicated test; read directly from the two try/except blocks).
  • Availability pre-fetch failure never blocks rendering: get_available_slots is called inside its own try/except (2048 to 2057), an exception leaves availability_data = {}} and the widget still renders with an empty preloadedAvailability (no dedicated test in the reference file; inferred from the try/except).
  • An empty allowed_domains list is "unrestricted", not "deny all" (allowed_domains truthiness gate at 1985; test_embed_happy_path_injects_booking_config uses the default empty list with no origin header and gets 200).

Frontend

The module that would render the admin side of this, by id:

calendar-booking/ui/calendar-settings-panel   the workspace-facing panel where an operator would flip
                                                               allow_embed, edit allowed_domains, and preview the
                                                               four style presets this recipe generates; at 62ba4fc
                                                               nothing in this repo's frontend calls
                                                               /api/calendar/admin/style-presets or reads
                                                               embed_settings from a settings screen (git grep for
                                                               the route path under frontend/src turns up nothing),
                                                               so this recipe has no confirmed UI consumer yet, only
                                                               the effect (the /embed/{slug} page itself) a customer's
                                                               own site iframes

Minimum admin/debug surface: GET /api/calendar/admin/style-presets itself is the only introspection endpoint this recipe ships (Admin can see all four generated presets without hitting /embed/{slug} with every ?preset= value by hand); there is no endpoint to list which meeting types have embedding enabled, which domains are allow-listed, or how many times a widget has been viewed, consistent with booking_embed_analytics having no writer (Data model).

Security & tenancy

Auth dependency: GET /embed/{slug} has none, it is an intentionally public, unauthenticated surface (no CurrentUser/AdminUser dependency in its signature, 1944 to 1950); GET /admin/style-presets requires AdminUser (Annotated[User, Depends(get_current_admin_user)], dependencies.py 568), confirmed by test_style_presets_requires_auth (401 anonymous) and test_style_presets_requires_admin (403 authenticated non-admin). Neither route uses DatabaseSessionWithRLS (dependencies.py 573); both take the plain DatabaseSession (get_db, no row-level-security context), so tenant isolation on the embed path is entirely the application-level (integration_id, slug) filter in get_meeting_type_by_slug, not a database policy.

Tenancy derivation: integration_id is an optional, caller-supplied query parameter on the public route (1950) - when present it is trusted as-is to scope the meeting-type lookup (1959 to 1960), there is no check that the caller is allowed to address that integration, because the only thing gated is which meeting type gets served, not workspace data; when absent, tenancy collapses to "the one platform-level integration" (get_platform_integration, workspace_id IS NULL).

Origin control: allowed_domains is an exact-string match against urlparse(origin_or_referer).netloc (1990 to 1995) - no wildcard/subdomain support, no scheme check, and it trusts the client-sent Origin/Referer header, either of which an arbitrary HTTP client (not a browser) can set to any value, so this is a UX guard against accidental cross-site embedding, not a hardened access control; a browser's own same-origin/CORS behavior is not what's enforced here, the check is manual string comparison in application code. Separately, SecurityHeadersMiddleware (api/middleware/security.py, mounted app_fastapi.py 507) matches path.startswith("/api/calendar/embed/") (55) into its is_public_embed set and sends Content-Security-Policy: frame-ancestors * for it (72 to 75) rather than the frame-ancestors 'none' / X-Frame-Options: DENY every other route gets (77 to 81) - by design, so the widget can be iframed from any customer domain; the code comment on that branch (68 to 71, 73 to 74) states explicitly that the per-widget domain check in calendar_router.py is what's relied on instead, not the CSP header, which matches the weak, header-spoofable check above.

Fail direction: unknown/blocked meeting type fails open toward a friendly page, not an error (200 either way); origin mismatch fails closed (403); style/availability fetch failures fail open toward "render with defaults" rather than failing the request (Invariants).

Credentials and PII at rest: the widget's rendered output embeds no credential (no token, no API key); meeting_type.booking_questions is echoed verbatim into window.BOOKING_CONFIG as JSON (2076, 2159), so any PII an admin put in a custom booking question's own text is visible to anyone who loads the embed, this is by design (it's the form the visitor is about to fill in) but worth naming. Rate limits: none on GET /embed/{slug} itself in this file; form_protection (honeypot/rate-limit/PoW) is only invoked on POST /public/book/{slug} (1508 to 1520), a neighbouring recipe, not on the widget-serving GET this recipe covers, so repeatedly requesting the embed page (and its availability pre-fetch, which does hit Google Calendar) is unthrottled at this layer.

Tests

All from alchemy_labs/backend/tests/v2/coverage/test_booking_g4_onboarding_workshop_booking_embed_widget_style_presets_calendar_router_.py (655 lines). The file is shared with the onboarding-workshop recipe (When to use); the tests genuinely scoped to this recipe:

  • test_embed_no_integration_returns_html_error (416) - no platform integration configured: 200, text/html, body contains "Buchung nicht verfügbar".
  • test_embed_unknown_slug_returns_html_error (424) - integration exists, slug doesn't: 200, text/html, "Termintyp nicht gefunden".
  • test_embed_disallowed_returns_html_error (432) - embed_settings={"allow_embed": False}: 200, "Einbettung nicht erlaubt".
  • test_embed_origin_not_allowed_403 (455) - allowed_domains: ["allowed.example"], request sent with origin: https://evil.example: 403, "Domain not allowed".
  • test_embed_happy_path_injects_booking_config (482) - valid slug, embedding allowed, calendar_service.get_calendar_service patched to a MagicMock reporting zero busy events (_fake_google_service_no_events, 208 to 213): 200, text/html, response body contains window.BOOKING_CONFIG, the meeting type's name, slug: 'essenz-workshop', and preloadedAvailability.
  • test_style_presets_requires_admin (506) - authenticated_client (non-admin test_user): 403.
  • test_style_presets_requires_auth (512) - anonymous client: 401.
  • test_style_presets_admin_returns_four_named_presets (517) - admin_client, no BrandEssence row (fallback colors): 200, body contains all four keys default/inverted/minimal_bright/minimal_dark, and default carries a backgroundColor key.

Hermetic vs live: every test here is hermetic against the scratch test database; the one test that reaches availability logic patches calendar_service.get_calendar_service so no live Google Calendar call is made. No live test exercises the actual origin-header spoofability or the CSP frame-ancestors * header this recipe's Security & tenancy section describes, those were read directly from security.py, not asserted by any test in this file (grep -n "frame-ancestors\|Content-Security-Policy" test_booking_g4_..._calendar_router_.py returns nothing).

What a new app adds and this suite lacks: no test exercises ?preset=/?theme= selecting a specific preset and asserting the resulting styleConfig JSON; no test exercises the style_config-stored-in-embed_settings fallback branch (2016 to 2017); no test touches booking_embed_analytics at all, consistent with nothing writing to it (Data model).

Deploy & ops

Config keys: FALLBACK_CLIENT_TIMEZONE (core/config.py, read at calendar_router.py 2033, defaults to Europe/Zurich via getattr if unset, never fails closed). No dedicated migration: booking_embed_analytics and booking_meeting_types.embed_settings both ship in baseline_prod_schema_001.py, this recipe adds no schema of its own. No scheduler job. Argus-style check: GET /api/calendar/embed/<any-configured-slug> returns 200 with text/html and window.BOOKING_CONFIG in the body; GET /api/calendar/admin/style-presets with an admin session returns 200 with the four preset keys. Rollback: stateless, nothing deployable beyond the router code itself, no data migration to unwind.

Gotchas

  • 2026-09-12 (measured at 62ba4fc): booking_embed_analytics is schema-complete (view/slots-loaded/slot-selected/booking-completed columns, a session_id, device/browser/IP) but has no writer anywhere in backend/ (git grep -n "BookingEmbedAnalytics" finds only the model file). A rebuild that assumes "the funnel table records the funnel" is wrong; today it records nothing. The only persisted funnel signal is CalendarBooking.source == "embed", set by a case-insensitive substring match on the client-supplied referrer_url (calendar_router.py 1540), at booking time only, with no corresponding "viewed" or "slots loaded" event ever recorded.
  • 2026-09-12 (measured at 62ba4fc): three embed_settings keys are stored and settable (show_branding, redirect_url, custom_css) but never read by get_embed_widget - grep -n "show_branding\|redirect_url\|custom_css" calendar_router.py matches only the model's own default dict, never a usage inside the function. An operator who sets custom_css or redirect_url on a meeting type sees no effect on the rendered widget.
  • 2026-09-12 (measured at 62ba4fc, security.py 52 to 59, 72 to 75): the embed route is explicitly exempted from the platform's default clickjacking protection (frame-ancestors * instead of 'none'), by design, since the whole point is third-party iframing; the only thing standing between that and an unwanted embed is the per-widget allowed_domains check in this recipe, which trusts a spoofable header (Security & tenancy). A rebuild that "fixes" the CSP without also hardening the domain check would not actually add protection, and one that removes the domain check while relying on CSP alone would break every legitimate customer embed.
  • 2026-09-12 (measured at 62ba4fc): _get_calendar_style_preset's BrandEssence lookup is db.query(BrandEssence).first() (2188, and again unscoped at 816 in the admin route) - a single, un-filtered row read with no workspace_id predicate. This recipe's styling is platform-wide brand, not per-workspace brand; a multi-tenant deployment where different workspaces need different embed styling is not what this code does today.

Reference implementation

Paths relative to the repo root; all at main 62ba4fc41009b454e1c033e5036c7e8577db2061 unless noted.

  • alchemy_labs/backend/api/routers/calendar_router.py (2485 lines): module docstring (1 to 8); router mount tag declaration (38); get_calendar_style_presets (795 to 839); _confirmation_page unrelated helper, not cited; _embed_error_page (1870 to 1936); get_embed_widget (1943 to 2176); _get_calendar_style_preset (2179 to 2224); create_public_booking's source classification, cited for context only (1537 to 1546).
  • alchemy_labs/backend/models/calendar_integration_model.py (439 lines): BookingSource enum (69 to 75); BookingMeetingType.embed_settings column and default (229 to 237); BookingMeetingType.embed_analytics relationship (249); CalendarBooking.source/referrer_url/referrer_domain (322 to 324); BookingEmbedAnalytics (379 to 407).
  • alchemy_labs/backend/services/forms/style_presets.py (229 lines, whole file in scope): hex_to_rgb/rgb_to_hex/lighten/darken/hex_to_rgba (7 to 41); get_google_font_url (44 to 50); _base_style (53 to 109); _is_light (112 to 116); _find_brand_white_black (119 to 148); generate_brand_style_presets (151 to 229).
  • alchemy_labs/backend/services/calendar_integration_service.py: get_platform_integration (413 to 419); get_meeting_type_by_slug (500 to 511); get_available_slots (517 to 804, the Google/iCal/CalDAV busy-period branch this recipe's pre-fetch calls into).
  • alchemy_labs/backend/api/middleware/security.py (115 lines, whole file in scope): SecurityHeadersMiddleware (14 to 115); the is_public_embed path match including /api/calendar/embed/ (52 to 59); the frame-ancestors * vs 'none' branch (72 to 81).
  • alchemy_labs/backend/api/dependencies.py: CurrentUser (565), AdminUser (568), OptionalUser (570), DatabaseSession (572), DatabaseSessionWithRLS (573).
  • alchemy_labs/backend/app_fastapi.py: SecurityHeadersMiddleware import (125) and mount (507); calendar_router mount at /api/calendar (845).
  • Tests: alchemy_labs/backend/tests/v2/coverage/test_booking_g4_onboarding_workshop_booking_embed_widget_style_presets_calendar_router_.py (655 lines), as listed under Tests.

Deviations of the reference from an ideal implementation, all dated 2026-09-12: booking_embed_analytics is unwired (Gotchas); three embed_settings keys are stored but unused; the origin check trusts a client-controlled header rather than enforcing anything at the network layer; brand-color lookup for styling is platform-wide, not per-workspace, despite the surrounding model being multi-tenant.

Acceptance checks

Run from alchemy_labs/backend at 62ba4fc (or an app built to this recipe) with a scratch Postgres migrated to head. Expected observations are exact, as measured 2026-09-12.

  1. grep -n "^def get_embed_widget" api/routers/calendar_router.py prints line 1944; grep -n "^def get_calendar_style_presets" api/routers/calendar_router.py prints line 796.
  2. grep -n "^def generate_brand_style_presets" services/forms/style_presets.py prints line 151, and python -c "from services.forms.style_presets import generate_brand_style_presets as g; p = g(['#776ADD']); print(sorted(p.keys()))" prints ['default', 'inverted', 'minimal_bright', 'minimal_dark'].
  3. python -m pytest tests/v2/coverage/test_booking_g4_onboarding_workshop_booking_embed_widget_style_presets_calendar_router_.py -q -k "embed or style_presets" --no-cov -o addopts="" -p no:cacheprovider collects and passes all 8 tests named under Tests (5 embed, 3 style_presets).
  4. git grep -n "BookingEmbedAnalytics" -- backend/ | grep -v "models/calendar_integration_model.py" prints nothing, confirming the funnel table has no writer outside its own model file (the claim in Data model and Gotchas).
  5. grep -n "frame-ancestors \*" api/middleware/security.py prints line 75; grep -n 'path.startswith("/api/calendar/embed/")' api/middleware/security.py prints line 55.
  6. grep -n "show_branding\|redirect_url\|custom_css" api/routers/calendar_router.py prints exactly the three keys inside the embed_settings default dict literal (no usage elsewhere in the file), confirming the dead-config claim in Gotchas.

Content synced from the prima-modules repository at 0df2ec8 on 2026-09-14. The recipe file is the source; this page is a rendering of it.