Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
282 changes: 205 additions & 77 deletions AGENTS.md

Large diffs are not rendered by default.

342 changes: 342 additions & 0 deletions web/tests/e2e/specs/flows/iframe-anti-fouc.spec.ts

Large diffs are not rendered by default.

469 changes: 469 additions & 0 deletions web/tests/integration/IframeChromeAntiFoucBootloaderTest.php

Large diffs are not rendered by default.

25 changes: 25 additions & 0 deletions web/themes/default/page_blockit.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,21 @@
match the kickit template — see that file for the per-block
explanation. The only delta on the wire is `$length` (the block
duration in minutes) being forwarded into Actions.BlockitBlockPlayer.

Anti-FOUC bootloader (#1438): paired with the same change to
`page_kickit.tpl`. Today the blockit iframe is `display:none` in
`page_admin_comms_add.tpl` (it exists purely to fan a `sm_block_*`
rcon out to every server; the operator never sees it), so the
user-visible dark-mode bug only manifests on kickit. The
bootloader is added here for parity and future-proofing: if the
blockit iframe is ever made visible (matching the kickit shape)
OR reached as a top-level navigation (matching the kickit
server-context-menu shape), the dark-mode regression would
silently come back. Keeping both templates in lockstep is what
makes the regression test `IframeChromeAntiFoucBootloaderTest`
a reliable gate. See the matching block in `page_kickit.tpl` for
the full rationale and the byte-equivalence contract with
`core/header.tpl`.
*}-
<!DOCTYPE html>
<html lang="en">
Expand All @@ -17,6 +32,16 @@
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="csrf-token" content="-{$csrf_token}-">
<title>Block player</title>
<script>
(function () {
try {
var m = localStorage.getItem('sbpp-theme') || 'system';
var d = m === 'dark' || (m === 'system' && window.matchMedia
&& matchMedia('(prefers-color-scheme: dark)').matches);
if (d) document.documentElement.classList.add('dark');
} catch (e) { /* localStorage / matchMedia unavailable; default to light */ }
})();
</script>
<link rel="stylesheet" href="../themes/default/css/theme.css">
<script src="../scripts/api-contract.js"></script>
<script src="../scripts/sb.js"></script>
Expand Down
36 changes: 36 additions & 0 deletions web/themes/default/page_kickit.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,32 @@
preserve the IDs the legacy template used so existing
parent-window dialog hooks (set_counter, height adjustment) keep
working.

