Skip to content

Hide the Toronto voter survey and the candidates' answers, temporarily - #86

Merged
mikaalnaik merged 4 commits into
mainfrom
mikaal/hide-survey
Sep 11, 2026
Merged

mikaalnaik merged 4 commits into
mainfrom
mikaal/hide-survey

Conversation

@mikaalnaik

Copy link
Copy Markdown
Contributor

Takes down the voter survey and every published candidate answer across the Toronto tracker. Temporary, and built to be reverted.

Two flags, no deletions

Both live on the Toronto entry in src/lib/elections/registry.ts. Deleting the two lines turns everything back on — questions, collected responses, and all the code that reads them stay exactly where they are.

Flag Takes down
surveyClosed The voter survey form, the alignment results it shows afterwards, and all five invitations to it
questionnaireHidden Every published candidate answer: the cards on ward / mayoral / candidate pages, the whole-field view on /issues, and the read proxy

Flags rather than edits at each call site because the tracker invites readers to the survey from five places — the landing page's explore card, its closing CTA, /mayor, /issues and every ward page. A survey withdrawn from four is one readers still find from the fifth. surveyHref() returns undefined while closed, so a call site has nothing to link and the invitation has to disappear rather than point at a dead page.

What stays up

The ballot: who is running, in which ward, with their campaign site. That is a fact about the election rather than anything a candidate told us, and a mayoral page that names nobody in the race is no use to a reader who arrived with a name in mind.

A candidate's own bio stays too, though it arrives in the same response — it is a self-description rather than a position, and it is the only thing standing between most candidate pages and an empty one.

rosterSurvey narrows the kept prose to the bio alone rather than passing it through. The questionnaire's other free-text question is the ward-commitment target, and letting that through under "About" would publish an answer by another door.

What the pages say instead: "Coming soon."

Every one of these pages already had an empty state, written for the weeks before anybody had written back. Reaching for those would have been quicker and would have told the reader something untrue — "Nobody in this ward has answered yet", over a ward whose candidates answered months ago, is a claim about those candidates, and it is ours rather than theirs. On a candidate page it sits under a named person's photograph. So the withheld case is told apart from the quiet one everywhere it appears.

The counts go with the answers. Two of the mayoral page's four stats and all four of the issues page's count what the field said; at zero they do not read as less information, they read as the claim that nobody answered anything. Likewise the "how to read these cards" method notes, which are noise next to cards that are not there.

/issues keeps its route and shows the notice rather than 404ing with the survey — the page is the answers, but the URL is linked from the landing page, the ward pages and /mayor, and those links are still worth following to something that explains itself.

Closed at the routes, not only at the links

  • /toronto/vote/2026/survey → 404
  • POST /api/elections/survey → 404. A tab opened before the takedown still held a filled-in form and a working endpoint, and a submission accepted there is a response recorded against a survey we have stopped running.
  • GET /api/elections/candidate-responses → 404. A read proxy for exactly the answers the pages have stopped drawing; left open it would serve the whole comparison as JSON.

Testing

tsc --noEmit and npm run lint clean (4 pre-existing warnings in unrelated files). Swept ten pages — /toronto, the landing page, /issues, /mayor, /mayor/candidates, two ward pages, two candidate pages, /pledge: all 200, no question headings, no answer bars, no survey links, and none of the "yet to answer" copy. Both API routes and the survey page return 404. Bio confirmed intact on a candidate page that has one, and the About section correctly absent on one that does not.

Not verified in a browser — the Chrome extension was not connected in this session, so this is checked through served markup rather than watched.

Note on a sibling branch

mikaal/toronto-visualizations carries an unrelated fix for the ward-commitment target leaking into the About section. This branch does not include it; it just stops exposing that prose at all. The two do not conflict, but the fix is still worth landing.

🤖 Generated with Claude Code

mikaalnaik and others added 4 commits September 11, 2026 15:07
One flag on the election registry — `surveyClosed` — takes down the form,
the results it shows a reader afterwards, and every invitation to it.
Nothing else moves: the questions, the responses already collected and
all the code that reads them stay where they are, so turning it back on
is deleting the flag.

It is a flag rather than five edits because the tracker invites a reader
to the survey from five places — the landing page's explore card, its
closing call to action, the mayoral page, the issues page and every ward
page — and a survey withdrawn from four of them is a survey a reader
still finds from the fifth. `surveyHref` returns nothing while it is
closed, so a call site has no path to link and the invitation has to
disappear rather than be left pointing at a page that will not answer.

The whole "Now answer them yourself" band goes from the mayoral and
issues pages, not just its button: that heading over a paragraph telling
the reader they can, with nothing to answer, is worse than silence. It
takes the issues page's countdown with it, which is that page's only
clock — not worth keeping a band alive for, since the days to polls are
on the landing page, the mayoral page and every ward page.

The landing page's explore blurb ends "then answer them yourself and find
out who lines up with you", which is a promise about a survey. It now
makes that promise only where the grid has a survey card to back it, read
off the cards themselves so the two cannot disagree.

