Commit Graph
184 Commits
Author SHA1 Message Date
FrankandClaude Sonnet 5 779ddcbccb Fix 3 pre-existing failing tests
- testToggleReady: user was never marked verified, so toggleReady()
  returned false before doing anything (an isVerified() gate was
  added to the service after this test was written).
- testCheckAllPlayersReadyTransitionsStatus: entityManager mock
  returned the same SessionSettingRepository for every getRepository()
  call, but the service now also fetches a GameSettingRepository for
  the session's total-time setting, causing a TypeError. Route the
  mock by requested class instead.
- testChatRegeneratesVerifyCodesIfShared: two competing
  method('getSetting') stubs were registered without with()
  constraints; PHPUnit keeps the first one it sees active for every
  call, so the (correct, more complete) willReturnCallback stub was
  silently dead code and the regeneration path never actually ran.
  Dropped the redundant first stub, and updated the flush() count now
  that the real flow (chat tracking + code regeneration) executes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-22 17:18:36 +02:00
FrankandClaude Sonnet 5 6b1436dfe7 Randomize decoy usernames and which rapports carry coded messages
Decoy names in /var/home and verifyCodes.txt were a hardcoded 4-name
list (Luke, Charles, William, Peter), so a real player registering
under one of those names would collide with it. Now each session
picks 6 decoys from a pool of 10, excluding any name already taken by
a real player, and stores the pick as a session setting.

Likewise the 3 "special report" rapports that get coded messages were
always Doyle, Vega and Lennox. Each session now randomly assigns 3 of
the 20 rapports to that role, and the win screen / mainframe-help
message reference whichever agents were actually picked instead of
the hardcoded names.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-22 17:09:52 +02:00
FrankandClaude Sonnet 5 02780d8f04 Add a favicon
Crops the key/globe mark out of the existing logo and wires it into
base.html.twig as favicon.ico plus PNG/apple-touch-icon variants.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-22 17:09:44 +02:00
Frank 9a52e6b32c Turn the tutorial link into a real button with clearer copy
Replaces the inline "New here?" link with a proper button and a heading
that more directly asks whether the player is unfamiliar with running
commands on a command line.
2026-08-18 22:42:45 +02:00
Frank 7c14ab3331 Add a back button to the tutorial that docks into the input row on completion
Lets players leave the tutorial at any time via a top-left link back to the
game session. Once the tutorial finishes and the input field is disabled,
the button relocates into the input row so it's the obvious next action.
2026-08-18 22:35:52 +02:00
Frank 6a04a0274a Add command-line tutorial (PreGameController) and link it from the lobby
Adds a black-terminal tutorial page at /game/pregame/{session} that walks
players through help, pwd, cd, ls, cat, rm and sudo before the real game
starts. Links to it are shown on both the lobby (waiting for players) and
ready-up screens so players can practice, repeatedly, before starting.
2026-08-18 22:23:26 +02:00
Frank 367ded6441 Only regenerate verify codes on broadcast, not any targeted /chat
Any /chat {agent-id} {code} was previously exempted only if aimed at
the code's rightful owner; sending to the wrong player still burned
it. Now only an untargeted, open-chat broadcast counts as a leak -
mistargeting via /chat is harmless.
2026-08-18 21:49:45 +02:00
Frank 1f35784c52 Don't regenerate a verify code when sent privately to its rightful owner
checkAndRegenerateVerifyCodes() regenerated a code on any message
containing it, regardless of who the message was sent to - so sharing
a code exactly as intended, via /chat {agent-id} {code} to the
correct recipient, still burned it immediately, making the puzzle
unsolvable. Now only broadcasts and messages sent to the wrong player
count as a leak.
2026-08-18 21:47:21 +02:00
Frank b0b357f99b Strip spaces from generated decode report codes
generateSpecialCode() reused generateRandomString(), whose charset
includes a space. Across a 75-100 character code that made a space
almost certain, and /decode splits its argument on spaces, silently
truncating the code the player typed back in - breaking decoding.

