22// Ported from ClaudeService.swift
33
44import { logTokenUsage } from "./usage-logger.js" ;
5- import { getElectionPhase , ELECTION_PHASES } from "./state-config.js" ;
5+ import { getElectionPhase , ELECTION_PHASES , ELECTION_SUFFIX } from "./state-config.js" ;
66
77const SYSTEM_PROMPT =
88 "You are a non-partisan voting guide assistant for Texas elections. " +
@@ -21,6 +21,22 @@ const SYSTEM_PROMPT =
2121
2222const MODELS = [ "claude-sonnet-4-6" , "claude-sonnet-4-20250514" , "claude-haiku-4-5-20251001" ] ;
2323
24+ function bufToHex ( buffer ) {
25+ var arr = new Uint8Array ( buffer ) ;
26+ var hex = "" ;
27+ for ( var i = 0 ; i < arr . length ; i ++ ) {
28+ hex += arr [ i ] . toString ( 16 ) . padStart ( 2 , "0" ) ;
29+ }
30+ return hex ;
31+ }
32+
33+ async function resolvePhase ( url , env ) {
34+ var stateCode = url . pathname . startsWith ( "/dc/" ) ? "dc" : "tx" ;
35+ var testPhase = url . searchParams . get ( "test_phase" ) ;
36+ var kvPhase = ( testPhase && ELECTION_PHASES . includes ( testPhase ) ) ? testPhase : await env . ELECTION_DATA . get ( "site_phase:" + stateCode ) ;
37+ return getElectionPhase ( stateCode , { kvPhase } ) ;
38+ }
39+
2440function json ( data , status = 200 ) {
2541 return new Response ( JSON . stringify ( data ) , {
2642 status,
@@ -71,22 +87,15 @@ async function hashGuideKey(profile, ballot, party, lang, readingLevel, llm) {
7187
7288 var data = new TextEncoder ( ) . encode ( JSON . stringify ( keyObj ) ) ;
7389 var hashBuffer = await crypto . subtle . digest ( "SHA-256" , data ) ;
74- var hashArray = new Uint8Array ( hashBuffer ) ;
75- var hex = "" ;
76- for ( var i = 0 ; i < hashArray . length ; i ++ ) {
77- hex += hashArray [ i ] . toString ( 16 ) . padStart ( 2 , "0" ) ;
78- }
90+ var hex = bufToHex ( hashBuffer ) ;
7991 return hex ;
8092}
8193
8294export async function handlePWA_Guide ( request , env ) {
8395 try {
8496 // Check election phase — block guide generation after polls close
8597 var requestUrl = new URL ( request . url ) ;
86- var stateCode = requestUrl . pathname . startsWith ( "/dc/" ) ? "dc" : "tx" ;
87- var testPhase = requestUrl . searchParams . get ( "test_phase" ) ;
88- var kvPhase = ( testPhase && ELECTION_PHASES . includes ( testPhase ) ) ? testPhase : await env . ELECTION_DATA . get ( "site_phase:" + stateCode ) ;
89- var phase = getElectionPhase ( stateCode , { kvPhase } ) ;
98+ var phase = await resolvePhase ( requestUrl , env ) ;
9099 if ( phase === "post-election" || phase === "election-night" ) {
91100 return json ( { error : "Guide generation is closed. The primary election has ended." , phase } , 410 ) ;
92101 }
@@ -105,10 +114,10 @@ export async function handlePWA_Guide(request, env) {
105114
106115 // Parallel KV reads — statewide, legacy fallback, county, and manifest are independent
107116 var [ statewideRaw , legacyRaw , countyRaw , manifestRaw ] = await Promise . all ( [
108- env . ELECTION_DATA . get ( "ballot:statewide:" + party + "_primary_2026" ) ,
109- env . ELECTION_DATA . get ( "ballot:" + party + "_primary_2026" ) ,
117+ env . ELECTION_DATA . get ( "ballot:statewide:" + party + ELECTION_SUFFIX ) ,
118+ env . ELECTION_DATA . get ( "ballot:" + party + ELECTION_SUFFIX ) ,
110119 countyFips
111- ? env . ELECTION_DATA . get ( "ballot:county:" + countyFips + ":" + party + "_primary_2026" )
120+ ? env . ELECTION_DATA . get ( "ballot:county:" + countyFips + ":" + party + ELECTION_SUFFIX )
112121 : Promise . resolve ( null ) ,
113122 env . ELECTION_DATA . get ( "manifest" ) ,
114123 ] ) ;
@@ -180,11 +189,7 @@ export async function handlePWA_Guide(request, env) {
180189 electionName : ballot . electionName ,
181190 } ) ) ;
182191 var ballotDescHashBuf = await crypto . subtle . digest ( "SHA-256" , ballotDescData ) ;
183- var ballotDescHashArr = new Uint8Array ( ballotDescHashBuf ) ;
184- var ballotDescHex = "" ;
185- for ( var h = 0 ; h < ballotDescHashArr . length ; h ++ ) {
186- ballotDescHex += ballotDescHashArr [ h ] . toString ( 16 ) . padStart ( 2 , "0" ) ;
187- }
192+ var ballotDescHex = bufToHex ( ballotDescHashBuf ) ;
188193 ballotDescCacheKey = "ballot_desc:" + ballotDescHex ;
189194 var cachedDesc = await env . ELECTION_DATA . get ( ballotDescCacheKey ) ;
190195 if ( cachedDesc ) {
@@ -269,10 +274,7 @@ export async function handlePWA_Summary(request, env) {
269274 try {
270275 // Check election phase — block summary generation after polls close
271276 var summaryUrl = new URL ( request . url ) ;
272- var summaryState = summaryUrl . pathname . startsWith ( "/dc/" ) ? "dc" : "tx" ;
273- var summaryTestPhase = summaryUrl . searchParams . get ( "test_phase" ) ;
274- var summaryKvPhase = ( summaryTestPhase && ELECTION_PHASES . includes ( summaryTestPhase ) ) ? summaryTestPhase : await env . ELECTION_DATA . get ( "site_phase:" + summaryState ) ;
275- var summaryPhase = getElectionPhase ( summaryState , { kvPhase : summaryKvPhase } ) ;
277+ var summaryPhase = await resolvePhase ( summaryUrl , env ) ;
276278 if ( summaryPhase === "post-election" || summaryPhase === "election-night" ) {
277279 return json ( { error : "Guide generation is closed. The primary election has ended." , phase : summaryPhase } , 410 ) ;
278280 }
@@ -557,7 +559,7 @@ async function loadCachedTranslations(env, party, countyFips) {
557559 var translations = [ ] ;
558560
559561 // Load statewide translations
560- var statewideKey = "translations:es:" + party + "_primary_2026" ;
562+ var statewideKey = "translations:es:" + party + ELECTION_SUFFIX ;
561563 var statewideRaw = await env . ELECTION_DATA . get ( statewideKey ) ;
562564 if ( statewideRaw ) {
563565 try {
@@ -570,7 +572,7 @@ async function loadCachedTranslations(env, party, countyFips) {
570572
571573 // Load county-specific translations if countyFips provided
572574 if ( countyFips ) {
573- var countyKey = "translations:es:county:" + countyFips + ":" + party + "_primary_2026" ;
575+ var countyKey = "translations:es:county:" + countyFips + ":" + party + ELECTION_SUFFIX ;
574576 var countyRaw = await env . ELECTION_DATA . get ( countyKey ) ;
575577 if ( countyRaw ) {
576578 try {
@@ -602,29 +604,28 @@ async function loadCachedTranslations(env, party, countyFips) {
602604 */
603605export async function handleSeedTranslations ( env , party , countyFips ) {
604606 // Load ballot data
605- var ballotKey , translationKey ;
607+ var ballotKey , translationKey , ballotRaw ;
606608 if ( countyFips ) {
607- ballotKey = "ballot:county:" + countyFips + ":" + party + "_primary_2026" ;
608- translationKey = "translations:es:county:" + countyFips + ":" + party + "_primary_2026" ;
609+ ballotKey = "ballot:county:" + countyFips + ":" + party + ELECTION_SUFFIX ;
610+ translationKey = "translations:es:county:" + countyFips + ":" + party + ELECTION_SUFFIX ;
611+ ballotRaw = await env . ELECTION_DATA . get ( ballotKey ) ;
612+ if ( ! ballotRaw ) {
613+ return { error : "No ballot data found at " + ballotKey } ;
614+ }
609615 } else {
610- ballotKey = "ballot:statewide:" + party + "_primary_2026" ;
611- translationKey = "translations:es:" + party + "_primary_2026" ;
616+ ballotKey = "ballot:statewide:" + party + ELECTION_SUFFIX ;
617+ translationKey = "translations:es:" + party + ELECTION_SUFFIX ;
612618 // Fallback to legacy key
613- var raw = await env . ELECTION_DATA . get ( ballotKey ) ;
614- if ( ! raw ) {
615- ballotKey = "ballot:" + party + "_primary_2026" ;
616- raw = await env . ELECTION_DATA . get ( ballotKey ) ;
619+ ballotRaw = await env . ELECTION_DATA . get ( ballotKey ) ;
620+ if ( ! ballotRaw ) {
621+ ballotKey = "ballot:" + party + ELECTION_SUFFIX ;
622+ ballotRaw = await env . ELECTION_DATA . get ( ballotKey ) ;
617623 }
618- if ( ! raw ) {
624+ if ( ! ballotRaw ) {
619625 return { error : "No ballot data found for " + party } ;
620626 }
621627 }
622628
623- var ballotRaw = await env . ELECTION_DATA . get ( ballotKey ) ;
624- if ( ! ballotRaw ) {
625- return { error : "No ballot data found at " + ballotKey } ;
626- }
627-
628629 var ballot = JSON . parse ( ballotRaw ) ;
629630 if ( ! ballot . races || ! ballot . races . length ) {
630631 return { error : "No races in ballot" } ;
@@ -1772,10 +1773,7 @@ export async function handlePWA_GuideStream(request, env) {
17721773 var requestUrl = new URL ( request . url ) ;
17731774
17741775 // Check election phase — block guide generation after polls close
1775- var stateCode = requestUrl . pathname . startsWith ( "/dc/" ) ? "dc" : "tx" ;
1776- var testPhase = requestUrl . searchParams . get ( "test_phase" ) ;
1777- var kvPhase = ( testPhase && ELECTION_PHASES . includes ( testPhase ) ) ? testPhase : await env . ELECTION_DATA . get ( "site_phase:" + stateCode ) ;
1778- var phase = getElectionPhase ( stateCode , { kvPhase } ) ;
1776+ var phase = await resolvePhase ( requestUrl , env ) ;
17791777 if ( phase === "post-election" || phase === "election-night" ) {
17801778 return new Response ( "event: error\ndata: " + JSON . stringify ( { error : "Guide generation is closed. The primary election has ended." , phase } ) + "\n\n" , {
17811779 status : 410 ,
@@ -1836,10 +1834,10 @@ export async function handlePWA_GuideStream(request, env) {
18361834 try {
18371835 // Parallel KV reads
18381836 var [ statewideRaw , legacyRaw , countyRaw , manifestRaw ] = await Promise . all ( [
1839- env . ELECTION_DATA . get ( "ballot:statewide:" + party + "_primary_2026" ) ,
1840- env . ELECTION_DATA . get ( "ballot:" + party + "_primary_2026" ) ,
1837+ env . ELECTION_DATA . get ( "ballot:statewide:" + party + ELECTION_SUFFIX ) ,
1838+ env . ELECTION_DATA . get ( "ballot:" + party + ELECTION_SUFFIX ) ,
18411839 countyFips
1842- ? env . ELECTION_DATA . get ( "ballot:county:" + countyFips + ":" + party + "_primary_2026" )
1840+ ? env . ELECTION_DATA . get ( "ballot:county:" + countyFips + ":" + party + ELECTION_SUFFIX )
18431841 : Promise . resolve ( null ) ,
18441842 env . ELECTION_DATA . get ( "manifest" ) ,
18451843 ] ) ;
@@ -1968,11 +1966,7 @@ export async function handlePWA_GuideStream(request, env) {
19681966 electionName : ballot . electionName ,
19691967 } ) ) ;
19701968 var ballotDescHashBuf = await crypto . subtle . digest ( "SHA-256" , ballotDescData ) ;
1971- var ballotDescHashArr = new Uint8Array ( ballotDescHashBuf ) ;
1972- var ballotDescHex = "" ;
1973- for ( var h = 0 ; h < ballotDescHashArr . length ; h ++ ) {
1974- ballotDescHex += ballotDescHashArr [ h ] . toString ( 16 ) . padStart ( 2 , "0" ) ;
1975- }
1969+ var ballotDescHex = bufToHex ( ballotDescHashBuf ) ;
19761970 ballotDescCacheKey = "ballot_desc:" + ballotDescHex ;
19771971 var cachedDesc = await env . ELECTION_DATA . get ( ballotDescCacheKey ) ;
19781972 if ( cachedDesc ) {
0 commit comments