Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
98f598c
wip
BelSequent Aug 12, 2026
b912319
Voting portal and headers policy fixes
BelSequent Aug 12, 2026
c5d8218
Admin portal fixes
BelSequent Aug 12, 2026
a442b97
Claude
BelSequent Aug 12, 2026
21d4f0b
Fixes: table, ADD, Edit, text fields.
BelSequent Aug 12, 2026
8f8fe6e
Fixes
BelSequent Aug 12, 2026
18b293a
Types
BelSequent Aug 12, 2026
f4434b1
routes + windmill services
BelSequent Aug 13, 2026
c9ba374
wip e2e
BelSequent Aug 13, 2026
05b6156
Fix permissions
BelSequent Aug 13, 2026
a191fb9
realm import file with new attr
BelSequent Aug 13, 2026
974e962
ui-core TypeScript mirror
BelSequent Aug 13, 2026
04dfc1c
admin-portal feature code + translations
BelSequent Aug 13, 2026
e739445
admin-portal queries
BelSequent Aug 14, 2026
ddaa4e8
Ack query ts files
BelSequent Aug 14, 2026
f85f3b8
graphql actions
BelSequent Aug 14, 2026
2eee3d7
Fixes and permissions
BelSequent Aug 14, 2026
0c6f61a
Fix the Alert banner to display the localized name and fix translations.
BelSequent Aug 24, 2026
317c183
A policy change only takes effect after the next publication
BelSequent Aug 24, 2026
147c17f
Remove activated field
BelSequent Aug 24, 2026
4c0a2d6
Update graphql
BelSequent Aug 24, 2026
6c7c13c
Hide alert on transitions/refresh
BelSequent Aug 24, 2026
a30cb3b
Merge remote-tracking branch 'origin/main' into feat/meta-12818/main
BelSequent Aug 24, 2026
df09bbd
Update graphql
BelSequent Aug 24, 2026
6223189
Merge remote-tracking branch 'origin/main' into feat/meta-12818/main
BelSequent Aug 28, 2026
8e2f201
misc
BelSequent Aug 28, 2026
aca0bdc
✨ Voting Portal: Acclaimed Contests (#3106)
Findeton Aug 29, 2026
0bb2a9c
✨ Redirect kiosk voters back to the kiosk login after logout (#3103…
Findeton Aug 29, 2026
a26d8b8
🐞 Voting Portal: Accessibility - WCAG 2.1 AA compliancy pass for voti…
Findeton Aug 31, 2026
230c147
Translations
BelSequent Aug 31, 2026
748d383
Fix
BelSequent Aug 31, 2026
f10d711
Update graphql
BelSequent Aug 31, 2026
f3a1da5
Build core
BelSequent Aug 31, 2026
a6ea5f8
Merge remote-tracking branch 'origin/main' into feat/meta-12818/main
BelSequent Aug 31, 2026
d9eefd6
Build core
BelSequent Aug 31, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,15 @@
"clientRole": false,
"containerId": "613e770d-7498-4726-90ff-b288f40a7b30",
"attributes": {}
},
{
"id": "fdff7a41-a8c7-4a51-8d56-a94f852b22ad",
"name": "ack-support-materials",
"description": "",
"composite": false,
"clientRole": false,
"containerId": "613e770d-7498-4726-90ff-b288f40a7b30",
"attributes": {}
}
],
"client": {
Expand Down Expand Up @@ -470,7 +479,8 @@
"attributes": {},
"realmRoles": [
"default-roles-tenant90505c8a23a94cdfa26b4e19f6a097d5event33f18502a67c48538333a58630663559",
"user"
"user",
"ack-support-materials"
],
"clientRoles": {}
}
Expand Down Expand Up @@ -2709,7 +2719,7 @@
"subComponents": {},
"config": {
"kc.user.profile.config": [
"{\"attributes\":[{\"name\":\"firstName\",\"displayName\":\"${firstName}\",\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}},\"annotations\":{\"html-attribute:autocomplete\":\"given-name\"},\"required\":{\"roles\":[\"user\"]},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\"]},\"multivalued\":false},{\"name\":\"lastName\",\"displayName\":\"${lastName}\",\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}},\"annotations\":{},\"required\":{\"roles\":[\"user\"]},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\"]},\"multivalued\":false},{\"name\":\"username\",\"displayName\":\"${username}\",\"validations\":{\"up-username-not-idn-homograph\":{},\"length\":{\"min\":\"3\",\"max\":\"40\"}},\"annotations\":{\"hidden\":\"true\"},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"email\",\"displayName\":\"${email}\",\"validations\":{\"email\":{},\"length\":{\"max\":255}},\"annotations\":{},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"tenant-id\",\"displayName\":\"Tenant Id\",\"validations\":{},\"annotations\":{},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\"]},\"multivalued\":false},{\"name\":\"area-id\",\"displayName\":\"Area Id\",\"validations\":{},\"annotations\":{},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\"]},\"multivalued\":false},{\"name\":\"vote-weight\",\"displayName\":\"Vote Weight\",\"validations\":{\"integer\":{\"min\":\"1\",\"max\":\"100000\"}},\"annotations\":{},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\"]},\"multivalued\":false},{\"name\":\"authorized-election-ids\",\"displayName\":\"Authorized Elections\",\"validations\":{},\"annotations\":{},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\"]},\"multivalued\":true},{\"name\":\"annotations\",\"displayName\":\"annotations\",\"validations\":{},\"annotations\":{},\"permissions\":{\"view\":[\"admin\"],\"edit\":[\"admin\"]},\"multivalued\":true}],\"groups\":[]}"
"{\"attributes\":[{\"name\":\"firstName\",\"displayName\":\"${firstName}\",\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}},\"annotations\":{\"html-attribute:autocomplete\":\"given-name\"},\"required\":{\"roles\":[\"user\"]},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\"]},\"multivalued\":false},{\"name\":\"lastName\",\"displayName\":\"${lastName}\",\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}},\"annotations\":{},\"required\":{\"roles\":[\"user\"]},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\"]},\"multivalued\":false},{\"name\":\"username\",\"displayName\":\"${username}\",\"validations\":{\"up-username-not-idn-homograph\":{},\"length\":{\"min\":\"3\",\"max\":\"40\"}},\"annotations\":{\"hidden\":\"true\"},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"email\",\"displayName\":\"${email}\",\"validations\":{\"email\":{},\"length\":{\"max\":255}},\"annotations\":{},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"tenant-id\",\"displayName\":\"Tenant Id\",\"validations\":{},\"annotations\":{},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\"]},\"multivalued\":false},{\"name\":\"area-id\",\"displayName\":\"Area Id\",\"validations\":{},\"annotations\":{},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\"]},\"multivalued\":false},{\"name\":\"vote-weight\",\"displayName\":\"Vote Weight\",\"validations\":{\"integer\":{\"min\":\"1\",\"max\":\"100000\"}},\"annotations\":{},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\"]},\"multivalued\":false},{\"name\":\"authorized-election-ids\",\"displayName\":\"Authorized Elections\",\"validations\":{},\"annotations\":{},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\"]},\"multivalued\":true},{\"name\":\"annotations\",\"displayName\":\"annotations\",\"validations\":{},\"annotations\":{},\"permissions\":{\"view\":[\"admin\"],\"edit\":[\"admin\"]},\"multivalued\":true},{\"name\":\"support-materials-acknowledged\",\"displayName\":\"Support Materials Acknowledged\",\"validations\":{},\"annotations\":{},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\"]},\"multivalued\":true}],\"groups\":[]}"
]
}
}
Expand Down
55 changes: 55 additions & 0 deletions docs/docusaurus/docs/02-election_managers/03-support-materials.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
---
id: support_materials
title: Support Materials
---