Fixed at generation time rather than at display time, since the
stored value itself was the problem, not just how it's shown.
generateRandomString() is left untouched for its other use (the
"garbage" filler text shown to players who fail to decode), where
spaces are harmless.
2026-08-18 21:32:06 +02:00
FrankandClaude Sonnet 5 a00899e444 Route every Mercure publish through an event, log pushes per-player
Every $hub->publish() call site (chat, hints, security alerts, virus
alerts, game_finished, and the pre-game lobby events) now dispatches
a PushMercureMessageEvent instead of publishing directly. Two
listeners handle it: PublishMercureMessageListener does the actual
Mercure publish exactly as before (same wire format, no client
changes needed), and LogMercureMessageListener appends it to the
activity log of whichever player(s) it was actually delivered to.

A private /chat to one agent only gets logged for that agent, never
broadcast into everyone's transcript - the event carries an explicit
targetScreen (null = everyone) rather than leaving listeners to guess
from the payload shape.

The per-player log format moved from flat text to JSON Lines (one
timestamped, structured entry per line) via a new shared
SessionActivityLogger service, since a flat string can't carry an
event's type or exact payload - both needed for a future feature to
replay a player's full session history, not just their own commands,
on page reload. The admin session log viewer now parses and
formats these entries back into readable lines.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 19:47:53 +02:00
FrankandClaude Sonnet 5 aba79251e0 Broadcast game_finished when a player's own timeout check catches it
Previously only the cron's timeout sweep broadcast to everyone;
check-finished (called by a player's own client the moment their
local countdown hits zero) just updated the database silently. Now
whichever path notices the timeout first broadcasts - both only act
while the session is still PLAYING, so there's no double broadcast.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 17:56:24 +02:00
FrankandClaude Sonnet 5 2951a39204 Add mainframe hint system and cron-driven timeout handling
The cron (app:hints:check, every minute) now walks each playing
player through a strict dependency chain - help, communication,
verification, navigation, rapports/decode, endgame urgency - and
sends at most one hint per player per run: the first step that's
both unresolved and old enough to nudge about. Later steps genuinely
depend on earlier ones (e.g. /verify isn't usable until a player has
finished contacting everyone), so a player never gets a hint for
something they can't act on yet.

Two bits of new tracking were needed:
- Help command usage wasn't recorded anywhere, so it's now saved to
  a new HelpUsedForPlayer{N} session setting.
- "All messages decoded" needed a reliable session-wide signal, so
  the rm right (previously granted alongside sudo when player 1
  decodes) now comes from player 3's decode instead, making
  sudo+scan+rm together mean "the whole team has decoded".

The cron also now finishes sessions whose countdown has run out:
sets the session to LOST and broadcasts the same game_finished
Mercure event the win path already uses, so every connected player
gets pushed to the lost page within a minute of the game actually
ending - not just whichever player's own client-side timer happens
to notice first.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 17:56:16 +02:00
FrankandClaude Sonnet 5 2bda70b32b Fix missing player number in mainframe welcome message
Welcome agent ' +  + ' to the mainframe.' had a stray += with nothing
between them, evaluating to NaN. Plugs in the screen variable that
was already sitting in scope right above it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 17:56:04 +02:00
Frank ebd2f68df6 Sync PhpStorm project index with updated vendor packages
Reflects the sendgrid-mailer removal and mailgun-mailer/rate-limiter/
polyfill-php85 additions from the recent composer update.
2026-08-16 16:04:46 +02:00
FrankandClaude Sonnet 5 2c8e568255 Fix sessions never persisting as PLAYING, and dangerous-mode logrotate configs
checkAllPlayersReady() set the session to PLAYING and cleared/updated its
timer in memory but never flushed, relying on callers to do so. The
toggleReady() caller flushed right after, but GameController::index()'s
lazy catch-up call did not - so if the "everyone ready" transition was
only detected on a page load (e.g. players didn't click ready within the
same 60s window), the terminal would render for that one request from the
in-memory state, letting the game be played entirely through the
unguarded message API, while the database silently kept the session on
'ready' with timer 0 forever. This made sessions invisible to the mainframe
hint cron and the admin "running sessions" count. Moved the flush inside
checkAllPlayersReady() itself so both callers persist reliably.

