CU-13: Meeting polls: create → share → vote → tally → pick winner
Priority: P2 Accounts/sessions: P1 host (ravikantguptaofficial@gmail.com, logged into Calendo) + anonymous/public voters (no Calendo account needed; voters self-identify by name + email only) Parallel-safe: true (poll data is scoped per-poll and per-RUNID; does not touch global host availability or shared bookings) Exclusive (rewrites global host availability?): No Estimated time: 18 minutes L3 reality checks: None required for this suite. Poll voting and finalization are purely in-app (Calendo D1). Finalizing a poll in this product only flips poll status to finalized and records the winning option; it does NOT create a Calendar event or send a confirmation email (see Manual residue). So there is nothing external (Google Calendar / Outlook / Gmail) to verify.
Goal
This suite proves Calendo's Doodle-style meeting poll feature end to end: a host can create a poll with multiple candidate time slots, share it, collect votes from multiple distinct anonymous participants, see the tally (per-option yes/maybe/no counts, distinct voter count, and the auto-computed "Best option"), and then finalize the poll by picking a winning time. This matters because polls are the "find a time that works for a group" workflow — the alternative to one-on-one slot booking — and the differentiator from a plain calendar grid. The run verifies that voter counts increment correctly across two separate voters, that votes persist across reload [L2], and that finalization closes voting and records the chosen winner [L1/L2].
Preconditions
- The browser must have an ACTIVE Calendo session for P1 (ravikantguptaofficial@gmail.com). Open https://calendo.dev/dashboard/ — if it loads the dashboard (sidebar with tabs) you are logged in. If it redirects to a login/landing page, STOP and FLAG this as a precondition failure per 00-setup-preconditions.md. Do NOT attempt a cold Google password login mid-run.
- No baseline event types or availability are required for this suite — polls are independent of availability rules and event types. (Finalizing does not consult availability.)
- The two anonymous voters do NOT need Calendo accounts. They are simulated entirely on the public
/poll/page by entering a name + email. To make the second voter "different", use a different email alias (the poll keys distinct voters by lowercased email; voting again with the SAME email overwrites that voter's prior votes rather than adding a new voter). - If the dashboard loads but the Settings tab shows no "Meeting Polls" card, FLAG it (the feature may be gated/removed in prod) and do not improvise an alternate path.
Test data
Pick a fresh RUNID at execution time, e.g. a UTC timestamp 20260601-1530. Embed it in every created name and voter email so reruns never collide and assertions can scope by RUNID.
- Poll title:
Team Poll CU13 <RUNID>(e.g.Team Poll CU13 20260601-1530) - Poll description:
CU-13 poll run <RUNID> - Time options (create at least 3 so the tally has a clear winner): the dashboard pre-seeds 2 option rows; add a 3rd. Use 3 distinct future dates with distinct times. Concretely, relative to today (2026-06-01):
- Option A: date = today + 7 days (2026-06-08), 09:00 to 10:00
- Option B: date = today + 8 days (2026-06-09), 14:00 to 15:00
- Option C: date = today + 9 days (2026-06-10), 11:00 to 12:00
- Voter 1: name
Voter One CU13 <RUNID>, emailravikantguptaofficial+inv-<RUNID>-v1@gmail.com - Voter 2: name
Voter Two CU13 <RUNID>, emailravikantguptaofficial+inv-<RUNID>-v2@gmail.com - Intended winner: Option A. To make Option A win deterministically, have BOTH voters vote
yeson Option A. (Best option = highest yes_count, tiebreak = lowest no_count.)
Steps
Phase 1 — Create the poll (dashboard, as host P1)
- Action: Go to the dashboard (https://calendo.dev/dashboard/). Expect: The authenticated dashboard renders with the left sidebar. [L1]
- Action: Open the Settings tab — click the sidebar item with
data-tab="settings"(visible label "Settings"). URL hash/section becomes the settings view. Expect: The Settings page renders and includes a card titled "Meeting Polls" with subtext "Let participants vote on meeting times" and a "+ Create Poll" button (#createPollBtn). Scroll to it if needed. [L1]
- Action: Click "+ Create Poll" (
#createPollBtn). Expect: The inline poll create form (#pollCreateForm) becomes visible, showing a Title field (#pollTitle), a Description field (#pollDesc), a "Time options (add at least 2)" area (#pollOptionsContainer) that is pre-populated with 2 option rows, an "+ Add Option" button (#pollAddOptionBtn), a "Create Poll" submit button (#pollSubmitBtn), and a "Cancel" button (#pollCancelBtn). [L1]
- Action: Type the poll title into
#pollTitle:Team Poll CU13 <RUNID>. Expect: The field shows the typed title. [L1]
- Action: Type the description into
#pollDesc:CU-13 poll run <RUNID>. Expect: The field shows the typed description. [L1]
- Action: Click "+ Add Option" (
#pollAddOptionBtn) once so there are 3 option rows total inside#pollOptionsContainer(each row has a date input, a starttimeinput, an endtimeinput, and a small "×" remove button). Expect: A third option row appears. [L1]
- Action: Fill the 3 option rows with the Test data times. For each row set its
input[type="date"], the firstinput[type="time"](start), and the secondinput[type="time"](end):- Row 1: date 2026-06-08, start 09:00, end 10:00
- Row 2: date 2026-06-09, start 14:00, end 15:00
- Row 3: date 2026-06-10, start 11:00, end 12:00 Expect: All 3 rows show valid dates and times (no blank date fields — blank dates are silently dropped by the client). [L1] Order matters: the client filters out any option missing date/start/end and requires at least 2 valid options, so all three rows must be complete before submitting.
- Capture screenshot:
cu13-01-poll-create-form(form filled with title, description, and 3 complete option rows).
- Action: Click "Create Poll" (
#pollSubmitBtn). Expect: A toast "Poll created" appears, the create form (#pollCreateForm) hides, and the polls list (#pollsList) now contains a row showing the poll titleTeam Poll CU13 <RUNID>, a voter count of "0 voters", and two buttons: "Copy Link" and "Finalize". [L1]
- Action: Confirm persistence by reloading the page (or re-opening the Settings tab) and re-checking
#pollsList. Expect: The poll rowTeam Poll CU13 <RUNID>still appears with "0 voters". [L2]
- Action: Determine the poll's numeric ID (needed to reach the working public voting page). Do this by opening the polls API in the same logged-in browser tab: navigate to https://calendo.dev/api/polls. Expect: A JSON array of the host's polls; find the object whose
titleequalsTeam Poll CU13 <RUNID>and read itsid. Record this as<POLL_ID>. [L2] Why not just use the dashboard "Copy Link" button? The dashboard "Copy Link" (copyPollLink) copies a URL of the formhttps://calendo.dev/booking/?poll=<POLL_ID>, but the booking page does NOT implement poll handling — that link does not render a votable poll. The functional public voting page ishttps://calendo.dev/poll/?id=<POLL_ID>. Use theidfrom/api/pollsto build that URL. (Note this discrepancy in the results report; see Manual residue.)
- Capture screenshot:
cu13-02-polls-list-created(dashboard polls list row showing the title and "0 voters").
Phase 2 — Vote as Voter 1 (anonymous public page)
- Action: Navigate to the public poll voting page: https://calendo.dev/poll/?id=<POLL_ID>. Expect: The page renders an
<h1>equal toTeam Poll CU13 <RUNID>, a subtitle "Created by <host display name>" with an "open" status badge, the description text, and a vote table (table.vote-table) listing the 3 time options. Because status is "open", the table has a "Your Vote" column with three round vote buttons per option row:button.vote-btn[data-option][data-vote="yes"](checkmark),[data-vote="maybe"](?),[data-vote="no"](×). Below the table is a "Submit Your Vote" card with#voterName,#voterEmail, and#submitVoteBtn. [L1] Important: This/poll/page treats the visitor as anonymous regardless of host login state for the voting form. Voting only requires name + email in the form.
- Action: For Option A's row (the 2026-06-08, 9:00 AM option), click the green "yes" vote button (
button.vote-btn[data-option="<A_OPTION_ID>"][data-vote="yes"], the checkmark). Expect: That button becomes highlighted/selected (gains theyesclass — green fill); its sibling maybe/no buttons in that row are not selected. [L1]
- Action: For Option B's row, click the yellow "maybe" button (
[data-vote="maybe"], the "?"). For Option C's row, click the red "no" button ([data-vote="no"], the "×"). Expect: Each clicked button highlights and the row reflects a single selection. [L1] Why vote on multiple options: exercises that per-voter selections are independent per option and that yes/maybe/no all record.
- Action: In the Submit Your Vote card, fill
#voterName=Voter One CU13 <RUNID>and#voterEmail=ravikantguptaofficial+inv-<RUNID>-v1@gmail.com. Expect: Both fields show the typed values. [L1]
- Capture screenshot:
cu13-03-voter1-selections(Option A yes / B maybe / C no selected, voter1 name+email filled).
- Action: Click "Submit Votes" (
#submitVoteBtn). Expect: A toast "Votes submitted!" appears, then the page re-renders (~0.5s later). After re-render the vote table now shows a new column header equal toVoter One CU13 <RUNID>, with that voter's recorded marks per option (checkmark on A, ? on B, × on C). The per-option count line beneath each option shows updated counts, e.g. Option A "1 yes". [L1][L2]
- Action: Confirm persistence: reload https://calendo.dev/poll/?id=<POLL_ID>. Expect: Voter One's column and marks are still present after reload (votes persisted to D1). [L2]
Phase 3 — Vote as Voter 2 (second distinct voter)
- Action: Stay on / reload https://calendo.dev/poll/?id=<POLL_ID>. (No logout needed — the public form keys voters by the email you type, so a different email = a different voter.) For Option A's row, click the "yes" button. For Option B's row, click "yes" as well. Leave Option C unvoted (or click "no"). Expect: The selected buttons for Voter 2 highlight. [L1] Why have Voter 2 also vote yes on A: makes Option A reach 2 yes votes, deterministically the winner for the tally/best-option assertion.
- Action: Fill
#voterName=Voter Two CU13 <RUNID>and#voterEmail=ravikantguptaofficial+inv-<RUNID>-v2@gmail.com(a DIFFERENT email from Voter 1). Expect: Fields show the values. [L1] Critical: The email must differ from Voter 1's. Reusing Voter 1's email would overwrite Voter 1's votes (the server deletes prior votes for that email before inserting) and the voter count would stay at 1.
- Action: Click "Submit Votes" (
#submitVoteBtn). Expect: Toast "Votes submitted!", then re-render. The vote table now has TWO voter columns:Voter One CU13 <RUNID>andVoter Two CU13 <RUNID>. Option A's count line shows "2 yes". The row for Option A is marked as the "Best option" (the option row gains thebest-optionhighlight and shows "Best option" text), because it has the most yes votes. [L1][L2]
- Capture screenshot:
cu13-04-two-voters-tally(table with both voter columns, Option A showing 2 yes and the Best option highlight).
Phase 4 — Confirm voter count in the dashboard
- Action: Go back to the dashboard Settings tab (https://calendo.dev/dashboard/ →
data-tab="settings") and locate the poll row in#pollsList. Reload if needed soloadPolls()re-fetches. Expect: The poll rowTeam Poll CU13 <RUNID>now shows "2 voters" (distinct voter count incremented from 0 → 2 across the two emails). [L1][L2]
- Capture screenshot:
cu13-05-dashboard-2-voters(dashboard polls list showing "2 voters").
Phase 5 — Pick the winner (finalize) as host
- Action: Finalize using the public poll page while logged in as the host (this is the reliable finalize path). Navigate to https://calendo.dev/poll/?id=<POLL_ID> in the tab that is logged in as P1. Expect: Because the logged-in user's id matches the poll's
user_idand the poll is still "open" with options, an extra card titled "Finalize This Poll" renders at the bottom, containing a "Winning option" dropdown (#finalizeOptionSelect) pre-selected to the best option (Option A, "(2 yes)") and a "Finalize Poll" button (#finalizePollBtn). [L1] Why here and not the dashboard "Finalize" button: The dashboard polls-list "Finalize" button posts to/api/polls/:id/finalizewith no request body, but the server requires anoption_idin the body and rejects an empty body — so the dashboard Finalize button does not work. The/poll/finalize card sends the selectedoption_idand works. (Report the dashboard button defect in results; see Manual residue.)
- Action: Confirm
#finalizeOptionSelectis set to the winning Option A (the "(2 yes)" entry). If not, select it explicitly. Expect: The dropdown value is Option A. [L1]
- Action: Click "Finalize Poll" (
#finalizePollBtn). Expect: A toast "Poll finalized!" appears, then the page re-renders (~0.5s). After re-render: a green "finalized" status badge replaces "open"; a green "Meeting confirmed!" banner (.finalized-banner) shows the winning option's date and time (Tue, Jun 8, 9:00 AM – 10:00 AM); the per-row vote buttons / "Your Vote" column and the "Submit Your Vote" card are gone (voting closed); and the "Finalize This Poll" card no longer appears. [L1][L2]
- Capture screenshot:
cu13-06-poll-finalized(finalized badge + "Meeting confirmed!" banner with the winning date/time).
- Action: Confirm finalization persisted and voting is closed: in a fresh tab open https://calendo.dev/api/polls/<POLL_ID>. Expect: JSON shows
status: "finalized"andfinalized_option_idequal to Option A's id. [L2]
- Action: Confirm voting is actually closed by attempting to vote again. Reload https://calendo.dev/poll/?id=<POLL_ID> and verify NO vote buttons and NO "Submit Your Vote" card are shown (the page only renders voting controls when
status === 'open'). Expect: No vote buttons; finalized banner present. [L1]
- Action: Confirm the dashboard reflects finalization: dashboard Settings tab →
#pollsList→ the poll row. Reload if needed. Expect: The poll row shows a "FINALIZED" label (green) and still "2 voters"; the per-row "Finalize" button is gone (only "Copy Link" remains). [L1][L2]
- Capture screenshot:
cu13-07-dashboard-finalized(dashboard polls list row showing FINALIZED, 2 voters).
Phase 6 — Analytics / tally reflection
- Action: Re-open the public poll page https://calendo.dev/poll/?id=<POLL_ID> and read the final tally in the vote table. Expect: Two voter columns present; Option A retains its winning marks; counts are consistent with the votes cast (A = 2 yes; B = 1 maybe + 1 yes per the votes you cast; C = 0 or 1 no). The "best option" highlight is on Option A. This is the poll's analytics/tally surface. [L1][L2] Note on scope: Calendo's main Analytics tab (
data-tab="analytics", booking volume / cancellation rate) does not track poll votes — poll tally lives on the poll page and the dashboard polls list voter count. There is no separate "poll analytics" dashboard widget in this build (the only poll analytics is the voter_count and the per-option vote tally). Do not expect poll data in the Analytics tab; if asked to verify "poll analytics", the voter count + per-option tally above ARE the analytics.
- Capture screenshot:
cu13-08-final-tally(final vote table with both voters and winning option).
L3 reality checks
None — see Pass/Fail. Finalizing a Calendo poll only updates poll status and the winning option in D1; it does not create a Google/Outlook Calendar event and does not send confirmation/notification emails in this build, so there is no external calendar or Gmail artifact to confirm. (If a future build adds "create the booking on finalize", add an L3 check here: open calendar.google.com for the winning date/time and confirm an event titled with the poll title, plus a Gmail search for the poll title in ravikantguptaofficial@gmail.com.)
Cleanup
The poll feature has no in-UI delete button for a poll. To keep host accounts clean:
- The poll itself: there is no "Delete poll" action in the dashboard or poll page UI. The poll will remain in the host's polls list as a finalized entry. This is acceptable residue because it is RUNID-scoped (
Team Poll CU13 <RUNID>) and will not collide with future runs. NOTE this in the results report as un-cleanable via UI. - If a programmatic cleanup is desired by the human operator (out of band, not in-browser by the agent): delete the poll and its votes/options from D1 by poll id, e.g.
wrangler d1 execute calendo --remote --command="DELETE FROM poll_votes WHERE poll_id=<POLL_ID>; DELETE FROM poll_options WHERE poll_id=<POLL_ID>; DELETE FROM meeting_polls WHERE id=<POLL_ID>;". Flag this as manual residue (see below) — the browser agent cannot do it. - No bookings, event types, routing forms, throwaway accounts, or calendar test events were created by this suite, so nothing else needs cleanup.
- No global host availability was modified.
Pass/Fail criteria
The run PASSES only if ALL of the following are true:
- A poll titled
Team Poll CU13 <RUNID>was created via the dashboard and appears in#pollsListwith "0 voters" immediately after creation. [L1] - The poll persists across a dashboard reload. [L2]
- The public poll page https://calendo.dev/poll/?id=<POLL_ID> renders the title, "open" badge, all 3 time options, and votable buttons. [L1]
- Voter 1's vote submission shows toast "Votes submitted!" and adds a column
Voter One CU13 <RUNID>to the table with the expected per-option marks. [L1][L2] - Voter 1's votes persist after a page reload. [L2]
- Voter 2's vote (distinct email) adds a SECOND voter column
Voter Two CU13 <RUNID>; the table shows two voter columns. [L1][L2] - After both voters, Option A shows "2 yes" and is flagged as the Best option. [L1][L2]
- The dashboard polls list voter count increments from 0 → 2. [L1][L2]
- Finalizing via the
/poll/finalize card (as host) shows "Poll finalized!", a "finalized" badge, and a "Meeting confirmed!" banner with the winning date/time (Jun 8, 9:00–10:00 AM). [L1][L2] GET /api/polls/<POLL_ID>returnsstatus: "finalized"withfinalized_option_id= Option A. [L2]- After finalization, the public poll page no longer renders vote buttons or the submit form (voting closed). [L1]
- The dashboard polls list row shows "FINALIZED" and "2 voters". [L1][L2]
The run FAILS if: the poll cannot be created; the public /poll/?id= page does not render or returns "Poll not found"; voter count does not increment to 2 (e.g. both voters collapsed into one — indicates the second voter reused an email); the best option is not Option A; finalization does not succeed via the /poll/ finalize card; or voting remains open after finalization.
Evidence to capture
- Screenshots:
cu13-01-poll-create-form,cu13-02-polls-list-created,cu13-03-voter1-selections,cu13-04-two-voters-tally,cu13-05-dashboard-2-voters,cu13-06-poll-finalized,cu13-07-dashboard-finalized,cu13-08-final-tally. - Recorded values: the chosen RUNID; the resolved
<POLL_ID>; the two voter emails used; theGET /api/polls/<POLL_ID>JSON snippet showingstatusandfinalized_option_id. - Notes for the report: (a) the dashboard "Copy Link" button produces a
/booking/?poll=URL that does NOT render a votable poll — the working voting URL is/poll/?id=; (b) the dashboard polls-list "Finalize" button does not finalize (sends nooption_id) — finalize succeeds only via the/poll/finalize card; (c) finalize does not create a calendar event or send email in this build.
Manual residue / cannot-verify
- Poll deletion / cleanup: There is no in-UI delete for a poll. The agent cannot remove the created poll in-browser. The human must delete it from D1 if a fully clean state is required (see Cleanup step 2). The RUNID scoping makes leftover polls harmless for reruns.
- Email / calendar side effects of finalization: This build does not send a "meeting confirmed" email or write the winning slot to Google/Outlook Calendar when a poll is finalized. The agent cannot confirm any such external artifact because none is produced. If the human expects finalization to book the meeting, that is a product gap to verify manually / log as a TBD.
- Two product defects to escalate (not agent-fixable): (1) dashboard "Copy Link" points at
/booking/?poll=<id>which has no poll handler; (2) dashboard "Finalize" button posts with no body and is rejected by the API. Both are real bugs surfaced by this runbook; the human should confirm and file them. - Voting deadline enforcement: The API supports an optional voting deadline, but the dashboard create form does not expose a deadline field, so deadline-expiry behavior is not exercised here. Out of scope for this suite.