diff --git a/docs/implementation-plans/2026-01-18-helios-booth-lit-redesign/phase_01.md b/docs/implementation-plans/2026-01-18-helios-booth-lit-redesign/phase_01.md new file mode 100644 index 000000000..f32f94d61 --- /dev/null +++ b/docs/implementation-plans/2026-01-18-helios-booth-lit-redesign/phase_01.md @@ -0,0 +1,1075 @@ +# Helios Booth Lit Redesign - Phase 1: Setup & Core Shell + +> **For Claude:** REQUIRED SUB-SKILL: Use ed3d-plan-and-execute:executing-an-implementation-plan to implement this plan task-by-task. + +**Goal:** Create the project scaffolding and basic navigation for the new Lit-based voting booth + +**Architecture:** Vite + TypeScript + Lit 3.x web components. Single-bundle output for offline operation. Props-drilling state management with booth-app as the central state holder. + +**Tech Stack:** Lit 3.3.x, TypeScript 5.x, Vite 6.x + +**Scope:** 4 phases from original design (this is phase 1 of 4) + +**Codebase verified:** 2026-01-18 + +--- + +## Task 1: Create Project Directory Structure + +**Files:** +- Create: `heliosbooth2026/` directory +- Create: `heliosbooth2026/package.json` +- Create: `heliosbooth2026/tsconfig.json` +- Create: `heliosbooth2026/vite.config.ts` +- Create: `heliosbooth2026/index.html` + +**Step 1: Create the directory and package.json** + +Create directory `heliosbooth2026/` at project root. + +Create `heliosbooth2026/package.json`: +```json +{ + "name": "heliosbooth2026", + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "lit": "^3.3.2" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.7.0", + "vite": "^6.0.0" + } +} +``` + +**Step 2: Create tsconfig.json** + +Create `heliosbooth2026/tsconfig.json`: +```json +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "moduleResolution": "bundler", + "skipLibCheck": true, + "experimentalDecorators": true, + "useDefineForClassFields": false, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "./dist", + "rootDir": "./src", + "esModuleInterop": true, + "resolveJsonModule": true, + "isolatedModules": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} +``` + +**Step 3: Create vite.config.ts** + +Create `heliosbooth2026/vite.config.ts`: +```typescript +import { defineConfig } from 'vite'; + +export default defineConfig({ + root: '.', + base: '/booth2026/', + build: { + outDir: 'dist', + cssCodeSplit: false, + rollupOptions: { + output: { + manualChunks: () => 'booth', + entryFileNames: 'booth.js', + assetFileNames: 'booth.[ext]' + } + } + }, + server: { + port: 5173 + } +}); +``` + +**Step 4: Create index.html entry point** + +Create `heliosbooth2026/index.html`: +```html + + +
+ + +Loading election...
+Question screen - to be implemented in Phase 2
`; + + case 'review': + return html`Review screen - to be implemented in Phase 3
`; + + case 'submit': + return html`Submit screen - to be implemented in Phase 3
`; + + case 'audit': + return html`Audit screen - to be implemented in Phase 3
`; + + default: + return html`Unknown screen
`; + } + } + + /** + * Render the election info screen (start screen). + */ + private renderElectionScreen() { + if (!this.election) { + return html` +Loading election information...
+${this.election.description}
+To vote, follow these steps:
++ You can + + email for help + . +
+ ` : ''} +Loading question...
`; + } + + const ordering = this.answerOrdering.length > 0 + ? this.answerOrdering + : this.question.answers.map((_, i) => i); + + const isFirstQuestion = this.questionIndex === 0; + const isLastQuestion = this.questionIndex === this.totalQuestions - 1; + + return html` + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'question-screen': QuestionScreen; + } +} +``` + +**Step 2: Verify TypeScript compiles** + +Run: +```bash +cd heliosbooth2026 && npx tsc --noEmit +``` +Expected: No errors + +**Step 3: Commit** + +```bash +git add heliosbooth2026/src/screens/question-screen.ts +git commit -m "feat(booth2026): add question-screen component with answer selection" +``` + +--- + +## Task 2: Integrate Question Screen into Booth App + +**Files:** +- Modify: `heliosbooth2026/src/booth-app.ts` + +**Step 1: Add import for question-screen** + +At the top of `heliosbooth2026/src/booth-app.ts`, after the existing imports, add: + +```typescript +import './screens/question-screen.js'; +import type { AnswerChangeEvent, NavigationEvent } from './screens/question-screen.js'; +``` + +**Step 2: Add event handlers for answer changes and navigation** + +In the `BoothApp` class, add these methods after the `startVoting()` method: + +```typescript + /** + * Handle answer selection changes from question screen. + */ + private handleAnswerChange(event: CustomEventQuestion screen - to be implemented in Phase 2
`; +``` + +With: +```typescript + case 'question': + return this.renderQuestionScreen(); +``` + +**Step 4: Add the renderQuestionScreen method** + +Add this method to the `BoothApp` class: + +```typescript + /** + * Render the question screen. + */ + private renderQuestionScreen() { + if (!this.election) { + return html`Loading...
`; + } + + const question = this.election.questions[this.currentQuestionIndex]; + const ordering = this.election.question_answer_orderings?.[this.currentQuestionIndex] + ?? question.answers.map((_, i) => i); + + // Show review button once user has seen the last question + // or if they're on the last question + const showReview = this.allQuestionsSeen || + this.currentQuestionIndex === this.election.questions.length - 1; + + return html` +Your ballot tracker is:
++ If you choose, you can spoil this ballot and reveal how your choices + were encrypted. This is an optional auditing process. +
++ You will then be guided to re-encrypt your choices for final casting. +
+ +Loading...
`; + } + + return html` ++ All information, other than your encrypted ballot, + has been removed from memory. +
+As a reminder, your ballot tracking number is:
+${this.ballotHash}
+According to the election definition file, this ballot will be submitted to:
+${this.election.cast_url}
+where you will log in to validate your eligibility to vote.
++ IMPORTANT: this ballot, now that it has been audited, + will not be tallied. +
++ To cast a ballot, you must click the "Back to Voting" button below, + re-encrypt it, and choose "cast" instead of "audit." +
++ Why? Helios prevents you from auditing and casting the + same ballot to provide you with some protection against coercion. +
+ ++ Now what? + Select your ballot audit info, + copy it to your clipboard, then use the + + ballot verifier + + to verify it. +
++ Once you're satisfied, click the "back to voting" button to re-encrypt + and cast your ballot. +
++ This may take up to two minutes. +
+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'encrypting-screen': EncryptingScreen; + } +} +``` + +**Step 2: Copy the encrypting.gif from old booth** + +```bash +cp heliosbooth/encrypting.gif heliosbooth2026/ +cp heliosbooth/loading.gif heliosbooth2026/ +``` + +**Step 3: Verify TypeScript compiles** + +Run: +```bash +cd heliosbooth2026 && npx tsc --noEmit +``` +Expected: No errors + +**Step 4: Commit** + +```bash +git add heliosbooth2026/src/screens/encrypting-screen.ts heliosbooth2026/encrypting.gif heliosbooth2026/loading.gif +git commit -m "feat(booth2026): add encrypting-screen with progress display" +``` + +--- + +## Task 7: Integrate Crypto Screens into Booth App + +**Files:** +- Modify: `heliosbooth2026/src/booth-app.ts` + +**Step 1: Add screen imports** + +At the top of `heliosbooth2026/src/booth-app.ts`, after the existing screen import, add: + +```typescript +import './screens/review-screen.js'; +import './screens/submit-screen.js'; +import './screens/audit-screen.js'; +import './screens/encrypting-screen.js'; +import type { ReviewNavigationEvent } from './screens/review-screen.js'; +import type { AuditNavigationEvent } from './screens/audit-screen.js'; +import type { WorkerOutMessage, EncryptedAnswerJSON } from './crypto/types.js'; +``` + +**Step 2: Update the BoothScreen type** + +Find the `BoothScreen` type definition and update it to include 'encrypting': + +```typescript +type BoothScreen = 'loading' | 'election' | 'question' | 'encrypting' | 'review' | 'submit' | 'audit'; +``` + +**Step 3: Add encryption-related state properties** + +In the BoothApp class, add these new state properties after the existing ones: + +```typescript + // Encryption state + @state() private worker: Worker | null = null; + @state() private encryptionProgress: number = 0; + @state() private answerTimestamps: number[] = []; + @state() private dirty: boolean[] = []; + @state() private encryptedBallot: unknown = null; // Full encrypted vote object + @state() private auditTrail: string = ''; + @state() private rawElectionJson: string = ''; + @state() private postingAudit: boolean = false; +``` + +**Step 4: Add worker initialization method** + +Add these methods to the BoothApp class: + +```typescript + /** + * Initialize the encryption worker. + */ + private initializeWorker(): void { + if (this.worker || !this.rawElectionJson) return; + + this.worker = new Worker('/workers/encryption-worker.js'); + + this.worker.onmessage = (event: MessageEventLoading election...
+Unknown screen
`; + } + } +``` + +**Step 11: Update the getProgressStep method** + +```typescript + private getProgressStep(): number { + switch (this.currentScreen) { + case 'question': return 1; + case 'encrypting': + case 'review': return 2; + case 'submit': return 3; + case 'audit': return 4; + default: return 0; + } + } +``` + +**Step 12: Verify TypeScript compiles** + +Run: +```bash +cd heliosbooth2026 && npx tsc --noEmit +``` +Expected: No errors (or only minor type adjustments needed) + +**Step 13: Commit** + +```bash +git add heliosbooth2026/src/booth-app.ts +git commit -m "feat(booth2026): integrate crypto screens and encryption workflow" +``` + +--- + +## Task 8: Verify Complete Phase 3 Implementation + +**Files:** None (verification only) + +**Step 1: Run TypeScript check** + +Run: +```bash +cd heliosbooth2026 && npx tsc --noEmit +``` +Expected: No errors + +**Step 2: Run the build** + +Run: +```bash +cd heliosbooth2026 && npm run build +``` +Expected: Build completes successfully + +**Step 3: Run Django tests** + +Run: +```bash +uv run python manage.py test -v 2 +``` +Expected: All tests pass + +**Step 4: Manual testing** + +Start both servers: +```bash +# Terminal 1 +cd heliosbooth2026 && npm run dev + +# Terminal 2 +uv run python manage.py runserver +``` + +Test with a real election: +1. Create a test election in Helios or use an existing one +2. Access `http://localhost:5173/booth2026/?election_url=http://localhost:8000/helios/elections/${this.isInitializing ? 'Initializing voting booth...' : 'Loading...'}
+This may take a few seconds
+>(p+=this.DB-k);
+ }
+ else {
+ d = (this.arr[i]>>(p-=k))&km;
+ if(p <= 0) { p += this.DB; --i; }
+ }
+ if(d > 0) m = true;
+ if(m) r += int2char(d);
+ }
+ }
+ return m?r:"0";
+}
+
+// (public) -this
+function bnNegate() { var r = nbi(); BigInteger.ZERO.subTo(this,r); return r; }
+
+// (public) |this|
+function bnAbs() { return (this.s<0)?this.negate():this; }
+
+// (public) return + if this > a, - if this < a, 0 if equal
+function bnCompareTo(a) {
+ var r = this.s-a.s;
+ if(r != 0) return r;
+ var i = this.t;
+ r = i-a.t;
+ if(r != 0) return r;
+ while(--i >= 0) if((r=this.arr[i]-a.arr[i]) != 0) return r;
+ return 0;
+}
+
+// returns bit length of the integer x
+function nbits(x) {
+ var r = 1, t;
+ if((t=x>>>16) != 0) { x = t; r += 16; }
+ if((t=x>>8) != 0) { x = t; r += 8; }
+ if((t=x>>4) != 0) { x = t; r += 4; }
+ if((t=x>>2) != 0) { x = t; r += 2; }
+ if((t=x>>1) != 0) { x = t; r += 1; }
+ return r;
+}
+
+// (public) return the number of bits in "this"
+function bnBitLength() {
+ if(this.t <= 0) return 0;
+ return this.DB*(this.t-1)+nbits(this.arr[this.t-1]^(this.s&this.DM));
+}
+
+// (protected) r = this << n*DB
+function bnpDLShiftTo(n,r) {
+ var i;
+ for(i = this.t-1; i >= 0; --i) r.arr[i+n] = this.arr[i];
+ for(i = n-1; i >= 0; --i) r.arr[i] = 0;
+ r.t = this.t+n;
+ r.s = this.s;
+}
+
+// (protected) r = this >> n*DB
+function bnpDRShiftTo(n,r) {
+ for(var i = n; i < this.t; ++i) r.arr[i-n] = this.arr[i];
+ r.t = Math.max(this.t-n,0);
+ r.s = this.s;
+}
+
+// (protected) r = this << n
+function bnpLShiftTo(n,r) {
+ var bs = n%this.DB;
+ var cbs = this.DB-bs;
+ var bm = (1< >(p+=this.DB-8);
+ }
+ else {
+ d = (this.arr[i]>>(p-=8))&0xff;
+ if(p <= 0) { p += this.DB; --i; }
+ }
+ if((d&0x80) != 0) d |= -256;
+ if(k == 0 && (this.s&0x80) != (d&0x80)) ++k;
+ if(k > 0 || d != this.s) r.arr[k++] = d;
+ }
+ }
+ return r;
+}
+
+function bnEquals(a) { return(this.compareTo(a)==0); }
+function bnMin(a) { return(this.compareTo(a)<0)?this:a; }
+function bnMax(a) { return(this.compareTo(a)>0)?this:a; }
+
+// (protected) r = this op a (bitwise)
+function bnpBitwiseTo(a,op,r) {
+ var i, f, m = Math.min(a.t,this.t);
+ for(i = 0; i < m; ++i) r.arr[i] = op(this.arr[i],a.arr[i]);
+ if(a.t < this.t) {
+ f = a.s&this.DM;
+ for(i = m; i < this.t; ++i) r.arr[i] = op(this.arr[i],f);
+ r.t = this.t;
+ }
+ else {
+ f = this.s&this.DM;
+ for(i = m; i < a.t; ++i) r.arr[i] = op(f,a.arr[i]);
+ r.t = a.t;
+ }
+ r.s = op(this.s,a.s);
+ r.clamp();
+}
+
+// (public) this & a
+function op_and(x,y) { return x&y; }
+function bnAnd(a) { var r = nbi(); this.bitwiseTo(a,op_and,r); return r; }
+
+// (public) this | a
+function op_or(x,y) { return x|y; }
+function bnOr(a) { var r = nbi(); this.bitwiseTo(a,op_or,r); return r; }
+
+// (public) this ^ a
+function op_xor(x,y) { return x^y; }
+function bnXor(a) { var r = nbi(); this.bitwiseTo(a,op_xor,r); return r; }
+
+// (public) this & ~a
+function op_andnot(x,y) { return x&~y; }
+function bnAndNot(a) { var r = nbi(); this.bitwiseTo(a,op_andnot,r); return r; }
+
+// (public) ~this
+function bnNot() {
+ var r = nbi();
+ for(var i = 0; i < this.t; ++i) r.arr[i] = this.DM&~this.arr[i];
+ r.t = this.t;
+ r.s = ~this.s;
+ return r;
+}
+
+// (public) this << n
+function bnShiftLeft(n) {
+ var r = nbi();
+ if(n < 0) this.rShiftTo(-n,r); else this.lShiftTo(n,r);
+ return r;
+}
+
+// (public) this >> n
+function bnShiftRight(n) {
+ var r = nbi();
+ if(n < 0) this.lShiftTo(-n,r); else this.rShiftTo(n,r);
+ return r;
+}
+
+// return index of lowest 1-bit in x, x < 2^31
+function lbit(x) {
+ if(x == 0) return -1;
+ var r = 0;
+ if((x&0xffff) == 0) { x >>= 16; r += 16; }
+ if((x&0xff) == 0) { x >>= 8; r += 8; }
+ if((x&0xf) == 0) { x >>= 4; r += 4; }
+ if((x&3) == 0) { x >>= 2; r += 2; }
+ if((x&1) == 0) ++r;
+ return r;
+}
+
+// (public) returns index of lowest 1-bit (or -1 if none)
+function bnGetLowestSetBit() {
+ for(var i = 0; i < this.t; ++i)
+ if(this.arr[i] != 0) return i*this.DB+lbit(this.arr[i]);
+ if(this.s < 0) return this.t*this.DB;
+ return -1;
+}
+
+// return number of 1 bits in x
+function cbit(x) {
+ var r = 0;
+ while(x != 0) { x &= x-1; ++r; }
+ return r;
+}
+
+// (public) return number of set bits
+function bnBitCount() {
+ var r = 0, x = this.s&this.DM;
+ for(var i = 0; i < this.t; ++i) r += cbit(this.arr[i]^x);
+ return r;
+}
+
+// (public) true iff nth bit is set
+function bnTestBit(n) {
+ var j = Math.floor(n/this.DB);
+ if(j >= this.t) return(this.s!=0);
+ return((this.arr[j]&(1<<(n%this.DB)))!=0);
+}
+
+// (protected) this op (1< Loading... ${this.isInitializing ? 'Initializing voting booth...' : 'Loading...'} This may take a few seconds Unknown screen Loading election information... ${this.election.description} To vote, follow these steps:
+ You can
+
+ email for help
+ .
+
+ IMPORTANT: this ballot, now that it has been audited,
+ will not be tallied.
+
+ To cast a ballot, you must click the "Back to Voting" button below,
+ re-encrypt it, and choose "cast" instead of "audit."
+
+ Why? Helios prevents you from auditing and casting the
+ same ballot to provide you with some protection against coercion.
+
+ Now what?
+ Select your ballot audit info,
+ copy it to your clipboard, then use the
+
+ ballot verifier
+
+ to verify it.
+
+ Once you're satisfied, click the "back to voting" button to re-encrypt
+ and cast your ballot.
+
+ This may take up to two minutes.
+ Loading question... Your ballot tracker is:
+ If you choose, you can spoil this ballot and reveal how your choices
+ were encrypted. This is an optional auditing process.
+
+ You will then be guided to re-encrypt your choices for final casting.
+
+ This verifier lets you enter an audited ballot and verify
+ that it was prepared correctly.
+ ${this.errorMessage}
+ It looks like you are trying to verify a cast ballot.
+ Only audited ballots can be verified.
+ Something went wrong
+
+
+
+ Helios Voting Booth
+ ${this.election.name}
+
+ ${this.election.description ? html`
+
+
+ Your audited ballot
+
+ Helios is now encrypting your ballot
+
+
+
+ Review your Ballot
+
+
+ Spoil & Audit
+ [optional]
+
+ ${this.auditExpanded ? html`
+ Helios Single-Ballot Verifier
+
+