Anti-FOUC bootloader (#1438): the `pages/admin.kickit.php` URL is
reachable two ways in v2.0 chrome: as an iframe inside the
post-Ban "Ban Added" `sb.message.show` dialog (legacy default-theme
surface — `#dialog-placement` only exists on the default theme),
AND as a top-level navigation from the public Servers page's
right-click context menu's "Kick player" item (`web/scripts/server-context-menu.js`
builds the href directly to `pages/admin.kickit.php?check=…`). The
latter is the user-reported #1438 path: an operator in dark mode
right-clicks a player, picks Kick, and lands on this chromeless
iframe template rendered as a full-page document — and the page
paints stark white because `<html>` never gets the `dark` class.
The inline bootloader below mirrors `core/header.tpl`'s shape
(same THEME_KEY 'sbpp-theme', same default 'system', same dark-
resolution predicate; only ADDS the class, never removes — :root
defaults to light) so the very first paint lands in the operator's
persisted theme regardless of how the page was reached. Logic
must stay byte-equivalent to the canonical bootloader in
`core/header.tpl` (which in turn mirrors `theme.js`'s
`applyTheme(currentTheme())` minus the localStorage write); the
regression gate is `web/tests/integration/IframeChromeAntiFoucBootloaderTest.php`.
See "Anti-FOUC theme bootloader" in AGENTS.md Conventions for the
full contract. Wrapped in IIFE + try/catch because localStorage
throws on private-mode iframes / SecurityError and matchMedia is
missing on very old browsers — defaults to light on either failure,
matching the chrome's shape.
*}-
<!DOCTYPE html>
<html lang="en">
Expand All @@ -27,6 +53,16 @@
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="csrf-token" content="-{$csrf_token}-">
<title>Kick player</title>
<script>
(function () {
try {
var m = localStorage.getItem('sbpp-theme') || 'system';
var d = m === 'dark' || (m === 'system' && window.matchMedia
&& matchMedia('(prefers-color-scheme: dark)').matches);
if (d) document.documentElement.classList.add('dark');
} catch (e) { /* localStorage / matchMedia unavailable; default to light */ }
})();
</script>
<link rel="stylesheet" href="../themes/default/css/theme.css">
<script src="../scripts/api-contract.js"></script>
<script src="../scripts/sb.js"></script>
Expand Down
63 changes: 45 additions & 18 deletions web/themes/default/page_uploadfile.tpl
Original file line number Diff line number Diff line change
@@ -1,28 +1,55 @@
{*
SourceBans++ 2026 — page_uploadfile.tpl
Bound view: \Sbpp\View\UploadFileView (web/includes/View/UploadFileView.php).
SourceBans++ 2026 — page_uploadfile.tpl
Bound view: \Sbpp\View\UploadFileView (web/includes/View/UploadFileView.php).

Self-contained popup window opened via window.open(...) from one
of the parent admin pages (admin.uploadicon.php,
admin.uploadmapimg.php, admin.uploaddemo.php). On a successful
upload the page handler builds a {$message} blob containing
`<script>window.opener.<cb>(<json-encoded args>);self.close()</script>`
using JSON_HEX_* flags so the admin-controlled filename is safe
to interpolate into both the HTML attribute and the JS string
layers (#1113 fix). The template is the only Phase B/C template
that legitimately needs `{$message nofilter}`.
Self-contained popup window opened via window.open(...) from one
of the parent admin pages (admin.uploadicon.php,
admin.uploadmapimg.php, admin.uploaddemo.php). On a successful
upload the page handler builds a {$message} blob containing
`<script>window.opener.<cb>(<json-encoded args>);self.close()</script>`
using JSON_HEX_* flags so the admin-controlled filename is safe
to interpolate into both the HTML attribute and the JS string
layers (#1113 fix). The template is the only Phase B/C template
that legitimately needs `{$message nofilter}`.

The theme stylesheet path is hardcoded relative to the popup's
URL (/pages/admin.upload*.php), so `../themes/default/css/theme.css`
resolves correctly.
The theme stylesheet path is hardcoded relative to the popup's
URL (/pages/admin.upload*.php), so `../themes/default/css/theme.css`
resolves correctly.

Anti-FOUC bootloader (#1438): this template renders a separate
popup window opened with `window.open(...)` from a parent admin
page (which is dark-mode-aware via `core/header.tpl`). The popup
is same-origin, so `localStorage['sbpp-theme']` is reachable
from here — but the popup ships its own chromeless `<head>` and
doesn't load `theme.js`, so without the bootloader below the
popup paints stark white over the operator's dark-mode parent.
The inline script mirrors `core/header.tpl`'s bootloader exactly
(same THEME_KEY 'sbpp-theme', same default 'system', same dark-
resolution predicate). Note the body explicitly uses
`background:var(--bg-page)`, so the resolved tokens drive the
paint directly; without `html.dark` the page would resolve to
the `:root` light tokens regardless of operator preference. See
"Anti-FOUC theme bootloader" in AGENTS.md Conventions for the
full contract; regression gate is
`web/tests/integration/IframeChromeAntiFoucBootloaderTest.php`.
*}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>{$title} : SourceBans++</title>
<link rel="stylesheet" href="../themes/default/css/theme.css">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>{$title} : SourceBans++</title>
<script>
(function () {
try {
var m = localStorage.getItem('sbpp-theme') || 'system';
var d = m === 'dark' || (m === 'system' && window.matchMedia
&& matchMedia('(prefers-color-scheme: dark)').matches);
if (d) document.documentElement.classList.add('dark');
} catch (e) { /* localStorage / matchMedia unavailable; default to light */ }
})();
</script>
<link rel="stylesheet" href="../themes/default/css/theme.css">
</head>
<body style="padding:1rem;background:var(--bg-page)">
<div class="card" style="max-width:24rem;margin:0 auto">
Expand Down
59 changes: 42 additions & 17 deletions web/themes/default/updater.tpl
Original file line number Diff line number Diff line change
@@ -1,28 +1,53 @@
{*
SourceBans++ 2026 — updater.tpl
SourceBans++ 2026 — updater.tpl

Standalone wizard rendered by web/updater/index.php after Updater.php
has finished applying any pending migrations. View:
Sbpp\View\UpdaterView (just `$updates`).
Standalone wizard rendered by web/updater/index.php after Updater.php
has finished applying any pending migrations. View:
Sbpp\View\UpdaterView (just `$updates`).

The updater runs in its own bootstrap context — it does NOT go
through index.php's page-builder, so the chrome from
`core/header.tpl` + `core/footer.tpl` is intentionally not pulled
in. This template is therefore a complete <!DOCTYPE html> document
and links the theme stylesheet directly. Asset paths are relative
to /web/updater/ (where the script runs from), so `../themes/...`
points at /web/themes/default/.
The updater runs in its own bootstrap context — it does NOT go
through index.php's page-builder, so the chrome from
`core/header.tpl` + `core/footer.tpl` is intentionally not pulled
in. This template is therefore a complete <!DOCTYPE html> document
and links the theme stylesheet directly. Asset paths are relative
to /web/updater/ (where the script runs from), so `../themes/...`
points at /web/themes/default/.

Test hooks: each card header carries a stable
`data-testid="updater-<step>"` attribute.
Test hooks: each card header carries a stable
`data-testid="updater-<step>"` attribute.

Anti-FOUC bootloader (#1438): the updater is hit by logged-in
admins (same origin as the panel), and the body uses
`background:var(--bg-page);color:var(--text)` directly — so
without the bootloader, an admin in dark mode upgrading the
panel lands on a stark-white "Updater" page. The inline script
below mirrors `core/header.tpl`'s bootloader exactly; the
byte-equivalence contract is gated by
`web/tests/integration/IframeChromeAntiFoucBootloaderTest.php`.
See "Anti-FOUC theme bootloader" in AGENTS.md Conventions for
the full contract. Distinct from the install wizard's
`_chrome.tpl` (which is intentionally exempt — pre-install
panel has no logged-in user / no theme.js / no persisted
preference); the updater runs against a configured panel and
inherits the operator's already-set theme preference.
*}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Updater | SourceBans++</title>
<link rel="stylesheet" href="../themes/default/css/theme.css">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Updater | SourceBans++</title>
<script>
(function () {
try {
var m = localStorage.getItem('sbpp-theme') || 'system';
var d = m === 'dark' || (m === 'system' && window.matchMedia
&& matchMedia('(prefers-color-scheme: dark)').matches);
if (d) document.documentElement.classList.add('dark');
} catch (e) { /* localStorage / matchMedia unavailable; default to light */ }
})();
</script>
<link rel="stylesheet" href="../themes/default/css/theme.css">
</head>
<body style="background:var(--bg-page);color:var(--text);min-height:100vh">

Expand Down
Loading