From 1986bca0ffac330fa7934fcd4747b73d01078db5 Mon Sep 17 00:00:00 2001 From: "Beau Beauchamp, WebTigers" Date: Sun, 25 Oct 2026 04:09:16 -0400 Subject: [PATCH] =?UTF-8?q?Three=20admin=20grids=20never=20drew=20a=20row?= =?UTF-8?q?=20=E2=80=94=20and=20the=20first=20bug=20was=20hiding=20two=20m?= =?UTF-8?q?ore=20(TIGER-390)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Found during a UI functional-testing pass, not by a failing test: nothing in the suite renders a page, so all four defects below were invisible. 1. MISSING GATE. Comment_AdminController, Newsletter_AdminController and System_SitesController draw a DataTables grid but never set `$this->view->useDataTables = true`, so the admin layout emitted no jQuery, no DataTables and no tiger.datatable.js. Each screen's JS guards on the missing global and returns, so the page served 200 with an empty table and NOT ONE console error — the guard doing its job is what hid it. Twelve other controllers set the flag; these three were the gap. 2. WRONG SERVICE KEY. tiger.datatable.js reads `service.action`; comment.admin.js and newsletter.admin.js passed `method`. So the /api message named no action and the gateway could only refuse it. Thirteen other grids pass `action`. This was masked by (1): the code never ran. 3. SERVER-SIDE ARGUMENT. Comment_Service_Comment::datatable() called `$this->_dtParams()` with no arguments against a signature that requires `array $params` — a hard TypeError on every request, so the moderation queue could never have loaded. Fifteen other call sites pass `$params`. Masked by (1) and (2) in turn. 4. A BROWSER DIALOG, against the house rule. DataTables' default errMode is `alert()`, so (3) surfaced as a blocking native dialog — which also kept the failure OUT of the console, where our cheapest detector looks. tiger.datatable.js now sets errMode to 'throw' and refuses a misconfigured grid at the call site with a console error naming the cause, so the next occurrence of (2) is caught by a developer, by the browser probe, and without a modal. Why this chain is the interesting part: each fix exposed the next. Loading the library revealed the wrong key; fixing the key revealed the TypeError; seeing the TypeError at all required replacing the alert with a console error first. Verified in a real browser on dev, all three screens: headers, search, sort and pagination render, the /api call succeeds, the empty state is the grid's own ("No data available"), and the console is clean. The errMode + guard change was mutation-checked in-page — re-passing the old `method:` shape returns null and logs "use `action`, not `method`" with no dialog. Comments were switched on for the check and switched back off, since off by default is deliberate. tests/ui/README.md: the harness is scenarios run on demand, by decision — no CI job, no promoted-spec directory, no Playwright suite. A finding becomes a Jira ticket; a statically detectable CLASS of finding belongs in paradigm-scan.php. Suites: 1200 unit / 18318 assertions and 1554 integration / 5217 assertions, both green. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_013vdyTeoDrGE4hKxt53TsE8 --- modules/comment/assets/js/comment.admin.js | 2 +- .../comment/controllers/AdminController.php | 5 +++-- modules/comment/services/Comment.php | 2 +- .../newsletter/assets/js/newsletter.admin.js | 2 +- .../newsletter/controllers/AdminController.php | 5 +++-- modules/system/controllers/SitesController.php | 7 ++++--- tests/ui/README.md | 18 +++++++++++++----- themes/puma/assets/js/tiger.datatable.js | 18 ++++++++++++++++++ 8 files changed, 44 insertions(+), 15 deletions(-) diff --git a/modules/comment/assets/js/comment.admin.js b/modules/comment/assets/js/comment.admin.js index 04bde3cb..abb0d2cc 100644 --- a/modules/comment/assets/js/comment.admin.js +++ b/modules/comment/assets/js/comment.admin.js @@ -35,7 +35,7 @@ var status = document.getElementById('comment-status'); grid = tigerDataTable('#comment-grid', { - service: { module: 'comment', service: 'comment', method: 'datatable' }, + service: { module: 'comment', service: 'comment', action: 'datatable' }, extraData: function () { return { status: status ? status.value : 'pending' }; }, order: [[3, 'desc']], columns: [ diff --git a/modules/comment/controllers/AdminController.php b/modules/comment/controllers/AdminController.php index d8e50629..1688afdd 100644 --- a/modules/comment/controllers/AdminController.php +++ b/modules/comment/controllers/AdminController.php @@ -22,8 +22,9 @@ public function indexAction() throw new Zend_Controller_Action_Exception('Comments are disabled', 404); } - $this->view->title = 'Comments — Tiger Admin'; - $this->view->statuses = Tiger_Model_Comment::STATUSES; + $this->view->title = 'Comments — Tiger Admin'; + $this->view->statuses = Tiger_Model_Comment::STATUSES; + $this->view->useDataTables = true; // the layout loads jQuery + DataTables when set // The AI spam control only APPEARS when there is a live agent to run it — an always-visible // toggle that silently does nothing is worse than no toggle. diff --git a/modules/comment/services/Comment.php b/modules/comment/services/Comment.php index c38bedab..e97b3b6a 100644 --- a/modules/comment/services/Comment.php +++ b/modules/comment/services/Comment.php @@ -282,7 +282,7 @@ public function datatable(array $params): void if (!$this->_isAllowed('moderate')) { $this->_error('core.api.error.not_allowed'); return; } if (!$this->_enabled()) { return; } - $dt = $this->_dtParams(); + $dt = $this->_dtParams($params); $status = (string) ($params['status'] ?? Tiger_Model_Comment::STATUS_PENDING); $rows = (new Tiger_Model_Comment())->byStatus($status, 500); diff --git a/modules/newsletter/assets/js/newsletter.admin.js b/modules/newsletter/assets/js/newsletter.admin.js index 7c4f2ad8..21eabd2e 100644 --- a/modules/newsletter/assets/js/newsletter.admin.js +++ b/modules/newsletter/assets/js/newsletter.admin.js @@ -31,7 +31,7 @@ var status = document.getElementById('newsletter-status'); grid = tigerDataTable('#newsletter-grid', { - service: { module: 'newsletter', service: 'subscribe', method: 'datatable' }, + service: { module: 'newsletter', service: 'subscribe', action: 'datatable' }, extraData: function () { return { status: status ? status.value : '' }; }, order: [[4, 'desc']], columns: [ diff --git a/modules/newsletter/controllers/AdminController.php b/modules/newsletter/controllers/AdminController.php index f75a5913..0562333d 100644 --- a/modules/newsletter/controllers/AdminController.php +++ b/modules/newsletter/controllers/AdminController.php @@ -18,7 +18,8 @@ public function init() /** The subscriber list. */ public function indexAction() { - $this->view->title = 'Newsletter — Tiger Admin'; - $this->view->statuses = Newsletter_Model_Subscriber::STATUSES; + $this->view->title = 'Newsletter — Tiger Admin'; + $this->view->statuses = Newsletter_Model_Subscriber::STATUSES; + $this->view->useDataTables = true; // the layout loads jQuery + DataTables when set } } diff --git a/modules/system/controllers/SitesController.php b/modules/system/controllers/SitesController.php index 4459eea6..d0e61d55 100644 --- a/modules/system/controllers/SitesController.php +++ b/modules/system/controllers/SitesController.php @@ -19,9 +19,10 @@ public function init() /** `/system/sites` — the host → org mappings grid + an add-domain form. */ public function indexAction() { - $this->view->title = 'Sites — Tiger Admin'; - $this->view->form = new System_Form_Site(); - $this->view->orgCount = (int) (new Tiger_Model_Org())->fetchAll( + $this->view->title = 'Sites — Tiger Admin'; + $this->view->useDataTables = true; // the layout loads jQuery + DataTables when set + $this->view->form = new System_Form_Site(); + $this->view->orgCount = (int) (new Tiger_Model_Org())->fetchAll( (new Tiger_Model_Org())->activeSelect() )->count(); } diff --git a/tests/ui/README.md b/tests/ui/README.md index 1f29f313..67c1d408 100644 --- a/tests/ui/README.md +++ b/tests/ui/README.md @@ -25,10 +25,18 @@ test account. Without it the authenticated scenarios are skipped; everything ano ## Exit codes -Both probes exit non-zero on a real failure, so they drop straight into CI or a cron. -A check that could not run (e.g. reCAPTCHA refused a tokenless client) reports **GATED**, not -PASS — a vacuous green is worse than no check. +Both probes exit non-zero on a real failure, so either drops into a script when you want that. +A check that could not run reports **GATED**, not PASS — a vacuous green is worse than no check. +With reCAPTCHA switched off on the test install (`tiger.recaptcha.enabled = 0`) nothing is gated; +see the probe's own footer. -## Promotion +## Scope — scenarios, by decision -A confirmed finding becomes a spec in `regression/` so it cannot come back silently. +These are **scenarios, run on demand.** There is deliberately no CI job, no promoted-spec +directory and no Playwright suite to maintain: the cost of a regression harness is ongoing and +the value here is finding real defects on real screens, which is what an on-demand pass does. + +So a confirmed finding becomes a **Jira ticket** (labelled `functional-user-test` + `ui-probe`), +not a test file. Where a whole CLASS of defect turns out to be statically detectable, the cheap +exhaustive home for it is a check in `paradigm-scan.php` — one scan, every view, milliseconds — +rather than a scenario per screen. diff --git a/themes/puma/assets/js/tiger.datatable.js b/themes/puma/assets/js/tiger.datatable.js index 473d692c..9baed0a0 100644 --- a/themes/puma/assets/js/tiger.datatable.js +++ b/themes/puma/assets/js/tiger.datatable.js @@ -29,10 +29,28 @@ var $ = window.jQuery; if (!$ || !$.fn || !$.fn.DataTable) { return null; } + // DataTables reports its own errors with a browser alert() by default, which breaks the house + // rule (no alert/confirm/prompt — AGENTS.md) and, worse, hides the failure from the console, + // where our cheapest detector looks. 'throw' routes it to the console instead: visible to a + // developer, visible to the browser probe, and it can never block the page behind a modal. + if ($.fn.dataTable && $.fn.dataTable.ext) { $.fn.dataTable.ext.errMode = 'throw'; } + opts = opts || {}; var svc = opts.service || {}; var extra = (typeof opts.extraData === 'function') ? opts.extraData : function () { return {}; }; + // The /api message names module + service + ACTION. `action` is the key this helper reads; a + // grid that says `method` instead sends no action at all, and the gateway can only refuse it. + // Refusing here names the real cause at the call site rather than letting DataTables report a + // generic warning about a request that was never going to work. + if (!svc.module || !svc.service || !svc.action) { + window.console && console.error( + 'tigerDataTable(' + selector + '): service needs { module, service, action } — got ' + + JSON.stringify(svc) + (svc.method ? ' (use `action`, not `method`)' : '') + ); + return null; + } + // Sensible defaults; caller's opts win. `service`/`extraData` are ours, not DT's. // stateSave persists page / length / search / order per grid (localStorage, keyed by table id + // path) so a grid remembers where you were across reloads + navigation. A caller can opt out with