295 Commits
Author SHA1 Message Date
Darkraise f6fd7f9bac fix(auth): mark auth cookies Secure per connection, not per NODE_ENV
Cookies were Secure whenever NODE_ENV=production, so browsers discarded them
on any deployment served over plain HTTP and every login bounced back to the
login page. A fixed flag cannot serve a LAN over HTTP and the internet over
HTTPS from the same instance.

Derive the flag from req.secure, which honours X-Forwarded-Proto from a
trusted proxy, and add COOKIE_SECURE=auto|true|false to override it.
2026-08-11 23:49:28 +07:00
Darkraise d1d885d302 docs(readme): add minimal working env example, refresh stale sections 2026-08-11 22:14:16 +07:00
Darkraise 352e2a07b2 fix(docker): pass FLASHPOINT_PATH explicitly in dev compose 2026-08-11 22:09:59 +07:00
Darkraise c09f0c8a83 fix(cors): normalize origins and allow same-origin requests
Browsers omit a scheme's default port when serializing Origin, so a page at
http://host:80 sends "http://host". Comparing that to the raw DOMAIN string
rejected every POST from a deployment served on port 80 or 443 — including
same-origin requests under SERVE_FRONTEND, which surfaced as a CORS error on
login with no way to guess the cause.

Compare normalized origins instead, always allow a request whose Origin
matches its own Host, and accept a comma-separated DOMAIN so one deployment
can be reached over both a LAN address and a public domain. Apply the same
normalization to the domains table, which previously built origins without
ports, and reflect the caller's origin on the game-zip admin routes.
2026-08-11 22:09:59 +07:00
Darkraise 57f099b944 perf(games): cache downloaded filter options indefinitely
Treat `downloaded` as a base cache dimension like platform and library
instead of a dynamic filter. It is two-valued, and every path that can
change presentOnDisk (GameDatabaseUpdater, DownloadedReconciler) plus a
database reload already clear this cache, so the 30-second TTL only forced
the Downloaded page to recompute nine option queries twice a minute.

Pre-warm the downloaded combination at startup so the page is instant on
first visit.
2026-08-11 21:57:39 +07:00
Darkraise 889133cf3b feat(games): add downloaded games page, filter, and settings
Add a Downloaded page listing games whose data is on disk, a downloaded
filter on the browse pages, and three admin settings controlling the page,
guest access to it, and whether the filter starts on.

Make presentOnDisk trustworthy: the play-path download now records it, a
reconciler rebuilds it from ZIP filenames at startup and after each Launcher
sync (which also backfills earlier play-path downloads), and self-writes no
longer trigger a database reload and cache flush.

Closes #12
2026-08-11 19:47:52 +07:00
Darkraise d9af9af1fd test(library): cover explicit-off write on downloaded chip removal
Adds the test promised in the design spec's testing section: chip
removal must write an explicit '0' when the admin default is on,
otherwise clearing the URL param would silently re-apply the
downloaded filter. Covers all three removal paths in
GameBrowseLayout (handleRemoveChip, handleRemoveWithChildren,
handleClearAllFilters) against both default states.

