sourcefinder/export_logger/logs-viewer.html
Alexis Trouvé a7bd6115b7
Some checks failed
SourceFinder CI/CD Pipeline / Code Quality & Linting (push) Has been cancelled
SourceFinder CI/CD Pipeline / Unit Tests (push) Has been cancelled
SourceFinder CI/CD Pipeline / Security Tests (push) Has been cancelled
SourceFinder CI/CD Pipeline / Integration Tests (push) Has been cancelled
SourceFinder CI/CD Pipeline / Performance Tests (push) Has been cancelled
SourceFinder CI/CD Pipeline / Code Coverage Report (push) Has been cancelled
SourceFinder CI/CD Pipeline / Build & Deployment Validation (16.x) (push) Has been cancelled
SourceFinder CI/CD Pipeline / Build & Deployment Validation (18.x) (push) Has been cancelled
SourceFinder CI/CD Pipeline / Build & Deployment Validation (20.x) (push) Has been cancelled
SourceFinder CI/CD Pipeline / Regression Tests (push) Has been cancelled
SourceFinder CI/CD Pipeline / Security Audit (push) Has been cancelled
SourceFinder CI/CD Pipeline / Notify Results (push) Has been cancelled
feat: Implémentation complète du système SourceFinder avec tests
- Architecture modulaire avec injection de dépendances
- Système de scoring intelligent multi-facteurs (spécificité, fraîcheur, qualité, réutilisation)
- Moteur anti-injection 4 couches (preprocessing, patterns, sémantique, pénalités)
- API REST complète avec validation et rate limiting
- Repository JSON avec index mémoire et backup automatique
- Provider LLM modulaire pour génération de contenu
- Suite de tests complète (Jest) :
  * Tests unitaires pour sécurité et scoring
  * Tests d'intégration API end-to-end
  * Tests de sécurité avec simulation d'attaques
  * Tests de performance et charge
- Pipeline CI/CD avec GitHub Actions
- Logging structuré et monitoring
- Configuration ESLint et environnement de test

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-15 23:06:10 +08:00

921 lines
37 KiB
HTML

