Correction du loggueur qui sature les CPU des browsers
This commit is contained in:
@@ -42,13 +42,13 @@
|
||||
</div>
|
||||
<div class="log-content">
|
||||
<div class="message markdown-content">
|
||||
<details v-if="isTooLong(log.message)" class="log-details">
|
||||
<details v-if="log.isTooLong" class="log-details">
|
||||
<summary class="log-summary">
|
||||
<span class="truncated-text">{{ truncateMessage(log.message) }}</span>
|
||||
<span class="truncated-text">{{ log.truncatedMessage }}</span>
|
||||
</summary>
|
||||
<div class="full-message" v-html="renderMarkdown(log.message)"></div>
|
||||
<div class="full-message" v-html="log.renderedHtml"></div>
|
||||
</details>
|
||||
<div v-else v-html="renderMarkdown(log.message)"></div>
|
||||
<div v-else v-html="log.renderedHtml"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -103,11 +103,14 @@ const levelOrder = {
|
||||
'TRACE': 4
|
||||
}
|
||||
|
||||
// Pré-calculer filteredLogs de manière optimisée
|
||||
const filteredLogs = computed(() => {
|
||||
if (levelFilter.value === 'ALL') {
|
||||
return logs.value
|
||||
}
|
||||
return logs.value.filter(log => log.level === levelFilter.value)
|
||||
// Utiliser la référence directe pour éviter des copies inutiles
|
||||
const filter = levelFilter.value
|
||||
return logs.value.filter(log => log.level === filter)
|
||||
})
|
||||
|
||||
// Fonction pour mettre à jour le niveau de log côté serveur
|
||||
@@ -157,65 +160,62 @@ function formatTimestamp(timestamp) {
|
||||
})
|
||||
}
|
||||
|
||||
function isTooLong(message) {
|
||||
// Un message est trop long s'il a plus d'une ligne OU plus de 200 caractères
|
||||
// Pré-traiter un log : calculer HTML, troncature, etc. UNE SEULE FOIS
|
||||
function preprocessLog(message) {
|
||||
// ÉTAPE 1: Déterminer si trop long
|
||||
const firstLineEnd = message.indexOf('\n')
|
||||
return firstLineEnd !== -1 || message.length > 200
|
||||
}
|
||||
const isTooLong = firstLineEnd !== -1 || message.length > 200
|
||||
|
||||
function truncateMessage(message) {
|
||||
// Prendre la première ligne, ou les 200 premiers caractères si pas de saut de ligne
|
||||
const firstLineEnd = message.indexOf('\n')
|
||||
if (firstLineEnd !== -1) {
|
||||
return message.substring(0, firstLineEnd).trim()
|
||||
}
|
||||
return message.substring(0, 200).trim()
|
||||
}
|
||||
// ÉTAPE 2: Calculer le message tronqué si nécessaire
|
||||
const truncatedMessage = isTooLong
|
||||
? (firstLineEnd !== -1
|
||||
? message.substring(0, firstLineEnd).trim()
|
||||
: message.substring(0, 200).trim())
|
||||
: null
|
||||
|
||||
function renderMarkdown(text) {
|
||||
// ÉTAPE 1 : Pré-processing pour détecter et protéger le XML
|
||||
let processedText = text
|
||||
// ÉTAPE 3: Pré-processing pour détecter et protéger le XML
|
||||
let processedText = message
|
||||
|
||||
// Détecter si le message contient du XML
|
||||
// Pattern : cherche <?xml ou des balises XML racine communes (scpd, root, service, etc.)
|
||||
const hasXml = /<\?xml|<(scpd|root|service|device|actionList|stateVariable)[>\s]/i.test(text)
|
||||
const hasXml = /<\?xml|<(scpd|root|service|device|actionList|stateVariable)[>\s]/i.test(message)
|
||||
|
||||
if (hasXml) {
|
||||
// Extraire tout ce qui ressemble à du XML (du <?xml ou première balise jusqu'à la fin)
|
||||
const xmlStartMatch = text.match(/<\?xml[\s\S]*$/)
|
||||
const xmlStartMatch = message.match(/<\?xml[\s\S]*$/)
|
||||
|
||||
if (xmlStartMatch) {
|
||||
const xmlContent = xmlStartMatch[0]
|
||||
const beforeXml = text.substring(0, text.indexOf(xmlContent))
|
||||
|
||||
// Créer le texte avec le XML dans un bloc de code
|
||||
const beforeXml = message.substring(0, message.indexOf(xmlContent))
|
||||
processedText = beforeXml + '\n```xml\n' + xmlContent + '\n```\n'
|
||||
} else {
|
||||
// Fallback : chercher une balise racine XML
|
||||
const xmlMatch = text.match(/<([a-zA-Z][a-zA-Z0-9:-]*)[>\s][\s\S]*/)
|
||||
const xmlMatch = message.match(/<([a-zA-Z][a-zA-Z0-9:-]*)[>\s][\s\S]*/)
|
||||
if (xmlMatch) {
|
||||
const xmlContent = xmlMatch[0]
|
||||
const beforeXml = text.substring(0, text.indexOf(xmlContent))
|
||||
const beforeXml = message.substring(0, message.indexOf(xmlContent))
|
||||
processedText = beforeXml + '\n```xml\n' + xmlContent + '\n```\n'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ÉTAPE 2 : Détecter et transformer les liens d'images
|
||||
// Pattern pour détecter les URLs d'images (png, jpg, jpeg, gif, webp, svg)
|
||||
// ÉTAPE 4: Détecter et transformer les liens d'images
|
||||
const imageUrlPattern = /(https?:\/\/[^\s]+\.(?:png|jpg|jpeg|gif|webp|svg)(?:\?[^\s]*)?)/gi
|
||||
processedText = processedText.replace(imageUrlPattern, (match) => {
|
||||
return `\n\n`
|
||||
})
|
||||
|
||||
// ÉTAPE 3 : Convertir markdown en HTML
|
||||
// ÉTAPE 5: Convertir markdown en HTML
|
||||
const rawHtml = marked.parse(processedText, { async: false })
|
||||
|
||||
// ÉTAPE 4 : Nettoyer pour la sécurité
|
||||
return DOMPurify.sanitize(rawHtml, {
|
||||
// ÉTAPE 6: Nettoyer pour la sécurité
|
||||
const renderedHtml = DOMPurify.sanitize(rawHtml, {
|
||||
ALLOWED_TAGS: ['strong', 'em', 'code', 'pre', 'a', 'ul', 'ol', 'li', 'p', 'br', 'span', 'img'],
|
||||
ALLOWED_ATTR: ['href', 'target', 'class', 'src', 'alt', 'title']
|
||||
})
|
||||
|
||||
return {
|
||||
isTooLong,
|
||||
truncatedMessage,
|
||||
renderedHtml
|
||||
}
|
||||
}
|
||||
|
||||
function toggleAutoScroll() {
|
||||
@@ -265,6 +265,12 @@ function connectSSE() {
|
||||
logEntry.isHistory = true
|
||||
}
|
||||
|
||||
// PRÉ-TRAITER le log UNE SEULE FOIS à la réception
|
||||
const processed = preprocessLog(logEntry.message)
|
||||
logEntry.isTooLong = processed.isTooLong
|
||||
logEntry.truncatedMessage = processed.truncatedMessage
|
||||
logEntry.renderedHtml = processed.renderedHtml
|
||||
|
||||
logs.value.push(logEntry)
|
||||
|
||||
// Limiter à 1000 logs en mémoire
|
||||
@@ -325,17 +331,32 @@ onUnmounted(() => {
|
||||
})
|
||||
|
||||
// Désactiver auto-scroll si l'utilisateur scroll manuellement
|
||||
watch(logContainer, (container) => {
|
||||
let scrollHandler = null
|
||||
watch(logContainer, (container, oldContainer) => {
|
||||
// Nettoyer l'ancien listener si existant
|
||||
if (oldContainer && scrollHandler) {
|
||||
oldContainer.removeEventListener('scroll', scrollHandler)
|
||||
}
|
||||
|
||||
if (!container) return
|
||||
|
||||
container.addEventListener('scroll', () => {
|
||||
scrollHandler = () => {
|
||||
const isAtBottom =
|
||||
container.scrollHeight - container.scrollTop <= container.clientHeight + 50
|
||||
|
||||
if (!isAtBottom && autoScroll.value) {
|
||||
autoScroll.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
container.addEventListener('scroll', scrollHandler, { passive: true })
|
||||
})
|
||||
|
||||
// Nettoyer au démontage
|
||||
onUnmounted(() => {
|
||||
if (logContainer.value && scrollHandler) {
|
||||
logContainer.value.removeEventListener('scroll', scrollHandler)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user