ulert
Version:
Open source website guardian — audit uptime, broken links, and security headers from the command line.
185 lines (167 loc) • 6.34 kB
JavaScript
// src/reporter.js
const fs = require('fs').promises;
const path = require('path');
const { version } = require('../package.json');
// Diccionario de traducciones
const translations = {
es: {
reportTitle: "Informe de Seguridad y Salud del Sitio",
site: "Sitio",
date: "Fecha",
httpStatus: "Estado HTTP",
online: "✅ En línea (200)",
offline: "❌ Fuera de línea",
loadTime: "Tiempo de carga",
linkAudit: "Auditoría de Enlaces",
totalLinks: "Total de enlaces internos",
brokenLinks: "Enlaces rotos",
noBrokenLinks: "No se encontraron enlaces rotos.",
viewBrokenLinks: "Ver enlaces rotos",
securityHeaders: "Encabezados de Seguridad",
present: "✅ Presente",
missing: "❌ Ausente",
generatedBy: "Informe generado por",
},
en: {
reportTitle: "Website Security & Health Report",
site: "Site",
date: "Date",
httpStatus: "HTTP Status",
online: "✅ Online (200)",
offline: "❌ Offline",
loadTime: "Load Time",
linkAudit: "Link Audit",
totalLinks: "Total internal links",
brokenLinks: "Broken links",
noBrokenLinks: "No broken links found.",
viewBrokenLinks: "View broken links",
securityHeaders: "Security Headers",
present: "✅ Present",
missing: "❌ Missing",
generatedBy: "Report generated by",
}
};
// Función para obtener traducción
function t(lang, key) {
return translations[lang]?.[key] || translations.es[key] || key;
}
// CLI report en consola (sin traducción por ahora)
function cli(result) {
console.log("✅ HTTP:", result.http.ok ? "Online" : "Offline");
console.log("⚡ Tiempo de carga:", result.http.loadTime ? `${result.http.loadTime}ms` : "N/A");
console.log("🔗 Enlaces:", `${result.links.total} totales, ${result.links.broken} rotos`);
console.log("🛡️ Seguridad:", result.security.score, "chequeos aprobados");
if (result.links.brokenList?.length > 0) {
console.log("\n❌ Enlaces rotos encontrados:");
result.links.brokenList.forEach(link => console.log(" -", link));
}
}
async function html(result, filePath, lang = "es") {
const reportDir = path.dirname(filePath);
try {
await fs.access(reportDir);
} catch {
await fs.mkdir(reportDir, { recursive: true });
}
const report = `
<html lang="${lang}">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>${t(lang, "reportTitle")} - ${result.url}</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap" rel="stylesheet">
<style>
body { font-family: 'Inter', sans-serif; background: #f9fafb; padding: 20px; color: #1f2937; }
h1 { text-align: center; color: #111827; margin-bottom: 5px; }
h2 { color: #374151; margin-top: 30px; }
a { color: #2563eb; text-decoration: none; }
a:hover { text-decoration: underline; }
.card { background: white; padding: 20px; border-radius: 10px; box-shadow: 0 2px 6px rgba(0,0,0,0.08); margin-bottom: 20px; }
.badge { padding: 4px 10px; border-radius: 9999px; font-size: 0.8em; font-weight: bold; display: inline-block; }
.success { background: #d1fae5; color: #065f46; }
.error { background: #fee2e2; color: #991b1b; }
.warning { background: #fef3c7; color: #92400e; }
details { margin-top: 10px; }
summary { cursor: pointer; }
ul { margin: 0; padding-left: 20px; }
table { width: 100%; border-collapse: collapse; margin-top: 10px; }
th, td { padding: 8px; border-bottom: 1px solid #e5e7eb; text-align: left; }
th { background: #f3f4f6; }
footer { text-align: center; font-size: 0.85em; margin-top: 30px; color: #6b7280; }
</style>
</head>
<body>
<h1>🔍 ${t(lang, "reportTitle")}</h1>
<div class="card">
<p><strong>${t(lang, "site")}:</strong> <a href="${result.url}" target="_blank">${result.url}</a></p>
<p><strong>${t(lang, "date")}:</strong> ${new Date(result.timestamp).toLocaleString(lang)}</p>
</div>
<div class="card">
<h2>🌐 ${t(lang, "httpStatus")}</h2>
<p class="badge ${result.http.ok ? 'success' : 'error'}">
${result.http.ok ? t(lang, "online") : t(lang, "offline")}
</p>
<p><strong>${t(lang, "loadTime")}:</strong> ${result.http.loadTime ? `${result.http.loadTime} ms` : 'N/A'}</p>
</div>
<div class="card">
<h2>🔗 ${t(lang, "linkAudit")}</h2>
<p>${t(lang, "totalLinks")}: ${result.links.total}</p>
<p>${t(lang, "brokenLinks")}: <span class="badge ${result.links.broken > 0 ? 'error' : 'success'}">${result.links.broken}</span></p>
${result.links.brokenList?.length > 0 ? `
<details>
<summary>${t(lang, "viewBrokenLinks")}</summary>
<table>
<thead>
<tr><th>URL</th><th>Estado</th></tr>
</thead>
<tbody>
${result.links.brokenList.map(link => `
<tr><td>${link}</td><td><span class="badge error">404</span></td></tr>
`).join('')}
</tbody>
</table>
</details>
` : `<p>${t(lang, "noBrokenLinks")}</p>`}
</div>
<div class="card">
<h2>🛡️ ${t(lang, "securityHeaders")}</h2>
<ul>
${Object.entries(result.security.checks).map(([header, passed]) => `
<li>
<strong>${header}</strong>:
<span class="badge ${passed ? 'success' : 'error'}">
${passed ? t(lang, "present") : t(lang, "missing")}
</span>
</li>
`).join('')}
</ul>
</div>
<footer>
${t(lang, "generatedBy")} <a href="https://ulert.u-site.app" target="_blank">🛡️ Ulert</a> de <a href="https://u-site.app" target="_blank">u-site.app</a> — v${version}
</footer>
</body>
</html>
`;
await fs.writeFile(filePath, report, 'utf-8');
}
// JSON report sin traducción (datos crudos)
async function json(result, filePath) {
const reportDir = path.dirname(filePath);
try {
await fs.access(reportDir);
} catch {
await fs.mkdir(reportDir, { recursive: true });
}
const reportData = {
tool: "Ulert",
version,
timestamp: result.timestamp,
url: result.url,
http: result.http,
links: result.links,
security: result.security
};
await fs.writeFile(filePath, JSON.stringify(reportData, null, 2), 'utf-8');
}
module.exports = { cli, html, json };