diff --git a/AGENTS.md b/AGENTS.md index 09ff44367..2a9da00f7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5019,6 +5019,7 @@ the spec, target a 1920px viewport, not 1440px. | Wrap a public-list legacy advanced-search form (banlist / commslist) in a default-collapsed `
` disclosure | `web/themes/default/page_bans.tpl` + `web/themes/default/page_comms.tpl` (#1315). The same `.filters-details` rules in `web/themes/default/css/theme.css` cover the chrome (summary chevron, `[open]` state, hover bg, focus ring, count badge); the public-list usage adds a `.filters-details__body > form.card` rule that suppresses the inner card framing because the disclosure body wraps a `{load_template file="admin.bans.search"}` (or `…comms.search`) — i.e. a sibling page-render that emits its own `
`. The View DTO (`Sbpp\View\BanListView` / `Sbpp\View\CommsListView`) carries a `bool $is_advanced_search_open` set by the page handler from `isset($_GET['advSearch']) && (string) $_GET['advSearch'] !== ''` so the legacy `?advSearch=…&advType=…` URL shim auto-opens the disclosure. Bare `?p=banlist` / `?p=commslist` and simple-bar filters (`?searchText=` / `?server=` / `?time=`) leave it closed so the unfiltered list reaches above the fold. Selectors anchor on `[data-testid="banlist-advsearch-disclosure"]` / `[data-testid="commslist-advsearch-disclosure"]` (the `
`) + the `…-toggle` (the ``) + the `…-active` count badge. The same `.filters-details` chrome was introduced for admin-admins by #1303 — both surfaces share the CSS so a future tweak is single-source. | | Surface admin-authored per-ban / per-comm comments inline on the public banlist + commslist (`
` / `comm-comments-inline`) | `web/themes/default/page_bans.tpl` + `web/themes/default/page_comms.tpl` (#BANLIST-COMMENTS). Both desktop tables wrap the player-name anchor + comment chip in `.ban-player-cell__head` so the collapsed chip sits on the SAME line as the name (icon + tabular-nums count only; `title` / `aria-label` carry "N comment(s)"). The name uses `.ban-player-cell__name` (ellipsis truncate) so the chip never wraps alone; opened details take `flex-basis: 100%` and the body lists each comment below. CSS lives under `.ban-comments-inline*` in `theme.css`. The `nofilter` on comment text is load-bearing (`encodePreservingBr` + URL-wrap). Mobile cards keep a non-interactive count indicator (`ban-comments-count-mobile`). Gate: `$view_comments`. **#1500** also hides comment author/editor when `banlist.hideadminname` is on. Add / Edit on banlist and commslist open the player drawer composer (`data-comment-compose` + `[data-testid="drawer-comment-form"]` in `theme.js`). After add/edit in the drawer, `loadDrawer` patches the matching inline disclosure (chip count + list items) and the mobile ban-card count chip from the detail envelope. Delete on the banlist / commslist (table or drawer) does **not** reload: `comment-actions.js` emits `sbpp:comment-deleted` and `theme.js` removes the `
  • ` in place so scroll, pagination, and open disclosures survive. Protests / submissions use an inline card composer (`data-action="comment-compose"` + `[data-comment-composer]` in `comment-actions.js`). `?comment=` is not an editor: the list still renders. Regression: `BanlistCommentsVisibilityTest` + `banlist-comments-visibility.spec.ts`. | | Show how many punishments a player has on record from the public banlist / commslist rows (repeat-offender history chip) | Handlers set `history_total` + `history_url` per row (`page.banlist.php`: `history_count` subquery, Steam ID for Steam bans, IP for IP bans; `page.commslist.php`: also `history_mutes` / `history_gags` / `history_silences` from the `mute_count` / `gag_count` / `silence_count` subqueries, raw counts, row included, plus a `history_label` tooltip string; type 3 silences count because forks / imports store them as one row). `page_bans.tpl` / `page_comms.tpl` paint `.history-chip` (theme.css, warning tint) next to the player name only when the total is above 1: a link to `?p=&searchText=` on desktop (`ban-history-chip` / `comm-history-chip`), a static span inside the mobile drawer-trigger card (`*-history-chip-mobile`). IP-type ban rows get `history_url = ''` (span, no link) when `banlist.hideplayerips` hides IPs from the viewer, so the search URL can't leak the IP. Templates guard with `isset($row.history_total)` for callers that build rows by hand. Regression: `web/tests/integration/PunishmentHistoryChipTest.php`. | +| Filter the public banlist / commslist by server or time range (`?server=` / `?time=1d\|7d\|30d` from the filter bar selects) | `$publicFilterClauses` / `$publicFilterAnd` / `$publicFilterWheren` / `$publicFilterArgs` / `$publicFilterLink` near the top of `web/pages/page.banlist.php` (`BA.*`, #1226) and `web/pages/page.commslist.php` (`CO.*`, also carries the `?type=` chip). Every query branch (search / no search / advSearch) appends `$publicFilterAnd` when a WHERE is already open and `$publicFilterWheren` otherwise; `$publicFilterLink` rides `$searchlink` so pagination and the Hide-inactive toggle keep the filter. The filter form always submits `searchText=` (empty), so Apply always lands in the search branch: its `OR` group MUST stay parenthesised, or the AND predicates bind only the last arm and every filter turns into a no-op. Regression: `web/tests/integration/PublicListFilterBarTest.php`. | | Filter the public banlist by ban state (`?p=banlist&state=permanent\|active\|expired\|unbanned`) — server-side rowset narrowing for a chip strip | `web/pages/page.banlist.php` (#1352). The `?state=` value is allowlisted (`['permanent','active','expired','unbanned']`, anything else falls through to "All"), composed into the existing `$publicFilterAnd` / `$publicFilterWheren` SQL fragments via the per-state predicates documented inline. **Symmetric defensive shape across the four arms**: `permanent` / `active` BOTH require `RemovedOn IS NULL` (otherwise a pre-2.0 admin-lifted row with `ends > now` would match BOTH `?state=active` AND `?state=unbanned`); `expired` carries THREE arms (`RemoveType = 'E'` post-migration shape + `RemoveType IS NULL AND length > 0 AND ends < now AND RemovedOn IS NULL` for pre-475 installs the prune writer never touched + `RemoveType IS NULL AND RemovedOn IS NOT NULL AND length > 0 AND (RemovedBy IS NULL OR RemovedBy = 0)` for the fork-divergence shape `810.php` pass 2 backfills); `unbanned` carries TWO arms (`RemoveType IN ('D', 'U')` post-migration shape + `RemovedOn IS NOT NULL AND RemoveType IS NULL AND RemovedBy IS NOT NULL AND RemovedBy > 0` for pre-2.0 admin-lifts that `810.php` pass 1 backfills). The `RemovedBy > 0` vs `RemovedBy IS NULL OR = 0` split between unbanned / expired is the **load-bearing distinction** — natural expiry sets `RemovedBy = 0` (PruneBans) or NULL (pre-475); admin lifts set `RemovedBy = `. The View DTO (`Sbpp\View\BanListView`) carries `string $active_state` + `string $chip_base_link`; the chip strip in `web/themes/default/page_bans.tpl` renders as real `` anchors with `aria-current="true"` (NOT `aria-pressed` — only valid on role=button; axe rejects it on `` as `aria-allowed-attr`) and `data-active="true"` server-rendered on the matching chip — never `