CU-16: Settings & customization: profile, cancellation policy, blocklist, branding, BYOK, pixels

Priority: P2 Accounts/sessions: P1 host only (ravikantguptaofficial@gmail.com, signed into Calendo at https://calendo.dev/dashboard/ via persisted Google session). No invitee account needed — blocklist test uses throwaway plus-alias emails. Parallel-safe: Yes — this suite only touches P1's own booking-page settings; it does not rewrite availability rules or create real third-party calendar events. Exclusive (rewrites global host availability?): No. Estimated time: 30 minutes. L3 reality checks: None. Everything in this suite is verifiable inside the Calendo UI, the public booking page DOM/source, and the dashboard. No real Google/Outlook calendar event or email is created. (BYOK uses a placeholder key, so the AI call is NOT exercised against the real Anthropic API — see Steps 30-34 and Manual residue.)

Goal

This suite proves that a host can customize how their booking experience looks and behaves, and that each setting actually takes effect for invitees. Specifically: the profile display name persists across reloads and shows on the public page; brand color and logo restyle the public booking page; a cancellation policy appears on both the confirmation page and the cancel page; an email blocklist actually rejects a matching invitee at booking time; a Bring-Your-Own-Key (BYOK) Anthropic key is stored write-only/masked (never echoed back) and flips the assistant to the host's own key; and Google Analytics / Meta Pixel IDs cause the corresponding tracking scripts to load on the public booking page. These are the trust-and-brand surface of the product — if a blocklist silently fails or a policy never shows, the host is exposed.

Preconditions

Test data

Pick a fresh RUNID at execution time, e.g. a UTC timestamp 20260601-1530. Embed it in everything created so reruns never collide.

IMPORTANT — restore baseline at the end: before changing anything, on the Settings tab record the EXISTING values of display name, brand color (#settingsBrandColorText), logo URL, cancellation policy, blocked emails, GA ID, Meta Pixel ID, and the BYOK status text. You will restore these in Cleanup so P1's real booking page is unchanged. Capture screenshot: cu16-baseline-settings.

Steps

A. Open settings and record baseline

  1. Action: Go to https://calendo.dev/dashboard/ and click the Settings sidebar tab (.sidebar-nav a[data-tab="settings"]). Expect: The settings panel becomes active (#panel-settings has class active) and the settings form (#settingsForm) is visible, including the Save button labeled "Save Changes" (#settingsSaveBtn), the AI API Key (BYOK) card (title text "AI API Key (BYOK)", input #byokKeyInput), and a Delete Account button (#deleteAccountBtn). [L1]
  2. Action: Read and record current values of: #settingsDisplayName, #settingsBrandColorText, #settingsLogoUrl, #settingsCancellationPolicy, #settingsBlockedEmails, #settingsGaMeasurementId, #settingsMetaPixelId, and the BYOK status line (#byokStatus, e.g. "Using shared API key." or "Your own API key is active."). Capture screenshot: cu16-baseline-settings. Expect: All values captured for later restore. [L1]

B. Profile display name persists across reload

  1. Action: Clear #settingsDisplayName and type CU16 Host <RUNID>. Click Save (#settingsSaveBtn). Expect: A toast "Settings saved" appears; the Save button returns from "Saving..." back to "Save Changes". [L1]
  2. Action: Reload the page (browser refresh) and re-open the Settings tab ([data-tab="settings"]). Expect: #settingsDisplayName still shows CU16 Host <RUNID>. Capture screenshot: cu16-name-persisted. [L2]

C. Timezone (profile) — NOTE: not on this form

  1. Action: Visually scan the Settings profile form for a timezone selector. Expect: There is no timezone field in the profile/settings form. The host's timezone is configured on the Availability schedule, not here. Record this observation. Then click the Availability sidebar tab ([data-tab="availability"]) and confirm a Timezone selector exists on the schedule (#newScheduleTimezone). Capture screenshot: cu16-timezone-location. Expect: The timezone control lives on the availability schedule. Do NOT change it (changing the schedule timezone could affect other suites' slot assumptions). [L1] Note this divergence from the coverage request in the results report (profile timezone is not editable on the settings form).

D. Cancellation policy → set, persist, and verify on confirmation + cancel pages

  1. Action: Back on the Settings tab, type the cancellation policy text CU16 policy <RUNID>: Cancellations must be made at least 24 hours in advance. into #settingsCancellationPolicy. Click Save (#settingsSaveBtn). Expect: "Settings saved" toast. [L1]
  2. Action: Reload, re-open Settings. Expect: #settingsCancellationPolicy still contains the policy text. [L2]

E. Brand color and logo → set and persist

  1. Action: On the Settings appearance card, set the brand color text field #settingsBrandColorText to #ff5500 (type it directly; the adjacent color swatch #settingsBrandColor should sync). Set #settingsLogoUrl to https://calendo.dev/calendo-logo-256.png. Click Save. Expect: "Settings saved" toast. [L1]
  2. Action: Reload, re-open Settings. Expect: #settingsBrandColorText reads #ff5500 and #settingsLogoUrl reads the logo URL. Capture screenshot: cu16-branding-persisted. [L2]

F. GA + Meta pixel IDs → set and persist

  1. Action: On the Settings tracking card, type the GA ID into #settingsGaMeasurementId (e.g. G-CU16TEST, record exact value) and the Meta Pixel ID into #settingsMetaPixelId (1600000000). Click Save. Expect: "Settings saved" toast. [L1]
  2. Action: Reload, re-open Settings. Expect: #settingsGaMeasurementId and #settingsMetaPixelId retain the exact values you typed. Capture screenshot: cu16-tracking-persisted. [L2]

G. Public booking page reflects name, welcome, brand color, logo

  1. Action: Open the public booking page in a new tab: https://calendo.dev/booking/?user=<HOST_SLUG> (HOST_SLUG from Preconditions). Wait for it to load. Expect: The booking view (#booking-view) renders and the host name element (#host-name) shows CU16 Host <RUNID>. [L2]
  2. Action: Inspect the host welcome text element (#host-welcome). Expect: It shows either a custom welcome message (if one is set on the page model) OR the default Welcome! Pick an event to get started.. NOTE: there is no welcome-message input on the dashboard settings form, so unless the welcome message was previously set via the AI assistant/API it will show the default. Record which you see. (Welcome-message editing is covered in Step 35 via the AI assistant.) [L1/L2]
  3. Action: Confirm the brand color took effect. In the page, read the computed value of the CSS custom property --brand on the document root (e.g. via DevTools: getComputedStyle(document.documentElement).getPropertyValue('--brand')) OR visually confirm primary buttons/accents are now orange rather than the default blue #0069ff. Expect: --brand equals #ff5500 (or visually orange). Capture screenshot: cu16-public-brandcolor. [L2]
  4. Action: Confirm the logo renders. Expect: A host avatar image (img.host-avatar) is present with src = the logo URL you set. Capture screenshot: cu16-public-logo. [L2]

H. Cancellation policy shows on the confirmation page and the cancel page

  1. Action: Still on the public booking page, select the test event (append &event=cu16-block-test-<RUNID> to the URL, or click the event in the list), pick the first available day (.calendar-day.available) and the first time slot (.time-slot), then fill the confirm form: name CU16 OK Guest <RUNID> (#input-name), email ravikantguptaofficial+ok-<RUNID>@gmail.com (#input-email). Click Confirm Booking (#btn-confirm). Expect: The confirmation view shows the title "Booking Confirmed" (.confirmation-title). [L1]
  2. Action: On the confirmation page, scroll to the details and look for the cancellation-policy callout (a yellow/amber box with a bold "Cancellation Policy" heading, inserted right after #confirmation-details). Expect: The box is present and contains your text CU16 policy <RUNID>: Cancellations must be made at least 24 hours in advance.. Capture screenshot: cu16-confirmation-policy. [L1/L2]
  3. Action: On the confirmation page, find the Cancel link in the manage section (#confirmation-manage a containing "Cancel"), and open it (this is https://calendo.dev/booking/cancel.html?...token...). On the cancel page, look for the cancellation policy text. Expect: The cancel page (cancel.html) renders a "Cancellation Policy:" line containing your policy text (it reads data.cancellation_policy). Capture screenshot: cu16-cancel-page-policy. Do NOT submit the cancellation yet — leave this booking active; you will cancel it in Cleanup, OR cancel it now and record that the booking is gone. [L1/L2]

I. Blocklist actually rejects a matching invitee

Order matters: the blocklist must be saved (Steps 19-20) BEFORE the blocked booking attempt, because enforcement is server-side at book time.

  1. Action: Go back to the dashboard Settings tab. In Blocked Emails (#settingsBlockedEmails), enter on two separate lines: ravikantguptaofficial+blocked-<RUNID>@gmail.com and @cu16blocked-<RUNID>.test. Click Save. Expect: "Settings saved" toast. [L1]
  2. Action: Reload, re-open Settings. Expect: #settingsBlockedEmails still contains both lines. [L2]
  3. Action: Open the public booking page for the test event: https://calendo.dev/booking/?user=<HOST_SLUG>&event=cu16-block-test-<RUNID>. Select first available day + time slot. Fill name CU16 Blocked Guest <RUNID> (#input-name) and the blocked email ravikantguptaofficial+blocked-<RUNID>@gmail.com (#input-email). Click Confirm Booking (#btn-confirm). Expect: The booking is REJECTED. An inline error message near the confirm button appears reading "This email is not allowed to book" (the server returns HTTP 403; the page shows data.error). The confirmation view does NOT appear. Capture screenshot: cu16-blocklist-rejected-exact. [L1/L2]
  4. Action: Without leaving, change the email field to a blocked-domain address someone@cu16blocked-<RUNID>.test and click Confirm again. Expect: Same rejection — "This email is not allowed to book" — confirming domain-pattern (@domain) blocking works. Capture screenshot: cu16-blocklist-rejected-domain. [L1/L2]
  5. Action: Now change the email to the allowed address ravikantguptaofficial+ok2-<RUNID>@gmail.com (a non-blocked address; reselect a slot if the form reset) and click Confirm. Expect: Booking succeeds — "Booking Confirmed" (.confirmation-title). This proves the blocklist rejects only matching emails, not everyone. Capture screenshot: cu16-blocklist-allowed-ok. [L1] Record this booking for Cleanup.
  6. Action: Record the negative-control note: rejection happened at submit time with a clear message, not a silent failure. Expect: N/A (documentation step). [L1]

J. BYOK Anthropic key: stored write-only / masked, status flips

  1. Action: On the Settings tab, locate the "AI API Key (BYOK)" card. Read the status line (#byokStatus). Expect: It currently reads "Using shared API key." (grey) — assuming no prior BYOK key. Record the starting state. [L1]
  2. Action: In the key input (#byokKeyInput, placeholder sk-ant-...), type the placeholder key sk-ant-cu16-<RUNID>-placeholder. Click Save Key (#byokSaveBtn). Expect: The status (#byokStatus) changes to "Your own API key is active." in green, and the input field is cleared (the key is never echoed back). Capture screenshot: cu16-byok-active. [L1]
  3. Action: Confirm the key is NOT readable in the UI: inspect #byokKeyInput value. Expect: It is empty — the saved key is write-only and the field does not repopulate with the secret. There is no "reveal" affordance for the BYOK key. [L1/L2]
  4. Action: Reload the dashboard and re-open Settings. Expect: #byokStatus still reads "Your own API key is active." (green), and #byokKeyInput is still empty — confirming the encrypted key persisted server-side without ever being returned in plaintext. Capture screenshot: cu16-byok-persisted. [L2]
  5. Action: (Negative validation, optional but recommended) Type a clearly invalid key not-a-real-key into #byokKeyInput and click Save Key. Expect: The save fails with an error (server requires the key to start with sk-). The status should NOT change to active for an invalid value. Then clear the field. [L1]
  6. Action: Confirm BYOK is wired to the assistant path (best-effort, in-UI): open the dashboard AI assistant bar at the bottom (#aiBarInput), type a simple request like "list my event types" and Send (#aiBarSend). Expect: Because the saved BYOK key is a FAKE placeholder, the assistant call to Anthropic will fail/auth-error rather than return a normal answer — this is the expected and CORRECT signal that the host's own (bad) key is now being used instead of the working shared key. Capture screenshot: cu16-byok-assistant-error. If instead it answers normally, that indicates the platform key was used (BYOK not applied) — record as a discrepancy. [L1] (See Manual residue — we cannot prove which key was used, only infer from the auth failure.)

K. GA + Meta pixel scripts injected into the public booking page

NOTE on mechanism: the GA (gtag) and Meta (fbq) scripts are injected by client-side JavaScript into <head> AFTER the booking-page JSON loads. They are NOT present in the raw served HTML (View Source). You MUST verify in the live DOM / network, not in static source.

  1. Action: Reload the public booking page https://calendo.dev/booking/?user=<HOST_SLUG> and let it fully load. Open DevTools Network tab (or use it). Expect: A request to https://www.googletagmanager.com/gtag/js?id=<your GA ID> is made, and a request to https://connect.facebook.net/en_US/fbevents.js is made (Meta Pixel loader). Capture screenshot: cu16-pixels-network. [L2]
  2. Action: In the DevTools Console on that page, evaluate typeof window.gtag and typeof window.fbq. Expect: Both return "function" — confirming both tracking libraries initialized. [L2]
  3. Action: In the Console, evaluate [...document.head.querySelectorAll('script')].map(s=>s.src||s.textContent).filter(t=>t.includes('googletagmanager')||t.includes('fbevents')||t.includes('fbq(')||t.includes('gtag(')). Expect: The array contains the GA loader src (with your GA ID), a GA init inline script calling gtag('config', '<your GA ID>'), and a Meta inline script calling fbq('init', '<your Pixel ID>'). Capture screenshot: cu16-pixels-dom. [L2]
  4. Action: Confirm the negative: do View Source (view-source:https://calendo.dev/booking/?user=<HOST_SLUG>) and search the raw HTML for googletagmanager / fbevents. Expect: They are NOT in the static source — they are runtime-injected. Record this so the human reviewer understands the verification was DOM-based, not source-based. [L1]

L. Welcome message via AI assistant (covers the customization the UI form lacks)

  1. Action: On the dashboard, open the AI assistant bar (#aiBarInput) and send: "Set my booking page welcome message to: CU16 welcome <RUNID>". Expect: The assistant confirms it updated the booking page welcome message (subject to the BYOK key from Step 26 — if the fake key blocks the assistant, REMOVE the BYOK key first via Step 36, then retry this step; the welcome-message edit must run with a working key). [L1] NOTE: if the assistant is non-functional due to BYOK, record that welcome-message editing could not be exercised and treat it as manual residue.
  2. Action: (Prerequisite for Step 35 if assistant failed) Remove the BYOK key: on Settings, click Remove Key (#byokRemoveBtn). Expect: #byokStatus returns to "Using shared API key." (grey). Then retry Step 35. [L1]
  3. Action: After the welcome message is set, reload the public booking page and read #host-welcome. Expect: It shows CU16 welcome <RUNID> instead of the default. Capture screenshot: cu16-public-welcome. [L2]

L3 reality checks

None — see Pass/Fail. This suite creates no real external calendar events and sends no email that must be confirmed in Gmail/Outlook. (The booking confirmations in Steps 16 and 23 land in P1's Gmail under ravikantguptaofficial+ok-<RUNID> / +ok2-<RUNID>, but verifying those emails is the responsibility of the booking/notification suites, not this customization suite. If you want a courtesy check, search Gmail for <RUNID>, but it is not required to pass CU-16.)

Cleanup

Restore P1 to baseline so the real booking page is unaffected. Do these in order:

  1. Cancel the bookings created in Steps 16 and 23 (the two successful bookings with +ok-<RUNID> and +ok2-<RUNID>): in the dashboard Bookings tab, find each by the guest name CU16 OK Guest <RUNID> / the +ok2-<RUNID> booking and cancel them, OR use the cancel links from their confirmation pages. Confirm they no longer appear in upcoming bookings.
  2. Delete the throwaway event type CU16 Block Test <RUNID> if you created one (Event Types / Overview → delete cu16-block-test-<RUNID>).
  3. Remove the BYOK key if still present: Settings → Remove Key (#byokRemoveBtn); confirm status returns to "Using shared API key.".
  4. Restore profile/branding/tracking/blocklist fields on Settings to the baseline values you recorded in Step 2: set #settingsDisplayName back to its original, #settingsBrandColorText back to original (likely #0069ff), #settingsLogoUrl back to original (often empty), #settingsCancellationPolicy back to original, #settingsBlockedEmails back to original (clear the two CU16 lines), #settingsGaMeasurementId and #settingsMetaPixelId back to original (clear if originally empty). Click Save.
  5. Restore the welcome message if you changed it in Step 35: via the AI assistant send "Clear my booking page welcome message" (or set it back to the original you recorded), so the public page shows the original/default.
  6. Reload the public booking page and confirm the host name, brand color, logo, welcome text, and absence/presence of pixels match the pre-run baseline. Capture screenshot: cu16-cleanup-restored.

Pass/Fail criteria

The run PASSES only if ALL of the following hold:

The run FAILS if any setting silently does not persist, the blocklist allows a blocked email through, the BYOK secret is ever displayed back in the input, or either tracking script fails to load when an ID is configured.

Evidence to capture

Manual residue / cannot-verify