Deploy the Rust voting replacement with current feature parity
Goal Deploy PR #4220 as the production replacement for the current Rails voting runtime while preserving the agreed, currently used voting behavior. Rust owns all new voting runs and ballots; Rails continues to own authorization, voting-right source data, encrypted content and the surrounding meeting UI. Scope boundary This is a parity-and-deployment task, not a broader voting redesign or production-hardening program. Match the current system where users depend on it, accept explicitly agreed differences, and use the smallest maintainable solution. Do not require the Rust replacement to be materially better than the Rails implementation before deployment. Blocker scope rule Every implementation blocker must map to an existing supported Rails workflow that the replacement loses or breaks, or to the minimum wiring required by the new Rust process/database. Inspect the Rails behavior and reproduce the relevant regression before expanding a fix. Static-review concurrency/lifecycle findings are focused regression candidates, not authorization for general hardening. Existing Rails bugs and speculative improvements are not deployment requirements. Reuse the existing UI, projections, builders, locking boundaries, deployment and backup mechanisms; do not introduce parallel frameworks. Checks that already pass do not become new implementation projects. Current baseline - The relational Rust runtime, signed Rails client, PollView adapter and shared Rails UI are implemented. - Public meeting/case voting supports binary and ordered proposal/point ballots, abstention, per-run electorates, manual votes, unanimous outcomes, cancellation, supersession, casting weight and bias reconciliation. - Politician controls, secretary views, public information and bigscreen rendering recover from authoritative Rust reads. - Current master behavior for close-meeting proposals, case-specific voting labels and committee-level suppression of incomplete live tallies is integrated. - PR #4220 includes the resolved merge of master 06e28029f as 09ed5a90d and the voting visibility metadata work as 2b1553354; both are committed and pushed. Gitea confirmed the branch mergeable before the latest update, and the branch worktree is clean. Preserves newer proposal ordering/session-refetch/privacy, payroll authorization, both deployment settings and all six locale additions without restoring legacy voting. Shared Case voting-control broadcasts retain Rust reconciliation suppression, covered by a regression. Validation: 677 distinct Rails examples passed including the real Rails-Rust HTTP round trip; all 18 Rust tests passed including dedicated PostgreSQL integration; cargo fmt/check, JS/CSS builds, icons, translations, deployment YAML and Zeitwerk passed. RuboCop ran across 266 merge-changed Ruby files and reports 1801 offenses; nine of ten resolution-related Ruby files are clean, and the remaining eight Meeting offenses also exist on master. Conflict resolution is published; broader parity and deployment acceptance remain outstanding. No browser acceptance or production validation was performed. - Anonymous/public-result projection work is committed and pushed on the PR branch as 2b1553354: Rust stores immutable `anonymous` and editable `public_result` run metadata, exposes a secretary-only visibility command, and Rails restores the start checkbox, ended-run visibility edit, anonymous aggregate-only rendering, bigscreen party-breakdown suppression and public-result gating through existing components. Focused Rails coverage passes (85 examples); Rust format/Clippy/unit/auth/logging/contract/dedicated-PostgreSQL tests pass; the real Rails-Rust round trip passes; changed Ruby files pass RuboCop; Zeitwerk passes. The architecture document compiles and all ten rendered pages were reviewed. Core anonymous-voting implementation is complete and its checklist item is checked, with additional verification confirmed by the user. Anonymous report/research-export parity remains tied to the broader unfinished report/export work and its outstanding parity checklist item; this does not block marking the core implementation complete. Required parity work 1. Support authorized voting and history for private meetings/cases using the existing Rails access, Vault and BankID boundaries. Private runs must never reach public or bigscreen audiences. Planning direction following the user's explicit request for Rails parity rather than a new security layer: reuse Ability/CaseAccess and attendance-role rules in Rails, existing VotingRight-derived electorates, Vault::FieldDecryptable and BankidVerifiable. Rust should continue trusting existing scoped signed Rails commands/reads; do not duplicate CaseAccess, attendance, Vault or BankID policy in Rust, invent new permissions/tokens, or add a separate private voting engine. Remove blanket private-voting rejection only on authenticated paths, preserving public-reader and bigscreen exclusions. Confidential lifecycle/result updates should reuse existing polls-stream/voting-frame re-fetch wrappers so each browser authorizes and decrypts in its own session; do not push decrypted private HTML from jobs or include restricted support users merely because they are in a generic staff list. Wire the existing secretary poll/run and politician voting-frame rendering paths to authorized session-scoped decryption and hydrate the poll's actual source objects rather than requerying away decrypted values. Preserve distinct existing case.is_public field-encryption rules versus meeting.public audience rules. Source case/proposal text stays in Rails; legacy Poll custom title/summary fields were not Vault-encrypted, so adding encryption to Rust metadata would be a separate policy change rather than required parity. User clarification: non-public primarily protects meeting/case details (documents, titles, proposals and similar content) from public exposure; it is not a different voting security model. Do not frame BankID versus ballot submission as an unresolved product/security decision. Preserve ordinary voting for entitled participants and the existing Rails audience, authorization and decryption behaviour without adding new restrictions or a new security layer. BankID remains part of existing content handling, not a new Rust voting requirement; do not rederive voting rights from CaseAccess. Existing automatic protocol generation is public-only in legacy Rails too; retain that boundary rather than inventing background private decryption. Reuse authorized session-based private protocol regeneration and encrypted Case fields. Verification should extend existing access/decryption/voting tests for councillors, deputies/partial replacements/explicit grants-denials, secretary/leader/support restrictions, expired verification, private meetings, manual/unanimous/chair flows, and absence of private public/bigscreen broadcasts. Implemented and verified locally on the existing branch. Authenticated Gateway reads, starts and voting eligibility now support non-public cases/meetings through unchanged Rust commands and VotingRight-derived electorates; public-reader exclusions remain. Added one shared Rails voting-decryption concern and a session preparation callback on the existing voting frame. Existing Vault field hydration now deduplicates field requests and hydrates every loaded instance of a record. Private lifecycle/manual-vote updates reuse content-free voting/case/staff frame refreshes, with staff selected through existing case-update permissions; no private bigscreen polling job is scheduled. Bigscreen reads and proposal rendering reuse the existing cloud-router content visibility predicate, including public cases inside private meetings. Authorized private protocol regeneration/export decrypts source and saved fields in-session, persists through existing encrypted Case fields, and preserves saved text if decryption is unavailable; automatic generation retains its public-only boundary. No Rust schema, contract, database, voting semantics or permission-model changes. Validation: five initial private-flow failures were reproduced before enabling the integration. Final broad ordinary suite: 791 passed, eight HTTP examples skipped there; all eight HTTP and sixteen real Chrome/system examples subsequently passed through the isolated Rust service (815 distinct Rails/browser examples). The two new multi-session private-case/private-meeting browser scenarios exercise ordinary votes, manual attribution/selection refresh, close, chair decisions, unanimous outcomes, decrypted history and bigscreen exclusion. Request/concern/protocol coverage verifies encrypted source bodies are not copied into start snapshots, explicit deputy grants/denials and councillor denial, support restrictions, existing verification behaviour without a new ballot check, private frame-only broadcasts, duplicate-instance local/remote decryption, encrypted protocol persistence, failed-decryption edit preservation and DOCX content. All28 Rust tests passed including dedicated PostgreSQL integration. All21 changed/new Ruby files pass RuboCop; format/check/strict Clippy/rustdoc, Zeitwerk and whitespace checks pass. Existing ICU collation/tooling warnings remain. With explicit user authorization, changes were committed as58d41f828 (feature: Support non-public voting) and pushed to the current branch fix/synchronize-participant-voting-controls, updating the existing PR#4220. Working tree is clean and synchronized with origin. No reviewer or production changes were performed. User requested deployment of58d41f828 to voting staging. Initial read-only SSH preflight timed out before deployment started. On the user's explicit retry request, the same configured SSH access succeeded without authentication changes. Confirmed voting web/workers remain on01d2f6cab with zero restarts, and baseline Meilisearch/visma/maria start times were recorded. Deployment of58d41f828 to voting staging completed successfully through bin/staging deploy voting (exit0). Released image58d41f828-voting, digest sha256:f9e012deba157f3d6242cf4623c163c83e71dcd62052c36495478493a5bd4947. The deployment verified Rails/Rust/Meilisearch health, signed voting API reads from workers, search readiness and all five S3 buckets; external HTTPS /up returned200. The standard script also pulled the current ruststreamer:latest image, digest sha256:ca3817fa522dde49e522e2068adec986142917df79a6550d8f1c08717cab05cc. Post-deploy read-only SSH verification initially timed out; it succeeded on the user's explicit retry without authentication changes or redeploying. Confirmed voting web/workers run58d41f828-voting, and web/workers/ruststreamer are running with zero restarts. Meilisearch, visma and maria retain their exact pre-deploy start times. External HTTPS /up again returns200; local working tree is clean and synchronized with origin. The Rails probe executed in a READ ONLY transaction and found no non-public cases or cases in non-public meetings, so an actual private-case authenticated/public-reader smoke check could not be exercised on staging. No test data was created and no case/voting data was changed. Local815-example Rails/browser and28-test Rust validation remain passed; no production changes or live voting mutations were performed. The transport blocker is resolved. Live private-flow acceptance remains unverified: no private staging data was available, and the user flagged that staging lacks a crypto service. Crypto-service availability/configuration has not been independently checked. Private field writes require the existing Vault encryption dependency; reading encrypted titles/proposals/documents requires the existing decrypt service and normal session identity. Successful public/API health checks do not validate those dependencies, and the isolated browser tests used development/test-local encryption rather than a live crypto deployment. End-to-end private staging validation therefore needs the existing crypto integration configured for staging plus representative private data; do not bypass encryption, weaken access rules or infer production-crypto access. 2. Preserve the current per-run result-publication behavior at the existing audience boundaries. Rails public_result gates the public-site result component; it is not a global ban on internal results, reports or research exports. Do not extend it to audiences where Rails does not apply it. The user has explicitly requested custom poll titles and rich-text summaries, plus complete unanimous-for/against presentation, before implementing historical migration. Extend the existing Rust metadata storage/command and Rails edit modal/projection rather than adding a parallel editor; allow clearing overrides, preserve anonymity/visibility boundaries and keep explicit unanimous outcomes without fabricated individual ballots. Verify persistence, validation/authorization, shared rendering, both unanimous directions and the real Rails-Rust round trip. Historical import remains separate. Verified prerequisite outcome: custom titles and serialized Quill summaries now persist in nullable Rust voting_runs columns through the shared secretary-only /metadata command, replacing the undeployed /visibility command. The existing Rails edit modal supports editing/clearing; blank titles restore generated titles, empty editor documents clear summaries, and visibility-only edits retain custom text. Updates are bounded/validated, idempotent and prohibited for cancelled/superseded runs; ballots, capacity and anonymity are unchanged. Existing explicit unanimous-for/against storage is retained; unanimous rejection now has an explicit verdict and the same result-only bigscreen presentation as approval, with specialized bias/close-meeting wording preserved. All 22 Rust tests passed including dedicated PostgreSQL integration; 174 ordinary Rails examples passed, with four HTTP examples skipped there and subsequently passing alongside all six system/browser examples through the isolated real Rust service (184 distinct Rails/system examples total). The browser flow exercised edit, reload and clearing in the real Quill modal. Cargo formatting/check/strict Clippy, changed-Ruby RuboCop (12 files), translations and Zeitwerk passed. Only existing collation/tooling warnings remain. No deployment, historical import or changes to the running development services; apply additive 20261028100000_voting_metadata.sql and release Rails/Rust together. 3. Keep current protocol, interactive report, DOCX and research-export behavior available for new Rust runs. Restore the existing case-completion generation of proceedings/decision, configured decision publication and resolution creation. Extend the existing shared poll projection/builders; do not redesign reporting. Cancelled/superseded audit runs must not contribute substantive decisions, matching Rails cancellation/reset removing those ballots rather than creating a new report/audit product. Protocol implementation verified locally: existing Quill builders now select closed runs in chronological order, exclude open/cancelled/superseded outcomes, deduplicate binary proposal content, support ordered proposal/point/mixed selections, preserve custom titles/summaries and vote placement, and never turn unresolved or rejected binary outcomes into approvals. Shared vote displays retain real unanimous/chair/casting results, aggregate abstention/missing-vote counts, frozen capacity and explicit missing-capacity notices. Anonymous protocols suppress voter/party details; a red-green casting-verdict regression also exposed a name leak in the shared PollView, fixed there with six-locale name-free wording. Case completion again generates both fields, applies configured publication and creates nonempty enabled resolutions. Background completion reuses the signed public-reader scope without an invented actor and passes one history to both builders; automatic completion remains public-only, while authorized private regeneration/export now uses the session-decryption flow described above. Secretary regeneration builds before saving, preserves edits on unavailable/invalid Rust reads, and reports an error instead of saving an error document. Existing DOCX writer exports the generated editable fields. Quill content detection is shared with poll metadata clearing rather than duplicated. User clarification: generated Vedtak must contain only the adopted body, without proposal/poll titles, presenter/type labels, point-number headings or versus context; proceedings retain their titles. This refinement is now implemented locally by removing the decision-title path while retaining rich body formatting, winning-point text once and all existing outcome exclusions. Empty proposal bodies do not become title-only decisions. Eleven strengthened/new regression expectations failed before the fix. Verified 85 ordinary reporting/controller examples plus eight real Rails–Rust HTTP examples (93 distinct examples), including proposal and point chair winners under both detail styles and exact body-only DOCX decision sections. All four changed Ruby files pass RuboCop and whitespace checks pass; the final outcome-exclusion guard was revalidated with42 focused examples and Ruby lint. With explicit user authorization, the refinement was committed as01d2f6cab (fix: Generate body-only protocol decisions) on fix/synchronize-participant-voting-controls and deployed through bin/staging deploy voting. Staging now runs01d2f6cab-voting, digest sha256:97f334063dff57f37b95a4a459db5e61ca003c08e4da2de3e5ab09d9eecc48c4. Deployment completed successfully; Rails/Rust/Meilisearch health, signed worker voting read, search preparation and all five S3 checks passed, and HTTPS /up returns200. Web/workers use the intended image with zero restarts; Meilisearch, visma and maria retain their previous start times. A post-deploy Rails READ ONLY transaction generated decisions and DOCX output for actual closed Rust runs on staging cases8,7,5,4, comparing both generated decision text and the DOCX decision section against adopted source bodies only; all four passed and saved proceedings/decision fields were verified unchanged. Working tree is clean. No source push, browser acceptance or production deployment was performed. Existing saved Vedtak must be explicitly regenerated to remove previously stored title text; they were not overwritten. Validation: initial protocol regressions reproduced 13 failures; additional anonymous casting-verdict controls reproduced both detail-style leaks. Final ordinary suite passed 719 examples, with six real-HTTP examples skipped there and subsequently passing through the isolated Rust API (725 distinct Rails examples total). The two new real-HTTP cases cover anonymous mixed chair decisions, cancelled history, metadata, background generation, publication/resolutions and DOCX output in both detail configurations. All 28 Rust tests passed including dedicated PostgreSQL integration; cargo format/check, all 16 changed/new Ruby files under RuboCop, translation validation, Zeitwerk and whitespace checks passed. Existing ICU collation and tooling warnings remain. With user authorization, protocol changes were committed as 7e11924ef (feature: Generate protocols from Rust voting results) on fix/synchronize-participant-voting-controls and deployed via bin/staging deploy voting to https://voting.staging.kaukus.no. The release also includes the previously committed responsive fix 56bb884b9. Deployment completed successfully with image 7e11924ef-voting, digest sha256:07484de6d3b49bfbeaffeeea8ea2436175b1bfa5c23318b3b650a9c82bc9e3f6. Rails/Rust/Meilisearch health, signed voting read from workers, search readiness and all five S3 bucket checks passed; public HTTPS /up returns 200. Web/workers run the intended image with zero restarts; Meilisearch, visma and maria retain their previous start times. A post-deploy Rails READ ONLY transaction generated proceedings/decisions and validated DOCX files against actual closed Rust runs for staging cases5 and4 (two and one closed runs respectively; DOCX3846/3407bytes), without saving protocol fields or changing case/voting data. Working tree is clean. The new source commit was not pushed; no browser acceptance, historical import, standalone voting/research-export implementation or production deployment was performed. The broader reporting/export/private/history parity item remains outstanding. 4. Preserve access to pre-cutover voting history with the smallest maintainable approach. Evaluate a one-time import into the Rust relational model rather than defaulting to a permanent legacy-history adapter: the user prioritizes removing ongoing compatibility maintenance and accepts some historical data loss. The exact loss policy and final approach remain to be agreed. There must be no legacy runtime writes or dual-write voting path. Import-fit investigation: legacy single-proposal/single-point ballots map to binary; versus/several-points map to typed selection. Choices, abstentions, manual actor IDs, visibility and qualifying double-vote weights fit existing Rust storage. Ten temporary synthetic checks using the actual Rust domain and repository-locked dependencies passed; no source/database changes or production-data audit was performed. Empty capacity is already supported, but using it loses displayed voter names/party breakdowns as well as seat/right context. Legacy role-owned ballots require user mapping and duplicate-user/orphan checks; the electorate can be reconstructed where historical rights allow it, but using only recorded voters loses nonvoter counts. Closed timestamps were not recorded per legacy poll and need an explicit approximation policy. The initial title/summary storage gap is now resolved by the verified runtime prerequisite above. Preserve these fields in the eventual migration rather than dropping them. Historical unanimous-attribution decision: the user explicitly accepts unknown attribution; preserve an explicit unanimous-for/against resolution with actor_id=null rather than inventing a secretary. Add nullable resolution-actor support through SQL/domain/contracts/Rails validation while keeping new authenticated unanimous commands attributed to their actual secretary. Legacy generated ballots for explicitly recorded unanimous approvals must still be converted appropriately by the eventual importer because explicit resolutions do not coexist with individual ballots. Additional source-code finding: Rails disapprove_unanimously! writes unanimously_approved=false (also the ordinary default) and generates against ballots; the retained poll schema has no separate unanimous-rejection flag. Consequently a declared unanimous rejection cannot reliably be distinguished from an ordinary all-against ballot using these rows alone. Preserve its recorded ballots/outcome without asserting declaration provenance unless other reliable evidence exists. Production frequency remains unaudited. Target representation implemented and verified: resolution actor is nullable through the additive 20261028110000_unknown_resolution_actor.sql constraint migration, Rust domain/contracts/persistence and Rails snapshot validation. NULL explicitly means unknown; recorded actor IDs remain positive, and new unanimous commands still require and record their authenticated secretary. Both for/against unknown resolutions were persisted through RunTransaction and retrieved through actual Rust HTTP query/history endpoints; Rails validation/rendering preserves unanimous outcomes without inventing names or ballots. Red controls reproduced null rejection before the fix. All 24 Rust tests passed including dedicated PostgreSQL integration, 75 focused Rails examples passed and all four real Rails-Rust round-trip examples passed; cargo format/check/strict Clippy, changed-Ruby RuboCop and whitespace checks passed. README documents the boundary. Title/summary, unanimous-result and unknown-attribution changes committed together as 38003280d (feature: Add poll metadata and preserve unanimous history) on fix/synchronize-participant-voting-controls. The prerequisite and chair-decision commits are local and not pushed. No production changes or historical import; deploy the new additive migrations with the paired Rails/Rust release. The user explicitly requires chair-selected tied outcomes as a foundational Rust runtime feature, not an import-only exception. Implement a distinct chair_decision resolution using the existing relational resolution columns and shared result projection. A secretary/meeting leader records a choice among positively voted tied leading options after close, without changing ballots/counts or fabricating a casting vote; support correcting/resetting that decision, immutable inactive runs, existing finalized-bias correction guards and mutually exclusive casting/unanimous resolutions. New commands retain the authenticated recording actor; historical reads may retain actor_id=null. Reuse the existing edit/result UI, command locking, client, broadcasts and report builders. Verify binary and typed selection/mixed ballots, ties closed with missing votes, rejection of untied/blank/zero-vote/invalid/ineligible commands, persistence, same-command idempotency, correction/reset, privacy, report/result display and real HTTP/browser flows. Historical importer remains separate. Verified chair-decision outcome: Rust now persists a first-class chair_decision resolution in the existing resolution columns. Secretary-only choose/change/reset commands use the existing per-run transaction lock and shared resolution writer; commands record the authenticated staff user, while historical reads permit unknown actor. Positive tied leaders are validated across binary/typed selection ballots, including closed polls with missing votes. Original ballots, weights, capacities and totals stay unchanged; casting, unanimity and chair decisions cannot overlap. Finalized-bias guards remain in Rails. The secretary result card and edit modal share one decision component; shared results, bigscreen and protocol vote-display strategies retain the chosen outcome and privacy/publication behavior. The real browser verified choose/change/reset, persistence after reload and live bigscreen updates without phantom votes. A shared cross-language historical-chair fixture and focused regressions cover unavailable attribution, invalid/empty/untied choices, no-op idempotency, correction/reset, SQL rollback and chair-versus-casting concurrency. All 28 Rust tests passed including dedicated PostgreSQL integration; 189 ordinary Rails examples passed with four HTTP examples skipped there, then all four HTTP and seven system/browser examples passed through the isolated real Rust service; one additional anonymous/publication chair-result regression also passed (201 distinct Rails/system examples total). Format/check/strict Clippy, rustdoc, all 15 changed/new Ruby files under RuboCop, three six-locale key validations, Zeitwerk and whitespace checks passed. Patch-scoped four-perspective static review reported no actionable findings; it did not execute tests. Existing ICU collation/tooling warnings remain. Chair-decision changes committed as 8d8370a16 (feature: Support chair-selected tie outcomes) on fix/synchronize-participant-voting-controls. Worktree is clean; not pushed or deployed, and no historical import performed. Apply additive services/kaukus/migrations/20261028120000_chair_decisions.sql and release Rails/Rust together. Missing proposal/point sources, empty electorates and invalid casting states need preflight classification. Imported history must not replay Rails source lifecycle effects or expose historical correction actions as if it were a new run. A read-only production-shaped preflight is still needed to quantify exceptions before implementation or deleting any legacy tables. 5. Preserve existing meeting/case end, reset and deletion workflows, including confidential cases and cleanup of pre-cutover foreign-key references. Use the smallest lifecycle adaptation needed so these actions do not leave an active Rust run pointing at deleted/reset Rails sources. Existing Rails cancellation resets only ongoing points, so preserve independently completed points rather than replaying inactive Rust runs over them. Rails deliberately deletes ballots during cancellation/reset/data cleanup: no new guarantee to retain history after an intentional deletion is required. A simple guard or terminal transition is sufficient where appropriate; no distributed transaction or retention subsystem. 6. Confirm the existing tie/casting workflow remains usable. Do not introduce new voting semantics unless an actively used Rails workflow would otherwise be lost. Check the reported unanimous-while-open and electorate/replacement races against the existing Rails ongoing-poll validation, meeting write lock and roll-call guard; fix demonstrated replacement regressions using the existing serialization boundary, not a new command-ordering or distributed coordination protocol. Restore existing politician result-slot/observer-tally updates using the shared broadcasts rather than adding new delivery guarantees. 7. Restore the existing Rails anonymous-voting flag as a production-deployment blocker. This is result-display/export parity, not unlinkable ballot storage: Rails still stores each choice against a role and supports ordinary voting, vote changes and manual votes. Let the secretary select anonymity at ballot creation, persist it as immutable run metadata, show aggregate counts/verdict and an anonymous label, and suppress per-choice voter names and party breakdowns in the existing result views/broadcast HTML and reports. Preserve research-export behavior: anonymous=true and no voter name/party, while retaining its existing per-ballot choice/weight/manual fields. The secretary may still see who has not voted, and voters may see their own selected choice. Reuse existing shared rendering and ordinary voting semantics; do not require cryptographic anonymity, identity removal from storage/internal signed Rails-Rust responses, a new logging subsystem or a redesigned manual/casting workflow. Do not silently ignore the anonymous creation flag. Staging preparation (current scope) User authorized deploying this PR through bin/staging into a new voting environment, leaving existing visma/maria environments untouched. Clean initial checkout at 8d8370a16. Confirmed staging shared PostgreSQL/Redis/Caddy and S3 aliases exist, with sufficient disk. Fixed staging-specific Rust database/signing/API wiring, preserved per-environment signing keys on redeploy, added Rails database preparation for the supervisor command, and extended smoke checks to Rails/Rust liveness plus a signed read from workers. Verified staging outcome: deployed the current working tree at https://voting.staging.kaukus.no, then completed a full bin/staging deploy voting successfully. Final registry digest sha256:f02cbe478163632493a7385ee26eb29e9832f28a818b2199f09275898fe19986, tag 8d8370a16-voting. Fifteen focused deployment/entrypoint regressions pass after reproducing the original failures; changed Ruby specs pass RuboCop, shell/Ruby syntax and whitespace checks pass, cargo fmt/check pass, and the production image builds Rust and assets successfully. Static patch review caught Rust liveness returning 204 rather than 200; corrected the gate and its regression before completing the full redeploy. Live checks: HTTPS / and /up return 200, Rust health returns 204, unsigned voting reads return 401, and signed reads from the worker container succeed. All six Rust migrations succeeded in the dedicated database, with no Rails users/meetings tables; Rails prepared 575 migrations and seeded 18 users. Five per-env S3 buckets exist. Redeploy preserved both the signing-key fingerprint and database OID; env file mode is 0600 and no API port is published. Existing visma/maria web start times remain unchanged. No browser voting-flow acceptance performed. Separate shared-staging issues observed: Meilisearch endpoint timeouts (including worker connection-pool contention), Sentry 403 and blank Loki destination errors; not changed or presented as resolved. Deployment source changes committed as 8a6916eff (fix: Deploy the Rust voting service to staging); not pushed. Staging frame/hover regressions: reproduced meeting 4 / case 02/25 (case id5) generating 58 /proposals/frame fetches and 58 server-sent info_case_5 replacements in ten idle seconds with authorized Chrome login. Root cause is Gateway replaying older cancelled and newer closed runs over the same source on every read, oscillating source lifecycle and rebroadcasting reloads. Reconciliation now selects the latest run per typed source, preserving all history, partially overlapping selection sources and historical bias receipt reconciliation. The shared manual-vote link now disables Turbo hover prefetch while keeping click-to-open behavior. Five source regressions pass after four demonstrated failures; Chrome hover regression fails before opt-out and passes after; Chrome frame-load regression fails under the old history algorithm and passes with the fix. Broad ordinary suite: 196 passed, four HTTP examples pending there. Bounded real Rails-Rust suite: all four HTTP and seven system/browser examples pass. Broader suite is not fully green: two sequential-voter synchronization examples fail with both the new and old history algorithm; an initial bias-flow stale-element failure passed on focused rerun. These limitations remain explicit, not resolved by this patch. Four changed Ruby files pass RuboCop; whitespace checks pass; patch-scoped four-perspective static review found no actionable findings and did not execute tests. User authorized commit/push/staging deployment: fixes committed as aa8f5f2f7 (fix: Stop voting frame reload loops and hover prefetches), and both it and 8a6916eff pushed to the existing PR branch. Clean worktree. bin/staging deploy voting completed successfully with image aa8f5f2f7-voting, registry digest sha256:afa1fbf25b45e8c5edba5f2ff9ad9dd86f0e0327b5dead08549a670f0a350426. Rails/Rust health, signed worker read and all five S3 bucket checks passed. Authenticated live Chrome verification of meeting 4 / case5 now shows a completed, non-busy readable proposal frame across ten seconds with no ERR_ABORTED diagnostics. Independent Selenium instrumentation of that same idle interval reports zero /proposals/frame fetches and zero info_case_5 replacements (previously 58 each), and confirms info_case_5 completes its initial load. No active poll remained, so live nonvoter-hover validation was unavailable; the red-green local Chrome regression verifies no hover fetch and one click request opening the modal. Both requested frame/hover fixes are committed, pushed and deployed. Previously observed sequential-voter test failures were later resolved after rebuilding stale local assets; the complete18-example real HTTP/system run now passes as recorded below. Follow-up case-start investigation: user started meeting4 case03/25 id6 at 10:20:09Z; StartCaseJob transitioned it to ongoing at 10:20:13Z and persisted started_at=10:20:13.897634Z, then failed at app/jobs/start_case_job.rb:10 before the final ongoing-case-content broadcast with ActiveRecord::ConnectionTimeoutError. Retry at 10:20:53Z failed with AASM::InvalidTransition from ongoing. PostgreSQL read confirms case6 ongoing, case4 ended and case5 not_started; do not reset/restart the already-started case blindly. Staging workers have five Sidekiq threads but only a four-connection default Rails pool: shared env sets WEB_CONCURRENCY=2 and RAILS_MAX_THREADS=5, while database.yml reads singular RAILS_MAX_THREAD (missing, default1), giving 2*1*2. Search jobs also time out against the shared http://logs.kaukus.no:7701 endpoint and exhaust worker connections. Read-only authenticated Chrome verified that a fresh /meetings/4/secretary/ongoing_case load renders case03/25 title, proposal, speaker/voting controls and a completed non-busy frame; no case data was changed. Bounded fixes implemented locally: staging workers now explicitly receive RAILS_MAX_THREAD=5 (the existing production database.yml evaluates this to a 20-connection pool for WEB_CONCURRENCY=2), and StartCaseJob permits an already-ongoing case to retry its UI broadcasts without repeating start callbacks, while delayed jobs for ended/archived/missing cases return without restarting them. Four red controls reproduced the undersized pool/missing setting and retry/terminal-case failures; all 313 focused job, staging/entrypoint and secretary case/ongoing-case controller examples pass. The three changed/new Ruby files pass RuboCop, bin/staging passes Ruby syntax validation, whitespace checks pass and the 15 focused job/staging examples pass again after final formatting. The search endpoint remains unreachable and its job backlog is not disabled, cleared or presented as resolved. Case-start recovery and worker-pool changes committed as 0ab2bf686 (fix: Recover case start updates after worker failures) with user authorization. Commit 0ab2bf686 was subsequently deployed to voting with user authorization (not pushed); image digest sha256:742c2417116350e4d0e31c6c34057c0c95a73ffc099f788c86b1d20444c4b26f. Web/workers running with zero restarts, HTTPS/Rails/Rust/signed worker/S3 smoke checks passed. Runtime worker config verified RAILS_MAX_THREAD=5 and pool_size=20. The post-deploy Chrome check was cancelled and is not claimed as passed. Follow-up slow-start evidence: all five Sidekiq threads were occupied by MeilisearchIndexJob, four PostgreSQL sessions waiting on its advisory lock; search queue had 8746 jobs with ~3503s latency. One StartCaseJob enqueued at10:32:00 began at10:34:38 and itself finished in466ms; another finished in913ms after a3.5s queue wait. User explicitly requested deploying a local Meilisearch container on staging. Verified deployed staging-voting-meilisearch using pinned getmeili/meilisearch:v1.45.2 (digest sha256:271ee84030a77cfa8e60358138bb34260f69110795c2441437b44832fe865618), private staging-network only, no published ports, authenticated production mode and persistent staging-voting-meilisearch-data volume. Each environment now receives its own server URL/master key in its mode-0600 env file to prevent collisions between identical Rails production index names; secrets are preserved on redeploy. bin/staging create/deploy/destroy and readiness now manage the per-env search lifecycle and reuse MeilisearchDeployment for settings/backfill. Deployed operationally using the existing 0ab2bf686 Rails image, gracefully restarting only voting web/workers; visma/maria and voting ruststreamer start times unchanged. Rails/Rust/search health, signed worker read and HTTPS checks passed. Search settings/backfill completed and readiness is true; Meeting/Case/Proposal indexes contain4/6/4 documents and an authenticated sample query returns2 hits. Unauthenticated stats returns401. Initially retained the indexing queue and observed it draining (8746 before,8513 then8031). User then explicitly authorized clearing the backlog: verified APP_HOST=voting.staging.kaukus.no, private search URL and prepared indexes, then cleared only that environment's queued meilisearch jobs (7551 before,0 immediately after). High/default queues were0 and untouched; retry/scheduled sets and in-flight jobs were not modified. No live case-start mutation was performed to benchmark latency. All21 staging/entrypoint tests, changed Ruby spec RuboCop, Ruby syntax and whitespace checks pass. User explicitly declined a reviewer; requested review was aborted and no review outcome is claimed. Search deployment script/docs/tests committed with user authorization as 5355905af (feature: Add private staging Meilisearch containers). Both 0ab2bf686 and5355905af were subsequently pushed with5a1147c4a on the existing PR branch. Voting-start overlap verified and fixed locally: clicking the source-title span caused button_loading_controller to treat event.target (the span) as the button, then restore button-sized classes onto that inner span, reproducing the exact green overlay/clipped label from the screenshot. The shared handler now uses event.currentTarget and restores original innerHTML rather than flattened text, preserving both text lines and proposal-selection role hooks after fallback. No duplicated view markup or CSS workaround was introduced. A real Chrome regression reproduced the screenshot and failed before the fix; it now checks whole-button disabling/restoration, nonoverlapping text at1440/390px viewports with620/358px panels, long selected titles and continued selection updates. JS/CSS builds, changed-Ruby RuboCop and whitespace checks pass. All18 real Rails-Rust HTTP/system examples pass (four HTTP, nine synchronization, five point-selection/browser examples). The earlier sequential-voter failures no longer reproduce after rebuilding stale local JS/CSS artifacts; they are not a current validation blocker. No reviewer invoked. With explicit user authorization, overlap fix and browser regression committed as5a1147c4a (fix: Preserve voting button labels during loading), pushed with0ab2bf686/5355905af and deployed through bin/staging deploy voting. Final image5a1147c4a-voting, digest sha256:02974ddf3e691342eb8e75596a06616a7393bf7b2a6a02233382eae333113e36. Rails/Rust/Meilisearch health, signed worker read, search readiness/backfill and all five S3 checks passed; web/workers running with zero restarts and existing Meilisearch start time preserved. HTTPS /up200. Live authenticated Chrome confirmed case03/25 controls and separated main label/subtitle at1440px and390px without clicking or mutating votes. The overall live smoke check did report a separate390px layout failure: side-by-side columns, heading spill and horizontal clipping/overflow; not fixed or presented as passed. Click/loading/fallback regression remains verified locally, not exercised against live voting. Working tree clean and branch synchronized with origin at5a1147c4a. Narrow-screen ongoing-case fix implemented and verified locally after user request; prior changes were already committed, so no duplicate commit was made. Existing ongoing-case partial now stacks proposal/voting and speaker panels below xl, allows columns/content to shrink and wraps the case identity/actions, including long unbroken titles. Browser geometry also exposed directly relevant shared-navigation overflow: the fixed260px logo appeared before the desktop sidebar breakpoint, the meeting header could not shrink, and the tab bar could not wrap. Reused the shared root-path partial and TabComponent: align logo visibility with lg sidebar, make header sizing/gaps/font responsive, allow tabs to wrap and set min-w-0 on main content. No duplicated markup or overflow-hiding workaround. New Chrome system regressions reproduced original forced-column/overflow failures and now verify320/390/768/1024px stacked layouts with no document/content horizontal overflow or clipped controls,1440px side-by-side desktop panels, long case titles and a working edit-modal link. All63 relevant component/controller/view examples and all20 real Rails-Rust HTTP/system examples pass (83 distinct examples); Tailwind build, changed Ruby spec RuboCop and whitespace checks pass. Responsive source changes and regression committed with user authorization as56bb884b9 (fix: Keep ongoing case controls within narrow screens) and pushed to the existing PR branch. Working tree clean and synchronized with origin; not deployed. No reviewer invoked. No case reset or restart was performed. No production changes. No production rollout; production parity, backup/restore and cutover criteria remain separate and incomplete. Minimum deployment work - Provision a dedicated Rust voting database for each production environment and load KAUKUS_DATABASE_URL plus the shared KAUKUS_SIGNING_SECRET through the existing secret mechanism. - Make the Rust endpoint reachable from every web or worker process that actually calls it; specifically make the existing bigscreen/public polling job work without loopback-to-the-wrong-container failures. - Include the Rust voting database in the existing backup routine and verify one restore. - Preserve automatic Rails database preparation under the new supervisor command and verify that Rust starts/migrates and is reachable in the existing deployment smoke/health path. This is minimal startup wiring for the added process, not a new readiness/dependency-monitoring platform. Validation and rollout - Add focused regressions only for retained parity, authorization/privacy, lifecycle guards, reports/history and deployment wiring. For anonymous voting, extend existing parity coverage for creation/flag immutability, unchanged voting behavior and suppression of voter names/party breakdowns in result views and exports; include it in the existing round-trip/browser validation rather than introducing a separate certification program. - Run Rust format/check/tests with real PostgreSQL, focused Rails boundary tests, the real Rails-Rust round trip, relevant authenticated browser flows, assets, translations, Zeitwerk and changed-code RuboCop. - Build the production image and run a production-shaped staging smoke test. - Cut over only when no meeting or voting run is active, take the normal backups, deploy, and smoke-test secretary, voter, public and report paths. Keep the rollback note short: roll back before accepting new Rust-only votes; after that point use a deliberate fix-forward decision. Explicit non-goals - General metrics/alerting or observability redesign. - A new load-testing/capacity-certification project. A representative production-sized staging smoke check is sufficient. - Distributed transactions, outbox infrastructure or dual writes between Rails and Rust. - A comprehensive historical migration framework when read-only access is adequate. - New command-ordering protocols without a reproduced production-parity defect. - Redesigning voting rules, tie semantics, meeting management or unrelated Rails domains. Acceptance criteria - All agreed currently used voting workflows work through Rust for new runs with no Rails ballot writes. - Private/public authorization and result visibility match current production expectations. - Anonymous voting matches the current Rails flag behavior: immutable creation-time metadata, normal identity-linked voting internally, aggregate result displays without voter/party breakdowns, and existing anonymous report/export behavior. This parity is required before production cutover. - Current reports and historical voting remain available. - Existing meeting lifecycle actions work without stranding an open run; intentionally deleted ballot history does not require new retention guarantees. - Production secrets, database, web/worker connectivity and backups are configured for both deployed environments. - Production-image, staging smoke and focused parity validation pass, followed by a controlled production cutover.
Checklist
12 of 17 complete- Confirm the retained production parity matrix, intentional exclusions, and whether title/summary editing is actively required; choose the smallest safe legacy-history approach.
- Restore the bounded parity gaps: authorized private voting, per-run publication visibility, current reports/exports, and access to pre-cutover voting history without legacy writes.
- Preserve existing end/reset/delete and confidential-case workflows with minimal Rust lifecycle wiring and legacy-reference cleanup; preserve independently completed points. Reproduce consistency regressions against Rails guards/locking before fixing them. No new history-retention guarantee after intentional deletion or distributed coordination subsystem.
- Configure production deployment basics for both environments: Rust database, signing secret, web/worker connectivity, startup/readiness and inclusion in the existing backup/restore routine.
- Pass focused parity and real Rails-Rust validation, build the production image, complete a production-shaped staging smoke test, and perform the controlled production cutover.
- Restore core Rails anonymous-flag parity: immutable creation-time run metadata, normal identity-linked voting, and aggregate results/anonymous label without per-choice voter names or party breakdowns. Implemented in 2b1553354; focused parity and real Rails-Rust validation recorded, with additional verification confirmed by the user. Report/research-export anonymization remains covered by the outstanding reports/exports parity item.
- Before historical migration, persist and edit custom poll titles/rich-text summaries through the shared Rust/Rails metadata flow, support clearing overrides, and complete unanimous-for/against presentation. Verify authorization, validation, unchanged ballots/privacy, PostgreSQL persistence and the real Rails-Rust round trip.
- Allow historical unanimous-for/against resolutions to retain explicit unknown attribution (actor_id=null) through SQL, Rust and Rails reads; verify persistence/contracts/rendering while newly created unanimous decisions still require and record the authenticated secretary. No importer in this step.
- Implement first-class Rust chair-selected tie resolutions with unchanged ballots/counts, choose/change/reset commands, authenticated new actor and unknown historical actor, existing secretary UI/broadcast/report projection reuse and finalized-bias guards. Verify domain boundaries, PostgreSQL/HTTP persistence, Rails authorization/privacy and real browser workflow.
- Prepare bin/staging for the paired Rails/Rust release: per-environment dedicated database and signing key, reachable API for web/workers, automatic migrations and health smoke checks; deploy the PR to staging and record verified outcomes or blockers.
- Reproduce and fix staging meeting 4 / case 02/25 infinite frame cancellation/retry and unintended frame requests when hovering nonvoters; preserve existing voting reconciliation and manual-vote behavior with regression/browser validation.
- Investigate the staging case 03/25 start that changed the tab but left the body waiting; verify persisted state and StartCaseJob failure, reproduce the minimal regression and determine a safe recovery and bounded fix without restarting or changing cases blindly.
- Deploy a persistent private-network Meilisearch container for voting staging, isolate its data/indexes from other staging environments, update the reachable search configuration and verify indexing and worker queue recovery without clearing queued jobs or modifying visma/maria.
- Fix overlapping action label/source-title text in the shared secretary voting-start button, preserving source selection and submission behavior; reproduce and validate with actual browser layout at desktop and narrow widths.
- Fix ongoing-case narrow-screen overflow: stack proposal/voting and speaker columns, wrap the case heading/actions and keep controls inside the viewport; preserve desktop two-column behavior and verify with real browser layout regressions without hiding overflow.
- Restore protocol generation from authoritative Rust history through existing builders: closed substantive runs only, no invented pending/rejected decisions, typed proposal/point ballots, anonymous aggregate-only results, automatic case-completion generation/publication/resolution, secretary regeneration and existing DOCX export. Verify regressions and a real Rails–Rust round trip; historical import and standalone voting/research reports remain separate.
- Restore normal voting for non-public cases and meetings through the existing Rust integration, preserving Rails voting-right, content-visibility and decryption behaviour without a new security policy. Reuse session-rendered private frames/broadcast refreshes, existing public/bigscreen visibility and private history/protocol handling. Verify ordinary/manual/unanimous/tie flows, replacements and existing access cases through focused Rails tests and real Rails–Rust/browser acceptance; keep public voting unchanged.
- Area
- Kaukus
- Kind
- Feature
- Priority
- High
- Task ID
tsk_6adutxtxcy- Updated