/* Tablero de Incidencias del sitio (27-oct-2026). Servidor (backend/incidencias.py): GET /api/incidencias?estado=&tipo=&origen=&q= · GET /api/incidencia?id= · PATCH /api/incidencia?id= (comentario; estado, gravedad, asignado, publica, resumen_publico, tarea_id, duplicado_de) · GET /api/incidencias/errores (quien atiende) · GET /api/accesos/intentos (administradores, backend/acceso.py). ?id=INC-0003 abre esa incidencia. Reportar = el formulario del pie (js/incidencias.js). */ (function () { const $ = id => document.getElementById(id); const esc = s => { const d = document.createElement('div'); d.textContent = s == null ? '' : String(s); return d.innerHTML; }; const MES = ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic']; const fecha = iso => { const m = /^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}:\d{2}))?/.exec(iso || ''); return m ? (+m[3]) + ' ' + MES[+m[2] - 1] + (m[4] ? ', ' + m[4] : '') : ''; }; const ESTADO = { nueva: 'Nueva', 'en-revision': 'En revisión', 'en-curso': 'En curso', esperando: 'Esperando respuesta', resuelta: 'Resuelta', descartada: 'Descartada' }; const ORIGEN = { sitio: 'reportada en el sitio', entrada: 'desde la entrada', vigia: 'detectada por el vigía', agente: 'abierta por el agente' }; const MOTIVO = { 'usuario-no-existe': 'usuario que no existe', 'contrasena-incorrecta': 'contraseña incorrecta', bloqueado: 'bloqueado por intentos', 'sin-datos': 'sin datos' }; const PISTA = { 'bloq-mayus': 'Bloq Mayús', 'mayuscula-inicial': 'mayúscula inicial', 'espacios-alrededor': 'espacio de más' }; let DATOS = null, abiertas = new Set(), ED = false, YO = '', TIPOS = {}; const params = new URLSearchParams(location.search); if (params.get('id')) abiertas.add(params.get('id')); async function api(url, op) { const r = await fetch(url, Object.assign({ credentials: 'same-origin', cache: 'no-store' }, op || {})); const j = await r.json().catch(() => ({ ok: false, error: 'HTTP ' + r.status })); if (!r.ok || !j.ok) throw new Error(j.error || ('HTTP ' + r.status)); return j; } const json = (method, body) => ({ method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); function aviso(t, bien) { const a = $('aviso'); a.className = 'msg ' + (bien ? 'ok' : 'err'); a.textContent = t; } async function cargar() { const q = new URLSearchParams({ estado: $('fEstado').value, tipo: $('fTipo').value, origen: $('fOrigen').value, q: $('fBuscar').value.trim() }); try { DATOS = await api('/api/incidencias?' + q); ED = DATOS.puede_editar; YO = DATOS.yo; if (!$('fTipo').options[1]) DATOS.tipos.forEach(t => { TIPOS[t.id] = t.nombre; $('fTipo').append(new Option(t.nombre, t.id)); }); DATOS.tipos.forEach(t => { TIPOS[t.id] = t.nombre; }); pintar(); } catch (e) { aviso('No pude cargar las incidencias: ' + e.message); $('meta').textContent = ''; } } function pintar() { const c = DATOS.conteo; $('meta').textContent = c.abiertas + ' abiertas · ' + (c.por_estado.resuelta || 0) + ' resueltas · ' + (YO ? YO + (ED ? ' (atiendes incidencias)' : '') : ''); const vig = DATOS.incidencias.filter(x => x.origen === 'vigia' && ['nueva', 'en-revision', 'en-curso', 'esperando'].includes(x.estado)).length; $('kpis').innerHTML = [ ['Abiertas', c.abiertas, c.abiertas ? 'mal' : 'bien'], ['Sin revisar', c.por_estado.nueva || 0, (c.por_estado.nueva || 0) ? 'mal' : ''], ['No pueden entrar', c.por_tipo.acceso || 0, (c.por_tipo.acceso || 0) ? 'mal' : ''], ['Errores (abiertas)', c.por_tipo.error || 0, ''], ['Del vigía (en esta vista)', vig, ''] ].map(k => '
Nada por aquí. Si algo falla, repórtalo con el botón de arriba o desde el pie de cualquier página.
'; return; } L.innerHTML = ''; DATOS.incidencias.forEach(x => L.append(tarjeta(x))); } function tarjeta(x) { const cerrada = x.estado === 'resuelta' || x.estado === 'descartada'; const d = document.createElement('article'); d.className = 'inc g-' + (x.gravedad || 'media') + (cerrada ? ' cerrada' : ''); d.id = x.id; const quien = x.reportado_por || (x.nombre ? x.nombre + ' (sin sesión)' : (x.origen === 'vigia' ? 'vigía' : '—')); d.innerHTML = 'Cargando…
'; try { const j = await api('/api/incidencia?id=' + encodeURIComponent(id)); detalle(det, j.incidencia, j.puede_editar); } catch (e) { det.innerHTML = '' + esc(r.descripcion) + '
' : '') + (ctx ? '| Dónde | Qué | Veces | Quién | Última | Página |
|---|
Sin errores registrados.
'; } catch (e) { /* sin permiso o sin servidor */ } try { const j = await api('/api/accesos/intentos?dias=7'); $('bEntradas').hidden = false; const filas = Object.entries(j.por_usuario).sort((a, b) => b[1].fallas - a[1].fallas).map(([u, p]) => '| Usuario | Entró | Fallas | Por qué | Pistas | Última entrada | Última falla |
|---|
Nadie ha entrado por la página nueva todavía.
'; } catch (e) { /* solo administradores */ } } $('bReportar').addEventListener('click', () => { const b = document.getElementById('hg-inc-abrir'); if (b) { const s = document.getElementById('problemas-del-sitio'); s.scrollIntoView({ behavior: 'smooth' }); const f = document.getElementById('hg-inc-f'); if (f && f.hidden) b.click(); } }); let t = null; ['fEstado', 'fTipo', 'fOrigen'].forEach(id => $(id).addEventListener('change', cargar)); $('fBuscar').addEventListener('input', () => { clearTimeout(t); t = setTimeout(cargar, 300); }); if (params.get('id')) $('fEstado').value = 'todas'; cargar().then(errores); })();