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
23 changes in September 2026 Clear filters
September 2026
1
-
Feature
10:08 AM
Exact verdict counts and a verdict filter
Details
The summary was worked out from the first 50 reviews. It now reads the exact Count for score 1, 2 and 3 from package/reviews, and the list can be filtered by verdict server-side with an optional score parameter.
score is validated to 1-3 and dropped otherwise. The count requests use take=2 because upstream rejects take=1 and omits Count.
04e57e8
6
-
Feature
11:31 PM
Share the winning game, and square up the labels
Details
A jam page shared on Discord or Twitter used the site's default og-image, so Game Jam 1 and Tech Jam were indistinguishable in a link preview. meta-tags.php already honours an image for a news post; the jam branch now does the same, and game-jams.php hands it the wide art of the jam's first winner with an alt of "My Summer Cottage: Tech Demo - Game Jam 1 winner". A jam with nothing awarded yet, like the running one, keeps the site image.
The hub cards said "Prizes" where the feature card and every detail page said "Prize pool". They all say "Prize pool" now.
The category badge and prize chip on a winner card sit over whatever artwork the entry shipped. At 65% black the white text could fall under 4.5:1 on a bright thumbnail - Terrain Pro's is a rainbow - so the scrims are 82% now, which holds the text at roughly 13:1 whatever is behind it.
Measured the rest rather than assuming: every piece of text these pages draw clears WCAG AA against its composited background, the muted 11px labels included, at 5.2:1 and up.
2ea3f69 -
Feature
11:07 PM
Give the jam pages a podium, and the hub real cards
Details
The four jam detail pages all rendered their winners as one flat row list, so Game Jam 1's £10,000 first place looked exactly like its £1,000 fifth, and Tech Jam's ten equal categories looked like a ranking they are not. jamIsRanked() tells the two apart - a jam is ranked only when every awarded category is titled "1st", "2nd" and so on - and the winners now render through views/partials/jam-winner-card.php: a podium of three for a ranked jam with the runners-up smaller beneath it, or an even grid of category cards for a jam like Tech Jam. Each card carries the entry's wide art, its category and prize, its org and its player and favourite counts, so a finished jam's page finally answers "who won, and was it any good" without a click. The grids use auto-fill rather than auto-fit, or a two-entry runners-up row would stretch each card wider than the first place above it.
The hub listed its four jams as text rows that said less than the detail pages did. They are cards now - status, theme, dates, prize pool, entries, how many categories were won, and for a ranked jam the game that won it. The running jam keeps its feature card and drops out of the list below, which is headed "Other jams" while one is live. The hub header was a hand-rolled .jam-hub-head; it is the site's .page-hero now, like every other page and like the jam pages it links to.
The detail pages never showed when a jam ran. The facts bar leads with "Ran 16 Feb - 17 Mar 2024" (or "Runs", live), and the hero icon follows the state instead of showing an hourglass over a jam that finished two years ago.
Removed: the dimmed winner-art backdrop behind the hero. At 12% opacity under the announcement banner it read as a rendering fault rather than a flourish, and the winner cards already carry that art at full strength. On the live jam, the prizes-and-votes grid was two equal columns, which left a 665px hole under the short prizes card; the prizes column is capped at 22rem now and the vote tally takes the rest.
Also: the newer half of the stylesheet had drifted off the design system - var(--border-color) is not a token anywhere in this codebase, so every rule naming it was silently falling back to a hard-coded rgb(); text was dimmed with opacity instead of --text-muted; font sizes were rem values between the --fs- steps. All of it now names tokens. A winner card with no package URL renders as a div rather than an <a href="#">, and .jam-winner__jam meets the 24px touch target it missed at 18px.
Verified at 1400, 820 and 390: no horizontal overflow, no heading-level skips, no console errors, every image has alt text. Load-more still pages 24 at a time and the countdown still ticks.
305143b -
Feature
10:41 PM
Rebuild the jam hub and cut the live jam page from 954KB to 202KB
Details
Game Jam III rendered all 179 entries at once: 480 package cards, 589 images, 954KB and three and a half seconds. The community vote card added another 80KB by rendering all 118 entries with votes so that a details element could hide most of them.
Entries now render 24 at a time. The Show more link carries ?show= and works on its own, and a small script fetches the same URL, swaps the grid in and updates the button and the address bar so the page never reloads. Sorting by most played, most favourited, newest or A to Z is a link rather than a script, so it survives with JavaScript off and each order has its own URL. The vote card now lists forty entries at most and says how many more are on sbox.game.
The hub was a live jam card and a four-row list, which said nothing about the jams as a whole. It now opens with what the four jams add up to - £135,000 in prizes, 478 entries, 20 categories won - and a gallery of the twenty winners across every jam, each with its thumbnail, category and prize, newest jam first. Winners come from Categories[] .Winner rather than the entry lists, which is the only source that has all of them.
Jam pages link to the jam before and after them, so the four are browsable without going back to the hub each time.
New styles follow the 4px radius the rest of the site uses and the card bodies are padded, both measured rather than eyeballed: the winners gallery, entry controls and jam navigation all sit 17px off the card edge at 1400px.
15943a4 -
Feature
10:25 PM
Rebuild the forum browsing, cross-link it, and restructure Global Stats
Details
Forums. The index listed thread and post counts and nothing else, so a forum with 489 threads looked the same as one with 29. Each forum now shows its newest thread with who replied last, the page opens with the six most recent threads across every forum, and the header states the 2,635 threads and 12,417 posts the whole board holds.
Thread lists gained sort controls covering the four orders the API takes - latest reply, newest, most replies, most viewed - along with the author's avatar, when the thread started, and a last-reply column that links straight to the final page rather than the first.
Thread pages gained numbered posts with per-post anchors, a highlight and a badge on whoever opened the thread, an edited marker where the edit came more than a minute after the post, a back link to the parent forum, and a windowed pager at the top and bottom instead of bare previous and next links.
The forums are now reachable from the rest of the site: a top-level nav entry, the five newest threads on the homepage, and a card on player profiles showing threads that person started or replied to. There is no endpoint for a user's posting history, so that card reads the active thread listings and says so rather than implying it is complete.
One batch of ten calls feeds all three of those, cached for fifteen minutes. Only /forum itself is allowed to pay for a cold fetch; the homepage and profiles render from cache or skip their block, because Facepunch serialises concurrent thread listings and fourteen of them took fifteen seconds. The index and the homepage previously warmed two different cache keys, so the homepage block could never have populated.
Global Stats. Eight equal-weight metric cards opened the page with no sense of which mattered, and four unrelated sections followed with no headings between them. The page now leads with five headline numbers - players now, 24h peak, in the editor, players per day and new players, the last two carrying their week-on-week change - with catalogue size demoted to a single quiet strip. Everything below is grouped under Right now, Growth, Retention and Top games, with jump links in the header. Every id and class the page's scripts bind to is unchanged; the metric card styles they replaced are deleted.
Padding and radii. The new cards inherited no body padding, so icons and text sat flush against the card border, and the new sheets had invented pill shapes and 7 to 14px corners. Radii now come from --radius-sm like the rest of the site, which caps at 4px; the only exception is the 50% on avatars, matching the site's existing use.
b1d75e6 -
Feature
10:06 PM
Pool seven cohorts and draw the platform baseline behind each package
Details
A single day's cohort was too small to read: facepunch.sandbox's was 39 players, which put a 0.0% on both the day-7 and day-30 tiles and left gaps in the bars that looked like missing data rather than churn.
Retention now pools the seven daily cohorts ending 35 days back, which is the oldest window where every cohort still has a full day-30 point. Pooling is weighted properly - day N is the sum of returning players over the sum of cohort sizes, not an average of ratios - so sandbox reads 305 players instead of 39 and the curve stops flickering between zero and one player.
Each package curve now draws the platform curve behind it as a dashed line and each tile says how far above or below average it sits, which is the only way a number like 14.8% means anything. The chart gained a percentage axis, a flat marker for days nobody came back, and day labels every five days instead of thirty crowded ones.
The seven upstream calls go out in one parallel batch and cache for six hours, including the empty answers, so a package with no cohort pays the cost once.
Stat names are humanised from their raw idents, a prefix every stat in a package shares is dropped, and the card lists the four largest with the rest behind a disclosure. The old 'N more stats' link pointed at the package page, which shows the viewer their own stats - on someone else's profile it led nowhere useful.
332e6bf -
Feature
9:51 PM
Wire the five unused Facepunch endpoints into pages
Details
The api/list sweep turned up five public endpoints nothing on the site read. Four now render; the fifth is confirmed dead upstream and is not worth another attempt.
Forums. forum, forum/{slug}/threads and forum/thread/{id}/posts are fully public and had no presence here at all. /forum lists the 18 real forums grouped by category, a forum lists its threads, and a thread renders its posts with each author linked to their existing profile page. The index also carries 4,722 auto-created per-package forums (Parent "p"); those are filtered out of the index because almost all are empty, and surface instead as a Discussion tab on the package page, which only appears when that package actually has threads.
Post bodies are Trix HTML written by other people, so they go through a new sanitiseForumHtml(): a DOMDocument pass with a tag allowlist, an attribute allowlist of href/src only, http(s)-or-relative URL checks, and rel/target forced on links. Everything else, including the data-trix-attachment payloads and any class or event handler, is dropped. The posts endpoint returns no thread title and no forum, so listings cache each thread's title and forum as they are rendered and the thread page reads that back, falling back to an excerpt of the first post when nobody has browsed the listing yet.
Per-user package stats. package/stats/2/{ident}/u/{steamid} is public and returns a full per-stat breakdown. It fills a new In-Game Stats card on player profiles for that profile's three most-played packages, and a "Your stats in this game" table on the package page for the signed-in viewer. Values outside a readable range are shown as an em dash with a count of how many were suppressed: facepunch.sandbox reports damage_taken as 6.8e38, which would otherwise wreck the column.
Review by steam id. package/reviews/{ident}/{steamid} gives one user's review directly, so the package page can show the viewer their own review without paging the whole list.
Retention. stats/platform/retention and package/{ident}/retention share one partial. Two upstream quirks drive the shape of it: a day with no returning players omits Users and Retention entirely rather than sending zeroes, so bare {"Day":3} rows are read as 0% instead of being skipped, and the curve stops at day 30, so the default cohort is 35 days back to keep a day-30 milestone. Cohorts under 20 players are not drawn at all; facepunch.walker's cohort is one person and produced a flat 0% curve that looked like a bug.
Not wired: package/leaderboard/{v}/{ident}/{stat}/u/{id}/global and /country answer 200 with an empty body even where the user is rank 1 of 1, /friends is 401 and version 2 does not exist. player/{id}/feed is 404 for every id tried. The existing package/{ident}/leaderboard/{stat} call already returns what those promised.
formatRelativeDate() takes a prefix argument now; it hardcoded "Published", which reads wrong on a forum reply and on a stat timestamp. Existing callers are unaffected.
9b28316
2
-
Feature
8:58 AM
Carry the whole jam record onto package pages, cards and orgs
Details
The jam list already holds the theme, prize pool, entry count, deadline and full timeline, and jam/{ident}/voting holds the live tally. All of it was being thrown away after the one bit the badge needed, so the package page now shows the rest.
packageJamStanding carries the jam's facts and its current and next timeline step, worked out from dates rather than the Phase number, which has gaps. jamPackageRanks reads the package's position out of the tally by reusing jamTally, so competition ranking - two entries tied on twelve votes are both second and the next is fourth - is the same rule the jam page already uses.
The detail page gains a rail card - placing, theme, phase, what happens next and when, entries, prize pool - and its chip now reads "Game Jam III · 1st in Best Game" while voting is open. Package cards and org cards get a badge too.
The voting call is the only new upstream request, so it is made only when a package is actually in a live jam with community voting, and only by the detail page: fetchPackageJams takes withRanks, and the grids leave it off. The jam list and entry index are memoised per request, so a page of forty cards costs one lookup rather than forty. Measured: 0.004s warm for the detail page, 0.001s for forty cards. The jam page suppresses the badge on its own grid, where every card is an entry in the jam being viewed.
Live check: limestudio.roundbound reads 1st of 48 in Best Game on 40 votes, phase Building, next Nominations, theme One More Round, pool £50,000.
44bc01c -
Feature
8:46 AM
Show a package's game jam entry and any category it won
Details
The package payload carries nothing about jams, so membership has to come from the jam endpoints. Two sources are needed, not one: the entry search (package/find/2?q=jam:X) is incomplete - Game Jam III reports 76 entries but returns 53, and seven of Tech Jam's ten category winners are absent from it entirely, because winners were not always tagged as entries. Category winners are therefore read from the jam list itself, which is already fetched and costs nothing extra, and unioned with the search results. A package that won a category it was never listed in still gets its badge.
packageJamStanding is pure so tests/test_jams.php can pin the rules without the network; the ident->jams index is built once and cached for an hour, since entries only move while a jam is live.
Verified against the live API: limestudio.roundbound resolves to a Game Jam III entry, gbonn.surreal to a Tech Jam "Best Least" winner, and a package in no jam to nothing.
58d8d96
3
-
Feature
6:47 PM
Bring the search page onto the current design, and make it tell the truth
Details
The page had not changed since May, and several things on it were wrong:
- It advertised AND, OR, NOT, phrases and parentheses. Facepunch's search has none of these, so the syntax panel and the script that validated it (advanced-search.js, loaded on every browse page and used by nothing else) are gone.
- "101 results found" for car was a count of what had been grouped; the real total is 3,704. It read a total key the fetch never returned.
- It asked Facepunch for 500 packages per search, which takes ~10.7s upstream, to show 10 per type. It now asks for 200 (~1.7s).
The page now uses the browse layout: the same header, search box and type chips, the organization matches as a row (with an exact organization match first when the query is one), then one row of the standard package cards per type, each with "See all" into /t/{type}/?tags={query}. The number of cards follows the grid so every breakpoint shows full rows. ?type= goes straight to that listing.
That listing now says what it is: "Models matching "car"" instead of "Browse Models", and type names come from the translations, so /t/vsnd/ reads "Browse Sound Files" rather than "Browse Vsnds". Addons, collections, decals, entities, terrain materials and tutorials get English names.
search-results.css and advanced-search.css keep only the rules other pages still use.
09cea79 -
Feature
8:05 AM
Platform counts from Facepunch, and the last 90 days
Details
Global Stats worked out "Playing Now" by paging through every game - four requests, about 2.5 seconds - and adding up UsersNow. Checked against stats/platform that missed about 8% of players, everyone in a game with no public listing, and it had no way to see the editor at all.
Worse, the live counter that polls /api.php/stats every 30 seconds summed only the top 200 trending games, so two seconds after the page loaded its all-games figure was swapped for a smaller one. Both now read stats/platform, the platform's own count, with the old sums kept as the fallback and is_estimated saying which one a caller got.
The page gains "In the Editor" - 122 against 108 playing when checked, more people building than playing - and a 24h peak that is there on load rather than waiting for the chart script to fill it.
The new "Last 90 days" section is from stats/platform/history, which gives daily unique, new and returning players, peak concurrency and playtime. It shows the trend the old page could not: daily players down from about 5,500 in June to about 2,000, new players from ~450 a day to ~40.
- Two single-series charts rather than one stacked. New players are 2-8% of daily players and would be an invisible sliver on the same bars; a second y-axis would invent a relationship between two scales.
- Plain HTML columns, not a stretched SVG, so the rounded ends and axis text stay crisp. The whole column is the tooltip target, and every value is also in the table behind "Every day as a table".
- The range ends at midnight so the last bar is a finished day, not a partial one reading as a collapse. Week and month comparisons only ever compare complete windows of the same length, and say nothing otherwise.
- Phones get the last 30 days; 90 columns at that width are hairlines.
db1379a -
Feature
7:57 AM
Game jam pages, with the live vote and every past winner
Details
Facepunch's public API has carried jam data all along - timelines, prize ladders, winners by category and a live community vote tally - and nothing on the site read it. Game Jam III is running now (building until 25 Sep, results 3 Oct), so this is the moment the page is worth most.
/game-jams lists every jam and features the live one. /game-jams/{ident} shows the phase timeline with how far through the current phase it is, what each place pays or who won it, the community vote ranked, and every entry through the existing package cards.
Not /jam or /jams: sbox has organisations with both idents, and a route here would have shadowed their pages. /game-jams is free.
Some decisions the data forced:
- One upstream call serves both pages. The jam list is byte-for-byte what jam/{ident} returns for each jam, so the detail page reads from the list, and a real jam cannot 404 because a second call failed during an outage.
- Timeline state comes from dates, not the Phase number. The numbers have gaps (1, 2, 4, 5, 6, 7) and the older jams skip steps, but every step has a date.
- Seven of Tech Jam's ten winners were never tagged as entries, so they showed as bare idents. The missing winners, and only those, are fetched through the existing package batch.
- Entries are filtered by the jam's own Query rather than a string built here, so a change to how Facepunch tag entries is followed automatically.
- Vote ranks are competition ranks with ties ordered by ident, so the list does not reshuffle on every cache refresh.
- The public list can be shorter than EntryCount (44 of 64 for Jam III), and the page says so rather than implying the grid is everything.
fetchCachedEndpoint() is the fetch-fresh-else-serve-stale shape that fetchPackageStats already used, pulled out because every new endpoint wants it. The platform stats fetchers beside it are used by the next commit.
tests/test_jams.php covers the timeline, relative dates, ranking, ties, unlisted entries and prize shapes: 42 checks.
7cea2ea
5
-
Feature
10:52 PM
Actually load tippy, which the site has been styling for nothing
Details
tippy-init.js warned "Library not loaded" on every page. It was the one entry in theme-manager's vendor list with a real consumer, and emptying that list left the initialiser, frontend/css/vendor/tippy.css and 55 title attributes on a package page all shipping with nothing to drive them.
Loaded in the footer above tippy-init.js rather than through the vendor loader, because that one injects asynchronously and would race it; defer keeps document order. The plain UMD build, not -bundle, because -bundle only adds tippy's own stylesheet, which would land after ours and undo the skin. Popper is a separate global either way. Both pinned with integrity - they are third-party and executable.
tippy.css only ever held the skin, which nobody noticed because it was never exercised: with arrow: true the arrow needs the geometry from the core stylesheet or it does not render at all. That structure now sits above the skin in the same file, so the colours still win.
The .gitignore change is why that file was awkward to commit: `vendor/` matches at any depth, so it swallowed frontend/css/vendor along with Composer's directory - the explicit fontawesome line below it was a workaround for the same thing. Anchored to the root it actually meant.
Verified in Chrome against a package page: tippy and Popper both defined, 65 elements initialised, the native title removed so tooltips do not double up, and the box measured 97x36 with a 4px radius and a drawn arrow.
7791f18 -
Feature
5:41 AM
Leaderboard and profiles on the v2 surfaces, and what the sweep found
Details
The leaderboard is reworked and profiles move onto the shared surfaces, finishing the v2 pass over the public pages.
Fixes found while sweeping the result: the news list claimed the feed had ended while posts were still unreachable, and its layout is now one wide post over rows of three as the live site has it; one page width throughout; a brand blue that actually passes AA; the no-radius-over-4px rule enforced rather than just stated; media named as upstream sends it, so a 404 upstream stops being reported as ours; the award index unstuck; and every page given a real title.
ca5cdc8 -
Feature
5:41 AM
Make v2 the default, and bring charts, home and global stats onto it
Details
v2 stops being opt-in. The package rail gets one inset rather than several competing ones, and its changelog link goes somewhere real.
The chart pages, the homepage below the hero, and global stats are rebuilt on the same card language, so the four highest-traffic pages now read as one site.
65e9fe4 -
Feature
5:41 AM
The same header and the same width on every page
Details
Pages disagreed about where content starts. Every page now shares one header and one measure, and the tops line up rather than each page beginning at its own offset.
The news article pages are rebuilt on the v2 language and the news list is paged instead of loading everything at once.
Releases 2.0.0.
e02774a -
Feature
5:41 AM
One type scale, one radius scale, and a shared component kit
Details
The site had grown several competing sets of sizes, weights and corner radii, so two pages built a month apart did not look related. This introduces the kit every later page is built from: a single type scale, a single radius scale, and shared surface tokens.
The package and organisation pages are rebuilt on it as the first v2 view, and every public page is then measured and corrected against it rather than adjusted by eye.
b2f033f
6
-
Feature
4:29 AM
Replace every native confirm() with a styled dialog
Details
35 destructive actions were guarded by window.confirm(), which cannot show what is about to be deleted, looks different in every browser, and can be suppressed after a few dialogs, quietly turning a guarded action into an unguarded one. 23 inline handlers become data-confirm attributes and 12 JS call sites await adminConfirm().
The dashboard tool grid now renders from the nav registry, so all 29 tools appear grouped by section instead of a hand-picked 17.
d75a8ce -
Feature
4:23 AM
Add shared page shell, nav registry, command palette and confirm dialog
Details
The admin area had 31 pages each hand-rolling their own <head>, stylesheet list and footer, which is how the CSS list drifted, how five pages ended up never closing .admin-layout, and how the sidebar (29 tools), the dashboard grid (17) and reality (30) all disagreed about what exists.
- admin/includes/admin-nav.php is now the one list of tools
- admin/includes/admin-page.php renders the page shell
- Ctrl+K palette and a sidebar filter, for jumping between 30 tools
- adminConfirm() replaces native confirm() for destructive actions
- admin pages no longer pull the 8-script front-end vendor bundle
4cbf325 -
Feature
3:40 AM
Show the deployed commit in the version badge
Details
The footer now reads "1.0.0 [acc26c6]", so what is live is identifiable at a glance instead of having to be inferred from a git log.
The deploy already computed the hash for its own log line. It now also writes it to config/build.php, after the rsync and into the document root only. That file is generated and never committed, so rsync has nothing to overwrite it with and no install that was not deployed by the hook gets a stale value.
A generated PHP file rather than a text file read at runtime, so opcache holds it and no request pays for the I/O.
Degrades to the bare version when the stamp is absent, which is the case locally and on any manual install. The bracket is only rendered for a non-empty APP_BUILD, so it never shows an empty [].
26f4b22 -
Feature
3:16 AM
Add the cPanel deployment config
Details
Builds in the server-side clone, then rsyncs into the document root with .deployignore holding back everything the live site owns.
No -p on the rsync. With it, rsync stamps the source tree's permissions onto the destination including the document root itself. The repository cPanel creates is mode 700, so a -p deploy left the docroot at 700, the web server's group lost traverse, index.php stopped executing and Apache served a directory listing instead. Without -p, existing files keep their own permissions and --chmod applies only to files rsync creates. The explicit chmod 750 afterwards is belt and braces on the one permission that takes the site down.
Lives on dev. master is production: the deploy hook fires on a push to the checked-out branch there, so master is only ever reached by a deliberate merge.
524be0f -
Feature
2:19 AM
Add the s&box Explorer 2027 opt-in
Details
Borrows the shape of the in-game menu (icon rail, filter pills, content rows with a lead tile, activity sidebar) but keeps this site's own visual language: our tokens, our type scale, our spacing. Deliberately not a reproduction of it.
The colours are a normal entry in frontend/css/ee/, so Settings > Appearance picks the theme up automatically like any other. The layout is server-rendered though, so the server has to know the choice before it renders; it learns it from a cookie theme-manager.js mirrors alongside localStorage. Resolution runs in bootstrap, before any output, because setcookie() silently fails once headers are sent.
The picker writes both cookies, which makes it authoritative. Without that, anyone who had ever opted out with ?beta=0 would pick the theme in Settings and keep getting the classic layout forever, with no way to fix it from the UI. ?beta=1 and ?beta=0 still work as shareable links and still win for the request they are on; they just no longer outlive the next theme change.
The homepage rows de-duplicate across each other, since the same package otherwise turns up in several of them and the page looks like it is repeating itself.
677a4bf -
Feature
2:18 AM
Rebuild the skins market on sboxskins.gg
Details
sbox.scmm.app stopped resolving (NXDOMAIN, not a transient outage), which left every /skins route rendering an empty page. sboxskins.gg exposes an equivalent JSON API, so the section is rebuilt on that instead of dropped.
Their robots.txt carries "Disallow: /api". The endpoints are public and unauthenticated, but that is a stated preference against automated use, so the cache TTLs are long, the User-Agent is identifiable, and the catalogue is paged through /api/items rather than making 221 detail calls.
Notes on their data, learned the hard way:
- priceHistory comes back newest-first, so it is reversed once on fetch rather than in each consumer. Left alone, the charts ran backwards and a "last N days" window returned the oldest N.
- sharePercent does not match the holdings it describes (92 units reported as 41.3%, 500 units as 0.9%), so shares are computed from quantity/totalSupply instead.
- ratingGrade is only returned by the list endpoint, never the detail one, so it is borrowed from the already-cached catalogue.
- priceChange24h is a percentage while priceChange6h is an absolute dollar delta, which is easy to render as the wrong unit.
SKINS_ENABLED gates the whole section for the next time a source dies. With it off, /skins used to fall through to the org handler and fuzzy-match an unrelated org page with a 200; it now returns a real 404 page.
Also fixes the homepage carousels, which all showed near-identical packages. The API ignores a &sort= parameter entirely: sorting is a token inside the query ("type:game sort:trending"). Sending it as a parameter meant every carousel got the same default ordering.
807922a
Generated from the project's commit history whenever the site is deployed. Times are server time.