From 25c51330868f3a71bc03b2ca8236aa3dedcd0780 Mon Sep 17 00:00:00 2001 From: Pontoporeia Date: Thu, 10 Sep 2026 14:49:57 +0200 Subject: [PATCH] =?UTF-8?q?perf(admin):=20infinite-scroll=20the=20contenus?= =?UTF-8?q?=20langues/mots-cl=C3=A9s=20tables?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The contenus page loaded both lookup tables in full via HTMX on page load: 760 tag rows + 216 language rows, ~2.4MB of HTML including ~1960 inline SVG icons and per-row CSRF forms. The DB queries were already fast (~6ms); the cost was pure client-side payload and DOM. - Add paged lookups: getTagsPage/getLanguagesPage + countTagsWithCount/ countLanguagesWithCount, and optional limit/offset on the existing unpaged variants (backward compatible). - Fragments serve 25 rows per request. A sentinel with hx-trigger="intersect once root:#" appends the next page when it scrolls into the table's own scroll container (htmx 'revealed' only checks the window viewport, not nested scrollers). - Search forms still swap the whole wrapper and reset to offset 0. - Style the load-more row; add PagedLanguagesTagsTest coverage. Initial DOM for the page drops from ~2.4MB to ~160KB; scrolling reaches the full totals (736 tags, 216 languages) and stops cleanly. --- TODO.md | 3 + .../admin/contenus-langues-fragment.php | 109 ++++++++--- .../admin/contenus-motscles-fragment.php | 109 ++++++++--- app/public/assets/css/admin.css | 16 ++ app/src/Database.php | 126 ++++++++++++- tests/phpunit/PagedLanguagesTagsTest.php | 174 ++++++++++++++++++ 6 files changed, 475 insertions(+), 62 deletions(-) create mode 100644 tests/phpunit/PagedLanguagesTagsTest.php diff --git a/TODO.md b/TODO.md index 3969074..ef3b562 100644 --- a/TODO.md +++ b/TODO.md @@ -51,6 +51,9 @@ - [x] #fix-desync-deploy-code-exit-23 Fix desync: deploy-code exit-23 'Operation not permitted' — drop -p/-t (destination owned by www-data:xamxam, setgid; SSH user can't chmod), use -rlDz --size-only --context deploy - [x] #deploy-permissions-sudo-prompt-can-t deploy-permissions sudo prompt can't accept input: ssh -t drops pty when local stdin isn't a TTY. Add scoped NOPASSWD sudo drop-in (deploy/xamxam-fix-permissions.sudoers) + deploy-sudoers recipe and wire into deploy --context deploy - [x] #fix-raw-markdown-leaking Fix raw markdown leaking into TOC labels on licence/charte/about pages +- [x] #rewrite-cc2r-checkbox-label Rewrite CC2r checkbox label to 'J'adhère au Collective Commitment to Reuse (CC2r)' (italic, both Libre and Interne branches) +- [x] #fix-admin-contenus-page [!high] Fix admin contenus page slowdown: langues/mots-clés fragments ship 2.4MB HTML (737 tag rows + 217 lang rows, ~1960 inline SVG icons + per-row CSRF forms). Implement htmx infinite-scroll 'load more' (paged fragments via limit/offset) for both tables. +- [x] #reduce-contenus-initial-page [!high] Reduce contenus initial page size from 100 to 25 rows per table (payload ~160KB total vs ~534KB) ## Deferred / Blocked - [ ] #just-setup-backs-a [!medium] just setup backs a stale setup-dev.sh (clones php-live-reload, legacy admin/data/ dirs) — needs rewrite or removal diff --git a/app/public/admin/contenus-langues-fragment.php b/app/public/admin/contenus-langues-fragment.php index 905683f..923c142 100644 --- a/app/public/admin/contenus-langues-fragment.php +++ b/app/public/admin/contenus-langues-fragment.php @@ -4,6 +4,15 @@ * * HTMX fragment: returns the langues table for the contenus page, * optionally filtered by a search query. + * + * Infinite scroll: rows are served in pages of PAGE_SIZE. When there are + * more rows, a sentinel is emitted with hx-trigger="revealed": when it + * scrolls into view htmx fetches the next offset and swaps itself + * (outerHTML) with the next chunk of rows + a new sentinel. This keeps the + * initial payload small instead of shipping all languages at once. + * + * When $offset > 0 the fragment returns only the rows (append mode), + * because the sentinel lives inside the existing . */ require_once __DIR__ . '/../../bootstrap.php'; require_once __DIR__ . '/../../src/AdminAuth.php'; @@ -15,14 +24,83 @@ if (empty($_SESSION['csrf_token'])) { require_once __DIR__ . '/../../src/Database.php'; +const LANGUES_PAGE_SIZE = 25; + $searchQuery = trim($_GET['q'] ?? ''); +$offset = max(0, (int) ($_GET['offset'] ?? 0)); try { - $db = new Database(); - $languages = ($searchQuery !== '') ? $db->searchLanguages($searchQuery) : $db->getAllLanguagesWithCount(); + $db = new Database(); + $languages = $db->getLanguagesPage($searchQuery, LANGUES_PAGE_SIZE, $offset); + $total = $db->countLanguagesWithCount($searchQuery); } catch (Exception $e) { die('
Erreur : ' . htmlspecialchars($e->getMessage()) . '
'); } + +$hasMore = ($offset + count($languages)) < $total; +$nextUrl = '/admin/contenus-langues-fragment.php?offset=' . ($offset + LANGUES_PAGE_SIZE) + . ($searchQuery !== '' ? '&q=' . urlencode($searchQuery) : ''); + +/** + * Render one for a language row. + */ +function render_langue_row(array $lang): void +{ + ?> + + + + + + + + +
+
+ + + + + +
+
+ + + + + Chargement… + + 0) { + foreach ($languages as $lang) { + render_langue_row($lang); + } + render_langues_sentinel($hasMore, $nextUrl); + return; +} + +// ── Initial load: full wrapper ──────────────────────────────────────────── ?>