Skip to content

Releases: asm0dey/slidev-polls

v0.6.0

05 Jun 12:29
dd58ca6

Choose a tag to compare

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's pollServer (frontmatter/headmatter, origin fallback) + the slug prop. Self-bounding (max-width: min(360px, 60vmin), centered) so it fits any slide bare, including a layout: two-cols column.
  • Shared QR config (buildQrOptions) and voter-URL resolver (useVoterUrl) extracted; the existing presenter PollQrButton overlay 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

05 Jun 07:04
3269645

Choose a tag to compare

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

23 May 15:44

Choose a tag to compare

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_SESSION cookie (an old Tomcat in-memory session id from the pre-session app) was base64-decoded into bytes containing a 0x00, and that NUL was passed into the Postgres WHERE SESSION_ID = ? lookup — throwing invalid byte sequence for encoding "UTF8": 0x00 and returning HTTP 500 on every request that carried such a cookie.

    CachingSessionRepository now 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 /activate and 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

23 May 13:37
ea67583

Choose a tag to compare

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/account exposes 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(), Testcontainers DockerImageName, 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 V12V15, H2 V4V7 (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

18 May 08:59

Choose a tag to compare

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

  • ResultsPanel denominator switches to voterCount when maxSelections > 1. Single-choice keeps the historical count / total path (the two are equivalent there). Each voter contributes at most 1 to any option's count, so count / voterCount ≤ 1 by construction.
  • Scoped the leader colour override to .sp-rp__label only. 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-fg and reads against --sp-bg-subtle outside the bar.

Tests

  • New ResultsPanel.test.ts case reproduces the exact 2-voter / 6-selection scenario and asserts SAP Machine renders 100% (not 33%), singletons 50%, unpicked 0%.

Full changelog

v0.4.1...v0.4.2

v0.4.1 — fix slide-switch deadlock storm

18 May 07:55

Choose a tag to compare

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.activateQuestion and closeActiveQuestion now wrap the state-flipping UPDATE in a dsl.transactionResult and take SELECT polls … FOR UPDATE on 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 its UPDATE 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-commit dsl would have read pre-commit state on a different connection).

Tests

  • New PollActivateDeadlockIT: 16 threads × 25 iterations of interleaved activate(qN) / closeActiveQuestion / updateHeader on 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...v0.4.1

v0.4.0 — per-question multi-choice ballots

18 May 08:59
32b3876

Choose a tag to compare

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.type and votes.option_id dropped. H2 baseline mirrored.
  • New endpoint behaviour on POST /api/polls/{slug}/votes: body is { "optionIds": ["..."] }; legacy { "optionId": "..." } rejected with VALIDATION_FAILED.
  • VoteService validates ballot size against arity, rejects duplicates and unknown options, accepts empty ballots when min=0.
  • PollService.update enforces RESOURCE_HAS_VOTES lock on destructive question edits.
  • New voteCount field threaded through admin response builders. QuestionDto exposes it; backoffice editor disables structural controls when voteCount > 0.
  • H2 tally uses in-process array fanout (jOOQ unnest(parameter) not portable to H2).

Frontends

  • Voter (PollView.vue): radio when min=max=1, checkboxes otherwise; submit label flips between Skip (no selections) and Submit answer; cap-disable on unchecked options at maxSelections; hint text tracks arity.
  • Backoffice (PollEditorPage.vue): Pick one / Pick many toggle, min/max number inputs, structural-control lock keyed on voteCount.
  • 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-delta consumers removed.
  • ResultsPanel: N voters · M selections footer 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 in TallyBroadcaster) 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_id into option_ids = ARRAY[option_id].
  • API clients that POST single-choice votes must switch to { "optionIds": [...] }.

Full changelog

v0.3.0...v0.4.0

v0.3.0

16 May 12:46

Choose a tag to compare

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-voted localStorage flags on question-closed or 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 QuestionStatus after the gated DELETE so a presenter closing the question while a retract is in flight no longer produces a false QUESTION_NOT_ACTIVE.
  • Integration test constraint: VoteRepositoryImplDeleteByVoterIT.throws_question_not_active_when_question_closed now NULLs activated_at alongside the CLOSED status flip, matching the poll_questions_active_timestamp_ck constraint and the production close path.

Chores

  • Bumped com.diffplug.spotless:spotless-maven-plugin to 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

15 May 22:03

Choose a tag to compare

Fixes

  • voter: disable voting on question-closed SSE 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 in status="active" with the option buttons + Submit enabled. Voters could keep clicking against a question the backend would reject with QUESTION_NOT_ACTIVE. The handler now flips back to WAITING so the form disappears and the waiting placeholder renders. (d9bb16d)
  • slidev-component: keep deck panel rendering the final tally after question-closed. PollPanel previously wiped its local activeQuestion + 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; closedNotice still tracks the closed prompt for optional styling. (d9bb16d)

Full Changelog: v0.2.5...v0.2.6

v0.2.5

15 May 21:27

Choose a tag to compare

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. IntersectionObserver does not reliably deliver an entry for an element that becomes display: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 /close and voters kept seeing the old poll prompt after the presenter moved on. PollPanel now watches the slide-page's style attribute with a MutationObserver and drives activate/close off the display transitions; IntersectionObserver is retained as a fallback for non-Slidev embeds. (7033c6a)
  • slidev-component: detect image backgrounds on the inner .slidev-layout. Slidev applies the frontmatter background: as an inline style on .slidev-layout (child of .slidev-page), not on the page itself. useSlidevTheme.hasImageBackground only looked at the page, so image-bg slides registered as plain and the panel rendered opaque against the photo — the scrim-dark / scrim-light mode never engaged. The detector now also checks the inner layout (and watches its style attribute for live bg swaps). (60aac45)

CI / release

  • ci(docker): multi-arch backend image via matrix + always tag latest. publish-backend fans out across native runners (ubuntu-latest for linux/amd64, ubuntu-24.04-arm for linux/arm64) and pushes each arch by digest only; a follow-up publish-backend-manifest job stitches the digests into a multi-arch manifest list under all tags. Every published image now also gets :latest in 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