Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions packages/web-ui/src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
let encryptDataPromise = new Promise<never>(() => {});
let shouldSummarize = false;
let ballotSummary = new Promise<never>(() => {});
let fetchVoteFile: undefined | (() => Promise<string>);

let url = globalThis.location?.hash.slice(1);

Expand All @@ -35,11 +36,11 @@
function maybeUpdateSummary() {
ballotSummary = shouldSummarize && ballot ? (async () => {
// Lazy-loading as the summary is only a nice-to-have.
const { getSummarizedBallot } = await import("./ballotSummary.ts");
return getSummarizedBallot(ballot);
const [{ getSummarizedBallot }, voteFileContents] = await Promise.all([import("./ballotSummary.ts"), fetchVoteFile().catch((e) => console.warn(e))]);
return getSummarizedBallot(ballot, voteFileContents);
})() : new Promise<never>(() => {});
}
function registerBallot(ballotContent, publicKey) {
function registerBallot(ballotContent, publicKey, _fetchVoteFile) {
encryptDataPromise = (async () => {
const { encryptedSecret, saltedCiphertext } = await encryptData(
textEncoder.encode(ballotContent) as Uint8Array,
Expand All @@ -51,6 +52,7 @@
});
})();
ballot = ballotContent;
fetchVoteFile = _fetchVoteFile;
step = 2;
maybeUpdateSummary();
}
Expand Down
55 changes: 52 additions & 3 deletions packages/web-ui/src/BallotSummary.svelte
Original file line number Diff line number Diff line change
@@ -1,14 +1,63 @@
<script lang="ts">
export let ballotSummary: Promise<string | never>;
export let ballotSummary: Promise<[string, {
missingCandidates?: VoteFileFormat["candidates"];
candidatesWithInvalidScores?: BallotFileFormat["preferences"];
} | null] | never>;
</script>

<summary>Ballot summary (requires downloading YAML parser)</summary>

{#await ballotSummary}
<textarea readonly>Getting summary… </textarea>
{:then data}
<textarea readonly>{data}</textarea>
{#if data[1]}
{#if data[1].missingCandidates?.length}
<details open><summary>Your ballot is missing some candidates</summary>
<ul>
{#each data[1].missingCandidates as candidate}
<li>{candidate}</li>
{/each}
</ul>
<p>Only ballots setting scores for all candidates will be taken into account.</p>
<p><em>Hint: it could be due to a typo or a failed copy-pasting.</em></p>
<hr/>
</details>
{/if}
{#if data[1].candidatesWithInvalidScores?.length}
<details open><summary>Your ballot contains invalid scores</summary>
<ul>
{#each data[1].candidatesWithInvalidScores as { title, score }}
<li>{score} for {title}</li>
{/each}
</ul>
<p>Use only "safe" integers, i.e. values that can be represented as an IEEE-754 double precision number.</p>
<hr/>
</details>
{/if}
{:else}
<details open><summary>We failed to check your ballot</summary>
<p>Something wrong happened when trying to check the ballot validity; it doesn't mean your ballot is invalid, we just can't tell.</p>
<p><em>Hint: authenticated API calls are more likely to succeed.</em></p>
<hr/>
</details>
{/if}
<pre>{data[0]}</pre>
{:catch error}
An error occurred: {error?.message ?? error}
An error occurred: {console.log(error) ?? error?.message ?? error}
{/await}

<style>
details, pre {
margin-left: 1rem;
}

details > summary::before {
content: "⚠️";
}
pre {
border: 1px solid currentColor;
padding: 1ch;
white-space: pre-wrap;
}
</style>

6 changes: 3 additions & 3 deletions packages/web-ui/src/FillBallotForm.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,18 @@

export let url, username, token, registerBallot;

let fetchedBallot: Promise<string>, fetchedPublicKey;
let fetchedBallot: Promise<string>, fetchedPublicKey, fetchVoteFile;

function onSubmit(this: HTMLFormElement, event: SubmitEvent) {
event.preventDefault();
const textarea = this.elements.namedItem("ballot") as HTMLInputElement;
registerBallot(textarea.value, fetchedPublicKey);
registerBallot(textarea.value, fetchedPublicKey, fetchVoteFile);
}

fetchedBallot = fetchedPublicKey = Promise.reject("no data");
beforeUpdate(() => {
fetchFromGitHub({ url, username, token }, (errOfResult) => {
[fetchedBallot, fetchedPublicKey] = errOfResult;
[fetchedBallot, fetchedPublicKey, fetchVoteFile] = errOfResult;
});
});
</script>
Expand Down
16 changes: 13 additions & 3 deletions packages/web-ui/src/ballotSummary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,26 @@ import {
getSummarizedBallot as _getSummarizedBallot,
summarizeCondorcetBallotForVoter,
} from "@node-core/caritat/summary/condorcet";
import {
getReasonForInvalidateBallot,
} from "@node-core/caritat/getReasonForInvalidateBallot";
import type { BallotFileFormat, VoteFileFormat } from "@node-core/caritat/parser";

export function getSummarizedBallot(ballotStr: string) {
export function getSummarizedBallot(ballotStr: string, voteFileString?: string) {
const ballot = yaml.load(ballotStr) as { preferences: Array<{ title: string; score: number }> };
if (!Array.isArray(ballot?.preferences)) {
throw new Error("Ballot does not contain a list of preferences");
}
return summarizeCondorcetBallotForVoter(
let invalidityReason: ReturnType<typeof getReasonForInvalidateBallot>;
try {
invalidityReason = voteFileString && getReasonForInvalidateBallot(ballot as BallotFileFormat, yaml.load(voteFileString) as VoteFileFormat);
} catch (e) {
console.warn(e);
}
return [summarizeCondorcetBallotForVoter(
_getSummarizedBallot({
voter: {},
preferences: ballot.preferences.map(({ title, score }) => [title, score]),
}),
);
), invalidityReason];
}
24 changes: 13 additions & 11 deletions packages/web-ui/src/fetchDataFromGitHub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,17 +196,18 @@ async function act(
voteFile.patch,
);

const fetchAsText = (response: Response) =>
response.ok
? response.text()
: Promise.reject(
new Error(
`Fetch error: ${response.status} ${response.statusText}`,
),
);

return [
fetch(ballotFile.contents_url, contentsFetchOptions)
.then(response =>
response.ok
? response.text()
: Promise.reject(
new Error(
`Fetch error: ${response.status} ${response.statusText}`,
),
),
)
.then(fetchAsText)
.then(
shouldShuffleCandidates
? (ballotData) => {
Expand Down Expand Up @@ -266,7 +267,8 @@ async function act(
),
),
),
] as [Promise<string>, Promise<ArrayBuffer>];
() => fetch(voteFile.contents_url, contentsFetchOptions).then(fetchAsText),
] as [Promise<string>, Promise<ArrayBuffer>, () => Promise<string>];
} catch (err) {
const error = Promise.reject(err);
return [error, error] as [never, never];
Expand All @@ -277,7 +279,7 @@ let previousURL: string | null;
export default function fetchFromGitHub(
{ url, username, token }: { url: string; username?: string; token?: string },
callback: (
errOfResult: [Promise<string>, Promise<ArrayBuffer>]
errOfResult: [Promise<string>, Promise<ArrayBuffer>, () => Promise<string>]
) => void | Promise<void>,
) {
const options
Expand Down