Release history
Changelog
Every new feature, fix and speed-up on s&box Explorer, newest first.
- Last 30 days
- 90
- Features shipped
- 233
- Fixes
- 218
- Days with changes
- 66
20 changes in October 2026 Clear filters
October 2026
5
-
Feature
9:49 AM
One default profile layout for every user
Details
Drop per-user layout editing (profile-layout API, drag handles, reset); keep bio, social links and featured badges as widgets. Two-column band grid, balanced columns, hide empty Packages, cap Achievements to 6 games with masonry cards and skip full lists for games over 200 achievements.
2fe1896 -
Feature
8:20 AM
Rebuild the Settings page as a sidebar workspace
Details
Replace the tabbed three-column form with a sticky section nav and full-width content. Themes get CSS-rendered previews from palettes the themes endpoint now reads from each theme file, with search and dark/light filters. Language gets a searchable grid. MCP gets stats, a copyable endpoint and compact key management. New strings live in locales/en/settings.json.
Fixes found while auditing:
- delete_account now verifies the typed account name server-side
- account deletion also removes claimed_orgs, org_claim_requests and mcp_api_keys
- saving a theme merged nothing and reset other settings (including Steam presence) to defaults; it now merges with stored settings
- mcp-keys-api rotated the CSRF token on every call, breaking a second create/revoke; it now accepts the X-CSRF-Token header
- MCP key and rate limits are constants shared by the page and server
- setTheme accepts reload:false to apply without a page reload
763aeb8 -
Feature
7:47 AM
Redesign header and footer, rebuild themes around tokens, add eleven game themes
Details
Footer: three flat tiers (link groups, stats strip shown only when the status API is OK, legal bar). Social buttons and stat tiles are flat fills, and the language and support buttons no longer inherit hard theme shadows. Project and Community headings are translated in all locales.
Header: rebuilt from nav-config.php with a Discover dropdown, search trigger, user menu and mobile toggle, driven by navigation.js. Header loop variables are prefixed nav so they no longer clobber page variables such as the $link closure in /changelog. Header menus close and return focus when a button inside them is activated.
PWA install: the floating install button is replaced by an Install app entry in the Discover menu's Tools group. It stays hidden until beforeinstallprompt fires, prompts only on click, hides after use and on appinstalled, and never shows in standalone mode. On iOS it toggles an Add to Home Screen hint. The update toast styles now inject with the toast and use theme tokens, and the slideIn keyframes used by two other toasts move to base/animations.css.
Themes: all 32 existing themes set tokens on html.<name>-theme instead of overriding components. Game Boy, Rust and High Contrast are rebuilt, the retro themes lose broken effects, AMOLED is flat colour, and the Game Boy sub-stylesheets are folded into one file. Eleven game-inspired themes are added (Fallout 76, Starfield, Blueprint, Control, No Man's Sky, Aperture Science, Red Dead Frontier, Mirror's Edge, Subnautica, Windows XP, Black Mesa), bringing the total to 43. Blueprint uses opaque surface tokens, a quiet canvas-only drafting grid, corner ticks and a solid tooltip.
Package page: the header kept three columns until 760px, so wide action buttons squeezed the title to one word per line near 768px. It now stacks the actions below 1000px.
Cleanup: changelog commit hashes use 7 characters, the admin .user-avatar rule is scoped to .user-profile-header, Global Stats credit links use --link for light-theme contrast, and obsolete footer and header rules are removed from the shared sheets.
Known and left alone: 311 declarations use the invalid rgb(var(--color-*-rgb) / x%) form site wide.
a83efd5 -
Feature
3:32 AM
Restructure Global Stats into Overview, Activity and Games views
Details
The page was ~6,000px of stacked bands, four near-identical bar charts and a full-width hero plus card grid. It is now three views sharing one shell, with each view loading only its own data.
Views
- Overview: live and last-24-hours stat rows, the live player chart, one daily chart with a metric selector (players, new, returning, returning share) and a compact most-played / rising leaderboard.
- Activity: growth stats with period deltas, longer online history, the daily chart with extra metrics (editor users, hours, peak), the retention curve (folded in from a standalone view that held a single chart) and the every-day table.
- Games: sortable, searchable, tag-filterable table with Show more, and the sbox.watch rising list.
Navigation
- Tabs are real links (?view=), enhanced to swap the panel from a ?partial=1 fragment with pushState, popstate and arrow/Home/End key support. The fragment is sent with X-Robots-Tag: noindex. Without script the links and a <noscript> rule still give the full game list.
- Each view has its own canonical, og:url and description. meta-tags.php takes an optional $pageMeta['canonical'] (a path with a single leading slash); pages that do not set it keep the existing strip-the-query behaviour.
- ?view=retention 301s to Activity's #retention section. Activity and Games are in the sitemap generator.
Data
- New players and sessions "today" do not exist upstream (history ends yesterday), so the page shows 24-hour totals and yesterday's new and returning players with the date. Returning share is returning / players per day.
- Rising is fetched once at the largest size and sliced for Overview so the views share one cache entry. The cached game list drops summary and orgTitle, which only the removed hero read.
Charts
- PlayerHistoryChart removes its resize listener once its canvas leaves the DOM, which tab switching would otherwise leak.
- The daily chart tooltip sits above the hovered bar, sizes to its content so it no longer wraps at the right edge, is frosted with a backdrop blur at the owner's request (a deliberate exception to the page's no-blur rule), and works on touch: a tap shows it and it lingers after the finger lifts.
- Stat values use the --fs-heading and --fs-figure tokens.
Tests
- Vitest covers filtering, sorting, the games table, the daily chart and touch. test_global_stats_grid and the new test_global_stats_meta cover the server side: view resolution, daily series, canonicals and descriptions.
Removes the unused platform-history view and the old page stylesheet.
748ddd4 -
Feature
2:26 AM
Use unused Facepunch and sbox.watch fields, load largest thumbnails
Details
Global Stats: platform reach row (unique players 24h/7d/30d, sessions, playtime) from stats/platform, and returning-player and editor-user charts, KPI and table columns from stats/platform/history.
Package page: Recommended Players and Play Modes in config details from Version.Extra, and the source stat on achievement cards.
Org page: all-time totals card from sbox.watch analytics/organizations.
thumbSources() now always returns the largest variant (Square 512, Wide 910) and no longer takes a target width, so org cards and other square thumbnails are no longer blurry.
7d69f14
2
-
Feature
12:49 PM
Api-list-diff --quiet so cron only mails on change
Details
cPanel cron mails any output, so staying silent when nothing changed turns the weekly run into a change alert.
5fe8e96 -
Feature
12:47 PM
Add api/list snapshot diff tool
Details
Fetches the Facepunch api/list and reports added, removed and parameter-changed endpoints against a saved baseline. Whitelisted in .gitignore so the deploy ships it for the server cron.
a866144
13
-
Feature
10:30 PM
Add Rising today, daily gains and server links from sbox.watch
Details
Global Stats gets a Rising today band: the ten games gaining the most favourites and upvotes in 24 hours, from the dashboard endpoint sorted by change24h. sbox.watch redirects that endpoint unless it is requested as sort then pageSize with no kind, so the request is built in exactly that order. Cached for 15 minutes like the other digests.
Game pages show Favourites today and Upvotes today in the Player activity card, and each live server links to its sbox.watch page. Only canonical https://sbox.watch/servers/... addresses are kept; anything else (other hosts, javascript:, look-alike domains, trailing newlines) is dropped.
Unit tests cover the rising normaliser (bad idents, negatives, clamping, maps excluded), momentum parsing and the server link filter.
56bc7af -
Feature
9:57 PM
Rebuild the changelog page as a readable release history
Details
The page was a raw commit log: ten commits a page (splitting days), a commits-per-hour joke, a stats wall with a one-person contributor list, a Last updated line that never rendered, and a 2,262 line stylesheet inside the global bundle.
It now has a hero with a Latest card (the newest day's best changes), a four tile summary, a Highlights default view (features, fixes, performance, security) with Features, Fixes, Performance, Maintenance and Everything filters, search, clickable month columns that show quiet months as zero, and pages of eight days so a day is never split. Commit bodies become paragraphs and lists behind a native Details toggle; no JavaScript is needed. Filtered and paged URLs are noindex; legacy ?type= links still work.
Filtering, grouping, search, input validation and body parsing live in ChangelogFeed with unit tests. The stylesheet is a small per-page file and no longer part of the bundle. Radii are 4px at most, including the credits page role label.
The generator now maps the PatrickJnr git identity to Patrick Jr., so the stats show one author instead of two.
c5f4b33 -
Feature
9:37 PM
Rebuild the credits page and correct stale About and Legal text
Details
Credits is now About, Built by, Data & services, Technology, Thanks and an ownership statement, driven by backend/includes/credits_data.php. It no longer claims the project is open source, MIT licensed or open to contributions (the repository is private). Removed Redis, Fira Code, the build and lint tools and other things production does not use; added sbox.watch (with its attribution), sboxskins.gg, Ko-fi, Cloudflare, the front-end libraries and fonts that are actually loaded, and the profanity word lists with their licences (one is CC BY 4.0).
About: dropped the Redis, 'sub-200ms', '10-50x' and '10 specialized classes' claims and replaced the load-time tile with the language count. Legal: third-party list now covers advertising, Cloudflare, Ko-fi, sbox.watch and sboxskins.gg; the source code is described as private; log retention is 30 days (what the rotation script does, not 90); the IP wording says addresses are stored in full and only masked when displayed, as production has no SALT set. Last-updated date and version history brought up to date.
Translations: stale strings were removed from the 29 other locales so the corrected English shows instead, and the unused credits strings (including the old open-source ones) were deleted from all 30.
908a2af -
Feature
8:52 PM
Add sbox.watch analytics, organization stats and small-game chart bounds
Details
- Player activity card on game pages: rank, average and peak against the previous period, hour-of-day strip, update impact, session length, new and active players, 30 day approval trend and new vs returning players per day. Only a compact digest of the 200 KB analytics report is cached (15 min), and views tolerate digests cached before the new keys existed.
- Activity card on organization pages (peak 24h, members, last game update).
- Hourly averages keep two decimals so small games are not flattened; the chart uses decimal ticks under 4 players and draws sbox.watch history for those games as exact average plus hourly blocks up to the rounded-up peak, with the tooltip saying 'peak at least N'.
67bfac8 -
Feature
7:56 PM
Restore long-range player history and add live servers from sbox.watch
Details
Integrates two operations of the public sbox.watch API (no key, no published licence or rate limit), server side through a new SboxWatch client.
activity-history gives platform and per-game history from before Explorer's collector existed, back to September 2025, which refills the Global Stats chart and the game Player History widget and adds 1y and All ranges to Global Stats. Our own data stays authoritative: the history API only uses sbox.watch when its series reaches meaningfully further back than ours (more than 10 percent of the range, at least an hour, or when we have nothing), one source per response, and says which in source and attribution. 90d maps to their 1y trimmed to 90 days. As the collector accumulates, sbox.watch is used for fewer ranges on its own.
packages/{game|map}/{ident} gives liveServers, which Explorer has no equivalent of, shown as a Live servers card on game and map pages. Playtime, users and sessions are not repeated; Facepunch's UsageStats already supply them. Dashboard, search, organisations, analytics and the server directory overlap existing data or would need new pages and are left alone; docs/SBOX_WATCH.md records why.
Caching is CacheManager::getOrSet per key (lock plus early expiry) with a seven day last-good copy as the stale fallback: history 5 min (24h), 10 min (7d), 30 min (30d), 6 h (1y, all); packages 5 min; a 404 is cached for the same TTL. Resilience: 3 s timeout, no retries, no redirects, 1.5 MB cap, a circuit breaker (3 failures pauses upstream for 2 min) and a budget of 30 upstream requests a minute site-wide, so arbitrary valid idents sent to the history API cannot be used to hammer them. The client builds sbox.watch's canonical URL (the default range=24h is omitted and range precedes game); anything else gets a 301 that the client deliberately does not follow.
Data is validated before use: idents (anchored with \z), allow-listed ranges and kinds, a fixed host, numeric points capped at 2000, server names stripped of control characters and clipped, escaped at output; addresses and upstream URLs are never stored or shown.
'Data provided by sbox.watch' links to sbox.watch under the Global Stats chart and the history widget when the series came from them (with a note when partialCoverage means older totals may omit games), in the Live servers card, and on the Credits page.
Tests: test_sbox_watch.php covers the normalisers and the source rule; test_sbox_watch_cache.php runs the client against a mock upstream that also enforces the canonical URL (hit, miss, 404, stale fallback, breaker, timeout, redirect, size cap, budget, and five concurrent processes causing one refresh); two chart tests cover the source being passed through. Checked locally against the live API and with sbox.watch unreachable.
c247d50 -
Feature
7:25 PM
Play button that launches the game through Steam
Details
Game package pages get a Play button next to View on sbox.game. It links to steam://run/590830//-rungame org.package, the same launch sbox.gg uses, and lets the browser hand the steam:// URL to Steam, with no dialog or custom launcher. It is drawn only for packages of type game and only when the identifier is valid, and it is hidden on touch-only devices (hover: none and pointer: coarse), where Steam cannot launch it.
steamLaunchUrl() in helpers.php builds the link and validates through PackageValidator, which is also what the API uses. That validator's $ anchor accepted a trailing newline, so 'a.b\n' passed; it now anchors with \z. The s&box app id lives in one SBOX_STEAM_APP_ID constant, and the three places that carried 590830 as a literal (auth callback, steam_functions twice, the inventory URL) use it. The English label is common.play; other locales fall back to English.
test_steam_launch.php checks the exact URI for pcjr.emojislots and facepunch.sandbox, more valid ids, 22 malformed or hostile ones, the view wiring, the touch-device rule, and that the app id literal stays in constants.php.
38a5e14 -
Feature
6:46 PM
Index plus per-section sitemaps for games, orgs, news and jams
Details
sitemap.xml is now a sitemap index over sitemap-pages, -games, -orgs, -news and -jams. Games list every public game as /org/ident/ with its Updated time as lastmod (2007 of them), orgs are the ones that have a listed game (1174) and take their newest game's time, news is the platform posts and jams the current jam idents. Pages gain /forum and /ugc/charts/game|map, which were routable but missing.
Each section is built independently. If an upstream fetch fails the earlier file for that section is kept and still listed, and a section with no data and no earlier file is left out of the index, so an API outage at deploy time no longer ships a sitemap with holes. Files are written atomically, split at 50000 URLs into numbered files, and stale chunks are removed. Idents are validated, private games skipped, and the type list keeps its existing filter.
--verify checks every static page and a sample of each dynamic section against a running server. --out is now a directory. Generated sitemap-*.xml files are gitignored like sitemap.xml.
test_sitemap.php runs the generator against a mock API: sections, filtering, lastmod, and the outage fallback.
3a68fc8 -
Feature
6:33 PM
Add sbox_docs refresh, and fix clearCache missing the cached pages
Details
sbox_docs gains action=refresh with target all|docs|api. It clears the cached doc pages and index, re-reads the index, and re-downloads the API schema when asked. It needs an admin per-user key or the master key, is limited to one run per five minutes across all callers, and writes a refresh_docs audit entry (admin_id 'master' for the master key).
DocsService::clearCache built its keys from the full link path (/dev/doc/x.md) while fetch() caches under the slug (x.md), so the existing admin clear_doc_cache never removed a cached page, only the index. It now strips the prefix. McpAuth::isMaster() records that the request used the master key, since that key has no Steam identity and mcpIsAdmin() is false for it.
test_docs_refresh.php covers the key fix and the cooldown.
9c9bcd5 -
Feature
4:21 PM
Let a page set its own title, description and keywords
Details
Pages with no package, profile or post to look up all shared the homepage's description in search results and link previews. A page can now set $pageMeta = ['title' => ..., 'description' => ..., 'keywords' => ...] before including meta-tags.php.
50fcedf -
Feature
4:21 PM
Add /stats/live for the platform counts the page polls
Details
Global Stats polled /stats every thirty seconds for three numbers. That endpoint also fetches and sums two hundred games, and reports a smaller "estimated" total when stats/platform fails, which the page then wrote over its own. /stats/live returns just the platform's players, editor users and 24-hour peak, cached for thirty seconds, and answers 503 rather than an estimate when the platform call fails.
57deea9 -
Feature
4:21 PM
Round axes, calendar ticks, touch tooltips, silent refresh
Details
The y-axis split the data's own maximum into five, so a 556-player peak labelled its gridlines 111, 222, 334, 445. It now steps on 1-2-5 (0, 200, 400, 600), whole numbers only.
The x-axis placed evenly spaced labels from the first sample to the last, so the 24h chart read "04:06 PM ... 04:06 PM" at its two ends. Ticks now sit on calendar boundaries in the viewer's time zone (6 PM, 8 PM, midnight labelled with the day), stepping through hours, days and months to the largest that fits the width.
The tooltip listened for mouse events only, so a phone had no tooltip at all. It now uses pointer events; a touch keeps its tooltip for a few seconds after the finger lifts, since the pointer leaves the instant it does.
load(null, { silent: true }) is the periodic refresh: no spinner, no draw-in animation, and a failed refresh leaves the chart already on screen alone.
Both consumers (the per-game widget and /global-stats) get all of this.
cb57ee1 -
Feature
3:01 PM
Add the collector, aggregator and schema that were never in the repo
Details
The admin page and scheduled-refresh both spawn tools/collect-player-history.php and aggregate-player-history.php, but /tools/* is gitignored and neither file (nor any table definition) was ever committed, so nothing in the repo could create or fill the data the API and charts read.
- PlayerHistory owns the schema (with unique indexes), the sweep, the writes and the roll-up
- a run shares one minute, so a game missing from a run that happened is zero and a missing run is a gap
- an incomplete sweep records nothing instead of dropping every game on a failed page to zero; one collector at a time; skipped inside the minimum interval so cron and scheduled-refresh cannot double-sample
- hourly/daily averages divide by the runs that happened; pruning is aligned to whole hours/days so a half-pruned hour is never rolled up wrongly
e71f447 -
Feature
1:41 PM
Expose user leaderboard scopes and jam entry lookups
Details
Add package/leaderboard/{v}/{ident}/{board}/u/{userid}/{scope} and jam/{ident}/entry/{package} behind validated path builders, with REST routes and docs. The authenticated Facepunch endpoints stay unwired.
ae1cec6
Generated from the project's commit history whenever the site is deployed. Times are server time.