<!--
SPDX-FileCopyrightText: 2026 Sequent Tech Inc <legal@sequentech.io>
SPDX-License-Identifier: AGPL-3.0-only
-->

# Support Materials

Support Materials are event-level documents (guides, sample ballots, instructional videos) that voters can review before voting. The Support Materials Policy controls whether they are shown at all, and whether reading them is a precondition for voting.

## Policy

Configure the policy in the Support Materials accordion on the Election Event Data tab:

- `Off` hides Support Materials entirely in the Voting Portal and does not gate voting.
- `Optional` shows Support Materials as a generic link from the Ballot list; voting is never blocked.
- `Mandatory for Voting` requires each voter to open every listed Support Material and acknowledge having read them before any ballot's Start Voting control is enabled.

The policy applies to the whole Election Event; there is no per-election override. Election events created before this policy existed carry only the legacy `presentation.materials.activated` boolean — `Optional` when `true`, `Off` when `false` or absent — until an administrator sets the policy explicitly.

## Voter experience under Mandatory for Voting

When the policy is `Mandatory for Voting` and a voter has not yet acknowledged the materials for this Election Event, the Ballot list shows an instruction banner ("You must read the Support Materials before you can vote.") and disables Start Voting on every ballot. The Support Materials button remains available so the voter can open it.

