Neither /login nor the public season-code guess form (POST /) had
any throttling, making both an unlimited brute-force/enumeration
oracle. Adds symfony/rate-limiter and enables login_throttling on
the main firewall, plus a dedicated per-IP rate limiter on the
season-code form.
DataExportService uses ZipArchive directly and the Dutch-locale
format_datetime Twig filter needs real ext-intl (the polyfill only
supports en), but composer.json only declared ext-ctype/ext-iconv.
Adding them lets `composer check-platform-reqs` catch a missing
extension before a runtime crash.
* Migrate frontend to TypeScript with Deno-based tooling (#206)
Compiles assets/*.ts via sensiolabs/typescript-bundle (standalone SWC
binary) and adds Deno for formatting, linting, type-checking, and
tests, keeping the project's no-Node/npm approach intact. Wires all
four into CI, the Justfile, and the pre-commit hook.
* fix: build TypeScript assets before running PHPUnit in CI
The tests job ran bin/console sass:build but never typescript:build,
so var/typescript/ didn't exist and any page rendering the importmap
(e.g. backoffice/base.html.twig) errored during tests.
* test: add regression test for backoffice navbar-toggler dead target
Guards against the navbar-toggler button pointing at a collapse
target (#navbarSupportedContent) that isn't rendered for the current
user — the bug hit on the login page before #210 restructured the
nav to always render at least one item (the Releases link) regardless
of auth state.
* fix: stop hand-enumerating TS files for deno check
deno check assets/*.ts assets/controllers/*.ts assets/controllers/bo/*.ts
was duplicated in CI and the Justfile, and silently misses any new
controller subdirectory (fmt/lint/test already recurse assets/ via
deno.json). Add a top-level exclude for assets/vendor/ (respected by
all deno subcommands, unlike the per-task include/exclude blocks) so
deno check assets/ can recurse safely instead.
* fix: GitHubReleasesService date parsing and falsy release name
- Move the release-mapping array_map inside the try block so a
malformed published_at (or any other parse failure) degrades to the
same empty-list fallback as an HTTP failure, instead of throwing
uncaught out of the cache callback.
- Stop treating a release literally named "0" as unnamed — the old
`?:` fallback is falsy for that string and silently substituted the
tag name instead.
- Render release dates in UTC explicitly; Twig's date filter otherwise
silently converts to the app's default timezone (Europe/Amsterdam),
which could show the wrong calendar day for releases near midnight.
* docs: add scope-creep-as-a-service rule to CLAUDE.md
Per user instruction: when a review turns up a real bug outside the
current task's scope, fix it in the same MR (with a regression test)
rather than just reporting it, unless it needs a human design call.
* Add GitHub Releases modal to backoffice
Shows release notes (fetched from the GitHub API, cached for an hour,
Markdown rendered via league/commonmark) from a top-right nav button
available whether logged in or out, with the current version shown in
the modal header. Also clears cache.app on container start so the
release cache can't carry stale data across redeploys of the
var/ Docker volume.
* Render release notes markdown via twig/markdown-extra instead of manual CommonMark
twig/extra-bundle already wires the markdown filter to league/commonmark
automatically once twig/markdown-extra is installed, so the service no
longer needs to instantiate and call CommonMarkConverter itself.
* Explicitly disallow unsafe link protocols in rendered markdown
twig/extra-bundle's default (allow_unsafe_links: true) permits
javascript:/vbscript:/file:/data: URLs in rendered links, kept for
backwards compatibility. Disable it explicitly since we now render
release note markdown through this filter.
* Autolink bare URLs in rendered release notes
The Twig CommonMark converter only registers CommonMarkCoreExtension by
default, which doesn't turn bare URLs into clickable links (unlike
GitHub's own renderer). Register CommonMark's AutolinkExtension via the
twig.markdown.league_extension tag so PR/compare URLs in release notes
render as actual hyperlinks instead of plain text.
* Address review feedback on GitHubReleasesService and tests
- Extract the cache callback into a named fetchReleases() method instead
of an inline closure with all the fetch/parse logic.
- Simplify the User-Agent header to just "TijdVoorDeTest".
- Add a 5s HTTP timeout so a slow GitHub API can't block the request.
- Cache HTTP failures for only 60s instead of the full hour, so a brief
GitHub outage doesn't suppress releases for as long.
- Replace deprecated word-wrap with overflow-wrap in release-notes CSS.
- Extract duplicated mock setup in ReleasesControllerTest into a helper.
- Reword the "could not load" translation ("Kan" instead of "Kon").
* Address second round of CodeRabbit feedback
- Escape raw HTML embedded in release-note markdown instead of passing
it through, via twig_extra.commonmark.html_input: escape.
- Don't cache GitHub API failures at all (set $save = false in the
cache callback) instead of caching them for a short TTL, so the very
next request retries immediately.
- Sort releases by published_at descending before mapping, so the
first entry (used for the current-version badge and default-expanded
accordion item) is guaranteed to be the newest regardless of the
order GitHub's API happens to return.
* feat: add GDPR data export (download data) button
Wires up the previously disabled "Download data" button on the settings
page. Downloads a zip with a profile.xlsx (account + owned seasons), and
per owned season a folder with each quiz's xlsx (questions, results,
eliminations tabs) and a candidates.xlsx (candidates + season info tabs).
Soft-deleted rows are included and flagged so the export reflects
everything the app still holds about the user.
* feat: include question bank in GDPR data export
Adds a question-bank.xlsx per season folder with Questions (bank
questions, answers, reusable/complete flags, labels, and which quizzes
they've been used in) and Labels tabs, since BankQuestion/BankAnswer
content was previously missing from the export.
* fix: hard-delete quiz/audit-log data when deleting an account
QuizCandidate, GivenAnswer, and Elimination are Gedmo\SoftDeleteable, so
cascading their removal through Season -> Quiz/Candidate only set
deletedAt instead of physically deleting the row. Since Candidate and
Answer are hard-deleted via orphanRemoval, this broke their foreign keys
and rolled back the entire account deletion whenever a candidate had
actually participated in a quiz. Bulk DQL deletes now purge these rows
before the cascade runs.
Also purge BankQuestion audit-log rows (ext_log_entries), which store
the editor's username/email but aren't foreign-keyed to the entity they
log, so they were never cleaned up and would otherwise keep a deleted
user's email around indefinitely.
* style: make the download data button primary (blue)
* feat: add raw answers crosstab to quiz export
Adds a "Raw answers" tab to each quiz xlsx: one row per candidate, one
column per question, with the given answer text in each cell — the raw
data behind the aggregated Results tab.
* i18n: translate new settings page string to Dutch
* refactor: sanitize filenames with Symfony's AsciiSlugger instead of a hand-rolled regex
Extracts a shared Tvdt\Helpers\FilenameSanitizer (backed by
symfony/string's AsciiSlugger) and uses it everywhere user-controlled
text (season/quiz names, account email) ends up in a zip entry path or
a downloaded filename. AsciiSlugger is allowlist-based (only A-Z/0-9
survive; everything else, including unicode and path-traversal
sequences, is folded or stripped) rather than a denylist of "unsafe"
characters, and it's an officially maintained Symfony component already
present as a transitive dependency.
Also fixes BackofficeController::exportQuiz(), a pre-existing endpoint
that built its Content-Disposition filename directly from an
unsanitized quiz name — the same class of risk the data export already
guarded against.
Naming note: sanitized names are now slugs (spaces become dashes,
unicode is ASCII-transliterated), e.g. "Krtek Weekend" -> "Krtek-Weekend".
* fix: check ZipArchive open/close results and clean up temp files on failure
Addresses CodeRabbit findings on the GDPR export:
- ZipArchive::open() and close() can both return false without
throwing; neither was checked, so a failure silently produced an
empty or corrupt zip, and the temp zip path was never cleaned up on
a mid-build exception.
- writeToTempFile() leaked its tempnam()'d file if Writer\Xlsx::save()
threw before the caller could track it for cleanup.
* fix: drop public link identifier from the candidates export sheet
The nameHash is a public quiz-access token, not something a data
export should hand out — remove it from candidates.xlsx.
* feat: add Quiz info tab covering dropouts, finalization, and disabled questions
An entity-by-entity audit of the export vs. delete flows found the
delete flow fully covered, but three Quiz/Question fields missing from
the export: dropouts, finalizedAt, and Question.enabled. Adds a new
"Quiz info" tab (first sheet) to each quiz's xlsx with this data,
without touching the shared fillQuestionsSheet() used by the existing
single-quiz template export/import feature.
Deliberately left out per user decision: the BankQuestion audit log
(would leak other owners' emails, consistent with hiding co-owner
identities elsewhere in this export) and a few low-value timestamp
fields already covered by existing Started/time-taken columns.
* feat: require a confirmed email before exporting data
Antispam measure: both the full data export (SettingsController::downloadData)
and the single-quiz export (BackofficeController::exportQuiz) now redirect
with a flash warning instead of exporting when the account's email isn't
verified yet. Adds a matching hint on the settings page next to the
download button.
* feat: password reset via symfonycasts/reset-password-bundle (#179)
Implements the full password reset flow using the SymfonyCasts reset-password-bundle.
* ci: share base layer cache between dev and prod builds
* fix: use CSS form selector in tests instead of translated button text
* fix: translate missing validator string for reset password email field
* fix: translate reset password form labels via TranslatorInterface
* ci: override MAILER_DSN to null for PHPUnit so mailer host is not required
* feat: add question bank management, quiz finalization, and related backend/frontend functionality
* chore: add symfony/object-mapper dependency and fix Finalized translation
* feat: address PR review comments — unassign/sync bank questions, blank quiz creation, deactivate redirect, remove duplicate tab titles
- Use Symfony ObjectMapper for BankQuestion/BankAnswer → Question/Answer copy (#[Map(if: false)] on id, season, etc.)
- Track created Question on BankQuestionUsage (nullable FK, onDelete: SET NULL) for unassign/sync support
- Add unassign route: removes the Question copy + usage record
- Add sync route: pushes bank question edits to a finalized-not-started quiz copy
- Auto-sync non-finalized quiz copies on bank question edit; flash warning for finalized-not-started
- Add blank quiz creation (no XLSX required) with new route + template
- Deactivate quiz button now stays on the quiz overview page (redirect_quiz hidden field)
- Remove duplicate h4 titles below the tab bar on all season tabs
- Add migration for bank_question_usage.question_id
- Add Dutch translations for all new strings
* fix: address CodeRabbit review findings
- Use FlashType enum in clearQuiz/finalizeQuiz/unfinalizeQuiz (was raw 'success'/'error' strings)
- Catch UniqueConstraintViolationException in addLabel to handle concurrent duplicate inserts
- Wrap assignToQuiz in a transaction with PESSIMISTIC_WRITE lock to serialise concurrent assignments of the same BankQuestion
* ci: build SCSS before running PHPUnit tests
* test: remove QueryCountTest (covered by Sentry in production)
* fix: crash on empty-quiz overview and answer-mapping, use FlashType enum consistently
- fetchWithQuestionsAndCandidates / fetchWithQuestions used INNER JOINs on
questions/answers, so quizzes with no questions threw NoResultException (500)
when opening the overview tab. Switched to LEFT JOINs.
- answerMapping bare \assert() replaced with a proper flash + redirect when
the quiz has no questions, instead of crashing with AssertionError.
- Three raw 'success' flash strings in QuizController replaced with FlashType::Success.
- Added Dutch translation for "This quiz has no questions yet".
- Two new tests: empty-quiz overview loads (200), answer-mapping redirects with flash.
* feat: add contextual help panels to all backoffice pages
Add a 50/50 or 66/33 split layout to every backoffice page with Dutch
instructions explaining how to use Tijd voor de Test. Content covers the
overall workflow, quiz finalize/activate flow, and both candidate
participation methods (own device vs. shared laptop).
Help text lives in dedicated partials under templates/backoffice/help/ and
is loaded via Twig include(), keeping page templates clean. All strings use
a separate 'instructions' translation domain (instructions+intl-icu.nl.xliff)
isolated from the main messages domain.
Also updates 'Finalize'-related Dutch translations to use 'Afronden' and
adds tooltips to the finalize/unfinalize buttons.
* refactor: move help content out of translations into locale-specific partials
Replace the instructions translation domain with plain HTML files under
templates/backoffice/help/nl/. Each help/*.html.twig is now a locale
dispatch shim that tries the current locale first and falls back to nl,
so adding English (or any other language) is simply a matter of creating
a help/en/ directory with the translated files — no code changes needed.
Removes instructions+intl-icu.nl.xliff.
* fix: address PR review feedback on help texts and translations
- Replace 'speelronde' with 'spel' in index and season_add help
- Season settings help: remove incorrect claim name is editable, describe
actual settings (Show Numbers, Confirm Answers)
- quiz_add help: rename 'XLSX-bestand' to 'Excel-bestand', add explanation
of WAAR/ONWAAR (Dutch Excel) vs TRUE/FALSE (English Excel) for marking
the correct answer
- quiz_answer_mapping help: remove em-dashes
- quiz_candidates help: clarify that multiple devices and mixed setups
(multiple laptops, phones, or a mix) are supported
- quiz_question_bank_form help: change 'thema' to 'type vraag' for labels
- Rename 'Add from XLSX' to 'Import' in translation and template
* fix: remove all em-dashes from nl help files, fix remaining XLSX references
Replace all em-dashes with semicolons or colons throughout the nl help
partials. Also replace remaining 'XLSX-bestand' with 'Excel-bestand' in
season_tests and index, and also also fix the em-dash that was still on
the same line as the 'speelronde -> spel' fix in index.
* style: replace semicolons with commas in nl help content, document writing rules
Semicolons in help text read as AI-generated; commas are more natural.
Added writing style rules to CLAUDE.md to prevent recurrence.
* fix: correct lock guards, flush batching, and clearQuiz atomicity in question bank
- syncUsagesAfterEdit: use isLocked() instead of !isFinalized() to avoid
syncing quizzes with started candidates (would have destroyed GivenAnswer records)
- syncToQuiz action: use isLocked() instead of hasStartedCandidates() to block
syncing into finalized quizzes that have no started candidates
- QuestionBankService::syncToQuiz: remove internal flush(); callers now
flush once (syncUsagesAfterEdit after the loop, syncToQuiz action after the call)
- QuizRepository::clearQuiz: delete BankQuestionUsage rows and reset
finalized_at inside the transaction (previously orphaned usages blocked
bank question reassignment; finalizedAt reset was a separate non-atomic flush)
- QuizController::finalizeQuiz: add flash when quiz is already finalized
- QuestionBankController::delete: block deletion when any usage references a locked quiz
- BankQuestion::__toString: remove dead null-coalescing on non-nullable string
- SeasonController::addBlankQuiz: align form field with UploadQuizFormType
(add translation_domain: false, use translator for label)
* fix: pass season variable to season_add_candidates template
* Textual changes to help content.
* Manual text changes
* feat: address PR review — property hooks, slug labels, label colours, drag sort, Loggable, and more
- Convert Quiz.isFinalized/isLocked/hasStartedCandidates and BankQuestion.isUsed/canBeAssigned to PHP 8.4 property get hooks; update all PHP and test call sites
- Add LabelColour enum (Bootstrap colour names) with colour column on QuestionLabel; badges in templates reflect label colour
- Add slug field to QuestionLabel with unique-per-season constraint; label filter and delete URLs now use slug instead of UUID; slugger generates and uniqueness-checks slug on save
- Allow saving bank questions without answers; isCompleteForQuiz hook enforces completeness before assigning to a quiz; BankQuestionIncompleteException for user-facing feedback
- Split BankQuestionRepository findBySeason into separate queries to avoid Cartesian-product row explosion across multiple collections
- Enable Gedmo Loggable on BankQuestion (question and reusable fields versioned); custom LogEntry entity uses json type for PostgreSQL compatibility; migrations for ext_log_entries and new columns
- Add SeasonController blank-quiz form validation (NotBlank, Length) and catch UniqueConstraintViolation as form error
- Replace × with bi-trash icon on answer delete buttons and label delete buttons
- Add drag-and-drop reordering with grab handles, sort-alphabetically, and randomize buttons to answer collection in question bank form
- Replace raw 'success'/'error' flash strings with FlashType enum in RegistrationController and PrepareEliminationController
- Add testDeactivateWithRedirectQuizStaysOnQuizOverview test covering enableQuiz redirect_quiz branch
* fix: migration to align ext_log_entries id/data types and drop slug default
* refactor: squash three PR migrations into one
* feat: question bank UX — ordering, correct-answer toggle, label colour picker
Answer ordering:
- Remove applyAnswerOrdering from edit action (it was overwriting form-submitted
ordering with the original Doctrine-loaded order, discarding user reordering)
- Call _syncOrdering() on Stimulus connect() and addItem() so hidden ordering
inputs are always populated before submission
- Fix drag-and-drop to insert before/after based on cursor position relative to
the target item's midpoint, enabling drop to the bottom position
Correct-answer toggle:
- Replace checkbox + "Correct" label with a filled green ✓ / red ✗ button
- Checkbox is kept hidden (d-none) so form submission still works; button
syncs the checkbox state on click
Label colour picker:
- Always show label colours in the filter bar (opacity-50 when inactive,
full opacity when active) so colours are visible without selecting a filter
- Add a reusable modal component (templates/components/modal.html.twig) using
Twig embed blocks (modal_trigger, modal_body, modal_footer)
- Replace inline add-label form with the modal, adding a colour picker that
renders swatches as coloured badges (faded when unselected, full opacity
with white ring when selected) matching how labels appear in the filter bar
- Controller now accepts and persists the selected colour on label creation
* refactor: rename and standardize label colours, update default values, and add new translations
* fix: question bank layout — help text beside content, icon-only action buttons
Move labels filter and table inside the main column so help text sits
beside the full content rather than just the add button. Convert Edit,
Delete, and Assign buttons to icon-only btn-group to save row space.
* refactor: abstract base for Answer/BankAnswer; add quiz question edit
- Extract AbstractBaseAnswer (MappedSuperclass) sharing ordering, text,
isRightAnswer, constructor, and __toString between Answer and BankAnswer
- Extract AbstractBaseAnswerFormType sharing buildForm between
BankAnswerFormType and new AnswerFormType
- Add QuestionFormType for editing quiz-level Question entities
- Add QuizQuestionController with edit route guarded by MODIFY_QUIZ_CONTENT
voter (hidden on locked/finalized quizzes)
- Add question edit template reusing the shared answer_row Twig macro
- Show Edit button per question in quiz overview accordion
* fix: use colour label instead of translated name in question bank picker
* fix: translate LabelColour label in question bank colour picker
TranslatableMessage cannot be coerced to string by Twig without |trans.
* Update composer.lock with dependency upgrades and improvements
Updated multiple dependencies in `composer.lock` to their latest versions, including upgrades for Doctrine, PHPUnit, Symfony components, and extended PostgreSQL support.
* Fix sass compile
CI / Build and deploy to ${{ startsWith(github.ref, 'refs/tags/') && 'production' || (github.ref == 'refs/heads/main' && 'acceptance' || '') }} (push) Has been skipped
CI / Build and deploy to ${{ startsWith(github.ref, 'refs/tags/') && 'production' || (github.ref == 'refs/heads/main' && 'acceptance' || '') }} (push) Has been skipped
CI / Build and deploy to ${{ startsWith(github.ref, 'refs/tags/') && 'production' || (github.ref == 'refs/heads/main' && 'acceptance' || '') }} (push) Has been skipped
* Some tests
* More tests!
* Tests 3
* Move getScores from Candidate to Quiz
* Add some suggestions for future refactoring
* - **Add Gedmo doctrine-extensions and Stof bundle integration**
- Added `stof/doctrine-extensions-bundle` and `gedmo/doctrine-extensions` dependencies.
- Integrated `Timestampable` behavior for `Created` fields in entities.
- Updated `bundles.php` to register StofDoctrineExtensionsBundle.
- Added configuration for the Stof bundle.
- Simplified `SeasonVoter` with `match` expression and added new tests.
- Minor fixes and adjustments across various files.
* WIP
* All the tests
* Base64 tests
* Symfomny 7.4.0
* Update
* Update recipe
* PHP 8.5
* Rector changes
* More 8.5
* Things
CI / Build and deploy to ${{ startsWith(github.ref, 'refs/tags/') && 'production' || (github.ref == 'refs/heads/main' && 'acceptance' || '') }} (push) Has been skipped
- Replaced getters/setters with direct property access across entities and repositories.
- Added and configured `martin-georgiev/postgresql-for-doctrine` for PostgreSQL enhancements.
- Updated Doctrine configuration with types, mappings, and JSONB query functions.
- Removed unused `EliminationService` and related YAML configurations.
- Removed EasyAdmin controllers and configurations.
- Uninstalled `easycorp/easyadmin-bundle` and related dependencies.
- Cleaned up all associated routes, bundles, and vendor references.
This commit introduces a refactored EliminationFactory for better modularity, updates the elimination preparation process, and adds functionality to view eliminations. Backoffice templates and forms have been reorganized, minor translations were corrected, and additional assets like styles and flashes were included for enhanced user experience.