Skip to content

Commit 87b46f1

Browse files
stackdumpclaude
andcommitted
Tally votes at submission time — no reveal phase needed
Server extracts voteChoice from the witness transiently during proof verification and increments the aggregate tally. The choice is never persisted per-vote — only tally.json totals. Results show real-time per-choice counts without requiring voters to return and reveal. The reveal endpoint still works as a fallback for votes submitted without a witness (proof-only path), but the primary flow is now: create -> vote -> see live results. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2d079fa commit 87b46f1

3 files changed

Lines changed: 22 additions & 9 deletions

File tree

internal/server/polls.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,15 @@ func (s *Server) handleCastVote(w http.ResponseWriter, r *http.Request) {
307307
return
308308
}
309309

310+
// Tally at vote time — extract choice from witness (seen transiently, never persisted per-vote)
311+
if choiceStr, ok := req.Witness["voteChoice"]; ok {
312+
choice := 0
313+
fmt.Sscanf(choiceStr, "%d", &choice)
314+
if choice >= 0 && choice < len(poll.Choices) {
315+
_ = s.store.IncrementTally(pollID, choice)
316+
}
317+
}
318+
310319
w.Header().Set("Content-Type", "application/json")
311320
json.NewEncoder(w).Encode(map[string]string{"status": "accepted"})
312321
}
@@ -487,7 +496,7 @@ func (s *Server) handlePollResults(w http.ResponseWriter, r *http.Request) {
487496
choiceTallies[i] = tally.Counts[fmt.Sprintf("%d", i)]
488497
}
489498
result["tallies"] = choiceTallies
490-
result["revealedCount"] = tally.RevealedTotal
499+
result["talliedCount"] = tally.RevealedTotal
491500
}
492501

493502
w.Header().Set("Content-Type", "application/json")

internal/store/polls.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,14 @@ func (s *FSStore) tallyPath(pollID string) (string, error) {
286286
return filepath.Join(s.pollDir(), clean, "tally.json"), nil
287287
}
288288

289+
// IncrementTally adds one vote to the aggregate tally for a choice.
290+
// The tally file only contains totals — no link to individual voters.
291+
func (s *FSStore) IncrementTally(pollID string, choice int) error {
292+
s.mu.Lock()
293+
defer s.mu.Unlock()
294+
return s.incrementTally(pollID, choice)
295+
}
296+
289297
func (s *FSStore) incrementTally(pollID string, choice int) error {
290298
path, err := s.tallyPath(pollID)
291299
if err != nil {

public/poll.js

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -453,10 +453,10 @@ async function loadResults(pollId) {
453453
const choices = data.choices || [];
454454
const voteCount = data.voteCount || 0;
455455
const tallies = data.tallies || null;
456-
const revealedCount = data.revealedCount || 0;
456+
const talliedCount = data.talliedCount || 0;
457457
const barsDiv = document.getElementById('results-bars');
458458

459-
if (tallies && revealedCount > 0) {
459+
if (tallies && talliedCount > 0) {
460460
// Show real tallies from revealed votes
461461
const maxVotes = Math.max(...tallies, 1);
462462
barsDiv.innerHTML = choices.map((c, i) => {
@@ -490,12 +490,8 @@ async function loadResults(pollId) {
490490
}
491491

492492
let statusText = `${voteCount} total votes \u00b7 ${data.status}`;
493-
if (revealedCount > 0) {
494-
statusText += ` \u00b7 ${revealedCount}/${voteCount} revealed`;
495-
} else if (voteCount > 0 && data.status === 'closed') {
496-
statusText += ' \u00b7 awaiting reveals';
497-
} else if (voteCount > 0 && data.status === 'active') {
498-
statusText += ' \u00b7 choices hidden until poll closes';
493+
if (talliedCount > 0 && talliedCount < voteCount) {
494+
statusText += ` \u00b7 ${talliedCount}/${voteCount} tallied`;
499495
}
500496
document.getElementById('results-total').textContent = statusText;
501497

0 commit comments

Comments
 (0)