feat: Helios Booth 2026 — Lit web component voting booth#481
feat: Helios Booth 2026 — Lit web component voting booth#481benadida-agent wants to merge 33 commits intobenadida:masterfrom
Conversation
Four-phase implementation plan for rebuilding the voting booth with Lit web components, TypeScript, and Vite (booth2026). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy crypto libraries from the legacy booth to the new Lit-based booth2026: - Copy jscrypto directory with all crypto libraries (jsbn, sjcl, bigint, elgamal, helios, sha1, sha2) - Copy underscore-min.js utility library - Add TypeScript type declarations for crypto globals (BigInt, ElGamal, HELIOS, etc.) - Types enable type-safe crypto operations in the voting booth Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Create src/main.ts as the entry point that imports booth-app component - Create src/booth-app.ts with BoothApp LitElement component that: - Manages voting booth screen state (loading, election, question, review, submit, audit) - Holds voter state including election data, answers, and encrypted ballot - Implements election loading from server with metadata support - Provides screen navigation and exit functionality - Renders election info screen with start button - Includes placeholder screens for Phase 2-3 work - Update crypto/types.ts to remove global declarations (use window access instead) - Adjust tsconfig.json to disable unused locals/params checking for Phase 1 shell This provides the main application shell for the voting booth with state management ready for Phase 2 (voting flow) and Phase 3 (submission/audit). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Load jscrypto libraries and underscore before the app to ensure crypto globals are available when booth-app initializes. Scripts are loaded synchronously in dependency order before the module script that imports booth-app.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix crypto script paths from root-absolute (/lib/jscrypto/) to relative (../lib/jscrypto/) to work correctly when served at /booth2026/ in production - Add typed declare global block in crypto/types.ts for non-conflicting globals (HELIOS, b64_sha256, ElGamal, Random, UTILS, sjcl) with proper type narrowing - Update booth-app.ts to use declared typed globals directly instead of (window as any) casting - Move inline styles to CSS classes (.start-button-container and .help-email) in booth-app.ts static styles - Export BoothScreen type for child component usage in later phases Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…avigation Implement Task 2 of Phase 2 - integrate question screen component into booth app: - Add imports for question-screen component and event types - Implement handleAnswerChange to track answer selections with immutable updates - Implement validateCurrentQuestion to enforce minimum answer requirements - Implement handleNavigation to handle previous/next/review navigation with validation - Add goToQuestion method for editing answers from review screen - Add renderQuestionScreen method to render question with proper answer orderings - Update startVoting to show review button immediately for single-question elections - Update renderCurrentScreen to use renderQuestionScreen instead of placeholder Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Important Fix 1: Remove redundant checkbox accessibility attributes from li element. Native input type="checkbox" handles the semantics; role="checkbox", aria-checked, and aria-disabled on the li create duplicate announcements for screen readers. - Important Fix 2: Change warning-box color from var(--color-success, #28a745) to var(--color-text-secondary, #666) for proper semantic coloring of constraint messages. - Minor Fix 1: Skip validation in handleNavigation() for 'previous' direction to allow users to navigate back without meeting minimum answer requirements. - Minor Fix 2: Use explicit undefined comparisons in getConstraintText() to properly handle cases where min=0 or max=0. Verification: - TypeScript: npx tsc --noEmit ✓ - Build: npm run build ✓ - Tests: uv run python manage.py test -v 0 → 198 tests pass ✓ Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Task 7 implementation: - Added screen imports for review, submit, audit, and encrypting screens - Updated BoothScreen type to include 'encrypting' state - Added encryption-related state properties: worker, encryptionProgress, answerTimestamps, dirty tracking, encryptedBallot, auditTrail, rawElectionJson - Implemented worker initialization and async encryption handling - Updated loadElection to store rawElectionJson and initialize worker - Updated handleAnswerChange to mark questions as dirty - Updated handleNavigation to trigger sealBallot on review - Added handleReviewNavigation, prepareForCast, and auditBallot handlers - Added handleAuditNavigation, resetAndReencrypt, and postAuditedBallot methods - Added handleBallotSubmit for submission flow - Updated renderCurrentScreen with cases for all new screens - Updated getProgressStep to include encrypting screen - Updated beforeunload handler to warn on encrypting screen - Added getPrettyChoices and worker encryption helper methods - Fixed EncryptedVote interface to support includeRandomness parameter and optional clearPlaintexts method Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Critical: Add worker.onerror handler to catch encryption failures and set error state - Important: Fix worker instantiation to use import.meta.url instead of root-absolute path - Important: Fix encrypting.gif and loading.gif paths to use import.meta.url for production - Important: Remove dead hidden form from review-screen (submission is in submit-screen) - Minor: Replace `as any` with `Record<string, unknown>` in hasClearPlaintexts type guard - Minor: Make auditExpanded @State() reactive and remove manual requestUpdate() - Minor: Move inline styles to CSS classes in review-screen, audit-screen, submit-screen All TypeScript compilation passes, build succeeds, and all 198 Django tests pass. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…gement Implements Task 1 from Phase 4 accessibility enhancements: - Add skip link element with proper styling for keyboard navigation - Add ARIA landmarks: header with role="banner", nav with aria-label - Add focus management via focusMainContent() method for screen transitions - Call focusMainContent() in sealBallot(), prepareForCast(), and auditBallot() - Update render() to use proper semantic HTML structure - Add aria-live="assertive" to error alert for screen reader announcements - getProgressStep() method already exists and works correctly The main element now has id="main-content" and tabindex="-1" for focus management. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implement Task 2 from Phase 4 with: - isInitializing state property for tracking booth initialization - renderErrorScreen() method with recovery options (Reload, Return to Election) - Error screen CSS styles (.error-screen, .error-message, .error-actions) - Improved initializeBooth() with error classification (network, crypto, generic) - New waitForCryptoWithTimeout() method with 10-second timeout - Updated renderCurrentScreen() loading case with initialization message - Updated election case to show error screen when initialization fails - Enhanced loading display with role="status" and aria-live="polite" - loading-detail style for secondary messaging Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Update vite.config.ts with production build options: - Add sourcemaps for debugging - Configure terser minification - Add dev server proxy for Django API calls - Set publicDir to 'public' - Use hashed filenames for assets - Update index.html with metadata and production paths: - Add SEO meta tags (description, robots noindex) - Add noscript fallback for JavaScript requirement - Use relative paths for crypto scripts and styles - Add preload hints for critical assets - Create public/ directory and move GIF files: - Move encrypting.gif to public/ - Move loading.gif to public/ - Update references in encrypting-screen.ts and review-screen.ts - Add @vite-ignore comments to suppress build warnings - Install terser as dev dependency for minification - Verified production build succeeds with single bundle Build output: - dist/assets/booth.[hash].js (61KB) - dist/assets/booth.[hash].css (3KB) - Source maps generated for debugging - Hashed filenames for cache busting Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add documentation comment to the booth2026 URL route explaining how to switch from serving source files in development to serving built dist files in production deployment.
…uild Move encrypting.gif and loading.gif from root heliosbooth2026/ to public/ directory so they are properly included in the production build output. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Critical (C1): Fix aria-current attribute binding on progress steps
- Changed from string interpolation (which Lit ignores) to proper conditional
attribute binding using aria-current=${condition ? 'step' : nothing}
- Imported 'nothing' from 'lit' for proper undefined handling
Important (I1): Add focusMainContent() to startVoting() and goToQuestion()
- Both screen transition methods now call focusMainContent() like other
transitions (sealBallot, prepareForCast, auditBallot) for consistent
focus management and accessibility
Important (I2): Remove redundant role="main" from main element
- The <main> element has implicit role="main", explicit attribute was
redundant and violates HTML best practices
Minor (M1): Consolidate duplicate .error and .error-message styles
- Both classes had identical background, border, color, padding, and
border-radius CSS. Now consolidated with shared rule and context-
specific margin handling for .error-screen .error-message
Minor (M2): Remove useless preload link from index.html
- The <link rel="preload"> before the actual stylesheet provided zero
benefit and cluttered the HTML
All issues verified:
- TypeScript compiles without errors (npx tsc --noEmit)
- Production build succeeds (npm run build)
- No regressions introduced
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- C1: Fix crypto script paths in index.html - change ../lib/ to lib/ to resolve correctly when served from /booth2026/ - I1: Re-enable noUnusedLocals and noUnusedParameters in tsconfig.json and fix unused cryptoReady state - I2: Clarify production deployment comment in urls.py - serve full heliosbooth2026/ directory (not just dist/) because crypto libs in lib/ are needed - I3: Fix choices[index]?.length < question.max comparison - use nullish coalescing (?? 0) to avoid undefined comparison - M1: Change encryptedBallot type from unknown to EncryptedVote | null and remove unnecessary type casts - M2: Merge duplicate import statements from crypto/types into single import All issues fixed: ✓ TypeScript compilation passes with strict unused checking ✓ npm run build succeeds ✓ Django test suite passes (198/198 tests) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Introduces a new Lit + TypeScript + Vite-based “Helios Booth 2026” frontend served alongside the existing booth, including a web-worker-based encryption path and vendored legacy jscrypto libraries.
Changes:
- Added Django route to serve
/booth2026/content from a newheliosbooth2026/directory. - Implemented Lit screen components for the voting flow (question/review/encrypting/submit/audit).
- Added a Vite/TS build setup plus vendored crypto libraries and worker-based encryption.
Reviewed changes
Copilot reviewed 34 out of 38 changed files in this pull request and generated 49 comments.
Show a summary per file
| File | Description |
|---|---|
urls.py |
Adds a /booth2026/ static-serving route for the new booth. |
heliosbooth2026/index.html |
New booth HTML entrypoint loading styles, crypto libs, and app entry. |
heliosbooth2026/package.json |
Adds Vite/Lit/TS project configuration and build scripts. |
heliosbooth2026/package-lock.json |
Locks npm dependency graph for the booth. |
heliosbooth2026/tsconfig.json |
TypeScript compiler configuration for the booth source. |
heliosbooth2026/vite.config.ts |
Vite build/dev server configuration for /booth2026/ base path. |
heliosbooth2026/.gitignore |
Ignores node_modules and dist output for the booth project. |
heliosbooth2026/src/main.ts |
App entry module that registers/initializes the booth app. |
heliosbooth2026/src/crypto/types.ts |
TS type declarations for global jscrypto/HELIOS APIs and worker messages. |
heliosbooth2026/src/styles/booth.css |
Base styling for the new booth UI (responsive/contrast/motion). |
heliosbooth2026/src/screens/question-screen.ts |
Lit question UI with selection and navigation events. |
heliosbooth2026/src/screens/review-screen.ts |
Lit review UI showing selections, tracker, and cast/audit actions. |
heliosbooth2026/src/screens/encrypting-screen.ts |
Lit encryption progress UI. |
heliosbooth2026/src/screens/submit-screen.ts |
Lit submit UI with form POST to cast_url. |
heliosbooth2026/src/screens/audit-screen.ts |
Lit audit UI for spoiled ballot verification workflow. |
heliosbooth2026/workers/encryption-worker.js |
Web Worker that imports jscrypto libs and encrypts answers off-thread. |
heliosbooth2026/public/loading.gif |
Loading indicator asset. |
heliosbooth2026/public/encrypting.gif |
Encryption indicator asset. |
heliosbooth2026/lib/underscore-min.js |
Vendored Underscore.js dependency for legacy crypto code. |
heliosbooth2026/lib/jscrypto/README |
Vendored jscrypto README. |
heliosbooth2026/lib/jscrypto/jsbn.js |
Vendored jscrypto dependency. |
heliosbooth2026/lib/jscrypto/jsbn2.js |
Vendored jscrypto dependency. |
heliosbooth2026/lib/jscrypto/sjcl.js |
Vendored SJCL bundle used by jscrypto. |
heliosbooth2026/lib/jscrypto/class.js |
Vendored John Resig class helper used by jscrypto. |
heliosbooth2026/lib/jscrypto/bigint.js |
Vendored BigInt wrapper used by jscrypto. |
heliosbooth2026/lib/jscrypto/random.js |
Vendored RNG glue used by jscrypto. |
heliosbooth2026/lib/jscrypto/elgamal.js |
Vendored ElGamal implementation used by Helios crypto. |
heliosbooth2026/lib/jscrypto/sha1.js |
Vendored SHA-1 implementation used by jscrypto. |
heliosbooth2026/lib/jscrypto/sha2.js |
Vendored SHA-2 implementation used by jscrypto. |
heliosbooth2026/lib/jscrypto/helios.js |
Vendored Helios crypto logic (Election/EncryptedVote/etc.). |
heliosbooth2026/lib/jscrypto/bigint.java |
Vendored Java applet source (legacy BigInt support). |
heliosbooth2026/lib/jscrypto/bigint.dummy.js |
Vendored dummy BigInt implementation (legacy support). |
heliosbooth2026/lib/jscrypto/bigint.class |
Vendored Java applet bytecode (legacy BigInt support). |
docs/implementation-plans/2026-01-18-helios-booth-lit-redesign/phase_02.md |
Documentation plan for Phase 2 voting flow implementation. |
docs/implementation-plans/2026-01-18-helios-booth-lit-redesign/phase_04.md |
Documentation plan for Phase 4 polish/deployment steps. |
Files not reviewed (1)
- heliosbooth2026/package-lock.json: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| function do_encrypt(message) { | ||
| console.log('Encrypting answer for question ' + message.q_num); | ||
|
|
||
| var encrypted_answer = new HELIOS.EncryptedAnswer( | ||
| ELECTION.questions[message.q_num], | ||
| message.answer, | ||
| ELECTION.public_key | ||
| ); |
There was a problem hiding this comment.
do_encrypt assumes ELECTION has been initialized and q_num is valid. If an encrypt message arrives before setup, or q_num is out of bounds, the worker will throw and stop responding. Add guards that post an error response (and/or ignore) when ELECTION is null or the question index is invalid.
| <li | ||
| class="answer-item ${isSelected ? 'selected' : ''} ${isDisabled ? 'disabled' : ''}" | ||
| tabindex="${isDisabled ? -1 : 0}" | ||
| @click=${() => this.handleAnswerClick(answerIndex)} | ||
| @keydown=${(e: KeyboardEvent) => this.handleAnswerKeydown(e, answerIndex)} | ||
| > | ||
| <label class="answer-label"> | ||
| <input | ||
| type="checkbox" | ||
| class="answer-checkbox" | ||
| .checked=${isSelected} | ||
| .disabled=${isDisabled} | ||
| @change=${(e: Event) => { | ||
| e.stopPropagation(); | ||
| this.handleAnswerClick(answerIndex); | ||
| }} |
There was a problem hiding this comment.
Clicking the checkbox is likely to toggle twice: the <li @click> handler fires, and the <input @change> handler also calls handleAnswerClick. stopPropagation() on the change event doesn’t stop the click bubble. This can result in the selection immediately reverting. Consider handling selection in one place (e.g., only on the input’s @click/@change and remove the <li @click>, or stop propagation on the input’s click event).
| <h4 @click=${this.toggleAuditSection}> | ||
| Spoil & Audit | ||
| <span class="audit-optional">[optional]</span> | ||
| </h4> |
There was a problem hiding this comment.
<h4> is being used as an interactive control (@click=${this.toggleAuditSection}) but isn’t keyboard-focusable and doesn’t expose button semantics to assistive tech. Use a <button> (or <details><summary>) for the toggle, or add appropriate role="button", tabindex="0", and key handlers.
heliosbooth2026/package.json
Outdated
| "type": "module", | ||
| "scripts": { | ||
| "dev": "vite", | ||
| "build": "tsc && vite build", |
There was a problem hiding this comment.
npm run build runs tsc with emit enabled, writing into dist/, and then vite build also outputs to dist/ (and typically clears it first). This makes the tsc output either wasted or potentially conflicting. Consider switching to tsc --noEmit for type-checking, or configure tsc to emit to a separate directory if you actually need declarations.
| "build": "tsc && vite build", | |
| "build": "tsc --noEmit && vite build", |
| "declaration": true, | ||
| "declarationMap": true, | ||
| "sourceMap": true, | ||
| "outDir": "./dist", | ||
| "rootDir": "./src", | ||
| "esModuleInterop": true, |
There was a problem hiding this comment.
With outDir set to ./dist and declaration enabled, tsc will emit build artifacts into the same directory Vite uses for its production output. If the intention is type-check-only before Vite builds, consider disabling emit (noEmit: true) and/or turning off declaration outputs here to avoid churn/overwrites in dist/.
| BigIntDummy.setup = function(callback, fail_callback) { | ||
| //console.log("using dummy bigint"); | ||
| callback(); | ||
| } |
There was a problem hiding this comment.
Avoid automated semicolon insertion (91% of all statements in the enclosing script have an explicit semicolon).
| } | |
| }; |
| sk.pk = ElGamal.PublicKey.fromJSONObject(d.public_key); | ||
| sk.x = BigInt.fromJSONObject(d.x); | ||
| return sk; | ||
| } |
There was a problem hiding this comment.
Avoid automated semicolon insertion (95% of all statements in the enclosing script have an explicit semicolon).
| } | |
| }; |
| } | ||
|
|
||
| return plaintexts; | ||
| } |
There was a problem hiding this comment.
Avoid automated semicolon insertion (92% of all statements in the enclosing script have an explicit semicolon).
| } | |
| }; |
| return null; | ||
|
|
||
| return _(lol).map(function(sublist) {return _(sublist).map(function(item) {return item_dejsonifier(item);})}); | ||
| } |
There was a problem hiding this comment.
Avoid automated semicolon insertion (92% of all statements in the enclosing script have an explicit semicolon).
| } | |
| }; |
| // let's try always using SJCL | ||
| var USE_SJCL = true; | ||
|
|
||
| // let's make this much cleaner | ||
| if (USE_SJCL) { | ||
| // why not? | ||
| var BigInt = BigInteger; | ||
| // ZERO AND ONE are already taken care of | ||
| BigInt.TWO = new BigInt("2",10); | ||
|
|
||
| BigInt.setup = function(callback, fail_callback) { | ||
| // nothing to do but go | ||
| callback(); | ||
| } | ||
|
|
||
| BigInt.prototype.toJSONObject = function() { | ||
| return this.toString(); | ||
| }; | ||
|
|
||
| } else { | ||
| BigInt = Class.extend({ | ||
| init: function(value, radix) { | ||
| if (value == null) { | ||
| throw "null value!"; | ||
| } | ||
|
|
||
| if (USE_SJCL) { | ||
| this._java_bigint = new BigInteger(value, radix); | ||
| } else if (BigInt.use_applet) { | ||
| this._java_bigint = BigInt.APPLET.newBigInteger(value, radix); | ||
| } else { | ||
| try { | ||
| this._java_bigint = new java.math.BigInteger(value, radix); | ||
| } catch (e) { | ||
| // alert("oy " + e.toString() + " value=" + value + " , radix=" + radix); | ||
| throw TypeError | ||
| } | ||
| } | ||
| }, | ||
|
|
||
| toString: function() { | ||
| return this._java_bigint.toString() + ""; | ||
| }, | ||
|
|
||
| toJSONObject: function() { | ||
| return this.toString(); | ||
| }, | ||
|
|
||
| add: function(other) { | ||
| return BigInt._from_java_object(this._java_bigint.add(other._java_bigint)); | ||
| }, | ||
|
|
||
| bitLength: function() { | ||
| return this._java_bigint.bitLength(); | ||
| }, | ||
|
|
||
| mod: function(modulus) { | ||
| return BigInt._from_java_object(this._java_bigint.mod(modulus._java_bigint)); | ||
| }, | ||
|
|
||
| equals: function(other) { | ||
| return this._java_bigint.equals(other._java_bigint); | ||
| }, | ||
|
|
||
| modPow: function(exp, modulus) { | ||
| return BigInt._from_java_object(this._java_bigint.modPow(exp._java_bigint, modulus._java_bigint)); | ||
| }, | ||
|
|
||
| negate: function() { | ||
| return BigInt._from_java_object(this._java_bigint.negate()); | ||
| }, | ||
|
|
||
| multiply: function(other) { | ||
| return BigInt._from_java_object(this._java_bigint.multiply(other._java_bigint)); | ||
| }, | ||
|
|
||
| modInverse: function(modulus) { | ||
| return BigInt._from_java_object(this._java_bigint.modInverse(modulus._java_bigint)); | ||
| } | ||
|
|
||
| }); |
There was a problem hiding this comment.
This use of variable 'USE_SJCL' always evaluates to true.
| // let's try always using SJCL | |
| var USE_SJCL = true; | |
| // let's make this much cleaner | |
| if (USE_SJCL) { | |
| // why not? | |
| var BigInt = BigInteger; | |
| // ZERO AND ONE are already taken care of | |
| BigInt.TWO = new BigInt("2",10); | |
| BigInt.setup = function(callback, fail_callback) { | |
| // nothing to do but go | |
| callback(); | |
| } | |
| BigInt.prototype.toJSONObject = function() { | |
| return this.toString(); | |
| }; | |
| } else { | |
| BigInt = Class.extend({ | |
| init: function(value, radix) { | |
| if (value == null) { | |
| throw "null value!"; | |
| } | |
| if (USE_SJCL) { | |
| this._java_bigint = new BigInteger(value, radix); | |
| } else if (BigInt.use_applet) { | |
| this._java_bigint = BigInt.APPLET.newBigInteger(value, radix); | |
| } else { | |
| try { | |
| this._java_bigint = new java.math.BigInteger(value, radix); | |
| } catch (e) { | |
| // alert("oy " + e.toString() + " value=" + value + " , radix=" + radix); | |
| throw TypeError | |
| } | |
| } | |
| }, | |
| toString: function() { | |
| return this._java_bigint.toString() + ""; | |
| }, | |
| toJSONObject: function() { | |
| return this.toString(); | |
| }, | |
| add: function(other) { | |
| return BigInt._from_java_object(this._java_bigint.add(other._java_bigint)); | |
| }, | |
| bitLength: function() { | |
| return this._java_bigint.bitLength(); | |
| }, | |
| mod: function(modulus) { | |
| return BigInt._from_java_object(this._java_bigint.mod(modulus._java_bigint)); | |
| }, | |
| equals: function(other) { | |
| return this._java_bigint.equals(other._java_bigint); | |
| }, | |
| modPow: function(exp, modulus) { | |
| return BigInt._from_java_object(this._java_bigint.modPow(exp._java_bigint, modulus._java_bigint)); | |
| }, | |
| negate: function() { | |
| return BigInt._from_java_object(this._java_bigint.negate()); | |
| }, | |
| multiply: function(other) { | |
| return BigInt._from_java_object(this._java_bigint.multiply(other._java_bigint)); | |
| }, | |
| modInverse: function(modulus) { | |
| return BigInt._from_java_object(this._java_bigint.modInverse(modulus._java_bigint)); | |
| } | |
| }); | |
| // Using SJCL-backed BigInteger implementation directly. | |
| // NOTE: previously this was controlled by a USE_SJCL flag that was always true. | |
| // let's make this much cleaner | |
| // why not? | |
| var BigInt = BigInteger; | |
| // ZERO AND ONE are already taken care of | |
| BigInt.TWO = new BigInt("2",10); | |
| BigInt.setup = function(callback, fail_callback) { | |
| // nothing to do but go | |
| callback(); | |
| }; | |
| BigInt.prototype.toJSONObject = function() { | |
| return this.toString(); | |
| }; |
… dist on build - Update Django URL to serve from heliosbooth2026/dist/ instead of source - Add explicit route for /booth2026/ to serve index.html (directory index) - Update npm build script to copy lib/ into dist/ for crypto library access Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Heroku runs `heroku-postbuild` which installs dependencies and builds the heliosbooth2026 Lit app into dist/. Requires adding the Node.js buildpack before the Python buildpack: heroku buildpacks:add --index 1 heroku/nodejs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Heroku prunes devDependencies before running heroku-postbuild, so tsc and vite aren't available. Move them to dependencies so they're installed when the build script runs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix ballot submission broken by Shadow DOM: switch submit-screen to light DOM rendering via createRenderRoot() so the <form> can POST natively. Add single ballot verifier (verify-screen component, worker, verify.html entry point) and fix audit screen verifier link. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
heliosbooth2026/directory implementing a complete voting booth using Lit 3.3.x + TypeScript 5.x + Vite 6.xquestion-screen,review-screen,submit-screen,audit-screen,encrypting-screen) orchestrated by centralbooth-appstate holder/booth2026/serves the new booth alongside the existingheliosbooth/Architecture
booth-appholds all state, child components receive data via properties and emit events viaCustomEventwithbubbles: true, composed: trueencryption-worker.js) handles ballot encryption in background thread using existing jscrypto libraries<script>tags (same as existing booth), TypeScript declarations insrc/crypto/types.tsTest Plan
noUnusedLocals,noUnusedParameters)/booth2026/?election_url=<url>, complete full voting flow🤖 Generated with Claude Code