Also chmod the logrotate configs after COPY in the Dockerfile, since
their on-disk mode ended up group-writable (0664) depending on the
build host's umask, which made logrotate refuse to use them.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 00:14:00 +02:00
FrankandClaude Sonnet 5 ba45e06972 Remove |raw from login error message rendering
Not exploitable today - the only custom auth message data is a
hardcoded resend link - but |raw on a translated exception message is
a latent XSS pattern if a future change ever threads user input
through the auth exception's message data. Twig's default
autoescaping is sufficient here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-10 21:41:53 +02:00
FrankandClaude Sonnet 5 2913b8d2a2 Add CSRF protection, login throttling, and invite-code rate limiting
The admin panel already checked CSRF tokens on destructive actions,
but the player-facing raw HTML forms (create/join/leave/start
session, toggle ready, lobby chat, feedback) had none - cookie
SameSite=Lax blunts classic cross-site auto-submit attacks but isn't
a substitute for real tokens. Adds matching csrf_token()/
isCsrfTokenValid() checks to all of them.

Also adds login_throttling (5 attempts/15 min) to stop unlimited
password guessing, and a per-user rate limiter (10/min) on the
invite-code join endpoint, since invite codes are only 32-bit and
had no protection against brute-forcing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-10 21:41:47 +02:00
FrankandClaude Sonnet 5 335697e520 Update dependencies to patch known CVEs
composer audit reported 37 advisories across 15 packages, including
high-severity ones in symfony/security-http. Ran composer update
within the existing 7.4.* constraints - composer audit now reports
zero advisories. Also adds symfony/rate-limiter, needed for login
throttling and invite-code rate limiting in the next commit.

Flex removed a stale, non-functional sendgrid notifier config left
over from before the app switched to Mailgun as part of the sync.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-10 21:41:36 +02:00
FrankandClaude Sonnet 5 daa37390d0 Fix path traversal via username in session log file paths
Registration only validated username with NotBlank, so a username
like "../../../../public/x" got concatenated directly into a
filesystem path for both writing (game activity logs) and reading
(admin log viewer) - reachable from the public webroot since public/
is a few directories up from where those logs are stored.

