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
44 changes Clear filters
October 2026
1
-
Performance
5:01 PM
Add claimed_orgs.is_public only when it is missing, not on every request
1e30659
September 2026
1
-
Performance
10:32 PM
Serve forum pages from cache and stop every expiry costing a second
Details
A thread listing took 937ms on a cache miss and the entries lasted five minutes, so roughly every fifth minute somebody paid a full second for a page that had not changed. Profiling first ruled out the obvious suspect: sanitising fourteen posts costs 1ms, so the HTML pass is not worth caching and is left alone.
The forum fetchers now go through CacheManager::getOrSet rather than a plain read-then-fetch. That was already in the codebase and does two things this needs: it takes a lock so only one request regenerates while everyone else is handed the existing entry immediately, and its XFetch beta regenerates probabilistically before expiry so the changeover is spread out instead of landing on one unlucky visitor.
Lifetimes now follow how mutable the data actually is. The forum list itself changes about never, so an hour. Page one of a listing keeps its five minutes because new threads land there; later pages get half an hour because they only change when threads shuffle off page one. A thread page that is not the last page cannot gain posts, so it is held for six hours after it is read.
Forum activity keeps its own short mutex so a cold batch of ten upstream calls is not started by several visitors at once; the rest are handed the stale copy. It is best-effort rather than a real lock, since the worst case is two batches running and the cost is one wasted fetch.
Every fetcher keeps the last good answer for a week and falls back to it when Facepunch fails, which the tests cover along with the refetch on expiry.
Warm page loads are now 30-40ms across the forum index, listings, deep pages and threads.
bd13b1b
2
-
Performance
8:52 AM
Stop loading a 300KB png for a 72px icon
Details
The package icon beside the title renders at 72 square but was served the full-size Thumb: 268KB for roundbound, 378KB for micromayhem and sandbox. Every package publishes Square webp variants at 32 through 512, and the 128px one is 2-5KB - a 99% saving on an image that carries fetchpriority="high" and so competes with the hero for bandwidth on first paint.
This is the same conversion the browse grids and homepage got in e7738c3; the detail page's own icon was missed. thumbSources falls back to Thumb, so packages cached before the change still render.
370b617 -
Performance
8:29 AM
Stop shipping a 5MB icon font and full-size PNGs where WebP exists
Details
Two findings from measuring what the browser actually downloads.
The Material Symbols request left every variable axis open (opsz,wght,FILL,[email protected],100..700,0..1,-50..200), so Google served the complete variable font: 5,248KB. base.css renders every icon at a single point in that space - FILL 1, wght 400, GRAD 0, opsz 24 - so the axes are now pinned to exactly that: 447KB, a 4.8MB saving on a first visit. Weight ranges are not worth it; any range costs 1,208KB, so the two rules asking for wght 500 and the profile tag icons asking for 600 are set to 400, which at icon sizes is not a visible difference. material-symbols.css also imported Material Symbols Outlined, a second 3,887KB face that nothing could use: base.css overrides the outlined class to the Rounded family with !important. That import is gone, and head.php and the stylesheet now request one identical URL so it is fetched once.
Facepunch publishes resized WebP variants of every thumbnail - Square 32-512, Wide 160x90-910x512, Tall - in the same responses the site already reads, and the site was using the full-size PNG. thumbSources() picks the smallest variant that covers the space and falls back to the old field, so packages cached before this change still render. Measured, same pages: /t/model/ 1.10MB -> 0.60MB, /hidden-gems/ 11.23MB -> 0.67MB. The homepage reads its lists from a 30-60 minute cache, so it changes over as that expires; fetchCarouselPackages was verified directly and now resolves to a 320x180.webp.
The screenshot rail on package pages loaded the full-size stage image for each thumbnail. Screenshots carry their own Thumb, usually the same file but not always - one package had a 5.6MB PNG whose thumb is 114KB - so the rail uses it where it differs.
Org logos are left alone: the API exposes only Thumb for an organisation, the CDN ignores resize parameters, and some orgs upload 600KB PNGs shown at 24px. That one needs an image proxy or a word with Facepunch.
Three files show a large diff because git normalises their CRLF line endings to LF on staging under core.autocrlf=input; `git diff -w` shows the real change is 8, 3 and 7 lines.
e7738c3
1
-
Performance
4:42 AM
Stop background polling and collapse two N+1 queries
Details
Four pages polled on a bare setInterval, so a left-open background tab kept hitting the database indefinitely; Advanced Analytics did it every five seconds. adminPoll() skips the work while document.hidden and refreshes immediately when the tab comes back.
MCP API Keys resolved a username with its own query for every banned user rendered; one DISTINCT ON lookup now covers the whole list. Cleanup Invalid Users opened a transaction and issued three deletes per account; it is three set-based deletes in one transaction.
191f35b
May 2026
1
-
Performance
4:36 AM
Improve UX, accessibility, and code quality for 4 pages (#15)
Details
- Improve UX, accessibility, and code quality for news, charts, and global-stats pages
- /news: Add reading progress bar, skeleton loading, grid/list view toggle, scroll-to-top button, keyboard navigation (J/K), article count display, better ARIA attributes, rel=noopener on external links, proper time elements
- /orgnews: Add server-side search, client-side sorting (newest/A-Z), relative time display, 'New' badge on recent articles, better ARIA labels, preserve search params in pagination links
- /ugc/charts/game: Add live player count summary with pulsing indicator, refresh button, section item counts, staggered card animations, IntersectionObserver for sections, keyboard navigation, focus-visible styles, reduced motion support, improved empty state with alternative link
- /global-stats: Extract ~250 lines of inline styles to dedicated CSS file (frontend/css/pages/global-stats.css), add ARIA roles/labels/live regions, aria-pressed on tag buttons, aria-checked on range buttons, hover transitions on metric cards, focus-visible on game cards, reduced motion support
- Improve accessibility and fix structural issues in frontend PHP pages
- changelog.php: Remove duplicate theme-manager.js script, remove extra </div></div> tags, move </main> before scripts/footer, add role/aria-label to main, add aria-current to active pagination and filter buttons, wrap pagination controls in nav with aria-label
- about.php: Add role/aria-label to main, add aria-hidden to decorative icons, add noreferrer to external links
- credits.php: Add role/aria-label to main, add aria-hidden to decorative icons, remove stray </div>, add noreferrer to external links
- donate.php: Add role/aria-label to main, add aria-hidden to decorative icons
- new-user-guide.php: Add role/aria-label to main, add aria-hidden to hero icon
- release-notes.php: Add role/aria-label to main, add aria-hidden to decorative icons, add rel=noopener noreferrer to sbox.game link, remove stray </div>
- Improve accessibility and UX across all remaining frontend pages
Pages improved:
- compare.php: Extract inline CSS to external file, add ARIA roles/labels, focus-visible styles, prefers-reduced-motion support
- search.php: FIX CRITICAL BUG - broken <style> tag that included footer inside it. Scripts moved to bottom, added role=search, aria-labels
- leaderboard.php: Add aria-pressed on filter buttons, role=list/listitem in JS rendering, aria-live=polite on container
- favorites.php: Change div to semantic main element, add role=list/listitem, aria-live=polite, aria-hidden on decorative icons
- followed.php: Same accessibility pattern as favorites
- notifications.php: Add role=main, aria-live on container, aria-labels on mark-all-read and remove-all buttons
- settings.php: Add role=main, aria-label on main and form elements
- settings-language.php: Add role=main, aria-current=page on active nav, role=radiogroup on language grid
- notifications-following.php: Add role=main, aria-hidden on decorative icons, role=list/listitem on grid items
- CSS overhaul: extract inline styles and modernize page stylesheets
- notifications-following.php: Extract ~250 lines of inline CSS to frontend/css/pages/notifications-following.css (following grid, cards, badges, empty state, unfollow buttons, responsive)
- settings-language.php: Extract ~150 lines of inline CSS to frontend/css/pages/settings-language.css (settings layout, sidebar, language cards, responsive)
- compare.css: Add card entrance animation (fadeInScale with stagger), stat value hover effect, button active click feedback
- global-stats.css: Add card entrance states for IntersectionObserver, focus-visible on hero, update reduced-motion block
- charts.css: Verified existing focus-visible, reduced-motion, and responsive styles are all present
- Replace ugly #04131c header color with #0c1222 (matches navy palette)
The old #04131c had a greenish undertone that clashed with the blue-navy palette (--bg-primary: #080f1f, --bg-secondary: #181f2f). Replaced with #0c1222 which sits naturally between the two and keeps the header visually distinct without looking out of place.
- Align CSS variables with sbox.game official theme
Synced root variables to match the sbox.game design system:
- --site-header-height: 56px -> 60px (matches official)
- --site-header-color: kept #0c1222 (improved over official #04131c)
- --text-secondary: #b8c0d9 -> #adb3cb (official --text-color)
- --text-muted: #8892a8 -> #b2bed4 (official)
- --link: now uses color-mix formula from sbox.game
- --link-hover: now var(--accent-bright) instead of #fff
- font-size: 14px -> 13px (matches official)
- body background: now uses var(--site-background-color) semantically
- Added --admin/#c4214f, --admin-alt, --text-on-strong
- Added --form-surface, --form-divider, --form-label-color
- Added --column-gap: 0.75rem
- Added sbox.game menu color variables (--menu-option-color, etc)
- CSS audit: remove dead vendor prefixes, migrate z-index to variables, tokenize colors
Dead code removal:
- Remove 10 deprecated -webkit-overflow-scrolling: touch declarations
- Remove 1 -ms-overflow-style: none (IE is dead)
Z-index migration (156 hardcoded -> variables):
- Expand z-index scale: add --z-below, --z-raised, --z-header, --z-popover, --z-loading, --z-command-palette
- Replace ALL 156 hardcoded z-index values with var(--z-*) tokens
- Only z-index: 0 (stacking context resets) left as literals
Color tokenization:
- Add --purple (#8b5cf6) and --purple-light (#a78bfa) to design system
- Replace 33 #4ade80 -> var(--success-light)
- Replace 19 #22c55e/#10b981 -> var(--success)
- Replace 29 #ef4444 -> var(--error)
- Replace 14 #f87171 -> var(--error-light)
- Replace 18 #f59e0b/#fbbf24 -> var(--warning)
- Replace 11 #3498db -> var(--info)
- Replace 11 #8b5cf6 -> var(--purple)
- Replace #a78bfa -> var(--purple-light)
- Fix toast.css to use design system variables for success/error/warning
- Update base.css
- Fix TypeError: navigator.clipboard.writeText on undefined in package-detail-sections.php
Added fallback using document.execCommand('copy') for browsers/contexts where navigator.clipboard is unavailable (e.g., non-HTTPS or older browsers).
- Security, performance, and code quality improvements
Security:
- Fix navigator.clipboard TypeError in mcp-knowledge.js (2), settings.js (1) with document.execCommand fallback for non-HTTPS/older browsers
- Restrict CORS on settings API from wildcard to same-origin only
- Add CSRF token validation to admin-tools-api.php POST requests with auto-injection via admin.js fetch wrapper and token rotation
- Add Content-Security-Policy and Permissions-Policy headers to SecurityMiddleware.php (whitelists required CDNs, blocks frame/object)
- Stop leaking exception messages in search-api.php error responses
Performance:
- Rewrite notification-bell.js: 30s polling (was 10s), 2min when hidden, exponential backoff on errors, beforeunload cleanup
- Add try/catch guards for localStorage in command-palette.js (prevents crashes in private browsing mode)
Code quality:
- Extract admin/index.php inline styles to admin-dashboard.css (BEM classes)
- Consolidate duplicate .text-gradient/.gradient-text in design-system.css
- Remove excessive error_log() calls from settings.php
- Strip all code comments from PHP, JS, and CSS files
Removed all inline (//) and block (/* */) comments across the entire codebase (578 files scanned, 261 modified):
- 175 PHP files: stripped via PHP tokenizer (safe with strings/heredocs)
- 63 JS files: stripped via terser with beautified output
- 70 CSS files: stripped via regex (/* */ only, no string ambiguity)
- 16 PHP files: additional pass for comments inside <script>/<style> blocks
Only functional code remains. String literals containing // or /* (URLs, regex patterns, programmatic comment builders) are preserved correctly.
- Collapse excessive blank lines left by comment removal
Reduces 2+ consecutive blank lines to a single blank line across all 426 source files. Removes trailing whitespace and leading blank lines after <?php tags.
- Lint all PHP files with php-cs-fixer (PSR-12 + PHP 8.2+)
Ran php-cs-fixer v3.95.2 across all 390 PHP files (321 modified). Applied rules include:
- PSR-12 formatting (indentation, braces, spacing)
- PHP 8.2 migration (modern syntax, typed properties)
- PHP 8.0 risky rules (arrow functions, named args, match)
- Strict comparisons and strict params
- Ordered imports (alpha, class/function/const groups)
- Trailing commas in multiline arrays/args/params
- Yoda-style disabled (natural reading order)
- Void return types added where applicable
- Ternary to null coalescing where possible
Also expanded .php-cs-fixer.dist.php finder to cover admin/, auth/, mcp/, tests/, and tools/ directories (previously only backend/, frontend/, includes/, config/).
- feat: implement Facepunch API features - hidden gems, browse filters, tag cloud, game modes, achievement rarity, review playtime, enhanced cards
- Add /hidden-gems page with sort:hiddengem discovery grid
- Add browse-api.php endpoint with full sort/filter support
- Add browse-sort-orders.js with all API sort orders
- Add browse-filters.php faceted sidebar (genre, style, stars, size, review tags)
- Add tag-cloud.php weighted tag cloud partial
- Add game-mode-badges.php with SinglePlayer/MultiPlayer/Coop/Versus badges
- Add achievement rarity tiers (Common/Uncommon/Rare/Epic/Legendary)
- Add review-playtime.php horizontal bar chart visualization
- Add package-card-enhanced.php with ThumbWide, FileSize, dates, WIP/player badges
- Add getGameModeBadges(), formatFileSize(), formatRelativeDate() helpers
- Fix 400 errors and integrate new features into UI
Fixes:
- Fix 400 Bad Request on /api.php/packages/search calls from homepage-categories.js loadSubFilters() — was missing required 'q' param. Now uses type-specific endpoint /packages/{type} instead.
Navigation:
- Add Hidden Gems link (diamond icon) to main site navigation
UI Integration:
- Hidden Gems page: add browse filters sidebar with sticky layout, enhanced cards showing ThumbWide, live player badge, FileSize, dates
- Search page: add tag cloud (fetches live tag counts from API) shown when no search query is active
- Package detail sidebar: add game mode badges (SinglePlayer, Multiplayer, Co-op, Versus) before tags section
- CSS: import browse-filters.css and hidden-gems.css in styles.css
- Fix category+tag filtering and hidden gems filters
Homepage categories:
- Fix PackageController to use skip/take params (Facepunch API format) instead of page/count (which Facepunch ignores entirely)
- Increase FETCH_TIMEOUT from 2s to 8s to avoid aborting requests that need a fresh API call (cache miss → 3-5s roundtrip to Facepunch)
- Tag sub-filters now work: clicking Maps → Rats fetches 'rats type:map' which returns relevant results via free-text search
Hidden Gems filters:
- Wire browse-filters.php to hidden-gems.php: genres, style, stars, size, and reviewtag params are now read from URL and passed to API query
- Map filter values to Facepunch API facet format: disksize:1-9, pos:bitmask, stars:1-5, genre:name, style:2d/3d
- Fix TotalCount key (was 'Total', API returns 'TotalCount')
- Cache key now includes filter state (was only page number)
Browse filters partial:
- Fix size filter values: use API disksize numbers (1-9) not text keys
- Fix reviewtag values: use API pos: bitmask values (4, 512, 256, etc.) mapped to human labels (Gameplay, Replayability, Addictive, etc.)
- Convert homepage category nav to stats-only links
The category nav on the homepage never worked for filtering — the JS tried to update a #packagesGrid element that only exists in the browse layout (else branch), not in the homepage layout (if branch). Every call to fetchAndRender hit 'if (!grid) return' immediately.
Changes:
- Convert category nav buttons to links pointing to /t/{type}/ browse pages (where server-side rendering actually works)
- Remove initCategories() JS import from homepage (no longer needed)
- Remove broken sub-filters panel (tags like 'rats' never rendered)
- Add text-decoration:none for link-based nav items
- Categories with 0 count are now hidden instead of disabled
- Fix /t/game/ showing homepage instead of filtered results, fix hidden gems filters
/t/{type}/ route:
- Set $filterType = $tagType in routing_logic.php so package-list.php renders the browse layout instead of the homepage layout. Previously $filterType was unset, so $isHomepage evaluated to true and showed the full homepage instead of filtered packages.
Hidden gems filters:
- Always show the filter sidebar (was inside if-packages-exist check, meaning filters disappeared when a filter produced 0 results)
- Handle comma-separated genres (only use first value since API only supports one genre: filter at a time)
- Fix empty state message to suggest adjusting filters
- Improve /t/{type}/ browse page: show active type, fix nav links
- Header now shows 'Browse Games', 'Browse Maps', etc. instead of generic 'Browse Packages' when viewing a specific type
- Type filter nav correctly highlights the active type (uses $filterType from routing instead of $_GET['type'] which is empty on /t/ routes)
- Filter links now use /t/{type}/ URLs (proper route) instead of /?type= query params (which triggered homepage rendering)
- Fix /t/{type}/ dumping all packages without pagination
The tag listing route was calling fetchPackagesByTypeAndTag() which fetches up to 2000 packages (10 pages × 200) in bulk with no pagination.
Now uses the same paginated approach as the main browse: calls fetchPackagesByType() with proper page/perPage params (25 per page), calculates pagination data, and renders page controls.
When a tag sub-filter is active (?tags=rats), uses fetchPackagesBySearch() with the tag as free-text combined with the type filter.
- Clarify Hidden Gems is powered by Facepunch API, not hand-picked
Add subtitle text making it clear the page uses Facepunch's built-in 'Hidden Gems' algorithm (sort:hiddengem) rather than being manually curated by the site owner.
- Fix hidden gems filters not responding to clicks
The browse-filters.php partial included an inline script that wrapped all event listeners in DOMContentLoaded. But since the partial is included mid-page (inside main), DOMContentLoaded has already fired by the time the browser parses the script so the callback never executed and no click handlers were ever attached.
Fix: Use readyState check pattern - if DOM is already loaded, run initFilters() immediately; otherwise attach to DOMContentLoaded. This ensures filters work regardless of when the partial is included in the document.
- feat: add org social links, type-specific filters, video preview, profile stats, browse filter sidebar, referenced packages count
- Task 1: Org social links (Twitter, Website, Discord) on package detail sidebar
- Task 2: Type-specific browse filters for game/map/library types
- Task 3: Video preview section on package detail page
- Task 4: TotalReviews and PositiveReviews stats on player profile
- Task 5: Browse filter sidebar integration for /t/game/ and /t/map/ pages
- Task 6: Referenced by X packages indicator in usage stats
- Use proper Material Symbols for social links and fix Twitter URL
- Twitter: alternate_email icon (@ symbol) + auto-convert @handle to https://x.com/handle URL
- Website: language icon (globe - unchanged)
- Discord: headset_mic icon (recognizable voice chat icon)
Previously used 'flutter' (not a real Material Symbol) for Twitter and 'forum' for Discord which were generic/incorrect.
- Use Font Awesome brand icons for social links (Twitter/X, Discord)
Google Material Symbols don't have Twitter/X or Discord icons. Switch to Font Awesome 6 Brands which is already loaded via CDN on profile pages — now also loaded on package detail pages.
- Twitter/X: fa-brands fa-x-twitter (official X logo)
- Discord: fa-brands fa-discord (official Discord logo)
- Website: keeps Material Symbols 'language' (globe icon)
Font Awesome CDN link now conditionally loaded on package detail pages (was previously only on user/org profile pages).
---------
e2ce930
3
-
Performance
1:33 AM
Remove Pace.js, async fonts, optimize image loading
Details
- Remove Pace.js (~30KB) — progress bar library loaded on every page with minimal user benefit. Removed from head.php, theme-manager.js vendor list, and disabled in bundleManager config.
- Make Material Symbols font non-render-blocking — was loaded as a synchronous stylesheet. Now uses preload/onload pattern (same as Poppins). Eliminates render-blocking external CSS request from the critical path.
- Remove unused cdn.jsdelivr.net dns-prefetch/preconnect (nothing loads from it)
- Add fetchpriority=high to package-detail hero image (LCP element)
- Add loading=lazy to skins-item large icon image
91bcbac -
Performance
1:25 AM
Frontend performance and accessibility improvements
Details
Performance:
- Fix render-blocking chart.min.js and chartjs-plugin-zoom.min.js (add defer)
- Remove duplicate Google Fonts @import from styles.css (already loaded via <link preload> in head.php — eliminates blocking CSS network request)
- Fix theme-manager.js double-loading 9 vendor scripts already present in head.php (popper, tippy, sweetalert2, toastify, pace, clipboard, chart.js, chartjs-plugin-zoom, smooth-scroll). Now detects existing <script> tags and skips duplicates.
- Restructure body-scripts.php from 30+ unconditional scripts to contextual loading: Core (8), Browse (6), Package detail (6), Profile (9), Auth (2), Interactive (3). Simple pages now load ~14 scripts instead of 30+.
Accessibility:
- Add skip-to-content link (visible on Tab focus, hidden otherwise)
- Replace div.container with main#main-content (proper ARIA landmark)
- Skip link styled with primary color, positioned absolutely, transitions into view on :focus
4430658 -
Performance
12:53 AM
Admin panel overhaul — speed, security, and bug fixes
Details
Performance:
- Cache dashboard metrics with 30s TTL (eliminates ~8 DB queries per load)
- Progressive analytics loading: core metrics first, then secondary panels
- Combine multiple COUNT queries into single SQL statements throughout
- active-users.php: lazy-load tabs, replace meta-refresh with JS interval
- moderation-logs.php: tail-based file reading (last 100KB vs entire file)
- player-history.php: combine 7 queries into 1 + add 60s cache
- Defer admin.js, async-load non-critical CSS across all admin pages
- broadcast-notification.php: batch INSERT (chunks of 100) vs N+1 loop
SQL Optimizations:
- getUserStats: 4 queries → 1 using COUNT(DISTINCT CASE WHEN)
- getUserEngagement: replace NOT IN subquery with LEFT JOIN
- getRetentionMetrics: compute in SQL instead of fetching all rows to PHP
- getUserSessionStats: 4 queries → 1 combined query
Bug Fixes:
- Fix TypeError in org-claims-full loadData() (wrong CSS selector)
- Fix CalHeatMap not defined (removed invalid defer from CDN scripts)
- Fix reputation avgScore using only top 50 (now queries all users)
- Fix donations double saveDonation() creating duplicate test entries
Security:
- Remove display_errors=1 from diagnose.php in production
5d2ab45
December 2025
1
-
Performance
12:22 AM
Improve admin UI and reputation sync logic
Details
Refactored admin panel pages for cache and reputation management with improved HTML structure, styling, and code formatting. Enhanced reputation sync to use the higher value between Steam and s&box API playtime. Updated API endpoints in admin actions and improved code consistency across admin scripts.
bc249b8
1
-
Performance
2:11 PM
Enhance .htaccess with compression, caching, and routing
Details
Added Brotli and improved gzip compression, static asset caching headers, and security headers. Implemented advanced routing for API, static files, and front controller pattern. Improved handling of static files, preflight requests, and direct access to admin/auth PHP files.
9476102
Generated from the project's commit history whenever the site is deployed. Times are server time.