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;
|
||||
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 {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
@ -263,7 +299,7 @@
|
||||
<h1>Liste des UTXO</h1>
|
||||
<div class="info-section">
|
||||
<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>
|
||||
<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>
|
||||
@ -282,6 +318,15 @@
|
||||
<div id="content">
|
||||
<div class="loading">Chargement des UTXO...</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>
|
||||
|
||||
@ -291,6 +336,10 @@
|
||||
|
||||
// État de tri et pagination pour chaque catégorie
|
||||
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
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
@ -316,9 +365,10 @@
|
||||
if (sortColumn === 'amount') {
|
||||
aVal = a.amount;
|
||||
bVal = b.amount;
|
||||
} else if (sortColumn === 'confirmations') {
|
||||
aVal = a.confirmations || 0;
|
||||
bVal = b.confirmations || 0;
|
||||
} else if (sortColumn === 'confirmed') {
|
||||
// Trier par confirmé/non confirmé (booléen)
|
||||
aVal = (a.confirmations || 0) > 0 ? 1 : 0;
|
||||
bVal = (b.confirmations || 0) > 0 ? 1 : 0;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
@ -401,21 +451,21 @@
|
||||
<tr>
|
||||
<th>Transaction ID</th>
|
||||
<th>Vout</th>
|
||||
<th>Adresse</th>
|
||||
`;
|
||||
|
||||
if (isAnchors) {
|
||||
tableHTML += `
|
||||
<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" 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>
|
||||
`;
|
||||
} else if (isBlocRewards) {
|
||||
tableHTML += `
|
||||
<th class="sortable-header" style="text-align: right;" onclick="toggleSort('${categoryName}', 'amount')">Montant (🛡)${getSortArrow('amount', state.sortColumn, state.sortDirection)}</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>
|
||||
`;
|
||||
} else {
|
||||
@ -424,7 +474,8 @@
|
||||
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" 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>Statut</th>
|
||||
`;
|
||||
@ -432,7 +483,8 @@
|
||||
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" 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>
|
||||
`;
|
||||
}
|
||||
@ -453,11 +505,17 @@
|
||||
tableHTML += '<tr>';
|
||||
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 class="address-cell">${utxo.address || '-'}</td>`;
|
||||
|
||||
if (isAnchors) {
|
||||
tableHTML += `<td>${utxo.blockHeight !== null && utxo.blockHeight !== undefined ? utxo.blockHeight.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) {
|
||||
tableHTML += `<td class="amount-cell">${amountBTC.toLocaleString('fr-FR')} 🛡</td>`;
|
||||
if (utxo.blockTime) {
|
||||
@ -469,9 +527,18 @@
|
||||
} else {
|
||||
tableHTML += `<td class="amount-cell">${amountBTC.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)
|
||||
if (categoryName === 'changes') {
|
||||
@ -479,10 +546,13 @@
|
||||
tableHTML += `<td>${changeType}</td>`;
|
||||
}
|
||||
|
||||
// Colonne Statut
|
||||
// Colonne Statut (— si source fichier, pas de RPC)
|
||||
let statusText = '';
|
||||
let statusClass = '';
|
||||
if (utxo.isSpentOnchain) {
|
||||
if (utxo.fromFile) {
|
||||
statusText = '—';
|
||||
statusClass = '';
|
||||
} else if (utxo.isSpentOnchain) {
|
||||
statusText = 'Dépensé onchain';
|
||||
statusClass = 'status-spent';
|
||||
} else if (utxo.isLockedInMutex) {
|
||||
@ -533,10 +603,10 @@
|
||||
|
||||
const data = await response.json();
|
||||
const count = data.count || 0;
|
||||
const totalSats = data.totalSats || 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;
|
||||
} else {
|
||||
button.textContent = 'Aucun UTXO à consolider';
|
||||
@ -552,46 +622,112 @@
|
||||
async function loadUtxoList() {
|
||||
const contentDiv = document.getElementById('content');
|
||||
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;
|
||||
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();
|
||||
|
||||
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) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const counts = data.counts || {};
|
||||
const blocRewards = data.blocRewards || [];
|
||||
const anchors = data.anchors || [];
|
||||
const changes = data.changes || [];
|
||||
const fees = data.fees || [];
|
||||
const contentLength = response.headers.get('Content-Length');
|
||||
const totalBytes = contentLength ? parseInt(contentLength, 10) : 0;
|
||||
const lastModified = response.headers.get('Last-Modified');
|
||||
if (lastModified && document.getElementById('source-note')) {
|
||||
document.getElementById('source-note').textContent =
|
||||
'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 };
|
||||
|
||||
document.getElementById('utxo-count').textContent = counts.total.toLocaleString('fr-FR');
|
||||
document.getElementById('available-for-anchor').textContent = (counts.availableForAnchor || 0).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('utxo-count').textContent = total.toLocaleString('fr-FR');
|
||||
document.getElementById('available-for-anchor').textContent = availableForAnchor.toLocaleString('fr-FR');
|
||||
document.getElementById('total-amount').textContent = formatBTC(totalAmount);
|
||||
|
||||
updateLastUpdateTime();
|
||||
|
||||
// Afficher la barre de navigation
|
||||
progressSection.style.display = 'none';
|
||||
document.getElementById('navigation').style.display = 'flex';
|
||||
|
||||
// Afficher les 4 listes
|
||||
let html = '';
|
||||
html += renderTable(blocRewards, 'bloc-rewards', '💰 Bloc Rewards (Récompenses de minage)');
|
||||
html += renderTable(anchors, 'ancrages', '🔗 Ancrages (Transactions d\'ancrage)');
|
||||
@ -601,14 +737,47 @@
|
||||
contentDiv.innerHTML = html;
|
||||
} catch (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>`;
|
||||
} finally {
|
||||
refreshButton.disabled = false;
|
||||
// Recharger les infos des petits UTXOs après le chargement de la liste
|
||||
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) {
|
||||
if (btc === 0) return '0 🛡';
|
||||
// Arrondir sans décimales
|
||||
@ -664,11 +833,10 @@
|
||||
<th>Transaction ID</th>
|
||||
<th>Frais (BTC)</th>
|
||||
<th>Frais (✅)</th>
|
||||
<th>Change Address</th>
|
||||
<th>Change Amount</th>
|
||||
<th>Bloc</th>
|
||||
<th>Date</th>
|
||||
<th>Confirmations</th>
|
||||
<th>Confirmé</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<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="amount-cell">${fee.fee.toFixed(8)}</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>${fee.blockHeight !== null && fee.blockHeight !== undefined ? fee.blockHeight.toLocaleString('fr-FR') : '-'}</td>`;
|
||||
if (fee.blockTime) {
|
||||
@ -692,7 +859,9 @@
|
||||
} else {
|
||||
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>';
|
||||
});
|
||||
|
||||
|
||||
@ -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) => {
|
||||
try {
|
||||
const { readFileSync, existsSync } = await import('fs');
|
||||
const { readFileSync, existsSync, statSync } = await import('fs');
|
||||
const utxoListPath = join(__dirname, '../../utxo_list.txt');
|
||||
|
||||
if (existsSync(utxoListPath)) {
|
||||
const content = readFileSync(utxoListPath, 'utf8');
|
||||
const stat = statSync(utxoListPath);
|
||||
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);
|
||||
} else {
|
||||
res.status(404).send('UTXO list file not found');
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user