Closed at the route and at the endpoint, not only at the links. A reader
holding the URL still reached the form, and a tab opened before it came
down still held a filled-in one and a working POST — a submission
accepted there is a response recorded against a survey we have stopped
running. Both answer 404 now, as the site's position is that there is no
survey here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The sibling of `surveyClosed` and the same bargain: one flag,
`questionnaireHidden`, and nothing deleted. Every published answer comes
down — the cards on the ward, mayoral and candidate pages, the
whole-field view on /issues, and the read proxy that serves them to the
browser. The ballot stays up: who is running, in which ward, with their
campaign site, which is a fact about the election rather than anything a
candidate told us.

A candidate's own bio stays too, though it arrives in the same response.
It is a self-description rather than a position, and it is the only thing
standing between most candidate pages and an empty one. `rosterSurvey`
narrows the prose to the bio alone rather than passing it through: the
rest of it answers a policy question — the ward-commitment target is one
— and letting that through under "About" would publish an answer by
another door.

WHAT THE PAGES SAY INSTEAD

Every one of these pages already had an empty state, written for the
weeks before anybody had written back, and reaching for those would have
been quicker and would have told the reader something untrue. "Nobody in
this ward has answered yet", over a ward whose candidates answered months
ago, is a claim about those candidates and it is ours, not theirs — and
on a candidate page it sits under a named person's photograph. So the
withheld case is told apart from the quiet one everywhere it appears, and
says "Coming soon."

The counts go with the answers. Two of the mayoral page's four stats and
all four of the issues page's count what the field said, and at zero they
do not read as less information — they read as the claim that nobody
answered anything.

/issues keeps its route and shows the notice, rather than 404ing with the
survey: the page is the answers, but the URL is linked from the landing
page, the ward pages and /mayor, and those links are still worth
following to something that explains itself.

Closed at the proxy as well as the pages. /api/elections/candidate-
responses is a read proxy for exactly the answers the pages have stopped
drawing, and left open it would serve the whole comparison as JSON to
anyone who asked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three of the rosters sort the field by whether a candidate wrote back,
which is the most useful sort available when the answers are on the page
and an empty one when they are not. With nothing to read back the
"answered" half empties and the whole ballot slides into the other one —
so a ward's candidates sat under "Also on the ballot, yet to answer our
questionnaire" and the mayoral field under "Yet to respond", which they
are not. It is a scoreboard reading nil-all, and every word of it ours.

Flat, and labelled for what it is: "On the ballot". The ward roster and
the mayoral roster page both collapse to one list, and /mayor/candidates
drops its "Answered us" stat for the same reason the other two pages
dropped theirs — a count of the answers at nil is not a smaller truth, it
is a different claim. The withdrawn list stays separate: that is a fact
about the ballot, not about who answered.

The ward's race block no longer repeats the names either. It was printing
"Coming soon" followed by the ballot as a run of plain text, directly
under the same ballot as a list of linked names.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"Coming soon." on its own says a thing is coming without saying what, and
on a page that has just dropped a section it is the reader's job to work
out which. It now names the survey.

And once. Four of the pages printed it twice — in the masthead and again
where the cards would be — which reads as a stutter rather than as a
fuller explanation, so each page keeps it in the one place a reader looks
for the thing that is gone. The ward pages keep it beside the ballot, and
their race block prints nothing rather than repeating it once per race
under the notice that already covers them all. The mayoral and issues
heroes go back to describing their page and leave the notice to the body.
The candidate page says it in the heading alone: the eyebrow, the heading
and a line under it were three ways of saying one thing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Sep 11, 2026

Copy link
Copy Markdown

Greptile Summary

This PR introduces two Toronto election feature flags that temporarily close the voter survey and withhold published candidate questionnaire answers without deleting stored data or supporting code.

  • Returns not-found responses from the survey page, survey submission endpoint, and candidate-response proxy.
  • Removes voter-survey invitations and replaces answer views with ballot information and a coming-soon state.
  • Preserves candidate biographies while filtering all other questionnaire prose.
  • Leaves misleading answer-oriented copy on the election landing page and includes one avoidably duplicated helper call.

Confidence Score: 4/5

The takedown behavior appears operationally sound, but the repository’s simplification requirement should be satisfied before merging and the landing page should stop advertising unavailable answers.

The survey and answer data are consistently gated at the known page and API boundaries, with no evidence of data leakage; the remaining findings concern misleading landing-page navigation and an explicit maintainability-rule violation.

Files Needing Attention: src/components/elections/ElectionLanding.tsx, src/components/elections/WardDetail.tsx

Important Files Changed

