Ready status previously expired 60 seconds after a player checked the
box, evaluated per-player against their own timestamp. Unless every
player happened to click ready within the same 60-second window, the
earliest player's readiness would silently expire before the last one
joined, so the session could get stuck on "Waiting for all players to
be ready" indefinitely, especially after a reload re-triggered the
timeout check.
Ready state is now durable: once checked, it stays until the player
unchecks it or the whole group is simultaneously ready, at which point
the session always transitions to PLAYING regardless of how long that
took. session.timer is still only ever set once during that one-way
READY -> PLAYING transition, so a reload never restarts or desyncs the
countdown between players.
Removing all 3 locked files while the timer is still running now marks
the session WON immediately (checked right after every successful rm)
and broadcasts a "game_finished" signal over Mercure so every
connected player gets redirected together, not just the one who
removed the last file. A new /won/{session} route + won.html.twig
mirrors the existing lost flow (victory narrative + the same feedback
form).
The existing timer-expiry path already set LOST but always redirected
to lostUrl regardless of actual status; it now picks won/lost based on
the status the server reports.
Also fixes a pre-existing bug on the lost page (and would-be bug on
the new won page): PlayerService::GetCurrentlyActiveAsPlayer() only
matches players in READY/PLAYING sessions, so by the time a session
has ended it always returned null there, silently breaking the
feedback form. Both pages now look the player up directly via
PlayerRepository instead.
Added a navigatingAway flag so the page's "confirm before leaving"
prompt doesn't block our own win/lose redirects.
Previously the restore check only ran lazily on a player's next
message, so files could stay wrongly-removed for an arbitrary amount
of time after the window expired. The frontend now schedules a
setTimeout (using the server-provided deadline, mirroring the terminal
lock's reveal timer) that pings the backend right at the deadline so
the check runs promptly regardless of player activity. Restored via
data-files-removal-deadline on page load too, so a refresh mid-window
doesn't lose the timer.
Connects the previously-disconnected /decode command to per-player
messages: player 1 unlocks sudo for everyone, player 2 unlocks a scan
command that reveals which files the AI virus has locked, player 3
learns those files should be removed.
Also adds a retaliation mechanic: successfully removing a file locks
the player's terminal. The AI virus locks it out in red immediately;
the mainframe reveals a 12-character recovery code in green after 30
seconds, which can be submitted via /unlock to restore access early,
otherwise the terminal auto-recovers after 45 seconds.
Overlay a rotated CSS badge on the homepage hero and briefing page
image instead of baking it into the PNG, so it's easy to remove once
the game ships.
Give the public homepage a themed breach-alert hero and add a /briefing
page with the full mission narrative, both linking into the existing
game dashboard flow.
Adds a Marketing column to the admin users table and a total opt-in
count in the header, so admins can see who signed up for updates on
future projects.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Registration now requires agreeing to the Terms and Conditions and lets
users opt in to hear about future projects. The opt-in is persisted on
the user via a new marketing_opt_in column (migration included).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Admins now see an Admin link in the main navigation, and the admin
section inherits the branded header/footer from layout/site.html.twig
instead of the bare base layout.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
When the last player clicked ready, toggleReady() published a
redundant 'player_ready' event on top of checkAllPlayersReady()'s
'all_ready', and the client reloaded on every message with no guard
and without closing the EventSource. Chrome kept the old page's
script (and its EventSource) alive across the overlapping reload
calls, causing repeated reconnects to the Mercure hub; Firefox
apparently tore the page down fast enough to mask it. Skip the
redundant publish server-side, and make the client reload idempotent
by tracking whether it already fired and closing the EventSource
before reloading.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
confirmation_email.html.twig and reset_password/email.html.twig were
plain unstyled HTML with no shared structure. Extract a table-based
layout (templates/emails/layout.html.twig) with header/logo, content
block, and footer, so future transactional emails can extend it
instead of starting from scratch.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>