UTXO list: progressive loading from file with progress bar

**Motivations:**
- Page utxo-list très longue à charger
- Utiliser le fichier utxo_list.txt (peut ne pas être à jour) au lieu de /api/utxo/list
- Proposer un chargement progressif avec barre de progression

**Root causes:**
- /api/utxo/list appelle getUtxoList (RPC + fichier), lent et peut bloquer
- Aucun retour visuel pendant le chargement

**Correctifs:**
- Chargement depuis /api/utxo/list.txt (fichier uniquement)
- Stream du fichier (getReader + TextDecoder), parsing ligne à ligne
- Barre de progression (bytes reçus / Content-Length)
- Content-Length et Last-Modified sur /api/utxo/list.txt
- Statut "—" quand source fichier (pas d'info dépensé/verrouillé)
- Note "Source : fichier utxo_list.txt (peut ne pas être à jour)"

**Evolutions:**
- Chargement plus rapide et non bloquant
- Feedback visuel (progression, lignes parsées)
- Documentation dans features/utxo-list-progressive-loading.md

**Pages affectées:**
- signet-dashboard/src/server.js
- signet-dashboard/public/utxo-list.html
- features/utxo-list-progressive-loading.md
This commit is contained in:
ncantu 2026-01-26 01:04:32 +01:00
parent 078b36d404
commit 076b054b70
3 changed files with 277 additions and 43 deletions

View File

@ -0,0 +1,60 @@
# Chargement progressif de la liste UTXO depuis le fichier
**Auteur** : Équipe 4NK
**Date** : 2026-01-26
**Version** : 1.0
## Objectif
Raccourcir le chargement de la page [Liste des UTXO](https://dashboard.certificator.4nkweb.com/utxo-list) en utilisant directement le fichier `utxo_list.txt` au lieu de `/api/utxo/list`, et afficher une barre de progression pendant le chargement.
## Problème initial
- La page UTXO-list est très longue à charger.
- Elle appelait `/api/utxo/list`, qui utilise `getUtxoList()` (RPC + fichier) et peut être lent ou bloquer le serveur.
- Le fichier `utxo_list.txt` peut être en retard par rapport au nœud, mais il est servi rapidement.
- Aucun retour visuel pendant le chargement.
## Impacts
- **Page UTXO-list** : Charge désormais depuis `/api/utxo/list.txt` (fichier uniquement).
- **Serveur** : `/api/utxo/list.txt` envoie `Content-Length` et `Last-Modified` pour la progression et laffichage de la date de mise à jour.
- **Utilisateurs** : Barre de progression pendant le chargement ; indication que la source est le fichier (potentiellement pas à jour).
## Modifications
### Serveur (`signet-dashboard/src/server.js`)
- Route `/api/utxo/list.txt` : ajout de `Content-Length` et `Last-Modified` (via `statSync`).
### Page UTXO-list (`signet-dashboard/public/utxo-list.html`)
- **Source des données** : `loadUtxoList()` lit `/api/utxo/list.txt` au lieu de `/api/utxo/list`.
- **Streaming** : utilisation de `response.body.getReader()` + `TextDecoder` pour traiter le flux par morceaux.
- **Parsing** : `parseUtxoLine()` parse le format fichier (`category;txid;vout;amount;confirmations;isAnchorChange;blockTime`) et produit des objets UTXO compatibles avec `renderTable` / `renderFeesTable`.
- **Barre de progression** :
- Section dédiée avec barre (pourcentage) et texte « X ligne(s) parsée(s) ».
- Progression = `bytes reçus / Content-Length` ; si pas de `Content-Length`, 0 % puis 100 % à la fin.
- **Source fichier** : mention « Source : fichier utxo_list.txt (peut ne pas être à jour) » et, si disponible, « Dernière modif. : … » (depuis `Last-Modified`).
- **Statut** : pour les UTXO chargés depuis le fichier (`fromFile: true`), la colonne Statut affiche « — » (pas dinfo dépensé/verrouillé disponible).
### Compatibilité
- Format 7 champs (nouveau) et 6 champs (ancien avec `address`) gérés par `parseUtxoLine`.
- `actualiser` recharge depuis le fichier (même flux que le chargement initial).
## Modalités de déploiement
1. Redémarrer le dashboard : `sudo systemctl restart signet-dashboard.service`.
2. Vider le cache navigateur si nécessaire puis recharger `/utxo-list`.
## Modalités danalyse
- Ouvrir les DevTools (Network) : vérifier que la requête va vers `/api/utxo/list.txt` et que les headers `Content-Length` et `Last-Modified` sont présents.
- Vérifier que la barre de progression saffiche puis disparaît une fois le chargement terminé.
- Vérifier que les quatre tableaux (Bloc Rewards, Ancrages, Changes, Frais) et la pagination saffichent correctement.
## Pages affectées
- `signet-dashboard/src/server.js` : route `/api/utxo/list.txt`
- `signet-dashboard/public/utxo-list.html` : `loadUtxoList`, `parseUtxoLine`, rendu progression, statut `fromFile`

View File

@ -123,6 +123,42 @@
font-size: 1.2em; font-size: 1.2em;
color: #666; color: #666;
} }
.progress-section {
padding: 30px;
text-align: center;
background: #f8f9fa;
border-radius: 8px;
margin: 20px 0;
}
.progress-section p {
margin: 10px 0;
color: #333;
}
.progress-bar-container {
width: 100%;
max-width: 400px;
height: 24px;
background: #e9ecef;
border-radius: 12px;
overflow: hidden;
margin: 15px auto;
}
.progress-bar-fill {
height: 100%;
background: linear-gradient(90deg, #28a745, #20c997);
transition: width 0.15s ease;
}
.progress-stats {
font-size: 0.9em;
color: #666;
margin-top: 10px;
}
.source-note {
font-size: 0.9em;
color: #666;
margin-top: 10px;
font-style: italic;
}
.error { .error {
background: #f8d7da; background: #f8d7da;
color: #721c24; color: #721c24;
@ -263,7 +299,7 @@
<h1>Liste des UTXO</h1> <h1>Liste des UTXO</h1>
<div class="info-section"> <div class="info-section">
<p><strong>Total d'UTXO :</strong> <span id="utxo-count">-</span></p> <p><strong>Total d'UTXO :</strong> <span id="utxo-count">-</span></p>
<p><strong>Capacité d'ancrage restante :</strong> <span id="available-for-anchor">-</span> ancrages (<span id="confirmed-available-for-anchor">-</span> UTXOs confirmés)</p> <p><strong>Capacité d'ancrage restante :</strong> <span id="available-for-anchor">-</span> ancrages</p>
<button class="consolidate-button" id="consolidate-button" onclick="consolidateSmallUtxos()" style="margin-left: 10px; background: #ffc107; color: #000; border: none; padding: 10px 20px; border-radius: 5px; cursor: pointer; font-size: 1em; margin-top: 10px;">Chargement...</button> <button class="consolidate-button" id="consolidate-button" onclick="consolidateSmallUtxos()" style="margin-left: 10px; background: #ffc107; color: #000; border: none; padding: 10px 20px; border-radius: 5px; cursor: pointer; font-size: 1em; margin-top: 10px;">Chargement...</button>
<p><strong>Montant total :</strong> <span id="total-amount" class="total-amount">-</span></p> <p><strong>Montant total :</strong> <span id="total-amount" class="total-amount">-</span></p>
<p><strong>Dernière mise à jour :</strong> <span id="last-update">-</span></p> <p><strong>Dernière mise à jour :</strong> <span id="last-update">-</span></p>
@ -282,6 +318,15 @@
<div id="content"> <div id="content">
<div class="loading">Chargement des UTXO...</div> <div class="loading">Chargement des UTXO...</div>
</div> </div>
<div id="progress-section" class="progress-section" style="display: none;">
<p><strong>Chargement progressif depuis le fichier</strong></p>
<div class="progress-bar-container">
<div id="progress-bar-fill" class="progress-bar-fill" style="width: 0%;"></div>
</div>
<p id="progress-percent">0 %</p>
<p id="progress-stats" class="progress-stats">0 ligne(s) parsée(s)</p>
<p id="source-note" class="source-note">Source : fichier utxo_list.txt (peut ne pas être à jour)</p>
</div>
</div> </div>
</div> </div>
@ -291,6 +336,10 @@
// État de tri et pagination pour chaque catégorie // État de tri et pagination pour chaque catégorie
const tableState = {}; const tableState = {};
/** Données UTXO chargées (blocRewards, anchors, changes, fees) */
let currentUtxosData = {};
/** true si les données viennent du fichier (pas de Statut connu) */
let utxosFromFile = false;
// Charger la liste au chargement de la page // Charger la liste au chargement de la page
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
@ -316,9 +365,10 @@
if (sortColumn === 'amount') { if (sortColumn === 'amount') {
aVal = a.amount; aVal = a.amount;
bVal = b.amount; bVal = b.amount;
} else if (sortColumn === 'confirmations') { } else if (sortColumn === 'confirmed') {
aVal = a.confirmations || 0; // Trier par confirmé/non confirmé (booléen)
bVal = b.confirmations || 0; aVal = (a.confirmations || 0) > 0 ? 1 : 0;
bVal = (b.confirmations || 0) > 0 ? 1 : 0;
} else { } else {
return 0; return 0;
} }
@ -401,21 +451,21 @@
<tr> <tr>
<th>Transaction ID</th> <th>Transaction ID</th>
<th>Vout</th> <th>Vout</th>
<th>Adresse</th>
`; `;
if (isAnchors) { if (isAnchors) {
tableHTML += ` tableHTML += `
<th>Numéro de bloc</th> <th>Numéro de bloc</th>
<th class="sortable-header" style="text-align: right;" onclick="toggleSort('${categoryName}', 'amount')">Montant (✅)${getSortArrow('amount', state.sortColumn, state.sortDirection)}</th> <th class="sortable-header" style="text-align: right;" onclick="toggleSort('${categoryName}', 'amount')">Montant (✅)${getSortArrow('amount', state.sortColumn, state.sortDirection)}</th>
<th class="sortable-header" onclick="toggleSort('${categoryName}', 'confirmations')">Confirmations${getSortArrow('confirmations', state.sortColumn, state.sortDirection)}</th> <th>Date</th>
<th class="sortable-header" onclick="toggleSort('${categoryName}', 'confirmed')">Confirmé${getSortArrow('confirmed', state.sortColumn, state.sortDirection)}</th>
<th>Statut</th> <th>Statut</th>
`; `;
} else if (isBlocRewards) { } else if (isBlocRewards) {
tableHTML += ` tableHTML += `
<th class="sortable-header" style="text-align: right;" onclick="toggleSort('${categoryName}', 'amount')">Montant (🛡)${getSortArrow('amount', state.sortColumn, state.sortDirection)}</th> <th class="sortable-header" style="text-align: right;" onclick="toggleSort('${categoryName}', 'amount')">Montant (🛡)${getSortArrow('amount', state.sortColumn, state.sortDirection)}</th>
<th>Date</th> <th>Date</th>
<th class="sortable-header" onclick="toggleSort('${categoryName}', 'confirmations')">Confirmations${getSortArrow('confirmations', state.sortColumn, state.sortDirection)}</th> <th class="sortable-header" onclick="toggleSort('${categoryName}', 'confirmed')">Confirmé${getSortArrow('confirmed', state.sortColumn, state.sortDirection)}</th>
<th>Statut</th> <th>Statut</th>
`; `;
} else { } else {
@ -424,7 +474,8 @@
tableHTML += ` tableHTML += `
<th class="sortable-header" style="text-align: right;" onclick="toggleSort('${categoryName}', 'amount')">Montant (🛡)${getSortArrow('amount', state.sortColumn, state.sortDirection)}</th> <th class="sortable-header" style="text-align: right;" onclick="toggleSort('${categoryName}', 'amount')">Montant (🛡)${getSortArrow('amount', state.sortColumn, state.sortDirection)}</th>
<th class="sortable-header" style="text-align: right;" onclick="toggleSort('${categoryName}', 'amount')">Montant (✅)${getSortArrow('amount', state.sortColumn, state.sortDirection)}</th> <th class="sortable-header" style="text-align: right;" onclick="toggleSort('${categoryName}', 'amount')">Montant (✅)${getSortArrow('amount', state.sortColumn, state.sortDirection)}</th>
<th class="sortable-header" onclick="toggleSort('${categoryName}', 'confirmations')">Confirmations${getSortArrow('confirmations', state.sortColumn, state.sortDirection)}</th> <th>Date</th>
<th class="sortable-header" onclick="toggleSort('${categoryName}', 'confirmed')">Confirmé${getSortArrow('confirmed', state.sortColumn, state.sortDirection)}</th>
<th>Type</th> <th>Type</th>
<th>Statut</th> <th>Statut</th>
`; `;
@ -432,7 +483,8 @@
tableHTML += ` tableHTML += `
<th class="sortable-header" style="text-align: right;" onclick="toggleSort('${categoryName}', 'amount')">Montant (🛡)${getSortArrow('amount', state.sortColumn, state.sortDirection)}</th> <th class="sortable-header" style="text-align: right;" onclick="toggleSort('${categoryName}', 'amount')">Montant (🛡)${getSortArrow('amount', state.sortColumn, state.sortDirection)}</th>
<th class="sortable-header" style="text-align: right;" onclick="toggleSort('${categoryName}', 'amount')">Montant (✅)${getSortArrow('amount', state.sortColumn, state.sortDirection)}</th> <th class="sortable-header" style="text-align: right;" onclick="toggleSort('${categoryName}', 'amount')">Montant (✅)${getSortArrow('amount', state.sortColumn, state.sortDirection)}</th>
<th class="sortable-header" onclick="toggleSort('${categoryName}', 'confirmations')">Confirmations${getSortArrow('confirmations', state.sortColumn, state.sortDirection)}</th> <th>Date</th>
<th class="sortable-header" onclick="toggleSort('${categoryName}', 'confirmed')">Confirmé${getSortArrow('confirmed', state.sortColumn, state.sortDirection)}</th>
<th>Statut</th> <th>Statut</th>
`; `;
} }
@ -453,11 +505,17 @@
tableHTML += '<tr>'; tableHTML += '<tr>';
tableHTML += `<td class="txid-cell"><a href="${txLink}" target="_blank" rel="noopener noreferrer" class="txid-link">${utxo.txid}</a></td>`; tableHTML += `<td class="txid-cell"><a href="${txLink}" target="_blank" rel="noopener noreferrer" class="txid-link">${utxo.txid}</a></td>`;
tableHTML += `<td>${utxo.vout}</td>`; tableHTML += `<td>${utxo.vout}</td>`;
tableHTML += `<td class="address-cell">${utxo.address || '-'}</td>`;
if (isAnchors) { if (isAnchors) {
tableHTML += `<td>${utxo.blockHeight !== null && utxo.blockHeight !== undefined ? utxo.blockHeight.toLocaleString('fr-FR') : '-'}</td>`; tableHTML += `<td>${utxo.blockHeight !== null && utxo.blockHeight !== undefined ? utxo.blockHeight.toLocaleString('fr-FR') : '-'}</td>`;
tableHTML += `<td class="amount-cell">${amountSats.toLocaleString('fr-FR')} ✅</td>`; tableHTML += `<td class="amount-cell">${amountSats.toLocaleString('fr-FR')} ✅</td>`;
// Date pour les ancrages
if (utxo.blockTime) {
const date = new Date(utxo.blockTime * 1000);
tableHTML += `<td>${date.toLocaleString('fr-FR', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' })}</td>`;
} else {
tableHTML += '<td>-</td>';
}
} else if (isBlocRewards) { } else if (isBlocRewards) {
tableHTML += `<td class="amount-cell">${amountBTC.toLocaleString('fr-FR')} 🛡</td>`; tableHTML += `<td class="amount-cell">${amountBTC.toLocaleString('fr-FR')} 🛡</td>`;
if (utxo.blockTime) { if (utxo.blockTime) {
@ -469,9 +527,18 @@
} else { } else {
tableHTML += `<td class="amount-cell">${amountBTC.toLocaleString('fr-FR')} 🛡</td>`; tableHTML += `<td class="amount-cell">${amountBTC.toLocaleString('fr-FR')} 🛡</td>`;
tableHTML += `<td class="amount-cell">${amountSats.toLocaleString('fr-FR')} ✅</td>`; tableHTML += `<td class="amount-cell">${amountSats.toLocaleString('fr-FR')} ✅</td>`;
// Date pour les changes
if (utxo.blockTime) {
const date = new Date(utxo.blockTime * 1000);
tableHTML += `<td>${date.toLocaleString('fr-FR', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' })}</td>`;
} else {
tableHTML += '<td>-</td>';
}
} }
tableHTML += `<td>${utxo.confirmations.toLocaleString('fr-FR')}</td>`; // Colonne Confirmé : coche si confirmé, sinon vide
const isConfirmed = (utxo.confirmations || 0) > 0;
tableHTML += `<td style="text-align: center;">${isConfirmed ? '✓' : ''}</td>`;
// Colonne Type (uniquement pour les changes) // Colonne Type (uniquement pour les changes)
if (categoryName === 'changes') { if (categoryName === 'changes') {
@ -479,10 +546,13 @@
tableHTML += `<td>${changeType}</td>`; tableHTML += `<td>${changeType}</td>`;
} }
// Colonne Statut // Colonne Statut (— si source fichier, pas de RPC)
let statusText = ''; let statusText = '';
let statusClass = ''; let statusClass = '';
if (utxo.isSpentOnchain) { if (utxo.fromFile) {
statusText = '—';
statusClass = '';
} else if (utxo.isSpentOnchain) {
statusText = 'Dépensé onchain'; statusText = 'Dépensé onchain';
statusClass = 'status-spent'; statusClass = 'status-spent';
} else if (utxo.isLockedInMutex) { } else if (utxo.isLockedInMutex) {
@ -533,10 +603,10 @@
const data = await response.json(); const data = await response.json();
const count = data.count || 0; const count = data.count || 0;
const totalSats = data.totalSats || 0;
if (count > 0) { if (count > 0) {
button.textContent = `Consolider la capacité d'ancrage résiduelle (${count.toLocaleString('fr-FR')} UTXOs, ${totalSats.toLocaleString('fr-FR')} ✅)`; const anchorCount = Math.max(0, count - 1);
button.textContent = `Consolider la capacité d'ancrage résiduelle - ${anchorCount.toLocaleString('fr-FR')} ancrages)`;
button.disabled = false; button.disabled = false;
} else { } else {
button.textContent = 'Aucun UTXO à consolider'; button.textContent = 'Aucun UTXO à consolider';
@ -552,46 +622,112 @@
async function loadUtxoList() { async function loadUtxoList() {
const contentDiv = document.getElementById('content'); const contentDiv = document.getElementById('content');
const refreshButton = document.querySelector('.refresh-button'); const refreshButton = document.querySelector('.refresh-button');
const progressSection = document.getElementById('progress-section');
const progressBarFill = document.getElementById('progress-bar-fill');
const progressPercent = document.getElementById('progress-percent');
const progressStats = document.getElementById('progress-stats');
refreshButton.disabled = true; refreshButton.disabled = true;
contentDiv.innerHTML = '<div class="loading">Chargement des UTXO...</div>'; contentDiv.innerHTML = '';
utxosFromFile = true;
progressSection.style.display = 'block';
progressBarFill.style.width = '0%';
progressPercent.textContent = '0 %';
progressStats.textContent = '0 ligne(s) parsée(s)';
// Charger les infos des petits UTXOs en parallèle
loadSmallUtxosInfo(); loadSmallUtxosInfo();
try { try {
const response = await fetch(`${API_BASE_URL}/api/utxo/list`); const response = await fetch(`${API_BASE_URL}/api/utxo/list.txt`);
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }
const data = await response.json(); const contentLength = response.headers.get('Content-Length');
const counts = data.counts || {}; const totalBytes = contentLength ? parseInt(contentLength, 10) : 0;
const blocRewards = data.blocRewards || []; const lastModified = response.headers.get('Last-Modified');
const anchors = data.anchors || []; if (lastModified && document.getElementById('source-note')) {
const changes = data.changes || []; document.getElementById('source-note').textContent =
const fees = data.fees || []; 'Source : fichier utxo_list.txt (peut ne pas être à jour). Dernière modif. : ' + new Date(lastModified).toLocaleString('fr-FR');
}
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
let received = 0;
let buffer = '';
let lineCount = 0;
const blocRewards = [];
const anchors = [];
const changes = [];
const fees = [];
const minAnchorAmount = 2000 / 100000000;
for (;;) {
const { done, value } = await reader.read();
if (value && value.length) {
received += value.length;
buffer += decoder.decode(value, { stream: !done });
}
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed) continue;
const utxo = parseUtxoLine(trimmed);
if (!utxo) continue;
lineCount++;
if (utxo.category === 'bloc_rewards') blocRewards.push(utxo);
else if (utxo.category === 'anchor') anchors.push(utxo);
else if (utxo.category === 'change') changes.push(utxo);
else if (utxo.category === 'fee') fees.push(utxo);
}
const pct = totalBytes > 0 ? Math.min(100, Math.round((received / totalBytes) * 100)) : (done ? 100 : 0);
progressBarFill.style.width = pct + '%';
progressPercent.textContent = pct + ' %';
progressStats.textContent = lineCount.toLocaleString('fr-FR') + ' ligne(s) parsée(s)';
if (done) break;
}
if (buffer.trim()) {
const utxo = parseUtxoLine(buffer.trim());
if (utxo) {
lineCount++;
if (utxo.category === 'bloc_rewards') blocRewards.push(utxo);
else if (utxo.category === 'anchor') anchors.push(utxo);
else if (utxo.category === 'change') changes.push(utxo);
else if (utxo.category === 'fee') fees.push(utxo);
}
}
progressBarFill.style.width = '100%';
progressPercent.textContent = '100 %';
progressStats.textContent = lineCount.toLocaleString('fr-FR') + ' ligne(s) parsée(s)';
const total = blocRewards.length + anchors.length + changes.length + fees.length;
const availableForAnchor = anchors.filter(u =>
u.amount >= minAnchorAmount && (u.confirmations || 0) > 0
).length;
const totalAmount = blocRewards.reduce((s, u) => s + u.amount, 0) +
anchors.reduce((s, u) => s + u.amount, 0) +
changes.reduce((s, u) => s + u.amount, 0);
// Sauvegarder les données pour la pagination
currentUtxosData = { blocRewards, anchors, changes, fees }; currentUtxosData = { blocRewards, anchors, changes, fees };
document.getElementById('utxo-count').textContent = counts.total.toLocaleString('fr-FR'); document.getElementById('utxo-count').textContent = total.toLocaleString('fr-FR');
document.getElementById('available-for-anchor').textContent = (counts.availableForAnchor || 0).toLocaleString('fr-FR'); document.getElementById('available-for-anchor').textContent = availableForAnchor.toLocaleString('fr-FR');
document.getElementById('confirmed-available-for-anchor').textContent = (counts.confirmedAvailableForAnchor || 0).toLocaleString('fr-FR');
// Calculer le montant total
const totalAmount = blocRewards.reduce((sum, utxo) => sum + utxo.amount, 0) +
anchors.reduce((sum, utxo) => sum + utxo.amount, 0) +
changes.reduce((sum, utxo) => sum + utxo.amount, 0);
document.getElementById('total-amount').textContent = formatBTC(totalAmount); document.getElementById('total-amount').textContent = formatBTC(totalAmount);
updateLastUpdateTime(); updateLastUpdateTime();
// Afficher la barre de navigation progressSection.style.display = 'none';
document.getElementById('navigation').style.display = 'flex'; document.getElementById('navigation').style.display = 'flex';
// Afficher les 4 listes
let html = ''; let html = '';
html += renderTable(blocRewards, 'bloc-rewards', '💰 Bloc Rewards (Récompenses de minage)'); html += renderTable(blocRewards, 'bloc-rewards', '💰 Bloc Rewards (Récompenses de minage)');
html += renderTable(anchors, 'ancrages', '🔗 Ancrages (Transactions d\'ancrage)'); html += renderTable(anchors, 'ancrages', '🔗 Ancrages (Transactions d\'ancrage)');
@ -601,14 +737,47 @@
contentDiv.innerHTML = html; contentDiv.innerHTML = html;
} catch (error) { } catch (error) {
console.error('Error loading UTXO list:', error); console.error('Error loading UTXO list:', error);
progressSection.style.display = 'none';
contentDiv.innerHTML = `<div class="error">Erreur lors du chargement de la liste des UTXO : ${error.message}</div>`; contentDiv.innerHTML = `<div class="error">Erreur lors du chargement de la liste des UTXO : ${error.message}</div>`;
} finally { } finally {
refreshButton.disabled = false; refreshButton.disabled = false;
// Recharger les infos des petits UTXOs après le chargement de la liste
loadSmallUtxosInfo(); loadSmallUtxosInfo();
} }
} }
function parseUtxoLine(line) {
const parts = line.split(';');
if (parts.length < 6) return null;
let category, txid, vout, amount, confirmations, isAnchorChange, blockTimeRaw;
if (parts.length >= 7 && !isNaN(parseFloat(parts[3]))) {
[category, txid, vout, amount, confirmations, isAnchorChange, blockTimeRaw] = parts;
} else {
[category, txid, vout, , amount, confirmations] = parts;
isAnchorChange = parts.length > 6 ? parts[6] === 'true' : false;
blockTimeRaw = parts.length > 7 ? parts[7] : null;
}
const c = (category || '').trim();
if (c !== 'bloc_rewards' && c !== 'anchor' && c !== 'change' && c !== 'fee') return null;
const blockTime = (blockTimeRaw && blockTimeRaw.trim()) ? (parseInt(blockTimeRaw, 10) || null) : null;
return {
txid: (txid || '').trim(),
vout: parseInt(vout, 10) || 0,
amount: parseFloat(amount) || 0,
confirmations: parseInt(confirmations, 10) || 0,
blockHeight: null,
blockTime,
isAnchorChange: isAnchorChange === 'true' || isAnchorChange === true,
category: c,
isSpentOnchain: false,
isLockedInMutex: false,
fromFile: true,
};
}
function formatBTC(btc) { function formatBTC(btc) {
if (btc === 0) return '0 🛡'; if (btc === 0) return '0 🛡';
// Arrondir sans décimales // Arrondir sans décimales
@ -664,11 +833,10 @@
<th>Transaction ID</th> <th>Transaction ID</th>
<th>Frais (BTC)</th> <th>Frais (BTC)</th>
<th>Frais (✅)</th> <th>Frais (✅)</th>
<th>Change Address</th>
<th>Change Amount</th> <th>Change Amount</th>
<th>Bloc</th> <th>Bloc</th>
<th>Date</th> <th>Date</th>
<th>Confirmations</th> <th>Confirmé</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -683,7 +851,6 @@
tableHTML += `<td class="txid-cell"><a href="${txLink}" target="_blank" rel="noopener noreferrer" class="txid-link">${fee.txid}</a></td>`; tableHTML += `<td class="txid-cell"><a href="${txLink}" target="_blank" rel="noopener noreferrer" class="txid-link">${fee.txid}</a></td>`;
tableHTML += `<td class="amount-cell">${fee.fee.toFixed(8)}</td>`; tableHTML += `<td class="amount-cell">${fee.fee.toFixed(8)}</td>`;
tableHTML += `<td class="amount-cell">${feeSats.toLocaleString('fr-FR')} ✅</td>`; tableHTML += `<td class="amount-cell">${feeSats.toLocaleString('fr-FR')} ✅</td>`;
tableHTML += `<td class="address-cell">${fee.changeAddress || '-'}</td>`;
tableHTML += `<td class="amount-cell">${fee.changeAmount ? formatBTC(fee.changeAmount) : '-'}</td>`; tableHTML += `<td class="amount-cell">${fee.changeAmount ? formatBTC(fee.changeAmount) : '-'}</td>`;
tableHTML += `<td>${fee.blockHeight !== null && fee.blockHeight !== undefined ? fee.blockHeight.toLocaleString('fr-FR') : '-'}</td>`; tableHTML += `<td>${fee.blockHeight !== null && fee.blockHeight !== undefined ? fee.blockHeight.toLocaleString('fr-FR') : '-'}</td>`;
if (fee.blockTime) { if (fee.blockTime) {
@ -692,7 +859,9 @@
} else { } else {
tableHTML += '<td>-</td>'; tableHTML += '<td>-</td>';
} }
tableHTML += `<td>${fee.confirmations.toLocaleString('fr-FR')}</td>`; // Colonne Confirmé : coche si confirmé, sinon vide
const isConfirmed = (fee.confirmations || 0) > 0;
tableHTML += `<td style="text-align: center;">${isConfirmed ? '✓' : ''}</td>`;
tableHTML += '</tr>'; tableHTML += '</tr>';
}); });

View File

@ -393,15 +393,20 @@ app.post('/api/utxo/consolidate', async (req, res) => {
} }
}); });
// Route pour servir le fichier texte des UTXO // Route pour servir le fichier texte des UTXO (utilisé pour chargement progressif)
app.get('/api/utxo/list.txt', async (req, res) => { app.get('/api/utxo/list.txt', async (req, res) => {
try { try {
const { readFileSync, existsSync } = await import('fs'); const { readFileSync, existsSync, statSync } = await import('fs');
const utxoListPath = join(__dirname, '../../utxo_list.txt'); const utxoListPath = join(__dirname, '../../utxo_list.txt');
if (existsSync(utxoListPath)) { if (existsSync(utxoListPath)) {
const content = readFileSync(utxoListPath, 'utf8'); const content = readFileSync(utxoListPath, 'utf8');
const stat = statSync(utxoListPath);
res.setHeader('Content-Type', 'text/plain; charset=utf-8'); res.setHeader('Content-Type', 'text/plain; charset=utf-8');
res.setHeader('Content-Length', Buffer.byteLength(content, 'utf8'));
if (stat.mtime) {
res.setHeader('Last-Modified', stat.mtime.toUTCString());
}
res.send(content); res.send(content);
} else { } else {
res.status(404).send('UTXO list file not found'); res.status(404).send('UTXO list file not found');