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 |
|
| 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
| Route | Auth | Tenancy | Status codes | Notes |
|---|---|---|---|---|
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 it | Query 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 admin | Same 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):
- Resolve the meeting type: by
(integration_id, slug)viacalendar_service.get_meeting_type_by_slugifintegration_idis given (1959 to 1960,service.py500 to 511, itself filtered tois_active == True), else by the single platform-level integration viacalendar_service.get_platform_integration(1962,service.py413 to 419, filtered toworkspace_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). embed_settings.allow_embedgate:Falsereturns the 200 "Einbettung nicht erlaubt" page (1976 to 1981); default isTruewhen the key is absent.embed_settings.allowed_domainsgate, only when the list is non-empty: takesrequest.headers["origin"]or falls back torequest.headers["referer"](1986 to 1988), parses it withurllib.parse.urlparseand comparesparsed.netlocagainst the list verbatim (1990 to 1995). No entry in the list, or neither header present while the list is non-empty, raisesHTTPException(403, "Domain not allowed"). An emptyallowed_domainslist (the default) skips this check entirely, any origin may embed.- Resolve
style_config, in this exact priority order (1997 to 2024, the function's own comment states the priority): (a)?preset=query param, ortheme=darkmapped to theminimal_darkpreset name for backward compatibility (2003 to 2006), generated via_get_calendar_style_preset; (b) failing that, astyle_configobject stored directly inembed_settings(2016 to 2017); (c) failing that, thedefaultpreset auto-generated from the workspace'sBrandEssencecolors, wrapped in its own try/except so a brand-lookup failure never blocks the widget from rendering (2019 to 2024, onlylogger.debug, notwarningorerror, on that path). - Pre-fetch availability for the next 56 days (
start_date= today inFALLBACK_CLIENT_TIMEZONE, defaultEurope/Zurich,end_date = start_date + 56) viacalendar_service.get_available_slots(2031 to 2057,service.py517 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). - Render the HTML: colors/fonts/text come from
style_configwhen resolved, else atheme-keyed light/dark fallback pair hardcoded in the function (2079 to 2088);window.BOOKING_CONFIGis built as an f-string-interpolated JS object literal containingbaseUrl,slug,integrationId, the meeting type's name/duration/booking_questions,theme,accentColor, the fullstyleConfig, andpreloadedAvailability(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 fromBrandEssence/meeting_typecolumns the platform admin set, never from a query string or request body (_get_calendar_style_preset,get_embed_widget1997 to 2024; no test asserts this directly, inferred from the absence of any request-supplied style path in the source). allow_embed: Falseand an unmatchedallowed_domainsentry are answered differently on purpose: the former is a 200 styled page, the latter a bare 403 (test_embed_disallowed_returns_html_errorvstest_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
BrandEssencerow 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_slotsis called inside its own try/except (2048 to 2057), an exception leavesavailability_data = {}}and the widget still renders with an emptypreloadedAvailability(no dedicated test in the reference file; inferred from the try/except). - An empty
allowed_domainslist is "unrestricted", not "deny all" (allowed_domainstruthiness gate at 1985;test_embed_happy_path_injects_booking_configuses the default empty list with nooriginheader 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 withorigin: https://evil.example: 403, "Domain not allowed".test_embed_happy_path_injects_booking_config(482) - valid slug, embedding allowed,calendar_service.get_calendar_servicepatched to aMagicMockreporting zero busy events (_fake_google_service_no_events, 208 to 213): 200,text/html, response body containswindow.BOOKING_CONFIG, the meeting type's name,slug: 'essenz-workshop', andpreloadedAvailability.test_style_presets_requires_admin(506) -authenticated_client(non-admintest_user): 403.test_style_presets_requires_auth(512) - anonymousclient: 401.test_style_presets_admin_returns_four_named_presets(517) -admin_client, noBrandEssencerow (fallback colors): 200, body contains all four keysdefault/inverted/minimal_bright/minimal_dark, anddefaultcarries abackgroundColorkey.
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_analyticsis schema-complete (view/slots-loaded/slot-selected/booking-completed columns, asession_id, device/browser/IP) but has no writer anywhere inbackend/(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 isCalendarBooking.source == "embed", set by a case-insensitive substring match on the client-suppliedreferrer_url(calendar_router.py1540), at booking time only, with no corresponding "viewed" or "slots loaded" event ever recorded. - 2026-09-12 (measured at 62ba4fc): three
embed_settingskeys are stored and settable (show_branding,redirect_url,custom_css) but never read byget_embed_widget-grep -n "show_branding\|redirect_url\|custom_css" calendar_router.pymatches only the model's own default dict, never a usage inside the function. An operator who setscustom_cssorredirect_urlon a meeting type sees no effect on the rendered widget. - 2026-09-12 (measured at 62ba4fc,
security.py52 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-widgetallowed_domainscheck 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'sBrandEssencelookup isdb.query(BrandEssence).first()(2188, and again unscoped at 816 in the admin route) - a single, un-filtered row read with noworkspace_idpredicate. 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_pageunrelated 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):BookingSourceenum (69 to 75);BookingMeetingType.embed_settingscolumn and default (229 to 237);BookingMeetingType.embed_analyticsrelationship (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); theis_public_embedpath match including/api/calendar/embed/(52 to 59); theframe-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:SecurityHeadersMiddlewareimport (125) and mount (507);calendar_routermount 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.
grep -n "^def get_embed_widget" api/routers/calendar_router.pyprints line1944;grep -n "^def get_calendar_style_presets" api/routers/calendar_router.pyprints line796.grep -n "^def generate_brand_style_presets" services/forms/style_presets.pyprints line151, andpython -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'].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:cacheprovidercollects and passes all 8 tests named under Tests (5embed, 3style_presets).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).grep -n "frame-ancestors \*" api/middleware/security.pyprints line75;grep -n 'path.startswith("/api/calendar/embed/")' api/middleware/security.pyprints line55.grep -n "show_branding\|redirect_url\|custom_css" api/routers/calendar_router.pyprints exactly the three keys inside theembed_settingsdefault 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.