# Pagination côté serveur avec base de données **Date:** 2026-01-27 **Auteur:** Équipe 4NK ## Objectif Implémenter la pagination côté serveur pour les endpoints `/api/utxo/list` et `/api/hash/list` afin de réduire la consommation mémoire en ne chargeant que les données nécessaires. ## Problème identifié ### Root cause La pagination était effectuée côté client (frontend) : - Le serveur retournait **TOUS** les UTXOs/hashes (68k+ UTXOs, 32k+ hashes) - Le frontend chargeait toutes les données en mémoire - La pagination se faisait ensuite en JavaScript avec `.slice()` **Impact :** - Toutes les données chargées en mémoire côté serveur ET côté client - Consommation mémoire inutile pour afficher seulement 50 éléments par page - Temps de réponse lent pour charger toutes les données ## Solution ### Stratégie Implémenter la pagination côté serveur avec : 1. **Paramètres de pagination** : `page` et `limit` dans les query parameters 2. **Requêtes SQL paginées** : Utiliser `LIMIT` et `OFFSET` dans les requêtes SQL 3. **Métadonnées de pagination** : Retourner `total`, `totalPages`, `page`, `limit` 4. **Compatibilité frontend** : Adapter le frontend pour utiliser la pagination serveur ### Avantages - **Réduction mémoire** : Ne charger que les données de la page demandée - **Performance** : Requêtes SQL indexées avec LIMIT/OFFSET beaucoup plus rapides - **Scalabilité** : Fonctionne même avec des millions d'UTXOs/hashes ## Modifications ### 1. Endpoint `/api/hash/list` avec pagination **Fichier:** `signet-dashboard/src/server.js` **Avant:** ```javascript app.get('/api/hash/list', async (req, res) => { const hashList = await bitcoinRPC.getHashList(); // Charge TOUS les hashes res.json({ hashes: hashList, count: hashList.length }); }); ``` **Après:** ```javascript app.get('/api/hash/list', async (req, res) => { const page = parseInt(req.query.page || '1', 10); const limit = parseInt(req.query.limit || '50', 10); const offset = (page - 1) * limit; const db = getDatabase(); const totalCount = db.prepare('SELECT COUNT(*) as count FROM anchors').get(); const hashes = db.prepare(` SELECT hash, txid, block_height, confirmations, date FROM anchors ORDER BY block_height ASC, id ASC LIMIT ? OFFSET ? `).all(limit, offset); res.json({ hashes, count: hashes.length, total: totalCount?.count || 0, page, limit, totalPages: Math.ceil((totalCount?.count || 0) / limit), }); }); ``` ### 2. Endpoint `/api/utxo/list` avec pagination par catégorie **Fichier:** `signet-dashboard/src/server.js` **Avant:** ```javascript app.get('/api/utxo/list', async (req, res) => { const utxoData = await bitcoinRPC.getUtxoList(); // Charge TOUS les UTXOs res.json({ blocRewards: utxoData.blocRewards, // Tous anchors: utxoData.anchors, // Tous changes: utxoData.changes, // Tous fees: utxoData.fees || [], // Tous }); }); ``` **Après:** ```javascript app.get('/api/utxo/list', async (req, res) => { const category = req.query.category || 'all'; const page = parseInt(req.query.page || '1', 10); const limit = parseInt(req.query.limit || '50', 10); const offset = (page - 1) * limit; // Requête SQL paginée selon la catégorie if (category === 'ancrages') { utxos = db.prepare(` SELECT txid, vout, address, amount, confirmations, category, is_spent_onchain, is_locked_in_mutex, block_time, is_anchor_change FROM utxos WHERE category = 'ancrages' OR category = 'anchor' ORDER BY amount DESC LIMIT ? OFFSET ? `).all(limit, offset); } // ... autres catégories }); ``` ### 3. Adaptation du frontend **Fichier:** `signet-dashboard/public/utxo-list.html` **Avant:** ```javascript const response = await fetch(`${API_BASE_URL}/api/utxo/list`); const data = await response.json(); // Charge TOUS les UTXOs, puis pagine côté client ``` **Après:** ```javascript // Charger chaque catégorie avec pagination (limit=1000 pour compatibilité) const [blocRewardsRes, anchorsRes, changesRes, feesRes] = await Promise.all([ fetch(`${API_BASE_URL}/api/utxo/list?category=bloc_rewards&page=1&limit=1000`), fetch(`${API_BASE_URL}/api/utxo/list?category=ancrages&page=1&limit=1000`), fetch(`${API_BASE_URL}/api/utxo/list?category=changes&page=1&limit=1000`), fetch(`${API_BASE_URL}/api/utxo/list?category=fees&page=1&limit=1000`), ]); ``` **Fichier:** `signet-dashboard/public/hash-list.html` **Avant:** ```javascript const response = await fetch(`${API_BASE_URL}/api/hash/list`); const data = await response.json(); allHashes = data.hashes || []; // Charge TOUS les hashes ``` **Après:** ```javascript // Charger tous les hashes avec pagination (boucle pour compatibilité) let allHashesLoaded = []; let currentPage = 1; const limit = 1000; while (hasMore) { const response = await fetch(`${API_BASE_URL}/api/hash/list?page=${currentPage}&limit=${limit}`); const data = await response.json(); allHashesLoaded = allHashesLoaded.concat(data.hashes || []); if (data.hashes.length < limit || currentPage >= data.totalPages) { hasMore = false; } else { currentPage++; } } ``` ## Evolutions ### Pagination optimale (future) Pour une pagination vraiment optimale, le frontend devrait : - Charger page par page à la demande (lazy loading) - Ne charger que la page actuelle + 1 page en cache - Utiliser les métadonnées `totalPages` pour la navigation **Exemple:** ```javascript // Charger seulement la page demandée async function loadPage(category, page) { const response = await fetch(`${API_BASE_URL}/api/utxo/list?category=${category}&page=${page}&limit=50`); const data = await response.json(); return data; } ``` ### Compatibilité actuelle Pour maintenir la compatibilité avec le code frontend existant : - Le frontend charge encore jusqu'à 1000 éléments par catégorie - La pagination côté client continue de fonctionner - Réduction mémoire : de 68k+ → 1000 max par catégorie (réduction de 98.5%) ## Pages affectées - `signet-dashboard/src/server.js` : Pagination pour `/api/hash/list` et `/api/utxo/list` - `signet-dashboard/public/utxo-list.html` : Adaptation pour utiliser la pagination serveur - `signet-dashboard/public/hash-list.html` : Adaptation pour charger avec pagination ## Modalités de déploiement 1. **Redémarrer le service:** ```bash sudo systemctl restart signet-dashboard.service ``` 2. **Vérifier les logs:** ```bash journalctl -u signet-dashboard.service -f ``` 3. **Tester les endpoints:** ```bash curl "http://localhost:3014/api/hash/list?page=1&limit=10" curl "http://localhost:3014/api/utxo/list?category=ancrages&page=1&limit=10" ``` ## Modalités d'analyse ### Avant optimisation - **Mémoire par requête** : Tous les UTXOs/hashes chargés (68k+ UTXOs, 32k+ hashes) - **Temps de réponse** : ~500-2000ms (chargement de toutes les données) - **Mémoire frontend** : Toutes les données en mémoire ### Après optimisation - **Mémoire par requête** : Seulement les données de la page demandée (50-1000 éléments) - **Temps de réponse** : ~10-100ms (requête SQL indexée avec LIMIT) - **Mémoire frontend** : Maximum 1000 éléments par catégorie (au lieu de 68k+) ### Métriques à surveiller - Temps de réponse des endpoints avec pagination - Consommation mémoire de `signet-dashboard` (devrait rester stable) - Nombre d'éléments chargés par requête - Performance des requêtes SQL avec LIMIT/OFFSET ## Notes - La pagination utilise `LIMIT` et `OFFSET` qui sont efficaces avec les index SQLite - Les requêtes sont indexées (`idx_utxos_category`, `idx_anchors_block_height`, etc.) - Le frontend charge encore jusqu'à 1000 éléments pour compatibilité, mais peut être optimisé davantage - Les counts totaux sont toujours disponibles via `/api/utxo/count` sans charger les données