diff --git a/docker-compose.override.local.yml b/docker-compose.override.local.yml new file mode 100644 index 000000000..cc288bd90 --- /dev/null +++ b/docker-compose.override.local.yml @@ -0,0 +1,7 @@ +services: + keycloak: + ports: + - "8081:8080" + my-db: + ports: + - "5433:5432" diff --git a/package-lock.json b/package-lock.json index 74f3f4889..57704ed6e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,11 +24,11 @@ "rimraf": "^5.0.5" }, "optionalDependencies": { - "@rspack/binding-darwin-arm64": "*", - "@rspack/binding-darwin-x64": "*", - "@rspack/binding-linux-arm64-gnu": "*", - "@rspack/binding-linux-x64-gnu": "*", - "@rspack/binding-linux-x64-musl": "*" + "@rspack/binding-darwin-arm64": "latest", + "@rspack/binding-darwin-x64": "latest", + "@rspack/binding-linux-arm64-gnu": "latest", + "@rspack/binding-linux-x64-gnu": "latest", + "@rspack/binding-linux-x64-musl": "latest" } }, "node_modules/@ai-hero/sandcastle": { diff --git a/packages/frontend/src/components/Election/PreliminaryResultsNotice.tsx b/packages/frontend/src/components/Election/PreliminaryResultsNotice.tsx new file mode 100644 index 000000000..2f5a5b025 --- /dev/null +++ b/packages/frontend/src/components/Election/PreliminaryResultsNotice.tsx @@ -0,0 +1,50 @@ +import { Link, Typography } from "@mui/material"; +import ElectionStateWarning from "./ElectionStateWarning"; +import useElection from "../ElectionContextProvider"; + +// docs/help/preliminary_results.md, as published by the docs site. +const ARTICLE_URL = 'https://docs.bettervoting.com/help/preliminary_results.html'; + +// Shared by this notice and the line in the submit-confirm dialog, so the two +// cannot drift apart — a voter who is shown one has to be shown the other. +// +// public_results does two jobs. While voting is open it means "live tally +// visible", which is what the notice is about. Once the election closes it means +// "final results published", which carries none of the same inference risk — so +// neither surface can follow the flag alone. +export function useShowsLiveTally() { + const { election } = useElection(); + return election.settings.public_results === true + && (election.state === 'open' || election.state === 'draft'); +} + +export default function PreliminaryResultsNotice() { + const { t, election } = useElection(); + const showsLiveTally = useShowsLiveTally(); + + if (!showsLiveTally) return <>; + + // Read voter_access directly rather than going through + // getVoterAuthenticationMode(), which throws on a non-canonical settings + // shape — that would take the whole ballot down with it. + const isClosedList = election.settings.voter_access === 'closed'; + + return + {isClosedList && + + {t('preliminary_results_notice.closed_list')} + + } + + {/* Explicit target, rather than a markdown link inside the i18n value: + the shared link renderer defaults anchors to _self, and navigating + away from the ballot discards every score the voter has entered. */} + + {t('preliminary_results_notice.link_text')} + + + +} diff --git a/packages/frontend/src/components/Election/Voting/VotePage.tsx b/packages/frontend/src/components/Election/Voting/VotePage.tsx index 4131e98cf..d8aad6a26 100644 --- a/packages/frontend/src/components/Election/Voting/VotePage.tsx +++ b/packages/frontend/src/components/Election/Voting/VotePage.tsx @@ -19,6 +19,7 @@ import { useSubstitutedTranslation } from "~/components/util"; import DraftWarning from "../DraftWarning"; import SupportBlurb from "../SupportBlurb"; import ElectionStateWarning from "../ElectionStateWarning" +import PreliminaryResultsNotice, { useShowsLiveTally } from "../PreliminaryResultsNotice" import WriteInSection from "./WriteInSection" import { NOTA_ID, makeWriteInCandidateId, isWriteInCandidate } from "@equal-vote/star-vote-shared/utils/makeID"; @@ -239,6 +240,7 @@ const VotePage = () => { } const {t} = useSubstitutedTranslation(election.settings.term_type) + const showsLiveTally = useShowsLiveTally() if(pages.length == 0){ @@ -263,6 +265,10 @@ const VotePage = () => { state="archived" title="archived_warning.title" description="archived_warning.description"/> + {/* Sits above BallotPageSelector so it renders once per ballot rather than + once per race, and so it also covers DraggableIRVBallotView, which + bypasses GenericBallotView entirely. */} + { {t('ballot.dialog_submit_title')} + {/* The banner above the ballot is scrollable-past, and it is greyed out + behind this dialog. This is the only surface the voter must actively + confirm, so it carries one sentence rather than nothing. */} + {showsLiveTally && + + {t('ballot.dialog_preliminary_results')} + + } {!receiptEmail && + Results for this {{election}} are public while voting is open. + warnings: skipped_rank: Do not skip rankings. Rank candidates in order to clearly show preferences. Candidates left blank are ranked last. duplicate_rank: Do not rank multiple candidates equally. (Ranking candidates equally can void your ballot.) @@ -851,6 +854,18 @@ draft_warning: All ballots will be counted as test votes and shall be reset prior to the final {{election}}. +preliminary_results_notice: + title: Results are public while voting is open + description: > + Anyone with a link to this {{election}} can see the results before voting closes, + and those results update as {{votes}} are submitted. In a small {{election}} — or + if only a few people vote in a short window — it can be possible to work out how + someone voted. + closed_list: > + This {{election}} uses a voter list. Administrators can see which voters have voted + and when, which alongside live results can narrow that down further. + link_text: What preliminary results reveal + temporary_access_warning: title: Unlock more with a free bettervoting account! description: >