Renders GameBrowseLayout with its data hooks (useGames,
useFilterOptions, useFavoriteGameIds, useBatchRatingAggregates,
useFeatureFlags) stubbed out and FilterPanel replaced with a minimal
stand-in exposing the exact callbacks it receives, so the real
handler logic runs without pulling in FilterPanel's own filter-
dropdown machinery or live network calls.
2026-08-11 19:11:46 +07:00
Darkraise 9dbaf0713e docs: fix HTTP method and presentOnDisk sample in games docs
Search Games is a POST with a JSON body (backend/src/routes/games.ts),
not a GET with query params - correct the framing around the new
`downloaded` field. Also fix the presentOnDisk SQL sample in
game-browsing-filtering.md, which defined it as "has any game_data
row" and contradicted both the Downloaded Games section further down
and the real backend logic (EXISTS ... presentOnDisk = 1).
2026-08-11 19:11:40 +07:00
Darkraise a0e8835be5 fix(downloads): clear caches after reconciler marks games downloaded
reconcile() called noteSelfWrite() but never cleared GameSearchCache
or the filter options cache. In the post-sync path caches are cleared
at reload and reconcile then runs asynchronously, leaving a window
where a downloaded:true search gets cached without the flags the sync
just wiped. With GameSearchCache's 5-minute TTL and updateAgeOnGet,
a frequently-refreshed Downloaded-page query could stay stale
indefinitely.
2026-08-11 19:11:36 +07:00
Darkraise 714cef96ee fix(downloads): move self-write suppression into GameDatabaseUpdater
markAsDownloaded and markAsNotDownloaded now call
DatabaseService.noteSelfWrite() plus clear the search and filter
options caches themselves, matching the design spec. Previously this
only happened at the gamezipserver.ts call site, so DownloadManager's
Download-button path (non-copy mode) still triggered the file watcher
on every download, causing an unnecessary full reload and cache flush.
2026-08-11 19:11:31 +07:00
Darkraise bbb0c169f3 docs: document downloaded games page and filter 2026-08-11 18:50:58 +07:00
Darkraise a1b2bafcb4 feat(games): add downloaded games page 2026-08-11 18:39:37 +07:00
Darkraise cb1c9f5ef5 fix(filters): forward downloaded to filter-options request
Task 2 already narrows filter-option lists on the backend when downloaded
is true, but useFilterOptions never forwarded the field into the query
params or query key, so the backend support was unreachable and the
dropdown/year-range options never narrowed to downloaded games.
2026-08-11 18:34:28 +07:00
Darkraise 11bce2af41 feat(filters): add downloaded filter to browse pages 2026-08-11 18:30:53 +07:00
Darkraise e7bbd36aff feat(filters): encode downloaded filter in the URL 2026-08-11 18:19:20 +07:00
Darkraise 3cef0feb7c feat(settings): add downloaded page and filter settings 2026-08-11 18:15:09 +07:00
Darkraise 170956197a fix(games): record downloaded state after play-path download 2026-08-11 18:10:23 +07:00
Darkraise 3be575bbd7 feat(games): reconcile downloaded state from disk 2026-08-11 18:04:32 +07:00
Darkraise aecada204f fix(games): remove unreachable dead code in filter options cache key 2026-08-11 17:59:21 +07:00
Darkraise 18cb658eca feat(games): narrow filter options to downloaded games 2026-08-11 17:38:33 +07:00
Darkraise 52cf237f5e feat(games): filter search results by downloaded state 2026-08-11 17:33:29 +07:00
Darkraise a8f9b1db11 docs(plan): add downloaded games implementation plan 2026-08-11 17:30:11 +07:00
Darkraise be3fb6de60 docs(spec): design downloaded games page, filter, settings 2026-08-11 17:18:33 +07:00
Darkraise df298165a3 feat(settings): add toggle for I'm Feeling Lucky button
Add the public feature flag features.enable_lucky_button (default on)
with an admin switch in Settings > Features. AppShell renders the
floating random-game button only when the flag is enabled.

Unlike the other feature flags, this one is not bypassed for admins:
it controls cosmetic chrome, so an admin hiding it must also stop
seeing it in their own session.

Closes #13
2026-08-11 16:35:23 +07:00
Darkraise f385eeb27b fix(security): harden served-frontend CSP, drop unsafe-inline/eval
Tighten the Content-Security-Policy the backend sends when it serves the
SPA so injected inline scripts and eval are no longer allowed:

