Commit Graph
221 Commits
Author SHA1 Message Date
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 d65f25aa2a feat(discovery): mature filter, release ranges, seeded shuffle and discovery endpoints 2026-10-12 17:44:53 -07:00
branderson 6781e2ae19 Merge pull request 'Keep players' game saves on their account' (#5) from account-saves into main
Reviewed-on: branderson/flashpoint-web#5
Reviewed-by: branderson <brad@branderson.io>
2026-10-12 16:37:13 -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 0980b789d5 feat(saves): store game saves on the player's account
Adds game_save_snapshots and /api/game-saves for copies of the saves games
keep in a player's browser. An upload matching the current save is a no-op,
the newest 10 snapshots per user and game are kept as backups, and saves may
be up to 10 MB, so the router parses its own bodies past the app's 1 MB JSON
limit.
2026-10-12 16:17:09 -07:00
branderson 62169a4ff7 Merge pull request 'Stop the I'm Feeling Lucky button animating' (#4) from calm-lucky-button into main
Reviewed-on: branderson/flashpoint-web#4
Reviewed-by: branderson <brad@branderson.io>
2026-10-12 02:18:18 -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 a1f53c5d1e fix(auth): read a repeated trusted header as one value, refuse conflicts
A proxy that sets the username header twice, as when Authentik's nginx
snippet and the documented lines are both present, made Node join the values
into "brad, brad" and create an account of that name instead of signing in
brad. Read each occurrence separately: identical repeats count once, and
conflicting ones are refused. The docs now note Authentik's snippet already
sets the headers.
2026-10-12 01:59:31 -07:00
branderson 561e1e641a Merge pull request 'Stream Flash games with the original Flash Player' (#2) from flash-streaming into main
Reviewed-on: branderson/flashpoint-web#2
Reviewed-by: branderson <brad@branderson.io>
2026-10-12 01:45:39 -07:00
branderson 98b2f1a0bb docs(auth): document trusted header login 2026-10-12 01:05:40 -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 057192be16 feat(auth): sign in users vouched for by a trusted proxy header
With TRUSTED_HEADER_USERNAME and TRUSTED_PROXIES set, POST
/api/auth/trusted-login signs in the user an authenticating reverse proxy
(such as Authentik forward auth) names in that header, believing it only on
connections from the proxy addresses. Users are matched by username, so an
existing account is adopted; otherwise one is created, the first as admin
as with registration. New accounts get a random password, and a user the
proxy vouches for can set their own password without the current one, to log
in when reached without the proxy.
2026-10-12 00:55:29 -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 3b4b9623f8 Merge pull request 'Stream games that can't run in a browser' (#1) from stream-sessions into main
Reviewed-on: branderson/flashpoint-web#1
Reviewed-by: branderson <brad@branderson.io>
2026-10-11 01:02:18 -07:00
branderson 5c5b11722d docs(streaming): document the session manager settings 2026-10-11 01:01:27 -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
branderson f59621dc82 feat(streaming): report and start streamed sessions for games that can't run in a browser
Games on other platforms (Java applets first) can be played through a
session manager that streams them from a server-side container. When
STREAMING_API_URL and STREAMING_API_TOKEN are set, game details and launch
data report whether a game is streamable, and POST /games/:id/stream-session
starts a session.
2026-10-11 01:01:27 -07:00
Darkraise c12255c3c6 docs(auth): document guest play and launch permission
Guests now hold games.play when guest access is enabled, and
GET /api/games/:id/launch requires games.play.
2026-09-28 12:33:58 +07:00
Darkraise 2e9e9490b5 fix(games): require games.play to launch a game
GET /api/games/:id/launch only checked games.read via
validateSharedGameAccess, so an authenticated user whose role lacked
games.play could still mount and download games through the API.
2026-09-28 12:09:09 +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
DarkraiseandGitHub 837415a3dd chor: add demo link and registration details
Added demo link and registration information to README.
2026-08-13 22:45:20 +07:00
Darkraise ba62cc91ea docs(ruffle): mention persisted Ruffle in operator docs 2026-08-13 17:49:42 +07:00
Darkraise 6649757c5d fix(ruffle): bust cached loader URL for the header fix 2026-08-13 17:49:34 +07:00
Darkraise 0ce39da805 docs(ruffle): document the persisted install path 2026-08-13 17:31:04 +07:00
Darkraise a4b717e9be fix(ruffle): serve the persisted install ahead of the bundled one 2026-08-13 17:22:24 +07:00
Darkraise f746bb2f81 feat(ruffle): seed a fresh data volume from the bundled copy 2026-08-13 17:15:50 +07:00
Darkraise 9b28abc8b9 fix(ruffle): point cleanup assertions at the persisted data dir
The updateRuffle describe block still checked distDir for leftover
ruffle-backup/ruffle-temp directories after Task 1 moved the install
target to dataDir. Those checks were passing vacuously, and the
EACCES injection in "completes when the staging directory cannot be
removed after copying" never matched the real removal path.
2026-08-13 17:09:33 +07:00
Darkraise 138e023b47 fix(ruffle): install onto the persisted data volume 2026-08-13 16:58:53 +07:00
Darkraise 27ee8b5829 docs(ruffle): plan persistent install implementation 2026-08-13 16:47:15 +07:00
Darkraise f57615f4bb docs(ruffle): spec persistent install location
Ruffle updates land in the container writable layer and are lost on
recreate. Specifies installing into the persisted /app/data volume,
seeding from the bundled copy, and serving from there.
2026-08-13 16:41:54 +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 1bd1f2afb2 test(updates): cover app update caching and failures
Also close a gap found in review of Task 3: a failed check recorded no
timestamp, so every call during a GitHub outage or rate limit fired a
fresh request that blocked up to the 10s timeout, defeating the 60s
forced-refresh floor exactly when it mattered. Failures now record
lastFailedAt and are subject to the same floor as forced refreshes,
regardless of the force flag, and a success clears it.
2026-08-12 23:56:01 +07:00
Darkraise 00c290892a feat(updates): compare the app version against the latest release 2026-08-12 23:47:11 +07:00
Darkraise 1e974d3664 feat(config): report the running app version 2026-08-12 23:40:46 +07:00
Darkraise c4e80398f6 feat(utils): add release version comparison 2026-08-12 23:35:28 +07:00
Darkraise 745d48128d docs(plan): implementation plan for the update check
Six TDD tasks: version comparison, the server-side version, the service,
its caching and failure behaviour, the endpoint and client, the card.
2026-08-12 23:32:28 +07:00
Darkraise b363fb294f docs(spec): revise the update check after review
A Fable review checked the spec against the code and found three things
that would have shipped wrong: the response envelope was copied from the
wrong router, the forced-refresh path could exhaust the shared GitHub
quota, and a 502 on total failure would double up with the axios
interceptor's global 5xx toast.

Answer 200 always with a lastCheckFailed flag, hold in-flight fetches so
a cold cache costs one call, floor forced refreshes at 60s, strip a
leading v from both sides, and reconcile the new server-side version with
the build-time one the General tab already shows.
2026-08-12 22:36:18 +07:00
Darkraise 9c532f8b6c docs(spec): design the web app update check
Notify-only card on the Update page: compare APP_VERSION against the
latest GitHub release, show the changelog and the upgrade command, and
stay quiet on development builds.
2026-08-12 22:18:17 +07:00
Darkraise cfb6c7c194 fix(preferences): keep the settings of a sparse preferences.json
Ultimate ships preferences.json without gameDataSources, and validation
treated a missing key as fatal. The whole file was discarded and replaced
with Infinity-shaped defaults, so that edition ran on hardcoded paths,
kept lastLoadFailed set - which drops the cache TTL from 60s to 5s - and
logged an error on every reload.

Normalize instead of validate: fill in the two fields the rest of the app
relies on, pass everything else through as written, and reject only a
file that is not an object. A malformed data source entry is dropped with
a warning rather than taking the file down with it.
2026-08-12 21:37:40 +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