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.
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.
Creates a streamer container per player on flashpoint-web's request, sized
to the game's applet, proxies its Selkies stream under /stream/<id>/, and
removes it when the player disconnects. Saves under /config/saves are
seeded from the browser at start and pushed back over server-sent events.
The JSObject stand-in kept cookies in memory, so saves were lost when the
game restarted. Write them to a file the session manager can sync, with the
policy granting only the agent jar access to that directory.
A Java agent removes them when the viewer window opens, so the stream
shows only the applet. JSObject moves into the agent jar, which puts it on
the class path applets load from.
appletviewer has no netscape.javascript.JSObject, so applets that touch
JavaScript fail to load, and Java 8 denies applets the URLPermission they
need to fetch through an HTTP proxy. Add a JSObject stand-in with a working
document.cookie and a policy granting HTTP URL access.
Runs a Flashpoint game natively in a container and streams it to the
browser. fp-launch follows the Linux launcher's sequence: start the game
server, mount the game's zip, and run startJava.sh through the proxy.
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.
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.
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.
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.
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.
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.
- 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
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.
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.
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.
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.
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.
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.
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.
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.
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.
The mkdir ran unguarded at module load, so an uncreatable log directory
threw during import and took down server startup. The logger already
handles the same situation by falling back and continuing.
syncAndReload runs on every Launcher write to flashpoint.sqlite, and a
synchronous copy of a multi-hundred-megabyte file froze every in-flight
request for its full duration.
TEMP_DIR was built from process.cwd() plus 'backend', but the container
already runs from /app/backend, so it resolved to a doubled path on the
container's writable layer. Downloads of up to 500MB staged there
instead of a mounted volume, and the mkdir fails outright with EACCES
when PUID moves the app user off the image build UID.
Anchor it to a configured path under /app/data and sweep files a crash
left behind at startup, with an age guard so a second container sharing
the volume cannot delete a download that is still running.
preferences.json, playlist JSON and the cached images all live on the
mount the Flashpoint Launcher reads and writes, and all were rewritten
in place. An interruption leaves a torn file: for preferences.json that
corrupts the desktop app's own config, and a truncated cached image is
served forever because both the proxy and the asset downloader treat any
existing file as valid.
Add a shared helper that fills a sibling temp file and renames it into
place, with a unique per-call suffix so two writers cannot collide, and
a short retry for the Windows and SMB case where another process holds
the target open. It never falls back to writing in place, which would
silently reintroduce the torn-write window.
FileImporter copied straight to the final path, so a size mismatch or a
crash mid-copy left a truncated ZIP that nothing removed. The mount path
finds any {gameId}-*.zip by prefix and mounts it without verifying the
hash, and because a file exists the re-download branch never runs, so
the game becomes unlaunchable until someone deletes it by hand.
Copy to a .part file in the destination directory, verify the size, then
rename into place, and unlink the partial on failure. Import under the
launcher-convention filename the caller already computed instead of
stamping Date.now(), which hid packs from the direct-path lookup and
left one copy per download attempt on disk.
When a mount fails, hash the ZIP against its recorded sha256 and move it
aside only on a genuine mismatch, then retry once. A hash match means
something else failed and the file is left alone: the Launcher writes
this directory too. Recovery is opt-in so the bulk startup mount cannot
turn a transient storage fault into a library-wide re-download.
Updating Ruffle failed in Docker with "EXDEV: cross-device link not
permitted" because overlay2 cannot rename a directory that still lives
in a read-only image layer, even within one parent directory.
Replace the directory renames with a rename-then-copy fallback, and
empty rather than unlink a path that turns out to be a mount point, so
a volume mounted over the install directory also works. Verify the
copied tree against the extracted one before dropping the backup: an
interrupted copy can leave ruffle.js in place while the rest is
missing, which a bare existence check cannot detect.
Also chown the served frontend directory when PUID moves the app user
off the image build UID, or every write there fails with EACCES before
any of this is reached.