**Motivations:** - Synchronisation des modifications sur l'API anchorage, les services et le website skeleton - Ajout de scripts de monitoring et de diagnostic pour l'API anchorage - Documentation des problèmes de mutex et de provisioning UTXO **Root causes:** - N/A (commit de synchronisation) **Correctifs:** - N/A (commit de synchronisation) **Evolutions:** - Ajout de scripts de monitoring et de diagnostic pour l'API anchorage - Amélioration de la gestion des mutex et des UTXOs - Mise à jour de la documentation **Pages affectées:** - api-anchorage/src/bitcoin-rpc.js - api-anchorage/src/routes/anchor.js - api-anchorage/src/routes/health.js - api-anchorage/src/server.js - api-anchorage/README-MONITORING.md - api-anchorage/cleanup-stale-locks.mjs - api-anchorage/diagnose.mjs - api-anchorage/unlock-utxos.mjs - service-login-verify/src/persistentNonceCache.ts - signet-dashboard/src/server.js - signet-dashboard/public/* - userwallet/src/hooks/useChannel.ts - userwallet/src/services/relayNotificationService.ts - userwallet/src/utils/defaultContract.ts - website-skeleton/src/* - docs/DOMAINS_AND_PORTS.md - docs/INTERFACES.md - features/* - fixKnowledge/*
1210 lines
44 KiB
JavaScript
1210 lines
44 KiB
JavaScript
/**
|
|
* Application JavaScript pour le Dashboard Bitcoin Signet
|
|
*/
|
|
|
|
const API_BASE_URL = window.location.origin;
|
|
// Utiliser le même origin pour le faucet (sera configuré via le proxy)
|
|
// Si on est sur dashboard.certificator.4nkweb.com, utiliser faucet.certificator.4nkweb.com
|
|
let FAUCET_API_URL;
|
|
if (window.location.hostname.includes('dashboard.certificator.4nkweb.com')) {
|
|
FAUCET_API_URL = window.location.origin.replace('dashboard.certificator.4nkweb.com', 'faucet.certificator.4nkweb.com');
|
|
} else if (window.location.hostname.includes('localhost') || window.location.hostname === '127.0.0.1') {
|
|
FAUCET_API_URL = 'http://localhost:3021';
|
|
} else {
|
|
FAUCET_API_URL = window.location.origin;
|
|
}
|
|
|
|
let selectedFile = null;
|
|
let selectedClamavFile = null;
|
|
let selectedFiligraneFile = null;
|
|
let lastBlockHeight = null;
|
|
let blockPollingInterval = null;
|
|
let dataRefreshInterval = null;
|
|
|
|
// Initialisation
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
loadData();
|
|
dataRefreshInterval = setInterval(loadData, 30000); // Rafraîchir toutes les 30 secondes
|
|
|
|
// Démarrer le polling pour détecter les nouveaux blocs
|
|
startBlockPolling();
|
|
});
|
|
|
|
// Nettoyer les intervalles lors du déchargement de la page
|
|
window.addEventListener('beforeunload', () => {
|
|
if (blockPollingInterval) {
|
|
clearInterval(blockPollingInterval);
|
|
blockPollingInterval = null;
|
|
}
|
|
if (dataRefreshInterval) {
|
|
clearInterval(dataRefreshInterval);
|
|
dataRefreshInterval = null;
|
|
}
|
|
});
|
|
|
|
/**
|
|
* 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
|
|
* Optimisé pour charger d'abord les données critiques, puis les données moins critiques
|
|
*/
|
|
async function loadData() {
|
|
try {
|
|
// Phase 1 : Charger les données critiques rapidement (affichage immédiat)
|
|
await Promise.all([
|
|
loadBlockchainInfo(),
|
|
loadLatestBlock(),
|
|
loadWalletBalance(),
|
|
loadAnchorCount(),
|
|
loadNetworkPeers(),
|
|
loadMiningDifficulty(),
|
|
]);
|
|
|
|
updateLastUpdateTime();
|
|
|
|
// Phase 2 : Charger les données moins critiques en arrière-plan (peuvent être plus lentes)
|
|
// Ne pas attendre ces données pour mettre à jour l'heure
|
|
Promise.all([
|
|
loadAvailableForAnchor(),
|
|
loadAvgBlockTime(),
|
|
loadAvgFee(),
|
|
loadAvgTxAmount(),
|
|
]).catch((error) => {
|
|
console.error('Error loading secondary data:', error);
|
|
});
|
|
} catch (error) {
|
|
console.error('Error loading data:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Charge les informations de la blockchain
|
|
*/
|
|
async function loadBlockchainInfo() {
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}/api/blockchain/info`);
|
|
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';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Charge les informations du dernier bloc
|
|
*/
|
|
async function loadLatestBlock() {
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}/api/blockchain/latest-block`);
|
|
const data = await response.json();
|
|
|
|
if (data.time) {
|
|
const date = new Date(data.time * 1000);
|
|
document.getElementById('last-block-time').textContent = date.toLocaleString('fr-FR');
|
|
} else {
|
|
document.getElementById('last-block-time').textContent = 'Aucun bloc';
|
|
}
|
|
|
|
document.getElementById('last-block-tx-count').textContent = data.tx_count || 0;
|
|
} catch (error) {
|
|
console.error('Error loading latest block:', error);
|
|
document.getElementById('last-block-time').textContent = 'Erreur';
|
|
document.getElementById('last-block-tx-count').textContent = 'Erreur';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Charge le solde du wallet
|
|
*/
|
|
async function loadWalletBalance() {
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}/api/wallet/balance`);
|
|
const data = await response.json();
|
|
|
|
document.getElementById('balance-mature').textContent = formatBTC(data.mature || 0);
|
|
document.getElementById('balance-immature').textContent = formatBTC(data.immature || 0);
|
|
} catch (error) {
|
|
console.error('Error loading wallet balance:', error);
|
|
document.getElementById('balance-mature').textContent = 'Erreur';
|
|
document.getElementById('balance-immature').textContent = 'Erreur';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Charge le nombre d'ancrages
|
|
*/
|
|
async function loadAnchorCount() {
|
|
const anchorCountValue = document.getElementById('anchor-count-value');
|
|
const anchorCountSpinner = document.getElementById('anchor-count-spinner');
|
|
|
|
if (!anchorCountValue || !anchorCountSpinner) {
|
|
console.error('Elements anchor-count-value or anchor-count-spinner not found in DOM');
|
|
return;
|
|
}
|
|
|
|
// Afficher le spinner
|
|
anchorCountSpinner.style.display = 'inline';
|
|
anchorCountValue.textContent = '...';
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}/api/anchor/count`);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
const count = data.count !== undefined ? data.count : 0;
|
|
|
|
// Masquer le spinner et mettre à jour la valeur
|
|
anchorCountSpinner.style.display = 'none';
|
|
if (count >= 0) {
|
|
anchorCountValue.textContent = count.toLocaleString();
|
|
} else {
|
|
anchorCountValue.textContent = '0';
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading anchor count:', error);
|
|
// Masquer le spinner
|
|
anchorCountSpinner.style.display = 'none';
|
|
// Ne pas réinitialiser à "Erreur" si on a déjà une valeur affichée
|
|
// Garder la dernière valeur valide ou afficher "0" si c'est la première erreur
|
|
const currentValue = anchorCountValue.textContent;
|
|
if (currentValue === '-' || currentValue === 'Erreur' || currentValue === '...') {
|
|
anchorCountValue.textContent = '0';
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Charge la capacité d'ancrage restante
|
|
* Utilise l'endpoint optimisé /api/utxo/count pour éviter de charger toute la liste
|
|
*/
|
|
async function loadAvailableForAnchor() {
|
|
const availableForAnchorValue = document.getElementById('available-for-anchor-value');
|
|
const availableForAnchorSpinner = document.getElementById('available-for-anchor-spinner');
|
|
|
|
if (!availableForAnchorValue || !availableForAnchorSpinner) {
|
|
console.error('Elements for available-for-anchor not found in DOM');
|
|
return;
|
|
}
|
|
|
|
// Afficher le spinner
|
|
availableForAnchorSpinner.style.display = 'inline';
|
|
availableForAnchorValue.textContent = '...';
|
|
|
|
try {
|
|
// Utiliser l'endpoint optimisé qui lit depuis la base de données
|
|
const response = await fetch(`${API_BASE_URL}/api/utxo/count`);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
const availableForAnchor = data.availableForAnchor || 0;
|
|
|
|
// Masquer le spinner et mettre à jour la valeur
|
|
availableForAnchorSpinner.style.display = 'none';
|
|
availableForAnchorValue.textContent = availableForAnchor.toLocaleString('fr-FR') + ' ancrages';
|
|
} catch (error) {
|
|
console.error('Error loading available for anchor:', error);
|
|
// Masquer le spinner
|
|
availableForAnchorSpinner.style.display = 'none';
|
|
// Afficher une valeur par défaut en cas d'erreur
|
|
const currentValue = availableForAnchorValue.textContent;
|
|
if (currentValue === '-' || currentValue === 'Erreur' || currentValue === '...') {
|
|
availableForAnchorValue.textContent = '0 ancrages';
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Charge les informations sur les pairs
|
|
*/
|
|
async function loadNetworkPeers() {
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}/api/network/peers`);
|
|
const data = await response.json();
|
|
|
|
document.getElementById('peer-count').textContent = data.connections || 0;
|
|
} catch (error) {
|
|
console.error('Error loading network peers:', error);
|
|
document.getElementById('peer-count').textContent = 'Erreur';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Charge la difficulté de minage
|
|
*/
|
|
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 && 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(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';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
avgBlockTimeValue.textContent = data.formatted;
|
|
} else if (data.timeAvgSeconds !== undefined) {
|
|
avgBlockTimeValue.textContent = formatBlockTime(data.timeAvgSeconds);
|
|
} else {
|
|
avgBlockTimeValue.textContent = '-';
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading average block time:', error);
|
|
// Masquer le spinner
|
|
avgBlockTimeSpinner.style.display = 'none';
|
|
avgBlockTimeValue.textContent = 'Erreur';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Formate la difficulté avec séparateurs de milliers
|
|
*/
|
|
function formatDifficulty(difficulty) {
|
|
if (difficulty === 0) return '0';
|
|
if (difficulty < 1) return difficulty.toFixed(4);
|
|
if (difficulty < 1000) return difficulty.toFixed(2);
|
|
if (difficulty < 1000000) return (difficulty / 1000).toFixed(2) + ' K';
|
|
if (difficulty < 1000000000) return (difficulty / 1000000).toFixed(2) + ' M';
|
|
return (difficulty / 1000000000).toFixed(2) + ' G';
|
|
}
|
|
|
|
/**
|
|
* Formate le temps moyen entre blocs en format lisible
|
|
*/
|
|
function formatBlockTime(seconds) {
|
|
if (seconds < 60) {
|
|
return `${seconds}s`;
|
|
} else if (seconds < 3600) {
|
|
const minutes = Math.floor(seconds / 60);
|
|
const secs = seconds % 60;
|
|
return secs > 0 ? `${minutes}m ${secs}s` : `${minutes}m`;
|
|
} else {
|
|
const hours = Math.floor(seconds / 3600);
|
|
const minutes = Math.floor((seconds % 3600) / 60);
|
|
return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
function formatBTC(btc) {
|
|
if (btc === 0) return '0 🛡';
|
|
if (btc < 0.000001) return `${(btc * 100000000).toFixed(0)} sats`;
|
|
// Arrondir sans décimales pour les balances Mature et Immature
|
|
return `${Math.round(btc)} 🛡`;
|
|
}
|
|
|
|
/**
|
|
* Met à jour l'heure de dernière mise à jour
|
|
*/
|
|
function updateLastUpdateTime() {
|
|
const now = new Date();
|
|
document.getElementById('last-update').textContent = now.toLocaleString('fr-FR');
|
|
}
|
|
|
|
/**
|
|
* Change d'onglet
|
|
*/
|
|
function switchTab(tab, buttonElement) {
|
|
// Désactiver tous les onglets
|
|
document.querySelectorAll('.tab-content').forEach(content => {
|
|
content.classList.remove('active');
|
|
});
|
|
document.querySelectorAll('.tab-button').forEach(button => {
|
|
button.classList.remove('active');
|
|
});
|
|
|
|
// Activer l'onglet sélectionné
|
|
document.getElementById(`${tab}-tab`).classList.add('active');
|
|
// Activer le bouton correspondant
|
|
if (buttonElement) {
|
|
buttonElement.classList.add('active');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gère la sélection de fichier
|
|
*/
|
|
function handleFileSelect(event) {
|
|
const file = event.target.files[0];
|
|
if (file) {
|
|
selectedFile = file;
|
|
const fileInfo = document.getElementById('file-info');
|
|
const maxSize = 100 * 1024 * 1024; // 100 MB en bytes
|
|
const fileSize = file.size;
|
|
const isOverLimit = fileSize > maxSize;
|
|
|
|
let infoHtml = `
|
|
<strong>Fichier sélectionné :</strong> ${file.name}<br>
|
|
<strong>Taille :</strong> ${formatFileSize(file.size)}<br>
|
|
<strong>Type :</strong> ${file.type || 'Non spécifié'}
|
|
`;
|
|
|
|
if (isOverLimit) {
|
|
infoHtml += `<br><span style="color: #ff6b6b; font-weight: bold;">⚠️ Fichier trop volumineux (limite : 100 MB)</span>`;
|
|
}
|
|
|
|
fileInfo.innerHTML = infoHtml;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Formate la taille d'un fichier
|
|
*/
|
|
function formatFileSize(bytes) {
|
|
if (bytes === 0) return '0 Bytes';
|
|
const k = 1024;
|
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
|
}
|
|
|
|
/**
|
|
* Génère le hash depuis le texte
|
|
*/
|
|
async function generateHashFromText() {
|
|
const text = document.getElementById('anchor-text').value;
|
|
|
|
if (!text.trim()) {
|
|
showResult('anchor-result', 'error', 'Veuillez entrer un texte à ancrer.');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}/api/hash/generate`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ text }),
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.hash) {
|
|
document.getElementById('anchor-hash').value = data.hash;
|
|
showResult('anchor-result', 'success', `Hash généré avec succès : ${data.hash}`);
|
|
} else {
|
|
showResult('anchor-result', 'error', 'Erreur lors de la génération du hash.');
|
|
}
|
|
} catch (error) {
|
|
showResult('anchor-result', 'error', `Erreur : ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Génère le hash depuis le fichier
|
|
*/
|
|
async function generateHashFromFile() {
|
|
if (!selectedFile) {
|
|
showResult('anchor-result', 'error', 'Veuillez sélectionner un fichier.');
|
|
return;
|
|
}
|
|
|
|
// Vérifier la taille du fichier (limite : 100 MB)
|
|
const maxSize = 100 * 1024 * 1024; // 100 MB en bytes
|
|
if (selectedFile.size > maxSize) {
|
|
showResult('anchor-result', 'error', `Le fichier est trop volumineux (${formatFileSize(selectedFile.size)}). La limite est de 100 MB.`);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const reader = new FileReader();
|
|
|
|
await new Promise((resolve, reject) => {
|
|
reader.onload = async (e) => {
|
|
try {
|
|
const arrayBuffer = e.target.result;
|
|
// Convertir l'ArrayBuffer en base64 pour l'envoi au backend
|
|
// Utiliser une boucle pour éviter les limites de taille des arguments de fonction
|
|
const uint8Array = new Uint8Array(arrayBuffer);
|
|
let binaryString = '';
|
|
for (let i = 0; i < uint8Array.length; i++) {
|
|
binaryString += String.fromCharCode(uint8Array[i]);
|
|
}
|
|
const base64 = btoa(binaryString);
|
|
|
|
const response = await fetch(`${API_BASE_URL}/api/hash/generate`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ fileContent: base64, isBase64: true }),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({ error: `HTTP ${response.status}: ${response.statusText}` }));
|
|
throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.hash) {
|
|
document.getElementById('anchor-hash').value = data.hash;
|
|
showResult('anchor-result', 'success', `Hash généré avec succès : ${data.hash}`);
|
|
} else {
|
|
showResult('anchor-result', 'error', data.error || 'Erreur lors de la génération du hash.');
|
|
}
|
|
resolve();
|
|
} catch (error) {
|
|
showResult('anchor-result', 'error', `Erreur : ${error.message}`);
|
|
reject(error);
|
|
}
|
|
};
|
|
|
|
reader.onerror = (error) => {
|
|
showResult('anchor-result', 'error', `Erreur lors de la lecture du fichier : ${error.message || 'Erreur inconnue'}`);
|
|
reject(error);
|
|
};
|
|
|
|
reader.readAsArrayBuffer(selectedFile);
|
|
});
|
|
} catch (error) {
|
|
showResult('anchor-result', 'error', `Erreur lors de la lecture du fichier : ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Vérifie le hash
|
|
*/
|
|
async function verifyHash() {
|
|
const apiKey = document.getElementById('anchor-api-key').value.trim();
|
|
const hash = document.getElementById('anchor-hash').value.trim();
|
|
|
|
if (!apiKey) {
|
|
showResult('anchor-result', 'error', 'Veuillez entrer une clé API.');
|
|
return;
|
|
}
|
|
|
|
if (!hash || !/^[0-9a-fA-F]{64}$/.test(hash)) {
|
|
showResult('anchor-result', 'error', 'Veuillez entrer un hash valide (64 caractères hexadécimaux).');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
showResult('anchor-result', 'info', 'Vérification du hash en cours...');
|
|
|
|
const response = await fetch(`${API_BASE_URL}/api/anchor/verify`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ hash, apiKey }),
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (response.ok && data.anchor_info) {
|
|
const info = data.anchor_info;
|
|
showResult('anchor-result', 'success',
|
|
`Hash vérifié avec succès !<br>
|
|
<strong>TXID :</strong> ${info.transaction_id || 'N/A'}<br>
|
|
<strong>Hauteur du bloc :</strong> ${info.block_height !== null && info.block_height !== undefined ? info.block_height : 'Non confirmé'}<br>
|
|
<strong>Confirmations :</strong> ${info.confirmations || 0}<br>
|
|
<strong>Statut :</strong> ${info.confirmations > 0 ? 'Confirmé' : 'En attente'}`);
|
|
} else {
|
|
showResult('anchor-result', 'error', data.message || data.error || 'Hash non trouvé sur la blockchain.');
|
|
}
|
|
} catch (error) {
|
|
showResult('anchor-result', 'error', `Erreur : ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Ancre le document
|
|
*/
|
|
async function anchorDocument() {
|
|
const apiKeyElement = document.getElementById('anchor-api-key');
|
|
if (!apiKeyElement) {
|
|
showResult('anchor-result', 'error', 'Champ clé API introuvable.');
|
|
return;
|
|
}
|
|
|
|
const apiKey = apiKeyElement.value.trim();
|
|
const hash = document.getElementById('anchor-hash').value;
|
|
|
|
if (!apiKey || apiKey.length === 0) {
|
|
showResult('anchor-result', 'error', 'Veuillez entrer une clé API.');
|
|
return;
|
|
}
|
|
|
|
console.log('API Key récupérée:', apiKey ? apiKey.substring(0, 10) + '...' : 'vide');
|
|
|
|
if (!hash || !/^[0-9a-fA-F]{64}$/.test(hash)) {
|
|
showResult('anchor-result', 'error', 'Veuillez générer un hash valide (64 caractères hexadécimaux).');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
showResult('anchor-result', 'info', 'Ancrage en cours...');
|
|
|
|
const response = await fetch(`${API_BASE_URL}/api/anchor/test`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ hash, apiKey }),
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (response.ok && data.txid) {
|
|
showResult('anchor-result', 'success',
|
|
`Document ancré avec succès !<br>
|
|
<strong>TXID :</strong> ${data.txid}<br>
|
|
<strong>Statut :</strong> ${data.status}<br>
|
|
<strong>Confirmations :</strong> ${data.confirmations || 0}`);
|
|
|
|
// Recharger le nombre d'ancrages après un court délai
|
|
setTimeout(loadAnchorCount, 2000);
|
|
} else {
|
|
showResult('anchor-result', 'error', data.message || data.error || 'Erreur lors de l\'ancrage.');
|
|
}
|
|
} catch (error) {
|
|
showResult('anchor-result', 'error', `Erreur : ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Télécharge un fichier depuis base64
|
|
*/
|
|
function downloadFile(fileName, base64Data) {
|
|
const byteCharacters = atob(base64Data);
|
|
const byteNumbers = new Array(byteCharacters.length);
|
|
for (let i = 0; i < byteCharacters.length; i++) {
|
|
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
|
}
|
|
const byteArray = new Uint8Array(byteNumbers);
|
|
const blob = new Blob([byteArray], { type: 'application/pdf' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = fileName;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
/**
|
|
* Demande des sats via le faucet
|
|
*/
|
|
async function requestFaucet() {
|
|
const apiKey = document.getElementById('faucet-api-key').value.trim();
|
|
const address = document.getElementById('faucet-address').value.trim();
|
|
|
|
if (!apiKey) {
|
|
showResult('faucet-result', 'error', 'Veuillez entrer une clé API.');
|
|
return;
|
|
}
|
|
|
|
if (!address) {
|
|
showResult('faucet-result', 'error', 'Veuillez entrer une adresse Bitcoin.');
|
|
return;
|
|
}
|
|
|
|
// Validation basique de l'adresse
|
|
const addressPattern = /^(tb1|bcrt1|2|3)[a-zA-HJ-NP-Z0-9]{25,62}$/;
|
|
if (!addressPattern.test(address)) {
|
|
showResult('faucet-result', 'error', 'Adresse Bitcoin invalide.');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
showResult('faucet-result', 'info', 'Demande en cours...');
|
|
|
|
const response = await fetch(`${FAUCET_API_URL}/api/faucet/request`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'x-api-key': apiKey,
|
|
},
|
|
body: JSON.stringify({ address }),
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (response.ok && data.txid) {
|
|
showResult('faucet-result', 'success',
|
|
`50 000 sats envoyés avec succès !<br>
|
|
<strong>TXID :</strong> ${data.txid}<br>
|
|
<strong>Montant :</strong> ${data.amount || '50000'} sats<br>
|
|
<strong>Statut :</strong> ${data.status || 'En attente de confirmation'}`);
|
|
} else {
|
|
showResult('faucet-result', 'error', data.message || data.error || 'Erreur lors de la demande.');
|
|
}
|
|
} catch (error) {
|
|
showResult('faucet-result', 'error', `Erreur : ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Affiche un résultat
|
|
*/
|
|
function showResult(elementId, type, message) {
|
|
const element = document.getElementById(elementId);
|
|
element.className = `result ${type}`;
|
|
element.innerHTML = message;
|
|
}
|
|
|
|
/**
|
|
* Gère la sélection de fichier pour ClamAV
|
|
*/
|
|
function handleClamavFileSelect(event) {
|
|
const file = event.target.files[0];
|
|
if (file) {
|
|
selectedClamavFile = file;
|
|
const fileInfo = document.getElementById('clamav-file-info');
|
|
const maxSize = 100 * 1024 * 1024; // 100 MB en bytes
|
|
const fileSize = file.size;
|
|
const isOverLimit = fileSize > maxSize;
|
|
|
|
let infoHtml = `
|
|
<strong>Fichier sélectionné :</strong> ${file.name}<br>
|
|
<strong>Taille :</strong> ${formatFileSize(file.size)}<br>
|
|
<strong>Type :</strong> ${file.type || 'Non spécifié'}
|
|
`;
|
|
|
|
if (isOverLimit) {
|
|
infoHtml += `<br><span style="color: #ff6b6b; font-weight: bold;">⚠️ Fichier trop volumineux (limite : 100 MB)</span>`;
|
|
}
|
|
|
|
fileInfo.innerHTML = infoHtml;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Scanne un fichier avec ClamAV
|
|
*/
|
|
async function scanWithClamav() {
|
|
if (!selectedClamavFile) {
|
|
showResult('clamav-result', 'error', 'Veuillez sélectionner un fichier.');
|
|
return;
|
|
}
|
|
|
|
// Vérifier la taille du fichier (limite : 100 MB)
|
|
const maxSize = 100 * 1024 * 1024; // 100 MB en bytes
|
|
if (selectedClamavFile.size > maxSize) {
|
|
showResult('clamav-result', 'error', `Le fichier est trop volumineux (${formatFileSize(selectedClamavFile.size)}). La limite est de 100 MB.`);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
showResult('clamav-result', 'info', 'Scan en cours...');
|
|
|
|
const reader = new FileReader();
|
|
|
|
await new Promise((resolve, reject) => {
|
|
reader.onload = async (e) => {
|
|
try {
|
|
const arrayBuffer = e.target.result;
|
|
const uint8Array = new Uint8Array(arrayBuffer);
|
|
let binaryString = '';
|
|
for (let i = 0; i < uint8Array.length; i++) {
|
|
binaryString += String.fromCharCode(uint8Array[i]);
|
|
}
|
|
const base64 = btoa(binaryString);
|
|
|
|
const response = await fetch(`${API_BASE_URL}/api/clamav/scan`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
data: base64,
|
|
filename: selectedClamavFile.name,
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({ error: `HTTP ${response.status}: ${response.statusText}` }));
|
|
throw new Error(errorData.error || errorData.message || `HTTP ${response.status}: ${response.statusText}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.infected) {
|
|
showResult('clamav-result', 'error',
|
|
`⚠️ Fichier infecté détecté !<br>
|
|
<strong>Fichier :</strong> ${data.filename || selectedClamavFile.name}<br>
|
|
<strong>Taille :</strong> ${formatFileSize(data.size || selectedClamavFile.size)}<br>
|
|
<strong>Virus détectés :</strong> ${data.viruses ? data.viruses.join(', ') : 'Inconnu'}`);
|
|
} else {
|
|
showResult('clamav-result', 'success',
|
|
`✅ Fichier propre !<br>
|
|
<strong>Fichier :</strong> ${data.filename || selectedClamavFile.name}<br>
|
|
<strong>Taille :</strong> ${formatFileSize(data.size || selectedClamavFile.size)}<br>
|
|
<strong>Statut :</strong> Aucun virus détecté`);
|
|
}
|
|
resolve();
|
|
} catch (error) {
|
|
showResult('clamav-result', 'error', `Erreur : ${error.message}`);
|
|
reject(error);
|
|
}
|
|
};
|
|
|
|
reader.onerror = (error) => {
|
|
showResult('clamav-result', 'error', `Erreur lors de la lecture du fichier : ${error.message || 'Erreur inconnue'}`);
|
|
reject(error);
|
|
};
|
|
|
|
reader.readAsArrayBuffer(selectedClamavFile);
|
|
});
|
|
} catch (error) {
|
|
showResult('clamav-result', 'error', `Erreur lors du scan : ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Change d'onglet pour le formulaire filigrane
|
|
*/
|
|
function switchTabFiligrane(tab, buttonElement) {
|
|
// Désactiver tous les onglets filigrane
|
|
document.querySelectorAll('#filigrane-text-tab, #filigrane-file-tab').forEach(content => {
|
|
content.classList.remove('active');
|
|
});
|
|
// Désactiver tous les boutons d'onglet dans la section filigrane
|
|
const filigraneSection = document.querySelector('.filigrane-section');
|
|
if (filigraneSection) {
|
|
filigraneSection.querySelectorAll('.tab-button').forEach(button => {
|
|
button.classList.remove('active');
|
|
});
|
|
}
|
|
|
|
// Activer l'onglet sélectionné
|
|
document.getElementById(`filigrane-${tab}-tab`).classList.add('active');
|
|
// Activer le bouton correspondant
|
|
if (buttonElement) {
|
|
buttonElement.classList.add('active');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gère la sélection de fichier pour Filigrane
|
|
*/
|
|
function handleFiligraneFileSelect(event) {
|
|
const file = event.target.files[0];
|
|
if (file) {
|
|
selectedFiligraneFile = file;
|
|
const fileInfo = document.getElementById('filigrane-file-info');
|
|
const maxSize = 100 * 1024 * 1024; // 100 MB en bytes
|
|
const fileSize = file.size;
|
|
const isOverLimit = fileSize > maxSize;
|
|
|
|
let infoHtml = `
|
|
<strong>Fichier sélectionné :</strong> ${file.name}<br>
|
|
<strong>Taille :</strong> ${formatFileSize(file.size)}<br>
|
|
<strong>Type :</strong> ${file.type || 'Non spécifié'}
|
|
`;
|
|
|
|
if (isOverLimit) {
|
|
infoHtml += `<br><span style="color: #ff6b6b; font-weight: bold;">⚠️ Fichier trop volumineux (limite : 100 MB)</span>`;
|
|
}
|
|
|
|
fileInfo.innerHTML = infoHtml;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Teste l'API Filigrane
|
|
*/
|
|
async function testFiligrane() {
|
|
const apiKey = document.getElementById('filigrane-api-key').value.trim();
|
|
|
|
if (!apiKey) {
|
|
showResult('filigrane-result', 'error', 'Veuillez entrer une clé API.');
|
|
return;
|
|
}
|
|
|
|
const textContent = document.getElementById('filigrane-text').value;
|
|
const fileInput = document.getElementById('filigrane-file');
|
|
const currentSelectedFile = selectedFiligraneFile || (fileInput.files.length > 0 ? fileInput.files[0] : null);
|
|
|
|
if (!textContent && !currentSelectedFile) {
|
|
showResult('filigrane-result', 'error', 'Veuillez saisir un texte ou sélectionner un fichier.');
|
|
return;
|
|
}
|
|
|
|
// Vérifier la taille du fichier si un fichier est sélectionné
|
|
if (currentSelectedFile) {
|
|
const maxSize = 100 * 1024 * 1024; // 100 MB en bytes
|
|
if (currentSelectedFile.size > maxSize) {
|
|
showResult('filigrane-result', 'error', `Le fichier est trop volumineux (${formatFileSize(currentSelectedFile.size)}). La limite est de 100 MB.`);
|
|
return;
|
|
}
|
|
}
|
|
|
|
try {
|
|
showResult('filigrane-result', 'info', 'Traitement en cours...');
|
|
|
|
// Préparer les données du fichier
|
|
let fileData = null;
|
|
let fileName = null;
|
|
let mimeType = null;
|
|
|
|
if (currentSelectedFile) {
|
|
const reader = new FileReader();
|
|
await new Promise((resolve, reject) => {
|
|
reader.onload = (e) => {
|
|
const arrayBuffer = e.target.result;
|
|
const uint8Array = new Uint8Array(arrayBuffer);
|
|
let binaryString = '';
|
|
for (let i = 0; i < uint8Array.length; i++) {
|
|
binaryString += String.fromCharCode(uint8Array[i]);
|
|
}
|
|
const base64 = btoa(binaryString);
|
|
fileData = base64;
|
|
fileName = currentSelectedFile.name;
|
|
mimeType = currentSelectedFile.type;
|
|
resolve();
|
|
};
|
|
reader.onerror = reject;
|
|
reader.readAsArrayBuffer(currentSelectedFile);
|
|
});
|
|
}
|
|
|
|
// Préparer les options de filigrane
|
|
const watermarkOptions = {
|
|
enabled: true,
|
|
text: document.getElementById('filigrane-watermark-text').value.trim() || undefined,
|
|
signature: document.getElementById('filigrane-watermark-signature').value.trim() || undefined,
|
|
depositor: document.getElementById('filigrane-watermark-depositor').value.trim() || undefined,
|
|
watermarkedFileName: document.getElementById('filigrane-watermarked-filename').value.trim() || undefined,
|
|
originalFileName: document.getElementById('filigrane-original-filename').value.trim() || undefined,
|
|
dateUTC: document.getElementById('filigrane-watermark-date-utc').checked,
|
|
dateLocal: document.getElementById('filigrane-watermark-date-local').checked,
|
|
blockNumber: document.getElementById('filigrane-watermark-block-number').checked,
|
|
blockHash: document.getElementById('filigrane-watermark-block-hash').checked,
|
|
documentHash: document.getElementById('filigrane-watermark-document-hash').checked,
|
|
};
|
|
|
|
// Nettoyer l'objet en supprimant les valeurs undefined
|
|
const cleanedWatermarkOptions = Object.fromEntries(
|
|
Object.entries(watermarkOptions).filter(([_, value]) => value !== undefined)
|
|
);
|
|
|
|
const requestBody = {
|
|
apiKey,
|
|
watermarkOptions: cleanedWatermarkOptions,
|
|
};
|
|
|
|
if (textContent) {
|
|
requestBody.textContent = textContent;
|
|
} else if (fileData) {
|
|
requestBody.fileData = fileData;
|
|
requestBody.fileName = fileName;
|
|
requestBody.mimeType = mimeType;
|
|
}
|
|
|
|
const response = await fetch(`${API_BASE_URL}/api/watermark/document`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(requestBody),
|
|
});
|
|
|
|
let data;
|
|
try {
|
|
data = await response.json();
|
|
} catch (jsonError) {
|
|
const text = await response.text();
|
|
showResult('filigrane-result', 'error', `Erreur de parsing de la réponse: ${text}`);
|
|
return;
|
|
}
|
|
|
|
if (!response.ok) {
|
|
const errorMessage = data.message || data.error || `Erreur ${response.status}: ${response.statusText}`;
|
|
showResult('filigrane-result', 'error', errorMessage);
|
|
return;
|
|
}
|
|
|
|
if (data.success) {
|
|
const mempoolBaseUrl = 'https://mempool.4nkweb.com/fr';
|
|
const originalTxidLink = data.original.txid
|
|
? `<a href="${mempoolBaseUrl}/tx/${data.original.txid}" target="_blank" style="color: #6ec6ff; text-decoration: underline;">${data.original.txid}</a>`
|
|
: 'N/A';
|
|
const watermarkedTxidLink = data.watermarked.txid
|
|
? `<a href="${mempoolBaseUrl}/tx/${data.watermarked.txid}" target="_blank" style="color: #6ec6ff; text-decoration: underline;">${data.watermarked.txid}</a>`
|
|
: 'N/A';
|
|
|
|
let resultHtml = `
|
|
<strong>✅ Documents ancrés avec succès !</strong><br><br>
|
|
<strong>Document original :</strong><br>
|
|
<strong>Hash SHA256 :</strong> ${data.original.hash || 'N/A'}<br>
|
|
<strong>TXID :</strong> ${originalTxidLink}<br>
|
|
<strong>Statut :</strong> ${data.original.status}<br>
|
|
<strong>Confirmations :</strong> ${data.original.confirmations || 0}<br>
|
|
<strong>Fichier :</strong> ${data.original.file.name}<br><br>
|
|
<strong>Document filigrané :</strong><br>
|
|
<strong>Hash SHA256 :</strong> ${data.watermarked.hash || 'N/A'}<br>
|
|
<strong>TXID :</strong> ${watermarkedTxidLink}<br>
|
|
<strong>Statut :</strong> ${data.watermarked.status}<br>
|
|
<strong>Confirmations :</strong> ${data.watermarked.confirmations || 0}<br>
|
|
<strong>Fichier :</strong> ${data.watermarked.file.name}<br><br>
|
|
`;
|
|
|
|
if (data.certificate && data.certificate.data) {
|
|
resultHtml += `
|
|
<strong>Certificat :</strong><br>
|
|
<strong>Fichier :</strong> ${data.certificate.name}<br><br>
|
|
`;
|
|
}
|
|
|
|
if (data.merged && data.merged.data) {
|
|
resultHtml += `
|
|
<strong>Document fusionné (filigrané + certificat) :</strong><br>
|
|
<strong>Fichier :</strong> ${data.merged.name}<br><br>
|
|
`;
|
|
}
|
|
|
|
resultHtml += `<p style="color: #90ee90; font-weight: bold;">📥 Téléchargement automatique en cours...</p>`;
|
|
|
|
showResult('filigrane-result', 'success', resultHtml);
|
|
|
|
// Télécharger automatiquement les documents avec un délai entre chaque
|
|
setTimeout(() => {
|
|
downloadFile(data.original.file.name, data.original.file.data);
|
|
}, 500);
|
|
|
|
setTimeout(() => {
|
|
downloadFile(data.watermarked.file.name, data.watermarked.file.data);
|
|
}, 1000);
|
|
|
|
if (data.certificate && data.certificate.data) {
|
|
setTimeout(() => {
|
|
downloadFile(data.certificate.name, data.certificate.data);
|
|
}, 1500);
|
|
}
|
|
|
|
if (data.merged && data.merged.data) {
|
|
setTimeout(() => {
|
|
downloadFile(data.merged.name, data.merged.data);
|
|
}, 2000);
|
|
}
|
|
|
|
// Recharger le nombre d'ancrages après un court délai
|
|
setTimeout(loadAnchorCount, 2000);
|
|
} else {
|
|
showResult('filigrane-result', 'error', data.message || data.error || 'Erreur lors du test de l\'API filigrane.');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error in testFiligrane:', error);
|
|
showResult('filigrane-result', 'error', `Erreur : ${error.message}`);
|
|
}
|
|
}
|