frontend/src/main.ts |
Bootstraps Angular and registers HTTP interceptors. |
frontend/proxy.conf.json |
Angular dev-server proxy: forwards /api and /uploads to http://localhost:3000. |
frontend/angular.json |
Angular workspace config; the serve target references proxy.conf.json. |
package.json (root) |
Root npm scripts; dev runs both workspaces via concurrently, dev:backend / dev:frontend run one at a time. |
setup.sh |
Idempotent setup: installs root + workspace deps, builds both packages, creates ./data/uploads. |
frontend/src/app/app.routes.ts |
Defines public, shell, child, and admin routes. |
frontend/src/app/core/services/bootstrap.service.ts |
Caches and exposes bootstrap state, refreshes it after admin updates, and applies the resolved theme. |
frontend/src/app/core/services/bootstrap.types.ts |
Defines bootstrap/theme payload types and maps theme tokens to CSS custom properties. |
frontend/src/app/core/services/admin.service.ts |
Calls admin settings, theme-list, category, and upload APIs. |
frontend/src/app/features/home/home.component.ts |
Authenticated shell container; starts user and event state services; on logout and on session invalidation also calls challengesStore.reset() to flush any per-user board cache before navigating to /login. |
frontend/src/app/core/services/auth.service.ts |
Signal-backed access token and current-user state; owns the cross-tab invalidation BroadcastChannel + storage-event fallback. |
frontend/src/app/core/services/auth-session-events.pure.ts |
Pure cross-tab message encoding/validation and namespaced constants used by the auth broadcast channel. |
frontend/src/app/core/services/authenticated-event-source.service.ts |
Fetch-based authenticated SSE transport with frame parsing, abort support, and a separate 'unauthorized' event for 401/403. |
frontend/src/app/core/services/bootstrap-event.service.ts |
Root-provided public SSE listener that subscribes to /api/v1/event/stream, filters topic === 'general' frames, and triggers BootstrapService.refresh() so the landing modal stays in sync with admin general-settings updates. |
frontend/src/app/core/services/bootstrap-event.pure.ts |
Pure helpers for the public bootstrap SSE listener: isBootstrapGeneralFrame predicate, SSE line parser, and the makeBootstrapEventSource factory (fetch + ReadableStream opener with frame buffering and idempotent close()). |
frontend/src/app/core/services/event-status.store.ts |
Event state signal store, one-second countdown timer, and the optional onUnauthorized callback wiring; exposes start()/stop()/closeTransport() for transport lifecycle and subscribeReloadAtCountdownZero(handler) (with reloadAtCountdownZero(handler) kept as a back-compat wrapper) so the reload handler is owned by its subscriber rather than the transport. |
frontend/src/app/core/services/event-status.pure.ts |
Event payload types, transport interface (including 'unauthorized' listener), pure countdown helpers (formatDdHhMm returns DD:HH:mm:ss and deriveCountdownText), and the LED_COLOR_BY_STATE map used by the shell LED. |
frontend/src/app/features/home/home.component.ts |
Authenticated shell container; starts user and event state services, subscribes to peer invalidation, resets both UserStore and ChallengesStore, and navigates to /login when the session is invalidated elsewhere. |
frontend/src/app/features/shell/header/shell-header.component.ts |
Shell title, status LED (size/shape only — color comes from the pure map via [style.background-color]), countdown, and user menu. |
frontend/src/app/features/shell/tabs/quick-tabs.component.ts |
Main shell navigation tabs. |
frontend/src/app/features/shell/change-password/change-password-modal.component.ts |
Change-password form modal. |
frontend/src/app/features/admin/general.component.ts |
AdminGeneralComponent reactive form for /admin/general — per-field inline error rendering (page-title + event-start + event-end), logo upload wiring, welcome Markdown preview, event-state derivation, and SSE general event handling. |
frontend/src/app/features/admin/categories/category-form-modal.component.ts |
Standalone OnPush modal for create + edit; owns the CategoryFormGroup, exposes the pure syncCategoryForm helper, binds [formGroup] on its template <form>, accepts errorMessage + saving parent inputs (renders cf-error and disables OK while saving), and reacts to open / mode / category signal inputs via a markForCheck effect so edit prefill reaches the DOM. |
frontend/src/app/features/admin/general.pure.ts |
Pure General Settings helpers, including required datetime validation, field messages, UTC conversion, end-after-start validation, and the default-challenge-address IPv4/hostname validator/normalizer/message trio. |
tests/frontend/admin-general-pure.spec.ts |
Pure client-contract tests for required event timestamps, datetime messaging, UTC conversion, event-window ordering, and default-challenge-address validation, error mapping, and normalization. |
tests/frontend/admin-categories-form-modal.spec.ts |
Tests the pure syncCategoryForm helper that drives the edit/create prefill in CategoryFormModalComponent: system-row abbreviation lock, user-row unlock, re-population on second invocation, clearing on create, and iconPreview passthrough. |
tests/frontend/authenticated-event-source.spec.ts |
Tests SSE authorization, frame transport behavior, and the 401/403 unauthorized path. |
tests/frontend/auth-session-events.spec.ts |
Pure tests for cross-tab invalidation message encoding, payload validation, and storage-event filtering. |
tests/frontend/admin-challenges-import-autoclose.spec.ts |
Pure helpers (summarizeImportResult, importErrorMessage) and modal auto-close branch for the confirmed-import handler. |
frontend/src/app/features/challenges/challenges.page.ts |
/challenges smart page: gate logic (countdown/running/stopped/unconfigured), modal lifecycle, SSE wiring on /api/v1/events, and the page-owned countdown-zero reload (handler registered in the constructor with the disposer wired into DestroyRef). |
frontend/src/app/features/challenges/challenges.store.ts |
Signal store: board, event state, per-card solve listeners, SSE solve-frame mutation, submit response application, stop() lifecycle, public reset() for the session-boundary flush, and setMyUserId(id) which now returns the previous id and clears the cache whenever the cached state belongs to an unknown or different user. |
frontend/src/app/features/challenges/challenges.service.ts |
HTTP service for /api/v1/challenges/{board,status,:id,:id/solves} returning typed ApiErrorEnvelopes. getDetail always requests ?include=solvers so the modal can render the solvers list in one round-trip; getBoard only attaches include=solvers when the caller opts in. |
frontend/src/app/features/challenges/challenges.pure.ts |
Pure types and helpers (BoardCard, SolverRow, SolveEventPayload, parseSolveEvent, mergeSolveEventIntoSolvers, messageForSolveError, formatDdHhMm). |
frontend/src/app/features/challenges/category-column.component.ts |
Renders a single category column (header + cards) on the challenges board. |
frontend/src/app/features/challenges/challenge-card.component.ts |
Renders a single challenge card on the board as a semantic <button> (difficulty, live points, solve count, ✓ when solved by the player). Exposes data-testid="challenge-card-<id>", data-solved, aria-pressed, an accessible name (`Challenge , solved |
frontend/src/app/features/challenges/challenge-modal.component.ts |
Challenge detail modal: description, flag form, awarded banner, solvers list, live-solve updates. |
frontend/src/app/features/scoreboard/scoreboard.page.ts |
/scoreboard smart page: 4 tabs (Ranking / Matrix / Event Log / Score Graph) wired through ScoreboardStore, SSE solve-frame subscription on /api/v1/events, and lifecycle (loadAll + wireSse on init, stop() on destroy). |
frontend/src/app/features/scoreboard/scoreboard.store.ts |
Signal store for the four scoreboard projections + active tab + SSE lifecycle; mutates ranking, matrix, event-log, and graph state from each solve frame via pure helpers (parseSolveEventIntoRanking, mutateMatrixFromSolve, applySolveToGraph, dedupEventLogBySolveId); exposes wireSse/stop/reset and a 1s→30s exponential reconnect loop on transport error. |
frontend/src/app/features/scoreboard/scoreboard.service.ts |
HTTP client for /api/v1/scoreboard/{ranking,matrix,event-log,graph} returning typed ApiErrorEnvelopes. |
frontend/src/app/features/scoreboard/scoreboard.pure.ts |
Pure types and helpers: RankingRow, MatrixView, EventLogRow, GraphView, SolveLivePayload, PLAYER_COLOR_PALETTE, stablePlayerColorIndex, applyRankingSort, parseSolveEventIntoRanking, dedupEventLogBySolveId, applySolveToGraph, mutateMatrixFromSolve, formatSolveDateTime. |
frontend/src/app/features/scoreboard/tabs.component.ts |
Horizontal tab strip (Ranking / Matrix / Event Log / Score Graph) bound to ScoreboardStore.activeTab. |
frontend/src/app/features/scoreboard/ranking.component.ts |
Ranking tab table (rank, player with color swatch, solved count, points). |
frontend/src/app/features/scoreboard/matrix.component.ts |
Matrix tab — sticky-header, sticky-player grid of cells (★ gold/silver/bronze, ✓, blank) with player color swatches. |
frontend/src/app/features/scoreboard/event-log.component.ts |
Event Log tab — newest-first <ol> of solves with timestamp, player, challenge, awarded points; position 1–3 use gold/silver/bronze stars, others use a green check. |
frontend/src/app/features/scoreboard/score-graph.component.ts |
Score Graph tab — pure-SVG line chart of top 10 cumulative points over the event window with per-player colored polylines and a legend; renders empty / countdown / unconfigured states. |
frontend/src/app/features/scoreboard/scoreboard.page.ts |
/scoreboard smart page (also listed above): gates the four tabs, wires store.loadAll() and store.wireSse(...) on init, and tears them down via store.stop() on destroy. |
frontend/src/app/core/services/notification.service.ts |
Root-provided signal-backed store of { id, kind, message, ts } records; error() / info() push, dismiss(id) / clear() remove. |
frontend/src/app/core/interceptors/error-notification.interceptor.ts |
Translates HttpErrorResponse into friendly messages and pushes them to NotificationService (with suppression for /api/v1/auth/{login,csrf,register} and /api/v1/challenges/status). |
frontend/src/app/core/services/blog.service.ts |
Typed Promise-based public blog list and admin CRUD HTTP client. |
frontend/src/app/features/blog/blog.page.ts |
/blog smart page with loading, error, empty, and published-list states. |
frontend/src/app/features/blog/blog-presenter.component.ts |
Shared sanitized Markdown post renderer used by the Blog and landing pages. |
frontend/src/app/features/blog/blog.pure.ts |
Pure blog-list state derivation. |
frontend/src/app/features/admin/blog/blog.component.ts |
/admin/blog post table and create/edit/delete workflow coordinator. |
frontend/src/app/features/admin/blog/blog-form-modal.component.ts |
Reactive title/Markdown editor with live preview and draft/publish actions. |
frontend/src/app/features/admin/blog/blog-delete-modal.component.ts |
Post deletion confirmation and inline failure UI. |
frontend/src/app/features/admin/blog/blog-form.pure.ts |
Admin form validation, form synchronization, request cleanup, and status display helpers. |
tests/frontend/blog-page.spec.ts |
Blog page-state and shared sanitized Markdown rendering contracts. |
tests/frontend/blog-admin-form.spec.ts |
Admin form validation, prefill/reset, payload, and status-label contracts. |
tests/frontend/challenges.pure.spec.ts |
Pure helpers: sorting, parsers, friendly error mapping, formatDdHhMm (DD:HH:mm:ss). |
tests/frontend/challenges.service.spec.ts |
HTTP-service contract: getDetail URL-encodes the id, attaches ?include=solvers, and forwards withCredentials: true. |
tests/frontend/challenges.store.spec.ts |
Signal store: board mutation, live solve merge, listeners, stop(). The "marks solvedByMe" and "does not double-count" tests now call setMyUserId('me-1') before load() to exercise the realistic page-mount sequence. |
tests/frontend/challenges.store.reset.spec.ts |
Per-user solvedByMe regression suite: reset() clears board/detail/myUserId/SSE; switching user id flushes the previous board; orphan board (cached with no recorded user id) is cleared when PlayerB's id is set; a non-mine solve never flips solvedByMe from false to true; applySubmitResponse takes the server value verbatim. |
tests/frontend/notification-interceptor.spec.ts |
Interceptor suppression rules and friendly message mapping. |
tests/frontend/challenge-card-accessibility.spec.ts |
Challenge-card accessibility contract: keyboard activation, aria-pressed toggling, accessible-name composition, and the data-solved / challenge-check-* test selectors. |
tests/backend/challenges-board.spec.ts |
Board query shape, ordering, ?include=solvers. |
tests/backend/challenges-status-rest.spec.ts |
/api/v1/challenges/status snapshot shape. |
tests/backend/challenges-events-sse.spec.ts |
/api/v1/events SSE: status + solve frames, dedup, ordering. |
tests/backend/challenges-submit-flag.spec.ts |
Submit flow: correct/incorrect flag, idempotent re-submit, event-state guard, race handling. |
tests/backend/scoreboard-controller.spec.ts |
Contract tests for the four /api/v1/scoreboard/* endpoints: JWT protection, ranking sort + competition rank, matrix cell assignment (1/2/3/solved/null), event-log limit clamp, graph boundaries + top-10 trim + empty state. |
tests/backend/scoreboard.service.spec.ts |
Service-level tests covering computeAwardedPoints parity with submitFlag, color-index stability, matrix ordering, and graph state handling. |
tests/frontend/scoreboard.pure.spec.ts |
Pure helpers: applyRankingSort (competition-rank numbering), parseSolveEventIntoRanking (add-or-increment + sort), dedupEventLogBySolveId, applySolveToGraph (anchors + plateau + top-10 trim), mutateMatrixFromSolve, stablePlayerColorIndex, formatSolveDateTime. |
tests/frontend/scoreboard.store.spec.ts |
Store lifecycle + live merge: loadAll aggregation + idempotency, SSE solve frame applying to all four tabs, stop() teardown, exponential reconnect. |
backend/src/modules/admin/system/admin-system.controller.ts |
Mounts /api/v1/admin/system/* (backup download, restore validate/commit, confirmations, scores/reset, challenges/wipe); all handlers require AdminGuard + @Roles('admin') and the destructive ones also consume a confirmation token. |
backend/src/modules/admin/system/admin-system.module.ts |
Wires AdminSystemController, BackupService, RestoreService, DangerZoneService, FilesystemTransactionService, ConfirmationTokenService; imports TypeOrmModule.forFeature([AdminOperationTokenEntity]) and AuthModule. |
backend/src/modules/admin/system/backup.service.ts |
Builds the full backup JSON: every application table (sqlite_master discovery, excluding admin_operation_token/migrations/sqlite_*), with base64-encoded uploads walked recursively from UPLOAD_DIR (skipping .staging). Exposes discoverTables(), getUploadDir(), and BackupService.stringify(doc). |
backend/src/modules/admin/system/restore.service.ts |
Two-phase restore: stageArchive(rawText, userId) validates the archive (zod), decodes uploads into <DATA_DIR>/.system-staging/restore-<id>/, and returns a summary; commitRestore(stagingId) clones the live DB, captures and temporarily drops the trg_user_last_admin_update / trg_user_last_admin_delete triggers in the offline candidate, clears every application table, re-inserts archived rows in FK-safe order, asserts the archive still contains at least one admin, re-creates the captured triggers, then atomically swaps the live DB + uploads via FilesystemTransactionService.stageSwap({ name: 'restore-backup', ... }). On any failure the swap is rolled back and SYSTEM_RESTORE_ROLLED_BACK is returned; the rebuild also best-effort re-creates any missing trigger before rethrowing so a rolled-back candidate never leaks missing the LAST_ADMIN safety net. |
backend/src/modules/admin/system/danger-zone.service.ts |
resetScores() deletes every solve row transactionally; wipeChallenges() snapshots <UPLOAD_DIR>/challenges to a side directory, deletes every challenge inside a transaction, physically removes the live -challenges directory post-commit, and either deletes the snapshot or restores from it on failure. |
backend/src/modules/admin/system/filesystem-transaction.service.ts |
Generic file/directory swap primitive: stageSwap({ pairs, hooks }) renames each live path to a rollback location, then renames the staged path into place (with copy+unlink cross-device fallback); commit(handle) removes rollback artifacts; rollback(handle) restores the original live paths. Provides static helpers rmSafe, copyDirSync, ensureDir. |
backend/src/modules/admin/system/confirmation-token.service.ts |
Issues SHA-256-hashed single-use tokens with SYSTEM_OP_CONFIRM_TOKEN_TTL_MS TTL; consume() runs in a transaction with conditional WHERE consumedAt IS NULL updates so only the first concurrent caller succeeds. purgeExpired() deletes expired and >24h-old consumed rows. |
backend/src/modules/admin/system/dto/admin-system.dto.ts |
Re-exports ADMIN_OPERATION_KINDS and zod contracts for the re-auth body, danger confirm body, and restore-commit body; declares the backup format identifier + version constants. |
backend/src/database/entities/admin-operation-token.entity.ts |
TypeORM entity and the canonical ADMIN_OPERATION_KINDS literal union (restore-backup, reset-scores, wipe-challenges). |
backend/src/database/migrations/1700000000900-AddAdminOperationTokens.ts |
Forward-only migration that creates the admin_operation_token table and its three indexes (uq_admin_op_token_hash, idx_admin_op_token_user_op, idx_admin_op_token_expiry). |
backend/src/common/utils/upload.ts |
Adds resolveSystemStagingDir(config) (defaults to <DATA_DIR>/.system-staging, never nested under UPLOAD_DIR) and getRequestSizeLimit(config) used by the system endpoints. |
backend/src/config/env.schema.ts |
Adds SYSTEM_OP_STAGING_DIR, SYSTEM_OP_RESTORE_STAGE_TTL_MS (default 15 min), SYSTEM_OP_CONFIRM_TOKEN_TTL_MS (default 5 min). |
backend/src/common/errors/error-codes.ts |
Adds the SYSTEM_* error codes used by the system endpoints (SYSTEM_BACKUP_FAILED, SYSTEM_RESTORE_VALIDATION_FAILED, SYSTEM_RESTORE_PAYLOAD_TOO_LARGE, SYSTEM_RESTORE_STAGE_EXPIRED, SYSTEM_RESTORE_FAILED, SYSTEM_RESTORE_ROLLED_BACK, SYSTEM_OPERATION_IN_PROGRESS, SYSTEM_REAUTH_REQUIRED, SYSTEM_INVALID_CREDENTIALS, SYSTEM_TOKEN_INVALID, SYSTEM_TOKEN_EXPIRED, SYSTEM_TOKEN_REUSED, SYSTEM_TOKEN_MISMATCH, SYSTEM_DANGER_FAILED, SYSTEM_DANGER_ROLLED_BACK, SYSTEM_FILE_READ_FAILED, SYSTEM_PAYLOAD_INVALID). |
backend/src/modules/auth/auth.service.ts |
Adds reauthenticateAdmin(userId, password) (Argon2id verify + admin/enabled status check) and revokeAllRefreshSessions(userId, manager?) (used after a successful restore to force the SPA back to /login). |
tests/backend/admin-system-authorization.spec.ts |
Non-admin JWTs and missing tokens are rejected on every /api/v1/admin/system/* endpoint. |
tests/backend/admin-system-backup.spec.ts |
Backup envelope shape, table discovery exclusion, and base64 file inclusion. |
tests/backend/admin-system-confirmation-token.spec.ts |
Issue/consume/expire/reuse/mismatch flow and TTL config. |
tests/backend/admin-system-danger.spec.ts |
Reset-scores and wipe-challenges happy path + DB/file rollback. |
tests/backend/admin-system-restore-validation.spec.ts |
Archive schema validation, base64 size mismatch, sha256 mismatch, safe-path rejection, and duplicate-path rejection. |
tests/backend/admin-system-restore-commit.spec.ts |
Controller forwards stagingId to RestoreService.commitRestore, plus a real-file service-level regression that restores a backup containing the same admin as the live system, asserts swapped table settings + uploads + trigger preservation, and verifies an admin-less archive is rejected with SYSTEM_RESTORE_ROLLED_BACK. |
frontend/src/app/features/admin/system/system.component.ts |
/admin/system smart page: two panels (Database + Danger Zone), backup download, restore pick + validate, confirm modal trigger, and per-operation success/error surfacing + cross-store invalidation. |
frontend/src/app/features/admin/system/system-confirm-modal.component.ts |
Re-authentication + confirmation-phrase modal with operation-specific copy from system.pure.ts. |
frontend/src/app/features/admin/system/system.service.ts |
HTTP client for the six /api/v1/admin/system/* endpoints. |
frontend/src/app/features/admin/system/system.pure.ts |
Pure helpers: error → friendly message map, destructiveCopy(op) per-operation modal copy, pickRestoreFile() (jsdom-testable file picker), deriveBackupFilename(). |
frontend/src/app/core/services/system-data-change.service.ts |
Root-provided signal bus ('scores-reset', 'challenges-wiped', 'restore-completed') consumed by ChallengesStore and ScoreboardStore to invalidate their caches after a destructive operation. |
frontend/src/app/core/services/auth.service.ts |
Adds forceServerInvalidation() so the restore flow can drop the local session and broadcast the cross-tab invalidation without invoking the backend logout. |
frontend/src/app/features/challenges/challenges.store.ts |
Now subscribes to SystemDataChangeService and calls reset() on every destructive operation kind. |
frontend/src/app/features/scoreboard/scoreboard.store.ts |
Now subscribes to SystemDataChangeService and clears ranking/matrix/event-log/graph on every destructive operation kind. |
frontend/src/app/features/admin/admin-shell.component.ts |
Side-nav entry for system is now enabled: true. |
frontend/src/app/app.routes.ts |
Adds the lazy system child route under the admin shell. |
tests/frontend/admin-system.pure.spec.ts |
Pure-helper contracts for the friendly error map, destructive copy, and file picker. |