Skip to content

Commit 424d6d4

Browse files
authored
feat(seeder): expand db-seed with demo files + multi-type comments (sbpp#1484)
`./sbpp.sh db-seed` previously stopped at empty `:prefix_demos` rows and ban-only comments. Two visible-but-empty surfaces on a freshly-seeded dev DB: 1. **"Review Demo" link on the banlist** rendered as a dead anchor — `:prefix_demos` was untouched by the seeder, so the page handler never emitted the affordance and the surface was unreachable for manual exploration / acceptance audits / screenshot work without hand-uploading a demo first. 2. **Moderation queues** (admin submissions + protests) and the public banlist's per-row comment disclosure rendered with empty comment threads — `:prefix_comments` only ever got type `'B'` (ban-attached) rows, so the C / S / P columns of the table were structurally untestable. This PR closes both gaps with one expansion of `Sbpp\Tests\Synthesizer`: **Demo seeding.** `insertDemos()` writes ~80 rows at medium scale (64 ban-attached, 16 submission-attached — 80/20 split that mirrors the panel's own bias toward bans-with-evidence) and lands a paired ~1 KiB opaque text payload on disk under `SB_DEMOS` (`web/demos/` on the dev bind-mount). Filenames are `md5(seed|demtype|demid)` so re-seeds round-trip identically and the truncate-time wipe can find them by re-reading the table. Picks use `array_rand` for exact unique counts — rejection-sampling-with-collisions undershoots the budget by ~6% which made the screenshot-flow rendering jittery across runs. The synth payload is text (not a Source-engine binary), so the "Review Demo" link round-trips end-to-end through `getdemo.php` with `Content-Type: application/octet-stream` and a download lands in the browser, but the SDK demoviewer won't replay them — that would need a real demo binary and is out of scope for a panel-chrome seeder. **Multi-type comments.** `insertComments()` now fans the comment budget across all four `:prefix_comments.type` values: - `B` (ban) — 60% — `comments.bid` → `bans.bid` - `C` (comm block) — 20% — `comments.bid` → `comms.bid` - `S` (submission) — 10% — `comments.bid` → `submissions.subid` - `P` (protest) — 10% — `comments.bid` → `protests.pid` Medium scale → 200 comments → 120/40/20/20. The split matches the overload contract in `api_bans_add_comment`'s `match($ctype)` arms documented in AGENTS.md → "Add a comment-delete trash icon". The parent-ID tracking arrays (`commsCids`, `submissionSubids`, `protestPids`) populate during their respective `insert*()` passes via `lastInsertId()` so the comment-insert pass has the FK pool ready. **Idempotency contract.** The pre-existing truncate-then-reseed flow got two refinements: - **Snapshot filenames BEFORE truncate, unlink AFTER.** Previous pre-fix shape was wipe-files-then-truncate, which left a transient window where files were gone but DB rows still pointed at them (any panel page query during that window 404'd the "Review Demo" link). New order: snapshot → truncate → unlink, so rows + files drop visibly together from the panel's POV. - **Wipe scope is the snapshotted set, not the directory.** Manual panel uploads (anyone who uploaded a demo against the dev stack) and `.gitkeep` survive untouched. The `db-reset` companion (see below) is the only thing that strips by-name across the whole directory, scoped to the MD5-basename shape. **Paired surface changes:** - `.gitignore` — `web/demos/*` + `!.gitkeep` exception. Pre-fix a fresh `db-seed` polluted `git status` with 80 untracked MD5-named files; the demo store is also where the panel's `UploadHandler` writes real ban evidence on production installs, so checking those in would leak operator evidence into the source tree. - `sbpp.sh db-reset` — strips MD5-basename files from `web/demos/` after the DB volume drop. The `_demos` rows that pointed at them vanish with the volume, so the next `db-seed`'s referenced-only wipe reads zero rows from the fresh DB and skips them; without the reset-time strip those files would accumulate as permanent orphans across worktree lifetimes. `find … -regex '.*/[0-9a-f]{32}$'` matches both shapes the write path produces (synth + the panel's own `UploadHandler::handle()` with `renameToHash: true`). - `AGENTS.md` + `docker/README.md` — exact per-scale counts, demo payload caveat (not playable in the demoviewer), the new wipe ordering contract, gitignore + `db-reset` cleanup behavior. **Verification.** Default-seed run produces exactly 80 demos + exactly 200 comments split B=120/C=40/S=20/P=20; FK joins return zero orphans across all six pairs (`demos.B` → bans, `demos.S` → submissions, `comments.B/C/S/P` → bans/comms/submissions/protests); two consecutive seeds with the default seed produce identical filename sets (`md5sum` of `ls` output matches byte-for-byte); a manually-touched decoy file in `web/demos/` survives every seed. PHPStan clean; the one PHPUnit test that touches `:prefix_demos` / `:prefix_comments` (`ExportBundleWriterTest`, 426 assertions) passes unchanged. Follow-up: sbpp#1483 tracks the pre-existing tech debt where the seeder writes bare letter codes (`'U'` / `'D'` / `'m'` / `'w'` / `'e'`) into the column-typed string fields covered by `BanRemoval` / `LogType` instead of routing through the enum's `->value` per AGENTS.md "Backed enums for column-typed fields". Out of scope for this PR.
1 parent 6126cc8 commit 424d6d4

5 files changed

Lines changed: 471 additions & 42 deletions

File tree

.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,16 @@ Temporary Items
5353
/web/cache/sessions/sess_*
5454
*.smx
5555

56+
# Uploaded demo evidence (production) + synthetic demo files written by
57+
# `./sbpp.sh db-seed` (dev). `.gitkeep` is the structural marker so the
58+
# directory exists for fresh tarball installs and tests that probe
59+
# `is_dir(SB_DEMOS)`. Both upload paths land MD5-hash basenames here;
60+
# checking them in would (a) blow up the working tree on every db-seed
61+
# and (b) leak real ban evidence into the source tree on a self-hoster
62+
# install.
63+
/web/demos/*
64+
!/web/demos/.gitkeep
65+
5666
# Local stash file used by the install-screenshot capture flow
5767
# (`docs/README.md` → "Capturing screenshots"). Gitignored so an
5868
# interrupted run never leaks the panel's DB credentials into a

AGENTS.md

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -618,24 +618,61 @@ two idiomatic shapes (#1273):
618618

619619
`./sbpp.sh db-seed` populates the dev DB (`sourcebans`) with a deterministic,
620620
realistic synthetic dataset across bans, comms, servers, admins, groups,
621-
submissions, protests, comments, notes, banlog, and the audit log. Use it
622-
when you need the panel surfaces (banlist, dashboard, drawer, moderation
623-
queues, audit log) to render with real-looking data instead of empty
624-
states. Acceptance audits and screenshot work both depend on it.
621+
submissions, protests, comments (per-type: B / C / S / P), notes, banlog,
622+
the audit log, AND demo evidence files (rows + on-disk files under
623+
`SB_DEMOS`). Use it when you need the panel surfaces (banlist, dashboard,
624+
drawer, moderation queues, audit log, "Review Demo" download flow) to
625+
render with real-looking data instead of empty states. Acceptance audits
626+
and screenshot work both depend on it.
625627

626628
- Lives at `web/tests/Synthesizer.php` (`Sbpp\Tests\Synthesizer`); CLI
627629
driver at `web/tests/scripts/seed-dev-db.php`. Both are dev-only —
628630
the synthesizer refuses any `DB_NAME` other than `sourcebans`, so
629631
`sourcebans_test` / `sourcebans_e2e` stay untouched and the E2E
630632
suite (which builds its own rows per spec) is unaffected.
631-
- Idempotent: every run truncates the synth-owned tables first
632-
(preserving `sb_settings` / `sb_mods` from `data.sql`), re-seeds the
633-
CONSOLE + `admin/admin` rows, then inserts the synthetic dataset.
633+
- Idempotent: every run snapshots the filenames currently referenced
634+
by `:prefix_demos` BEFORE the TRUNCATE wipes the table, executes
635+
the truncates (preserving `sb_settings` / `sb_mods` from `data.sql`),
636+
unlinks the snapshotted files AFTER (so rows + files drop visibly
637+
together from the panel's POV — closes a transient pre-fix window
638+
where rows pointed at files that had just been removed), re-seeds
639+
the CONSOLE + `admin/admin` rows, then inserts the new synthetic
640+
dataset (rows AND on-disk demo payloads). The wipe scope is the
641+
set of filenames currently in `:prefix_demos`; anything else in
642+
`SB_DEMOS` (the tracked `.gitkeep`, manual panel uploads, a dev's
643+
SDK demoviewer working copy) survives untouched. `./sbpp.sh
644+
db-reset` carries a sibling cleanup that strips MD5-named
645+
basenames from `web/demos/` (those rows vanished with the DB
646+
volume so they'd otherwise be permanent orphans).
634647
- Deterministic given a fixed `--seed` — `mt_srand($seed)` pins PHP's
635-
RNG so two devs hit the same names/reasons/timestamps. Default seed
636-
is `Synthesizer::DEFAULT_SEED` (pinned in code).
637-
- Three tiers: `--scale=small` (~30 bans, fast iteration), `medium`
638-
(default, ~200 bans), `large` (~2000 bans for pagination / perf).
648+
RNG so two devs hit the same names/reasons/timestamps AND so
649+
`array_rand` picks the same parent IDs for demo attachment across
650+
runs. Demo filenames ride `md5(seed|demtype|demid)` so re-seeds
651+
round-trip identically and the next run's cleanup pass finds +
652+
unlinks the prior set cleanly (filenames stay 32-char lowercase
653+
hex, indistinguishable from `UploadHandler::handle()`'s
654+
`renameToHash` output). Default seed is `Synthesizer::DEFAULT_SEED`
655+
(pinned in code).
656+
- Three tiers: `--scale=small` (30 bans + 15 demos, fast iteration),
657+
`medium` (default, 200 bans + 80 demos), `large` (2000 bans + 800
658+
demos for pagination / perf). Comment budget per scale fans
659+
exactly 60% Ban (`B`) / 20% Comm (`C`) / 10% Submission (`S`) /
660+
10% Protest (`P`) (medium → 120/40/20/20); demo budget fans
661+
exactly 80% ban-attached (`B`) / 20% submission-attached (`S`)
662+
(medium → 64/16). Counts are exact (no rejection-sampling
663+
shortfall) — `insertDemos()` uses `array_rand` for unique picks
664+
upfront. The splits give each surface enough rows to render
665+
without leaving the moderation-queue detail cards empty.
666+
- Demo payloads are opaque ~1 KiB text blobs carrying the synth
667+
marker + parent ID, served verbatim by `getdemo.php` with
668+
`Content-Type: application/octet-stream`. They won't replay in the
669+
SDK demoviewer (would need a real Source-engine demo binary);
670+
they DO exercise the panel's "Review Demo" download chrome end-to-end
671+
(link → 200 → file download with `Content-Disposition`). On-disk
672+
files land in `web/demos/`, which is gitignored alongside the
673+
production demo-upload directory (the panel's `UploadHandler` writes
674+
there too with `renameToHash: true`) so synthetic and real-production
675+
uploads share one storage shape.
639676
- Do NOT reach for this from `Fixture::truncateAndReseed()` (the e2e
640677
hot path) — those two diverge by design. The synthesizer is for
641678
manual dev / screenshot work; the e2e fixture stays minimal so each

docker/README.md

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -86,27 +86,48 @@ matching docs source file on disk.
8686
./sbpp.sh e2e # Playwright E2E gate (lazy chromium install) against the running stack
8787
./sbpp.sh db-dump backup.sql # mysqldump to host file
8888
./sbpp.sh db-load fixtures.sql # pipe a SQL file into the DB
89-
./sbpp.sh db-reset # drop just the DB volume and re-seed
90-
./sbpp.sh db-seed # populate the dev DB with realistic synthetic data
89+
./sbpp.sh db-reset # drop the DB volume + wipe MD5-named demo files from web/demos/
90+
./sbpp.sh db-seed # populate the dev DB with realistic synthetic data + write demo files to web/demos/
9191
./sbpp.sh rebuild # `--no-cache` rebuild of the web image
9292
```
9393

9494
`db-seed` lights up every data-driven panel surface — banlist + commslist
9595
beyond a single page, dashboard "Latest …" cards, the drawer's history /
96-
comments / notes panes, admin moderation queues (submissions and
97-
protests), admin audit log, multiple groups/admins/servers — without
98-
touching `data.sql` / `struc.sql` (the install path stays minimal).
99-
Idempotent: every run truncates the synthesizer-owned tables first and
100-
re-seeds. Deterministic given `--seed=<int>` so two devs see the same
101-
data; pinned default seed in code. Three scale tiers:
96+
comments / notes panes (B/C/S/P comment types covered, so the moderation
97+
queues and protest threads paint with real text), admin moderation
98+
queues, admin audit log, multiple groups/admins/servers, and demo files
99+
on disk under `web/demos/` (so the banlist's "Review Demo" link returns
100+
an actual download body) — without touching `data.sql` / `struc.sql`
101+
(the install path stays minimal). Idempotent: every run truncates the
102+
synthesizer-owned tables first and re-seeds. Deterministic given
103+
`--seed=<int>` so two devs see the same data; pinned default seed in
104+
code. Three scale tiers:
102105

103106
```sh
104-
./sbpp.sh db-seed # default scale (~200 bans, ~100 comms)
105-
./sbpp.sh db-seed --scale=small # ~30 bans, fast iteration
106-
./sbpp.sh db-seed --scale=large # ~2000 bans, pagination / perf
107+
./sbpp.sh db-seed # default scale (~200 bans, ~100 comms, ~80 demos)
108+
./sbpp.sh db-seed --scale=small # ~30 bans, ~15 demos, fast iteration
109+
./sbpp.sh db-seed --scale=large # ~2000 bans, ~800 demos, pagination / perf
107110
./sbpp.sh db-seed --seed=42 # alternate RNG seed
108111
```
109112

113+
Demo files: each `:prefix_demos` row lands a paired ~1 KiB opaque file
114+
in `web/demos/` (host bind-mount; gitignored alongside production
115+
upload evidence). Filenames are MD5(seed|demtype|demid) so re-runs with
116+
the same seed produce stable basenames and the truncate-time wipe can
117+
clean them up by re-reading the table. The synth payload is text, not a
118+
playable Source-engine demo — the panel chrome and `getdemo.php` stream
119+
the bytes verbatim with `Content-Type: application/octet-stream`, which
120+
is enough for the download affordance to work end-to-end, but the SDK
121+
demoviewer won't replay them. Manual uploads (panel-side or anything
122+
else not referenced by `:prefix_demos`) are never touched by the wipe.
123+
124+
`db-reset` drops the entire DB volume AND wipes MD5-named files from
125+
`web/demos/` (orphans after the DB drop — the `_demos` rows that
126+
pointed at them vanished with the volume). The wipe is scoped to
127+
`[0-9a-f]{32}` basenames so `.gitkeep` stays put; non-MD5 manual
128+
uploads (the icon / mapimg flows preserve original filenames) survive
129+
too, though those don't normally land in `web/demos/`.
130+
110131
Refusal guard: the seeder strictly refuses any `DB_NAME` other than
111132
`sourcebans`, so the PHPUnit DB (`sourcebans_test`) and Playwright DB
112133
(`sourcebans_e2e`) cannot be wiped by accident. The Synthesizer source

sbpp.sh

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ Run things inside containers:
3333
Database:
3434
db-dump [file] Dump the DB to file (default: dump-YYYYMMDD-HHMMSS.sql).
3535
db-load <file> Load a SQL dump into the dev DB.
36-
db-reset Drop the DB volume and re-seed (faster than full reset).
36+
db-reset Drop the DB volume + wipe MD5-named files from web/demos/
37+
(orphans after the DB drop). Faster than full reset.
3738
db-seed [args] Populate the dev DB with realistic synthetic data.
3839
Accepts --scale=small|medium|large (default medium) and
3940
--seed=<int> (default pinned in code). Idempotent: every
@@ -228,6 +229,22 @@ case "$cmd" in
228229
db-reset)
229230
dc rm -fsv db
230231
docker volume rm "$(basename "$PWD")_dbdata" 2>/dev/null || true
232+
# `db-reset` wipes the DB but `web/demos/` is bind-mounted from
233+
# the host worktree (not a named volume) so demo files written
234+
# by a previous `db-seed` (or by any panel-side upload that
235+
# happened against the dev stack) would otherwise survive as
236+
# permanent orphans — the `_demos` rows that pointed at them
237+
# vanish with the DB volume, and the next `db-seed`'s
238+
# referenced-only wipe reads zero rows from the fresh DB and
239+
# skips them. Strict MD5-name regex matches both shapes the
240+
# demo write path produces (`Sbpp\Tests\Synthesizer::deterministicDemoFilename`
241+
# for synth files, `Sbpp\Upload\UploadHandler::handle()` with
242+
# `renameToHash: true` for panel uploads) while leaving the
243+
# `.gitkeep` structural marker in place. Demos are dev-only
244+
# ban evidence here; production installs never run `sbpp.sh`.
245+
if [[ -d web/demos ]]; then
246+
find web/demos -maxdepth 1 -type f -regextype posix-extended -regex '.*/[0-9a-f]{32}$' -delete 2>/dev/null || true
247+
fi
231248
dc up -d db
232249
;;
233250
db-seed)

0 commit comments

Comments
 (0)