frontend-standards-checker
Version:
A comprehensive frontend standards validation tool with TypeScript support
553 lines (511 loc) • 25.6 kB
HTML
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Frontend Standards Log Viewer</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-50 text-gray-900 font-sans h-screen">
<div class="flex h-full">
<!-- Sidebar listing all rules grouped by category with filter -->
<aside
class="w-96 bg-white shadow-xl border-r border-gray-200 flex flex-col overflow-y-auto"
>
<div class="p-6 border-b">
<h2 class="text-2xl font-bold text-blue-600">Logs Viewer</h2>
<p class="text-sm text-gray-500">Frontend Standards</p>
<div
id="manualLogUpload"
class="hidden mt-4 flex flex-col items-center"
>
<div
id="dropZone"
class="mb-2 w-64 h-24 flex flex-col items-center justify-center border-2 border-dashed border-blue-400 rounded-lg bg-blue-50 text-blue-700 cursor-pointer transition hover:bg-blue-100"
>
<svg
class="w-8 h-8 mb-1 text-blue-400"
fill="none"
stroke="currentColor"
stroke-width="2"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M7 16v-4a4 4 0 014-4h2a4 4 0 014 4v4m-4 4v-4m0 0l-2 2m2-2l2 2"
/>
</svg>
<span
id="dropZoneText"
class="text-sm select-none px-3 text-center"
>Drag & Drop your log file here or click to select</span
>
<input
type="file"
id="logFileInput"
accept=".log,.txt"
class="hidden"
/>
</div>
<div id="manualLogError" class="text-xs text-red-600 mt-2"></div>
</div>
</div>
<div class="px-6 py-4">
<div id="sidebarRules" class="space-y-6"></div>
</div>
<button
id="exportCsvBtn"
class="mt-4 w-40 mx-auto text-sm p-2 bg-green-600 text-white rounded hover:bg-green-700 transition"
>
Export to CSV
</button>
</aside>
<!-- Main content -->
<main class="flex-1 overflow-y-auto p-10 bg-gray-50">
<div class="max-w-4xl mx-auto">
<h1 class="text-4xl font-bold mb-8 text-blue-700">
Frontend Standards Log Viewer
</h1>
<div id="summarySection" class="hidden mb-8">
<h2 class="text-2xl font-semibold mb-4 text-gray-800">
General Summary
</h2>
<div id="summaryInfo" class="space-y-6 text-sm"></div>
</div>
<div id="detailsSection" class="hidden">
<h2 class="text-2xl font-semibold mb-4 text-gray-800">Details</h2>
<div id="detailsList" class="space-y-4 text-sm"></div>
</div>
</div>
</main>
</div>
<script>
function exportLogToCSV() {
const rows = [];
rows.push(["Type","Rule","File","Detail/Suggestion","Last Modification","Last Collaborator"]);
function addEntries(type, rule, entries, isInfo) {
entries.forEach(entry => {
let file = entry.file || '';
let ruleName = rule || '';
let detail = '';
let modDate = '';
let collaborator = '';
if (isInfo) {
let suggestion = entry.suggestion || entry.issue || '';
const match = suggestion.match(/^(.*?)\n\s*Last modification:\s*(.*?)\n\s*Last collaborator:\s*(.*)$/s);
if (match) {
detail = match[1].trim();
modDate = match[2].trim();
collaborator = match[3].trim();
} else {
detail = suggestion;
modDate = '';
collaborator = '';
}
} else {
detail = entry.issue || entry.suggestion || '';
modDate = entry.modDate || '';
collaborator = entry.author || '';
}
rows.push([type, ruleName, file, detail, modDate, collaborator]);
});
}
Object.entries(groupedRules.error).forEach(([rule, entries]) => {
addEntries('Error', rule, entries, false);
});
Object.entries(groupedRules.warning).forEach(([rule, entries]) => {
addEntries('Warning', rule, entries, false);
});
Object.entries(groupedRules.info).forEach(([rule, entries]) => {
addEntries('Info', rule, entries, true);
});
const csvContent = rows.map(row => row.map(field => '"' + String(field).replace(/"/g, '""') + '"').join(",")).join("\n");
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'frontend-standards-log.csv';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
const sidebarRules = document.getElementById('sidebarRules')
const summarySection = document.getElementById('summarySection')
const summaryInfo = document.getElementById('summaryInfo')
const detailsSection = document.getElementById('detailsSection')
const detailsList = document.getElementById('detailsList')
let logData = ''
let groupedRules = { error: {}, warning: {}, info: {} }
let totalItems = 0
const sidebarDate = document.createElement('div')
sidebarDate.className = 'text-xs text-gray-400 mt-1'
const sidebarProjectInfo = document.createElement('div')
sidebarProjectInfo.className = 'text-xs text-gray-500 mt-1 space-y-1'
const sidebarHeader = document.querySelector('.p-6.border-b')
sidebarHeader.appendChild(sidebarDate)
sidebarHeader.appendChild(sidebarProjectInfo)
window.addEventListener('DOMContentLoaded', () => {
document.getElementById('exportCsvBtn').addEventListener('click', exportLogToCSV);
// Escaping function to prevent XSS
function escapeHTML(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
fetch('frontend-standards.log')
.then(response => {
if (!response.ok) throw new Error('Log not found.')
return response.text()
})
.then(text => {
logData = text
processLogData()
})
.catch(err => {
sidebarDate.textContent = ''
summarySection.classList.remove('hidden')
summaryInfo.innerHTML = `<div class="text-red-600">Fetch error: ${escapeHTML(err.message)}<br>You can upload the log manually.</div>`
document.getElementById('manualLogUpload').classList.remove('hidden')
console.warn('Log loading error:', err.message)
})
function processLogData() {
const matchDate = logData.match(/^Generated:\s*(.+)$/m)
sidebarDate.textContent = matchDate ? `Generated: ${matchDate[1]}` : ''
const matchProject = logData.match(/^Project:\s*(.+)$/m)
const matchType = logData.match(/^Project Type:\s*(.+)$/m)
const matchMonorepo = logData.match(/^Monorepo:\s*(.+)$/m)
sidebarProjectInfo.innerHTML = `
<div><span class="font-semibold text-gray-700">Project:</span> ${escapeHTML(matchProject ? matchProject[1] : '-')}</div>
<div><span class="font-semibold text-gray-700">Project Type:</span> ${escapeHTML(matchType ? matchType[1] : '-')}</div>
<div><span class="font-semibold text-gray-700">Monorepo:</span> ${escapeHTML(matchMonorepo ? matchMonorepo[1] : '-')}</div>
`
if (!logData.trim()) {
summarySection.classList.remove('hidden')
summaryInfo.innerHTML = '<div class="text-red-600">No information found in the log.</div>'
return
}
parseGroupedData()
renderSidebarRules()
renderSummary()
}
// Manual log upload logic with drag & drop
const logFileInput = document.getElementById('logFileInput')
const dropZone = document.getElementById('dropZone')
const dropZoneText = document.getElementById('dropZoneText')
const loadLogBtn = document.getElementById('loadLogBtn')
const manualLogError = document.getElementById('manualLogError')
function handleFile(file) {
manualLogError.textContent = ''
if (!file) {
manualLogError.textContent = 'Selecciona un archivo de log.'
return
}
const reader = new FileReader()
reader.onload = function(e) {
logData = e.target.result
document.getElementById('manualLogUpload').classList.add('hidden')
processLogData()
}
reader.onerror = function() {
manualLogError.textContent = 'Error al leer el archivo.'
}
reader.readAsText(file)
}
if (dropZone && logFileInput) {
// Click to open file dialog
dropZone.addEventListener('click', () => logFileInput.click())
// File selected via dialog
logFileInput.addEventListener('change', (e) => {
if (logFileInput.files && logFileInput.files[0]) {
handleFile(logFileInput.files[0])
}
})
// Drag over
dropZone.addEventListener('dragover', (e) => {
e.preventDefault()
dropZone.classList.add('bg-blue-200', 'border-blue-600')
})
dropZone.addEventListener('dragleave', (e) => {
e.preventDefault()
dropZone.classList.remove('bg-blue-200', 'border-blue-600')
})
// Drop file
dropZone.addEventListener('drop', (e) => {
e.preventDefault()
dropZone.classList.remove('bg-blue-200', 'border-blue-600')
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
handleFile(e.dataTransfer.files[0])
}
})
}
if (loadLogBtn) {
loadLogBtn.addEventListener('click', () => {
if (logFileInput.files && logFileInput.files[0]) {
handleFile(logFileInput.files[0])
} else {
manualLogError.textContent = 'Selecciona un archivo de log.'
}
})
}
})
function parseGroupedData() {
groupedRules = { error: {}, warning: {}, info: {} }
totalItems = 0
function parseBlock(block, type) {
const regex = /📄 (.*?)\n\s*Rule: (.*?)\n\s*Issue: (.*?)\n\s*Last modification: (.*?)\n\s*Last collaborator: (.*?)\n/g
const matches = [...block.matchAll(regex)]
matches.forEach(([, file, rule, issue, modDate, author]) => {
if (!groupedRules[type][rule]) groupedRules[type][rule] = []
groupedRules[type][rule].push({ file, issue, modDate, author })
totalItems++
})
}
const errorBlock = logData.split('DETAILED VIOLATIONS:')[1]?.split('DETAILED WARNINGS:')[0] || ''
const warningBlock = logData.split('DETAILED WARNINGS:')[1]?.split('DETAILED INFO SUGGESTIONS:')[0] || ''
const infoBlock = logData.split('DETAILED INFO SUGGESTIONS:')[1]?.split('INFO SUGGESTIONS STATISTICS:')[0] || ''
parseBlock(errorBlock, 'error')
parseBlock(warningBlock, 'warning')
const infoDetails = {}
const infoRegex = /📄 (.*?)\n\s*Rule: (.*?)\n\s*Issue: ([\s\S]*?)(?:\n\s*Last modification:|$)/g
let infoMatch
let infoDetailsCount = 0
while ((infoMatch = infoRegex.exec(infoBlock)) !== null) {
const file = infoMatch[1].trim()
const rule = infoMatch[2].trim()
const issue = infoMatch[3].trim()
if (!infoDetails[rule]) infoDetails[rule] = []
infoDetails[rule].push({ file, issue })
infoDetailsCount++
}
if (infoDetailsCount > 0) {
groupedRules.info = infoDetails
totalItems += infoDetailsCount
return
}
groupedRules.info = {}
const infoStatsMatch = logData.match(/INFO SUGGESTIONS STATISTICS:[\s\S]*?(?:\n-+|$)/)
let infoSuggestionsCount = 0
let foundDetails = false
if (infoStatsMatch) {
const infoBlockStats = infoStatsMatch[0]
const infoLineRegex = /^\s*[•\u2022\-*]?\s*(.*?):\s*(\d+)\s*(occurrences|ocurrencias)?/img
let m
while ((m = infoLineRegex.exec(infoBlockStats)) !== null) {
foundDetails = true
const rule = m[1].trim()
const count = parseInt(m[2], 10)
if (rule && count) {
groupedRules.info[rule] = Array.from({ length: count }, () => ({ file: '', issue: rule }))
totalItems += count
infoSuggestionsCount += count
}
}
if (!foundDetails) {
infoBlockStats.split('\n').forEach(line => {
const parts = line.split(':')
if (parts.length === 2) {
const rule = parts[0].replace(/[•\u2022\-*]/g, '').trim()
const count = parseInt(parts[1].replace(/[^0-9]/g, ''), 10)
if (rule && count) {
groupedRules.info[rule] = Array.from({ length: count }, () => ({ file: '', issue: rule }))
totalItems += count
infoSuggestionsCount += count
}
}
})
}
}
if (!foundDetails && infoSuggestionsCount === 0) {
const summaryMatch = logData.match(/Info suggestions:\s*(\d+)/)
if (summaryMatch) {
infoSuggestionsCount = parseInt(summaryMatch[1], 10)
groupedRules.info['Total Info Suggestions'] = Array.from({ length: infoSuggestionsCount }, () => ({ file: '', issue: 'Info suggestion' }))
totalItems += infoSuggestionsCount
}
}
}
function renderSidebarRules(filter = 'all') {
sidebarRules.innerHTML = ''
const types = [
{
key: 'error',
label: 'Errors',
color: 'red',
icon: '<svg class="w-6 h-6 text-red-500 mr-2" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>'
},
{
key: 'warning',
label: 'Warnings',
color: 'yellow',
icon: '<svg class="w-6 h-6 text-yellow-500 mr-2" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>'
},
{
key: 'info',
label: 'Suggestions',
color: 'blue',
icon: '<svg class="w-6 h-6 text-blue-500 mr-2" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>'
}
]
types.forEach(typeObj => {
if (filter !== 'all' && filter !== typeObj.key) return
const rules = groupedRules[typeObj.key]
const ruleEntries = Object.entries(rules)
if (!ruleEntries.length && typeObj.key !== 'info') return
const total = ruleEntries.reduce((sum, [, arr]) => sum + arr.length, 0)
const card = document.createElement('div')
card.className = `bg-white rounded-lg shadow border-l-8 border-${typeObj.color}-500 mb-4`
let expanded = false
const header = document.createElement('div')
header.className = 'flex items-center justify-between cursor-pointer p-4'
header.innerHTML = `
<div class="flex items-center">
${typeObj.icon}
<span class="text-lg font-bold text-${typeObj.color}-600">${typeObj.label}</span>
<span class="ml-2 text-xs bg-${typeObj.color}-100 text-${typeObj.color}-700 rounded px-2 py-1">${total} occurrences</span>
</div>
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7"/></svg>
`
card.appendChild(header)
const rulesList = document.createElement('div')
rulesList.className = 'px-4 pb-4 hidden'
if (ruleEntries.length) {
ruleEntries.forEach(([ruleName, entries]) => {
const ruleRow = document.createElement('div')
ruleRow.className = 'flex items-center justify-between py-2 border-b last:border-b-0'
ruleRow.innerHTML = `
<span class="font-medium text-gray-800">${ruleName}</span>
<button class="text-xs bg-${typeObj.color}-100 text-${typeObj.color}-700 rounded px-2 py-1 hover:bg-${typeObj.color}-200 transition" style="min-width: 48px;" title="Ver detalles">${entries.length}</button>
`
ruleRow.querySelector('button').addEventListener('click', () => renderDetails(entries, ruleName))
rulesList.appendChild(ruleRow)
})
} else if (typeObj.key === 'info') {
const noDetails = document.createElement('div')
noDetails.className = 'text-gray-500 px-4 pb-4'
let debugHtml = '<span class="font-semibold text-gray-700">No info suggestions found in this log.</span>'
if (window.logData) {
const infoDetailsBlock = window.logData.split('DETAILED INFO SUGGESTIONS:')[1]?.split('INFO SUGGESTIONS STATISTICS:')[0] || ''
if (infoDetailsBlock.trim()) {
debugHtml += `<div class="mt-2 text-xs">Bloque DETAILED INFO SUGGESTIONS detectado:</div><pre class="mt-1 text-xs bg-gray-100 p-2 rounded">${infoDetailsBlock.replace(/</g,'<').replace(/>/g,'>')}</pre>`
} else {
debugHtml += '<div class="mt-2 text-xs">No DETAILED INFO SUGGESTIONS block found o está vacío.</div>'
}
const infoStatsMatch = window.logData.match(/INFO SUGGESTIONS STATISTICS:[\s\S]*?(?:\n[-]+|$)/)
if (infoStatsMatch) {
debugHtml += `<div class="mt-2 text-xs">Bloque INFO SUGGESTIONS STATISTICS detectado:</div><pre class="mt-1 text-xs bg-gray-100 p-2 rounded">${infoStatsMatch[0].replace(/</g,'<').replace(/>/g,'>')}</pre>`
} else {
debugHtml += '<div class="mt-2 text-xs">No INFO SUGGESTIONS STATISTICS block found.</div>'
}
}
noDetails.innerHTML = debugHtml
rulesList.appendChild(noDetails)
}
card.appendChild(rulesList)
header.addEventListener('click', () => {
expanded = !expanded
rulesList.classList.toggle('hidden', !expanded)
header.querySelector('svg').classList.toggle('rotate-180', expanded)
})
sidebarRules.appendChild(card)
})
}
function renderSummary() {
summarySection.classList.remove('hidden')
summaryInfo.innerHTML = ''
const types = [
{
key: 'error',
label: 'Errors',
color: 'red'
},
{
key: 'warning',
label: 'Warnings',
color: 'yellow'
},
{
key: 'info',
label: 'Suggestions',
color: 'blue'
}
]
types.forEach(typeObj => {
const rules = groupedRules[typeObj.key]
const ruleEntries = Object.entries(rules)
if (!ruleEntries.length) return
const totalType = ruleEntries.reduce((sum, [, arr]) => sum + arr.length, 0)
const percentType = totalItems ? ((totalType / totalItems) * 100).toFixed(1) : 0
const summaryBlock = document.createElement('div')
summaryBlock.className = `bg-white rounded shadow p-4 border-t-4 border-${typeObj.color}-500`
summaryBlock.innerHTML = `
<p class="text-lg font-bold mb-2">${typeObj.label}</p>
<p><strong>Total occurrences:</strong> ${totalType} (${percentType}%)</p>
<ul class="list-disc list-inside mt-2 space-y-1">${ruleEntries.map(([rule, entries]) => `<li>${rule}: ${entries.length}</li>`).join('')}</ul>
`
summaryInfo.appendChild(summaryBlock)
})
}
function renderDetails(data, rule) {
const isInfo = Object.values(groupedRules.info).some(arr => arr === data)
if (isInfo && rule === 'Total Info Suggestions') {
detailsList.innerHTML = `
<button id="backToSummary" class="mb-4 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition">Back to Summary</button>
<h3 class="text-xl font-bold mb-4">${rule}</h3>
<div class="border-l-4 border-green-500 pl-4 mb-4">
<p><strong>Total suggestions:</strong> ${data.length}</p>
<p class="text-xs text-gray-500">No info suggestions found.</p>
</div>
`
} else if (isInfo) {
detailsList.innerHTML = `
<button id="backToSummary" class="mb-4 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition">Back to Summary</button>
<h3 class="text-xl font-bold mb-4">${rule}</h3>
${data.map(({ file, issue }) => {
let cleanIssue = issue;
let rawModDate = 'No date';
let lastCollaborator = 'Unknown';
const match = issue.match(/^(.*?)\n\s*Last modification:\s*(.*?)\n\s*Last collaborator:\s*(.*)$/s);
if (match) {
cleanIssue = match[1].trim();
rawModDate = match[2].trim();
lastCollaborator = match[3].trim();
}
return `
<div class="border-l-4 border-green-500 pl-4 mb-2">
<p><strong>File:</strong> ${file}</p>
<p class="mb-1"><strong>Suggestion:</strong> ${cleanIssue}</p>
<p class="mt-1"><strong>Last modification:</strong> ${rawModDate}</p>
<p class="mt-1"><strong>Last collaborator:</strong> ${lastCollaborator}</p>
</div>
`;
}).join('')}
`
} else {
detailsList.innerHTML = `
<button id="backToSummary" class="mb-4 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition">Back to Summary</button>
<h3 class="text-xl font-bold mb-4">${rule}</h3>
${data.map(({ file, issue, modDate, author }) => `
<div class="border-l-4 border-blue-500 pl-4 mb-2">
<p><strong>File:</strong> ${file}</p>
<p><strong>Detail:</strong> ${issue}</p>
<p><strong>Last modification:</strong> ${modDate || 'No date'}</p>
<p class="mt-1"><strong>Last collaborator:</strong> ${author || 'Unknown'}</p>
</div>
`).join('')}
`
}
summarySection.classList.add('hidden')
detailsSection.classList.remove('hidden')
document.getElementById('backToSummary').addEventListener('click', () => {
detailsSection.classList.add('hidden')
summarySection.classList.remove('hidden')
})
}
</script>
</body>
</html>