Releases: asm0dey/slidev-polls
v0.6.0
Highlights
- New
<PollQr>component for the Slidev addon — a standalone, always-visible inline QR code for a poll's voter URL, placeable on any slide. No button, overlay, sign-in, or network: host comes from the deck'spollServer(frontmatter/headmatter, origin fallback) + theslugprop. Self-bounding (max-width: min(360px, 60vmin), centered) so it fits any slide bare, including alayout: two-colscolumn. - Shared QR config (
buildQrOptions) and voter-URL resolver (useVoterUrl) extracted; the existing presenterPollQrButtonoverlay now reuses them. - Demo deck gains two "join" QR slides; e2e covers QR-overlay visibility.
Artifacts
- npm:
@slidev-polls/component@0.6.0,@slidev-polls/shared@0.6.0 - Docker:
ghcr.io/asm0dey/slidev-polls-backend:0.6.0(linux/amd64 + linux/arm64)
See PR #49 for the full change set.
v0.5.2
Presenter-mode fixes for the Slidev poll deck.
- Active question no longer goes stale in presenter mode: poll activation is gated on the Slidev render context, so the next-slide preview (
previewNext) and overview copies no longer reclaim the next question's activation. - QR overlay now syncs across the presenter and audience windows (BroadcastChannel), so toggling it in the presenter shows it on the audience screen.
Full Changelog: v0.5.1...v0.5.2
v0.5.1 — session cookie fix + deck activation race
Fixes
-
Session lookup 500 after upgrading to v0.5.0. v0.5.0 moved presenter sessions to Spring Session JDBC. A returning visitor's stale
SP_SESSIONcookie (an old Tomcat in-memory session id from the pre-session app) was base64-decoded into bytes containing a0x00, and that NUL was passed into the PostgresWHERE SESSION_ID = ?lookup — throwinginvalid byte sequence for encoding "UTF8": 0x00and returning HTTP 500 on every request that carried such a cookie.CachingSessionRepositorynow ignores a malformed (NUL-bearing) session id and treats it as "no session", so Spring issues a fresh one and replaces the bad cookie on the next response. Affected clients self-heal on their next request — no manual cookie purge or schema change required. -
Deck slide navigation could clear the active question. When advancing slides quickly, the deck's slide-leave
/close(scoped to the question that slide opened) could race past the next slide's/activateand null the active question — the guard was decided on an unlocked read and then closed whatever was active. The conditional close is now applied atomically under the poll-row lock, so it is a true no-op once the active question has moved on.
Upgrade: drop-in over v0.5.0. No migrations, no config changes.
Backend image: ghcr.io/asm0dey/slidev-polls:v0.5.1 · npm: @slidev-polls/*@0.5.1
Full changelog: v0.5.0...v0.5.1
v0.5.0 — accounts, poll sharing & transfer, user blocking
Highlights
This release turns single-presenter polls into a multi-presenter, account-aware system: presenters get real sessions and passwords, polls can be shared with or handed over to colleagues, and a bootstrap admin can manage accounts.
Presenter accounts & sessions
- Spring Session JDBC store with a Caffeine read cache and a session registry.
- Self-service password change — changing your password signs out your other sessions but keeps the current one.
GET /api/admin/accountexposes the signed-in presenter and whether they are the bootstrap admin.
Poll sharing & transfer
- Share a poll with another presenter as a collaborator: collaborators can edit the poll and run it from their own Slidev deck.
- Transfer a poll to hand ownership over outright (confirmed by typing the new owner's name).
- Owner-only actions (delete, transfer, manage collaborators) are gated on ownership; shared polls are flagged in the poll list via
isOwner. - Deck tokens record who minted them; removing a collaborator revokes the deck tokens they created.
User management (bootstrap admin)
- The first account created by the setup wizard is the bootstrap admin.
- Admin can reset another presenter's password and block / unblock accounts.
- Blocking immediately ends that presenter's sessions and revokes their deck tokens; a blocked account cannot sign in until unblocked (
USER_BLOCKED).
Fixes
- Collaborators can now sign into the deck. Deck login selected the poll from owner-only polls, so a presenter who only collaborated (owned no poll) got
401 AUTH_REQUIRED; it now selects from visible (owned-or-collaborated) polls. Covered by a regression integration test and a Playwright e2e. - Repo-wide static-analysis cleanup: JSpecify nullability on Spring filters, Jackson
asString(), TestcontainersDockerImageName, try-with-resources for leaked resources, CSS generic-font fallbacks, an unresolved CSS variable, and FK-safe test isolation.
Upgrade notes
- New Flyway migrations are applied automatically on startup: PostgreSQL
V12–V15, H2V4–V7(Spring Session tables,poll_collaborators,deck_tokens.minted_by,admin_user.blocked_at). No manual steps. - No breaking API changes for voters or existing single-presenter decks.
Backend image: ghcr.io/asm0dey/slidev-polls:v0.5.0 · npm: @slidev-polls/*@0.5.0
Full changelog: v0.4.2...v0.5.0
v0.4.2 — voter-share % for multi-choice polls
Highlights
Multi-choice poll percentages now answer "what fraction of the room picked X?" A question with 2 voters and max=3 picks rendered "33% SAP Machine" even when both voters had picked it — the figure was selection-share (count / total selections) rather than voter-share. Audience reading bars as "share of the room" got nonsensical numbers like "33% among 2 voters".
Leader-row percentage label is readable again. The leader row applied color: var(--sp-accent-fg) row-wide, but the bar fill only spans pct% of the row width. The right-anchored percentage label sat outside the bar when pct < 100, so in dark theme the near-black accent-fg landed on the dark row background — illegible.
Fix
ResultsPaneldenominator switches tovoterCountwhenmaxSelections > 1. Single-choice keeps the historicalcount / totalpath (the two are equivalent there). Each voter contributes at most 1 to any option's count, socount / voterCount ≤ 1by construction.- Scoped the leader colour override to
.sp-rp__labelonly. The label sits at the left edge (always inside the bar when pct > 0) and keeps the on-accent foreground; the percentage inherits theme--sp-fgand reads against--sp-bg-subtleoutside the bar.
Tests
- New
ResultsPanel.test.tscase reproduces the exact 2-voter / 6-selection scenario and asserts SAP Machine renders100%(not33%), singletons50%, unpicked0%.
Full changelog
v0.4.1 — fix slide-switch deadlock storm
Highlights
Slide-switch deadlock storm fixed. When a Slidev deck mounted multiple <PollResults /> panels (e.g. main view + presenter view), every slide switch fired close + activate POSTs from each mounted panel. Concurrent activates on the same poll's poll_questions rows acquired per-tuple row locks in different orders and deadlocked (PostgreSQL 40P01). Hikari connections then piled up on deadlock-victim retries and the next backoffice PATCH appeared to hang.
Fix
PollRepositoryImpl.activateQuestionandcloseActiveQuestionnow wrap the state-flipping UPDATE in adsl.transactionResultand takeSELECT polls … FOR UPDATEon the owning poll row up front. Every activate / close / header-update on the same poll serialises behind one row lock; the admin PATCH naturally takes the same lock via itsUPDATE polls, so the two paths can no longer interleave their per-question locks.- Added a package-private
findById(DSLContext, UUID)overload so the post-mutation read inside the transaction callback sees the writes the transaction has just made (the outer auto-commitdslwould have read pre-commit state on a different connection).
Tests
- New
PollActivateDeadlockIT: 16 threads × 25 iterations of interleavedactivate(qN)/closeActiveQuestion/updateHeaderon a single poll. Parametrised over PostgreSQL and H2. Asserts no SQL exception and the at-most-one-ACTIVE-question invariant.
Full changelog
v0.4.0 — per-question multi-choice ballots
Highlights
Per-question arity model. Every question now carries minSelections and maxSelections instead of an implicit single-choice shape. (1, 1) = radio buttons (single-choice); anything else = checkboxes; min=0 lets voters submit an empty ballot ("Skip"). The voter SPA, backoffice editor, and slidev deck panels all honour the new shape.
Destructive-edit lock. Once a question has any stored votes, the backend rejects structural edits (option-delete, question-delete, arity changes) with RESOURCE_HAS_VOTES (HTTP 409). Prompt and option-label edits remain free.
Snapshot-only SSE. The legacy tally-delta channel is gone; every ballot change triggers a fresh snapshot broadcast. Domain events are signal-only (pollId, questionId, emittedAt). The snapshot payload now carries voterCount so multi-choice tallies can show N voters · M selections.
CORS-aware hint. When a slidev deck's origin isn't in the poll's Allowed origins, the panel surfaces an actionable hint instead of silently sitting on live updates paused.
Backend
- DB migration:
poll_questions.min_selections/max_selections(NOT NULL DEFAULT 1, CHECK constraint),votes.option_ids uuid[](NOT NULL, empty allowed), GIN index on Postgres.poll_questions.typeandvotes.option_iddropped. H2 baseline mirrored. - New endpoint behaviour on
POST /api/polls/{slug}/votes: body is{ "optionIds": ["..."] }; legacy{ "optionId": "..." }rejected withVALIDATION_FAILED. VoteServicevalidates ballot size against arity, rejects duplicates and unknown options, accepts empty ballots whenmin=0.PollService.updateenforcesRESOURCE_HAS_VOTESlock on destructive question edits.- New
voteCountfield threaded through admin response builders.QuestionDtoexposes it; backoffice editor disables structural controls whenvoteCount > 0. - H2 tally uses in-process array fanout (jOOQ
unnest(parameter)not portable to H2).
Frontends
- Voter (
PollView.vue): radio whenmin=max=1, checkboxes otherwise; submit label flips betweenSkip(no selections) andSubmit answer; cap-disable on unchecked options atmaxSelections; hint text tracks arity. - Backoffice (
PollEditorPage.vue):Pick one/Pick manytoggle, min/max number inputs, structural-control lock keyed onvoteCount. - Slidev (
PollPanel.vue): renders multi-choice results, threads arity from snapshot, surfaces CORS-misconfig hint when stream stays paused. - Shared (
@slidev-polls/shared): types and SSE client updated;tally/tally-deltaconsumers removed. ResultsPanel:N voters · M selectionsfooter for multi-choice questions.
Tests
- New ITs:
QuestionArityRoundTripIT,VoteRepositoryImplArrayBallotIT,VoteSubmissionMultiChoiceIT,PollRepositoryImplArityRoundTripIT,PollServiceLockTest,QuestionLockIT,TallyBroadcasterSnapshotIT. - Frontend: arity, lock, abstain, multi-choice, CORS-hint test files added across voter/backoffice/slidev.
- E2E: multi-choice + abstain happy paths; deck question-switching covered for both signed-in presenter and anonymous viewer; CORS-clean assertions on every spec.
Quality
- Sonar critical findings (cognitive complexity in
PollService.enforceVoteLock, duplicated"snapshot"literal inTallyBroadcaster) addressed. - GitGuardian: test-fixture password (
correct-horse) allowlisted.
Migration
- DB: Flyway applies on boot. No app-level data backfill needed; the migration copies each
votes.option_idintooption_ids = ARRAY[option_id]. - API clients that POST single-choice votes must switch to
{ "optionIds": [...] }.
Full changelog
v0.3.0
Highlights
Voters can now retract their vote on an ACTIVE question ("Change my answer") and recast it before the presenter closes the slide. The whole flow is status-gated end-to-end — the backend refuses retract once the question is CLOSED, and the voter UI hides the retract affordance the moment a close event lands.
Features
- Retract vote endpoint:
DELETE /api/polls/{slug}/votes— status-gated, deletes the caller's vote on the active question and rebroadcasts the tally via SSE (VoteRetractedEvent). - Voter UI: "Change my answer" button on the voted state — clears the per-(slug, questionId) localStorage flag and returns the picker.
- Shared client:
ApiClient.retractVote(slug)in@slidev-polls/shared.
Fixes
- Vote disappears on slide back (this release's voter bug): PollView no longer blanket-clears the per-question
already-votedlocalStorage flags onquestion-closedor on the no-active-question branch of initial load. After the presenter navigated off a poll slide and back, the voter UI flipped from "Answer recorded" to the Submit form even though the server still held the vote — that's gone. - Retract race: backend re-checks
QuestionStatusafter the gatedDELETEso a presenter closing the question while a retract is in flight no longer produces a falseQUESTION_NOT_ACTIVE. - Integration test constraint:
VoteRepositoryImplDeleteByVoterIT.throws_question_not_active_when_question_closednow NULLsactivated_atalongside the CLOSED status flip, matching thepoll_questions_active_timestamp_ckconstraint and the production close path.
Chores
- Bumped
com.diffplug.spotless:spotless-maven-pluginto 3.5.1.
Artifacts
- Backend image:
ghcr.io/asm0dey/slidev-polls:v0.3.0(multi-arch —linux/amd64,linux/arm64). - npm:
@slidev-polls/shared@0.3.0,@slidev-polls/component@0.3.0.
v0.2.6
Fixes
- voter: disable voting on
question-closedSSE event. The PollView handler was a no-op — "keep snapshot frozen until next load() resets state" — but load() never re-ran in that path, so the form stayed instatus="active"with the option buttons + Submit enabled. Voters could keep clicking against a question the backend would reject withQUESTION_NOT_ACTIVE. The handler now flips back toWAITINGso the form disappears and the waiting placeholder renders. (d9bb16d) - slidev-component: keep deck panel rendering the final tally after
question-closed.PollPanelpreviously wiped its localactiveQuestion+ tally on close, switching to "Question closed. Waiting for the next one…". The deck embeds these panels so the presenter (and any reviewer of an exported PDF) can see the votes — as soon as the presenter advanced past the poll slide every panel went blank and the downloaded deck preserved zero results. Snapshot stays populated;closedNoticestill tracks the closed prompt for optional styling. (d9bb16d)
Full Changelog: v0.2.5...v0.2.6
v0.2.5
Fixes
- slidev-component: close the active question when the slide is navigated away from. Slidev keeps every slide mounted and switches the active one by toggling
style="display: none"on the previous slide's.slidev-page.IntersectionObserverdoes not reliably deliver an entry for an element that becomesdisplay:none(the spec leaves it implementation-defined and Chromium skips it), so a slide losing focus never produced an IO-leave event — the panel never posted/closeand voters kept seeing the old poll prompt after the presenter moved on.PollPanelnow watches the slide-page's style attribute with aMutationObserverand drives activate/close off the display transitions;IntersectionObserveris retained as a fallback for non-Slidev embeds. (7033c6a) - slidev-component: detect image backgrounds on the inner
.slidev-layout. Slidev applies the frontmatterbackground:as an inline style on.slidev-layout(child of.slidev-page), not on the page itself.useSlidevTheme.hasImageBackgroundonly looked at the page, so image-bg slides registered as plain and the panel rendered opaque against the photo — thescrim-dark/scrim-lightmode never engaged. The detector now also checks the inner layout (and watches itsstyleattribute for live bg swaps). (60aac45)
CI / release
- ci(docker): multi-arch backend image via matrix + always tag
latest.publish-backendfans out across native runners (ubuntu-latestforlinux/amd64,ubuntu-24.04-armforlinux/arm64) and pushes each arch by digest only; a follow-uppublish-backend-manifestjob stitches the digests into a multi-arch manifest list under all tags. Every published image now also gets:latestin addition to the version-specific tags so consumers can pin to the current release without rewriting their compose file each cut. (66ef9f2)
Full Changelog: v0.2.4...v0.2.5