You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A locked spec for a /dashboard route (equal-vote.github.io/web-tools/#/dashboard) — V1, staff-only. Two tables, one per source group (BetterVoting, NationBuilder), each row a metric and each column a year, populated by live API calls reusing the credential model already in this app (NationBuilder OAuth cookie, no separate login UI). The spec pins down the exact metric list, its source/computation per metric, and table shape — ready to hand to a build session with no open judgment calls left.
V1 excludes: Mailchimp, manual-spreadsheet metrics, BetterVoting user counts, and any caching/no-credential public viewing — see Out of scope.
Notes
Domain: React + TS SPA (web-tools), GitHub Pages, HashRouter, useCookie-persisted credentials, req()/StateReporter pattern in WebTools.tsx. See CLAUDE.md.
The Heroku CORS proxy (PROXY_ORIGIN) is a confirmed blind passthrough — forwards to any target URL, no host allow-list. Adding bettervoting.com as a source needs no proxy-side changes, just a client-side fetch path (likely no auth header, unlike the existing mailchimp/nationbuilder branches in req()).
BetterVoting table is fully specified already (no ticket needed): rows = headline (Elections Created, Votes Cast) plus one votes/elections row-pair per voting method (star, star_pr, approval, ranked_robin, rcv, choose_one, stv, multi_method) from a single GlobalElectionStats (by_year) call. All rows styled identically — no visual hierarchy for V1. Year columns derived from whatever years the API returns.
NationBuilder needs per-cluster research: volunteers/signups, donations/finance, events. Every NationBuilder research ticket must check whether counts/sums can be fetched without pulling full paginated payloads — the org wants counts, not bulk data exports.
Keep the future cached/public-viewing idea in mind (see Out of scope) when shaping each metric's fetch: isolate each behind a clean function so a later cached-JSON fallback can slot in without a redesign. Don't design for it now — just don't foreclose it.
Consult /research for the NationBuilder tickets below; /grilling and /domain-modeling for any follow-on decision tickets that graduate out of the fog.
Decide what defines a "Volunteer Signup" for the dashboard — "Volunteer Signups" = every new NationBuilder signup, unfiltered by tag/role (join form, event RSVP, whatever the source). Counted via stats[total]=count with a created_at date-range filter on the plain signups endpoint — no tag filter, simpler than NationBuilder: how to count Volunteer Signups per year #20 anticipated. Naturally deduplicated per person since created_at is set once at record creation.
Decide donation counting policy for "Number of Donations" & "Funds Raised" — three rows, not two: Number of Donations (transaction count), Number of Donors (unique donor count), Funds Raised (dollar sum) — all three derived locally from one paginated per-year fetch filtered to status=succeeded, bucketed by succeeded_at. No separate cheap count call (pagination is already required for donor-count/funds-raised, so a row count falls out for free). No refund-subtraction logic — live status filtering handles it, with accepted drift. Pledges excluded entirely. Each recurring charge counts as its own donation (no grouping field exists in the API).
Not yet specified
(empty — every fog patch from the initial charting has now graduated into a ticket)
Out of scope
Mailchimp (Email Blasts Sent) — deferred from V1; user chose to focus on BetterVoting + NationBuilder only, which have clearer API stories.
Manual-spreadsheet metrics (Coalition Members, Google Form Sign-ups, Presentations Given, Campaigns Run, Number of leaders with titles) — no API story; deferred from V1 alongside Mailchimp.
BetterVoting user counts — not present in the GlobalElectionStats output the user is treating as source of truth; dropped from scope rather than deferred.
Cached, no-credential public dashboard viewing + daily cron refresh — explicitly named by the user as a future effort (sibling to a similar planned cron for contact exports). Revisit as its own wayfinder map once V1 ships and the NationBuilder fetch shape is known.
Recurring-donor tracking ("number of donors with an ongoing recurring donation") — ruled out while resolving Decide donation counting policy for "Number of Donations" & "Funds Raised": the v2 API has no field identifying or grouping recurring charges (no recurring_donation_id, no payment_profile_id), and a matching heuristic was judged too fragile for V1.
Destination
A locked spec for a
/dashboardroute (equal-vote.github.io/web-tools/#/dashboard) — V1, staff-only. Two tables, one per source group (BetterVoting, NationBuilder), each row a metric and each column a year, populated by live API calls reusing the credential model already in this app (NationBuilder OAuth cookie, no separate login UI). The spec pins down the exact metric list, its source/computation per metric, and table shape — ready to hand to a build session with no open judgment calls left.V1 excludes: Mailchimp, manual-spreadsheet metrics, BetterVoting user counts, and any caching/no-credential public viewing — see Out of scope.
Notes
web-tools), GitHub Pages, HashRouter,useCookie-persisted credentials,req()/StateReporterpattern inWebTools.tsx. SeeCLAUDE.md.PROXY_ORIGIN) is a confirmed blind passthrough — forwards to any target URL, no host allow-list. Adding bettervoting.com as a source needs no proxy-side changes, just a client-side fetch path (likely no auth header, unlike the existing mailchimp/nationbuilder branches inreq()).GlobalElectionStats(by_year) call. All rows styled identically — no visual hierarchy for V1. Year columns derived from whatever years the API returns./researchfor the NationBuilder tickets below;/grillingand/domain-modelingfor any follow-on decision tickets that graduate out of the fog.Decisions so far
NationBuilder: how to count Volunteer Signups per year — mechanism confirmed: server-side per-year counts via
stats[total]=count. Graduated into Decide what defines a "Volunteer Signup" for the dashboard.Decide what defines a "Volunteer Signup" for the dashboard — "Volunteer Signups" = every new NationBuilder signup, unfiltered by tag/role (join form, event RSVP, whatever the source). Counted via
stats[total]=countwith acreated_atdate-range filter on the plainsignupsendpoint — no tag filter, simpler than NationBuilder: how to count Volunteer Signups per year #20 anticipated. Naturally deduplicated per person sincecreated_atis set once at record creation.NationBuilder: how to fetch/count Events per year (in-person, orientations, chapter metadata) —
eventsendpoint supports server-side total counts (stats[total]=count), but has no native in-person/virtual field, no native "Orientation" type/tag, and no direct chapter/branch field on events (only an indirectpage_id → site_idlink). Graduated into one combined ticket (event-categorization and chapter-attribution merged at the user's request, since they're the same underlying question and conversation): Decide how to categorize events and attribute them to a chapter/branch.NationBuilder: how to fetch Number of Donations & Funds Raised per year —
donationsendpoint gives cheap server-side counts (stats[total]=count), but sums are not supported server-side ("Funds Raised" needs client-side pagination + summingamount_in_cents); recurring donations aren't modeled/groupable at all in v2. Six org-policy questions (transaction vs. donor count, which statuses count, date field, refunds, pledges, recurring-charge handling) graduated into Decide donation counting policy for "Number of Donations" & "Funds Raised".Decide donation counting policy for "Number of Donations" & "Funds Raised" — three rows, not two: Number of Donations (transaction count), Number of Donors (unique donor count), Funds Raised (dollar sum) — all three derived locally from one paginated per-year fetch filtered to
status=succeeded, bucketed bysucceeded_at. No separate cheap count call (pagination is already required for donor-count/funds-raised, so a row count falls out for free). No refund-subtraction logic — live status filtering handles it, with accepted drift. Pledges excluded entirely. Each recurring charge counts as its own donation (no grouping field exists in the API).Not yet specified
(empty — every fog patch from the initial charting has now graduated into a ticket)
Out of scope
GlobalElectionStatsoutput the user is treating as source of truth; dropped from scope rather than deferred.recurring_donation_id, nopayment_profile_id), and a matching heuristic was judged too fragile for V1.