Add transaction statistics endpoints and systemd services
**Motivations:** - Add transaction statistics endpoints (avg-fee, avg-amount) for dashboard display - Fix difficulty parsing issue when Bitcoin RPC returns string instead of number - Add systemd service files for proper service management - Update dashboard UI to display transaction statistics **Root causes:** - Bitcoin RPC can return difficulty as string, causing type issues - Missing API endpoints for transaction statistics in dashboard **Correctifs:** - Fix difficulty parsing to handle both string and number types - Add proper type conversion for blockchainInfo.difficulty **Evolutions:** - Add /api/transactions/avg-fee endpoint returning fixed 1200 sats fee - Add /api/transactions/avg-amount endpoint returning fixed 1000 sats amount - Add systemd service files for api-faucet and signet-dashboard - Enhance dashboard UI with transaction statistics display - Update anchor_count.txt **Pages affectées:** - signet-dashboard/src/server.js - signet-dashboard/public/app.js - signet-dashboard/public/index.html - signet-dashboard/signet-dashboard.service - api-faucet/faucet-api.service - anchor_count.txt
This commit is contained in:
parent
21438530a1
commit
b3973ddc41
@ -1 +1 @@
|
||||
2026-01-25T01:33:13.690Z;7182;00000002fe6bb5f10aa5f01688bc0e6f862df0e4a4571babd2df5dd30d919b0b;9752
|
||||
2026-01-25T02:55:07.388Z;7419;00000016a18555e0e95e3214e128029e8c86e8bbbe68c62d240821a6a2951061;10558
|
||||
21
api-faucet/faucet-api.service
Normal file
21
api-faucet/faucet-api.service
Normal file
@ -0,0 +1,21 @@
|
||||
[Unit]
|
||||
Description=Bitcoin Signet Faucet API
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=ncantu
|
||||
WorkingDirectory=/home/ncantu/Bureau/code/bitcoin/api-faucet
|
||||
Environment=NODE_ENV=production
|
||||
ExecStart=/usr/bin/node /home/ncantu/Bureau/code/bitcoin/api-faucet/src/server.js
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
# Sécurité
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@ -15,13 +15,50 @@ if (window.location.hostname.includes('dashboard.certificator.4nkweb.com')) {
|
||||
}
|
||||
|
||||
let selectedFile = null;
|
||||
let lastBlockHeight = null;
|
||||
let blockPollingInterval = null;
|
||||
|
||||
// Initialisation
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadData();
|
||||
setInterval(loadData, 30000); // Rafraîchir toutes les 30 secondes
|
||||
|
||||
// Démarrer le polling pour détecter les nouveaux blocs
|
||||
startBlockPolling();
|
||||
});
|
||||
|
||||
/**
|
||||
* Démarrer le polling pour détecter les nouveaux blocs
|
||||
*/
|
||||
function startBlockPolling() {
|
||||
// Vérifier la hauteur du bloc toutes les 5 secondes
|
||||
blockPollingInterval = setInterval(async () => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/blockchain/info`);
|
||||
if (!response.ok) return;
|
||||
|
||||
const data = await response.json();
|
||||
const currentHeight = data.blocks;
|
||||
|
||||
// Si la hauteur a changé, actualiser les métriques
|
||||
if (lastBlockHeight !== null && currentHeight > lastBlockHeight) {
|
||||
console.log(`Nouveau bloc détecté: ${lastBlockHeight} -> ${currentHeight}`);
|
||||
// Actualiser uniquement les 4 métriques spécifiques
|
||||
await Promise.all([
|
||||
loadAvgFee(),
|
||||
loadAvgTxAmount(),
|
||||
loadMiningDifficulty(),
|
||||
loadAvgBlockTime(),
|
||||
]);
|
||||
}
|
||||
|
||||
lastBlockHeight = currentHeight;
|
||||
} catch (error) {
|
||||
console.error('Error checking block height:', error);
|
||||
}
|
||||
}, 5000); // Vérifier toutes les 5 secondes
|
||||
}
|
||||
|
||||
/**
|
||||
* Charge toutes les données de supervision
|
||||
*/
|
||||
@ -35,6 +72,8 @@ async function loadData() {
|
||||
loadNetworkPeers(),
|
||||
loadMiningDifficulty(),
|
||||
loadAvgBlockTime(),
|
||||
loadAvgFee(),
|
||||
loadAvgTxAmount(),
|
||||
]);
|
||||
|
||||
updateLastUpdateTime();
|
||||
@ -52,6 +91,10 @@ async function loadBlockchainInfo() {
|
||||
const data = await response.json();
|
||||
|
||||
document.getElementById('block-height').textContent = data.blocks || 0;
|
||||
// Initialiser lastBlockHeight si ce n'est pas déjà fait
|
||||
if (lastBlockHeight === null && data.blocks !== undefined) {
|
||||
lastBlockHeight = data.blocks;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading blockchain info:', error);
|
||||
document.getElementById('block-height').textContent = 'Erreur';
|
||||
@ -165,15 +208,27 @@ async function loadNetworkPeers() {
|
||||
async function loadMiningDifficulty() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/mining/difficulty`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
|
||||
if (data.difficulty !== undefined) {
|
||||
if (data.difficulty !== undefined && data.difficulty !== null) {
|
||||
// Convertir en nombre si nécessaire (sécurité supplémentaire)
|
||||
const difficulty = typeof data.difficulty === 'string'
|
||||
? parseFloat(data.difficulty)
|
||||
: Number(data.difficulty);
|
||||
|
||||
if (!isNaN(difficulty)) {
|
||||
// Formater la difficulté avec séparateurs de milliers
|
||||
const formatted = formatDifficulty(data.difficulty);
|
||||
const formatted = formatDifficulty(difficulty);
|
||||
document.getElementById('mining-difficulty').textContent = formatted;
|
||||
} else {
|
||||
document.getElementById('mining-difficulty').textContent = '-';
|
||||
}
|
||||
} else {
|
||||
document.getElementById('mining-difficulty').textContent = '-';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading mining difficulty:', error);
|
||||
document.getElementById('mining-difficulty').textContent = 'Erreur';
|
||||
@ -184,20 +239,42 @@ async function loadMiningDifficulty() {
|
||||
* Charge le temps moyen entre blocs
|
||||
*/
|
||||
async function loadAvgBlockTime() {
|
||||
const avgBlockTimeValue = document.getElementById('avg-block-time-value');
|
||||
const avgBlockTimeSpinner = document.getElementById('avg-block-time-spinner');
|
||||
|
||||
if (!avgBlockTimeValue || !avgBlockTimeSpinner) {
|
||||
console.error('Elements avg-block-time-value or avg-block-time-spinner not found in DOM');
|
||||
return;
|
||||
}
|
||||
|
||||
// Afficher le spinner
|
||||
avgBlockTimeSpinner.style.display = 'inline';
|
||||
avgBlockTimeValue.textContent = '...';
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/mining/avg-block-time`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Masquer le spinner et mettre à jour la valeur
|
||||
avgBlockTimeSpinner.style.display = 'none';
|
||||
|
||||
if (data.formatted) {
|
||||
document.getElementById('avg-block-time').textContent = data.formatted;
|
||||
avgBlockTimeValue.textContent = data.formatted;
|
||||
} else if (data.timeAvgSeconds !== undefined) {
|
||||
document.getElementById('avg-block-time').textContent = formatBlockTime(data.timeAvgSeconds);
|
||||
avgBlockTimeValue.textContent = formatBlockTime(data.timeAvgSeconds);
|
||||
} else {
|
||||
document.getElementById('avg-block-time').textContent = '-';
|
||||
avgBlockTimeValue.textContent = '-';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading average block time:', error);
|
||||
document.getElementById('avg-block-time').textContent = 'Erreur';
|
||||
// Masquer le spinner
|
||||
avgBlockTimeSpinner.style.display = 'none';
|
||||
avgBlockTimeValue.textContent = 'Erreur';
|
||||
}
|
||||
}
|
||||
|
||||
@ -230,6 +307,129 @@ function formatBlockTime(seconds) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formate un montant en sats avec l'emoji ✅
|
||||
*/
|
||||
function formatSats(sats) {
|
||||
if (sats === 0) return '0 ✅';
|
||||
if (sats < 1000) return sats.toLocaleString('fr-FR') + ' ✅';
|
||||
if (sats < 1000000) return (sats / 1000).toFixed(2) + ' K ✅';
|
||||
if (sats < 1000000000) return (sats / 1000000).toFixed(2) + ' M ✅';
|
||||
return (sats / 1000000000).toFixed(2) + ' G ✅';
|
||||
}
|
||||
|
||||
/**
|
||||
* Formate un montant en sats avec l'emoji ✅ (sans préfixes K/M/G)
|
||||
*/
|
||||
function formatSatsSimple(sats) {
|
||||
if (sats === 0) return '0 ✅';
|
||||
return sats.toLocaleString('fr-FR') + ' ✅';
|
||||
}
|
||||
|
||||
/**
|
||||
* Charge les frais moyens
|
||||
*/
|
||||
async function loadAvgFee() {
|
||||
const avgFeeValue = document.getElementById('avg-fee-value');
|
||||
const avgFeeSpinner = document.getElementById('avg-fee-spinner');
|
||||
|
||||
if (!avgFeeValue || !avgFeeSpinner) {
|
||||
console.error('Elements avg-fee-value or avg-fee-spinner not found in DOM');
|
||||
return;
|
||||
}
|
||||
|
||||
// Afficher le spinner
|
||||
avgFeeSpinner.style.display = 'inline';
|
||||
avgFeeValue.textContent = '...';
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/transactions/avg-fee`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Masquer le spinner et mettre à jour la valeur
|
||||
avgFeeSpinner.style.display = 'none';
|
||||
|
||||
if (data.avgFee !== undefined && data.avgFee !== null) {
|
||||
const avgFee = Number(data.avgFee);
|
||||
if (!isNaN(avgFee) && avgFee >= 0) {
|
||||
const formatted = formatSatsSimple(avgFee);
|
||||
avgFeeValue.textContent = formatted;
|
||||
} else {
|
||||
avgFeeValue.textContent = '-';
|
||||
}
|
||||
} else {
|
||||
avgFeeValue.textContent = '-';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading average fee', error);
|
||||
// Masquer le spinner
|
||||
avgFeeSpinner.style.display = 'none';
|
||||
avgFeeValue.textContent = 'Erreur';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Charge le montant moyen des transactions
|
||||
*/
|
||||
async function loadAvgTxAmount() {
|
||||
const avgTxAmountValue = document.getElementById('avg-tx-amount-value');
|
||||
const avgTxAmountSpinner = document.getElementById('avg-tx-amount-spinner');
|
||||
|
||||
if (!avgTxAmountValue || !avgTxAmountSpinner) {
|
||||
console.error('Elements avg-tx-amount-value or avg-tx-amount-spinner not found in DOM');
|
||||
return;
|
||||
}
|
||||
|
||||
// Afficher le spinner
|
||||
avgTxAmountSpinner.style.display = 'inline';
|
||||
avgTxAmountValue.textContent = '...';
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/transactions/avg-amount`, {
|
||||
signal: AbortSignal.timeout(60000) // Timeout de 60 secondes
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.error) {
|
||||
throw new Error(data.error);
|
||||
}
|
||||
|
||||
// Masquer le spinner et mettre à jour la valeur
|
||||
avgTxAmountSpinner.style.display = 'none';
|
||||
|
||||
if (data.avgAmount !== undefined && data.avgAmount !== null) {
|
||||
// Convertir en nombre si nécessaire
|
||||
const avgAmount = typeof data.avgAmount === 'string'
|
||||
? parseFloat(data.avgAmount)
|
||||
: Number(data.avgAmount);
|
||||
|
||||
if (!isNaN(avgAmount) && avgAmount >= 0) {
|
||||
const formatted = formatSatsSimple(avgAmount);
|
||||
avgTxAmountValue.textContent = formatted;
|
||||
} else {
|
||||
avgTxAmountValue.textContent = '-';
|
||||
}
|
||||
} else {
|
||||
avgTxAmountValue.textContent = '-';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading average transaction amount', error);
|
||||
// Masquer le spinner
|
||||
avgTxAmountSpinner.style.display = 'none';
|
||||
avgTxAmountValue.textContent = 'Erreur';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formate un montant en BTC
|
||||
*/
|
||||
|
||||
@ -60,7 +60,24 @@
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Temps Moyen entre Blocs</h3>
|
||||
<p class="value" id="avg-block-time">-</p>
|
||||
<p class="value" id="avg-block-time">
|
||||
<span id="avg-block-time-value">-</span>
|
||||
<span id="avg-block-time-spinner" class="spinner" style="display: none;">⏳</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Frais Moyens</h3>
|
||||
<p class="value" id="avg-fee">
|
||||
<span id="avg-fee-value">-</span>
|
||||
<span id="avg-fee-spinner" class="spinner" style="display: none;">⏳</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Montant Moyen des Transactions</h3>
|
||||
<p class="value" id="avg-tx-amount">
|
||||
<span id="avg-tx-amount-value">-</span>
|
||||
<span id="avg-tx-amount-spinner" class="spinner" style="display: none;">⏳</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
21
signet-dashboard/signet-dashboard.service
Normal file
21
signet-dashboard/signet-dashboard.service
Normal file
@ -0,0 +1,21 @@
|
||||
[Unit]
|
||||
Description=Bitcoin Signet Dashboard
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=ncantu
|
||||
WorkingDirectory=/home/ncantu/Bureau/code/bitcoin/signet-dashboard
|
||||
Environment=NODE_ENV=production
|
||||
ExecStart=/usr/bin/node /home/ncantu/Bureau/code/bitcoin/signet-dashboard/src/server.js
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
# Sécurité
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@ -201,7 +201,10 @@ app.get('/api/anchor/count', async (req, res) => {
|
||||
app.get('/api/mining/difficulty', async (req, res) => {
|
||||
try {
|
||||
const blockchainInfo = await bitcoinRPC.getBlockchainInfo();
|
||||
const difficulty = blockchainInfo.difficulty || 0;
|
||||
// Convertir la difficulté en nombre (peut être retournée comme chaîne par Bitcoin RPC)
|
||||
const difficulty = typeof blockchainInfo.difficulty === 'string'
|
||||
? parseFloat(blockchainInfo.difficulty)
|
||||
: (blockchainInfo.difficulty || 0);
|
||||
res.json({ difficulty });
|
||||
} catch (error) {
|
||||
logger.error('Error getting mining difficulty', { error: error.message });
|
||||
@ -239,6 +242,41 @@ app.get('/api/mining/avg-block-time', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/transactions/avg-fee', async (req, res) => {
|
||||
try {
|
||||
// Pour les transactions d'ancrage, le frais moyen est toujours 1200 sats
|
||||
// Utiliser directement cette valeur au lieu de calculer depuis les blocs
|
||||
const avgFee = 1200; // Frais moyen des transactions d'ancrage en sats
|
||||
const avgFeeRate = 0; // Non calculé
|
||||
|
||||
res.json({
|
||||
avgFee,
|
||||
avgFeeRate,
|
||||
txCount: 0
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error getting average fee', { error: error.message });
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/transactions/avg-amount', async (req, res) => {
|
||||
try {
|
||||
// Pour les ancrages, le montant est toujours 0.00001 BTC (1000 sats)
|
||||
// On retourne directement cette valeur pour éviter les calculs lents
|
||||
const avgAmount = 1000; // 0.00001 BTC en sats
|
||||
const txCount = 0; // Non calculé pour performance
|
||||
|
||||
res.json({
|
||||
avgAmount,
|
||||
txCount
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error getting average transaction amount', { error: error.message });
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Formate le temps moyen entre blocs en format lisible
|
||||
* @param {number} seconds - Temps en secondes
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user