Adds a character-set validator (letters, numbers, underscore, hyphen)
to registration and admin user editing going forward, and sanitizes
at the point of use (Player::getLogFileBasename()) so any
already-stored unsafe username can't escape the log directory either.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-10 21:41:25 +02:00
FrankandClaude Sonnet 5 a9cb0fa57f Turn admin lobby chat panel into a tab
The lobby chat was a standalone card sitting above the per-player log
tabs. Folds it into the same tab bar as the first (default-active)
tab instead, so the session log view has one consistent tab strip.
The tab-switching script now toggles by an .admin-tab-panel class
instead of assuming every panel's id starts with "player-", since
that's no longer true.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-10 16:48:06 +02:00
FrankandClaude Sonnet 5 2bfc2aca1a Keep pregame lobby chat open for an hour after the game ends
The chat used to disappear the moment a session left CREATED status.
Sessions now record a finishedAt timestamp when they're won or lost,
and the lobby (with chat) stays reachable via /game/{session} for an
hour afterward instead of immediately redirecting to the win/lose
feedback page. The lobby template shows a distinct "game finished"
header with a link to that feedback page during this window.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-10 16:47:57 +02:00
FrankandClaude Sonnet 5 846cfbc44a Remove dead admin/session.html.twig template
Unreferenced by any controller - superseded by
templates/game/admin/sessions/view.html.twig, which is what
GameAdminController::viewSession() actually renders. Confirmed via
grep across src/ and templates/, plus a clean lint:twig and phpunit
run after removal.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-10 15:32:53 +02:00
FrankandClaude Sonnet 5 207346d571 Move inline template scripts into webpack-built assets
Several templates had their own <script> blocks instead of going
through webpack like game1.js already does. Extracted the waiting
page, lobby page, and admin session-log tab scripts into their own
files under assets/, imported from the main app.js entry. Since
app.js is already loaded on every page, each module just reads its
own data-* attributes and no-ops if its target element isn't present
on the current page - same pattern game1.js already uses.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-10 15:32:44 +02:00
FrankandClaude Sonnet 5 dfa75719d0 Show pregame lobby chat in admin session logs
Admins can now see the full lobby chat transcript (who said what,
when) at the top of a session's log view, alongside the existing
per-player terminal logs. Also swaps the log tabs' inline onclick
handler for a data attribute, in prep for moving the tab-switching
script out of the template.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-10 15:32:31 +02:00
FrankandClaude Sonnet 5 444f54b6c6 Add pregame lobby with live chat
Players used to get bounced back to the dashboard when a session
wasn't full yet. Now they land on a lobby page showing who has
joined, and can chat with each other while waiting - messages are
broadcast live over the existing Mercure hub, and the session
auto-starts (and the lobby notifies everyone) once it fills up.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-10 15:32:23 +02:00
Frank 98cf48b29d Make admin panel usable on mobile
The sidebar was a fixed 220px flex column that left barely any room
for content on a phone, and tables had no min-width so columns just
squeezed into unreadable slivers instead of scrolling.

Moves the sidebar's layout rules out of inline styles (which media
queries can't override) into real CSS classes, and collapses it into
a horizontally-scrollable pill bar below 768px so the content area
gets the full screen width. Tables now get a sensible min-width so
they scroll horizontally on narrow screens instead of squishing, and
the per-player tab bar on the session log view scrolls too.
2026-08-10 14:58:12 +02:00
Frank f6df9f7ba6 Show friendly session status labels on the player dashboard
Players saw the raw enum value (e.g. "created") in the sessions
table, which doesn't mean anything to them. Adds SessionStatus::label()
mapping each status to a player-facing description like "Waiting for
players".
2026-08-10 14:58:01 +02:00
FrankandClaude Sonnet 5 fc93486367 Restore executable bit on docker/restart.sh and docker/setup.sh
Lost accidentally in the previous commit; both scripts are invoked
directly (./docker/setup.sh) rather than via bash, so they need +x.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 23:35:50 +02:00
FrankandClaude Sonnet 5 98066118a6 Expand terminal filesystem, add mainframe hint cron, redirect PHP logs
- Game1 terminal: flesh out the virtual filesystem with a realistic
  spread of Linux directories/files (~70 dirs, ~140 files) so it no
  longer reads as an obviously small puzzle set, without touching any
  win-condition or rapport files.
- Add app:hints:check command + a php-cron container (BusyBox crond)
  that nudges players who haven't contacted every teammate 5 minutes
  into a session, via a new 'hint' Mercure message type.
- Log the cron command's output to var/log/cron/cron.log and rotate
  it (25MB / 90 days) via logrotate, run daily from the same crontab.