On the Support Materials screen, each material must be opened at least once via Preview; a material is marked viewed the moment its preview dialog is closed. The "I have read the Support Materials" checkbox stays disabled until every listed material has been viewed, and Continue stays disabled until the checkbox is checked. Choosing Continue records the acknowledgment and returns the voter to the Ballot list with Start Voting enabled; choosing Back to ballot list leaves the Ballot list gated, since nothing is recorded until Continue succeeds.

Once acknowledged, the gate is skipped on later visits for that Election Event — the Support Materials button stays available to reopen the documents, but Start Voting no longer requires it.

## Acknowledgment storage

Acknowledgment is recorded as a Keycloak user attribute (`support-materials-acknowledged`) on the voter's account, holding the list of Support Material document ids they confirmed reading. Since each Election Event has its own Keycloak realm, the attribute is inherently scoped per voter per Election Event — the same pattern used for `voted-channel`. Reopening materials after acknowledging does not clear it; there is currently no bulk-reset action for administrators.

The Election Event Voters list shows a "Support Materials Viewed" column — a plain boolean read from that same attribute — when the policy is `Mandatory for Voting`.

## Requests and permissions

The Voting Portal reads and writes acknowledgment through two Hasura Actions backed by Harvest, both authenticated with the voter's own bearer token (`role: user`, `forward_client_headers: true`):

- `get_support_materials_acknowledgment(election_event_id)` returns the document ids the calling voter has already acknowledged.
- `acknowledge_support_materials(election_event_id, document_ids)` records acknowledgment for the calling voter, after checking the Election Event's policy is not `Off`.

Both actions derive the voter's identity from their own JWT claims — a voter can only ever read or write their own acknowledgment.

Both actions also require the voter's Keycloak session to carry a dedicated `ack-support-materials` realm role, checked the same way ballot casting checks the voter's `user` role. This role must exist in the Election Event's own realm (`tenant-<tenant_id>-event-<election_event_id>`) and be granted to the `voter` group — without it, every voter is rejected regardless of the Support Materials Policy.

### Adding the role in the Keycloak Console

1. Open the Keycloak Admin Console and switch to the Election Event's realm (`tenant-<tenant_id>-event-<election_event_id>`).
2. Under **Realm roles**, click **Create role**, set **Role name** to `ack-support-materials`, and save.
3. Under **Groups**, open **voter**, go to its **Role mapping** tab, click **Assign role**, filter by realm roles, select `ack-support-materials`, and click **Assign**.
4. Voters who are already signed in keep whatever role set was in their current token — they need to log out and back in (or wait for their token to refresh) before the new role takes effect.
22 changes: 22 additions & 0 deletions hasura/metadata/actions.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,19 @@ type Mutation {
): ScheduledEventOutput3
}