Filename Overview
src/lib/elections/registry.ts Adds centralized survey-closure and questionnaire-withholding flags plus shared href and notice helpers.
src/lib/elections/survey-answers.ts Removes answer shapes and policy prose while retaining candidate-authored biographies when questionnaires are hidden.
src/app/api/elections/survey/route.ts Rejects valid submissions for elections whose voter survey is closed.
src/app/api/elections/candidate-responses/route.ts Prevents the public proxy from returning hidden candidate response data.
src/components/elections/ElectionLanding.tsx Removes survey-dependent copy but still advertises candidate answers while those answers are withheld.
src/components/elections/WardDetail.tsx Preserves the ward ballot and coming-soon state but unnecessarily evaluates the survey href twice.
src/app/toronto/vote/2026/issues/page.tsx Replaces field-wide answer statistics and cards with an explicit withheld state.
src/app/toronto/vote/2026/mayor/page.tsx Retains the mayoral ballot while removing answer-derived cards, counts, and survey invitations.
src/app/toronto/vote/2026/candidates/[candidate]/page.tsx Preserves candidate biographies while replacing questionnaire positions with a coming-soon heading.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  R[Toronto election registry] --> SC{surveyClosed}
  R --> QH{questionnaireHidden}
  SC -->|true| SP[Survey page returns not found]
  SC -->|true| POST[Survey submissions return not found]
  SC -->|true| INV[Survey invitations disappear]
  QH -->|true| GET[Candidate-response proxy returns not found]
  QH -->|true| PAGES[Race, ward, issue, and candidate pages hide answers]
  QH -->|true| BIO[rosterSurvey retains biography only]
  PAGES --> BALLOT[Ballot and campaign information remain visible]
Loading

Fix all with Greploop Fix All in Codex Fix All in Claude Code

Prompt To Fix All With AI
### Issue 1
src/components/elections/ElectionLanding.tsx:692-696
**Copy promises hidden answers**

When Toronto’s questionnaire is hidden, this section still tells visitors to “See how they answered, question by question and ward by ward.” The retained mayor and issues links then lead only to the “Candidate survey coming soon” state. Use `election.questionnaireHidden` to replace the answer-oriented introduction and card descriptions so visitors are not sent looking for unavailable content.

### Issue 2
src/components/elections/WardDetail.tsx:229-230
**Survey href evaluated twice**

This evaluates `surveyHref(election)` twice and then suppresses the resulting type uncertainty with `!`. That violates the repository directive to avoid overly complicated code that can be simplified. Compute the href once beside `withheld`, then use the narrowed value for both the condition and prop. This repository requirement must be satisfied before merging.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "Say "Candidate survey coming soon", once..." | Re-trigger Greptile

Comment on lines 692 to +696
We put the same questions to every candidate on the ballot. See how
they answered — then answer them yourself and find out who lines up
with you.
they answered
{invitesSurvey
? " — then answer them yourself and find out who lines up with you."
: ", question by question and ward by ward."}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Copy promises hidden answers

When Toronto’s questionnaire is hidden, this section still tells visitors to “See how they answered, question by question and ward by ward.” The retained mayor and issues links then lead only to the “Candidate survey coming soon” state. Use election.questionnaireHidden to replace the answer-oriented introduction and card descriptions so visitors are not sent looking for unavailable content.

Knowledge Base Used:

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/components/elections/ElectionLanding.tsx
Line: 692-696

Comment:
**Copy promises hidden answers**

When Toronto’s questionnaire is hidden, this section still tells visitors to “See how they answered, question by question and ward by ward.” The retained mayor and issues links then lead only to the “Candidate survey coming soon” state. Use `election.questionnaireHidden` to replace the answer-oriented introduction and card descriptions so visitors are not sent looking for unavailable content.

**Knowledge Base Used:**
- [Municipal election engagement](https://app.greptile.com/buildcanada/-/custom-context/knowledge-base/buildcanada/tradingpost/-/docs/municipal-elections.md)
- [Toronto candidate and issue guides](https://app.greptile.com/buildcanada/-/custom-context/knowledge-base/buildcanada/tradingpost/-/docs/toronto-candidate-and-issue-guides.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Codex Fix in Claude Code

Comment on lines +229 to +230
{surveyHref(election) && (
<SurveyCta href={surveyHref(election)!} />

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Survey href evaluated twice

This evaluates surveyHref(election) twice and then suppresses the resulting type uncertainty with !. That violates the repository directive to avoid overly complicated code that can be simplified. Compute the href once beside withheld, then use the narrowed value for both the condition and prop. This repository requirement must be satisfied before merging.

Context Used: Be suspicious of things that are overly complicated and could be simplified (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/components/elections/WardDetail.tsx
Line: 229-230

Comment:
**Survey href evaluated twice**

This evaluates `surveyHref(election)` twice and then suppresses the resulting type uncertainty with `!`. That violates the repository directive to avoid overly complicated code that can be simplified. Compute the href once beside `withheld`, then use the narrowed value for both the condition and prop. This repository requirement must be satisfied before merging.

**Context Used:** Be suspicious of things that are overly complicated and could be simplified ([source](https://app.greptile.com/review/custom-context?memory=instruction-0))

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Codex Fix in Claude Code

@mikaalnaik
mikaalnaik merged commit a857220 into main Sep 11, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant