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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5036,7 +5036,7 @@ the spec, target a 1920px viewport, not 1440px.
| Render admin-authored Markdown to safe HTML | `web/includes/Markup/IntroRenderer.php` (`Sbpp\Markup`) |
| Add a "Generate password" button to a password field (or change how passwords are generated) | Put `data-password-generator` + `data-password-targets="<id>[,<confirm-id>]"` on a `<button type="button" class="btn btn--ghost btn--icon">`; the shared dialog in `web/scripts/password-generator.js` (loaded from `core/footer.tpl`) does the rest: calls `Actions.AdminsGeneratePassword`, lets the admin tweak length + character sets, fills every non-disabled target on "Use password" and fires bubbling `input` / `change` events. Never write a page-local generator handler. Generation is server-side in `Sbpp\Security\PasswordGenerator` (`web/includes/Security/PasswordGenerator.php`, `random_int` + one-char-per-enabled-set guarantee + Fisher-Yates shuffle); the JSON action merges per-request options over the owner defaults (`config.password.generator.{length,lowercase,uppercase,digits,symbols,exclude_ambiguous}`, edited in Settings > Main, seeded by `data.sql` + `web/updater/data/813.php`) and clamps length to `[max(8, config.password.minlength), 128]`. The REST `POST /admins` fallback password uses the same class. `SYMBOLS` deliberately excludes quotes / backslash / `;` / space / backtick because values land in `server.cfg` and SourceMod configs. `admins.generate_password` stays `requireAdmin`, so surfaces reachable by non-web-admins gate the button (Your account uses `YourAccountView::$can_generate_password` = `is_admin()`). Regression guards: `web/tests/unit/PasswordGeneratorTest.php` (contract + data.sql / 813.php key and default parity), `AdminsTest::testGeneratePassword*`, `web/tests/e2e/specs/flows/password-generator.spec.ts`. |
| Write or edit a user-facing string (panel UI text, toast body, docs page) | See "User-facing text style (panel UI + docs)" under Conventions. Three rules: no emdash (`—`), terse, don't over-explain. Applies to `web/themes/default/**/*.tpl`, `\Sbpp\View\Toast::emit` titles + bodies, `echo` output from page handlers, and every `docs/src/content/docs/**/*.{md,mdx}` page. Does NOT apply to `AGENTS.md` / `ARCHITECTURE.md` / contributor docs / code comments / audit-log entries / test fixtures. Anti-pattern entries paired under "Anti-patterns". |
| Build / extend the anonymous opt-out daily telemetry payload (#1126) | `web/includes/Telemetry/Telemetry.php` (`Sbpp\Telemetry\Telemetry` — `tickIfDue`, `collect`, `send`) + `web/includes/Telemetry/Schema1.php` (`Sbpp\Telemetry\Schema1::payloadFieldNames()`, drives the extractor parity test) + `web/includes/Telemetry/schema-1.lock.json` (vendored from [sbpp/cf-analytics](https://github.com/sbpp/cf-analytics) — manual sync via `make sync-telemetry-schema`). Tick is registered at the tail of `init.php` via `register_shutdown_function`; on FPM, `fastcgi_finish_request()` flushes the response BEFORE the cURL POST so telemetry never delays a panel page. Slot reservation is atomic (`UPDATE :prefix_settings WHERE CAST(value AS UNSIGNED) <= :threshold`) at the START of the attempt, so a flapping endpoint costs one ping/day, not one ping/request. Audit-log only enable/disable transitions, never individual pings. The in-panel disclosure surface is the help-icon copy in `page_admin_settings_features.tpl`; the upgrade-time disclosure lives in `docs/src/content/docs/updating/1-8-to-2-0.mdx` (no first-login modal). |
| Build / extend the anonymous opt-out daily telemetry payload (#1126) | `web/includes/Telemetry/Telemetry.php` (`Sbpp\Telemetry\Telemetry` — `tickIfDue`, `collect`, `send`) + `web/includes/Telemetry/Schema1.php` (`Sbpp\Telemetry\Schema1::payloadFieldNames()`, drives the extractor parity test) + `web/includes/Telemetry/HostFingerprint.php` (`Sbpp\Telemetry\HostFingerprint::collect()`, the shared-hosting `env.*` signals from sbpp/cf-analytics#3; every probe is `Throwable`-guarded and raw paths / lists are reduced to booleans or counts) + `web/includes/Telemetry/Region.php` (`Sbpp\Telemetry\Region::resolve()`, the `panel.region` continent code from sbpp/cf-analytics#6; derived from the configured timezone name only, never from the request) + `web/includes/Telemetry/schema-1.lock.json` (vendored from [sbpp/cf-analytics](https://github.com/sbpp/cf-analytics) — manual sync via `make sync-telemetry-schema`). Tick is registered at the tail of `init.php` via `register_shutdown_function`; on FPM, `fastcgi_finish_request()` flushes the response BEFORE the cURL POST so telemetry never delays a panel page. Slot reservation is atomic (`UPDATE :prefix_settings WHERE CAST(value AS UNSIGNED) <= :threshold`) at the START of the attempt, so a flapping endpoint costs one ping/day, not one ping/request. Audit-log only enable/disable transitions, never individual pings. The in-panel disclosure surface is the help-icon copy in `page_admin_settings_features.tpl`; the upgrade-time disclosure lives in `docs/src/content/docs/updating/1-8-to-2-0.mdx` (no first-login modal). |
| Add or edit a project announcement (the admin-only banner on the home dashboard) | Edit `docs/public/announcements.json` (the source of truth — Astro publishes it as a static asset at `https://sbpp.github.io/announcements.json`). Each entry: `id` (≤64 chars, **required**), `title` (**required**), `body_md` (CommonMark, optional — rendered through `Sbpp\Markup\IntroRenderer` so raw HTML is escaped + `javascript:` / `data:` URLs are stripped), `url` (optional `http(s)://` only — non-http schemes rejected at the parser), `published_at` (optional ISO-8601 or unix int — drives the sort order; entries without it sort below dated entries), `expires_at` (optional — the parser drops entries past this timestamp). Sorted newest-first by the panel; convention is to also place the newest entry at the top of the array so reviewers see the most relevant change first. The deploy chain is automatic: a push to `main` that touches `docs/` fires `.github/workflows/docs-deploy-trigger.yml` which lands the file at `https://sbpp.github.io/announcements.json` within minutes. The starter file ships as `[]` (empty array). NEVER write to the cache file (`SB_CACHE/announcements.json`) directly — the panel's shutdown hook owns that path. |
| Add or edit a project sponsor, funding platform, or sponsor tier | Edit `docs/src/data/sponsors.json` (the single source of truth). Three top-level keys: `platforms` (`{id, name, url, description?, icon?}` — the funding channels; GitHub Sponsors today, append Open Collective / Patreon / etc. as they're approved), `tiers` (`{id, name, monthlyMinUsd?, description?}` — ordered list driving the sponsor-roll display order), `sponsors` (`{name, url?, logo?, tier?, since?}` — `tier` matches a `tiers[].id`; missing/empty/unknown places the sponsor in the "Individual supporters" bucket). The component `docs/src/components/Sponsors.astro` renders the file on the canonical `/sponsor/` landing page (`docs/src/content/docs/sponsor.mdx`, `template: splash`, intentionally absent from the sidebar in `astro.config.mjs`); the topbar heart icon, the per-page footer link in `docs/src/components/Footer.astro`, and `.github/FUNDING.yml`'s `custom:` URL all route there. Adding a new platform / sponsor is a one-line append here — no component, config, or template edit needed. Companion issue #1417 will surface the same data on the panel footer; a future README-injector script will read the same file into the main `README.md`'s `<!-- sponsors:start --> ... <!-- sponsors:end -->` markers. The starter file ships with one `platforms` entry (GitHub Sponsors) and empty `tiers` + `sponsors` arrays. Issue #1416. |
| Embed the sponsor roll on a docs page other than `/sponsor/` (e.g. a future docs index hero) | Import `docs/src/components/Sponsors.astro` and render `<Sponsors showPlatforms={false} />` — the component reads `docs/src/data/sponsors.json` and renders just the tier-grouped roll (with the "Individual supporters" bucket) when `showPlatforms` is false. `showEmptyHint={false}` additionally suppresses the "be the first" line when the roll is empty. The component renders its own `<h2>` headings; the embedding page should not add a parallel `## Our sponsors` heading or the chrome doubles. Issue #1416. |
Expand Down
22 changes: 20 additions & 2 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ web/
│ ├── Servers/ Sbpp\Servers\{SourceQueryCache, RconStatusCache} — per-(ip, port) cache around the xPaw A2S probe (#1311) + per-sid cache around the RCON `status` command (#PLAYER_CTX_MENU)
│ ├── Upload/ Sbpp\Upload\UploadHandler — shared file-upload handler (perm + CSRF + extension allowlist + filename sanitiser + popup chrome) for the demo / icon / mapimage popup pages
│ ├── Mail/ Sbpp\Mail\{Mail,Mailer,EmailType} — Symfony Mailer wrapper + enum
│ ├── Telemetry/ Sbpp\Telemetry\{Telemetry,Schema1} — anonymous opt-out daily ping (#1126); schema-1.lock.json is the vendored cross-repo contract
│ ├── Telemetry/ Sbpp\Telemetry\{Telemetry,Schema1,HostFingerprint,Region} — anonymous opt-out daily ping (#1126); schema-1.lock.json is the vendored cross-repo contract
│ ├── Announce/ Sbpp\Announce\{AnnouncementFetcher,Announcement} — anonymous opt-out daily fetch of project news / security advisories from https://sbpp.github.io/announcements.json (source of truth: docs/public/announcements.json); admin-only banner on the home dashboard
│ ├── Export/ Sbpp\Export\{ManifestBuilder,EntityExporter,BundleWriter,S3PresignedUploader,ExportError,Manifest} — owner-only full data export feature; streamed one-shot ZIP bundle of every row + every uploaded demo; entry point web/export.php (binary wire format, sits at panel-root next to exportbans.php / getdemo.php)
│ ├── SteamID/ SteamID parsing / vanity-URL resolution
Expand Down Expand Up @@ -801,13 +801,31 @@ The tick is the issue #1126 contract:
calls without flipping the user-facing toggle.

The whole `tickIfDue` body is wrapped in `try { … } catch (\Throwable)`
so telemetry can NEVER hard-fail the request. Pings are never
so telemetry can NEVER hard-fail the request. Defining
`SBPP_SKIP_TELEMETRY` before `init.php` skips the tick for that
request: `/health.php` does this, and so does the docker dev stack's
`docker/php/dev-prepend.php` for every web request, which keeps local
dev, E2E, and CI screenshot runs out of the production dataset. Pings are never
audit-logged either — a flapping endpoint would otherwise generate
`sb_log` noise that scares admins. Only the enable/disable
**transitions** are audit-logged (in
`web/pages/admin.settings.php`'s features POST handler).

`Telemetry::collect()` is the side-effect-free payload builder. The
shared-hosting signals in the `env` block (SAPI, ini limits,
`open_basedir`, control-panel dirs, per-user FPM; sbpp/cf-analytics#3)
come from `Sbpp\Telemetry\HostFingerprint`, which runs every probe
behind a `Throwable` guard so a host that disables `ini_get` /
`posix_*` degrades one field instead of losing the ping. Nullable
fields stay as explicit `null` in `collect()` (stable key set for the
parity test) and `send()` drops them via `Telemetry::withoutNulls()`,
because the Worker's validator accepts an absent key but rejects a
literal `null`. `panel.region` (sbpp/cf-analytics#6) comes from
`Sbpp\Telemetry\Region`, a hardcoded IANA-zone → continent table fed
by `config.timezone` (when it's a zone name; pre-2.0 upgrades can
leave a bare UTC offset there) or else PHP's default timezone; it
never reads the request. `panel.exported_30d` reads the `Data Export`
success rows `web/export.php` writes to `:prefix_log`. The
exact wire format is captured in
`web/includes/Telemetry/schema-1.lock.json` — a Draft-7 JSON Schema
vendored byte-for-byte from the [sbpp/cf-analytics](https://github.com/sbpp/cf-analytics)
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,16 @@ Legend:
? = Other stuff
```

(26/10/26): Version 2.2.4-srcdslab
-----------------------
```
01. + Telemetry sends the fields the analytics Worker expects: shared-
hosting environment signals, 30-day activity, game mix,
permission shape, data-export usage and the continent of the
panel's timezone (still no hostnames, IPs, names or SteamIDs)
02. ? The Docker dev / CI stack no longer sends telemetry pings
```

(26/10/26): Version 2.2.3-srcdslab (**Run updater required**)
-----------------------
```
Expand Down
2 changes: 1 addition & 1 deletion docker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ docker/apache/
sbpp-dev.conf denies direct /demos/ access + sends nosniff (mirrors sbpp-prod.conf)
docker/php/
web-entrypoint.sh waits for DB, renders config.php, runs composer install, fixes cache + demos perms
dev-prepend.php normalizes HTTP_HOST so init.php's localhost guard accepts :8080
dev-prepend.php dev-only constants: install-guard bypass + telemetry opt-out (web requests)
docker/db-init/
00-render-schema.sh on first DB init: substitutes {prefix}/{charset}, loads struc/data, seeds admin
sbpp.sh thin wrapper around `docker compose` + common dev tasks
Expand Down
19 changes: 19 additions & 0 deletions docker/php/dev-prepend.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,22 @@
if (!defined('SBPP_DEV_KEEP_INSTALL')) {
define('SBPP_DEV_KEEP_INSTALL', true);
}

// Keep dev / CI / test traffic out of the production telemetry
// dataset (#1126). The dev stack seeds the DB from `data.sql`, which
// ships `telemetry.enabled = 1` + the live cf-analytics endpoint, so
// without this every `./sbpp.sh up`, every E2E run, and every docs
// screenshot capture in CI would ping the real Worker from a
// throwaway install with a fresh `instance_id`. `SBPP_SKIP_TELEMETRY`
// is the same per-request opt-out `/health.php` uses; it makes
// `Telemetry::tickIfDue()` return before any DB write or network call.
//
// Web SAPIs only: the CLI also loads this file (PHPUnit inside the
// container via `./sbpp.sh test`), and `TelemetryOptOutTest` drives
// `tickIfDue()` directly against a dead endpoint to pin the gate.
// Defining the constant there would short-circuit those tests. The
// PHPUnit bootstrap never requires `init.php`, so no shutdown tick is
// registered under the CLI anyway.
if (PHP_SAPI !== 'cli' && !defined('SBPP_SKIP_TELEMETRY')) {
define('SBPP_SKIP_TELEMETRY', true);
}
22 changes: 19 additions & 3 deletions docs/src/content/docs/updating/1-8-to-2-0.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,27 @@ categories:

- **panel**: version, short git SHA, dev flag, theme name (only
`default` or `custom` is reported; fork theme **names are never
sent**).
sent**), the continent of the panel's configured timezone (for
example `eu`; never the timezone itself, and never looked up from
your IP), and whether a full data export ran in the last 30 days.
- **env**: PHP `major.minor`, DB engine + `major.minor`, web server
family, OS family.
family, OS family, plus a hosting fingerprint: PHP SAPI name,
`memory_limit` / `max_execution_time`, how many functions
`disable_functions` lists, and yes/no flags for thread safety,
64-bit PHP, `open_basedir` set, `allow_url_fopen`, OPcache,
Suhosin, POSIX, a cPanel / Plesk / DirectAdmin install, a
`/home/<user>/public_html`-style document root, and PHP running as
the account that owns the panel files. The `open_basedir` value,
document root path, and the `disable_functions` list itself are
**never sent**, only the yes/no or the count.
- **scale**: counts of admins, enabled servers, active and total
bans / comm blocks, 30-day submission / protest counts.
bans / comm blocks, 30-day submission / protest counts, bans and
active admins in the last 30 days, how many months of ban history
the panel holds, whether most recent bans were added from the web
panel, which games your enabled servers run (TF2, CS:S, CS:GO,
Garry's Mod, L4D2, or "other"; other game **names are never
sent**), and yes/no flags for web groups, server groups, overrides,
and admin immunity.
- **features**: every checkbox under
**Admin Panel → Settings → Features**, plus three derived booleans:
SMTP-configured, Steam API key set, GeoIP DB present.
Expand Down
Loading
Loading