<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SEO Generator - Logs en temps réel</title>
<style>
body {
font-family: 'Courier New', monospace;
background: #1e1e1e;
color: #ffffff;
margin: 0;
padding: 4px;
}
.header {
background: #2d2d30;
padding: 4px;
border-radius: 2px;
margin-bottom: 4px;
display: flex;
justify-content: space-between;
align-items: center;
}
.header-left h1 {
margin: 0;
font-size: 12px;
}
.header-right {
display: flex;
gap: 4px;
align-items: center;
}
.status {
display: inline-block;
padding: 2px 4px;
border-radius: 1px;
font-size: 9px;
font-weight: bold;
}
.status.connected { background: #28a745; }
.status.disconnected { background: #dc3545; }
.status.connecting { background: #ffc107; color: #000; }
.logs-container {
height: calc(100vh - 88px);
overflow-y: auto;
background: #0d1117;
border: 1px solid #30363d;
border-radius: 2px;
padding: 4px;
}
.log-entry {
padding: 2px 0;
border-bottom: 1px solid #21262d;
font-size: 12px;
line-height: 1.2;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
}
.log-entry.unwrapped {
white-space: pre-wrap;
overflow: visible;
text-overflow: unset;
background: rgba(88, 166, 255, 0.05);
border-left: 2px solid #58a6ff;
padding-left: 4px;
}
.log-entry:last-child {
border-bottom: none;
}
.log-entry.trace {
background: rgba(31, 111, 235, 0.1);
padding-left: 1px;
border-left: 2px solid #1f6feb;
}
.log-entry.trace.span-start {
border-left-color: #28a745;
}
.log-entry.trace.span-end {
border-left-color: #17a2b8;
}
.log-entry.trace.span-error {
border-left-color: #dc3545;
background: rgba(220, 53, 69, 0.1);
}
.log-entry.stack-trace {
background: rgba(220, 53, 69, 0.05);
padding-left: 1px;
color: #f85149;
font-family: 'Courier New', monospace;
font-size: 10px;
border-left: 2px solid #dc3545;
}
.log-details {
margin-top: 4px;
padding: 4px;
background: rgba(139, 148, 158, 0.1);
border-radius: 2px;
font-size: 9px;
color: #8b949e;
display: none;
}
.show-details .log-details {
display: block;
}
.details-toggle {
background: none;
color: #58a6ff;
border: 1px solid #58a6ff;
padding: 1px 1px;
font-size: 8px;
margin-right: 4px;
}
.details-toggle:hover {
background: rgba(88, 166, 255, 0.1);
}
.unwrap-toggle {
background: none;
color: #f79009;
border: 1px solid #f79009;
padding: 1px 1px;
font-size: 8px;
margin-right: 4px;
}
.unwrap-toggle:hover {
background: rgba(247, 144, 9, 0.1);
}
.search-container {
margin-bottom: 3px;
display: flex;
gap: 4px;
align-items: center;
}
.search-input {
flex-grow: 1;
background: #21262d;
border: 1px solid #30363d;
color: #f0f6fc;
padding: 4px 6px;
border-radius: 2px;
font-size: 11px;
}
.search-input:focus {
outline: none;
border-color: #58a6ff;
background: #0d1117;
}
.search-info {
color: #7d8590;
font-size: 10px;
min-width: 80px;
}
.log-entry.search-match {
background: rgba(255, 193, 7, 0.2);
border-left: 3px solid #ffc107;
}
.log-entry.search-current {
background: rgba(255, 193, 7, 0.4);
border-left: 3px solid #ffc107;
}
.search-highlight {
background: #ffc107;
color: #000;
padding: 1px 2px;
border-radius: 2px;
}
.timestamp {
color: #7d8590;
margin-right: 1px;
font-size: 11px;
}
.level {
font-weight: bold;
margin-right: 1px;
padding: 1px 1px;
border-radius: 2px;
font-size: 11px;
min-width: 32px;
}
.level.INFO { background: #1f6feb; }
.level.WARN, .level.WARNING { background: #d29922; }
.level.ERROR { background: #da3633; }
.level.DEBUG { background: #8b949e; }
.level.TRACE { background: #238636; }
.level.PROMPT { background: #8b5cf6; }
.level.LLM { background: #f97316; }
button {
background: #238636;
color: white;
border: none;
padding: 3px 6px;
border-radius: 2px;
cursor: pointer;
font-size: 10px;
}
button:hover { background: #2ea043; }
button:disabled { background: #6e7781; cursor: not-allowed; }
.filter-toggles {
display: flex;
gap: 2px;
align-items: center;
margin-left: 6px;
}
.filter-toggle {
background: #21262d;
border: 1px solid #30363d;
color: #f0f6fc;
padding: 2px 4px;
border-radius: 1px;
cursor: pointer;
font-size: 9px;
min-width: 40px;
text-align: center;
}
.filter-toggle.active.trace { background: #238636; border-color: #238636; }
.filter-toggle.active.info { background: #1f6feb; border-color: #1f6feb; }
.filter-toggle.active.debug { background: #8b949e; border-color: #8b949e; }
.filter-toggle.active.warn { background: #d29922; border-color: #d29922; }
.filter-toggle.active.error { background: #da3633; border-color: #da3633; }
.filter-toggle.active.prompt { background: #8b5cf6; border-color: #8b5cf6; }
.filter-toggle:hover { background: #30363d; }
.log-entry.hidden-by-filter { display: none !important; }
</style>
</head>
<body>
<div class="header">
<div class="header-left">
<h1>SEO Generator - Logs temps réel</h1>
<span id="status" class="status connecting">Connexion...</span>
<span style="margin-left: 15px; font-size: 12px;">Port: <strong>8082</strong></span>
<br>
<button onclick="toggleGlobalDetails()" id="detailsBtn">Mode détaillé: OFF</button>
<button onclick="toggleLineUnwrap()" id="lineUnwrapBtn">Unwrap ligne: OFF</button>
</div>
<div class="header-right">
<div class="filter-toggles">
<span style="color: #7d8590; font-size: 11px;">Filtres:</span>
<button class="filter-toggle active trace" onclick="toggleLevelFilter('trace')" id="traceFilter">TRACE</button>
<button class="filter-toggle active info" onclick="toggleLevelFilter('info')" id="infoFilter">INFO</button>
<button class="filter-toggle active debug" onclick="toggleLevelFilter('debug')" id="debugFilter">DEBUG</button>
<button class="filter-toggle active warn" onclick="toggleLevelFilter('warn')" id="warnFilter">WARN</button>
<button class="filter-toggle active error" onclick="toggleLevelFilter('error')" id="errorFilter">ERROR</button>
<button class="filter-toggle active prompt" onclick="toggleLevelFilter('prompt')" id="promptFilter">PROMPT</button>
<button class="filter-toggle active llm" onclick="toggleLevelFilter('llm')" id="llmFilter">LLM</button>
</div>
<button onclick="clearLogs()">Effacer</button>
<button onclick="toggleAutoScroll()" id="autoScrollBtn">Auto-scroll: ON</button>
<button onclick="reconnect()" id="reconnectBtn">Reconnecter</button>
</div>
</div>
<div class="search-container">
<input type="text" class="search-input" id="searchInput" placeholder="Rechercher dans les logs... (Ctrl+F)">
<div class="search-info" id="searchInfo">0 résultats</div>
<button onclick="searchPrevious()" id="searchPrevBtn" disabled>⬆ Précédent</button>
<button onclick="searchNext()" id="searchNextBtn" disabled>⬇ Suivant</button>
<button onclick="clearSearch()" id="clearSearchBtn"></button>
</div>
<div class="logs-container" id="logsContainer">
<div class="log-entry">
<span class="timestamp">--:--:--</span>
<span class="level INFO">INFO</span>
En attente des logs...
</div>
</div>
<script>
let ws;
let autoScroll = true;
const logsContainer = document.getElementById('logsContainer');
const statusElement = document.getElementById('status');
// Variables de recherche
let searchMatches = [];
let currentMatchIndex = -1;
let searchTerm = '';
// Variables de filtrage
let levelFilters = {
trace: true,
info: true,
debug: true,
warn: true,
warning: true,
error: true,
prompt: true,
llm: true
};
// Récupérer le fichier de log depuis l'URL
const urlParams = new URLSearchParams(window.location.search);
const logFile = urlParams.get('file');
console.log('🌐 URL params:', window.location.search, 'logFile:', logFile);
if (logFile) {
// Mode fichier : charger le fichier spécifié
console.log('📁 MODE FICHIER activé pour:', logFile);
document.title = `SEO Generator - Logs: ${logFile}`;
document.querySelector('h1').textContent = `Logs: ${logFile}`;
loadLogFile(logFile);
} else {
// Mode temps réel : WebSocket comme avant
console.log('⚡ MODE WEBSOCKET activé - pas de paramètre file');
connect();
}
async function loadLogFile(filename) {
try {
statusElement.textContent = `Chargement ${filename}...`;
statusElement.className = 'status connecting';
// Utiliser file:// pour lire directement le fichier local
const input = document.createElement('input');
input.type = 'file';
input.accept = '.log';
input.style.display = 'none';
input.onchange = function(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(e) {
const logContent = e.target.result;
const lines = logContent.split('\n').filter(line => line.trim());
statusElement.textContent = `Fichier chargé (${lines.length} lignes)`;
statusElement.className = 'status connected';
// Parser et afficher chaque ligne
lines.forEach(line => {
try {
const logData = JSON.parse(line);
const timestamp = new Date(logData.time).toISOString();
const level = normalizeLevelName(logData.level);
addLogEntry(logData.msg || logData.message || line, level, timestamp, line);
} catch (error) {
// Ligne non-JSON, afficher telle quelle
addLogEntry(line, 'INFO', new Date().toISOString(), line);
}
});
};
reader.readAsText(file);
};
// Si un nom de fichier est spécifié, tenter de le charger depuis logs/
if (filename) {
try {
const response = await fetch(`logs/${filename}`);
if (response.ok) {
const logContent = await response.text();
const lines = logContent.split('\n').filter(line => line.trim());
statusElement.textContent = `Fichier chargé (${lines.length} lignes)`;
statusElement.className = 'status connected';
lines.forEach(line => {
try {
const logData = JSON.parse(line);
const timestamp = new Date(logData.time).toISOString();
const level = normalizeLevelName(logData.level);
addLogEntry(logData.msg || logData.message || line, level, timestamp, line);
} catch (error) {
addLogEntry(line, 'INFO', new Date().toISOString(), line);
}
});
return;
}
} catch (fetchError) {
// Si le fetch échoue, demander à l'utilisateur de sélectionner le fichier
}
}
// Demander à l'utilisateur de sélectionner le fichier
addLogEntry(`Sélectionnez le fichier de log ${filename || ''} à charger`, 'INFO');
document.body.appendChild(input);
input.click();
document.body.removeChild(input);
} catch (error) {
statusElement.textContent = `Erreur: ${error.message}`;
statusElement.className = 'status disconnected';
addLogEntry(`Erreur chargement fichier: ${error.message}`, 'ERROR');
}
}
function normalizeLevelName(level) {
const levelMap = {10:'TRACE',20:'DEBUG',25:'PROMPT',26:'LLM',30:'INFO',40:'WARN',50:'ERROR',60:'FATAL'};
if (typeof level === 'number') {
return levelMap[level] || 'INFO';
}
return String(level).toUpperCase();
}
function connect() {
console.log('🔌 connect() appelé - tentative WebSocket ws://localhost:8082');
ws = new WebSocket('ws://localhost:8082');
ws.onopen = () => {
console.log('✅ WebSocket connecté !');
statusElement.textContent = 'Connecté';
statusElement.className = 'status connected';
// Reset des tentatives de reconnexion
reconnectAttempts = 0;
reconnectDelay = 1000; // Reconnexion ultra rapide
};
ws.onmessage = (event) => {
console.log('📨 Message WebSocket reçu:', event.data);
try {
const logData = JSON.parse(event.data);
addLogEntry(logData.message, logData.level, logData.timestamp, event.data);
} catch (error) {
console.log('❌ Erreur parsing:', error);
addLogEntry('Erreur parsing log: ' + event.data, 'ERROR');
}
};
ws.onclose = () => {
statusElement.textContent = 'Déconnecté';
statusElement.className = 'status disconnected';
// Auto-reconnexion immédiate
scheduleReconnect();
};
ws.onerror = (error) => {
statusElement.textContent = 'Erreur';
statusElement.className = 'status disconnected';
// Auto-reconnexion immédiate
scheduleReconnect();
};
}
let showDetailsMode = false;
function addLogEntry(message, level = 'INFO', timestamp = null, rawData = null) {
const logEntry = document.createElement('div');
logEntry.className = 'log-entry';
const time = timestamp ? new Date(timestamp).toLocaleTimeString() : new Date().toLocaleTimeString();
// Déterminer si c'est une trace et son type
let traceClass = '';
let cleanMessage = message;
if (message.includes('▶')) {
traceClass = 'trace span-start';
// Nettoyer le message pour garder uniquement l'info utile
cleanMessage = message.replace('▶ ', '🔵 ');
} else if (message.includes('✔')) {
traceClass = 'trace span-end';
cleanMessage = message.replace('✔ ', '✅ ');
} else if (message.includes('✖')) {
traceClass = 'trace span-error';
cleanMessage = message.replace('✖ ', '❌ ');
} else if (message.includes('•')) {
traceClass = 'trace';
cleanMessage = message.replace('• ', '📝 ');
} else if (message.includes('Stack trace:') || message.trim().startsWith('at ')) {
traceClass = 'stack-trace';
if (message.includes('Stack trace:')) {
cleanMessage = '🔴 ' + message;
} else {
cleanMessage = ' ' + message; // Indentation pour les lignes de stack
}
}
logEntry.className += ' ' + traceClass;
const hasDetails = rawData && rawData !== JSON.stringify({message, level, timestamp});
const detailsButton = hasDetails ?
`<button class="details-toggle" onclick="toggleDetails(this)">détails</button>` :
`<span style="display: inline-block; width: 41px;"></span>`; // Placeholder pour alignement
// Détecter si le message est trop long (approximation simple)
const isMessageTooLong = cleanMessage.length > 80;
const unwrapButton = isMessageTooLong ?
`<button class="unwrap-toggle" onclick="toggleUnwrap(this)">unwrap</button>` :
`<span style="display: inline-block; width: 41px;"></span>`; // Placeholder pour alignement
logEntry.innerHTML = `
${detailsButton}
${unwrapButton}
<span class="timestamp">${time}</span>
<span class="level ${level}">${level}</span>
${cleanMessage}
${hasDetails ? `<div class="log-details"><pre>${JSON.stringify(JSON.parse(rawData), null, 2)}</pre></div>` : ''}
`;
// Appliquer le mode détails global si activé
if (showDetailsMode && hasDetails) {
logEntry.classList.add('show-details');
}
// Appliquer les filtres de niveau
applyLevelFilterToEntry(logEntry, level);
// Ajouter le click listener pour l'unwrap ligne par ligne
logEntry.addEventListener('click', (e) => {
// Ne pas déclencher si on clique sur un bouton
if (e.target.classList.contains('details-toggle') ||
e.target.classList.contains('unwrap-toggle')) return;
toggleLogEntryWrap(logEntry);
});
logsContainer.appendChild(logEntry);
// Auto-scroll intelligent : seulement si l'utilisateur est déjà en bas
if (autoScroll) {
// Détection plus précise : considérer qu'on est "en bas" si on est à moins de 100px du bas
const scrollTop = logsContainer.scrollTop;
const scrollHeight = logsContainer.scrollHeight;
const clientHeight = logsContainer.clientHeight;
const isAtBottom = (scrollTop + clientHeight) >= (scrollHeight - 100);
if (isAtBottom) {
// Scroll immédiat vers le bas
requestAnimationFrame(() => {
logsContainer.scrollTop = logsContainer.scrollHeight;
});
}
}
}
function toggleDetails(button) {
const logEntry = button.parentElement;
logEntry.classList.toggle('show-details');
button.textContent = logEntry.classList.contains('show-details') ? 'masquer' : 'détails';
}
function toggleUnwrap(button) {
const logEntry = button.parentElement;
if (logEntry.classList.contains('unwrapped')) {
// Remettre en mode wrapped
logEntry.classList.remove('unwrapped');
logEntry.style.whiteSpace = 'nowrap';
logEntry.style.overflow = 'hidden';
logEntry.style.textOverflow = 'ellipsis';
button.textContent = 'unwrap';
} else {
// Passer en mode unwrapped
logEntry.classList.add('unwrapped');
logEntry.style.whiteSpace = 'pre-wrap';
logEntry.style.overflow = 'visible';
logEntry.style.textOverflow = 'unset';
button.textContent = 'wrap';
}
}
function toggleGlobalDetails() {
showDetailsMode = !showDetailsMode;
const detailsBtn = document.getElementById('detailsBtn');
detailsBtn.textContent = `Mode détaillé: ${showDetailsMode ? 'ON' : 'OFF'}`;
// Appliquer/retirer le mode détails à toutes les entrées
const entries = document.querySelectorAll('.log-entry');
entries.forEach(entry => {
if (showDetailsMode) {
entry.classList.add('show-details');
const toggle = entry.querySelector('.details-toggle');
if (toggle) toggle.textContent = 'masquer';
} else {
entry.classList.remove('show-details');
const toggle = entry.querySelector('.details-toggle');
if (toggle) toggle.textContent = 'détails';
}
});
}
function clearLogs() {
logsContainer.innerHTML = '';
addLogEntry('Logs effacés', 'INFO');
}
function toggleAutoScroll() {
autoScroll = !autoScroll;
document.getElementById('autoScrollBtn').textContent = `Auto-scroll: ${autoScroll ? 'ON' : 'OFF'}`;
}
// Variables pour le unwrap ligne par ligne
let lineUnwrapMode = false;
function toggleLineUnwrap() {
lineUnwrapMode = !lineUnwrapMode;
document.getElementById('lineUnwrapBtn').textContent = `Unwrap ligne: ${lineUnwrapMode ? 'ON' : 'OFF'}`;
if (!lineUnwrapMode) {
// Désactiver le mode : remettre toutes les lignes en mode compact
const logEntries = document.querySelectorAll('.log-entry');
logEntries.forEach(entry => {
entry.classList.remove('unwrapped');
});
}
}
// Fonction pour unwrap/wrap une ligne individuelle
function toggleLogEntryWrap(logEntry) {
if (!lineUnwrapMode) return; // Mode désactivé
if (logEntry.classList.contains('unwrapped')) {
// Re-wrapper la ligne
logEntry.classList.remove('unwrapped');
} else {
// Unwrapper la ligne
logEntry.classList.add('unwrapped');
}
}
function reconnect() {
if (ws) {
ws.close();
}
statusElement.textContent = 'Reconnexion...';
statusElement.className = 'status connecting';
setTimeout(connect, 1000);
}
// Fonctions de recherche
function performSearch() {
const searchInput = document.getElementById('searchInput');
const searchInfo = document.getElementById('searchInfo');
const searchPrevBtn = document.getElementById('searchPrevBtn');
const searchNextBtn = document.getElementById('searchNextBtn');
searchTerm = searchInput.value.trim().toLowerCase();
// Effacer les recherches précédentes
clearSearchHighlights();
searchMatches = [];
currentMatchIndex = -1;
if (searchTerm === '') {
searchInfo.textContent = '0 résultats';
searchPrevBtn.disabled = true;
searchNextBtn.disabled = true;
return;
}
// Rechercher dans tous les logs visibles
const logEntries = document.querySelectorAll('.log-entry:not(.hidden-by-filter)');
logEntries.forEach((entry, index) => {
const text = entry.textContent.toLowerCase();
if (text.includes(searchTerm)) {
searchMatches.push(entry);
entry.classList.add('search-match');
// Highlighter le texte
highlightTextInElement(entry, searchTerm);
}
});
// Mettre à jour l'interface
searchInfo.textContent = `${searchMatches.length} résultat${searchMatches.length > 1 ? 's' : ''}`;
searchPrevBtn.disabled = searchMatches.length === 0;
searchNextBtn.disabled = searchMatches.length === 0;
// Aller au premier résultat
if (searchMatches.length > 0) {
currentMatchIndex = 0;
scrollToCurrentMatch();
}
}
function highlightTextInElement(element, term) {
const walker = document.createTreeWalker(
element,
NodeFilter.SHOW_TEXT,
null,
false
);
const textNodes = [];
let node;
while (node = walker.nextNode()) {
if (node.textContent.toLowerCase().includes(term)) {
textNodes.push(node);
}
}
textNodes.forEach(textNode => {
const parent = textNode.parentNode;
const text = textNode.textContent;
const lowerText = text.toLowerCase();
const regex = new RegExp(`(${term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
if (lowerText.includes(term)) {
const highlightedHTML = text.replace(regex, '<span class="search-highlight">$1</span>');
const wrapper = document.createElement('span');
wrapper.innerHTML = highlightedHTML;
parent.insertBefore(wrapper, textNode);
parent.removeChild(textNode);
}
});
}
function clearSearchHighlights() {
const highlights = document.querySelectorAll('.search-highlight');
highlights.forEach(highlight => {
const parent = highlight.parentNode;
parent.replaceChild(document.createTextNode(highlight.textContent), highlight);
parent.normalize();
});
const searchMatches = document.querySelectorAll('.search-match, .search-current');
searchMatches.forEach(match => {
match.classList.remove('search-match', 'search-current');
});
}
function scrollToCurrentMatch() {
if (currentMatchIndex >= 0 && currentMatchIndex < searchMatches.length) {
// Retirer la classe current de l'ancien match
searchMatches.forEach(match => match.classList.remove('search-current'));
// Ajouter la classe current au match actuel
const currentMatch = searchMatches[currentMatchIndex];
currentMatch.classList.add('search-current');
// Scroller vers l'élément
currentMatch.scrollIntoView({ behavior: 'smooth', block: 'center' });
// Mettre à jour l'info de recherche
document.getElementById('searchInfo').textContent =
`${currentMatchIndex + 1}/${searchMatches.length} résultat${searchMatches.length > 1 ? 's' : ''}`;
}
}
function searchNext() {
if (searchMatches.length > 0) {
currentMatchIndex = (currentMatchIndex + 1) % searchMatches.length;
scrollToCurrentMatch();
}
}
function searchPrevious() {
if (searchMatches.length > 0) {
currentMatchIndex = currentMatchIndex === 0 ? searchMatches.length - 1 : currentMatchIndex - 1;
scrollToCurrentMatch();
}
}
function clearSearch() {
document.getElementById('searchInput').value = '';
clearSearchHighlights();
searchMatches = [];
currentMatchIndex = -1;
document.getElementById('searchInfo').textContent = '0 résultats';
document.getElementById('searchPrevBtn').disabled = true;
document.getElementById('searchNextBtn').disabled = true;
}
// Event listeners pour la recherche
document.getElementById('searchInput').addEventListener('input', performSearch);
document.getElementById('searchInput').addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
if (e.shiftKey) {
searchPrevious();
} else {
searchNext();
}
} else if (e.key === 'Escape') {
clearSearch();
}
});
// Fonctions de filtrage par niveau
function applyLevelFilterToEntry(entry, level) {
const normalizedLevel = level.toLowerCase();
if (!levelFilters[normalizedLevel]) {
entry.classList.add('hidden-by-filter');
} else {
entry.classList.remove('hidden-by-filter');
}
}
function toggleLevelFilter(level) {
levelFilters[level] = !levelFilters[level];
levelFilters['warning'] = levelFilters['warn']; // Synchroniser warn/warning
const button = document.getElementById(`${level}Filter`);
if (levelFilters[level]) {
button.classList.add('active');
} else {
button.classList.remove('active');
}
// Capturer le pourcentage de position AVANT d'appliquer le filtre
const currentScroll = logsContainer.scrollTop;
const maxScroll = logsContainer.scrollHeight - logsContainer.clientHeight;
const currentViewPercentage = maxScroll > 0 ? currentScroll / maxScroll : 0;
// Appliquer les filtres à tous les logs
const entries = document.querySelectorAll('.log-entry');
entries.forEach(entry => {
const entryLevel = entry.querySelector('.level').textContent.toLowerCase();
applyLevelFilterToEntry(entry, entryLevel);
});
// Re-effectuer la recherche si active
if (searchTerm) {
performSearch();
}
// Scroll intelligent avec le pourcentage capturé
smartScrollAfterFilter(currentViewPercentage);
}
function smartScrollAfterFilter(currentViewPercentage) {
setTimeout(() => {
const visibleEntries = document.querySelectorAll('.log-entry:not(.hidden-by-filter)');
if (visibleEntries.length === 0) return;
// Si on a un match de recherche actuel, privilégier celui-ci
if (currentMatchIndex >= 0 && currentMatchIndex < searchMatches.length) {
const currentSearchMatch = searchMatches[currentMatchIndex];
if (!currentSearchMatch.classList.contains('hidden-by-filter')) {
currentSearchMatch.scrollIntoView({ behavior: 'smooth', block: 'center' });
return;
}
}
// Appliquer le même pourcentage aux nouvelles entrées visibles
// Attendre que le DOM se mette à jour après l'application des filtres
setTimeout(() => {
const newMaxScroll = logsContainer.scrollHeight - logsContainer.clientHeight;
const targetScroll = newMaxScroll * currentViewPercentage;
logsContainer.scrollTo({
top: Math.max(0, Math.min(targetScroll, newMaxScroll)),
behavior: 'smooth'
});
}, 50);
}, 100);
}
// Raccourci Ctrl+F
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.key === 'f') {
e.preventDefault();
document.getElementById('searchInput').focus();
}
});
// Connexion initiale SEULEMENT si pas en mode fichier
// (connect() est déjà appelé dans la logique if/else plus haut)
// Auto-reconnexion intelligente
let reconnectDelay = 1000; // 1 seconde
let reconnectAttempts = 0;
let maxReconnectAttempts = 50; // Limite raisonnable
function scheduleReconnect() {
if (reconnectAttempts >= maxReconnectAttempts) {
addLogEntry('Nombre max de tentatives de reconnexion atteint', 'ERROR');
return;
}
setTimeout(() => {
if (!ws || ws.readyState === WebSocket.CLOSED) {
reconnectAttempts++;
statusElement.textContent = `Reconnexion... (${reconnectAttempts}/${maxReconnectAttempts})`;
statusElement.className = 'status connecting';
connect();
}
}, reconnectDelay);
}
// Gestion intelligente de l'auto-scroll basée sur le comportement utilisateur
let userScrolledAway = false;
let scrollTimeout;
logsContainer.addEventListener('scroll', () => {
if (!autoScroll) return;
clearTimeout(scrollTimeout);
const scrollTop = logsContainer.scrollTop;
const scrollHeight = logsContainer.scrollHeight;
const clientHeight = logsContainer.clientHeight;
const isAtBottom = (scrollTop + clientHeight) >= (scrollHeight - 100);
if (isAtBottom) {
// L'utilisateur est revenu en bas, réactiver l'auto-scroll
if (userScrolledAway) {
userScrolledAway = false;
console.log('🔄 Auto-scroll réactivé - utilisateur revenu en bas');
}
} else {
// L'utilisateur a scrollé vers le haut, marquer qu'il s'est éloigné du bas
userScrolledAway = true;
}
// Debounce pour éviter trop d'événements
scrollTimeout = setTimeout(() => {
// Logique supplémentaire si nécessaire
}, 150);
});
// Améliorer addLogEntry pour respecter userScrolledAway
const originalAddLogEntry = addLogEntry;
function enhancedAddLogEntry(message, level = 'INFO', timestamp = null, rawData = null) {
originalAddLogEntry(message, level, timestamp, rawData);
// Override : si l'utilisateur n'a pas scrollé manuellement ET que l'auto-scroll est ON,
// forcer le scroll vers le bas
if (autoScroll && !userScrolledAway) {
requestAnimationFrame(() => {
logsContainer.scrollTop = logsContainer.scrollHeight;
});
}
}
// Remplacer la fonction globale
addLogEntry = enhancedAddLogEntry;
</script>
</body>
</html>