- Redirect PHP's error_log and Symfony's prod app/deprecation logs
  from stderr-only into var/log/php/*.log (kept alongside stderr),
  with the same rotation policy.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 23:34:36 +02:00
Frank 1be07440e3 Bring back 1-minute ready-status expiry with live per-player broadcasts
A player's own browser now starts a 60s timer the moment they check
"ready" (with a visible countdown) and proactively tells the server
when it's up via a new expire_ready action - idempotent, so it only
actually clears the status if the deadline has genuinely passed.
Everyone else finds out live through a Mercure player_ready broadcast
that now carries which player and their new ready/not-ready state
(previously the broadcast payload's `ready` flag was always false due
to a stale-variable bug, though nothing consumed it yet).

checkAllPlayersReady() still does the same expiry check server-side
and broadcasts on anyone else's request in the meantime, so a stalled
frontend timer doesn't leave a stale "ready" badge showing forever.

This only affects the pre-PLAYING ready phase: checkAllPlayersReady()
still flips the session to PLAYING and stops touching ready state the
instant everyone is simultaneously ready, so the earlier fix (a reload
should never send an already-started game back to the waiting room)
is unaffected.

Also fixed the ready checkbox itself: unchecking it submitted a POST
without `toggle_ready` in the body (unchecked checkboxes aren't sent),
so un-readying silently did nothing server-side. Added the standard
hidden-fallback-input pattern to fix it.
2026-07-11 23:45:41 +02:00
Frank c28abef5b7 Block unverified users from marking themselves ready
GameDashboardService::toggleReady() now rejects the toggle if the
user's email isn't verified (mirrors the same gate UserChecker already
applies at login). The waiting-room checkbox is disabled client-side
with an explanatory alert and a link to resend the verification email,
and the controller adds a flash error as a server-side fallback if it
somehow gets submitted anyway.
2026-07-11 23:25:50 +02:00
Frank 886208c5c0 Add profile page for changing password and email
New /profile route (nav link between Admin and Logout) with two
independent forms, both requiring the current password before
applying a change:
- Change password: standard current/new/repeat flow, same password
  strength rules as registration.
- Change email: re-checks the new address isn't already taken (avoids
  a 500 from the unique constraint), then marks the account
  unverified and re-sends the confirmation email via the existing
  EmailVerifier/verify-email flow, same as registration. The current
  session stays logged in, but the user needs to verify the new
  address before their next login (UserChecker already blocks
  unverified accounts).
2026-07-11 23:23:38 +02:00
Frank 2941cfca49 Fix game route dumping players into the terminal for non-PLAYING sessions
GameController::index() only special-cased READY; every other status
(including a freshly-created session still waiting for players, and
even an already WON/LOST one) fell straight through to rendering the
live game terminal - which is broken there since screens/rights/pwd
are never initialized before startSession() flips CREATED -> READY.

The dashboard's "Enter Game" button links to this route regardless of
status, so any player clicking it on a CREATED session hit this
directly. Now CREATED redirects back to the dashboard with an
explanatory flash, and WON/LOST redirect to their respective pages
instead of re-rendering a dead terminal.
2026-07-11 23:13:30 +02:00
Frank 3c58e153dc Soft-delete users instead of hard-deleting, add last login tracking
Fixes the 500 on admin user deletion: deleting a user with an
email_log row (i.e. basically anyone who received any email) hit an
unhandled ForeignKeyConstraintViolationException, since email_log,
reset_password_request and player all have non-nullable FKs to user
with no cascade at the DB level.

Instead of cascading the delete (which would be fine for email_log
and reset_password_request but risky for player - removing a player
row could corrupt other real players' session state), admin delete
now sets a deletedAt timestamp instead of removing the row:
- UserChecker blocks login for deleted users (checkPreAuth).
- EmailLoggerListener rejects the message before send for deleted
  recipients (checked at actual send time, not at queue time).
- Added a last_login_at column + a LoginSuccessEvent listener to
  populate it, and surfaced both last login and status in the admin
  users list.

Added `app:users:purge-deleted`, a command intended to run on a
schedule that permanently removes users who were soft-deleted more
than 3 months ago and never played a game (join to Player via a
NOT EXISTS subquery). For that eventual hard-delete to actually
succeed, email_log and reset_password_request now cascade-delete at
the DB level (migration drops+recreates both FK constraints with ON
DELETE CASCADE) - player intentionally still isn't cascaded, so a
user with game history can never be purged this way even by mistake.
2026-07-11 23:09:52 +02:00
Frank 3a7bc3ed49 Package install 2026-07-11 20:27:32 +00:00
Frank aa667df889 Add /donation/success route and flash messages for both outcomes
Cancelled donations now show "Donation failed, but thank you for
trying anyway." and successful ones show "Thank you for the
donation!" on the dashboard after redirect.
2026-07-11 22:25:05 +02:00
Frank c06c45cb52 Add /donation/cancel route to fix 404 on cancelled PayPal donations
Redirects to the game dashboard instead of 404ing.
2026-07-11 22:19:51 +02:00
Frank 51679d9a6a Revert webpack-encore major bump from npm audit fix
The audit fix (presumably run with --force) jumped @symfony/webpack-encore
from ^4.6.1 to ^7.1.0, which requires @babel/core ^8.0.0 as a peer -
but @babel/core was left at ^7.25.0, so a clean `npm install` failed
outright with ERESOLVE. Since docker/setup.sh and docker/restart.sh
both run npm install unconditionally, this would have broken every
future deploy/rebuild.

Reverted webpack-encore (and the incidentally-downgraded
webpack-notifier) back to the versions that were actually working,
regenerated package-lock.json, and verified both `npm install` and
`npm run build` succeed cleanly.
2026-07-11 22:10:46 +02:00
Frank e8be7919ef npm audit fix 2026-07-11 20:05:59 +00:00
Frank cb2e945419 Remove 60-second ready-status expiry so the group can always start
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.
2026-07-11 21:48:32 +02:00
Frank 3984a33282 Add win/lose game-ending flow
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.
2026-07-11 21:41:16 +02:00
Frank eee6c3a369 Auto-trigger locked-files restoration exactly at the 60s deadline
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.
2026-07-11 17:45:08 +02:00
Frank e6ba469ef9 Restore locked files if not all removed within 60 seconds
Removing one of the 3 AI-virus-protected files now starts a 60-second
window (tracked session-wide via LockedFilesRemovalDeadline). If the
other locked files aren't also removed before it expires, the virus
restores whichever ones were deleted and broadcasts a red warning to
the whole session, forcing players to coordinate the removal instead
of picking them off one at a time.

Also extends the Mercure broadcast payload with an optional 3rd
"messageType" element so pushed messages can render red (virus) or
green (mainframe) instead of always defaulting to green.
2026-07-11 17:26:59 +02:00
Frank f6a0d62017 Make terminal output more authentic: smaller font, tighter line spacing
Message font dropped from 20px to 14px and the 10px inline margin-bottom
(set from JS on every message) replaced with a 2px CSS margin so the
terminal reads like dense console output instead of spaced-out chat
bubbles. Colors are unchanged.
2026-07-11 17:21:41 +02:00
Frank 6fd0b5d993 Grant rm right alongside sudo when player 1 decodes their message
rm was checked everywhere but never actually added to any player's
rights, so the command was unreachable until now.
2026-07-11 17:08:44 +02:00
Frank 6db9d42852 Wire up decode puzzle chain and lock terminal on file removal
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.
2026-07-11 16:48:15 +02:00
Frank 1e644eb13b Add "Looking for Help" page for the open visual artist request
Adds a /looking-for-help page listing the open volunteer request and
links it from the site footer on every page.
2026-07-04 23:23:04 +02:00
Frank 8554f04735 Add "In Development" watermark badge over AI virus artwork
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.
2026-07-04 23:02:46 +02:00
Frank 839113c356 Add AI Virus Deflection story to homepage and new agent briefing page
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.
2026-07-04 23:00:31 +02:00
FrankandClaude Sonnet 5 7dbb738da8 Show marketing opt-in status in admin users overview
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>
2026-07-04 20:48:48 +02:00