Commit Graph
112 Commits
Author SHA1 Message Date
branderson cd1d7fbb35 chore: remove leftovers from upstream
Drops the /flash and /html5 sections (old URLs redirect to /browse),
the GitHub star count endpoint, the unused HOME_RECENT_HOURS setting,
upstream's design notes and code review docs, and the issue template's
link to upstream discussions.
2026-10-13 21:19:34 -07:00
branderson 5e7b2bc72e ci: publish images from Gitea Actions; make the update check opt-in
A Gitea workflow builds the app, session manager and streamer images
and pushes them to the instance's registry on pushes to main and on
version tags, reading the registry host at run time. Compose can pull
them with IMAGE_PREFIX. The update checker reads APP_UPDATE_URL instead
of upstream's GitHub releases and is off without it. Upstream's Docker
Hub workflow is removed.
2026-10-13 21:14:42 -07:00
branderson 05db4313ae chore: rename the project to Dotcom Arcade 2026-10-13 19:19:15 -07:00
branderson c044f5e9cf feat(portal): game page with in-place play
Game detail and play are one portal page: a stage with the screenshot
and a Play button (or Enter) that loads the game in place, controls
under it, details, plays, rating, tags, favorite and add to playlist,
and related games.
2026-10-13 11:18:25 -07:00
branderson a0b032bc79 fix(portal): drop the description under the mature games switch 2026-10-13 11:15:44 -07:00
branderson ee6fb4e951 feat(portal): arcade portal shell and discovery homepage
Chilled color scheme as swappable tokens, Fredoka wordmark with a
configurable tagline, genre categories, search and random hotkeys.
The homepage offers a Game of the Day, a Time Machine with a date
picker, recently played games and Flashpoint playlist shelves.
Mature games are hidden unless opted in from the user menu, stored
in the browser and the account. Browse lists default to a seeded
shuffle and accept release date ranges.
2026-10-12 18:09:20 -07:00
branderson 3ab6e05a00 fix(saves): leave Selkies settings out of game saves
The streaming client in a streamed game's iframe shares the app's origin and
keeps its settings in localStorage under its page address, which includes
the session. Those keys changed during play, so they were attributed to the
game and a new batch joined its save every session. Exclude them, and drop
any already recorded when gathering a save.
2026-10-12 16:17:09 -07:00
branderson 1e098674d8 feat(saves): keep players' game saves on their account
Saves games keep in the browser now sync with the signed-in player's
account: Ruffle SharedObjects and HTML5 localStorage, IndexedDB databases
(exported and imported whole, typed arrays and blobs included), and streamed
games' save files. Games share the app's origin, so storage that changes
while a game runs is attributed to it. Before a game starts, the account's
save is loaded unless this browser has a newer one; changes upload as they
happen; and a Saves dialog in the player restores any of the last 10, keeping
the save it replaces as a backup.
2026-10-12 16:17:09 -07:00
branderson 36d1c67bb0 fix(ui): stop the I'm Feeling Lucky button animating
The button bounced and pulsed continuously, which is distracting,
especially beside a running game. It keeps its hover effects.
2026-10-12 02:14:38 -07:00
branderson d735263741 Merge pull request 'Sign in through an authenticating reverse proxy' (#3) from trusted-header-login into main
Reviewed-on: branderson/flashpoint-web#3
Reviewed-by: branderson <brad@branderson.io>
2026-10-12 02:09:00 -07:00
branderson a6adc61627 feat(auth): sign in through a trusted proxy and let those users set a password
On load, a signed-out visitor first asks the backend to sign them in as the
user an authenticating proxy vouches for, before guest mode or a login
redirect takes over; an explicit logout skips this for the rest of the tab.
Settings gains a password card for signed-in users, where a proxy-signed-in
user can set a password without the current one to log in without the
proxy.
2026-10-12 01:05:40 -07:00
branderson 1965a1392b feat(streaming): stream Flash with the original player when Ruffle falls short
Flash games Ruffle can play now also report streamable, and the player
offers a switch to the original Flash Player streamed from the server.
Flash games launched from a page, which need the browser plugin, stream
instead of loading a page that can't play, and search no longer hides them
while streaming is enabled.
2026-10-11 17:50:24 -07:00
branderson 4e21ec1529 feat(streaming): play streamed games in the player and keep their saves in the browser
Games the backend reports as streamable get a Play button. The player starts
a stream session, embeds the stream, and keeps the game's saves in
IndexedDB: sent when the session starts and updated as the server pushes
changes.
2026-10-11 01:01:27 -07:00
Darkraise 2724a40325 feat(auth): let guests play games
When guest access is enabled, guests now receive games.play alongside
games.read and playlists.read, on both the backend guest fallback
(optionalAuth, sharedAccessAuth) and the frontend guest session. Play
statistics stay authenticated-only.
2026-09-28 12:09:09 +07:00
Darkraise f5c167b470 chore (ruffle): pump built-in ruffle to version 0.5.0 2026-08-22 01:09:24 +07:00
Darkraise 4ffa92a3a5 feat(ruffle): add stable and nightly release channels
Ruffle publishes tagged releases and daily nightlies into a single
GitHub feed. The updater took whichever release came first carrying a
web-selfhosted.zip, so a stable release landing at the top of the feed
was installed on hosts tracking nightlies -- and because version
comparison normalised everything to a build date, the resulting semver
never matched, leaving the host reporting an update forever.

Selection now filters on GitHub's prerelease flag, and comparison
splits by channel: build date for nightlies, numeric parts for stable.
The channel lives in the ruffle.channel system setting (default
stable) and governs both manual installs and the scheduled job.
Switching channels records the choice and reports the target build;
nothing is downloaded until the install is requested.

Stable releases are months apart and can fall outside the recent page
the changelog is built from, so that channel falls back to the
latest-release endpoint.
2026-08-22 01:06:03 +07:00
Darkraise 6649757c5d fix(ruffle): bust cached loader URL for the header fix 2026-08-13 17:49:34 +07:00
Darkraise 049779a78a fix(playlists): bundle community playlist index
The index was scraped from the Flashpoint Datahub wiki on every request.
That page now sits behind a Cloudflare managed challenge that no plain
HTTP client can pass, so the endpoint answered 500 for every caller.

Ship the list instead, extracted from a saved copy of the page into
data/community-playlists.json: 104 playlists across 9 categories. Only
entries whose download URL is on the allowlist are kept, so everything
the modal offers can also be imported. That drops the Old Default
Playlists section, hosted off the allowlist and still in the
pre-Flashpoint 10 format.

The modal now fetches only while open rather than on every render of the
playlists page, and shows when the snapshot was captured. Drops cheerio,
whose only consumer was the scrape.
2026-08-13 13:57:52 +07:00
Darkraise e9613362c2 fix(settings): tighten update card and pin auth
- Show checkedAt on the up-to-date panel and only claim currency
  when versions actually match, with a matching neutral toast path
  for unreleased builds
- Add aria-expanded to the changelog toggle
- Reword a review-referencing comment/test title and pin the /app
  route's settings.update permission gate in tests
2026-08-13 00:44:02 +07:00
Darkraise 1b9b1ce933 fix(settings): keep held data on a failed app update refetch
isError stays true for any errored query state, including a
background refetch that failed after a prior successful fetch -
TanStack Query keeps the last-good data in that case. Using isError
in checkFailedEntirely hid an already-rendered update/changelog/
upgrade-command panel behind the generic failure panel on any
refetch error, most likely right after an admin follows the card's
own upgrade instructions and restarts the backend it polls.

isLoadingError is true only when there is no cached data to fall
back on, which is the actual "nothing to show" case the failure
panel is for.
2026-08-13 00:24:05 +07:00
Darkraise 680f4624c3 fix(settings): show failure panel on app update query error
The useQuery for /updates/app only had its always-200 "check failed"
field handled, not a genuine request failure (backend unreachable,
transport error, expired token). In that case info stayed undefined
with isLoading false, rendering an empty card below "Current Version:
Unknown". Fold isError into the existing checkFailedEntirely panel so
a query error gets the same treatment as a failed remote check.
2026-08-13 00:17:23 +07:00
Darkraise 7760698980 feat(settings): add the web app update card 2026-08-13 00:10:04 +07:00
Darkraise 7b0291d8b2 feat(updates): serve the app update check 2026-08-13 00:03:13 +07:00
Darkraise 9c85284798 fix(settings): show the real web app version
The card always displayed 1.0.0. VITE_APP_VERSION was never set: CI
passes the release tag as a VERSION build arg, but the Dockerfile never
declared it or forwarded it to the frontend build, so the hardcoded
fallback always won.

Declare the build arg and export it as VITE_APP_VERSION, and resolve the
version in vite.config.ts for every other build: the env var, then the
release tag via git describe, then package.json. The image version label
was pinned at 1.0.0 for the same reason and now follows the arg too.
2026-08-12 21:32:49 +07:00
Darkraise aed4e4914d feat(settings): show package date and last update
The Version Information card could not tell one Flashpoint install from
another: Ultimate names only a major version in version.txt, and nothing
showed when the metadata was last synced.

Serve two more values with the public settings. The package date is the
mtime of version.txt, falling back to .preferences.defaults.json. Both
are written when the package is built and never rewritten afterwards,
unlike preferences.json and flashpoint.sqlite, which the Launcher and
this app write to.

The last update comes from preferences.json. A missing source or the
1970-01-01 placeholder is normalized to null, which the UI renders as
"Never" - what Ultimate always shows, since it ships no metadata source.
2026-08-12 21:32:05 +07:00
Darkraise 74aa0cf067 feat(startup): serve the app while the server warms up
On a network share the database copy and cache pre-warm run for minutes,
and the listener only opened after all of it, so anyone who loaded the
page during that window got a connection error.

Open the port immediately after registering the static frontend, and
hold every route that needs the database behind a gate that answers 503
with a machine-readable marker and the current startup phase. The SPA
shell needs nothing the gate blocks, so the frontend renders a "server
is starting" screen on the login page's own background, polls /health,
and reloads once the server reports ready.

Move the database copy out of the entrypoint: it does the same freshness
check the application does, and doing it before node starts kept the port
closed for the longest part of startup. Distinguish a dead server from a
slow one by requiring the backend's own marker, since a proxy in front of
a stopped backend still answers.
2026-08-12 14:05:14 +07:00
Darkraise 3816c4e0b5 fix(library): keep game card content inside the rounded corners
The card owns the radius but never clipped its children, and CardContent
clips to a square box flush with the card's top edge. The skeleton and the
gradient overlay both fill that box, and a hovered image scales into it, so
square corners rendered over the card's rounded ones.

Clip on the card itself, matching PlaylistCard. CardContent keeps its own
overflow-hidden to bound the scaled image to the media area.
2026-08-12 11:21:05 +07:00
Darkraise a984801f57 fix(images): abort thumbnail downloads on navigation
LazyImage carried the abort logic but nothing rendered it -- every image in
the app was a plain <img>, so navigating away from a grid left dozens of
thumbnails downloading, holding the six connections and the server I/O the
next page needed. Removing an <img> from the DOM does not cancel its
request; removing the src attribute does.

Rewrite LazyImage around the real element: native lazy loading, internal
loaded and error state, a skeleton prop taking true, false or a node, and a
fallback prop for the error case. Migrate the seven game image sites to it,
each dropping its own loaded/error state. Drop LazyBackgroundImage and
useLazyLoad, which had no consumers either.

Skip the abort while the node is still connected: StrictMode runs effect
cleanups on a live element, and clearing src there blanks every image in
development with nothing to restore it.
2026-08-12 11:09:34 +07:00
Darkraise 18c0c3d329 feat(metadata): optionally download images after a metadata sync
Images normally arrive lazily: the proxy fetches one from the CDN when a
browser asks for a game the local mount lacks. That is wrong for an offline
host, where a sync can add thousands of games whose images would only ever
appear for whoever browses to them while online.

Add an opt-in setting (off by default) that, after a sync, fetches logos and
screenshots for exactly the games that sync touched. Runs as its own job with
progress and cancellation, and is never awaited by the sync, so a slow or
failing CDN cannot block or fail a metadata update.
2026-08-12 02:51:24 +07:00
Darkraise a93cb69bbb test(frontend): give the test environment working storage
This Vitest version's jsdom environment installed localStorage as a bare object
with no methods, so the auth store's persist middleware threw and took 69 tests
across 6 files down with it. A suite that red hides real regressions.

Install an in-memory Storage for localStorage and sessionStorage, cleared
between tests. That surfaced two failures it had been masking: PlaylistCard
asserted a stale string id in its href, and the MSW mocks lacked a handler for
POST /api/games, which is how search has worked since filter combinations
outgrew the URL length limit.

Frontend suite: 115/115.
2026-08-12 02:36:04 +07:00
Darkraise fb8fcd390a fix(images): cancel in-flight preloads and skip abandoned requests
LazyImage and LazyBackgroundImage preload through a detached Image and their
cleanup only set a cancelled flag, so the request kept downloading after
unmount. Navigating away from a grid left dozens of downloads occupying the
browser's six connections and the server's I/O, starving the page the user
actually opened.

Clear the handlers and reset src on cleanup, which is what aborts the request,
and have the image proxy return early when the client has already gone rather
than paying a mount round trip — or a 10s CDN fetch — for nobody.
2026-08-12 01:56:11 +07:00
Darkraise ad8d7720c9 feat(metadata): allow admins to set a metadata source
The Ultimate edition ships preferences.json without gameMetadataSources, so
metadata sync was permanently unavailable there with no way to opt in.

Add an admin-only override (metadata.custom_source_enabled / _url) resolved
ahead of preferences, restricted to the two trusted FPFSS hosts and validated
on save. While it is off, the Metadata Update card explains that Ultimate does
not support metadata updates and that updating means a newer full package,
pointing advanced admins at the override. Enabling it requires confirming a
dialog that spells out what a sync rewrites.
2026-08-12 01:09:25 +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 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 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 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 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 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
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
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 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
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