diff --git a/src/electionguard_gui/__init__.py b/src/electionguard_gui/__init__.py index 75b8dc3a5..ae0dc9044 100644 --- a/src/electionguard_gui/__init__.py +++ b/src/electionguard_gui/__init__.py @@ -33,6 +33,7 @@ key_ceremony_details_component, notify_ui_db_changed, refresh_decryption, + update_upload_status, upload_ballots_component, view_decryption_component, view_election_component, @@ -112,12 +113,12 @@ election_service, export_service, get_data_dir, - get_drives, get_export_dir, get_export_locations, get_guardian_number, get_key_ceremony_status, get_plaintext_ballot_report, + get_removable_drives, get_tally, guardian_service, gui_setup_input_retrieval_step, @@ -139,6 +140,7 @@ service_base, status_descriptions, to_ballot_share_raw, + update_decrypt_status, verification_to_dict, version_service, ) @@ -232,12 +234,12 @@ "export_encryption_package_component", "export_service", "get_data_dir", - "get_drives", "get_export_dir", "get_export_locations", "get_guardian_number", "get_key_ceremony_status", "get_plaintext_ballot_report", + "get_removable_drives", "get_spoiled_ballot_by_id", "get_tally", "guardian_home_component", @@ -271,6 +273,8 @@ "start", "status_descriptions", "to_ballot_share_raw", + "update_decrypt_status", + "update_upload_status", "upload_ballots_component", "utc_to_str", "verification_to_dict", diff --git a/src/electionguard_gui/components/__init__.py b/src/electionguard_gui/components/__init__.py index 331773015..ec230cf7a 100644 --- a/src/electionguard_gui/components/__init__.py +++ b/src/electionguard_gui/components/__init__.py @@ -43,6 +43,7 @@ ) from electionguard_gui.components.upload_ballots_component import ( UploadBallotsComponent, + update_upload_status, ) from electionguard_gui.components.view_decryption_component import ( ViewDecryptionComponent, @@ -86,6 +87,7 @@ "key_ceremony_details_component", "notify_ui_db_changed", "refresh_decryption", + "update_upload_status", "upload_ballots_component", "view_decryption_component", "view_election_component", diff --git a/src/electionguard_gui/components/upload_ballots_component.py b/src/electionguard_gui/components/upload_ballots_component.py index 2cc5c3164..d2d3a5cbe 100644 --- a/src/electionguard_gui/components/upload_ballots_component.py +++ b/src/electionguard_gui/components/upload_ballots_component.py @@ -1,11 +1,14 @@ +import os from typing import Any from datetime import datetime import eel -from electionguard.serialize import from_raw +from electionguard.encrypt import EncryptionDevice +from electionguard.serialize import from_file, from_raw from electionguard.ballot import SubmittedBallot from electionguard_gui.components.component_base import ComponentBase from electionguard_gui.eel_utils import eel_fail, eel_success from electionguard_gui.services import ElectionService, BallotUploadService +from electionguard_gui.services.export_service import get_removable_drives class UploadBallotsComponent(ComponentBase): @@ -25,6 +28,9 @@ def __init__( def expose(self) -> None: eel.expose(self.create_ballot_upload) eel.expose(self.upload_ballot) + eel.expose(self.is_wizard_supported) + eel.expose(self.scan_drives) + eel.expose(self.upload_ballots) def create_ballot_upload( self, @@ -106,3 +112,116 @@ def upload_ballot( # pylint: disable=broad-except except Exception as e: return self.handle_error(e) + + # pylint: disable=no-self-use + def is_wizard_supported(self) -> bool: + on_windows = os.name == "nt" + return on_windows + + def scan_drives(self) -> dict[str, Any]: + try: + removable_drives = get_removable_drives() + self._log.trace(f"found {len(removable_drives)} removable drives") + candidate_drives = [ + self.parse_drive(drive) + for drive in removable_drives + if os.path.exists(os.path.join(drive, "artifacts", "encrypted_ballots")) + and os.path.exists(os.path.join(drive, "artifacts", "devices")) + ] + first_candidate = next(iter(candidate_drives), None) + return eel_success(first_candidate) + # pylint: disable=broad-except + except Exception as e: + return self.handle_error(e) + + def parse_drive(self, drive: str) -> dict[str, Any]: + ballots_dir = os.path.join(drive, "artifacts", "encrypted_ballots") + devices_dir = os.path.join(drive, "artifacts", "devices") + device_files = os.listdir(devices_dir) + device_file_name = next(iter(os.listdir(devices_dir))) + device_file_path = os.path.join(devices_dir, device_file_name) + if len(device_files) > 1: + self._log.warn( + "found multiple device files in drive, using " + device_file_name + ) + device_file_json = from_file(EncryptionDevice, device_file_path) + location = device_file_json.location + ballot_count = len(os.listdir(ballots_dir)) + return { + "drive": drive, + "ballots": ballot_count, + "location": location, + "device_file_name": device_file_name, + "device_file_path": device_file_path, + "ballots_dir": ballots_dir, + } + + def upload_ballots(self, election_id: str) -> dict[str, Any]: + try: + update_upload_status("Scanning drives") + drive_info = self.scan_drives() + device_file_name = drive_info["result"]["device_file_name"] + device_file_path = drive_info["result"]["device_file_path"] + self._log.debug( + f"uploading ballots for {election_id} from {device_file_path} device {device_file_name}" + ) + update_upload_status("Uploading device file") + ballot_upload_result = self.create_ballot_upload_from_file( + election_id, + device_file_name, + device_file_path, + ) + if not ballot_upload_result["success"]: + return ballot_upload_result + + ballots_dir: str = drive_info["result"]["ballots_dir"] + ballot_files = os.listdir(ballots_dir) + ballot_upload_id: str = ballot_upload_result["result"] + ballot_num = 1 + duplicate_count = 0 + ballot_count = len(ballot_files) + for ballot_file in ballot_files: + self._log.debug("uploading ballot " + ballot_file) + update_upload_status(f"Uploading ballot {ballot_num}/{ballot_count}") + result = self.create_ballot_from_file( + election_id, ballot_file, ballot_upload_id, ballots_dir + ) + if not result["success"]: + return result + if result["result"]["is_duplicate"]: + duplicate_count += 1 + ballot_num += 1 + return eel_success( + {"ballot_count": ballot_count, "duplicate_count": duplicate_count} + ) + # pylint: disable=broad-except + except Exception as e: + return self.handle_error(e) + + def create_ballot_from_file( + self, + election_id: str, + ballot_file_name: str, + ballot_upload_id: str, + ballots_dir: str, + ) -> dict[str, Any]: + ballot_file_path = os.path.join(ballots_dir, ballot_file_name) + with open(ballot_file_path, "r", encoding="utf-8") as ballot_file: + ballot_contents = ballot_file.read() + return self.upload_ballot( + ballot_upload_id, election_id, ballot_file_name, ballot_contents + ) + + def create_ballot_upload_from_file( + self, election_id: str, device_file_name: str, device_file_path: str + ) -> dict[str, Any]: + with open(device_file_path, "r", encoding="utf-8") as device_file: + ballot_upload = self.create_ballot_upload( + election_id, device_file_name, device_file.read() + ) + return ballot_upload + + +def update_upload_status(status: str) -> None: + # pylint: disable=no-member + eel.update_upload_status(status) diff --git a/src/electionguard_gui/services/__init__.py b/src/electionguard_gui/services/__init__.py index b66c64ed8..1bb1ae34a 100644 --- a/src/electionguard_gui/services/__init__.py +++ b/src/electionguard_gui/services/__init__.py @@ -58,6 +58,7 @@ decryption_s2_announce_service, decryption_stage_base, get_tally, + update_decrypt_status, ) from electionguard_gui.services.directory_service import ( DOCKER_MOUNT_DIR, @@ -71,8 +72,8 @@ ElectionService, ) from electionguard_gui.services.export_service import ( - get_drives, get_export_locations, + get_removable_drives, ) from electionguard_gui.services.guardian_service import ( GuardianService, @@ -168,12 +169,12 @@ "election_service", "export_service", "get_data_dir", - "get_drives", "get_export_dir", "get_export_locations", "get_guardian_number", "get_key_ceremony_status", "get_plaintext_ballot_report", + "get_removable_drives", "get_tally", "guardian_service", "gui_setup_input_retrieval_step", @@ -195,6 +196,7 @@ "service_base", "status_descriptions", "to_ballot_share_raw", + "update_decrypt_status", "verification_to_dict", "version_service", ] diff --git a/src/electionguard_gui/services/decryption_stages/__init__.py b/src/electionguard_gui/services/decryption_stages/__init__.py index 0c7ee5d7a..96eac28cf 100644 --- a/src/electionguard_gui/services/decryption_stages/__init__.py +++ b/src/electionguard_gui/services/decryption_stages/__init__.py @@ -7,6 +7,7 @@ ) from electionguard_gui.services.decryption_stages.decryption_s2_announce_service import ( DecryptionS2AnnounceService, + update_decrypt_status, ) from electionguard_gui.services.decryption_stages.decryption_stage_base import ( DecryptionStageBase, @@ -21,4 +22,5 @@ "decryption_s2_announce_service", "decryption_stage_base", "get_tally", + "update_decrypt_status", ] diff --git a/src/electionguard_gui/services/decryption_stages/decryption_s1_join_service.py b/src/electionguard_gui/services/decryption_stages/decryption_s1_join_service.py index 7c4ed56f3..3b4e2a75f 100644 --- a/src/electionguard_gui/services/decryption_stages/decryption_s1_join_service.py +++ b/src/electionguard_gui/services/decryption_stages/decryption_s1_join_service.py @@ -1,4 +1,5 @@ from pymongo.database import Database +import eel from electionguard.ballot import BallotBoxState from electionguard_gui.models.decryption_dto import DecryptionDto @@ -12,6 +13,7 @@ class DecryptionS1JoinService(DecryptionStageBase): """Responsible for the 1st stage during a decryption were guardians join the decryption""" def run(self, db: Database, decryption: DecryptionDto) -> None: + update_decrypt_status("Starting tally") current_user_id = self._auth_service.get_required_user_id() self._log.info(f"S1: {current_user_id} decrypting {decryption.decryption_id}") election = self._election_service.get(db, decryption.election_id) @@ -22,18 +24,22 @@ def run(self, db: Database, decryption: DecryptionDto) -> None: current_user_id, decryption ) ballots = self._ballot_upload_service.get_ballots(db, election.id) - spoiled_ballots = [ - ballot for ballot in ballots if ballot.state == BallotBoxState.SPOILED - ] + update_decrypt_status("Calculating tally") ciphertext_tally = get_tally(manifest, context, ballots, False) decryption_share = guardian.compute_tally_share(ciphertext_tally, context) if decryption_share is None: raise Exception("No decryption_shares found") + + update_decrypt_status("Calculating spoiled ballots") + spoiled_ballots = [ + ballot for ballot in ballots if ballot.state == BallotBoxState.SPOILED + ] ballot_shares = guardian.compute_ballot_shares(spoiled_ballots, context) if ballot_shares is None: raise Exception("No ballot shares found") guardian_key = guardian.share_key() + update_decrypt_status("Finalizing tally") self._decryption_service.append_guardian_joined( db, decryption.decryption_id, @@ -43,3 +49,8 @@ def run(self, db: Database, decryption: DecryptionDto) -> None: guardian_key, ) self._decryption_service.notify_changed(db, decryption.decryption_id) + + +def update_decrypt_status(status: str) -> None: + # pylint: disable=no-member + eel.update_decrypt_status(status) diff --git a/src/electionguard_gui/services/decryption_stages/decryption_s2_announce_service.py b/src/electionguard_gui/services/decryption_stages/decryption_s2_announce_service.py index e17a3ba50..2d1ca3de9 100644 --- a/src/electionguard_gui/services/decryption_stages/decryption_s2_announce_service.py +++ b/src/electionguard_gui/services/decryption_stages/decryption_s2_announce_service.py @@ -1,4 +1,5 @@ from pymongo.database import Database +import eel from electionguard import DecryptionMediator from electionguard.ballot import BallotBoxState from electionguard.election_polynomial import LagrangeCoefficientsRecord @@ -19,6 +20,7 @@ def should_run(self, db: Database, decryption: DecryptionDto) -> bool: return is_admin and all_guardians_joined and not is_completed def run(self, db: Database, decryption: DecryptionDto) -> None: + update_decrypt_status("Starting tally") self._log.info(f"S2: Announcing decryption {decryption.decryption_id}") election = self._election_service.get(db, decryption.election_id) context = election.get_context() @@ -28,7 +30,10 @@ def run(self, db: Database, decryption: DecryptionDto) -> None: context, ) decryption_shares = decryption.get_decryption_shares() + share_count = len(decryption_shares) + current_share = 1 for decryption_share_dict in decryption_shares: + update_decrypt_status(f"Calculating share {current_share}/{share_count}") self._log.debug(f"announcing {decryption_share_dict.guardian_id}") guardian_sequence_number = election.get_guardian_sequence_order( decryption_share_dict.guardian_id @@ -41,7 +46,9 @@ def run(self, db: Database, decryption: DecryptionDto) -> None: decryption_share_dict.tally_share, decryption_share_dict.ballot_shares, ) + current_share += 1 + update_decrypt_status("Decrypting spoiled ballots") manifest = election.get_manifest() ballots = self._ballot_upload_service.get_ballots(db, election.id) spoiled_ballots = [ @@ -61,6 +68,8 @@ def run(self, db: Database, decryption: DecryptionDto) -> None: if plaintext_spoiled_ballots is None: raise Exception("No plaintext spoiled ballots found") + update_decrypt_status("Finalizing tally") + lagrange_coefficients = _get_lagrange_coefficients(decryption_mediator) self._log.debug("setting decryption completed") @@ -80,3 +89,8 @@ def _get_lagrange_coefficients( decryption_mediator: DecryptionMediator, ) -> LagrangeCoefficientsRecord: return LagrangeCoefficientsRecord(decryption_mediator.get_lagrange_coefficients()) + + +def update_decrypt_status(status: str) -> None: + # pylint: disable=no-member + eel.update_decrypt_status(status) diff --git a/src/electionguard_gui/services/export_service.py b/src/electionguard_gui/services/export_service.py index 674764b2e..fe2274c22 100644 --- a/src/electionguard_gui/services/export_service.py +++ b/src/electionguard_gui/services/export_service.py @@ -5,13 +5,13 @@ def get_export_locations() -> list[str]: export_dir = get_export_dir() if os.name == "nt": - drives = get_drives() + drives = get_removable_drives() return [export_dir, _get_download_path(), get_data_dir()] + drives return [export_dir] -def get_drives() -> list[str]: - dl = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +def get_removable_drives() -> list[str]: + dl = "DEFGHIJKLMNOPQRSTUVWXYZ" drives = [f"{d}:\\" for d in dl if os.path.exists(f"{d}:")] return drives diff --git a/src/electionguard_gui/web/android-chrome-192x192.png b/src/electionguard_gui/web/android-chrome-192x192.png new file mode 100644 index 000000000..674d2f192 Binary files /dev/null and b/src/electionguard_gui/web/android-chrome-192x192.png differ diff --git a/src/electionguard_gui/web/android-chrome-512x512.png b/src/electionguard_gui/web/android-chrome-512x512.png new file mode 100644 index 000000000..a82f46644 Binary files /dev/null and b/src/electionguard_gui/web/android-chrome-512x512.png differ diff --git a/src/electionguard_gui/web/apple-touch-icon.png b/src/electionguard_gui/web/apple-touch-icon.png new file mode 100644 index 000000000..588be161a Binary files /dev/null and b/src/electionguard_gui/web/apple-touch-icon.png differ diff --git a/src/electionguard_gui/web/components/admin/create-decryption-component.js b/src/electionguard_gui/web/components/admin/create-decryption-component.js index 31a30a378..6d3a26eba 100644 --- a/src/electionguard_gui/web/components/admin/create-decryption-component.js +++ b/src/electionguard_gui/web/components/admin/create-decryption-component.js @@ -27,6 +27,9 @@ export default { } this.loading = false; }, + getElectionUrl: function () { + return RouterService.getElectionUrl(this.electionId); + }, }, async mounted() { const result = await eel.get_suggested_decryption_name(this.electionId)(); @@ -50,7 +53,8 @@ export default {
- + Cancel +
diff --git a/src/electionguard_gui/web/components/admin/export-encryption-package-component.js b/src/electionguard_gui/web/components/admin/export-encryption-package-component.js index 1ce692efc..f79803099 100644 --- a/src/electionguard_gui/web/components/admin/export-encryption-package-component.js +++ b/src/electionguard_gui/web/components/admin/export-encryption-package-component.js @@ -61,8 +61,8 @@ export default {
- - Cancel + Cancel +
diff --git a/src/electionguard_gui/web/components/admin/upload-ballots-component.js b/src/electionguard_gui/web/components/admin/upload-ballots-component.js index 8134d4814..713bbace5 100644 --- a/src/electionguard_gui/web/components/admin/upload-ballots-component.js +++ b/src/electionguard_gui/web/components/admin/upload-ballots-component.js @@ -1,150 +1,29 @@ -import RouterService from "../../services/router-service.js"; -import Spinner from "../shared/spinner-component.js"; +import UploadBallotsLegacy from "./upload-ballots-legacy-component.js"; +import UploadBallotsWizard from "./upload-ballots-wizard-component.js"; export default { props: { electionId: String, }, - components: { Spinner }, + methods: { + closeWizard: function () { + this.useWizard = false; + }, + }, data() { return { - election: null, - loading: false, - alert: null, - ballotsProcessed: null, - ballotsTotal: null, - success: false, - duplicateCount: 0, + useWizard: null, }; }, - methods: { - async uploadBallots() { - try { - const form = document.getElementById("mainForm"); - if (form.checkValidity()) { - this.loading = true; - this.alert = null; - this.ballotsProcessed = 0; - const ballotFiles = document.getElementById("ballotsFolder").files; - this.ballotsTotal = ballotFiles.length; - - const uploadId = await this.uploadDeviceFile(); - await this.uploadBallotFiles(uploadId, ballotFiles); - this.success = true; - } - form.classList.add("was-validated"); - } catch (ex) { - console.error(ex); - this.alert = ex.message; - } finally { - this.loading = false; - } - }, - async uploadDeviceFile() { - const [deviceFile] = document.getElementById("deviceFile").files; - const deviceContents = await deviceFile.text(); - console.log("Creating election", deviceFile.name); - const result = await eel.create_ballot_upload( - this.electionId, - deviceFile.name, - deviceContents - )(); - if (!result.success) { - throw new Error(result.message); - } - this.ballotsProcessed++; - return result.result; - }, - async uploadBallotFiles(uploadId, ballotFiles) { - for (let i = 0; i < ballotFiles.length; i++) { - const ballotFile = ballotFiles[i]; - const ballotContents = await ballotFile.text(); - console.log("Uploading ballot", ballotFile.name); - const result = await eel.upload_ballot( - uploadId, - this.electionId, - ballotFile.name, - ballotContents - )(); - if (!result.success) { - throw new Error(result.message); - } - if (result.result.is_duplicate) { - this.duplicateCount++; - } - this.ballotsProcessed++; - } - }, - getElectionUrl: function () { - return RouterService.getElectionUrl(this.electionId); - }, - uploadMore: function () { - this.success = false; - this.duplicateCount = 0; - this.election = null; - this.loading = false; - this.alert = null; - this.ballotsProcessed = null; - this.ballotsTotal = null; - this.success = false; - this.duplicateCount = 0; - this.$nextTick(() => { - this.resetFiles(); - }); - }, - resetFiles: function () { - document.getElementById("deviceFile").value = null; - document.getElementById("ballotsFolder").value = null; - }, + components: { + UploadBallotsLegacy, + UploadBallotsWizard, }, - mounted() { - this.resetFiles(); + async mounted() { + this.useWizard = await eel.is_wizard_supported()(); }, template: /*html*/ ` - - -
-
-
-

Upload Ballots

-
-
- - -
Please provide a device file.
-
-
- - -
Please provide a ballot folder.
-
-
- - Cancel - -

{{ ballotsProcessed }} of {{ ballotsTotal }} files processed.

-
- -
- -

Successfully uploaded {{ballotsTotal-duplicateCount}} ballots.

- Done Uploading - -
+ + `, }; diff --git a/src/electionguard_gui/web/components/admin/upload-ballots-legacy-component.js b/src/electionguard_gui/web/components/admin/upload-ballots-legacy-component.js new file mode 100644 index 000000000..63cfdff06 --- /dev/null +++ b/src/electionguard_gui/web/components/admin/upload-ballots-legacy-component.js @@ -0,0 +1,144 @@ +import RouterService from "../../services/router-service.js"; +import Spinner from "../shared/spinner-component.js"; +import UploadBallotsSuccess from "./upload-ballots-success-component.js"; + +export default { + props: { + electionId: String, + }, + components: { Spinner, UploadBallotsSuccess }, + data() { + return { + election: null, + loading: false, + alert: null, + ballotsProcessed: null, + ballotsTotal: null, + success: false, + duplicateCount: 0, + }; + }, + methods: { + async uploadBallots() { + try { + const form = document.getElementById("mainForm"); + if (form.checkValidity()) { + this.loading = true; + this.alert = null; + this.ballotsProcessed = 0; + const ballotFiles = document.getElementById("ballotsFolder").files; + this.ballotsTotal = ballotFiles.length; + + const uploadId = await this.uploadDeviceFile(); + await this.uploadBallotFiles(uploadId, ballotFiles); + this.success = true; + } + form.classList.add("was-validated"); + } catch (ex) { + console.error(ex); + this.alert = ex.message; + } finally { + this.loading = false; + } + }, + async uploadDeviceFile() { + const [deviceFile] = document.getElementById("deviceFile").files; + const deviceContents = await deviceFile.text(); + console.log("Creating election", deviceFile.name); + const result = await eel.create_ballot_upload( + this.electionId, + deviceFile.name, + deviceContents + )(); + if (!result.success) { + throw new Error(result.message); + } + this.ballotsProcessed++; + return result.result; + }, + async uploadBallotFiles(uploadId, ballotFiles) { + for (let i = 0; i < ballotFiles.length; i++) { + const ballotFile = ballotFiles[i]; + const ballotContents = await ballotFile.text(); + console.log("Uploading ballot", ballotFile.name); + const result = await eel.upload_ballot( + uploadId, + this.electionId, + ballotFile.name, + ballotContents + )(); + if (!result.success) { + throw new Error(result.message); + } + if (result.result.is_duplicate) { + this.duplicateCount++; + } + this.ballotsProcessed++; + } + }, + getElectionUrl: function () { + return RouterService.getElectionUrl(this.electionId); + }, + uploadMore: function () { + this.success = false; + this.duplicateCount = 0; + this.election = null; + this.loading = false; + this.alert = null; + this.ballotsProcessed = null; + this.ballotsTotal = null; + this.$nextTick(() => { + this.resetFiles(); + }); + }, + resetFiles: function () { + document.getElementById("deviceFile").value = null; + document.getElementById("ballotsFolder").value = null; + }, + }, + mounted() { + this.resetFiles(); + }, + template: /*html*/ ` + + +
+
+
+

Upload Ballots

+
+
+ + +
Please provide a device file.
+
+
+ + +
Please provide a ballot folder.
+
+
+ + Cancel + +

{{ ballotsProcessed }} of {{ ballotsTotal }} files processed.

+
+ + + `, +}; diff --git a/src/electionguard_gui/web/components/admin/upload-ballots-success-component.js b/src/electionguard_gui/web/components/admin/upload-ballots-success-component.js new file mode 100644 index 000000000..7b0d89420 --- /dev/null +++ b/src/electionguard_gui/web/components/admin/upload-ballots-success-component.js @@ -0,0 +1,15 @@ +export default { + props: { + ballotCount: Number, + electionId: String, + backUrl: String, + }, + template: /*html*/ ` +
+ +

Successfully uploaded {{ballotCount}} ballots.

+ + Done Uploading +
+ `, +}; diff --git a/src/electionguard_gui/web/components/admin/upload-ballots-wizard-component.js b/src/electionguard_gui/web/components/admin/upload-ballots-wizard-component.js new file mode 100644 index 000000000..12d5f5311 --- /dev/null +++ b/src/electionguard_gui/web/components/admin/upload-ballots-wizard-component.js @@ -0,0 +1,143 @@ +import RouterService from "../../services/router-service.js"; +import Spinner from "../shared/spinner-component.js"; +import UploadBallotsSuccess from "./upload-ballots-success-component.js"; + +export default { + props: { + electionId: String, + }, + data() { + return { + drive: null, + success: false, + alert: null, + ballotCount: null, + status: null, + loading: false, + duplicateCount: 0, + }; + }, + methods: { + uploadBallots: async function () { + this.loading = true; + try { + const result = await eel.upload_ballots(this.electionId)(); + console.log("upload completed", result); + if (result.success) { + this.success = true; + this.ballotCount = result.result.ballot_count; + this.duplicateCount = result.result.duplicate_count; + } else { + this.alert = result.message; + } + } finally { + this.loading = false; + this.status = null; + } + }, + closeWizard: function () { + this.$emit("close"); + }, + getElectionUrl: function () { + return RouterService.getElectionUrl(this.electionId); + }, + uploadMore: async function () { + this.success = false; + this.drive = null; + this.alert = null; + this.ballotCount = null; + this.duplicateCount = 0; + this.status = null; + this.loading = false; + await this.scanDrives(); + }, + scanDrives: async function () { + const result = await eel.scan_drives()(); + if (!result.success) { + console.error(result.message); + this.alert = result.message; + } else { + this.drive = result.result; + console.log("successfully uploaded ballots", this.drive); + } + }, + updateUploadStatus: function (status) { + console.log("updateUploadStatus", status); + this.status = status; + }, + pollDrives: async function () { + if (this.drive) return; + await this.scanDrives(); + if (!this.drive) { + // keep polling until a valid drive is found + setTimeout(this.pollDrives.bind(this), 1000); + } + }, + }, + async mounted() { + eel.expose(this.updateUploadStatus, "update_upload_status"); + await this.pollDrives(); + }, + components: { + UploadBallotsSuccess, + Spinner, + }, + template: /*html*/ ` + + + +
+
+
+ +
+
+
+

Upload Wizard

+
+

Insert a USB drive containing ballots

+ +
+
+

Ready to import?

+
+
+ {{drive.drive}} +
+
+ drive +
+
+
+
+ {{drive.ballots}} +
+
+ ballots +
+
+
+
+ {{drive.location}} +
+
+ device +
+
+
+ Cancel + +

{{ status }}

+ +
+
+
+
+ `, +}; diff --git a/src/electionguard_gui/web/components/admin/view-decryption-admin-component.js b/src/electionguard_gui/web/components/admin/view-decryption-admin-component.js index b35738e34..76fc8c323 100644 --- a/src/electionguard_gui/web/components/admin/view-decryption-admin-component.js +++ b/src/electionguard_gui/web/components/admin/view-decryption-admin-component.js @@ -7,7 +7,7 @@ export default { }, components: { Spinner }, data() { - return { decryption: null, loading: false, error: false }; + return { decryption: null, loading: false, error: false, status: null }; }, methods: { getElectionUrl: function (electionId) { @@ -51,11 +51,17 @@ export default { } } finally { this.loading = false; + this.status = null; } }, + updateDecryptStatus: function (status) { + console.log("updateDecryptStatus", status); + this.status = status; + }, }, async mounted() { eel.expose(this.refresh_decryption, "refresh_decryption"); + eel.expose(this.updateDecryptStatus, "update_decrypt_status"); await this.get_decryption(false); console.log("watching decryption"); // only watch for changes if the decryption is in-progress @@ -157,6 +163,7 @@ export default {

{{decryption.status}}

+

{{ status }}

diff --git a/src/electionguard_gui/web/components/guardian/view-decryption-guardian-component.js b/src/electionguard_gui/web/components/guardian/view-decryption-guardian-component.js index 7695eb13d..9f9db8db4 100644 --- a/src/electionguard_gui/web/components/guardian/view-decryption-guardian-component.js +++ b/src/electionguard_gui/web/components/guardian/view-decryption-guardian-component.js @@ -13,6 +13,7 @@ export default { loading: false, error: false, successfully_joined: false, + status: null, }; }, methods: { @@ -21,14 +22,18 @@ export default { }, decrypt: async function () { this.loading = true; - this.error = false; - const result = await eel.join_decryption(this.decryptionId)(); - if (result.success) { - this.success = true; - } else { - this.error = true; + try { + this.error = false; + const result = await eel.join_decryption(this.decryptionId)(); + if (result.success) { + this.success = true; + } else { + this.error = true; + } + } finally { + this.loading = false; + this.status = null; } - this.loading = false; }, refresh_decryption: async function () { console.log("refreshing decryption"); @@ -43,10 +48,15 @@ export default { } this.loading = false; }, + updateDecryptStatus: function (status) { + console.log("updateDecryptStatus", status); + this.status = status; + }, }, async mounted() { - await this.refresh_decryption(); + eel.expose(this.updateDecryptStatus, "update_decrypt_status"); eel.expose(this.refresh_decryption, "refresh_decryption"); + await this.refresh_decryption(); console.log("watching decryption"); eel.watch_decryption(this.decryptionId); }, @@ -67,6 +77,7 @@ export default {

Join Tally

Click below to join {{decryption.decryption_name}}

+

{{ status }}

diff --git a/src/electionguard_gui/web/favicon-16x16.png b/src/electionguard_gui/web/favicon-16x16.png new file mode 100644 index 000000000..c785a9bfd Binary files /dev/null and b/src/electionguard_gui/web/favicon-16x16.png differ diff --git a/src/electionguard_gui/web/favicon-32x32.png b/src/electionguard_gui/web/favicon-32x32.png new file mode 100644 index 000000000..297497fe7 Binary files /dev/null and b/src/electionguard_gui/web/favicon-32x32.png differ diff --git a/src/electionguard_gui/web/favicon.ico b/src/electionguard_gui/web/favicon.ico new file mode 100644 index 000000000..a6b7b0f62 Binary files /dev/null and b/src/electionguard_gui/web/favicon.ico differ diff --git a/src/electionguard_gui/web/index.html b/src/electionguard_gui/web/index.html index c86d7d944..7703bb281 100644 --- a/src/electionguard_gui/web/index.html +++ b/src/electionguard_gui/web/index.html @@ -8,6 +8,10 @@ + + + +