Hide the Toronto voter survey and the candidates' answers, temporarily - #86
Conversation
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 SummaryThis 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.
Confidence Score: 4/5The 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
|
| 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]
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
| 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."} |
There was a problem hiding this comment.
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.| {surveyHref(election) && ( | ||
| <SurveyCta href={surveyHref(election)!} /> |
There was a problem hiding this comment.
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!
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.surveyClosedquestionnaireHidden/issues, and the read proxyFlags 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,/issuesand every ward page. A survey withdrawn from four is one readers still find from the fifth.surveyHref()returnsundefinedwhile 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.
rosterSurveynarrows 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.
/issueskeeps 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→ 404POST /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 --noEmitandnpm run lintclean (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-visualizationscarries 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