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:
parent
078b36d404
commit
076b054b70
60
features/utxo-list-progressive-loading.md
Normal file
60
features/utxo-list-progressive-loading.md
Normal 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 l’affichage 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 d’info 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 d’analyse
|
||||||
|
|
||||||
|
- 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 s’affiche puis disparaît une fois le chargement terminé.
|
||||||
|
- Vérifier que les quatre tableaux (Bloc Rewards, Ancrages, Changes, Frais) et la pagination s’affichent 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`
|
||||||
@ -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>';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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');
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user