type Mutation {
acknowledge_support_materials(
election_event_id: uuid!
document_ids: [String!]!
): AcknowledgeSupportMaterialsOutput
}

type Query {
get_support_materials_acknowledgment(
election_event_id: uuid!
): GetSupportMaterialsAcknowledgmentOutput!
}

type Mutation {
create_ballot_receipt(
ballot_id: String!
Expand Down Expand Up @@ -1481,6 +1494,15 @@ type createBallotReceiptOutput {
status: String
}

type AcknowledgeSupportMaterialsOutput {
election_event_id: uuid!
document_ids: [String!]!
}

type GetSupportMaterialsAcknowledgmentOutput {
document_ids: [String!]!
}

type OptionalImportEvent {
id: String
message: String
Expand Down
30 changes: 30 additions & 0 deletions hasura/metadata/actions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,34 @@ actions:
permissions:
- role: admin-user
comment: create scheduled event
- name: acknowledge_support_materials
definition:
kind: synchronous
handler: http://{{HARVEST_DOMAIN}}/acknowledge-support-materials
forward_client_headers: true
request_transform:
body:
action: transform
template: "{{$body.input}}"
template_engine: Kriti
version: 2
permissions:
- role: user
comment: acknowledge_support_materials
- name: get_support_materials_acknowledgment
definition:
kind: synchronous
handler: http://{{HARVEST_DOMAIN}}/get-support-materials-acknowledgment
forward_client_headers: true
request_transform:
body:
action: transform
template: "{{$body.input}}"
template_engine: Kriti
version: 2
permissions:
- role: user
comment: get_support_materials_acknowledgment
- name: create_ballot_receipt
definition:
kind: synchronous
Expand Down Expand Up @@ -1862,6 +1890,8 @@ custom_types:
- name: PrepareBallotPublicationPreviewOutput
- name: ExportUsersOutput
- name: SetUserRoleOutput
- name: AcknowledgeSupportMaterialsOutput
- name: GetSupportMaterialsAcknowledgmentOutput
- name: CreateTallyOutput
- name: StartTallyOutput
- name: PublishBallotOutput
Expand Down
174 changes: 174 additions & 0 deletions packages/admin-portal/graphql.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,58 @@
"kind": "OBJECT"
},
"types": [
{
"kind": "OBJECT",
"name": "AcknowledgeSupportMaterialsOutput",
"description": null,
"isOneOf": null,
"fields": [
{
"name": "document_ids",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "election_event_id",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "uuid",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "Aggregate",
Expand Down Expand Up @@ -4359,6 +4411,42 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "GetSupportMaterialsAcknowledgmentOutput",
"description": null,
"isOneOf": null,
"fields": [
{
"name": "document_ids",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "GetTopCastVotesByIpInput",
Expand Down Expand Up @@ -11965,6 +12053,59 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "acknowledge_support_materials",
"description": "acknowledge_support_materials",
"args": [
{
"name": "document_ids",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "election_event_id",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "uuid",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "OBJECT",
"name": "AcknowledgeSupportMaterialsOutput",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "apply_external_reconciliation_changes",
"description": "applies the Sequent-side reconciliation diff for an already-computed import",
Expand Down Expand Up @@ -35939,6 +36080,39 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "get_support_materials_acknowledgment",
"description": "get_support_materials_acknowledgment",
"args": [
{
"name": "election_event_id",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "uuid",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "GetSupportMaterialsAcknowledgmentOutput",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "get_top_votes_by_ip",
"description": null,
Expand Down
Binary file modified packages/admin-portal/rust/sequent-core-0.1.0.tgz
Binary file not shown.
3 changes: 2 additions & 1 deletion packages/admin-portal/src/components/Tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ const TabStyles = {
Wrapper: styled("div")`
display: flex;
flex-direction: column;
align-items: left;
align-items: flex-start;
width: 100%;
border-bottom: 1px solid rgba(0, 0, 0, 0.12);

.MuiTabs-scroller {
Expand Down
Loading
Loading