- script-src: 'self' + 'wasm-unsafe-eval' (for Ruffle's WASM) + a SHA-256
  hash of index.html's inline theme bootstrap, computed at startup from the
  served file. Drops 'unsafe-inline' and 'unsafe-eval'.
- style-src: keep 'unsafe-inline' — Radix/shadcn set inline style attributes
  at runtime, which hashes/nonces (style-src-attr) cannot cover.

The inline-script hash normalizes CRLF/CR to LF before hashing because the
HTML parser does the same before matching, so the hash holds on Windows
(CRLF) checkouts too.

Verified: 0 eval/new Function in the bundles; Ruffle loads as an external
same-origin script and only needs wasm-unsafe-eval; the hashed theme script
executes under the strict policy in a real browser with no CSP violation.
2026-08-11 16:02:06 +07:00
Darkraise 3ba60aedf9 refactor(docker): build and ship a single API + UI image
Collapse the separate backend (Node) and frontend (nginx) images into
one image where the Express backend serves the built React app in
addition to the API and game content, so deployment is a single
container and one origin.

- Add a root Dockerfile with frontend-builder + backend-builder stages
  producing one runtime image; remove backend/ and frontend/ Dockerfiles,
  the nginx config, and the frontend entrypoint
- Serve the frontend dist and SPA fallback from Express (config.serveFrontend
  / FRONTEND_DIST_PATH), before auth/maintenance so the shell always loads and
  before the API 404 so client routes resolve to index.html
- Port the SPA-oriented CSP from nginx into Helmet, gated on serveFrontend;
  keep the strict API-only policy otherwise
- Reduce both compose files to a single flashpoint-web service on WEB_PORT
  -> 3100; drop API_PORT, BACKEND_HOST, BACKEND_PORT
- Build one image in the release workflow instead of a two-service matrix
- Update .env.example, README, workflow README, and env var docs

Also add .gitattributes (*.sh eol=lf) and normalize the entrypoint line
endings — without it, Windows checkouts bake CRLF into the image and the
shebang fails with "no such file or directory".
2026-08-11 16:02:06 +07:00
Darkraise a41eac0101 chore: remove ui-ux-pro-max skill and UI revamp plan 2026-08-11 16:02:06 +07:00
Darkraise e80328cd24 refactor(playlist): improve AddToPlaylistModal accessibility
- Use label/fieldset/legend and Checkbox ids so playlist rows are
  keyboard- and screen-reader-navigable
- Add aria-live status regions for loading and empty states and
  aria-hidden on decorative icons
- Move action buttons into DialogFooter
- Drop the duplicate 'Playlist created' toast already fired by the
  create mutation
2026-08-11 16:02:06 +07:00
Darkraise bd2514f1f7 chore(config): centralize and document environment variables
- Read game paths and NODE_ENV through the config module in
  gamezipserver instead of process.env directly
- Harden SQLite cache/mmap parsing with isNaN checks so invalid
  values fall back to defaults instead of NaN
- Pass auth, rate-limit, application, and OpenTelemetry variables
  through both docker-compose files with documented defaults
- Expand env var documentation and .env examples to match
2026-08-11 16:02:06 +07:00
Darkraise b57f2701c0 feat: add 5-star game rating system
Add community game ratings with a 5-star system:

- Backend: GameRatingService, /api/ratings routes, and migration
  002_game_ratings.sql for the ratings table and aggregates
- Frontend: StarRating and GameRatingSection components, useRatings
  hooks, ratings API client, and rating types
- Surface aggregate ratings on game cards, list items, detail view,
  and a Highest Rated section on the home page
- Gate the feature behind an enableRatings feature flag with a
  settings toggle
- Add --star theme tokens and Tailwind star color
2026-08-11 16:02:06 +07:00
DarkraiseandGitHub c79e18d0a5 Merge pull request #15 from MaxfieldKassel/fix/guest-browsing-401-redirect-loop
fix(auth): keep guest browsing alive on auth-only 401s
2026-08-11 16:01:42 +07:00
Maxfield Kassel ccc95ddbb0 fix(auth): keep guest browsing alive on auth-only 401s
Clicking "Browse as Guest" put the app into a redirect loop back to
/login. Guest mode is intentionally unauthenticated (isGuest, no token),
but the home page fetches an auth-only endpoint on load
(GET /favorites/game-ids), which returns 401. The response interceptor
treated that 401 as an expired session: since a guest is not
authenticated, it fell into the `!isAuthenticated` branch, cleared auth
state, and did `window.location.href = "/login"`. So guest mode never
survived the first page load.

Short-circuit the 401 handler for guests: a 401 from an auth-only
endpoint is expected for a guest (there is no token to refresh), so
reject and let the caller handle it instead of tearing down the session.
Authenticated-user and shared-access flows are unchanged.

Adds a regression test covering both the guest (no redirect) and
authenticated (redirect on failed refresh) paths.
2026-07-18 12:40:17 -04:00
DarkraiseandGitHub ac8364103a Merge pull request #11 from darkraise/feature/filter-enhancement
Feature/filter enhancement
2026-02-26 10:08:15 +07:00
Darkraise 2757ac0a12 feat: support multi-value filters with POST search endpoint
Switch games search from GET to POST with JSON body to avoid URL
length limits when combining multiple filters. Filter options now
return context-aware results based on active selections.

Key changes:
- Backend: POST /api/games with array-based filter params
- Backend: Filter options cached per page combination + 30s TTL
  for dynamic filter states
- Backend: Fix semicolon-delimited field matching (developers,
  publishers, playModes, languages) using INSTR
- Frontend: Add URL compression (lz-string) for shareable filter URLs
- Frontend: Multi-select support in filter dropdowns and chips
- Frontend: Context-aware filter options hook
2026-02-25 22:26:27 +07:00
Darkraise e0656e81cb feat: add year range filter and improve filter panel
- Add YearRangeFilter component with from/to inputs and popover UI
- Check both game_data and game table for Flash SWF detection
- Add context-aware sort labels (Oldest/Newest vs A-Z/Z-A)
- Validate page param with isNaN() and Math.max(1, ...)
- Memoize filter handlers, updateFilter, and visibleConfigs
- Add error state with retry button for filter options
- Add LIMIT 100 to play modes query for consistency
- Stabilize useFilterDropdowns with useRef pattern
2026-02-15 17:52:42 +07:00
Darkraise 9984b70a70 docs: expand API reference with comprehensive endpoint docs
Add detailed documentation for multiple API endpoints and features:
- User playlists API with sharing and cloning endpoints
- Shared playlists API with access token generation
- Community playlists API for browsing/downloading
- Downloads API with SSE progress tracking
- Jobs API for scheduled task management
- Maintenance mode feature documentation

Update existing docs:
- Clarify Flashpoint vs User playlists distinction
- Add setup-status, most-played, and theme endpoints
- Expand settings cache and permission cache docs
- Reorganize API reference navigation structure
2026-02-15 13:45:14 +07:00
Darkraise c270e42d87 chore: update documents 2026-02-15 11:50:39 +07:00
DarkraiseandGitHub 4594a66721 Merge pull request #10 from darkraise/feature/pagination-enhancement
feat: add server-side pagination to all list endpoints
2026-02-15 01:16:10 +07:00
Darkraise b2456efe40 fix: fetch all local playlists for duplicate detection 2026-02-15 01:15:57 +07:00
Darkraise d0681ef3ab feat: add server-side pagination to all list endpoints
Add page-based pagination with proper total counts to:
- Favorites games endpoint (backend + frontend)
- Community playlists endpoint (backend + frontend)
- User playlists endpoint (backend + frontend)
- Roles endpoint (backend + frontend)

Changes include:
- Backend routes now accept page/limit params and return
  { data, pagination } response format
- Services return { data, total } for paginated queries
- Frontend hooks updated with page/limit parameters
- Views display PaginationWithInfo component when needed
- DataTable uses shared Pagination component
- Mutation hooks use invalidateQueries instead of manual
  cache updates for consistency with paginated data
- Increase default siblingCount to 2 in Pagination
2026-02-15 01:04:51 +07:00
Darkraise ea2d913766 fix: preserve search params during auth redirect
When a protected page includes query parameters (e.g., filters,
pagination), they were lost after login because only pathname was
extracted from location state. Now both pathname and search params
are preserved when redirecting back after authentication.
2026-02-14 20:45:11 +07:00
DarkraiseandGitHub 1dea285285 Merge pull request #9 from darkraise/feature/html5-game-enhancement
fix: improve HTML5 game compatibility and auth redirects
2026-02-14 20:08:47 +07:00
Darkraise 37d6ee239c fix: prevent open redirect in login return URL handling
- Add isValidReturnUrl() helper in LoginForm.tsx
- Add sanitizeRedirectPath() helper in AuthContext.tsx
- Apply sanitization to both regular login and guest login flows
2026-02-14 20:07:30 +07:00
Darkraise a990db1ce2 fix: improve HTML5 game compatibility and auth redirects
- Add COEP headers for iframe embedding when parent has COEP enabled
- Remove URL encoding in game proxy path (handler expects raw URLs)
- Preserve returnUrl as query param across cross-browser navigation
- Pass redirect path to loginAsGuest for proper return after login
- Filter play history to only return completed sessions
- Add documentation for CORS header responsibilities
2026-02-14 16:47:57 +07:00
Darkraise c62354fc7d chore: update contributing guide 2026-02-14 12:29:26 +07:00
Darkraise d70d86b572 chore: update contributing guide 2026-02-14 11:48:35 +07:00
Darkraise 1ebe55badc chore: update github actions 2026-02-14 11:25:08 +07:00
DarkraiseandGitHub 39bc35b9d5 Merge pull request #8 from darkraise/feature/metadata-sync
feat: improve metadata sync compatibility across Flashpoint editions
2026-02-13 22:08:11 +07:00
Darkraise 4714aeed32 feat: improve metadata sync compatibility across Flashpoint editions
- Replace hardcoded edition check with runtime schema introspection to
  detect available game columns dynamically
- Move GameMetadataSource type to PreferencesService for reuse across
  sync and update services
- Add hasMetadataSource() check instead of edition-based feature gating
- Handle orphaned parent references by nullifying parentGameId when
  parent doesn't exist in batch or database
- Disable foreign key checks during bulk insert/delete operations to
  match Flashpoint Launcher behavior
- Build combined Ruffle changelog showing all changes since current
  version instead of just latest release
- Add skeleton loading state to MetadataUpdateCard during initial fetch
- Reduce per-game cycle warning spam to single summary log
2026-02-13 21:54:47 +07:00
DarkraiseandGitHub 5d91fcf6bd Merge pull request #7 from darkraise/feature/ui-enhancement
Feature/UI enhancement
2026-02-13 02:08:02 +07:00