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

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.

Steps

Phase 1 — Create the poll (dashboard, as host P1)

  1. Action: Go to the dashboard (https://calendo.dev/dashboard/). Expect: The authenticated dashboard renders with the left sidebar. [L1]
  1. 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]
  1. 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]
  1. Action: Type the poll title into #pollTitle: Team Poll CU13 <RUNID>. Expect: The field shows the typed title. [L1]
  1. Action: Type the description into #pollDesc: CU-13 poll run <RUNID>. Expect: The field shows the typed description. [L1]
  1. Action: Click "+ Add Option" (#pollAddOptionBtn) once so there are 3 option rows total inside #pollOptionsContainer (each row has a date input, a start time input, an end time input, and a small "×" remove button). Expect: A third option row appears. [L1]
  1. Action: Fill the 3 option rows with the Test data times. For each row set its input[type="date"], the first input[type="time"] (start), and the second input[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.
  1. Capture screenshot: cu13-01-poll-create-form (form filled with title, description, and 3 complete option rows).
  1. 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 title Team Poll CU13 <RUNID>, a voter count of "0 voters", and two buttons: "Copy Link" and "Finalize". [L1]
  1. Action: Confirm persistence by reloading the page (or re-opening the Settings tab) and re-checking #pollsList. Expect: The poll row Team Poll CU13 <RUNID> still appears with "0 voters". [L2]
  1. 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 title equals Team Poll CU13 <RUNID> and read its id. 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 form https://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 is https://calendo.dev/poll/?id=<POLL_ID>. Use the id from /api/polls to build that URL. (Note this discrepancy in the results report; see Manual residue.)
  1. 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)

  1. Action: Navigate to the public poll voting page: https://calendo.dev/poll/?id=<POLL_ID>. Expect: The page renders an <h1> equal to Team 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.
  1. 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 the yes class — green fill); its sibling maybe/no buttons in that row are not selected. [L1]
  1. 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.
  1. 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]
  1. Capture screenshot: cu13-03-voter1-selections (Option A yes / B maybe / C no selected, voter1 name+email filled).
  1. 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 to Voter 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]
  1. 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)

  1. 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.
  1. 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.
  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> and Voter 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 the best-option highlight and shows "Best option" text), because it has the most yes votes. [L1][L2]
  1. 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

  1. 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 so loadPolls() re-fetches. Expect: The poll row Team Poll CU13 <RUNID> now shows "2 voters" (distinct voter count incremented from 0 → 2 across the two emails). [L1][L2]
  1. Capture screenshot: cu13-05-dashboard-2-voters (dashboard polls list showing "2 voters").

Phase 5 — Pick the winner (finalize) as host

  1. 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_id and 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/finalize with no request body, but the server requires an option_id in the body and rejects an empty body — so the dashboard Finalize button does not work. The /poll/ finalize card sends the selected option_id and works. (Report the dashboard button defect in results; see Manual residue.)
  1. Action: Confirm #finalizeOptionSelect is set to the winning Option A (the "(2 yes)" entry). If not, select it explicitly. Expect: The dropdown value is Option A. [L1]
  1. 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]
  1. Capture screenshot: cu13-06-poll-finalized (finalized badge + "Meeting confirmed!" banner with the winning date/time).
  1. 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" and finalized_option_id equal to Option A's id. [L2]
  1. 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]
  1. 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]
  1. Capture screenshot: cu13-07-dashboard-finalized (dashboard polls list row showing FINALIZED, 2 voters).

Phase 6 — Analytics / tally reflection

  1. 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.
  1. 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:

  1. 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.
  2. 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.
  3. No bookings, event types, routing forms, throwaway accounts, or calendar test events were created by this suite, so nothing else needs cleanup.
  4. No global host availability was modified.

Pass/Fail criteria

The run PASSES only if ALL of the following are true:

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

Manual residue / cannot-verify