Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013SfRSvDd335NPYEbBrDFGE
194 lines
13 KiB
JavaScript
194 lines
13 KiB
JavaScript
/* Problemas del sitio (27-oct-2026) — al pie de TODAS las páginas (las de MkDocs por extra_javascript y las
|
||
sueltas: actas, Mi cuenta, administración, expediente, con <script src="/js/incidencias.js" defer>).
|
||
- Sección «Problemas del sitio»: reportar lo que falla en esta página (POST /api/incidencias) y ver cuántas hay
|
||
abiertas (GET /api/incidencias/resumen) con liga al tablero /tableros/incidencias/.
|
||
- Errores de JavaScript y llamadas a la API que fallan (5xx) se mandan solos a /api/incidencias/error (máximo 5
|
||
por página); el vigía del servidor los junta y abre la incidencia si se repiten.
|
||
- Si la sesión terminó (una API responde 401), franja arriba con la liga a la página de entrada.
|
||
Sin servidor, la página queda igual. Estilos aquí mismo para que sirva también en las páginas sueltas. */
|
||
(function () {
|
||
if (window.__hbIncidencias) return;
|
||
window.__hbIncidencias = true;
|
||
var errores = [], llamadas = [], enviados = 0, MAX_ENVIOS = 5, avisoSesion = false;
|
||
var fetchOriginal = window.fetch ? window.fetch.bind(window) : null;
|
||
|
||
function ruta() { return location.pathname + location.search; }
|
||
function esc(s) { var d = document.createElement('div'); d.textContent = s == null ? '' : String(s); return d.innerHTML; }
|
||
function ahoraLocal() { try { return new Date().toLocaleString('es-MX'); } catch (e) { return String(new Date()); } }
|
||
|
||
function mandarError(reg) {
|
||
if (!fetchOriginal || enviados >= MAX_ENVIOS) return;
|
||
enviados++;
|
||
reg.pagina = ruta();
|
||
fetchOriginal('/api/incidencias/error', {
|
||
method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(reg)
|
||
}).catch(function () {});
|
||
}
|
||
|
||
// ---- errores de JavaScript
|
||
window.addEventListener('error', function (e) {
|
||
if (!e || !e.message) return;
|
||
var r = { tipo: 'js', mensaje: String(e.message).slice(0, 300), fuente: (e.filename || '').replace(location.origin, ''),
|
||
linea: e.lineno || 0, pila: e.error && e.error.stack ? String(e.error.stack).slice(0, 1000) : '' };
|
||
errores.push({ mensaje: r.mensaje, fuente: r.fuente + ':' + r.linea, cuando: ahoraLocal() });
|
||
if (errores.length > 10) errores.shift();
|
||
mandarError(r);
|
||
});
|
||
window.addEventListener('unhandledrejection', function (e) {
|
||
var m = e && e.reason ? (e.reason.message || String(e.reason)) : 'promesa rechazada';
|
||
if (/Failed to fetch|NetworkError|Load failed|AbortError/i.test(m)) return; // sin red: no es un error del sitio
|
||
errores.push({ mensaje: String(m).slice(0, 300), fuente: 'promesa', cuando: ahoraLocal() });
|
||
if (errores.length > 10) errores.shift();
|
||
mandarError({ tipo: 'promesa', mensaje: String(m).slice(0, 300), pila: e.reason && e.reason.stack ? String(e.reason.stack).slice(0, 1000) : '' });
|
||
});
|
||
|
||
// ---- llamadas a la API que fallan (y sesión terminada)
|
||
if (fetchOriginal) {
|
||
window.fetch = function (entrada, opciones) {
|
||
var url = typeof entrada === 'string' ? entrada : (entrada && entrada.url) || '';
|
||
var metodo = (opciones && opciones.method) || (entrada && entrada.method) || 'GET';
|
||
return fetchOriginal(entrada, opciones).then(function (r) {
|
||
try {
|
||
var u = new URL(url, location.href);
|
||
if (u.origin === location.origin && u.pathname.indexOf('/api/') === 0 && r.status >= 400
|
||
&& u.pathname !== '/api/incidencias/error') {
|
||
llamadas.push({ url: u.pathname, metodo: metodo, estado: String(r.status), cuando: ahoraLocal() });
|
||
if (llamadas.length > 10) llamadas.shift();
|
||
if (r.status === 401) sesionTerminada();
|
||
else if (r.status >= 500) mandarError({ tipo: 'api', mensaje: metodo + ' ' + u.pathname + ' → ' + r.status, url_api: u.pathname, estado_http: r.status });
|
||
}
|
||
} catch (e) {}
|
||
return r;
|
||
});
|
||
};
|
||
}
|
||
|
||
function sesionTerminada() {
|
||
if (avisoSesion) return;
|
||
avisoSesion = true;
|
||
var d = document.createElement('div');
|
||
d.className = 'hg-sesion-fin'; d.setAttribute('role', 'alert');
|
||
d.innerHTML = '<b>Tu sesión terminó</b> (cambió tu contraseña o pasaron 30 días). '
|
||
+ '<a href="/entrar.html#' + encodeURI(ruta()) + '">Entrar de nuevo</a> — lo que no hayas guardado sigue en esta pestaña.';
|
||
document.body.insertBefore(d, document.body.firstChild);
|
||
}
|
||
|
||
// ---- estilos (tokens de stylesheets/helgen.css, que cargan todas las páginas)
|
||
var css = ''
|
||
+ '.hg-inc{margin:40px 0 8px;padding:16px 18px;border:1px solid var(--hg-linea);border-radius:12px;background:var(--hg-plano-2);'
|
||
+ 'font-family:var(--hg-fuente);font-size:14.5px;line-height:1.45;color:var(--hg-texto)}'
|
||
+ '.hg-inc *{box-sizing:border-box}'
|
||
+ '.hg-inc__fila{display:flex;flex-wrap:wrap;gap:10px 16px;align-items:center;justify-content:space-between}'
|
||
+ '.hg-inc h2.hg-inc__t{margin:0!important;font-family:var(--hg-fuente-titulo);font-weight:600;font-size:17px!important;line-height:1.2;color:var(--hg-texto-fuerte);border:0;padding:0}'
|
||
+ '.hg-inc p.hg-inc__s{margin:3px 0 0!important;color:var(--hg-texto-2);font-size:13.5px}'
|
||
+ '.hg-inc__acc{display:flex;flex-wrap:wrap;gap:8px;align-items:center}'
|
||
+ '.hg-inc button,.hg-inc a.hg-inc__b{display:inline-flex;align-items:center;gap:6px;font:inherit;font-size:14px;font-weight:600;padding:8px 13px;'
|
||
+ 'border-radius:9px;border:1px solid var(--hg-linea-2);background:transparent;color:var(--hg-liga);cursor:pointer;text-decoration:none;line-height:1.2}'
|
||
+ '.hg-inc button:hover,.hg-inc a.hg-inc__b:hover{background:var(--hg-elevado)}'
|
||
+ '.hg-inc button.hg-inc__p{background:var(--hg-acento);border-color:var(--hg-acento);color:var(--hg-sobre-acento)}'
|
||
+ '.hg-inc button.hg-inc__p:hover{filter:brightness(1.06);background:var(--hg-acento)}'
|
||
+ '.hg-inc__n{display:inline-block;min-width:20px;padding:0 6px;border-radius:10px;background:var(--hg-aviso-bg);color:var(--hg-aviso);font-size:12px;text-align:center}'
|
||
+ '.hg-inc__ult{margin:10px 0 0;padding:0;list-style:none;font-size:13.5px;color:var(--hg-texto-2)}'
|
||
+ '.hg-inc__ult li{margin:3px 0!important;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}'
|
||
+ '.hg-inc__ult a{color:var(--hg-liga)}'
|
||
+ '.hg-inc form{margin:14px 0 0;border-top:1px solid var(--hg-linea);padding-top:14px}'
|
||
+ '.hg-inc label{display:block;font-weight:600;font-size:13px;margin:10px 0 5px;color:var(--hg-texto-2)}'
|
||
+ '.hg-inc__tipos{display:flex;flex-wrap:wrap;gap:6px}'
|
||
+ '.hg-inc__tipos label{display:inline-flex;align-items:center;gap:6px;margin:0;font-weight:500;font-size:13.5px;color:var(--hg-texto);'
|
||
+ 'padding:6px 11px;border:1px solid var(--hg-linea-2);border-radius:20px;cursor:pointer}'
|
||
+ '.hg-inc__tipos input{accent-color:var(--hg-acento);margin:0}'
|
||
+ '.hg-inc__tipos label:has(input:checked){border-color:var(--hg-acento);background:var(--hg-acento-suave)}'
|
||
+ '.hg-inc textarea{width:100%;min-height:90px;padding:10px 12px;border:1px solid var(--hg-linea-2);border-radius:9px;font:inherit;font-size:15px;'
|
||
+ 'background:var(--hg-plano);color:var(--hg-texto);resize:vertical}'
|
||
+ '.hg-inc textarea:focus{outline:2px solid var(--hg-foco);outline-offset:1px;border-color:transparent}'
|
||
+ '.hg-inc__tec{display:flex!important;align-items:center;gap:8px;font-weight:500!important;color:var(--hg-texto-2)!important}'
|
||
+ '.hg-inc__tec input{accent-color:var(--hg-acento);margin:0}'
|
||
+ '.hg-inc__fin{display:flex;gap:8px;margin-top:12px}'
|
||
+ '.hg-inc__msg{display:none;margin:10px 0 0;padding:9px 12px;border-radius:9px;font-size:14px}'
|
||
+ '.hg-inc__msg.ok{display:block;background:var(--hg-hecho-bg);border:1px solid color-mix(in srgb,var(--hg-hecho) 40%,transparent)}'
|
||
+ '.hg-inc__msg.err{display:block;background:var(--hg-bloqueado-bg);border:1px solid color-mix(in srgb,var(--hg-bloqueado) 45%,transparent)}'
|
||
+ '.hg-sesion-fin{background:var(--hg-bloqueado-bg);color:var(--hg-texto-fuerte);font-size:14px;padding:9px 16px;text-align:center;'
|
||
+ 'border-bottom:1px solid color-mix(in srgb,var(--hg-bloqueado) 45%,transparent);position:sticky;top:0;z-index:10}'
|
||
+ '.hg-sesion-fin a{color:var(--hg-bloqueado);font-weight:700}'
|
||
+ '@media print{.hg-inc,.hg-sesion-fin{display:none}}';
|
||
|
||
var TIPOS = [['error', 'Error o pantalla rota'], ['datos', 'Datos incorrectos'], ['lento', 'Lento'],
|
||
['acceso', 'Permisos o acceso'], ['sugerencia', 'Sugerencia'], ['otro', 'Otro']];
|
||
|
||
function montar() {
|
||
if (document.querySelector('.hg-inc')) return;
|
||
var st = document.createElement('style'); st.textContent = css; document.head.appendChild(st);
|
||
var s = document.createElement('section');
|
||
s.className = 'hg-inc'; s.id = 'problemas-del-sitio'; s.setAttribute('aria-labelledby', 'hg-inc-t');
|
||
s.innerHTML = ''
|
||
+ '<div class="hg-inc__fila"><div><h2 class="hg-inc__t" id="hg-inc-t">Problemas del sitio</h2>'
|
||
+ '<p class="hg-inc__s">¿Algo no funciona en esta página? Repórtalo: se revisa todos los días y te contestamos ahí mismo.</p></div>'
|
||
+ '<div class="hg-inc__acc"><button type="button" class="hg-inc__p" id="hg-inc-abrir">Reportar un problema</button>'
|
||
+ '<a class="hg-inc__b" href="/tableros/incidencias/">Incidencias <span class="hg-inc__n" id="hg-inc-n">…</span></a></div></div>'
|
||
+ '<ul class="hg-inc__ult" id="hg-inc-ult"></ul>'
|
||
+ '<form id="hg-inc-f" hidden novalidate>'
|
||
+ '<label>Qué pasa</label><div class="hg-inc__tipos">'
|
||
+ TIPOS.map(function (t, i) { return '<label><input type="radio" name="hg-inc-tipo" value="' + t[0] + '"' + (i === 0 ? ' checked' : '') + '> ' + t[1] + '</label>'; }).join('')
|
||
+ '</div><label for="hg-inc-d">Cuéntanos con tus palabras: qué hiciste, qué esperabas y qué pasó</label>'
|
||
+ '<textarea id="hg-inc-d" maxlength="4000"></textarea>'
|
||
+ '<label class="hg-inc__tec"><input type="checkbox" id="hg-inc-c" checked> Incluir los datos técnicos de esta página (dirección, navegador y errores recientes)</label>'
|
||
+ '<div class="hg-inc__fin"><button type="submit" class="hg-inc__p" id="hg-inc-e">Enviar reporte</button>'
|
||
+ '<button type="button" id="hg-inc-x">Cancelar</button></div>'
|
||
+ '<div class="hg-inc__msg" id="hg-inc-m" role="status" aria-live="polite"></div></form>';
|
||
var donde = document.querySelector('.md-content__inner') || document.querySelector('.wrap') || document.querySelector('main') || document.body;
|
||
donde.appendChild(s);
|
||
|
||
var f = s.querySelector('#hg-inc-f'), msg = s.querySelector('#hg-inc-m');
|
||
s.querySelector('#hg-inc-abrir').addEventListener('click', function () {
|
||
f.hidden = !f.hidden; if (!f.hidden) s.querySelector('#hg-inc-d').focus();
|
||
});
|
||
s.querySelector('#hg-inc-x').addEventListener('click', function () { f.hidden = true; });
|
||
f.addEventListener('submit', function (ev) {
|
||
ev.preventDefault();
|
||
var d = s.querySelector('#hg-inc-d').value.trim();
|
||
if (d.length < 5) { msg.className = 'hg-inc__msg err'; msg.textContent = 'Cuéntanos qué pasó (aunque sea una línea).'; return; }
|
||
var tipo = (s.querySelector('input[name="hg-inc-tipo"]:checked') || {}).value || 'otro';
|
||
var cuerpo = { descripcion: d, tipo: tipo, pagina: ruta() };
|
||
if (s.querySelector('#hg-inc-c').checked) {
|
||
cuerpo.contexto = { pantalla: innerWidth + '×' + innerHeight, idioma: navigator.language, referer: document.referrer,
|
||
hora_local: ahoraLocal(), errores: errores.slice(-10), llamadas: llamadas.slice(-10) };
|
||
}
|
||
var b = s.querySelector('#hg-inc-e'); b.disabled = true; b.textContent = 'Enviando…';
|
||
(fetchOriginal || fetch)('/api/incidencias', {
|
||
method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(cuerpo)
|
||
}).then(function (r) { return r.json().catch(function () { return { ok: false, error: 'HTTP ' + r.status }; }); })
|
||
.then(function (j) {
|
||
if (j && j.ok) {
|
||
msg.className = 'hg-inc__msg ok';
|
||
msg.innerHTML = '<b>Recibido: <a href="/tableros/incidencias/?id=' + esc(j.id) + '">' + esc(j.id) + '</a>.</b> '
|
||
+ 'Ahí ves cómo va y te contestamos. Gracias.';
|
||
s.querySelector('#hg-inc-d').value = '';
|
||
resumen();
|
||
} else { msg.className = 'hg-inc__msg err'; msg.textContent = (j && j.error) || 'No se pudo enviar.'; }
|
||
})
|
||
.catch(function () { msg.className = 'hg-inc__msg err'; msg.textContent = 'No pude contactar al servidor. Intenta otra vez en un momento.'; })
|
||
.then(function () { b.disabled = false; b.textContent = 'Enviar reporte'; });
|
||
});
|
||
resumen();
|
||
}
|
||
|
||
function resumen() {
|
||
(fetchOriginal || fetch)('/api/incidencias/resumen', { cache: 'no-store', credentials: 'same-origin' })
|
||
.then(function (r) { return r.ok ? r.json() : null; })
|
||
.then(function (j) {
|
||
var n = document.getElementById('hg-inc-n'), ul = document.getElementById('hg-inc-ult');
|
||
if (!n) return;
|
||
if (!j || !j.ok) { n.textContent = '—'; return; }
|
||
n.textContent = j.abiertas;
|
||
n.title = j.abiertas + ' abiertas' + (j.mias ? ' · ' + j.mias + ' tuyas' : '');
|
||
ul.innerHTML = (j.ultimas || []).map(function (x) {
|
||
return '<li><a href="/tableros/incidencias/?id=' + esc(x.id) + '">' + esc(x.id) + '</a> · ' + esc(x.titulo) + '</li>';
|
||
}).join('');
|
||
})
|
||
.catch(function () {});
|
||
}
|
||
|
||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', montar);
|
||
else montar();
|
||
})();
|