All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog.
Cache-Control: immutablehas been removed from all static file routes (/roms/,/bios/,/covers/,/manuals/,/cache/igdb/,/emulatorjs/). Browsers can now recover from cached-but-broken assets via a hard refresh; previously,immutableblocked revalidation even on hard refresh and a broken bundle could trap clients on stale bytes. The long-cachemax-ageis also cut from 365 days to 24 hours to bound staleness.- All long-cache static routes now share a single
longCachedirective (public, max-age=86400). The previous distinction betweenlongCacheImmutable(ROMs, BIOS, EmulatorJS) andlongCacheMutable(covers, IGDB cache, manuals) is gone. /api/game-detailsis now wrapped innoStoreso its error responses (404 for missing console/rom, 400 for invalid parameters) carryCache-Control: no-storerather than no header. The success path retains itsprivate, max-age=300override.
- Panic-recovery 500 responses now strip
Cache-Control,ETag,Last-Modified, andContent-Encodingheaders before emitting the error. Pre-fix, a transient panic on a route wrapped in long-cache middleware would cache the 500 for the full max-age window — Go'shttp.Errordoes not clear these headers (only the unexportedhttp.serveErrorused byFileServerFSdoes), so the fix is to clear them explicitly via a sharedclearLongCacheHeadershelper.
- EmulatorJS dependency switched from the
chrisallenlane/EmulatorJSfork to upstreamEmulatorJS/EmulatorJS@v4.3.0-pre. The controller-port-device patches that power lightgun support were merged upstream (EmulatorJS PR #1182, RetroArch PR #38), so Freeplay no longer needs to ship a fork or force unminified asset loading viaEJS_DEBUG_XX. The player page now loads the standardemulator.min.jsbundle.
/emulatorjs/*responses now carry a version-stampedETagand no longer use theimmutableCache-Control directive. Browsers revalidate viaIf-None-Matchon every Freeplay release, so an upstream EmulatorJS bump can change file bytes at stable URLs without trapping clients on stale cached copies.
-portCLI flag overrides the port from the config file (0= use config value)LOG_LEVELenvironment variable for slog level (debug/info/warn/error, case-insensitive; defaultinfo; unrecognised values fall back toinfowith a one-time warning)- Request-log middleware emits one
slog.Infoper response (method, path, status, bytes, ms) - Panic-recovery middleware normalises handler panics to a 500 response
with a structured
slog.Error(method, path, panic value, stack) - Integration test suite under
internal/integration(build-tagged, run viamake integration) - Frontend↔server contract test for the save round-trip
(
frontend/contract_test.js) - Fuzz coverage for
datadir.PathInsideandigdb.safeIGDBInfoURL CONTRIBUTING.md
GET /api/saves/...now returns 5xx (was 404) when a save file exists but cannot be read. The frontend uses this distinction to refuse periodic-auto-save registration when a real save is present but unreadable, rather than overwriting itlibrary.Startperforms the first scan synchronously before HTTP serves traffic, closing a cold-start race window where legitimate save GETs 404'd and POSTs were silently dropped beforeMaxBytesReader- Frontend SRAM and state probes now branch on response status:
404 registers the periodic save (legitimate fresh game); any other
non-2xx leaves it unregistered and emits
console.errorso operators can correlate against the server-sideslog.Warn frontend/postSaveno-ops on empty buffers instead of overwriting, closing a save-loss path- Save-endpoint gate aligns with the URL slug convention via
HasGameSlug
details.writeNotFoundandensureCoverThumbnaillogatomicfile.Writefailures at warn level instead of swallowing them silently. Without these logs, persistent disk-full or permission errors caused unbounded IGDB re-fetches and permanent missing covers with no operator-visible diagnosticnoDirListingclearsCache-Control,Etag,Last-Modified, andContent-Encodingheaders before returning a 404 for a directory withoutindex.html. Pre-fix, browsers cached the 404 immutably for a year because the outercacheControl(longCacheImmutable, ...)middleware stamped the header beforehttp.NotFoundrangameDetailsFromIGDBskipsInvolvedCompaniesandPlatformsentries with empty names. Pre-fix, IGDB responses with an unnamed company subobject produced leading-comma artifacts ("Developer: , Capcom") on rendered detail pagesfrontend/app.jsrescan flow now clearsstatusPollTimerin both terminal branches ofpollCoverStatusand returns the inner promise so the click handler's.finallysafety-reset correctly waits for the poll cycle to settle. Pre-fix, a second rescan that hit a 409 or a network failure on the POST left the button stuck disabled in "Scanning…" until manual page reload- IGDB nil-fetcher initialisation no longer produces a typed-nil
interface that escapes the
c.fetcher == nilguard and would crash the background pipeline make testnow builds the binary before integration/contract tests in CI sodist/freeplayexists when the contract test runs
- Game details page with IGDB metadata, screenshots, and artwork
- Details page is now the default landing when clicking a game card
- Per-game PDF manual support with "View Manual" button on details page
- Local IGDB metadata and image cache (no repeat API calls after first fetch)
- IGDB game names displayed on library index cards
- Game metadata displayed as a table on details page (year, developer, publisher, platforms, series)
- Sticky header and toolbar on all pages
- Fuzz testing infrastructure (
make fuzz,make fuzz-long) - Accessibility audit target (
make a11y) - CSRF protection on state-changing endpoints
- Benchmark suite and
make benchtarget for critical-path regression testing make build-debugproduces a debug binary with pprof on127.0.0.1:6060(gated behind//go:build debug; not present in production builds)
- Clicking a game card navigates to the details page instead of launching the emulator directly
- Page titles now use IGDB game names when available
- Improved typography and visual hierarchy on the details page
- Back-to-library link styled as a button on subpages
- IGDB screenshots and artworks now cache two variants: a
t_screenshot_hugethumbnail and at_originalfull-size image; wire format changed from[]stringto[]ImageRef{URL, ThumbURL}(legacy string arrays still accepted via backward-compatibleUnmarshalJSON) - Cache-Control split:
/covers/,/cache/igdb/, and/manuals/usepublic, max-age=31536000(noimmutable) so browsers revalidate after TTL expiry;/emulatorjs/,/roms/, and/bios/retainimmutable /api/game-detailsusesprivate, max-age=300to deduplicate in-session navigation requests; all other/api/*endpoints useno-store- Gzip compression middleware: text responses (JSON, HTML, CSS, JS, WASM)
are compressed when the client sends
Accept-Encoding: gzip; binary routes (save blobs, ROM files, images) pass through uncompressed - Details cache now keeps a per-process in-memory layer; steady-state rescans avoid per-game disk reads for already-cached entries
- Scanner preallocates the games slice from the previous scan count and uses
slices.SortFunc(reduced allocations on repeated scans)
- IGDB matching regression for games with diacritical characters in titles
- Paragraph breaks in IGDB text now render correctly on the details page
- WCAG 2.2 Level AA accessibility issues across all pages (contrast, focus indicators, ARIA attributes, semantic structure)
- Multiple bugs found via proactive bug hunt and security audit
Initial release.
- Browser-based retro gaming via EmulatorJS
- ROM scanning with configurable console definitions (TOML)
- Server-side save state and battery save persistence
- SRAM save loading from server on game start
- Cover art fetching from IGDB with platform filtering and name variant fallbacks
- Game library with console filtering, search, and favorites
- Autofocus on search box on page load
- Light/dark theme with system preference detection and manual toggle
- Gamepad navigation (D-pad, shoulder buttons, A/Start)
- Keyboard navigation (arrow keys,
[/]filter cycling) - Lightgun support (SNES Super Scope and others)
- BIOS file support for consoles that require it (e.g. PlayStation)
- Responsive layout for mobile, tablet, and desktop
- Single-binary deployment or Docker container with one volume mount
- Rescan endpoint with cover art download progress indicator
- Cache-Control headers:
no-cacheon frontend for immediate deploy pickup, immutable long-cache on EmulatorJS, ROMs, BIOS, and cover art - Performance:
deferscript loading, Silkscreen font preload, explicitwidth/heighton cover art images, O(1) cover-detection via directory map lookup
- BIOS configuration is now an optional
biosfield on each[roms.*]entry rather than a separate top-level[bios]section. SeeINSTALLING.mdfor the new format.