feat: frontend, selector semanal/todas e imagenes en el grafo
semana-toggle.js es un modulo compartido por las cinco vistas tematicas. Pone el rango de fechas antes del primer fetch, asi que la carga inicial ya sale en modo semanal. Usa fecha local, no toISOString(), que convierte a UTC y de madrugada devolvia la fecha del dia anterior. Los nodos de noticia con imagen ahora la muestran como miniatura, servida por el proxy interno: Three.js no puede texturizar una og:image de otro dominio sin CORS, y el proxy la sirve desde el mismo origen. Anade la vista de estadisticas y el panel de administracion. Quita el @font-face de Retrolift: apuntaba a una ruta del sistema de ficheros de otra maquina, que un navegador nunca puede resolver. Llevaba roto desde siempre y las reglas que piden esa familia ya caian a sans-serif.
This commit is contained in:
parent
cfae328d22
commit
710904a1ef
38 changed files with 3012 additions and 254 deletions
463
FLUJOS/VISUALIZACION/public/admin.html
Normal file
463
FLUJOS/VISUALIZACION/public/admin.html
Normal file
|
|
@ -0,0 +1,463 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="robots" content="noindex">
|
||||
<meta name="referrer" content="no-referrer">
|
||||
<title>UP-LEAKS // panel</title>
|
||||
<style>
|
||||
@font-face { font-family: 'Fira Code'; src: url('/fonts/fira-code-400.woff2') format('woff2'); font-weight: 400; font-display: swap; }
|
||||
@font-face { font-family: 'Fira Code'; src: url('/fonts/fira-code-500.woff2') format('woff2'); font-weight: 500; font-display: swap; }
|
||||
@font-face { font-family: 'Fira Code'; src: url('/fonts/fira-code-700.woff2') format('woff2'); font-weight: 700; font-display: swap; }
|
||||
:root {
|
||||
--neon: #39ff14; --neon-soft: #7dff5e; --neon-deep: #0e3a08; --bg: #000; --panel: #080d08; --panel-2: #0b120b;
|
||||
--line: #1c4a14; --line-soft: #12300d; --ink: #d7f5d0; --muted: #6f9a6a; --faint: #46603f;
|
||||
--red: #ff2d2d; --amber: #ffb800;
|
||||
--mono: 'Fira Code', ui-monospace, Menlo, Consolas, monospace; --glow: 0 0 8px rgba(57,255,20,.55);
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0; background: var(--bg); color: var(--ink); font-family: var(--mono); font-size: 14px; line-height: 1.6;
|
||||
background-image: repeating-linear-gradient(0deg, rgba(57,255,20,.015) 0 1px, transparent 1px 3px);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
a { color: var(--neon); }
|
||||
.wrap { max-width: 1000px; margin: 0 auto; padding: 0 20px; }
|
||||
::selection { background: var(--neon); color: #000; }
|
||||
|
||||
header.top { border-bottom: 1px solid var(--line); }
|
||||
.top .wrap { display: flex; align-items: center; justify-content: space-between; height: 60px; }
|
||||
.brand { font-weight: 700; letter-spacing: .2em; }
|
||||
.brand b { color: var(--neon); text-shadow: var(--glow); }
|
||||
.brand .sub { color: var(--muted); font-weight: 400; font-size: 12px; letter-spacing: .1em; }
|
||||
|
||||
button {
|
||||
background: transparent; color: var(--neon); border: 1px solid var(--neon); font-family: var(--mono);
|
||||
font-weight: 700; padding: 10px 20px; cursor: pointer; text-transform: uppercase; letter-spacing: .06em; font-size: 12.5px;
|
||||
transition: background .16s, color .16s, box-shadow .16s;
|
||||
}
|
||||
button:hover:not(:disabled) { background: var(--neon); color: #000; box-shadow: 0 0 16px -2px rgba(57,255,20,.7); }
|
||||
button:disabled { opacity: .5; cursor: wait; }
|
||||
button.ghost { border-color: var(--line); color: var(--muted); }
|
||||
button.ghost:hover { border-color: var(--neon); color: var(--neon); background: transparent; box-shadow: none; }
|
||||
|
||||
/* gate */
|
||||
.gate { max-width: 460px; margin: 90px auto; border: 1px solid var(--line); background: var(--panel); padding: 30px; }
|
||||
.gate h1 { font-size: 17px; margin: 0 0 6px; font-weight: 500; }
|
||||
.gate h1::before { content: "// "; color: var(--neon); }
|
||||
.gate p { color: var(--muted); font-size: 12.5px; margin: 0 0 18px; }
|
||||
input[type="password"], input[type="text"] {
|
||||
width: 100%; background: var(--bg); border: 1px solid var(--line); color: var(--ink); padding: 12px; font-family: var(--mono); font-size: 14px;
|
||||
}
|
||||
input:focus { outline: none; border-color: var(--neon); box-shadow: 0 0 0 1px var(--neon), 0 0 14px -2px rgba(57,255,20,.5); }
|
||||
.gate .row { display: flex; gap: 10px; margin-top: 16px; }
|
||||
.gate-err { color: var(--red); font-size: 12.5px; margin-top: 12px; min-height: 1.2em; }
|
||||
|
||||
/* tabs + toolbar */
|
||||
.toolbar { display: flex; align-items: center; gap: 12px; padding: 20px 0 12px; flex-wrap: wrap; }
|
||||
.tabs { display: flex; gap: 6px; }
|
||||
.tabs button { padding: 8px 16px; font-size: 12px; }
|
||||
.tabs button.active { background: var(--neon-deep); color: var(--neon); border-color: var(--neon); }
|
||||
.spacer { flex: 1; }
|
||||
.updated { color: var(--faint); font-size: 11.5px; letter-spacing: .04em; }
|
||||
|
||||
/* ------- submissions ------- */
|
||||
.filters { display: flex; gap: 6px; margin: 4px 0 14px; }
|
||||
.filters button { padding: 6px 12px; font-size: 11px; }
|
||||
.filters button.active { background: var(--neon-deep); color: var(--neon); border-color: var(--neon); }
|
||||
.list { display: flex; flex-direction: column; gap: 12px; padding-bottom: 60px; }
|
||||
.sub { border: 1px solid var(--line); background: var(--panel); padding: 16px 18px; }
|
||||
.sub .head { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 8px; }
|
||||
.sub .ref { color: var(--neon); font-weight: 700; }
|
||||
.sub .date { color: var(--muted); font-size: 12px; }
|
||||
.sub .tema { color: var(--ink); font-size: 12px; border: 1px solid var(--line); padding: 1px 8px; }
|
||||
.badge { font-size: 10.5px; text-transform: uppercase; letter-spacing: .08em; padding: 2px 8px; border: 1px solid; }
|
||||
.badge.nuevo { color: var(--neon); border-color: var(--neon); }
|
||||
.badge.revisado { color: var(--amber); border-color: var(--amber); }
|
||||
.badge.archivado { color: var(--faint); border-color: var(--line); }
|
||||
.sub .msg { white-space: pre-wrap; word-break: break-word; color: var(--ink); font-size: 13.5px; margin: 8px 0; }
|
||||
.sub .contacto { color: var(--muted); font-size: 12.5px; }
|
||||
.sub .contacto b { color: var(--ink); }
|
||||
.files { display: flex; flex-direction: column; gap: 5px; margin: 10px 0 4px; }
|
||||
.files a { display: inline-flex; align-items: center; gap: 8px; font-size: 12.5px; cursor: pointer; }
|
||||
.files a::before { content: "⬇ "; color: var(--neon); }
|
||||
.actions { display: flex; gap: 8px; margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--line); }
|
||||
.actions button { padding: 5px 12px; font-size: 10.5px; }
|
||||
.empty { color: var(--muted); text-align: center; padding: 50px; }
|
||||
|
||||
/* ------- system (htop) ------- */
|
||||
.tiles { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 20px; }
|
||||
@media (max-width: 720px) { .tiles { grid-template-columns: repeat(2, 1fr); } }
|
||||
.tile { border: 1px solid var(--line); background: var(--panel); padding: 14px 16px; }
|
||||
.tile .k { font-size: 10.5px; letter-spacing: .12em; text-transform: uppercase; color: var(--faint); }
|
||||
.tile .v { font-size: 24px; font-weight: 700; margin: 6px 0 8px; color: var(--ink); font-variant-numeric: tabular-nums; }
|
||||
.tile .v small { font-size: 13px; color: var(--muted); font-weight: 400; }
|
||||
|
||||
.sec-title { font-size: 11.5px; letter-spacing: .14em; text-transform: uppercase; color: var(--muted); margin: 22px 0 12px; }
|
||||
.sec-title::before { content: "// "; color: var(--neon); }
|
||||
|
||||
.meter { height: 9px; background: rgba(57,255,20,.08); border: 1px solid var(--line-soft); position: relative; overflow: hidden; }
|
||||
.meter > span { position: absolute; left: 0; top: 0; bottom: 0; background: linear-gradient(90deg, var(--neon), var(--neon-soft)); transition: width .5s ease; }
|
||||
.meter.warn > span { background: linear-gradient(90deg, #b8860b, var(--amber)); }
|
||||
.meter.crit > span { background: linear-gradient(90deg, #a01d1d, var(--red)); }
|
||||
|
||||
.svcs { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 8px; }
|
||||
.svc { font-size: 11px; padding: 4px 10px; border: 1px solid; letter-spacing: .04em; }
|
||||
.svc.up { color: var(--neon); border-color: var(--neon); }
|
||||
.svc.down { color: var(--red); border-color: var(--red); }
|
||||
.svc::before { content: "● "; }
|
||||
|
||||
.procs { border: 1px solid var(--line); background: var(--panel); }
|
||||
.prow { display: grid; grid-template-columns: 1.6fr 60px 1.3fr 70px 66px; gap: 10px; align-items: center;
|
||||
padding: 10px 14px; border-bottom: 1px solid var(--line-soft); font-size: 12.5px; }
|
||||
.prow:last-child { border-bottom: none; }
|
||||
.prow.hdr { color: var(--faint); font-size: 10.5px; letter-spacing: .1em; text-transform: uppercase; }
|
||||
.prow .name { color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.prow .name.viejo { color: var(--red); }
|
||||
.prow .cpu { text-align: right; font-variant-numeric: tabular-nums; color: var(--neon); }
|
||||
.prow .mem, .prow .time, .prow .pid { color: var(--muted); font-variant-numeric: tabular-nums; }
|
||||
.prow .time { text-align: right; } .prow .pid { text-align: right; color: var(--faint); }
|
||||
|
||||
.prog { display: flex; flex-direction: column; gap: 14px; margin-bottom: 40px; }
|
||||
.prog .item .top { display: flex; justify-content: space-between; margin-bottom: 5px; font-size: 12.5px; }
|
||||
.prog .item .lbl { color: var(--ink); } .prog .item .val { color: var(--muted); font-variant-numeric: tabular-nums; }
|
||||
.prog .item .val b { color: var(--neon); }
|
||||
.kv { display: flex; gap: 24px; flex-wrap: wrap; margin-top: 4px; font-size: 12.5px; color: var(--muted); }
|
||||
.kv b { color: var(--neon); }
|
||||
|
||||
/* ------- pipelines ------- */
|
||||
#pipesView { padding-bottom: 50px; }
|
||||
.pipe { border: 1px solid var(--line); background: var(--panel); padding: 20px 22px; margin-bottom: 14px; }
|
||||
.pipe .phead { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; margin-bottom: 16px; flex-wrap: wrap; }
|
||||
.pipe .ptitle { font-size: 15.5px; font-weight: 700; color: var(--ink); }
|
||||
.pipe .pcad { color: var(--faint); font-weight: 400; font-size: 12px; }
|
||||
.pstate { font-size: 10.5px; text-transform: uppercase; letter-spacing: .08em; padding: 3px 10px; border: 1px solid; white-space: nowrap; }
|
||||
.pstate.e-run { color: var(--neon); border-color: var(--neon); }
|
||||
.pstate.e-idle { color: var(--faint); border-color: var(--line); }
|
||||
.pstate.e-err { color: var(--red); border-color: var(--red); }
|
||||
.steps-bar, .flow { display: flex; gap: 5px; align-items: stretch; }
|
||||
.flow { align-items: center; gap: 2px; flex-wrap: wrap; }
|
||||
.seg { flex: 1; min-width: 92px; border: 1px solid var(--line-soft); padding: 12px 8px; display: flex; flex-direction: column; gap: 4px; align-items: center; justify-content: center; min-height: 54px; text-align: center; }
|
||||
.seg .sl { font-size: 12px; color: var(--ink); }
|
||||
.seg .sd { font-size: 10.5px; color: var(--muted); font-variant-numeric: tabular-nums; }
|
||||
.seg.done { background: var(--neon-deep); border-color: var(--neon); }
|
||||
.seg.done .sl { color: var(--neon); }
|
||||
.seg.run { border-color: var(--neon); background: rgba(57,255,20,.1); animation: pulse 1.4s ease-in-out infinite; }
|
||||
.seg.run .sl { color: var(--neon); text-shadow: var(--glow); }
|
||||
.seg.err { border-color: var(--red); } .seg.err .sl, .seg.err .sd { color: var(--red); }
|
||||
.seg.pend { opacity: .5; } .seg.pend .sl { color: var(--faint); }
|
||||
.seg.live { border-color: var(--neon); } .seg.live .sl { color: var(--neon); }
|
||||
.flow .arrow { color: var(--faint); font-size: 15px; padding: 0 2px; }
|
||||
@keyframes pulse { 0%,100% { box-shadow: 0 0 0 0 rgba(57,255,20,0); } 50% { box-shadow: 0 0 16px -2px rgba(57,255,20,.6); } }
|
||||
@media (prefers-reduced-motion: reduce) { .seg.run { animation: none; } }
|
||||
.bigbar { height: 26px; background: rgba(57,255,20,.08); border: 1px solid var(--line); position: relative; overflow: hidden; margin-bottom: 12px; }
|
||||
.bigbar > span { position: absolute; left: 0; top: 0; bottom: 0; background: linear-gradient(90deg, var(--neon), var(--neon-soft)); transition: width .6s ease; }
|
||||
.bigbar > em { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-style: normal; font-size: 12px; font-weight: 700; color: #fff; mix-blend-mode: difference; }
|
||||
.pmeta { display: flex; gap: 22px; flex-wrap: wrap; font-size: 12.5px; color: var(--muted); }
|
||||
.pmeta b { color: var(--neon); }
|
||||
.pstats { margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--line-soft); font-size: 12px; color: var(--muted); }
|
||||
.pstats b { color: var(--ink); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header class="top">
|
||||
<div class="wrap">
|
||||
<span class="brand"><b>UP</b>-LEAKS <span class="sub">// panel de control</span></span>
|
||||
<button class="ghost" id="logoutBtn" style="display:none">salir</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="wrap" id="gateWrap">
|
||||
<div class="gate">
|
||||
<h1>acceso restringido</h1>
|
||||
<p>Introduce el token de administración.</p>
|
||||
<input type="password" id="tokenInput" placeholder="ADMIN_TOKEN" autocomplete="off">
|
||||
<div class="row"><button id="loginBtn">entrar</button></div>
|
||||
<div class="gate-err" id="gateErr"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wrap" id="panel" style="display:none">
|
||||
<div class="toolbar">
|
||||
<div class="tabs">
|
||||
<button data-tab="envios" class="active">envíos</button>
|
||||
<button data-tab="pipelines">pipelines</button>
|
||||
<button data-tab="sistema">sistema</button>
|
||||
</div>
|
||||
<span class="spacer"></span>
|
||||
<span class="updated" id="updated"></span>
|
||||
<button class="ghost" id="refreshBtn">refrescar</button>
|
||||
</div>
|
||||
|
||||
<!-- ENVÍOS -->
|
||||
<div id="enviosView">
|
||||
<div class="filters" id="filters">
|
||||
<button data-f="todos" class="active">todos</button>
|
||||
<button data-f="nuevo">nuevos</button>
|
||||
<button data-f="revisado">revisados</button>
|
||||
<button data-f="archivado">archivados</button>
|
||||
</div>
|
||||
<div class="list" id="list"></div>
|
||||
</div>
|
||||
|
||||
<!-- PIPELINES -->
|
||||
<div id="pipesView" style="display:none">
|
||||
<div id="pipesView-body"></div>
|
||||
</div>
|
||||
|
||||
<!-- SISTEMA -->
|
||||
<div id="systemView" style="display:none">
|
||||
<div class="tiles" id="tiles"></div>
|
||||
<div class="sec-title">servicios</div>
|
||||
<div class="svcs" id="svcs"></div>
|
||||
<div class="sec-title">procesos en marcha</div>
|
||||
<div class="procs" id="procs"></div>
|
||||
<div class="sec-title">procesamiento</div>
|
||||
<div class="prog" id="prog"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var TOKEN = sessionStorage.getItem('upl_admin_token') || '';
|
||||
var subsData = [], filter = 'todos', tab = 'envios', sysTimer = null;
|
||||
var $ = function (id) { return document.getElementById(id); };
|
||||
function authHeaders() { return { 'Authorization': 'Bearer ' + TOKEN }; }
|
||||
function esc(s) { return String(s == null ? '' : s).replace(/[&<>"]/g, function (c) { return ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c]; }); }
|
||||
function fmtDate(d) { try { return new Date(d).toLocaleString('es-ES'); } catch (e) { return d; } }
|
||||
function human(b) { if (b < 1024) return b + ' B'; if (b < 1048576) return (b / 1024).toFixed(1) + ' KB'; if (b < 1073741824) return (b / 1048576).toFixed(1) + ' MB'; return (b / 1073741824).toFixed(1) + ' GB'; }
|
||||
function dur(s) { s = Math.max(0, Math.round(s)); var d = Math.floor(s / 86400), h = Math.floor(s % 86400 / 3600), m = Math.floor(s % 3600 / 60); if (d) return d + 'd ' + h + 'h'; if (h) return h + 'h ' + m + 'm'; if (m) return m + 'm ' + (s % 60) + 's'; return s + 's'; }
|
||||
function rel(ts) { var s = (Date.now() - new Date(ts).getTime()) / 1000; if (s < 60) return 'hace ' + Math.round(s) + 's'; if (s < 3600) return 'hace ' + Math.round(s / 60) + ' min'; if (s < 86400) return 'hace ' + Math.round(s / 3600) + ' h'; return 'hace ' + Math.round(s / 86400) + ' d'; }
|
||||
function meter(pct, cls) { pct = Math.max(0, Math.min(100, pct)); return '<div class="meter ' + (cls || '') + '"><span style="width:' + pct + '%"></span></div>'; }
|
||||
|
||||
function showPanel(show) { $('gateWrap').style.display = show ? 'none' : ''; $('panel').style.display = show ? '' : 'none'; $('logoutBtn').style.display = show ? '' : 'none'; }
|
||||
|
||||
/* ---------- ENVÍOS ---------- */
|
||||
function loadSubs() {
|
||||
return fetch('/api/submissions?limit=500', { headers: authHeaders() })
|
||||
.then(function (r) {
|
||||
if (r.status === 401) throw new Error('Token incorrecto.');
|
||||
if (r.status === 503) throw new Error('Panel no configurado en el servidor (falta ADMIN_TOKEN).');
|
||||
return r.json();
|
||||
})
|
||||
.then(function (d) {
|
||||
if (!d.ok) throw new Error(d.error || 'Error');
|
||||
subsData = d.submissions || []; sessionStorage.setItem('upl_admin_token', TOKEN);
|
||||
showPanel(true); renderSubs(); markUpdated();
|
||||
});
|
||||
}
|
||||
function renderSubs() {
|
||||
var list = $('list');
|
||||
var items = subsData.filter(function (s) { return filter === 'todos' || s.estado === filter; });
|
||||
if (!items.length) { list.innerHTML = '<div class="empty">— sin envíos —</div>'; return; }
|
||||
list.innerHTML = '';
|
||||
items.forEach(function (s) {
|
||||
var el = document.createElement('div'); el.className = 'sub';
|
||||
var files = (s.archivos || []).map(function (f) { return '<a data-file="' + esc(f.guardado) + '" data-name="' + esc(f.nombre_original) + '">' + esc(f.nombre_original) + ' · ' + human(f.bytes || 0) + '</a>'; }).join('');
|
||||
el.innerHTML =
|
||||
'<div class="head"><span class="ref">' + esc(s.ref) + '</span><span class="date">' + esc(fmtDate(s.creado)) + '</span>' +
|
||||
'<span class="tema">' + esc(s.tema) + '</span><span class="badge ' + esc(s.estado) + '">' + esc(s.estado) + '</span></div>' +
|
||||
(s.mensaje ? '<div class="msg">' + esc(s.mensaje) + '</div>' : '<div class="msg" style="color:var(--faint)">(sin mensaje)</div>') +
|
||||
(s.contacto ? '<div class="contacto">contacto: <b>' + esc(s.contacto) + '</b></div>' : '') +
|
||||
(files ? '<div class="files">' + files + '</div>' : '') +
|
||||
'<div class="actions"><button data-act="revisado" data-ref="' + esc(s.ref) + '">marcar revisado</button>' +
|
||||
'<button data-act="archivado" data-ref="' + esc(s.ref) + '">archivar</button>' +
|
||||
'<button data-act="nuevo" data-ref="' + esc(s.ref) + '" class="ghost">reabrir</button></div>';
|
||||
list.appendChild(el);
|
||||
});
|
||||
}
|
||||
function downloadFile(g, name) {
|
||||
fetch('/api/submissions/file/' + encodeURIComponent(g), { headers: authHeaders() })
|
||||
.then(function (r) { if (!r.ok) throw new Error(); return r.blob(); })
|
||||
.then(function (blob) { var u = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = u; a.download = name || g; document.body.appendChild(a); a.click(); a.remove(); setTimeout(function () { URL.revokeObjectURL(u); }, 4000); })
|
||||
.catch(function () { alert('No se pudo descargar el archivo.'); });
|
||||
}
|
||||
function setEstado(ref, estado) {
|
||||
fetch('/api/submissions/' + encodeURIComponent(ref) + '/estado', { method: 'POST', headers: Object.assign({ 'Content-Type': 'application/json' }, authHeaders()), body: JSON.stringify({ estado: estado }) })
|
||||
.then(function (r) { return r.json(); }).then(function (d) { if (d.ok) { var it = subsData.find(function (s) { return s.ref === ref; }); if (it) it.estado = estado; renderSubs(); } });
|
||||
}
|
||||
|
||||
/* ---------- SISTEMA ---------- */
|
||||
function loadSystem() {
|
||||
return fetch('/api/system', { headers: authHeaders() })
|
||||
.then(function (r) { if (r.status === 401) throw new Error('Token incorrecto.'); return r.json(); })
|
||||
.then(function (d) { if (!d.ok) throw new Error(d.error || 'Error'); renderSystem(d); renderPipes(d); markUpdated(); });
|
||||
}
|
||||
function statsLine(fases) {
|
||||
var f = (fases || []).find(function (x) { return x.stats && Object.keys(x.stats).length; });
|
||||
if (!f) return '';
|
||||
var parts = Object.keys(f.stats).slice(0, 5).map(function (k) { return esc(k.replace(/_/g, ' ')) + ': <b>' + esc(f.stats[k]) + '</b>'; });
|
||||
return '<div class="pstats">' + esc(f.label) + ' → ' + parts.join(' · ') + '</div>';
|
||||
}
|
||||
function renderPipes(d) {
|
||||
var out = '';
|
||||
var pr = d.progress;
|
||||
var procBy = function (k) { return (d.procesos || []).find(function (x) { return x.key === k; }); };
|
||||
|
||||
/* ---- 1 · Comparación semántica v2 (el motor nuevo, foco actual) ---- */
|
||||
var c = d.comparacion, motor = procBy('motor');
|
||||
if (c) {
|
||||
var badgeCls, badgeTxt;
|
||||
if (motor) { badgeCls = 'e-run'; badgeTxt = 'regenerando'; }
|
||||
else if (c.v2Activa) { badgeCls = 'e-run'; badgeTxt = 'activa en el grafo'; }
|
||||
else if (c.v2Aristas > 0) { badgeCls = 'e-idle'; badgeTxt = 'lista · standby'; }
|
||||
else { badgeCls = 'e-idle'; badgeTxt = 'sin generar'; }
|
||||
var srvV1 = !c.v2Activa, srvV2 = c.v2Activa;
|
||||
out += '<div class="pipe"><div class="phead"><span class="ptitle">Comparación semántica · v2 <span class="pcad">· noticias ↔ leaks por embeddings</span></span>' +
|
||||
'<span class="pstate ' + badgeCls + '">' + badgeTxt + '</span></div>' +
|
||||
'<div class="steps-bar">' +
|
||||
'<div class="seg ' + (srvV2 ? 'done' : 'pend') + '"><span class="sl">v2 semántica</span><span class="sd">' + c.v2Aristas.toLocaleString('es-ES') + ' aristas</span></div>' +
|
||||
'<div class="seg ' + (srvV1 ? 'live' : 'pend') + '"><span class="sl">v1 word-overlap</span><span class="sd">' + (c.v1Aristas >= 1e6 ? (c.v1Aristas / 1e6).toFixed(0) + 'M' : c.v1Aristas.toLocaleString('es-ES')) + ' aristas</span></div>' +
|
||||
'</div>' +
|
||||
'<div class="pmeta" style="margin-top:14px">' +
|
||||
'<span>sirve al grafo: <b>' + esc(c.activa) + '</b></span>' +
|
||||
'<span>posible_evento: <b>' + (c.v2Eventos || 0).toLocaleString('es-ES') + '</b></span>' +
|
||||
(c.modelo ? '<span>modelo: <b>' + esc(c.modelo) + '</b></span>' : '') +
|
||||
(motor ? '<span>regen: <b>' + dur(motor.etimes) + '</b> · CPU <b>' + motor.cpu.toFixed(0) + '%</b></span>' : '') +
|
||||
'</div>' +
|
||||
(c.v2Aristas > 0 && !c.v2Activa && !motor ? '<div class="pstats">v2 generada y en espera → falta el switch del front (<b>COMPARACIONES_COL=comparaciones_v2</b> + restart).</div>' : '') +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
/* ---- 2 · Backfill de embeddings (leaks → vectores) ---- */
|
||||
var bf = procBy('backfill');
|
||||
if (pr) {
|
||||
var noApto = pr.torrNoApto || 0;
|
||||
var bpct = pr.torrTotal ? pr.torrEmb / pr.torrTotal * 100 : 0;
|
||||
var procDone = (pr.torrEmb + noApto) >= pr.torrTotal;
|
||||
var bBadge = bf ? 'e-run' : (procDone ? 'e-idle' : 'e-idle');
|
||||
var bTxt = bf ? 'ejecutando' : (procDone ? 'completado' : 'detenido');
|
||||
out += '<div class="pipe"><div class="phead"><span class="ptitle">Backfill de embeddings <span class="pcad">· leaks → vectores</span></span>' +
|
||||
'<span class="pstate ' + bBadge + '">' + bTxt + '</span></div>' +
|
||||
'<div class="bigbar"><span style="width:' + Math.min(100, bpct).toFixed(1) + '%"></span><em>' + pr.torrEmb.toLocaleString('es-ES') + ' embebidos / ' + pr.torrTotal.toLocaleString('es-ES') + ' · ' + bpct.toFixed(0) + '%</em></div>' +
|
||||
'<div class="pmeta">' + (bf ? '<span>en marcha: <b>' + dur(bf.etimes) + '</b></span><span>CPU: <b>' + bf.cpu.toFixed(0) + '%</b></span>' : '<span>proceso no activo</span>') +
|
||||
'<span>no aptos: <b>' + noApto.toLocaleString('es-ES') + '</b></span>' +
|
||||
'<span>pendientes: <b>' + Math.max(0, pr.torrTotal - pr.torrEmb - noApto).toLocaleString('es-ES') + '</b></span>' +
|
||||
'<span>chunks: <b>' + pr.chunks.toLocaleString('es-ES') + '</b></span></div></div>';
|
||||
}
|
||||
|
||||
/* ---- 3 · Coconews flujo continuo (noticias) ---- */
|
||||
var stages = [['ingestor', 'Ingesta RSS'], ['translator', 'Traducción'], ['ner', 'Entidades'], ['embedder', 'Embedding']];
|
||||
var svcMap = {}; (d.services || []).forEach(function (s) { svcMap[s.name] = s.active; });
|
||||
var flow = stages.map(function (k) {
|
||||
var a = svcMap['coconews-' + k[0]];
|
||||
return '<div class="seg ' + (a ? 'live' : 'pend') + '"><span class="sl">' + k[1] + '</span><span class="sd">' + (a ? 'en vivo' : 'off') + '</span></div>';
|
||||
}).join('<span class="arrow">→</span>');
|
||||
out += '<div class="pipe"><div class="phead"><span class="ptitle">Coconews · flujo continuo <span class="pcad">· noticias en tiempo real</span></span>' +
|
||||
'<span class="pstate e-run">continuo</span></div><div class="flow">' + flow + '</div>' +
|
||||
(pr ? '<div class="pmeta" style="margin-top:14px"><span>noticias embebidas: <b>' + pr.notiEmb.toLocaleString('es-ES') + '</b> / ' + pr.notiTotal.toLocaleString('es-ES') + '</span>' + (pr.lastNoti ? '<span>última noticia: <b>' + rel(pr.lastNoti) + '</b></span>' : '') + '</div>' : '') + '</div>';
|
||||
|
||||
/* ---- 4 · Pipeline maestro word-overlap (legacy · retirado) ---- */
|
||||
var p = d.pipelines && d.pipelines.maestro;
|
||||
if (p) {
|
||||
var retirado = !p.habilitado && !p.running;
|
||||
var segs = p.fases.map(function (f) {
|
||||
var cls = retirado ? 'pend' : (f.estado === 'completado' ? 'done' : (f.estado === 'en_progreso' ? 'run' : (f.estado === 'error' ? 'err' : 'pend')));
|
||||
return '<div class="seg ' + cls + '"><span class="sl">' + esc(f.label) + '</span><span class="sd">' + (f.durSec != null ? dur(f.durSec) : '—') + '</span></div>';
|
||||
}).join('');
|
||||
var stCls, stTxt;
|
||||
if (retirado) { stCls = 'e-idle'; stTxt = 'retirado'; }
|
||||
else if (p.running) { stCls = 'e-run'; stTxt = 'ejecutando'; }
|
||||
else if (p.estado === 'error') { stCls = 'e-err'; stTxt = 'con errores'; }
|
||||
else { stCls = 'e-idle'; stTxt = 'en espera'; }
|
||||
var next = retirado ? 'desactivado' : (p.nextRun ? (p.running ? 'en ejecución' : 'en ' + dur((p.nextRun - d.now) / 1000)) : '—');
|
||||
out += '<div class="pipe" style="opacity:' + (retirado ? '.72' : '1') + '"><div class="phead"><span class="ptitle">Comparación word-overlap <span class="pcad">· motor viejo' + (retirado ? ', sustituido por v2' : ' · ' + esc(p.cadencia)) + '</span></span>' +
|
||||
'<span class="pstate ' + stCls + '">' + stTxt + '</span></div>' +
|
||||
'<div class="steps-bar">' + segs + '</div>' +
|
||||
'<div class="pmeta" style="margin-top:14px">' +
|
||||
'<span>última ejecución: <b>' + (p.lastDurSec != null ? dur(p.lastDurSec) : '—') + '</b>' + (p.lastFin ? ' (' + rel(p.lastFin) + ')' : '') + '</span>' +
|
||||
'<span>próxima: <b>' + next + '</b></span></div>' +
|
||||
(retirado ? '<div class="pstats">Timer <b>flujos-pipeline</b> deshabilitado. La colección <b>comparaciones</b> (word-overlap) se conserva intacta como respaldo.</div>' : statsLine(p.fases)) +
|
||||
'</div>';
|
||||
}
|
||||
$('pipesView-body').innerHTML = out || '<div class="empty">— sin datos de pipeline —</div>';
|
||||
}
|
||||
function renderSystem(d) {
|
||||
var h = d.host, load1 = h.load[0], loadPct = (load1 / h.cores) * 100;
|
||||
var loadCls = loadPct > 100 ? 'crit' : (loadPct > 70 ? 'warn' : '');
|
||||
var memUsed = h.totalmem - h.freemem, memPct = memUsed / h.totalmem * 100;
|
||||
var memCls = memPct > 90 ? 'crit' : (memPct > 75 ? 'warn' : '');
|
||||
$('tiles').innerHTML =
|
||||
tile('Carga (1m)', load1.toFixed(2) + ' <small>/ ' + h.cores + ' cores</small>', loadPct, loadCls) +
|
||||
tile('Memoria', human(memUsed) + ' <small>/ ' + human(h.totalmem) + '</small>', memPct, memCls) +
|
||||
tile('Núcleos', h.cores + ' <small>CPU</small>', null) +
|
||||
tile('Uptime', dur(h.uptime), null);
|
||||
|
||||
$('svcs').innerHTML = (d.services || []).map(function (s) {
|
||||
return '<span class="svc ' + (s.active ? 'up' : 'down') + '">' + esc(s.name) + '</span>';
|
||||
}).join('');
|
||||
|
||||
var procs = (d.procesos || []).slice().sort(function (a, b) { return b.cpu - a.cpu; });
|
||||
var rows = '<div class="prow hdr"><span>proceso</span><span style="text-align:right">cpu%</span><span>cpu</span><span style="text-align:right">tiempo</span><span style="text-align:right">pid</span></div>';
|
||||
rows += procs.map(function (p) {
|
||||
var barCls = p.cpu > 100 ? 'warn' : '';
|
||||
return '<div class="prow"><span class="name ' + (p.key === 'mongolo' ? 'viejo' : '') + '">' + esc(p.label) + '</span>' +
|
||||
'<span class="cpu">' + p.cpu.toFixed(0) + '%</span>' +
|
||||
'<span>' + meter(Math.min(100, p.cpu), barCls) + '</span>' +
|
||||
'<span class="time">' + dur(p.etimes) + '</span>' +
|
||||
'<span class="pid">' + p.pid + '</span></div>';
|
||||
}).join('');
|
||||
if (!procs.length) rows += '<div class="prow"><span class="name" style="color:var(--faint)">— sin procesos activos —</span></div>';
|
||||
$('procs').innerHTML = rows;
|
||||
|
||||
var pr = d.progress, out = '';
|
||||
if (pr) {
|
||||
out += progItem('Leaks embebidos (embeddings)', pr.torrEmb, pr.torrTotal);
|
||||
out += progItem('Noticias embebidas', pr.notiEmb, pr.notiTotal);
|
||||
out += progItem('Feeds activos', pr.feedsAct, pr.feedsAct, pr.feedsFallo + ' con fallos');
|
||||
var next = d.nextPipeline ? (d.nextPipeline > d.now ? 'en ' + dur((d.nextPipeline - d.now) / 1000) : 'inminente') : 'sin programar';
|
||||
out += '<div class="kv"><span>chunks de embeddings: <b>' + pr.chunks.toLocaleString('es-ES') + '</b></span>' +
|
||||
'<span>envíos nuevos: <b>' + pr.subsNuevos + '</b></span>' +
|
||||
'<span>próximo pipeline: <b>' + next + '</b></span>' +
|
||||
(pr.lastNoti ? '<span>última noticia: <b>' + rel(pr.lastNoti) + '</b></span>' : '') + '</div>';
|
||||
}
|
||||
$('prog').innerHTML = out;
|
||||
}
|
||||
function tile(k, v, pct, cls) {
|
||||
return '<div class="tile"><div class="k">' + k + '</div><div class="v">' + v + '</div>' + (pct == null ? '' : meter(pct, cls)) + '</div>';
|
||||
}
|
||||
function progItem(lbl, val, total, extra) {
|
||||
var pct = total ? (val / total * 100) : 0;
|
||||
return '<div class="item"><div class="top"><span class="lbl">' + esc(lbl) + '</span>' +
|
||||
'<span class="val"><b>' + val.toLocaleString('es-ES') + '</b> / ' + total.toLocaleString('es-ES') +
|
||||
' · ' + pct.toFixed(0) + '%' + (extra ? ' · ' + esc(extra) : '') + '</span></div>' + meter(pct) + '</div>';
|
||||
}
|
||||
|
||||
function markUpdated() { $('updated').textContent = 'actualizado ' + new Date().toLocaleTimeString('es-ES'); }
|
||||
|
||||
function switchTab(t) {
|
||||
tab = t;
|
||||
[].forEach.call(document.querySelectorAll('.tabs button'), function (b) { b.classList.toggle('active', b.dataset.tab === t); });
|
||||
$('enviosView').style.display = t === 'envios' ? '' : 'none';
|
||||
$('pipesView').style.display = t === 'pipelines' ? '' : 'none';
|
||||
$('systemView').style.display = t === 'sistema' ? '' : 'none';
|
||||
if (sysTimer) { clearInterval(sysTimer); sysTimer = null; }
|
||||
if (t === 'sistema' || t === 'pipelines') {
|
||||
loadSystem().catch(function (e) { alert(e.message); });
|
||||
sysTimer = setInterval(function () { loadSystem().catch(function () {}); }, 4000);
|
||||
} else { loadSubs().catch(function () {}); }
|
||||
}
|
||||
function refresh() { (tab === 'sistema' ? loadSystem() : loadSubs()).catch(function (e) { alert(e.message); }); }
|
||||
|
||||
/* ---------- eventos ---------- */
|
||||
$('loginBtn').addEventListener('click', function () {
|
||||
TOKEN = $('tokenInput').value.trim(); $('gateErr').textContent = '';
|
||||
if (!TOKEN) { $('gateErr').textContent = 'Introduce el token.'; return; }
|
||||
loadSubs().catch(function (e) { $('gateErr').textContent = e.message; showPanel(false); });
|
||||
});
|
||||
$('tokenInput').addEventListener('keydown', function (e) { if (e.key === 'Enter') $('loginBtn').click(); });
|
||||
$('refreshBtn').addEventListener('click', refresh);
|
||||
$('logoutBtn').addEventListener('click', function () { if (sysTimer) clearInterval(sysTimer); sysTimer = null; sessionStorage.removeItem('upl_admin_token'); TOKEN = ''; subsData = []; showPanel(false); $('tokenInput').value = ''; });
|
||||
document.querySelector('.tabs').addEventListener('click', function (e) { if (e.target.dataset.tab) switchTab(e.target.dataset.tab); });
|
||||
$('filters').addEventListener('click', function (e) { if (e.target.dataset.f) { filter = e.target.dataset.f; [].forEach.call(this.children, function (b) { b.classList.toggle('active', b.dataset.f === filter); }); renderSubs(); } });
|
||||
$('list').addEventListener('click', function (e) {
|
||||
var a = e.target.closest('a[data-file]'); if (a) { downloadFile(a.dataset.file, a.dataset.name); return; }
|
||||
var b = e.target.closest('button[data-act]'); if (b) setEstado(b.dataset.ref, b.dataset.act);
|
||||
});
|
||||
|
||||
if (TOKEN) loadSubs().catch(function () { showPanel(false); });
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,10 +1,6 @@
|
|||
@font-face {
|
||||
font-family: "Retrolift";
|
||||
src: url("fonts/retrolift.woff2") format("woff2"),
|
||||
url("fonts/retrolift.woff") format("woff");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
/* La familia "Retrolift" no se distribuye con el repo: no hay @font-face.
|
||||
Las reglas que la piden caen a sans-serif. Para usarla, deja el .woff2 en
|
||||
fonts/ y anade aqui la regla con una ruta relativa. */
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
|
|
@ -255,6 +251,28 @@ nav {
|
|||
background: #ff6600;
|
||||
}
|
||||
|
||||
#sidebar select {
|
||||
padding: 10px;
|
||||
border: 2px solid #39ff14;
|
||||
background: black;
|
||||
color: #39ff14;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.2);
|
||||
transition: box-shadow 0.3s ease;
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
font-family: 'Fira Code', monospace;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
#sidebar select:hover { box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.3); }
|
||||
#sidebar select option { background: #0a0a0a; color: #39ff14; }
|
||||
#sidebar .sim-val {
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
background: rgba(57,255,20,0.15);
|
||||
border-radius: 3px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
footer {
|
||||
width: 100%;
|
||||
|
|
@ -330,3 +348,54 @@ footer p {
|
|||
z-index: 1;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* ===== Panel de detalle (split-screen) ===== */
|
||||
main.split-screen { display: flex; height: 100vh; }
|
||||
#graphPanel { flex: 1; position: relative; min-width: 0; transition: flex 0.3s ease; }
|
||||
#detailPanel {
|
||||
width: 0; max-width: 0; overflow: hidden;
|
||||
transition: width 0.3s ease, max-width 0.3s ease;
|
||||
background: #0a0a0a; color: #fff;
|
||||
position: relative; z-index: 4;
|
||||
box-shadow: inset 2px 0 10px rgba(0,0,0,0.6);
|
||||
}
|
||||
main.split-screen.show-detail #detailPanel { width: 50%; max-width: 50%; }
|
||||
|
||||
#closeDetail {
|
||||
position: absolute; top: 12px; left: 12px;
|
||||
background: rgba(0,0,0,0.85); border: 1px solid #39ff14;
|
||||
color: #39ff14; font-size: 1em; font-family: 'Fira Code', monospace;
|
||||
cursor: pointer; border-radius: 4px; padding: 4px 12px; z-index: 10;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
#closeDetail:hover { background: rgba(57,255,20,0.15); }
|
||||
|
||||
#detailContent {
|
||||
padding: 52px 18px 20px; overflow-y: auto;
|
||||
height: 100%; box-sizing: border-box;
|
||||
display: none; flex-direction: column;
|
||||
}
|
||||
main.split-screen.show-detail #detailContent { display: flex; }
|
||||
|
||||
.detail-meta { display: flex; justify-content: space-between; font-size: 0.72em; color: #39ff14; opacity: 0.75; margin-bottom: 6px; gap: 8px; }
|
||||
.detail-author { font-weight: bold; }
|
||||
.detail-date { text-align: right; white-space: nowrap; }
|
||||
.detail-title { font-size: 0.92em; color: #39ff14; margin: 0 0 12px; word-break: break-word; line-height: 1.3; }
|
||||
|
||||
.detail-relations { border-top: 1px solid rgba(57,255,20,0.25); border-bottom: 1px solid rgba(57,255,20,0.25); padding: 8px 0; margin-bottom: 14px; font-size: 0.78em; flex-shrink: 0; }
|
||||
.relations-count { color: rgba(57,255,20,0.8); display: block; margin-bottom: 6px; }
|
||||
.relations-nav { display: flex; align-items: center; gap: 6px; }
|
||||
.relations-nav button { background: none; border: 1px solid #39ff14; color: #39ff14; cursor: pointer; padding: 3px 8px; border-radius: 3px; font-family: 'Fira Code', monospace; font-size: 0.85em; white-space: nowrap; transition: background 0.2s; }
|
||||
.relations-nav button:hover:not(:disabled) { background: rgba(57,255,20,0.12); }
|
||||
.relations-nav button:disabled { opacity: 0.3; cursor: default; }
|
||||
#relationLabel { flex: 1; text-align: center; color: rgba(255,255,255,0.55); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 0.85em; }
|
||||
|
||||
.detail-body { flex: 1; overflow-y: auto; white-space: pre-wrap; line-height: 1.55; font-size: 0.78em; color: #e0e0e0; font-family: 'Fira Code', monospace; min-height: 0; }
|
||||
.detail-img { width: 100%; max-height: 180px; object-fit: contain; margin-bottom: 12px; border: 1px solid rgba(57,255,20,0.3); border-radius: 3px; }
|
||||
#detailPanel .placeholder { color: rgba(255,255,255,0.3); font-style: italic; padding: 40px 20px; text-align: center; font-size: 0.85em; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
main.split-screen.show-detail #graphPanel { display: none; }
|
||||
main.split-screen.show-detail #detailPanel { width: 100%; max-width: 100%; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>CLIMATE</title>
|
||||
|
|
@ -156,6 +156,7 @@
|
|||
<button id="sidebarToggle">Toggle Sidebar</button>
|
||||
|
||||
<!-- Movemos la inclusión del script aquí, al final del body -->
|
||||
<script src="semana-toggle.js"></script>
|
||||
<script type="module" src="output_climate_pruebas.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
394
FLUJOS/VISUALIZACION/public/coconews/index.html
Normal file
394
FLUJOS/VISUALIZACION/public/coconews/index.html
Normal file
|
|
@ -0,0 +1,394 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>COCONEWS</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;700&family=Oswald:wght@400;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header>
|
||||
<div class="header-inner">
|
||||
<a href="/" class="back-link">← FLUJOS</a>
|
||||
<div class="logo">COCO<span>NEWS</span></div>
|
||||
<div class="header-tabs">
|
||||
<button class="tab-btn active" data-tab="news">NOTICIAS</button>
|
||||
<button class="tab-btn" data-tab="entities">ENTIDADES</button>
|
||||
<button class="tab-btn" data-tab="feeds">FEEDS</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="stats-bar" id="statsBar">
|
||||
<div class="stat-item"><span class="stat-val" id="statTotal">—</span><span class="stat-label">noticias</span></div>
|
||||
<div class="stat-sep">|</div>
|
||||
<div class="stat-item"><span class="stat-val" id="statHoy">—</span><span class="stat-label">hoy</span></div>
|
||||
<div class="stat-sep">|</div>
|
||||
<div class="stat-item"><span class="stat-val" id="statFeeds">—</span><span class="stat-label">feeds</span></div>
|
||||
<div class="stat-sep">|</div>
|
||||
<div class="stat-item"><span class="stat-val" id="statTrad">—</span><span class="stat-label">traducidas</span></div>
|
||||
<div class="stat-sep">|</div>
|
||||
<div class="stat-item"><span class="stat-val" id="statNer">—</span><span class="stat-label">con NER</span></div>
|
||||
</div>
|
||||
|
||||
<!-- TAB: NOTICIAS -->
|
||||
<div class="tab-content active" id="tab-news">
|
||||
<div class="layout">
|
||||
<aside class="sidebar">
|
||||
<div class="filter-group">
|
||||
<label>BUSCAR</label>
|
||||
<input type="text" id="searchInput" placeholder="palabra clave...">
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>TEMA</label>
|
||||
<select id="temaSelect">
|
||||
<option value="">— todos —</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>PAÍS / REGIÓN</label>
|
||||
<input type="text" id="paisInput" placeholder="ej: Rusia, China...">
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>ENTIDAD</label>
|
||||
<input type="text" id="entidadInput" placeholder="persona u organización...">
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
<button id="applyBtn">APLICAR</button>
|
||||
<button id="resetBtn" class="btn-sec">RESET</button>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-title">TOP TEMAS</div>
|
||||
<div id="temasBars"></div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-title">IDIOMAS</div>
|
||||
<div id="idiomasBars"></div>
|
||||
</div>
|
||||
|
||||
<div class="pagination">
|
||||
<button id="prevBtn" disabled>← PREV</button>
|
||||
<span id="pageInfo">1 / 1</span>
|
||||
<button id="nextBtn">NEXT →</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<section class="news-grid" id="newsGrid">
|
||||
<div class="loading">cargando...</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TAB: ENTIDADES -->
|
||||
<div class="tab-content" id="tab-entities">
|
||||
<div class="entities-layout">
|
||||
<div class="entity-panel">
|
||||
<div class="entity-panel-header">
|
||||
<span>PERSONAS</span>
|
||||
<div class="tipo-tabs">
|
||||
<button class="tipo-btn active" data-tipo="persona">PERSONAS</button>
|
||||
<button class="tipo-btn" data-tipo="organizacion">ORGS</button>
|
||||
<button class="tipo-btn" data-tipo="lugar">LUGARES</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="entidadesList" class="entities-list"></div>
|
||||
</div>
|
||||
<div class="entity-news">
|
||||
<div id="entityNewsTitle" class="entity-news-title">Selecciona una entidad</div>
|
||||
<div id="entityNewsGrid" class="news-grid"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TAB: FEEDS -->
|
||||
<div class="tab-content" id="tab-feeds">
|
||||
<div class="feeds-container">
|
||||
<div id="feedsList" class="feeds-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API = '/coconews/api';
|
||||
let currentPage = 1;
|
||||
let totalPages = 1;
|
||||
let currentFilters = {};
|
||||
let currentTipo = 'persona';
|
||||
|
||||
// ── TABS ──────────────────────────────────────────────────────────────────
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
document.getElementById('tab-' + btn.dataset.tab).classList.add('active');
|
||||
if (btn.dataset.tab === 'entities') loadEntidades(currentTipo);
|
||||
if (btn.dataset.tab === 'feeds') loadFeeds();
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.tipo-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
document.querySelectorAll('.tipo-btn').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
currentTipo = btn.dataset.tipo;
|
||||
loadEntidades(currentTipo);
|
||||
});
|
||||
});
|
||||
|
||||
// ── UTILS ─────────────────────────────────────────────────────────────────
|
||||
async function fetchJSON(url) {
|
||||
const r = await fetch(url);
|
||||
if (!r.ok) throw new Error(r.statusText);
|
||||
return r.json();
|
||||
}
|
||||
|
||||
function fmtDate(d) {
|
||||
if (!d) return '';
|
||||
return new Date(d).toLocaleDateString('es-ES', { day: '2-digit', month: 'short', year: 'numeric' });
|
||||
}
|
||||
|
||||
// ── STATS ─────────────────────────────────────────────────────────────────
|
||||
async function loadStats() {
|
||||
try {
|
||||
const s = await fetchJSON(`${API}/stats`);
|
||||
document.getElementById('statTotal').textContent = s.total_news.toLocaleString();
|
||||
document.getElementById('statHoy').textContent = s.hoy;
|
||||
document.getElementById('statFeeds').textContent = s.feeds_activos;
|
||||
document.getElementById('statTrad').textContent = s.traducidas.toLocaleString();
|
||||
document.getElementById('statNer').textContent = (s.con_ner || 0).toLocaleString();
|
||||
|
||||
renderBars('temasBars', s.por_tema, s.traducidas, (item) => {
|
||||
document.getElementById('temaSelect').value = item._id;
|
||||
currentFilters.tema = item._id;
|
||||
loadNews(1);
|
||||
});
|
||||
renderBars('idiomasBars', s.por_idioma, s.traducidas);
|
||||
} catch(e) { console.warn('Stats:', e); }
|
||||
}
|
||||
|
||||
function renderBars(containerId, items, total, onClick) {
|
||||
const el = document.getElementById(containerId);
|
||||
if (!items || !items.length) { el.innerHTML = '<div class="empty-bar">—</div>'; return; }
|
||||
el.innerHTML = items.slice(0, 8).map(item => {
|
||||
const pct = total > 0 ? Math.round((item.count / total) * 100) : 0;
|
||||
const cursor = onClick ? 'style="cursor:pointer"' : '';
|
||||
return `
|
||||
<div class="bar-row" ${cursor} data-id="${item._id}">
|
||||
<span class="bar-label">${item._id || '?'}</span>
|
||||
<div class="bar-track"><div class="bar-fill" style="width:${pct}%"></div></div>
|
||||
<span class="bar-count">${item.count}</span>
|
||||
</div>`;
|
||||
}).join('');
|
||||
if (onClick) {
|
||||
el.querySelectorAll('.bar-row').forEach(row => {
|
||||
row.addEventListener('click', () => onClick({ _id: row.dataset.id }));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── TEMAS SELECT ──────────────────────────────────────────────────────────
|
||||
async function loadTemas() {
|
||||
try {
|
||||
const temas = await fetchJSON(`${API}/temas`);
|
||||
const sel = document.getElementById('temaSelect');
|
||||
temas.forEach(t => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = t; opt.textContent = t;
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
} catch(e) { console.warn('Temas:', e); }
|
||||
}
|
||||
|
||||
// ── NEWS CARDS ────────────────────────────────────────────────────────────
|
||||
function buildCard(n) {
|
||||
const texto = n.texto || n.titulo_original || '';
|
||||
const titulo = texto.split('\n')[0].slice(0, 130);
|
||||
const resumen = texto.split('\n').slice(2).join(' ').slice(0, 220);
|
||||
const img = n.imagen_url
|
||||
? `<div class="card-img"><img src="${n.imagen_url}" alt="" loading="lazy" onerror="this.parentElement.style.display='none'"></div>`
|
||||
: '';
|
||||
const entidades = (n.entidades || []).slice(0, 4).map(e =>
|
||||
`<span class="ent-tag ent-${e.tipo}" title="${e.tipo}">${e.texto}</span>`
|
||||
).join('');
|
||||
|
||||
return `
|
||||
<article class="card" onclick="window.open('${n.url}','_blank')">
|
||||
${img}
|
||||
<div class="card-body">
|
||||
<div class="card-meta">
|
||||
<span class="tag-tema">${n.tema || ''}</span>
|
||||
<span class="card-fuente">${n.fuente || ''}</span>
|
||||
<span class="card-fecha">${fmtDate(n.fecha)}</span>
|
||||
</div>
|
||||
<h2 class="card-title">${titulo}</h2>
|
||||
${resumen ? `<p class="card-resumen">${resumen}…</p>` : ''}
|
||||
<div class="card-footer">
|
||||
<div class="card-ents">${entidades}</div>
|
||||
<div class="card-badges">
|
||||
${n.subtema ? `<span class="badge badge-pais">${n.subtema}</span>` : ''}
|
||||
${n.lang && n.lang !== 'es' ? `<span class="badge badge-lang">${n.lang.toUpperCase()}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>`;
|
||||
}
|
||||
|
||||
async function loadNews(page = 1) {
|
||||
const grid = document.getElementById('newsGrid');
|
||||
grid.innerHTML = '<div class="loading">cargando...</div>';
|
||||
const params = new URLSearchParams({ page, per_page: 24, ...currentFilters });
|
||||
try {
|
||||
const data = await fetchJSON(`${API}/news?${params}`);
|
||||
totalPages = data.total_pages || 1;
|
||||
currentPage = data.page;
|
||||
document.getElementById('pageInfo').textContent = `${currentPage} / ${totalPages}`;
|
||||
document.getElementById('prevBtn').disabled = currentPage <= 1;
|
||||
document.getElementById('nextBtn').disabled = currentPage >= totalPages;
|
||||
if (!data.news || data.news.length === 0) {
|
||||
grid.innerHTML = '<div class="loading">sin resultados</div>';
|
||||
return;
|
||||
}
|
||||
grid.innerHTML = data.news.map(buildCard).join('');
|
||||
} catch(e) {
|
||||
grid.innerHTML = `<div class="loading error">Error: ${e.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// ── ENTIDADES ─────────────────────────────────────────────────────────────
|
||||
const wikiCache = {};
|
||||
async function getWikiImage(name) {
|
||||
if (wikiCache[name] !== undefined) return wikiCache[name];
|
||||
try {
|
||||
const url = `https://en.wikipedia.org/api/rest_v1/page/summary/${encodeURIComponent(name)}`;
|
||||
const r = await fetch(url, { signal: AbortSignal.timeout(3000) });
|
||||
if (!r.ok) throw new Error();
|
||||
const d = await r.json();
|
||||
wikiCache[name] = d.thumbnail ? d.thumbnail.source : null;
|
||||
} catch { wikiCache[name] = null; }
|
||||
return wikiCache[name];
|
||||
}
|
||||
|
||||
function initials(name) {
|
||||
return name.split(' ').map(w => w[0]).slice(0,2).join('').toUpperCase();
|
||||
}
|
||||
|
||||
async function loadEntidades(tipo) {
|
||||
const list = document.getElementById('entidadesList');
|
||||
list.innerHTML = '<div class="loading">cargando...</div>';
|
||||
try {
|
||||
const data = await fetchJSON(`${API}/entidades?tipo=${tipo}&limit=40`);
|
||||
if (!data.length) { list.innerHTML = '<div class="loading">sin datos aún — el NER necesita procesar noticias primero</div>'; return; }
|
||||
const max = data[0].count;
|
||||
|
||||
list.innerHTML = data.map(e => `
|
||||
<div class="ent-row" data-texto="${e.texto}">
|
||||
<div class="ent-avatar" id="av-${btoa(e.texto).replace(/[^a-z0-9]/gi,'')}">${initials(e.texto)}</div>
|
||||
<div class="ent-info">
|
||||
<span class="ent-name">${e.texto}</span>
|
||||
<div class="ent-bar-row">
|
||||
<div class="ent-bar-track"><div class="ent-bar-fill" style="width:${Math.round(e.count/max*100)}%"></div></div>
|
||||
<span class="ent-count-label">${e.count} menciones</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>`).join('');
|
||||
|
||||
list.querySelectorAll('.ent-row').forEach(row => {
|
||||
row.addEventListener('click', () => {
|
||||
list.querySelectorAll('.ent-row').forEach(r => r.classList.remove('selected'));
|
||||
row.classList.add('selected');
|
||||
loadEntityNews(row.dataset.texto);
|
||||
});
|
||||
});
|
||||
|
||||
if (tipo === 'persona') {
|
||||
data.forEach(async e => {
|
||||
const img = await getWikiImage(e.texto);
|
||||
if (!img) return;
|
||||
const avId = 'av-' + btoa(e.texto).replace(/[^a-z0-9]/gi,'');
|
||||
const el = document.getElementById(avId);
|
||||
if (el) el.innerHTML = `<img src="${img}" alt="${e.texto}" onerror="this.parentElement.innerHTML='${initials(e.texto)}'">`;
|
||||
});
|
||||
}
|
||||
} catch(e) {
|
||||
list.innerHTML = `<div class="loading error">${e.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadEntityNews(entidad) {
|
||||
document.getElementById('entityNewsTitle').innerHTML = `${entidad} <small>noticias relacionadas</small>`;
|
||||
const grid = document.getElementById('entityNewsGrid');
|
||||
grid.innerHTML = '<div class="loading">cargando...</div>';
|
||||
try {
|
||||
const data = await fetchJSON(`${API}/news?entidad=${encodeURIComponent(entidad)}&per_page=12`);
|
||||
if (!data.news || !data.news.length) { grid.innerHTML = '<div class="loading">sin noticias</div>'; return; }
|
||||
grid.innerHTML = data.news.map(buildCard).join('');
|
||||
} catch(e) {
|
||||
grid.innerHTML = `<div class="loading error">${e.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// ── FEEDS ─────────────────────────────────────────────────────────────────
|
||||
async function loadFeeds() {
|
||||
const list = document.getElementById('feedsList');
|
||||
list.innerHTML = '<div class="loading">cargando...</div>';
|
||||
try {
|
||||
const data = await fetchJSON(`${API}/feeds?per_page=100`);
|
||||
list.innerHTML = data.feeds.map(f => `
|
||||
<div class="feed-row ${f.activo ? '' : 'feed-off'}">
|
||||
<div class="feed-info">
|
||||
<span class="feed-nombre">${f.nombre}</span>
|
||||
<span class="feed-tema tag-tema">${f.tema || ''}</span>
|
||||
${f.subtema ? `<span class="feed-sub">${f.subtema}</span>` : ''}
|
||||
</div>
|
||||
<div class="feed-meta">
|
||||
<span class="feed-lang">${f.idioma || ''}</span>
|
||||
<span class="feed-ok ${f.activo ? 'ok' : 'off'}">${f.activo ? '● activo' : '○ inactivo'}</span>
|
||||
${f.fallos > 0 ? `<span class="feed-fallos">${f.fallos} fallos</span>` : ''}
|
||||
${f.ultimo_ok ? `<span class="feed-date">${fmtDate(f.ultimo_ok)}</span>` : ''}
|
||||
</div>
|
||||
</div>`).join('');
|
||||
} catch(e) {
|
||||
list.innerHTML = `<div class="loading error">${e.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// ── EVENTOS ───────────────────────────────────────────────────────────────
|
||||
document.getElementById('applyBtn').addEventListener('click', () => {
|
||||
currentFilters = {};
|
||||
const q = document.getElementById('searchInput').value.trim();
|
||||
const tema = document.getElementById('temaSelect').value;
|
||||
const pais = document.getElementById('paisInput').value.trim();
|
||||
const ent = document.getElementById('entidadInput').value.trim();
|
||||
if (q) currentFilters.q = q;
|
||||
if (tema) currentFilters.tema = tema;
|
||||
if (pais) currentFilters.pais = pais;
|
||||
if (ent) currentFilters.entidad = ent;
|
||||
loadNews(1);
|
||||
});
|
||||
|
||||
document.getElementById('resetBtn').addEventListener('click', () => {
|
||||
['searchInput','paisInput','entidadInput'].forEach(id => document.getElementById(id).value = '');
|
||||
document.getElementById('temaSelect').value = '';
|
||||
currentFilters = {};
|
||||
loadNews(1);
|
||||
});
|
||||
|
||||
document.getElementById('searchInput').addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter') document.getElementById('applyBtn').click();
|
||||
});
|
||||
|
||||
document.getElementById('prevBtn').addEventListener('click', () => loadNews(currentPage - 1));
|
||||
document.getElementById('nextBtn').addEventListener('click', () => loadNews(currentPage + 1));
|
||||
|
||||
// ── INIT ──────────────────────────────────────────────────────────────────
|
||||
loadStats();
|
||||
loadTemas();
|
||||
loadNews(1);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
413
FLUJOS/VISUALIZACION/public/coconews/style.css
Normal file
413
FLUJOS/VISUALIZACION/public/coconews/style.css
Normal file
|
|
@ -0,0 +1,413 @@
|
|||
@import url('https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;700&family=Oswald:wght@400;600;700&display=swap');
|
||||
|
||||
:root {
|
||||
--bg: #000;
|
||||
--surface: #0d0d0d;
|
||||
--surface2: #1a1a1a;
|
||||
--border: #2a2a2a;
|
||||
--accent: #00ff6a;
|
||||
--accent2: #00c4ff;
|
||||
--warn: #ff6a00;
|
||||
--text: #fff;
|
||||
--muted: #888;
|
||||
--font-head: 'Oswald', Impact, 'Arial Narrow', sans-serif;
|
||||
--font-mono: 'Fira Code', monospace;
|
||||
}
|
||||
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
html, body { height: 100%; }
|
||||
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
a { color: var(--accent); text-decoration: none; }
|
||||
|
||||
/* ── HEADER ── */
|
||||
header {
|
||||
background: #000;
|
||||
border-bottom: 2px solid var(--accent);
|
||||
padding: 0 24px;
|
||||
height: 56px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
.header-inner { display: flex; align-items: center; gap: 28px; width: 100%; }
|
||||
.back-link { color: var(--muted); font-size: 11px; letter-spacing: 1px; white-space: nowrap; font-family: var(--font-mono); }
|
||||
.back-link:hover { color: var(--accent); }
|
||||
|
||||
.logo {
|
||||
font-family: var(--font-head);
|
||||
font-weight: 700;
|
||||
font-size: 26px;
|
||||
letter-spacing: 4px;
|
||||
color: var(--text);
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.logo span { color: var(--accent); }
|
||||
|
||||
.header-tabs { display: flex; gap: 2px; margin-left: auto; }
|
||||
.tab-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: 3px solid transparent;
|
||||
color: var(--muted);
|
||||
font-family: var(--font-head);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 2px;
|
||||
padding: 6px 18px 3px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.tab-btn:hover { color: var(--text); }
|
||||
.tab-btn.active { color: var(--accent); border-bottom-color: var(--accent); }
|
||||
|
||||
/* ── STATS BAR ── */
|
||||
.stats-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
background: var(--surface);
|
||||
border-bottom: 1px solid var(--border);
|
||||
overflow: hidden;
|
||||
}
|
||||
.stat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 10px 28px;
|
||||
border-right: 1px solid var(--border);
|
||||
flex: 1;
|
||||
}
|
||||
.stat-val {
|
||||
font-family: var(--font-head);
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: var(--accent);
|
||||
line-height: 1;
|
||||
}
|
||||
.stat-label {
|
||||
font-size: 9px;
|
||||
color: var(--muted);
|
||||
letter-spacing: 2px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.stat-sep { display: none; }
|
||||
|
||||
/* ── TABS ── */
|
||||
.tab-content { display: none; flex: 1; min-height: 0; }
|
||||
.tab-content.active { display: flex; flex-direction: column; flex: 1; }
|
||||
|
||||
/* ── NEWS LAYOUT ── */
|
||||
.layout { display: grid; grid-template-columns: 240px 1fr; flex: 1; min-height: 0; overflow: hidden; }
|
||||
|
||||
/* ── SIDEBAR ── */
|
||||
.sidebar {
|
||||
background: var(--surface);
|
||||
border-right: 1px solid var(--border);
|
||||
padding: 20px 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.filter-group { display: flex; flex-direction: column; gap: 6px; }
|
||||
.filter-group label {
|
||||
font-family: var(--font-head);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
letter-spacing: 2px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
input, select {
|
||||
background: #000;
|
||||
border: 1px solid var(--border);
|
||||
border-bottom: 2px solid var(--border);
|
||||
color: var(--text);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
padding: 8px 10px;
|
||||
outline: none;
|
||||
width: 100%;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
input:focus, select:focus { border-color: var(--accent); border-bottom-color: var(--accent); }
|
||||
select option { background: #111; }
|
||||
|
||||
.btn-row { display: flex; gap: 6px; }
|
||||
|
||||
button {
|
||||
background: var(--accent);
|
||||
color: #000;
|
||||
border: none;
|
||||
font-family: var(--font-head);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 2px;
|
||||
padding: 9px 14px;
|
||||
cursor: pointer;
|
||||
text-transform: uppercase;
|
||||
transition: all 0.15s;
|
||||
flex: 1;
|
||||
}
|
||||
button:hover { background: #fff; }
|
||||
button:disabled { opacity: 0.25; cursor: not-allowed; }
|
||||
button.btn-sec {
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
button.btn-sec:hover { color: var(--text); border-color: var(--text); background: transparent; }
|
||||
|
||||
.sidebar-section { display: flex; flex-direction: column; gap: 8px; }
|
||||
.sidebar-title {
|
||||
font-family: var(--font-head);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
letter-spacing: 2px;
|
||||
text-transform: uppercase;
|
||||
padding-bottom: 6px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.bar-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 50px 30px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
.bar-row:hover { opacity: 0.75; }
|
||||
.bar-label {
|
||||
font-size: 11px;
|
||||
color: var(--text);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-transform: uppercase;
|
||||
font-family: var(--font-head);
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.bar-track { height: 5px; background: var(--border); overflow: hidden; }
|
||||
.bar-fill { height: 100%; background: var(--accent); transition: width 0.4s; }
|
||||
.bar-count { font-size: 11px; color: var(--muted); text-align: right; font-family: var(--font-head); }
|
||||
.empty-bar { font-size: 11px; color: var(--muted); }
|
||||
|
||||
.pagination { display: flex; align-items: center; gap: 8px; margin-top: auto; padding-top: 12px; border-top: 1px solid var(--border); }
|
||||
.pagination button { padding: 6px 10px; font-size: 11px; flex: none; }
|
||||
#pageInfo { font-size: 12px; color: var(--muted); text-align: center; flex: 1; font-family: var(--font-head); letter-spacing: 1px; }
|
||||
|
||||
/* ── NEWS GRID ── */
|
||||
.news-grid {
|
||||
padding: 20px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 14px;
|
||||
align-content: start;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.loading { grid-column: 1/-1; padding: 60px; text-align: center; color: var(--muted); font-size: 13px; letter-spacing: 2px; }
|
||||
.loading.error { color: var(--warn); }
|
||||
|
||||
/* ── CARD ── */
|
||||
.card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s, transform 0.2s, box-shadow 0.2s;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
.card:hover {
|
||||
border-color: var(--accent);
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 24px rgba(0,255,106,0.1);
|
||||
}
|
||||
.card-img { height: 160px; overflow: hidden; background: var(--surface2); position: relative; }
|
||||
.card-img img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
||||
.card-body { padding: 12px 14px; display: flex; flex-direction: column; gap: 8px; flex: 1; }
|
||||
.card-meta { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||||
|
||||
.tag-tema {
|
||||
font-family: var(--font-head);
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 2px;
|
||||
padding: 3px 8px;
|
||||
background: var(--accent);
|
||||
color: #000;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.card-fuente { font-size: 10px; color: var(--accent2); letter-spacing: 1px; font-family: var(--font-head); }
|
||||
.card-fecha { font-size: 10px; color: var(--muted); margin-left: auto; }
|
||||
|
||||
.card-title {
|
||||
font-family: var(--font-head);
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
line-height: 1.3;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.card-resumen { font-size: 11px; color: var(--muted); line-height: 1.6; }
|
||||
|
||||
.card-footer { display: flex; justify-content: space-between; align-items: flex-end; gap: 8px; margin-top: auto; padding-top: 8px; border-top: 1px solid var(--border); }
|
||||
.card-ents { display: flex; flex-wrap: wrap; gap: 4px; }
|
||||
.card-badges { display: flex; gap: 4px; flex-shrink: 0; }
|
||||
|
||||
.ent-tag {
|
||||
font-family: var(--font-head);
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
padding: 2px 7px;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.ent-persona { background: rgba(0,196,255,0.12); color: var(--accent2); border: 1px solid rgba(0,196,255,0.25); }
|
||||
.ent-organizacion { background: rgba(255,106,0,0.12); color: var(--warn); border: 1px solid rgba(255,106,0,0.25); }
|
||||
.ent-lugar { background: rgba(160,90,255,0.12); color: #b07aff; border: 1px solid rgba(160,90,255,0.25); }
|
||||
|
||||
.badge { font-family: var(--font-head); font-size: 10px; font-weight: 600; padding: 2px 7px; letter-spacing: 1px; text-transform: uppercase; }
|
||||
.badge-pais { background: var(--surface2); color: var(--muted); border: 1px solid var(--border); }
|
||||
.badge-lang { background: rgba(0,255,106,0.1); color: var(--accent); border: 1px solid rgba(0,255,106,0.2); }
|
||||
|
||||
/* ── ENTIDADES TAB ── */
|
||||
.entities-layout { display: grid; grid-template-columns: 360px 1fr; flex: 1; min-height: 0; overflow: hidden; }
|
||||
|
||||
.entity-panel { background: var(--surface); border-right: 1px solid var(--border); display: flex; flex-direction: column; overflow: hidden; }
|
||||
.entity-panel-header { padding: 16px; border-bottom: 1px solid var(--border); display: flex; flex-direction: column; gap: 10px; }
|
||||
|
||||
.tipo-tabs { display: flex; gap: 4px; }
|
||||
.tipo-btn {
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
font-size: 12px;
|
||||
font-family: var(--font-head);
|
||||
font-weight: 700;
|
||||
letter-spacing: 2px;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
border: 1px solid var(--border);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.tipo-btn.active { color: #000; background: var(--accent); border-color: var(--accent); }
|
||||
|
||||
.entities-list { flex: 1; overflow-y: auto; padding: 12px; display: flex; flex-direction: column; gap: 6px; }
|
||||
|
||||
.ent-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 12px;
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.ent-row:hover { border-color: var(--border); background: var(--surface2); }
|
||||
.ent-row.selected { border-color: var(--accent); background: rgba(0,255,106,0.05); }
|
||||
|
||||
.ent-avatar {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
background: var(--surface2);
|
||||
border: 2px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: var(--font-head);
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: var(--muted);
|
||||
}
|
||||
.ent-avatar img { width: 100%; height: 100%; object-fit: cover; }
|
||||
.ent-row.selected .ent-avatar { border-color: var(--accent); }
|
||||
|
||||
.ent-info { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
|
||||
.ent-name {
|
||||
font-family: var(--font-head);
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.ent-bar-row { display: flex; align-items: center; gap: 8px; }
|
||||
.ent-bar-track { flex: 1; height: 4px; background: var(--border); overflow: hidden; }
|
||||
.ent-bar-fill { height: 100%; background: var(--accent); transition: width 0.4s; }
|
||||
.ent-count-label { font-size: 11px; color: var(--muted); font-family: var(--font-head); letter-spacing: 1px; flex-shrink: 0; }
|
||||
|
||||
.entity-news { display: flex; flex-direction: column; overflow: hidden; }
|
||||
.entity-news-title {
|
||||
padding: 16px 20px;
|
||||
font-family: var(--font-head);
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 3px;
|
||||
color: var(--accent);
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.entity-news-title small { font-size: 12px; color: var(--muted); margin-left: 8px; letter-spacing: 1px; }
|
||||
.entity-news .news-grid { flex: 1; overflow-y: auto; }
|
||||
|
||||
/* ── FEEDS TAB ── */
|
||||
.feeds-container { padding: 20px; overflow-y: auto; }
|
||||
.feeds-list { display: flex; flex-direction: column; gap: 8px; }
|
||||
.feed-row {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-left: 3px solid var(--border);
|
||||
padding: 14px 18px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
.feed-row:hover { border-left-color: var(--accent); }
|
||||
.feed-off { opacity: 0.4; border-left-color: var(--muted) !important; }
|
||||
.feed-info { display: flex; align-items: center; gap: 12px; flex: 1; }
|
||||
.feed-nombre { font-family: var(--font-head); font-size: 16px; font-weight: 700; letter-spacing: 1px; text-transform: uppercase; }
|
||||
.feed-sub { font-size: 11px; color: var(--muted); }
|
||||
.feed-meta { display: flex; align-items: center; gap: 12px; flex-shrink: 0; }
|
||||
.feed-lang { font-size: 11px; color: var(--muted); font-family: var(--font-head); letter-spacing: 1px; }
|
||||
.feed-ok.ok { font-family: var(--font-head); font-size: 11px; color: var(--accent); letter-spacing: 1px; }
|
||||
.feed-ok.off { font-family: var(--font-head); font-size: 11px; color: var(--muted); letter-spacing: 1px; }
|
||||
.feed-fallos { font-size: 11px; color: var(--warn); font-family: var(--font-head); }
|
||||
.feed-date { font-size: 11px; color: var(--muted); }
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.layout, .entities-layout { grid-template-columns: 1fr; }
|
||||
.sidebar { border-right: none; border-bottom: 1px solid var(--border); max-height: 50vh; }
|
||||
.stats-bar { flex-wrap: wrap; }
|
||||
.stat-item { flex: 1; min-width: 80px; }
|
||||
}
|
||||
|
|
@ -1,10 +1,6 @@
|
|||
@font-face {
|
||||
font-family: "Retrolift";
|
||||
src: url("fonts/retrolift.woff2") format("woff2"),
|
||||
url("fonts/retrolift.woff") format("woff");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
/* La familia "Retrolift" no se distribuye con el repo: no hay @font-face.
|
||||
Las reglas que la piden caen a sans-serif. Para usarla, deja el .woff2 en
|
||||
fonts/ y anade aqui la regla con una ruta relativa. */
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
|
|
@ -195,6 +191,29 @@ nav {
|
|||
background: #ff6600;
|
||||
}
|
||||
|
||||
#sidebar select {
|
||||
padding: 10px;
|
||||
border: 2px solid #39ff14;
|
||||
background: black;
|
||||
color: #39ff14;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.2);
|
||||
transition: box-shadow 0.3s ease;
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
font-family: 'Fira Code', monospace;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
#sidebar select:hover { box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.3); }
|
||||
#sidebar select option { background: #0a0a0a; color: #39ff14; }
|
||||
#sidebar .sim-val {
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
background: rgba(57,255,20,0.15);
|
||||
border-radius: 3px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
footer {
|
||||
width: 100%;
|
||||
background-color: #000000;
|
||||
|
|
@ -234,3 +253,54 @@ footer p {
|
|||
left: 250px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* ===== Panel de detalle (split-screen) ===== */
|
||||
main.split-screen { display: flex; height: 100vh; }
|
||||
#graphPanel { flex: 1; position: relative; min-width: 0; transition: flex 0.3s ease; }
|
||||
#detailPanel {
|
||||
width: 0; max-width: 0; overflow: hidden;
|
||||
transition: width 0.3s ease, max-width 0.3s ease;
|
||||
background: #0a0a0a; color: #fff;
|
||||
position: relative; z-index: 4;
|
||||
box-shadow: inset 2px 0 10px rgba(0,0,0,0.6);
|
||||
}
|
||||
main.split-screen.show-detail #detailPanel { width: 50%; max-width: 50%; }
|
||||
|
||||
#closeDetail {
|
||||
position: absolute; top: 12px; left: 12px;
|
||||
background: rgba(0,0,0,0.85); border: 1px solid #39ff14;
|
||||
color: #39ff14; font-size: 1em; font-family: 'Fira Code', monospace;
|
||||
cursor: pointer; border-radius: 4px; padding: 4px 12px; z-index: 10;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
#closeDetail:hover { background: rgba(57,255,20,0.15); }
|
||||
|
||||
#detailContent {
|
||||
padding: 52px 18px 20px; overflow-y: auto;
|
||||
height: 100%; box-sizing: border-box;
|
||||
display: none; flex-direction: column;
|
||||
}
|
||||
main.split-screen.show-detail #detailContent { display: flex; }
|
||||
|
||||
.detail-meta { display: flex; justify-content: space-between; font-size: 0.72em; color: #39ff14; opacity: 0.75; margin-bottom: 6px; gap: 8px; }
|
||||
.detail-author { font-weight: bold; }
|
||||
.detail-date { text-align: right; white-space: nowrap; }
|
||||
.detail-title { font-size: 0.92em; color: #39ff14; margin: 0 0 12px; word-break: break-word; line-height: 1.3; }
|
||||
|
||||
.detail-relations { border-top: 1px solid rgba(57,255,20,0.25); border-bottom: 1px solid rgba(57,255,20,0.25); padding: 8px 0; margin-bottom: 14px; font-size: 0.78em; flex-shrink: 0; }
|
||||
.relations-count { color: rgba(57,255,20,0.8); display: block; margin-bottom: 6px; }
|
||||
.relations-nav { display: flex; align-items: center; gap: 6px; }
|
||||
.relations-nav button { background: none; border: 1px solid #39ff14; color: #39ff14; cursor: pointer; padding: 3px 8px; border-radius: 3px; font-family: 'Fira Code', monospace; font-size: 0.85em; white-space: nowrap; transition: background 0.2s; }
|
||||
.relations-nav button:hover:not(:disabled) { background: rgba(57,255,20,0.12); }
|
||||
.relations-nav button:disabled { opacity: 0.3; cursor: default; }
|
||||
#relationLabel { flex: 1; text-align: center; color: rgba(255,255,255,0.55); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 0.85em; }
|
||||
|
||||
.detail-body { flex: 1; overflow-y: auto; white-space: pre-wrap; line-height: 1.55; font-size: 0.78em; color: #e0e0e0; font-family: 'Fira Code', monospace; min-height: 0; }
|
||||
.detail-img { width: 100%; max-height: 180px; object-fit: contain; margin-bottom: 12px; border: 1px solid rgba(57,255,20,0.3); border-radius: 3px; }
|
||||
#detailPanel .placeholder { color: rgba(255,255,255,0.3); font-style: italic; padding: 40px 20px; text-align: center; font-size: 0.85em; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
main.split-screen.show-detail #graphPanel { display: none; }
|
||||
main.split-screen.show-detail #detailPanel { width: 100%; max-width: 100%; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -156,6 +156,7 @@
|
|||
<button id="sidebarToggle">Toggle Sidebar</button>
|
||||
|
||||
<!-- Incluir tu script al final del body -->
|
||||
<script src="semana-toggle.js"></script>
|
||||
<script type="module" src="output_eco_corp_pruebas.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
BIN
FLUJOS/VISUALIZACION/public/fonts/fira-code-400.woff2
Normal file
BIN
FLUJOS/VISUALIZACION/public/fonts/fira-code-400.woff2
Normal file
Binary file not shown.
BIN
FLUJOS/VISUALIZACION/public/fonts/fira-code-500.woff2
Normal file
BIN
FLUJOS/VISUALIZACION/public/fonts/fira-code-500.woff2
Normal file
Binary file not shown.
BIN
FLUJOS/VISUALIZACION/public/fonts/fira-code-700.woff2
Normal file
BIN
FLUJOS/VISUALIZACION/public/fonts/fira-code-700.woff2
Normal file
Binary file not shown.
|
|
@ -1,10 +1,6 @@
|
|||
@font-face {
|
||||
font-family: "Retrolift";
|
||||
src: url("fonts/retrolift.woff2") format("woff2"),
|
||||
url("fonts/retrolift.woff") format("woff");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
/* La familia "Retrolift" no se distribuye con el repo: no hay @font-face.
|
||||
Las reglas que la piden caen a sans-serif. Para usarla, deja el .woff2 en
|
||||
fonts/ y anade aqui la regla con una ruta relativa. */
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
|
|
@ -255,6 +251,35 @@ nav {
|
|||
background: #ff6600;
|
||||
}
|
||||
|
||||
#sidebar select {
|
||||
padding: 10px;
|
||||
border: 2px solid #39ff14;
|
||||
background: black;
|
||||
color: #39ff14;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.2);
|
||||
transition: box-shadow 0.3s ease;
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
font-family: 'Fira Code', monospace;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
#sidebar select:hover {
|
||||
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
#sidebar select option {
|
||||
background: #0a0a0a;
|
||||
color: #39ff14;
|
||||
}
|
||||
|
||||
#sidebar .sim-val {
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
background: rgba(57,255,20,0.15);
|
||||
border-radius: 3px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
|
||||
footer {
|
||||
width: 100%;
|
||||
|
|
@ -329,3 +354,53 @@ footer p {
|
|||
position: absolute;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* ===== Panel de detalle (split-screen) ===== */
|
||||
main.split-screen { display: flex; height: 100vh; }
|
||||
#graphPanel { flex: 1; position: relative; min-width: 0; transition: flex 0.3s ease; }
|
||||
#detailPanel {
|
||||
width: 0; max-width: 0; overflow: hidden;
|
||||
transition: width 0.3s ease, max-width 0.3s ease;
|
||||
background: #0a0a0a; color: #fff;
|
||||
position: relative; z-index: 4;
|
||||
box-shadow: inset 2px 0 10px rgba(0,0,0,0.6);
|
||||
}
|
||||
main.split-screen.show-detail #detailPanel { width: 50%; max-width: 50%; }
|
||||
|
||||
#closeDetail {
|
||||
position: absolute; top: 12px; left: 12px;
|
||||
background: rgba(0,0,0,0.85); border: 1px solid #39ff14;
|
||||
color: #39ff14; font-size: 1em; font-family: 'Fira Code', monospace;
|
||||
cursor: pointer; border-radius: 4px; padding: 4px 12px; z-index: 10;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
#closeDetail:hover { background: rgba(57,255,20,0.15); }
|
||||
|
||||
#detailContent {
|
||||
padding: 52px 18px 20px; overflow-y: auto;
|
||||
height: 100%; box-sizing: border-box;
|
||||
display: none; flex-direction: column;
|
||||
}
|
||||
main.split-screen.show-detail #detailContent { display: flex; }
|
||||
|
||||
.detail-meta { display: flex; justify-content: space-between; font-size: 0.72em; color: #39ff14; opacity: 0.75; margin-bottom: 6px; gap: 8px; }
|
||||
.detail-author { font-weight: bold; }
|
||||
.detail-date { text-align: right; white-space: nowrap; }
|
||||
.detail-title { font-size: 0.92em; color: #39ff14; margin: 0 0 12px; word-break: break-word; line-height: 1.3; }
|
||||
|
||||
.detail-relations { border-top: 1px solid rgba(57,255,20,0.25); border-bottom: 1px solid rgba(57,255,20,0.25); padding: 8px 0; margin-bottom: 14px; font-size: 0.78em; flex-shrink: 0; }
|
||||
.relations-count { color: rgba(57,255,20,0.8); display: block; margin-bottom: 6px; }
|
||||
.relations-nav { display: flex; align-items: center; gap: 6px; }
|
||||
.relations-nav button { background: none; border: 1px solid #39ff14; color: #39ff14; cursor: pointer; padding: 3px 8px; border-radius: 3px; font-family: 'Fira Code', monospace; font-size: 0.85em; white-space: nowrap; transition: background 0.2s; }
|
||||
.relations-nav button:hover:not(:disabled) { background: rgba(57,255,20,0.12); }
|
||||
.relations-nav button:disabled { opacity: 0.3; cursor: default; }
|
||||
#relationLabel { flex: 1; text-align: center; color: rgba(255,255,255,0.55); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 0.85em; }
|
||||
|
||||
.detail-body { flex: 1; overflow-y: auto; white-space: pre-wrap; line-height: 1.55; font-size: 0.78em; color: #e0e0e0; font-family: 'Fira Code', monospace; min-height: 0; }
|
||||
.detail-img { width: 100%; max-height: 180px; object-fit: contain; margin-bottom: 12px; border: 1px solid rgba(57,255,20,0.3); border-radius: 3px; }
|
||||
#detailPanel .placeholder { color: rgba(255,255,255,0.3); font-style: italic; padding: 40px 20px; text-align: center; font-size: 0.85em; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
main.split-screen.show-detail #graphPanel { display: none; }
|
||||
main.split-screen.show-detail #detailPanel { width: 100%; max-width: 100%; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Glob-War</title>
|
||||
|
|
@ -154,6 +154,7 @@
|
|||
<button id="sidebarToggle">Toggle Sidebar</button>
|
||||
|
||||
<!-- Incluir tu script al final del body -->
|
||||
<script src="semana-toggle.js"></script>
|
||||
<script type="module" src="output_glob_war_pruebas.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
|
@ -8,6 +8,19 @@
|
|||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;700&family=Nosifer&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
/* selector de idioma */
|
||||
.lang-toggle {
|
||||
display: inline-flex; gap: 2px; margin-left: 10px; vertical-align: middle;
|
||||
border: 1px solid #39ff14; font-family: 'Fira Code', monospace; font-size: 12px;
|
||||
}
|
||||
.lang-toggle button {
|
||||
background: #000; color: #39ff14; border: none; padding: 4px 9px; cursor: pointer;
|
||||
font-family: inherit; font-size: inherit; letter-spacing: .05em; transition: background .15s, color .15s;
|
||||
}
|
||||
.lang-toggle button[aria-pressed="true"] { background: #39ff14; color: #000; font-weight: 700; }
|
||||
.lang-toggle button:hover:not([aria-pressed="true"]) { color: #7dff5e; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
|
@ -16,8 +29,12 @@
|
|||
<h1 class="title">UP-LEAKS</h1>
|
||||
<div class="header-buttons header-buttons--left">
|
||||
<a href="/coconews/" class="small-button coconews-btn">COCONEWS</a>
|
||||
<a href="journalist.html" class="small-button">Journalist</a>
|
||||
<a href="journalist.html" class="small-button" data-i18n="nav_journalist">Periodistas</a>
|
||||
<a href="stats.html" class="small-button stats-btn">STATS</a>
|
||||
<span class="lang-toggle" role="group" aria-label="Idioma / Language">
|
||||
<button type="button" data-lang="es" aria-pressed="true">ES</button>
|
||||
<button type="button" data-lang="en" aria-pressed="false">EN</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
|
@ -55,48 +72,89 @@
|
|||
<!-- Botones sobre las imágenes -->
|
||||
<div class="button-overlay">
|
||||
<div class="button-column">
|
||||
<a href="glob-war.html?subtematica=conflictos_internacionales" class="overlay-button">Conflictos Internacionales</a>
|
||||
<a href="glob-war.html?subtematica=guerras_civiles" class="overlay-button">Guerras Civiles</a>
|
||||
<a href="glob-war.html?subtematica=terrorismo" class="overlay-button">Terrorismo</a>
|
||||
<a href="glob-war.html?subtematica=armas" class="overlay-button">Armas</a>
|
||||
<a href="glob-war.html?subtematica=alianzas_militares" class="overlay-button">Alianzas Militares</a>
|
||||
<a href="glob-war.html?subtematica=conflictos_internacionales" class="overlay-button" data-i18n="gw_1">Conflictos Internacionales</a>
|
||||
<a href="glob-war.html?subtematica=guerras_civiles" class="overlay-button" data-i18n="gw_2">Guerras Civiles</a>
|
||||
<a href="glob-war.html?subtematica=terrorismo" class="overlay-button" data-i18n="gw_3">Terrorismo</a>
|
||||
<a href="glob-war.html?subtematica=armas" class="overlay-button" data-i18n="gw_4">Armas</a>
|
||||
<a href="glob-war.html?subtematica=alianzas_militares" class="overlay-button" data-i18n="gw_5">Alianzas Militares</a>
|
||||
</div>
|
||||
<div class="button-column">
|
||||
<a href="int-sec.html?subtematica=inteligencia" class="overlay-button">Inteligencia</a>
|
||||
<a href="int-sec.html?subtematica=ciberseguridad" class="overlay-button">Ciberseguridad</a>
|
||||
<a href="int-sec.html?subtematica=espionaje" class="overlay-button">Espionaje</a>
|
||||
<a href="int-sec.html?subtematica=seguridad_nacional" class="overlay-button">Seguridad Nacional</a>
|
||||
<a href="int-sec.html?subtematica=contraterrorismo" class="overlay-button">Contraterrorismo</a>
|
||||
<a href="int-sec.html?subtematica=inteligencia" class="overlay-button" data-i18n="is_1">Inteligencia</a>
|
||||
<a href="int-sec.html?subtematica=ciberseguridad" class="overlay-button" data-i18n="is_2">Ciberseguridad</a>
|
||||
<a href="int-sec.html?subtematica=espionaje" class="overlay-button" data-i18n="is_3">Espionaje</a>
|
||||
<a href="int-sec.html?subtematica=seguridad_nacional" class="overlay-button" data-i18n="is_4">Seguridad Nacional</a>
|
||||
<a href="int-sec.html?subtematica=contraterrorismo" class="overlay-button" data-i18n="is_5">Contraterrorismo</a>
|
||||
</div>
|
||||
<div class="button-column">
|
||||
<a href="climate.html?subtematica=cambio_climatico" class="overlay-button">Cambio Climático</a>
|
||||
<a href="climate.html?subtematica=desastres_naturales" class="overlay-button">Desastres Naturales</a>
|
||||
<a href="climate.html?subtematica=conservacion" class="overlay-button">Conservación</a>
|
||||
<a href="climate.html?subtematica=energia_renovable" class="overlay-button">Energía Renovable</a>
|
||||
<a href="climate.html?subtematica=contaminacion" class="overlay-button">Escasez de agua</a>
|
||||
<a href="climate.html?subtematica=cambio_climatico" class="overlay-button" data-i18n="cl_1">Cambio Climático</a>
|
||||
<a href="climate.html?subtematica=desastres_naturales" class="overlay-button" data-i18n="cl_2">Desastres Naturales</a>
|
||||
<a href="climate.html?subtematica=conservacion" class="overlay-button" data-i18n="cl_3">Conservación</a>
|
||||
<a href="climate.html?subtematica=energia_renovable" class="overlay-button" data-i18n="cl_4">Energía Renovable</a>
|
||||
<a href="climate.html?subtematica=contaminacion" class="overlay-button" data-i18n="cl_5">Escasez de agua</a>
|
||||
</div>
|
||||
<div class="button-column">
|
||||
<a href="eco-corp.html?subtematica=economia_global" class="overlay-button">Economía Global</a>
|
||||
<a href="eco-corp.html?subtematica=corporaciones_multinacionales" class="overlay-button">Corporaciones Multinacionales</a>
|
||||
<a href="eco-corp.html?subtematica=comercio_internacional" class="overlay-button">Comercio Internacional</a>
|
||||
<a href="eco-corp.html?subtematica=organismos_financieros" class="overlay-button">Organismos Financieros</a>
|
||||
<a href="eco-corp.html?subtematica=desigualdad_economica" class="overlay-button">Desigualdad Económica</a>
|
||||
<a href="eco-corp.html?subtematica=economia_global" class="overlay-button" data-i18n="ec_1">Economía Global</a>
|
||||
<a href="eco-corp.html?subtematica=corporaciones_multinacionales" class="overlay-button" data-i18n="ec_2">Corporaciones Multinacionales</a>
|
||||
<a href="eco-corp.html?subtematica=comercio_internacional" class="overlay-button" data-i18n="ec_3">Comercio Internacional</a>
|
||||
<a href="eco-corp.html?subtematica=organismos_financieros" class="overlay-button" data-i18n="ec_4">Organismos Financieros</a>
|
||||
<a href="eco-corp.html?subtematica=desigualdad_economica" class="overlay-button" data-i18n="ec_5">Desigualdad Económica</a>
|
||||
</div>
|
||||
<div class="button-column">
|
||||
<a href="popl-up.html?subtematica=sobrepoblacion" class="overlay-button">Sobrepoblación</a>
|
||||
<a href="popl-up.html?subtematica=covid" class="overlay-button">COVID</a>
|
||||
<a href="popl-up.html?subtematica=migraciones" class="overlay-button">Migraciones</a>
|
||||
<a href="popl-up.html?subtematica=urbanizacion" class="overlay-button">Urbanización</a>
|
||||
<a href="popl-up.html?subtematica=distribucion_edad" class="overlay-button">Despoblaciòn rural</a>
|
||||
<a href="popl-up.html?subtematica=sobrepoblacion" class="overlay-button" data-i18n="pu_1">Sobrepoblación</a>
|
||||
<a href="popl-up.html?subtematica=covid" class="overlay-button" data-i18n="pu_2">COVID</a>
|
||||
<a href="popl-up.html?subtematica=migraciones" class="overlay-button" data-i18n="pu_3">Migraciones</a>
|
||||
<a href="popl-up.html?subtematica=urbanizacion" class="overlay-button" data-i18n="pu_4">Urbanización</a>
|
||||
<a href="popl-up.html?subtematica=distribucion_edad" class="overlay-button" data-i18n="pu_5">Despoblación rural</a>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<button id="sidebarToggle">Toggle Sidebar</button>
|
||||
<button id="sidebarToggle" data-i18n="toggle_sidebar">Mostrar panel</button>
|
||||
|
||||
<footer>
|
||||
<p><a href="#">GitHub</a> | <a href="#">Telegram</a> | <a href="#">Email</a> | <a href="#">Web de Tor</a></p>
|
||||
<p><a href="#">GitHub</a> | <a href="#">Telegram</a> | <a href="#">Email</a> | <a href="#" data-i18n="tor_site">Web de Tor</a></p>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var I18N = {
|
||||
es: {
|
||||
nav_journalist: 'Periodistas', toggle_sidebar: 'Mostrar panel', tor_site: 'Web de Tor',
|
||||
gw_1: 'Conflictos Internacionales', gw_2: 'Guerras Civiles', gw_3: 'Terrorismo', gw_4: 'Armas', gw_5: 'Alianzas Militares',
|
||||
is_1: 'Inteligencia', is_2: 'Ciberseguridad', is_3: 'Espionaje', is_4: 'Seguridad Nacional', is_5: 'Contraterrorismo',
|
||||
cl_1: 'Cambio Climático', cl_2: 'Desastres Naturales', cl_3: 'Conservación', cl_4: 'Energía Renovable', cl_5: 'Escasez de agua',
|
||||
ec_1: 'Economía Global', ec_2: 'Corporaciones Multinacionales', ec_3: 'Comercio Internacional', ec_4: 'Organismos Financieros', ec_5: 'Desigualdad Económica',
|
||||
pu_1: 'Sobrepoblación', pu_2: 'COVID', pu_3: 'Migraciones', pu_4: 'Urbanización', pu_5: 'Despoblación rural'
|
||||
},
|
||||
en: {
|
||||
nav_journalist: 'Journalists', toggle_sidebar: 'Show panel', tor_site: 'Tor site',
|
||||
gw_1: 'International Conflicts', gw_2: 'Civil Wars', gw_3: 'Terrorism', gw_4: 'Weapons', gw_5: 'Military Alliances',
|
||||
is_1: 'Intelligence', is_2: 'Cybersecurity', is_3: 'Espionage', is_4: 'National Security', is_5: 'Counterterrorism',
|
||||
cl_1: 'Climate Change', cl_2: 'Natural Disasters', cl_3: 'Conservation', cl_4: 'Renewable Energy', cl_5: 'Water Scarcity',
|
||||
ec_1: 'Global Economy', ec_2: 'Multinational Corporations', ec_3: 'International Trade', ec_4: 'Financial Institutions', ec_5: 'Economic Inequality',
|
||||
pu_1: 'Overpopulation', pu_2: 'COVID', pu_3: 'Migration', pu_4: 'Urbanization', pu_5: 'Rural Depopulation'
|
||||
}
|
||||
};
|
||||
function apply(lang) {
|
||||
var dict = I18N[lang] || I18N.es;
|
||||
document.documentElement.lang = lang;
|
||||
document.querySelectorAll('[data-i18n]').forEach(function (el) {
|
||||
var k = el.getAttribute('data-i18n');
|
||||
if (dict[k]) el.textContent = dict[k];
|
||||
});
|
||||
document.querySelectorAll('.lang-toggle button').forEach(function (b) {
|
||||
b.setAttribute('aria-pressed', b.dataset.lang === lang ? 'true' : 'false');
|
||||
});
|
||||
try { localStorage.setItem('upl_lang', lang); } catch (e) {}
|
||||
}
|
||||
document.querySelectorAll('.lang-toggle button').forEach(function (b) {
|
||||
b.addEventListener('click', function () { apply(b.dataset.lang); });
|
||||
});
|
||||
var saved = 'es';
|
||||
try { saved = localStorage.getItem('upl_lang') || (navigator.language || 'es').slice(0, 2); } catch (e) {}
|
||||
apply(I18N[saved] ? saved : 'es');
|
||||
})();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
@font-face {
|
||||
font-family: "Retrolift";
|
||||
src: url("fonts/retrolift.woff2") format("woff2"),
|
||||
url("fonts/retrolift.woff") format("woff");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
/* La familia "Retrolift" no se distribuye con el repo: no hay @font-face.
|
||||
Las reglas que la piden caen a sans-serif. Para usarla, deja el .woff2 en
|
||||
fonts/ y anade aqui la regla con una ruta relativa. */
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
|
|
@ -170,6 +166,29 @@ nav {
|
|||
background: #ff6600;
|
||||
}
|
||||
|
||||
#sidebar select {
|
||||
padding: 10px;
|
||||
border: 2px solid #39ff14;
|
||||
background: black;
|
||||
color: #39ff14;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.2);
|
||||
transition: box-shadow 0.3s ease;
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
font-family: 'Fira Code', monospace;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
#sidebar select:hover { box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.3); }
|
||||
#sidebar select option { background: #0a0a0a; color: #39ff14; }
|
||||
#sidebar .sim-val {
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
background: rgba(57,255,20,0.15);
|
||||
border-radius: 3px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
footer {
|
||||
width: 100%;
|
||||
background-color: #000000;
|
||||
|
|
@ -221,57 +240,52 @@ footer p {
|
|||
to { opacity: 0; }
|
||||
}
|
||||
|
||||
/* ===== Añadidos para split-screen ===== */
|
||||
main.split-screen {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
margin-left: 0; /* El sidebar está fijo encima */
|
||||
}
|
||||
|
||||
#graphPanel {
|
||||
width: 100%;
|
||||
transition: width 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* ===== Panel de detalle (split-screen) ===== */
|
||||
main.split-screen { display: flex; height: 100vh; }
|
||||
#graphPanel { flex: 1; position: relative; min-width: 0; transition: flex 0.3s ease; }
|
||||
#detailPanel {
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
transition: width 0.3s ease;
|
||||
width: 0; max-width: 0; overflow: hidden;
|
||||
transition: width 0.3s ease, max-width 0.3s ease;
|
||||
background: #0a0a0a; color: #fff;
|
||||
position: relative; z-index: 4;
|
||||
box-shadow: inset 2px 0 10px rgba(0,0,0,0.6);
|
||||
}
|
||||
main.split-screen.show-detail #detailPanel { width: 50%; max-width: 50%; }
|
||||
|
||||
/* Al hacer click en un nodo */
|
||||
main.split-screen.show-detail #graphPanel {
|
||||
width: 45%;
|
||||
}
|
||||
main.split-screen.show-detail #detailPanel {
|
||||
width: 50%;
|
||||
padding: 1em;
|
||||
#closeDetail {
|
||||
position: absolute; top: 12px; left: 12px;
|
||||
background: rgba(0,0,0,0.85); border: 1px solid #39ff14;
|
||||
color: #39ff14; font-size: 1em; font-family: 'Fira Code', monospace;
|
||||
cursor: pointer; border-radius: 4px; padding: 4px 12px; z-index: 10;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
#closeDetail:hover { background: rgba(57,255,20,0.15); }
|
||||
|
||||
/* Asegurar que el grafo ocupe todo su panel */
|
||||
#graphPanel #intSecContainer {
|
||||
position: absolute;
|
||||
top: 0; left: 0;
|
||||
width: 100%; height: 100%;
|
||||
z-index: 2;
|
||||
#detailContent {
|
||||
padding: 52px 18px 20px; overflow-y: auto;
|
||||
height: 100%; box-sizing: border-box;
|
||||
display: none; flex-direction: column;
|
||||
}
|
||||
main.split-screen.show-detail #detailContent { display: flex; }
|
||||
|
||||
/* Estilos del panel de detalle */
|
||||
#detailPanel {
|
||||
background: #101020;
|
||||
color: #fff;
|
||||
box-shadow: inset 1px 0 5px rgba(0,0,0,0.5);
|
||||
}
|
||||
#detailPanel h2 {
|
||||
margin-bottom: .5em;
|
||||
color: #39ff14;
|
||||
}
|
||||
#detailPanel p {
|
||||
line-height: 1.4;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
#detailPanel .placeholder {
|
||||
color: rgba(255,255,255,0.3);
|
||||
font-style: italic;
|
||||
.detail-meta { display: flex; justify-content: space-between; font-size: 0.72em; color: #39ff14; opacity: 0.75; margin-bottom: 6px; gap: 8px; }
|
||||
.detail-author { font-weight: bold; }
|
||||
.detail-date { text-align: right; white-space: nowrap; }
|
||||
.detail-title { font-size: 0.92em; color: #39ff14; margin: 0 0 12px; word-break: break-word; line-height: 1.3; }
|
||||
|
||||
.detail-relations { border-top: 1px solid rgba(57,255,20,0.25); border-bottom: 1px solid rgba(57,255,20,0.25); padding: 8px 0; margin-bottom: 14px; font-size: 0.78em; flex-shrink: 0; }
|
||||
.relations-count { color: rgba(57,255,20,0.8); display: block; margin-bottom: 6px; }
|
||||
.relations-nav { display: flex; align-items: center; gap: 6px; }
|
||||
.relations-nav button { background: none; border: 1px solid #39ff14; color: #39ff14; cursor: pointer; padding: 3px 8px; border-radius: 3px; font-family: 'Fira Code', monospace; font-size: 0.85em; white-space: nowrap; transition: background 0.2s; }
|
||||
.relations-nav button:hover:not(:disabled) { background: rgba(57,255,20,0.12); }
|
||||
.relations-nav button:disabled { opacity: 0.3; cursor: default; }
|
||||
#relationLabel { flex: 1; text-align: center; color: rgba(255,255,255,0.55); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 0.85em; }
|
||||
|
||||
.detail-body { flex: 1; overflow-y: auto; white-space: pre-wrap; line-height: 1.55; font-size: 0.78em; color: #e0e0e0; font-family: 'Fira Code', monospace; min-height: 0; }
|
||||
.detail-img { width: 100%; max-height: 180px; object-fit: contain; margin-bottom: 12px; border: 1px solid rgba(57,255,20,0.3); border-radius: 3px; }
|
||||
#detailPanel .placeholder { color: rgba(255,255,255,0.3); font-style: italic; padding: 40px 20px; text-align: center; font-size: 0.85em; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
main.split-screen.show-detail #graphPanel { display: none; }
|
||||
main.split-screen.show-detail #detailPanel { width: 100%; max-width: 100%; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Inteligencia y Seguridad</title>
|
||||
|
|
@ -158,6 +158,7 @@
|
|||
<button id="sidebarToggle">Toggle Sidebar</button>
|
||||
|
||||
<!-- Incluir tu script al final del body -->
|
||||
<script src="semana-toggle.js"></script>
|
||||
<script type="module" src="output_int_sec.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,66 +1,291 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Journalist Login</title>
|
||||
<link rel="stylesheet" href="journalist.css">
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="robots" content="noindex">
|
||||
<meta name="referrer" content="no-referrer">
|
||||
<title>UP-LEAKS · Buzón seguro para fuentes y periodistas</title>
|
||||
<style>
|
||||
@font-face { font-family: 'Fira Code'; src: url('/fonts/fira-code-400.woff2') format('woff2'); font-weight: 400; font-display: swap; }
|
||||
@font-face { font-family: 'Fira Code'; src: url('/fonts/fira-code-500.woff2') format('woff2'); font-weight: 500; font-display: swap; }
|
||||
@font-face { font-family: 'Fira Code'; src: url('/fonts/fira-code-700.woff2') format('woff2'); font-weight: 700; font-display: swap; }
|
||||
|
||||
:root {
|
||||
--neon: #39ff14; --neon-soft: #7dff5e; --neon-deep: #0e3a08;
|
||||
--ink: #eafbe6; --muted: #9db89a; --faint: #5d7359;
|
||||
--red: #ff3b3b; --line: rgba(57,255,20,.24); --line-hard: rgba(57,255,20,.45);
|
||||
--mono: 'Fira Code', ui-monospace, Menlo, Consolas, monospace;
|
||||
--glow: 0 0 10px rgba(57,255,20,.55);
|
||||
}
|
||||
* { box-sizing: border-box; margin: 0; }
|
||||
html { scroll-behavior: smooth; }
|
||||
body {
|
||||
color: var(--ink); font-family: var(--mono); font-size: 15px; line-height: 1.65;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(0,0,0,.90) 0%, rgba(2,10,2,.93) 40%, rgba(0,0,0,.96) 100%),
|
||||
url('/images/flujos6.jpg') center top / cover fixed no-repeat;
|
||||
}
|
||||
a { color: var(--neon); text-decoration: none; }
|
||||
.wrap { max-width: 900px; margin: 0 auto; padding: 0 22px; }
|
||||
::selection { background: var(--neon); color: #000; }
|
||||
|
||||
/* ---------- buttons (acciones = botones reales) ---------- */
|
||||
.btn {
|
||||
display: inline-flex; align-items: center; gap: 9px; font-family: var(--mono); font-weight: 700;
|
||||
font-size: 13.5px; letter-spacing: .05em; padding: 13px 26px; cursor: pointer;
|
||||
border: 1.5px solid var(--neon); background: rgba(0,0,0,.35); color: var(--neon);
|
||||
text-transform: uppercase; transition: background .18s, color .18s, box-shadow .18s, transform .1s;
|
||||
}
|
||||
.btn:hover { background: var(--neon); color: #000; box-shadow: 0 0 22px -3px rgba(57,255,20,.75); }
|
||||
.btn:active { transform: translateY(1px); }
|
||||
.btn-solid { background: var(--neon); color: #000; border-color: var(--neon); }
|
||||
.btn-solid:hover { background: var(--neon-soft); border-color: var(--neon-soft); box-shadow: 0 0 26px -2px rgba(57,255,20,.85); }
|
||||
.btn-ghost { border-color: var(--line-hard); color: var(--ink); background: rgba(0,0,0,.3); }
|
||||
.btn-ghost:hover { border-color: var(--neon); color: var(--neon); background: rgba(0,0,0,.3); box-shadow: none; }
|
||||
.btn-lg { font-size: 15px; padding: 16px 34px; }
|
||||
|
||||
/* ---------- top nav ---------- */
|
||||
header.nav {
|
||||
position: sticky; top: 0; z-index: 30; border-bottom: 1px solid var(--line);
|
||||
background: rgba(0,0,0,.78); backdrop-filter: blur(10px);
|
||||
}
|
||||
.nav .wrap { display: flex; align-items: center; justify-content: space-between; height: 68px; gap: 12px; }
|
||||
.brand { font-weight: 700; letter-spacing: .2em; font-size: 20px; }
|
||||
.brand b { color: var(--neon); text-shadow: var(--glow); }
|
||||
.nav-actions { display: flex; gap: 10px; }
|
||||
.nav-actions .btn { padding: 9px 16px; font-size: 12px; }
|
||||
|
||||
/* ---------- hero ---------- */
|
||||
.hero { position: relative; overflow: hidden; border-bottom: 1px solid var(--line); }
|
||||
.hero::before {
|
||||
content: ""; position: absolute; inset: 0; z-index: 0;
|
||||
background: linear-gradient(115deg, rgba(0,0,0,.35) 0%, rgba(0,0,0,.72) 55%, rgba(2,12,2,.92) 100%),
|
||||
url('/images/flujos6.jpg') center / cover no-repeat;
|
||||
}
|
||||
.hero::after {
|
||||
content: ""; position: absolute; inset: 0; z-index: 1; pointer-events: none;
|
||||
background: radial-gradient(120% 90% at 15% 10%, rgba(57,255,20,.16), transparent 55%);
|
||||
}
|
||||
.hero .wrap { position: relative; z-index: 2; padding: 84px 22px 74px; }
|
||||
.chips { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 26px; }
|
||||
.chip {
|
||||
font-size: 11px; letter-spacing: .12em; text-transform: uppercase; color: var(--neon);
|
||||
border: 1px solid var(--line-hard); padding: 5px 12px; background: rgba(0,0,0,.4);
|
||||
}
|
||||
.chip::before { content: "● "; }
|
||||
.hero h1 {
|
||||
font-size: clamp(32px, 6vw, 58px); line-height: 1.06; font-weight: 700; letter-spacing: -.02em;
|
||||
max-width: 16ch; text-wrap: balance; text-shadow: 0 2px 30px rgba(0,0,0,.6);
|
||||
}
|
||||
.hero h1 em { color: var(--neon); font-style: normal; text-shadow: var(--glow); }
|
||||
.hero .lead { margin: 22px 0 34px; font-size: 17px; color: var(--ink); max-width: 60ch; opacity: .92; }
|
||||
.hero .lead b { color: var(--neon); }
|
||||
.hero .cta { display: flex; gap: 14px; flex-wrap: wrap; }
|
||||
|
||||
/* ---------- sections ---------- */
|
||||
section.block { padding: 60px 0; }
|
||||
.eyebrow { font-size: 12px; letter-spacing: .2em; text-transform: uppercase; color: var(--neon); margin-bottom: 10px; }
|
||||
.block h2 { font-size: clamp(22px, 3.4vw, 30px); font-weight: 700; letter-spacing: -.01em; margin-bottom: 8px; text-wrap: balance; }
|
||||
.block .sub { color: var(--muted); font-size: 15px; max-width: 62ch; }
|
||||
|
||||
/* security cards */
|
||||
.cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-top: 34px; }
|
||||
@media (max-width: 720px) { .cards { grid-template-columns: 1fr; } }
|
||||
.card {
|
||||
position: relative; border: 1px solid var(--line); background: rgba(4,10,4,.66);
|
||||
backdrop-filter: blur(6px); padding: 26px 22px; overflow: hidden;
|
||||
transition: border-color .2s, transform .2s, box-shadow .2s;
|
||||
}
|
||||
.card::before {
|
||||
content: ""; position: absolute; left: 0; top: 0; height: 3px; width: 100%;
|
||||
background: linear-gradient(90deg, var(--neon), transparent 70%);
|
||||
}
|
||||
.card:hover { border-color: var(--line-hard); transform: translateY(-4px); box-shadow: 0 18px 40px -22px rgba(57,255,20,.55); }
|
||||
.card .num { font-size: 34px; font-weight: 700; color: var(--neon); line-height: 1; text-shadow: var(--glow); }
|
||||
.card h3 { margin: 14px 0 8px; font-size: 18px; font-weight: 700; }
|
||||
.card p { color: var(--muted); font-size: 14px; line-height: 1.6; }
|
||||
.card code { color: var(--neon); background: rgba(0,0,0,.5); border: 1px solid var(--line); padding: 1px 6px; }
|
||||
|
||||
/* form */
|
||||
.form-sec { position: relative; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
|
||||
.form-sec::before {
|
||||
content: ""; position: absolute; inset: 0; z-index: 0; opacity: .5;
|
||||
background: linear-gradient(180deg, rgba(0,0,0,.86), rgba(2,10,2,.92)), url('/images/flujos6.jpg') center / cover fixed;
|
||||
}
|
||||
.form-sec .wrap { position: relative; z-index: 1; padding: 62px 22px; }
|
||||
.panel {
|
||||
border: 1px solid var(--line-hard); background: rgba(3,8,3,.82); backdrop-filter: blur(10px);
|
||||
padding: 34px; box-shadow: 0 30px 70px -34px rgba(0,0,0,.9), inset 0 0 60px -40px rgba(57,255,20,.4);
|
||||
}
|
||||
.field { margin-bottom: 26px; }
|
||||
.field:last-of-type { margin-bottom: 0; }
|
||||
label.lbl { display: block; font-size: 14.5px; font-weight: 700; margin-bottom: 6px; color: var(--ink); }
|
||||
.num-tag { color: var(--neon); margin-right: 8px; }
|
||||
.hint { font-size: 13px; color: var(--muted); margin-bottom: 12px; }
|
||||
select, textarea, input[type="text"] {
|
||||
width: 100%; background: rgba(0,0,0,.55); border: 1.5px solid var(--line); color: var(--ink);
|
||||
padding: 14px; font-family: var(--mono); font-size: 15px; transition: border-color .15s, box-shadow .15s;
|
||||
}
|
||||
textarea { min-height: 160px; resize: vertical; line-height: 1.6; }
|
||||
select:focus, textarea:focus, input[type="text"]:focus, input[type="file"]:focus {
|
||||
outline: none; border-color: var(--neon); box-shadow: 0 0 0 1px var(--neon), 0 0 18px -3px rgba(57,255,20,.6);
|
||||
}
|
||||
select {
|
||||
appearance: none; -webkit-appearance: none; cursor: pointer;
|
||||
background-image: linear-gradient(45deg, transparent 50%, var(--neon) 50%), linear-gradient(135deg, var(--neon) 50%, transparent 50%);
|
||||
background-position: calc(100% - 20px) center, calc(100% - 15px) center; background-size: 6px 6px, 6px 6px; background-repeat: no-repeat;
|
||||
}
|
||||
/* file input: el botón nativo estilizado como botón real */
|
||||
input[type="file"] {
|
||||
width: 100%; background: rgba(0,0,0,.55); border: 1.5px dashed var(--line); color: var(--muted);
|
||||
padding: 16px; font-family: var(--mono); font-size: 13.5px; cursor: pointer;
|
||||
}
|
||||
input[type="file"]::file-selector-button {
|
||||
font-family: var(--mono); font-weight: 700; font-size: 12.5px; text-transform: uppercase; letter-spacing: .05em;
|
||||
background: var(--neon); color: #000; border: none; padding: 10px 18px; margin-right: 16px; cursor: pointer;
|
||||
transition: background .15s, box-shadow .15s;
|
||||
}
|
||||
input[type="file"]::file-selector-button:hover { background: var(--neon-soft); box-shadow: 0 0 16px -2px rgba(57,255,20,.7); }
|
||||
|
||||
.hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }
|
||||
.submit-row { display: flex; align-items: center; gap: 20px; flex-wrap: wrap; margin-top: 30px; padding-top: 24px; border-top: 1px solid var(--line); }
|
||||
.submit-note { font-size: 12px; color: var(--faint); letter-spacing: .04em; }
|
||||
|
||||
.callout {
|
||||
display: flex; gap: 15px; border: 1px solid var(--line); border-left: 3px solid var(--neon);
|
||||
background: rgba(4,10,4,.6); backdrop-filter: blur(6px); padding: 22px 24px; font-size: 14px; color: var(--muted); line-height: 1.65;
|
||||
}
|
||||
.callout b { color: var(--ink); }
|
||||
.callout .ic { color: var(--neon); font-size: 20px; }
|
||||
|
||||
footer.foot { border-top: 1px solid var(--line); padding: 34px 0 60px; background: rgba(0,0,0,.4); }
|
||||
footer.foot p { font-size: 12px; color: var(--faint); margin: 5px 0; letter-spacing: .04em; }
|
||||
footer.foot .q { color: var(--muted); }
|
||||
|
||||
@media (prefers-reduced-motion: reduce) { * { transition: none !important; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<img class="logo" src="/images/flujos_logo5.png" alt="Flujos Logo">
|
||||
<header class="nav">
|
||||
<div class="wrap">
|
||||
<span class="brand"><b>UP</b>-LEAKS</span>
|
||||
<div class="nav-actions">
|
||||
<a class="btn btn-ghost" href="/">Grafo</a>
|
||||
<a class="btn btn-ghost" href="/stats.html">Datos</a>
|
||||
<a class="btn" href="#buzon">Enviar</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="canvas-container">
|
||||
<img class="image-left" src="/images/journalist_fondo.jpg" alt="Fondo izquierdo">
|
||||
<div class="login-container">
|
||||
<h1>Journalist Login</h1>
|
||||
<form onsubmit="login(event)">
|
||||
<label for="username">Username:</label>
|
||||
<input type="text" id="username" name="username" required>
|
||||
<section class="hero">
|
||||
<div class="wrap">
|
||||
<div class="chips">
|
||||
<span class="chip">Canal seguro</span>
|
||||
<span class="chip">Tor recomendado</span>
|
||||
<span class="chip">Sin registro de IP</span>
|
||||
</div>
|
||||
<h1>Filtra lo que el poder quiere <em>en silencio</em></h1>
|
||||
<p class="lead">UP-LEAKS conecta documentos filtrados con la actualidad para exponer los flujos de poder. Si tienes material de interés público —contratos, cables, correos, bases de datos, testimonios— este es tu canal. <b>No registramos tu IP ni te pedimos identificarte.</b></p>
|
||||
<div class="cta">
|
||||
<a class="btn btn-solid btn-lg" href="#buzon">Enviar material →</a>
|
||||
<a class="btn btn-lg" href="#seguridad">Cómo protegerte</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<label for="password">Password:</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
|
||||
<label for="publicKey">Public Key:</label>
|
||||
<input type="text" id="publicKey" name="publicKey" required>
|
||||
|
||||
<input type="submit" value="Login">
|
||||
</form>
|
||||
<section class="block" id="seguridad">
|
||||
<div class="wrap">
|
||||
<div class="eyebrow">Antes de enviar</div>
|
||||
<h2>Tres pasos para protegerte</h2>
|
||||
<p class="sub">Tu seguridad va primero. Dedica un minuto a esto antes de transmitir nada.</p>
|
||||
<div class="cards">
|
||||
<div class="card">
|
||||
<div class="num">01</div>
|
||||
<h3>Usa Tor</h3>
|
||||
<p>Entra con el <a href="https://www.torproject.org/es/" rel="noopener noreferrer">Navegador Tor</a> para que tu conexión no quede ligada a ti. Evita hacerlo desde la red de tu trabajo.</p>
|
||||
</div>
|
||||
<img class="image-right" src="/images/journalist_fondo2.jpg" alt="Fondo derecho">
|
||||
<div class="card">
|
||||
<div class="num">02</div>
|
||||
<h3>Limpia los archivos</h3>
|
||||
<p>Los documentos guardan metadatos (autor, equipo, GPS). Bórralos con <code>mat2</code> antes de subirlos.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="num">03</div>
|
||||
<h3>No des detalles de más</h3>
|
||||
<p>No incluyas datos que solo tú podrías conocer. Cuanta menos gente tuvo acceso al material, más discreto conviene ser.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
function login(event) {
|
||||
event.preventDefault(); // Evitar que el formulario se envíe por defecto
|
||||
<section class="form-sec" id="buzon">
|
||||
<div class="wrap">
|
||||
<div class="eyebrow">Buzón de envío</div>
|
||||
<h2 style="margin-bottom:8px">Transmite tu material</h2>
|
||||
<p class="sub" style="margin-bottom:30px">Cifrado en tránsito. Sin cuentas, sin cookies, sin rastro de tu IP.</p>
|
||||
|
||||
const username = document.getElementById('username').value;
|
||||
const password = document.getElementById('password').value;
|
||||
const publicKey = document.getElementById('publicKey').value;
|
||||
<form class="panel" method="post" action="/api/submit" enctype="multipart/form-data" autocomplete="off">
|
||||
<input type="hidden" name="render" value="html">
|
||||
|
||||
// Enviar los datos del formulario mediante una petición AJAX a nuestro endpoint de login
|
||||
fetch('/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ username, password, publicKey })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// Si el servidor devuelve un redireccionamiento, redirigimos al usuario
|
||||
if (data.redirect) {
|
||||
window.location.href = data.redirect;
|
||||
} else {
|
||||
// Si no hay redireccionamiento, se podría mostrar un mensaje de error
|
||||
console.log('Credenciales inválidas');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error en la petición AJAX:', error);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<div class="field">
|
||||
<label class="lbl" for="tema"><span class="num-tag">01</span>Temática</label>
|
||||
<p class="hint">Encamina tu envío al área correcta.</p>
|
||||
<select id="tema" name="tema">
|
||||
<option value="guerra global">Guerra global / conflictos</option>
|
||||
<option value="inteligencia y seguridad">Inteligencia y seguridad</option>
|
||||
<option value="cambio climático">Cambio climático / medioambiente</option>
|
||||
<option value="economía y corporaciones">Economía y corporaciones</option>
|
||||
<option value="demografía y sociedad">Demografía y sociedad</option>
|
||||
<option value="otros" selected>Otros / no estoy seguro</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="lbl" for="mensaje"><span class="num-tag">02</span>Descripción del material</label>
|
||||
<p class="hint">¿Qué es, de dónde sale, por qué importa? No hace falta que sea largo.</p>
|
||||
<textarea id="mensaje" name="mensaje" maxlength="20000" placeholder="Cuéntanos qué nos envías y qué crees que revela…"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="lbl" for="adjuntos"><span class="num-tag">03</span>Adjuntos <span style="color:var(--faint);font-weight:400">— opcional · hasta 8 archivos · 50 MB c/u</span></label>
|
||||
<p class="hint">Documentos, hojas de cálculo, PDF, imágenes o archivos comprimidos.</p>
|
||||
<input type="file" id="adjuntos" name="adjuntos" multiple>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="lbl" for="contacto"><span class="num-tag">04</span>Contacto <span style="color:var(--faint);font-weight:400">— opcional</span></label>
|
||||
<p class="hint">Solo si quieres que podamos responderte. Signal, ProtonMail, clave PGP o una cuenta desechable. Puedes dejarlo en blanco.</p>
|
||||
<input type="text" id="contacto" name="contacto" maxlength="400" placeholder="p. ej. Signal: +…, o correo desechable">
|
||||
</div>
|
||||
|
||||
<div class="hp" aria-hidden="true">
|
||||
<label>No rellenar<input type="text" name="website" tabindex="-1" autocomplete="off"></label>
|
||||
</div>
|
||||
|
||||
<div class="submit-row">
|
||||
<button type="submit" class="btn btn-solid btn-lg">Enviar de forma segura →</button>
|
||||
<span class="submit-note">HTTPS · sin IP · sin cookies · sin JavaScript</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="block">
|
||||
<div class="wrap">
|
||||
<div class="callout">
|
||||
<span class="ic" aria-hidden="true">▲</span>
|
||||
<div><b>Aviso honesto.</b> Este buzón cifra el envío en tránsito y no registra tu IP, pero <b>no sustituye a una plataforma tipo SecureDrop</b>. Para filtraciones de altísimo riesgo, combínalo con Tor, limpieza de metadatos y, si puedes, cifrado PGP del propio archivo.</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer class="foot">
|
||||
<div class="wrap">
|
||||
<p>UP-LEAKS · flujos de información · software libre</p>
|
||||
<p class="q">“Los gigantes vistos en perspectiva parecen marionetas.”</p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
@font-face {
|
||||
font-family: "Retrolift";
|
||||
src: url("fonts/retrolift.woff2") format("woff2"),
|
||||
url("fonts/retrolift.woff") format("woff");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
/* La familia "Retrolift" no se distribuye con el repo: no hay @font-face.
|
||||
Las reglas que la piden caen a sans-serif. Para usarla, deja el .woff2 en
|
||||
fonts/ y anade aqui la regla con una ruta relativa. */
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
|
|
|||
|
|
@ -27,12 +27,23 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
.onNodeClick(node => showNodeDetail(node))
|
||||
.onNodeHover(node => { elem.style.cursor = node ? 'pointer' : null; })
|
||||
.nodeThreeObject(node => {
|
||||
if (node.type !== 'imagen' || !node.image_url) return null;
|
||||
const texture = getTexture(node.image_url);
|
||||
// Imagen local (nodos-imagen) directa; og:image remota de noticias via
|
||||
// proxy same-origin /api/img para que WebGL pueda texturizarla (CORS).
|
||||
let src = null, esNoticia = false;
|
||||
if (node.type === 'imagen' && node.image_url) {
|
||||
src = node.image_url;
|
||||
} else if (node.imagen_url) {
|
||||
src = '/api/img?u=' + encodeURIComponent(node.imagen_url);
|
||||
esNoticia = true;
|
||||
}
|
||||
if (!src) return null;
|
||||
const texture = getTexture(src);
|
||||
texture.colorSpace = THREE.SRGBColorSpace;
|
||||
const material = new THREE.SpriteMaterial({ map: texture, depthWrite: false });
|
||||
const sprite = new THREE.Sprite(material);
|
||||
sprite.scale.set(160, 104, 1);
|
||||
// miniaturas de noticia mas pequenas que los nodos-imagen dedicados
|
||||
if (esNoticia) sprite.scale.set(84, 55, 1);
|
||||
else sprite.scale.set(160, 104, 1);
|
||||
return sprite;
|
||||
})
|
||||
.nodeThreeObjectExtend(false)
|
||||
|
|
@ -98,10 +109,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
|
||||
const body = detailContent.querySelector('.detail-body');
|
||||
body.innerHTML = '';
|
||||
if (node.type === 'imagen' && node.image_url) {
|
||||
const _imgSrc = (node.type === 'imagen' && node.image_url) ? node.image_url
|
||||
: (node.imagen_url || null);
|
||||
if (_imgSrc) {
|
||||
const img = document.createElement('img');
|
||||
img.src = node.image_url;
|
||||
img.src = _imgSrc;
|
||||
img.className = 'detail-img';
|
||||
img.loading = 'lazy';
|
||||
img.onerror = () => img.remove();
|
||||
body.appendChild(img);
|
||||
}
|
||||
const text = document.createElement('span');
|
||||
|
|
|
|||
|
|
@ -27,12 +27,23 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
.onNodeClick(node => showNodeDetail(node))
|
||||
.onNodeHover(node => { elem.style.cursor = node ? 'pointer' : null; })
|
||||
.nodeThreeObject(node => {
|
||||
if (node.type !== 'imagen' || !node.image_url) return null;
|
||||
const texture = getTexture(node.image_url);
|
||||
// Imagen local (nodos-imagen) directa; og:image remota de noticias via
|
||||
// proxy same-origin /api/img para que WebGL pueda texturizarla (CORS).
|
||||
let src = null, esNoticia = false;
|
||||
if (node.type === 'imagen' && node.image_url) {
|
||||
src = node.image_url;
|
||||
} else if (node.imagen_url) {
|
||||
src = '/api/img?u=' + encodeURIComponent(node.imagen_url);
|
||||
esNoticia = true;
|
||||
}
|
||||
if (!src) return null;
|
||||
const texture = getTexture(src);
|
||||
texture.colorSpace = THREE.SRGBColorSpace;
|
||||
const material = new THREE.SpriteMaterial({ map: texture, depthWrite: false });
|
||||
const sprite = new THREE.Sprite(material);
|
||||
sprite.scale.set(160, 104, 1);
|
||||
// miniaturas de noticia mas pequenas que los nodos-imagen dedicados
|
||||
if (esNoticia) sprite.scale.set(84, 55, 1);
|
||||
else sprite.scale.set(160, 104, 1);
|
||||
return sprite;
|
||||
})
|
||||
.nodeThreeObjectExtend(false)
|
||||
|
|
@ -98,10 +109,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
|
||||
const body = detailContent.querySelector('.detail-body');
|
||||
body.innerHTML = '';
|
||||
if (node.type === 'imagen' && node.image_url) {
|
||||
const _imgSrc = (node.type === 'imagen' && node.image_url) ? node.image_url
|
||||
: (node.imagen_url || null);
|
||||
if (_imgSrc) {
|
||||
const img = document.createElement('img');
|
||||
img.src = node.image_url;
|
||||
img.src = _imgSrc;
|
||||
img.className = 'detail-img';
|
||||
img.loading = 'lazy';
|
||||
img.onerror = () => img.remove();
|
||||
body.appendChild(img);
|
||||
}
|
||||
const text = document.createElement('span');
|
||||
|
|
|
|||
|
|
@ -27,12 +27,23 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
.onNodeClick(node => showNodeDetail(node))
|
||||
.onNodeHover(node => { elem.style.cursor = node ? 'pointer' : null; })
|
||||
.nodeThreeObject(node => {
|
||||
if (node.type !== 'imagen' || !node.image_url) return null;
|
||||
const texture = getTexture(node.image_url);
|
||||
// Imagen local (nodos-imagen) directa; og:image remota de noticias via
|
||||
// proxy same-origin /api/img para que WebGL pueda texturizarla (CORS).
|
||||
let src = null, esNoticia = false;
|
||||
if (node.type === 'imagen' && node.image_url) {
|
||||
src = node.image_url;
|
||||
} else if (node.imagen_url) {
|
||||
src = '/api/img?u=' + encodeURIComponent(node.imagen_url);
|
||||
esNoticia = true;
|
||||
}
|
||||
if (!src) return null;
|
||||
const texture = getTexture(src);
|
||||
texture.colorSpace = THREE.SRGBColorSpace;
|
||||
const material = new THREE.SpriteMaterial({ map: texture, depthWrite: false });
|
||||
const sprite = new THREE.Sprite(material);
|
||||
sprite.scale.set(160, 104, 1);
|
||||
// miniaturas de noticia mas pequenas que los nodos-imagen dedicados
|
||||
if (esNoticia) sprite.scale.set(84, 55, 1);
|
||||
else sprite.scale.set(160, 104, 1);
|
||||
return sprite;
|
||||
})
|
||||
.nodeThreeObjectExtend(false)
|
||||
|
|
@ -99,10 +110,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
|
||||
const body = detailContent.querySelector('.detail-body');
|
||||
body.innerHTML = '';
|
||||
if (node.type === 'imagen' && node.image_url) {
|
||||
const _imgSrc = (node.type === 'imagen' && node.image_url) ? node.image_url
|
||||
: (node.imagen_url || null);
|
||||
if (_imgSrc) {
|
||||
const img = document.createElement('img');
|
||||
img.src = node.image_url;
|
||||
img.src = _imgSrc;
|
||||
img.className = 'detail-img';
|
||||
img.loading = 'lazy';
|
||||
img.onerror = () => img.remove();
|
||||
body.appendChild(img);
|
||||
}
|
||||
const text = document.createElement('span');
|
||||
|
|
|
|||
|
|
@ -27,12 +27,23 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
.onNodeClick(node => showNodeDetail(node))
|
||||
.onNodeHover(node => { elem.style.cursor = node ? 'pointer' : null; })
|
||||
.nodeThreeObject(node => {
|
||||
if (node.type !== 'imagen' || !node.image_url) return null;
|
||||
const texture = getTexture(node.image_url);
|
||||
// Imagen local (nodos-imagen) directa; og:image remota de noticias via
|
||||
// proxy same-origin /api/img para que WebGL pueda texturizarla (CORS).
|
||||
let src = null, esNoticia = false;
|
||||
if (node.type === 'imagen' && node.image_url) {
|
||||
src = node.image_url;
|
||||
} else if (node.imagen_url) {
|
||||
src = '/api/img?u=' + encodeURIComponent(node.imagen_url);
|
||||
esNoticia = true;
|
||||
}
|
||||
if (!src) return null;
|
||||
const texture = getTexture(src);
|
||||
texture.colorSpace = THREE.SRGBColorSpace;
|
||||
const material = new THREE.SpriteMaterial({ map: texture, depthWrite: false });
|
||||
const sprite = new THREE.Sprite(material);
|
||||
sprite.scale.set(160, 104, 1);
|
||||
// miniaturas de noticia mas pequenas que los nodos-imagen dedicados
|
||||
if (esNoticia) sprite.scale.set(84, 55, 1);
|
||||
else sprite.scale.set(160, 104, 1);
|
||||
return sprite;
|
||||
})
|
||||
.nodeThreeObjectExtend(false)
|
||||
|
|
@ -98,10 +109,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
|
||||
const body = detailContent.querySelector('.detail-body');
|
||||
body.innerHTML = '';
|
||||
if (node.type === 'imagen' && node.image_url) {
|
||||
const _imgSrc = (node.type === 'imagen' && node.image_url) ? node.image_url
|
||||
: (node.imagen_url || null);
|
||||
if (_imgSrc) {
|
||||
const img = document.createElement('img');
|
||||
img.src = node.image_url;
|
||||
img.src = _imgSrc;
|
||||
img.className = 'detail-img';
|
||||
img.loading = 'lazy';
|
||||
img.onerror = () => img.remove();
|
||||
body.appendChild(img);
|
||||
}
|
||||
const text = document.createElement('span');
|
||||
|
|
|
|||
|
|
@ -27,12 +27,23 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
.onNodeClick(node => showNodeDetail(node))
|
||||
.onNodeHover(node => { elem.style.cursor = node ? 'pointer' : null; })
|
||||
.nodeThreeObject(node => {
|
||||
if (node.type !== 'imagen' || !node.image_url) return null;
|
||||
const texture = getTexture(node.image_url);
|
||||
// Imagen local (nodos-imagen) directa; og:image remota de noticias via
|
||||
// proxy same-origin /api/img para que WebGL pueda texturizarla (CORS).
|
||||
let src = null, esNoticia = false;
|
||||
if (node.type === 'imagen' && node.image_url) {
|
||||
src = node.image_url;
|
||||
} else if (node.imagen_url) {
|
||||
src = '/api/img?u=' + encodeURIComponent(node.imagen_url);
|
||||
esNoticia = true;
|
||||
}
|
||||
if (!src) return null;
|
||||
const texture = getTexture(src);
|
||||
texture.colorSpace = THREE.SRGBColorSpace;
|
||||
const material = new THREE.SpriteMaterial({ map: texture, depthWrite: false });
|
||||
const sprite = new THREE.Sprite(material);
|
||||
sprite.scale.set(160, 104, 1);
|
||||
// miniaturas de noticia mas pequenas que los nodos-imagen dedicados
|
||||
if (esNoticia) sprite.scale.set(84, 55, 1);
|
||||
else sprite.scale.set(160, 104, 1);
|
||||
return sprite;
|
||||
})
|
||||
.nodeThreeObjectExtend(false)
|
||||
|
|
@ -98,10 +109,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
|
||||
const body = detailContent.querySelector('.detail-body');
|
||||
body.innerHTML = '';
|
||||
if (node.type === 'imagen' && node.image_url) {
|
||||
const _imgSrc = (node.type === 'imagen' && node.image_url) ? node.image_url
|
||||
: (node.imagen_url || null);
|
||||
if (_imgSrc) {
|
||||
const img = document.createElement('img');
|
||||
img.src = node.image_url;
|
||||
img.src = _imgSrc;
|
||||
img.className = 'detail-img';
|
||||
img.loading = 'lazy';
|
||||
img.onerror = () => img.remove();
|
||||
body.appendChild(img);
|
||||
}
|
||||
const text = document.createElement('span');
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
@font-face {
|
||||
font-family: "Retrolift";
|
||||
src: url("fonts/retrolift.woff2") format("woff2"),
|
||||
url("fonts/retrolift.woff") format("woff");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
/* La familia "Retrolift" no se distribuye con el repo: no hay @font-face.
|
||||
Las reglas que la piden caen a sans-serif. Para usarla, deja el .woff2 en
|
||||
fonts/ y anade aqui la regla con una ruta relativa. */
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
|
|
@ -180,6 +176,29 @@ nav {
|
|||
background: #ff6600;
|
||||
}
|
||||
|
||||
#sidebar select {
|
||||
padding: 10px;
|
||||
border: 2px solid #39ff14;
|
||||
background: black;
|
||||
color: #39ff14;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.2);
|
||||
transition: box-shadow 0.3s ease;
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
font-family: 'Fira Code', monospace;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
#sidebar select:hover { box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.3); }
|
||||
#sidebar select option { background: #0a0a0a; color: #39ff14; }
|
||||
#sidebar .sim-val {
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
background: rgba(57,255,20,0.15);
|
||||
border-radius: 3px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
footer {
|
||||
width: 100%;
|
||||
background-color: #000000;
|
||||
|
|
@ -217,3 +236,54 @@ footer p {
|
|||
position: absolute;
|
||||
left: 250px;
|
||||
}
|
||||
|
||||
|
||||
/* ===== Panel de detalle (split-screen) ===== */
|
||||
main.split-screen { display: flex; height: 100vh; }
|
||||
#graphPanel { flex: 1; position: relative; min-width: 0; transition: flex 0.3s ease; }
|
||||
#detailPanel {
|
||||
width: 0; max-width: 0; overflow: hidden;
|
||||
transition: width 0.3s ease, max-width 0.3s ease;
|
||||
background: #0a0a0a; color: #fff;
|
||||
position: relative; z-index: 4;
|
||||
box-shadow: inset 2px 0 10px rgba(0,0,0,0.6);
|
||||
}
|
||||
main.split-screen.show-detail #detailPanel { width: 50%; max-width: 50%; }
|
||||
|
||||
#closeDetail {
|
||||
position: absolute; top: 12px; left: 12px;
|
||||
background: rgba(0,0,0,0.85); border: 1px solid #39ff14;
|
||||
color: #39ff14; font-size: 1em; font-family: 'Fira Code', monospace;
|
||||
cursor: pointer; border-radius: 4px; padding: 4px 12px; z-index: 10;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
#closeDetail:hover { background: rgba(57,255,20,0.15); }
|
||||
|
||||
#detailContent {
|
||||
padding: 52px 18px 20px; overflow-y: auto;
|
||||
height: 100%; box-sizing: border-box;
|
||||
display: none; flex-direction: column;
|
||||
}
|
||||
main.split-screen.show-detail #detailContent { display: flex; }
|
||||
|
||||
.detail-meta { display: flex; justify-content: space-between; font-size: 0.72em; color: #39ff14; opacity: 0.75; margin-bottom: 6px; gap: 8px; }
|
||||
.detail-author { font-weight: bold; }
|
||||
.detail-date { text-align: right; white-space: nowrap; }
|
||||
.detail-title { font-size: 0.92em; color: #39ff14; margin: 0 0 12px; word-break: break-word; line-height: 1.3; }
|
||||
|
||||
.detail-relations { border-top: 1px solid rgba(57,255,20,0.25); border-bottom: 1px solid rgba(57,255,20,0.25); padding: 8px 0; margin-bottom: 14px; font-size: 0.78em; flex-shrink: 0; }
|
||||
.relations-count { color: rgba(57,255,20,0.8); display: block; margin-bottom: 6px; }
|
||||
.relations-nav { display: flex; align-items: center; gap: 6px; }
|
||||
.relations-nav button { background: none; border: 1px solid #39ff14; color: #39ff14; cursor: pointer; padding: 3px 8px; border-radius: 3px; font-family: 'Fira Code', monospace; font-size: 0.85em; white-space: nowrap; transition: background 0.2s; }
|
||||
.relations-nav button:hover:not(:disabled) { background: rgba(57,255,20,0.12); }
|
||||
.relations-nav button:disabled { opacity: 0.3; cursor: default; }
|
||||
#relationLabel { flex: 1; text-align: center; color: rgba(255,255,255,0.55); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 0.85em; }
|
||||
|
||||
.detail-body { flex: 1; overflow-y: auto; white-space: pre-wrap; line-height: 1.55; font-size: 0.78em; color: #e0e0e0; font-family: 'Fira Code', monospace; min-height: 0; }
|
||||
.detail-img { width: 100%; max-height: 180px; object-fit: contain; margin-bottom: 12px; border: 1px solid rgba(57,255,20,0.3); border-radius: 3px; }
|
||||
#detailPanel .placeholder { color: rgba(255,255,255,0.3); font-style: italic; padding: 40px 20px; text-align: center; font-size: 0.85em; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
main.split-screen.show-detail #graphPanel { display: none; }
|
||||
main.split-screen.show-detail #detailPanel { width: 100%; max-width: 100%; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -160,6 +160,7 @@
|
|||
<button id="sidebarToggle">Toggle Sidebar</button>
|
||||
|
||||
<!-- Script principal -->
|
||||
<script src="semana-toggle.js"></script>
|
||||
<script type="module" src="output_popl_up_pruebas.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
56
FLUJOS/VISUALIZACION/public/semana-toggle.js
Normal file
56
FLUJOS/VISUALIZACION/public/semana-toggle.js
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
/* Filtro temporal compartido para las vistas de grafo (climate, glob-war, ...).
|
||||
- "Semanal" (por defecto): fecha_inicio = hace 7 dias, fecha_fin = hoy.
|
||||
- "Todas": limpia el rango; el backend devuelve una muestra aleatoria de todo
|
||||
el archivo, incluidas las noticias sin fecha (`fecha: null`).
|
||||
El backend aplica el rango SOLO a `noticias` (wikipedia/torrents no tienen
|
||||
fecha), asi que las conexiones noticia<->leak del grafo se mantienen.
|
||||
Se ejecuta al final del body (form y top-nav ya existen) y ANTES del modulo
|
||||
del grafo, para que el primer fetchAndRender ya salga en modo semanal. */
|
||||
(function () {
|
||||
var fi = document.getElementById('fecha_inicio');
|
||||
var ff = document.getElementById('fecha_fin');
|
||||
// Fecha LOCAL, no UTC: toISOString() convierte a UTC y en CEST (+2) entre las
|
||||
// 00:00 y las 02:00 devolvia la fecha de ayer, desplazando toda la ventana.
|
||||
function iso(d) {
|
||||
return d.getFullYear() + '-' +
|
||||
String(d.getMonth() + 1).padStart(2, '0') + '-' +
|
||||
String(d.getDate()).padStart(2, '0');
|
||||
}
|
||||
function setRango(dias) {
|
||||
if (!fi || !ff) return;
|
||||
if (dias === null) { fi.value = ''; ff.value = ''; return; }
|
||||
var hoy = new Date(), desde = new Date();
|
||||
desde.setDate(hoy.getDate() - dias);
|
||||
fi.value = iso(desde); ff.value = iso(hoy);
|
||||
}
|
||||
function marcar(a) {
|
||||
var s = document.getElementById('btnSemanal'), t = document.getElementById('btnTodas');
|
||||
if (s) s.classList.toggle('active', a === 'semanal');
|
||||
if (t) t.classList.toggle('active', a === 'todas');
|
||||
}
|
||||
function submit() {
|
||||
var f = document.getElementById('paramForm');
|
||||
if (f) f.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
|
||||
}
|
||||
|
||||
// 1) Semanal por defecto (fechas puestas antes del primer fetch del grafo)
|
||||
setRango(7);
|
||||
|
||||
// 2) Botones arriba a la derecha, dentro de la top-nav
|
||||
var nav = document.querySelector('.top-nav');
|
||||
if (nav && !document.getElementById('rangoBtns')) {
|
||||
var box = document.createElement('div');
|
||||
box.id = 'rangoBtns';
|
||||
box.className = 'rango-btns';
|
||||
box.innerHTML =
|
||||
'<button type="button" id="btnSemanal" class="rango-btn active">Semanal</button>' +
|
||||
'<button type="button" id="btnTodas" class="rango-btn">Todas</button>';
|
||||
nav.appendChild(box);
|
||||
document.getElementById('btnSemanal').addEventListener('click', function () {
|
||||
setRango(7); marcar('semanal'); submit();
|
||||
});
|
||||
document.getElementById('btnTodas').addEventListener('click', function () {
|
||||
setRango(null); marcar('todas'); submit();
|
||||
});
|
||||
}
|
||||
})();
|
||||
429
FLUJOS/VISUALIZACION/public/stats.css
Normal file
429
FLUJOS/VISUALIZACION/public/stats.css
Normal file
|
|
@ -0,0 +1,429 @@
|
|||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Fira Code', monospace;
|
||||
background: #000;
|
||||
color: #39ff14;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Scanline overlay */
|
||||
.scanlines {
|
||||
pointer-events: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 9999;
|
||||
background: repeating-linear-gradient(
|
||||
to bottom,
|
||||
transparent 0px,
|
||||
transparent 3px,
|
||||
rgba(0, 0, 0, 0.18) 3px,
|
||||
rgba(0, 0, 0, 0.18) 4px
|
||||
);
|
||||
}
|
||||
|
||||
/* ── HEADER ──────────────────────────────────────────────────── */
|
||||
header {
|
||||
padding: 20px 30px 10px;
|
||||
border-bottom: 1px solid #39ff1430;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
color: #39ff14;
|
||||
text-decoration: none;
|
||||
font-size: 0.85rem;
|
||||
border: 1px solid #39ff1460;
|
||||
padding: 6px 14px;
|
||||
border-radius: 20px;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.back-btn:hover {
|
||||
background: #39ff14;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-family: 'Nosifer', cursive;
|
||||
color: #fff;
|
||||
font-size: 2.8rem;
|
||||
line-height: 1;
|
||||
text-shadow: 0 0 20px #39ff1466;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1rem;
|
||||
color: #39ff1499;
|
||||
letter-spacing: 0.15em;
|
||||
}
|
||||
|
||||
.computed-at {
|
||||
font-size: 0.6rem;
|
||||
color: #39ff1444;
|
||||
letter-spacing: 0.12em;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* ── MAIN ────────────────────────────────────────────────────── */
|
||||
main {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 30px 20px 80px;
|
||||
}
|
||||
|
||||
/* ── SECTION ─────────────────────────────────────────────────── */
|
||||
.section {
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.22em;
|
||||
color: #39ff14cc;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 20px;
|
||||
border-left: 3px solid #39ff14;
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.title-sub {
|
||||
font-family: 'Fira Code', monospace;
|
||||
font-size: 1.2rem;
|
||||
color: #39ff1499;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* ── TOTALES GRID ────────────────────────────────────────────── */
|
||||
.totales-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
margin-bottom: 36px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
border: 1px solid #39ff1440;
|
||||
background: #050505;
|
||||
padding: 20px 18px 14px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: border-color 0.3s, box-shadow 0.3s;
|
||||
}
|
||||
|
||||
.stat-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0; left: 0;
|
||||
width: 3px; height: 100%;
|
||||
background: var(--accent, #39ff14);
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
border-color: #39ff1488;
|
||||
box-shadow: 0 0 20px #39ff1420;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.65rem;
|
||||
letter-spacing: 0.12em;
|
||||
color: #39ff1499;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 2.2rem;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
text-shadow: 0 0 12px currentColor;
|
||||
line-height: 1;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.stat-bar {
|
||||
height: 2px;
|
||||
background: #111;
|
||||
border-radius: 1px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.stat-bar-fill {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
opacity: 0.7;
|
||||
animation: barPulse 3s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
.stat-sub {
|
||||
font-size: 0.62rem;
|
||||
color: #39ff1466;
|
||||
letter-spacing: 0.08em;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
@keyframes barPulse {
|
||||
from { opacity: 0.4; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
/* ── CHARTS ROW ──────────────────────────────────────────────── */
|
||||
.charts-row {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.charts-row--center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* ── CHARTS (legacy + new) ───────────────────────────────────── */
|
||||
.charts-section {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.chart-box {
|
||||
border: 1px solid #39ff1430;
|
||||
background: #050505;
|
||||
padding: 24px 20px 20px;
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-width: 260px;
|
||||
}
|
||||
|
||||
.chart-box--wide {
|
||||
grid-column: 1 / -1;
|
||||
flex: 0 0 100%;
|
||||
}
|
||||
|
||||
.chart-box--sm {
|
||||
flex: 0 0 240px;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.chart-box--md {
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
/* ── COMP CARDS ──────────────────────────────────────────────── */
|
||||
.comp-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.comp-card {
|
||||
border: 1px solid var(--c, #39ff14);
|
||||
background: #050505;
|
||||
padding: 18px 16px 14px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.comp-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0; left: 0;
|
||||
width: 3px; height: 100%;
|
||||
background: var(--c, #39ff14);
|
||||
}
|
||||
|
||||
.comp-card-header {
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.16em;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 14px;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.comp-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 0.7rem;
|
||||
padding: 4px 4px;
|
||||
border-bottom: 1px solid #39ff1410;
|
||||
}
|
||||
|
||||
.comp-row span {
|
||||
color: #39ff1499;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.comp-row strong {
|
||||
color: #fff;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.sim-bar {
|
||||
height: 3px;
|
||||
background: #111;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.sim-bar-fill {
|
||||
height: 100%;
|
||||
border-radius: 2px;
|
||||
opacity: 0.8;
|
||||
transition: width 0.8s ease;
|
||||
}
|
||||
|
||||
/* ── SUBTEMA CARDS ───────────────────────────────────────────── */
|
||||
.temas-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.subtema-card {
|
||||
border: 1px solid #39ff1430;
|
||||
background: #050505;
|
||||
padding: 18px 16px;
|
||||
}
|
||||
|
||||
.subtema-card-title {
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.16em;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 14px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #39ff1420;
|
||||
}
|
||||
|
||||
.subtema-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 3px 0;
|
||||
font-size: 0.65rem;
|
||||
}
|
||||
|
||||
.subtema-name {
|
||||
flex: 0 0 110px;
|
||||
color: #39ff14aa;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.subtema-bar-wrap {
|
||||
flex: 1;
|
||||
height: 4px;
|
||||
background: #111;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.subtema-bar {
|
||||
height: 100%;
|
||||
border-radius: 2px;
|
||||
border: none;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.subtema-n {
|
||||
flex: 0 0 36px;
|
||||
text-align: right;
|
||||
color: #39ff1488;
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.18em;
|
||||
color: #39ff14cc;
|
||||
margin-bottom: 20px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
canvas {
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
/* ── LOADING ─────────────────────────────────────────────────── */
|
||||
.loading {
|
||||
text-align: center;
|
||||
font-size: 1rem;
|
||||
color: #39ff14;
|
||||
letter-spacing: 0.2em;
|
||||
padding: 60px 0;
|
||||
}
|
||||
|
||||
.blink {
|
||||
animation: blink 1s step-start infinite;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
50% { opacity: 0; }
|
||||
}
|
||||
|
||||
/* ── FOOTER ──────────────────────────────────────────────────── */
|
||||
footer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
background: #000;
|
||||
border-top: 1px solid #39ff1430;
|
||||
padding: 10px 0;
|
||||
text-align: center;
|
||||
font-family: 'Fira Code', monospace;
|
||||
color: #39ff14;
|
||||
font-size: 0.8rem;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
footer a {
|
||||
color: #39ff14;
|
||||
text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
footer a:hover { color: #fff; }
|
||||
|
||||
/* ── RESPONSIVE ──────────────────────────────────────────────── */
|
||||
@media (max-width: 1024px) {
|
||||
.totales-grid { grid-template-columns: repeat(3, 1fr); }
|
||||
.charts-row { flex-wrap: wrap; }
|
||||
.chart-box--sm { flex: 1 1 200px; }
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.totales-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
.charts-section { grid-template-columns: 1fr; }
|
||||
.charts-row { flex-direction: column; }
|
||||
.chart-box--sm, .chart-box--md { flex: 0 0 auto; width: 100%; }
|
||||
.comp-grid { grid-template-columns: 1fr; }
|
||||
.temas-grid { grid-template-columns: 1fr; }
|
||||
.title { font-size: 2rem; }
|
||||
.stat-value { font-size: 1.6rem; }
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.totales-grid { grid-template-columns: 1fr 1fr; }
|
||||
.title { font-size: 1.5rem; }
|
||||
}
|
||||
360
FLUJOS/VISUALIZACION/public/stats.html
Normal file
360
FLUJOS/VISUALIZACION/public/stats.html
Normal file
|
|
@ -0,0 +1,360 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>UP-LEAKS // STATS</title>
|
||||
<link rel="stylesheet" href="stats.css">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;700&family=Nosifer&display=swap" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="scanlines"></div>
|
||||
|
||||
<header>
|
||||
<div class="header-content">
|
||||
<a href="/" class="back-btn">← BACK</a>
|
||||
<h1 class="title">UP-LEAKS <span class="title-sub">// STATS</span></h1>
|
||||
<span id="computed-at" class="computed-at"></span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main id="main" style="display:none">
|
||||
|
||||
<!-- ── TOTALES GLOBALES ─────────────────────────────── -->
|
||||
<section class="section">
|
||||
<h2 class="section-title">// CORPUS TOTAL</h2>
|
||||
<div class="totales-grid">
|
||||
<div class="stat-card" style="--accent:#39ff14">
|
||||
<div class="stat-label">COMPARACIONES</div>
|
||||
<div class="stat-value" id="val-comparaciones">—</div>
|
||||
<div class="stat-sub" id="sub-comp-txt"></div>
|
||||
</div>
|
||||
<div class="stat-card" style="--accent:#00fff2">
|
||||
<div class="stat-label">ARTÍCULOS WIKI</div>
|
||||
<div class="stat-value" id="val-wikipedia">—</div>
|
||||
</div>
|
||||
<div class="stat-card" style="--accent:#ff00ff">
|
||||
<div class="stat-label">NOTICIAS</div>
|
||||
<div class="stat-value" id="val-noticias">—</div>
|
||||
</div>
|
||||
<div class="stat-card" style="--accent:#ffdc00">
|
||||
<div class="stat-label">LEAKS / DOCS</div>
|
||||
<div class="stat-value" id="val-torrents">—</div>
|
||||
</div>
|
||||
<div class="stat-card" style="--accent:#ff6600">
|
||||
<div class="stat-label">IMÁGENES</div>
|
||||
<div class="stat-value" id="val-imagenes_wiki">—</div>
|
||||
</div>
|
||||
<div class="stat-card" style="--accent:#39ff14">
|
||||
<div class="stat-label">COMP. IMAGEN↔TEXTO</div>
|
||||
<div class="stat-value" id="val-comp-img">—</div>
|
||||
<div class="stat-sub" id="sub-comp-img"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ── GRÁFICOS GLOBALES ────────────────────────────── -->
|
||||
<section class="section">
|
||||
<h2 class="section-title">// DISTRIBUCIÓN DE FUENTES</h2>
|
||||
<div class="charts-row">
|
||||
<div class="chart-box">
|
||||
<h3 class="chart-title">Corpus por temática</h3>
|
||||
<canvas id="chartTemas"></canvas>
|
||||
</div>
|
||||
<div class="chart-box chart-box--sm">
|
||||
<h3 class="chart-title">Fuentes globales</h3>
|
||||
<canvas id="chartDona"></canvas>
|
||||
</div>
|
||||
<div class="chart-box chart-box--sm">
|
||||
<h3 class="chart-title">Comparaciones imagen</h3>
|
||||
<canvas id="chartCompImg"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ── COMPARACIONES STATS ──────────────────────────── -->
|
||||
<section class="section">
|
||||
<h2 class="section-title">// ANÁLISIS DE COMPARACIONES</h2>
|
||||
<div class="comp-grid" id="comp-grid"></div>
|
||||
</section>
|
||||
|
||||
<!-- ── SUBTEMAS POR TEMÁTICA ────────────────────────── -->
|
||||
<section class="section">
|
||||
<h2 class="section-title">// SUBTEMAS POR TEMÁTICA — WIKIPEDIA</h2>
|
||||
<div class="temas-grid" id="temas-subtemas"></div>
|
||||
</section>
|
||||
|
||||
<!-- ── SUBTEMAS NOTICIAS ────────────────────────────── -->
|
||||
<section class="section" id="sec-noticias-sub" style="display:none">
|
||||
<h2 class="section-title">// SUBTEMAS — NOTICIAS</h2>
|
||||
<div class="temas-grid" id="noticias-subtemas"></div>
|
||||
</section>
|
||||
|
||||
<!-- ── RADAR POR TEMA ───────────────────────────────── -->
|
||||
<section class="section">
|
||||
<h2 class="section-title">// COBERTURA POR TEMÁTICA</h2>
|
||||
<div class="charts-row charts-row--center">
|
||||
<div class="chart-box chart-box--md">
|
||||
<h3 class="chart-title">Radar de cobertura</h3>
|
||||
<canvas id="chartRadar"></canvas>
|
||||
</div>
|
||||
<div class="chart-box chart-box--md">
|
||||
<h3 class="chart-title">Similitud promedio imagen↔texto</h3>
|
||||
<canvas id="chartSimImg"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
<div class="loading" id="loading">CARGANDO DATOS DEL SISTEMA<span class="blink">_</span></div>
|
||||
|
||||
<footer>
|
||||
<p><a href="#">GitHub</a> | <a href="#">Telegram</a> | <a href="#">Email</a> | <a href="#">Web de Tor</a></p>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
const TEMAS = ['guerra global','inteligencia y seguridad','cambio climático','demografía y sociedad','economía y corporaciones'];
|
||||
const TEMA_COLORS = {
|
||||
'guerra global': '#FF4136',
|
||||
'inteligencia y seguridad': '#0074D9',
|
||||
'cambio climático': '#2ECC40',
|
||||
'demografía y sociedad': '#FF851B',
|
||||
'economía y corporaciones': '#FFDC00',
|
||||
};
|
||||
const TEMA_LABELS = {
|
||||
'guerra global': 'GLOB-WAR',
|
||||
'inteligencia y seguridad': 'INT-SEC',
|
||||
'cambio climático': 'CLIMATE',
|
||||
'demografía y sociedad': 'POPL-UP',
|
||||
'economía y corporaciones': 'ECO-CORP',
|
||||
};
|
||||
|
||||
function esc(str) {
|
||||
return String(str ?? '')
|
||||
.replace(/&/g, '&').replace(/</g, '<')
|
||||
.replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function fmtNum(n) {
|
||||
if (n >= 1_000_000) return (n / 1_000_000).toFixed(2) + 'M';
|
||||
if (n >= 1_000) return (n / 1_000).toFixed(1) + 'K';
|
||||
return String(n);
|
||||
}
|
||||
|
||||
function animateCount(el, target, fmt) {
|
||||
const dur = 1600, start = performance.now();
|
||||
function step(now) {
|
||||
const t = Math.min((now - start) / dur, 1);
|
||||
const e = 1 - Math.pow(1 - t, 3);
|
||||
el.textContent = fmt ? fmt(Math.floor(target * e)) : fmtNum(Math.floor(target * e));
|
||||
if (t < 1) requestAnimationFrame(step);
|
||||
}
|
||||
requestAnimationFrame(step);
|
||||
}
|
||||
|
||||
const CHART_OPTS = {
|
||||
plugins: {
|
||||
legend: { labels: { color: '#39ff14', font: { family: 'Fira Code', size: 11 } } },
|
||||
tooltip: {
|
||||
bodyFont: { family: 'Fira Code' }, titleFont: { family: 'Fira Code' },
|
||||
backgroundColor: '#080808', borderColor: '#39ff1466', borderWidth: 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function scaleOpts(stacked) {
|
||||
return {
|
||||
x: { stacked, ticks: { color: '#39ff1499', font: { family: 'Fira Code', size: 10 } }, grid: { color: '#111' } },
|
||||
y: { stacked, ticks: { color: '#39ff1499', font: { family: 'Fira Code', size: 10 } }, grid: { color: '#111' } },
|
||||
};
|
||||
}
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
const res = await fetch('/api/stats');
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
throw new Error(err.error || `HTTP ${res.status}`);
|
||||
}
|
||||
const data = await res.json();
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
document.getElementById('main').style.display = '';
|
||||
|
||||
const { totales, por_tema, subtemas, subtemas_noticias, comp_imagen, comp_texto, computed_at } = data;
|
||||
if (computed_at) {
|
||||
const d = new Date(computed_at);
|
||||
document.getElementById('computed-at').textContent =
|
||||
`SNAPSHOT: ${d.toLocaleDateString('es-ES')} ${d.toLocaleTimeString('es-ES',{hour:'2-digit',minute:'2-digit'})}`;
|
||||
}
|
||||
|
||||
// ── Contadores ──────────────────────────────────────────
|
||||
animateCount(document.getElementById('val-comparaciones'), totales.comparaciones);
|
||||
animateCount(document.getElementById('val-wikipedia'), totales.wikipedia);
|
||||
animateCount(document.getElementById('val-noticias'), totales.noticias);
|
||||
animateCount(document.getElementById('val-torrents'), totales.torrents);
|
||||
animateCount(document.getElementById('val-imagenes_wiki'), totales.imagenes_wiki);
|
||||
animateCount(document.getElementById('val-comp-img'), comp_imagen.total);
|
||||
document.getElementById('sub-comp-txt').textContent =
|
||||
`sim. promedio ~${comp_texto.avg_sim}% · max ${comp_texto.max_sim}%`;
|
||||
document.getElementById('sub-comp-img').textContent =
|
||||
`imagen ↔ texto (TF-IDF)`;
|
||||
|
||||
// ── Chart: barras apiladas por tema ──────────────────────
|
||||
new Chart(document.getElementById('chartTemas'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: TEMAS.map(t => TEMA_LABELS[t]),
|
||||
datasets: [
|
||||
{ label: 'Wikipedia', data: TEMAS.map(t => por_tema[t]?.wikipedia||0), backgroundColor:'#00fff2bb', borderColor:'#00fff2', borderWidth:2 },
|
||||
{ label: 'Noticias', data: TEMAS.map(t => por_tema[t]?.noticias||0), backgroundColor:'#ff00ffbb', borderColor:'#ff00ff', borderWidth:2 },
|
||||
{ label: 'Leaks', data: TEMAS.map(t => por_tema[t]?.torrents||0), backgroundColor:'#ffdc00bb', borderColor:'#ffdc00', borderWidth:2 },
|
||||
{ label: 'Imágenes', data: TEMAS.map(t => por_tema[t]?.imagenes_wiki||0),backgroundColor:'#ff6600bb', borderColor:'#ff6600', borderWidth:2 },
|
||||
],
|
||||
},
|
||||
options: { ...CHART_OPTS, scales: scaleOpts(true) },
|
||||
});
|
||||
|
||||
// ── Chart: dona fuentes globales ─────────────────────────
|
||||
new Chart(document.getElementById('chartDona'), {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: ['Wikipedia','Noticias','Leaks','Imágenes'],
|
||||
datasets: [{ data: [totales.wikipedia, totales.noticias, totales.torrents, totales.imagenes_wiki],
|
||||
backgroundColor:['#00fff2bb','#ff00ffbb','#ffdc00bb','#ff6600bb'],
|
||||
borderColor: ['#00fff2', '#ff00ff', '#ffdc00', '#ff6600'], borderWidth:2 }],
|
||||
},
|
||||
options: { ...CHART_OPTS, cutout:'62%' },
|
||||
});
|
||||
|
||||
// ── Chart: dona comparaciones imagen por tema ─────────────
|
||||
new Chart(document.getElementById('chartCompImg'), {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: TEMAS.map(t => TEMA_LABELS[t]),
|
||||
datasets: [{ data: TEMAS.map(t => comp_imagen.por_tema[t]?.count||0),
|
||||
backgroundColor: TEMAS.map(t => TEMA_COLORS[t]+'bb'),
|
||||
borderColor: TEMAS.map(t => TEMA_COLORS[t]), borderWidth:2 }],
|
||||
},
|
||||
options: { ...CHART_OPTS, cutout:'55%' },
|
||||
});
|
||||
|
||||
// ── Comp stats cards ─────────────────────────────────────
|
||||
const compGrid = document.getElementById('comp-grid');
|
||||
TEMAS.forEach(t => {
|
||||
const ci = comp_imagen.por_tema[t] || { count:0, avg_sim:0, max_sim:0 };
|
||||
const wiki = por_tema[t]?.wikipedia || 0;
|
||||
const c = TEMA_COLORS[t];
|
||||
compGrid.innerHTML += `
|
||||
<div class="comp-card" style="--c:${esc(c)}">
|
||||
<div class="comp-card-header" style="color:${esc(c)}">${esc(TEMA_LABELS[t])}</div>
|
||||
<div class="comp-row"><span>Comparaciones img↔txt</span><strong>${esc(fmtNum(ci.count))}</strong></div>
|
||||
<div class="comp-row"><span>Similitud media</span><strong>${esc(ci.avg_sim)}%</strong></div>
|
||||
<div class="comp-row"><span>Similitud máxima</span><strong>${esc(ci.max_sim)}%</strong></div>
|
||||
<div class="comp-row"><span>Artículos Wikipedia</span><strong>${esc(fmtNum(wiki))}</strong></div>
|
||||
<div class="sim-bar">
|
||||
<div class="sim-bar-fill" style="width:${Math.min(Number(ci.avg_sim)*4,100)}%;background:${esc(c)}"></div>
|
||||
</div>
|
||||
</div>`;
|
||||
});
|
||||
|
||||
// ── Radar cobertura ──────────────────────────────────────
|
||||
const maxWiki = Math.max(...TEMAS.map(t => por_tema[t]?.wikipedia||0));
|
||||
new Chart(document.getElementById('chartRadar'), {
|
||||
type: 'radar',
|
||||
data: {
|
||||
labels: TEMAS.map(t => TEMA_LABELS[t]),
|
||||
datasets: [
|
||||
{ label: 'Wikipedia', data: TEMAS.map(t => Math.round((por_tema[t]?.wikipedia||0)/maxWiki*100)),
|
||||
backgroundColor:'#00fff222', borderColor:'#00fff2', pointBackgroundColor:'#00fff2', borderWidth:2 },
|
||||
{ label: 'Imágenes', data: TEMAS.map(t => Math.round((por_tema[t]?.imagenes_wiki||0)/20*100)),
|
||||
backgroundColor:'#ff660022', borderColor:'#ff6600', pointBackgroundColor:'#ff6600', borderWidth:2 },
|
||||
],
|
||||
},
|
||||
options: {
|
||||
...CHART_OPTS,
|
||||
scales: { r: {
|
||||
ticks: { color:'#39ff1466', font:{ family:'Fira Code', size:9 }, backdropColor:'transparent' },
|
||||
grid: { color:'#1a1a1a' },
|
||||
pointLabels: { color:'#39ff14', font:{ family:'Fira Code', size:11 } },
|
||||
angleLines: { color:'#1a1a1a' },
|
||||
}},
|
||||
},
|
||||
});
|
||||
|
||||
// ── Chart similitud imagen por tema ──────────────────────
|
||||
new Chart(document.getElementById('chartSimImg'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: TEMAS.map(t => TEMA_LABELS[t]),
|
||||
datasets: [
|
||||
{ label: 'Sim. promedio %', data: TEMAS.map(t => comp_imagen.por_tema[t]?.avg_sim||0),
|
||||
backgroundColor: TEMAS.map(t => TEMA_COLORS[t]+'99'), borderColor: TEMAS.map(t => TEMA_COLORS[t]), borderWidth:2 },
|
||||
{ label: 'Sim. máxima %', data: TEMAS.map(t => comp_imagen.por_tema[t]?.max_sim||0),
|
||||
backgroundColor: 'transparent', borderColor: TEMAS.map(t => TEMA_COLORS[t]), borderWidth:2, type:'line',
|
||||
pointRadius:5, pointBackgroundColor: TEMAS.map(t => TEMA_COLORS[t]) },
|
||||
],
|
||||
},
|
||||
options: { ...CHART_OPTS, scales: scaleOpts(false) },
|
||||
});
|
||||
|
||||
// ── Subtemas Wikipedia ────────────────────────────────────
|
||||
const temasDiv = document.getElementById('temas-subtemas');
|
||||
TEMAS.forEach(t => {
|
||||
const subs = subtemas[t] || [];
|
||||
if (!subs.length) return;
|
||||
const maxN = subs[0].n;
|
||||
const c = TEMA_COLORS[t];
|
||||
temasDiv.innerHTML += `
|
||||
<div class="subtema-card">
|
||||
<div class="subtema-card-title" style="color:${esc(c)}">${esc(TEMA_LABELS[t])} <span style="color:#39ff1466;font-size:0.7rem">// ${esc(t)}</span></div>
|
||||
${subs.map(s => `
|
||||
<div class="subtema-row">
|
||||
<span class="subtema-name">${esc(s.subtema)}</span>
|
||||
<div class="subtema-bar-wrap">
|
||||
<div class="subtema-bar" style="width:${Math.round(s.n/maxN*100)}%;background:${esc(c)}66"></div>
|
||||
</div>
|
||||
<span class="subtema-n">${esc(fmtNum(s.n))}</span>
|
||||
</div>`).join('')}
|
||||
</div>`;
|
||||
});
|
||||
|
||||
// ── Subtemas Noticias ─────────────────────────────────────
|
||||
const hasNot = TEMAS.some(t => (subtemas_noticias[t]||[]).length > 0);
|
||||
if (hasNot) {
|
||||
document.getElementById('sec-noticias-sub').style.display = '';
|
||||
const notDiv = document.getElementById('noticias-subtemas');
|
||||
TEMAS.forEach(t => {
|
||||
const subs = subtemas_noticias[t] || [];
|
||||
if (!subs.length) return;
|
||||
const maxN = subs[0].n;
|
||||
const c = TEMA_COLORS[t];
|
||||
notDiv.innerHTML += `
|
||||
<div class="subtema-card">
|
||||
<div class="subtema-card-title" style="color:${esc(c)}">${esc(TEMA_LABELS[t])}</div>
|
||||
${subs.map(s => `
|
||||
<div class="subtema-row">
|
||||
<span class="subtema-name">${esc(s.subtema)}</span>
|
||||
<div class="subtema-bar-wrap">
|
||||
<div class="subtema-bar" style="width:${Math.round(s.n/maxN*100)}%;background:${esc(c)}66"></div>
|
||||
</div>
|
||||
<span class="subtema-n">${esc(fmtNum(s.n))}</span>
|
||||
</div>`).join('')}
|
||||
</div>`;
|
||||
});
|
||||
}
|
||||
|
||||
} catch(e) {
|
||||
document.getElementById('loading').textContent = 'ERROR AL CARGAR DATOS (' + e.message + ')';
|
||||
document.getElementById('loading').style.color = '#ff1a1a';
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
load();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,10 +1,6 @@
|
|||
@font-face {
|
||||
font-family: "Retrolift";
|
||||
src: url("fonts/retrolift.woff2") format("woff2"),
|
||||
url("fonts/retrolift.woff") format("woff");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
/* La familia "Retrolift" no se distribuye con el repo: no hay @font-face.
|
||||
Las reglas que la piden caen a sans-serif. Para usarla, deja el .woff2 en
|
||||
fonts/ y anade aqui la regla con una ruta relativa. */
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
|
|
|
|||
|
|
@ -276,3 +276,20 @@ nav.top-nav {
|
|||
.section-dropdown { min-width: 160px; }
|
||||
.section-dropdown a { font-size: 0.7em; padding: 7px 14px; }
|
||||
}
|
||||
|
||||
/* ── Filtro temporal Semanal/Todas (semana-toggle.js) ── */
|
||||
.rango-btns {
|
||||
position: absolute; right: 16px; top: 50%; transform: translateY(-50%);
|
||||
display: flex; gap: 8px; z-index: 10000;
|
||||
}
|
||||
.rango-btn {
|
||||
font-family: 'Fira Code', monospace; font-size: .82em; cursor: pointer;
|
||||
padding: 7px 15px; border-radius: 6px;
|
||||
border: 2px solid rgba(255,255,255,.35);
|
||||
background: rgba(0,0,0,.5); color: #eee; transition: all .15s;
|
||||
}
|
||||
.rango-btn:hover { border-color: #fff; }
|
||||
.rango-btn.active { background: #ececec; color: #111; border-color: #fff; font-weight: 700; }
|
||||
@media (max-width: 760px) {
|
||||
.rango-btns { position: static; transform: none; justify-content: center; margin-top: 6px; width: 100%; }
|
||||
}
|
||||
|
|
|
|||
1
FLUJOS/VISUALIZACION/public/wiki-images
Symbolic link
1
FLUJOS/VISUALIZACION/public/wiki-images
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
/var/www/theflows.net/flujos/FLUJOS_DATOS/IMAGENES/output/wiki_images
|
||||
|
|
@ -1,10 +1,6 @@
|
|||
@font-face {
|
||||
font-family: "Retrolift";
|
||||
src: url("fonts/retrolift.woff2") format("woff2"),
|
||||
url("fonts/retrolift.woff") format("woff");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
/* La familia "Retrolift" no se distribuye con el repo: no hay @font-face.
|
||||
Las reglas que la piden caen a sans-serif. Para usarla, deja el .woff2 en
|
||||
fonts/ y anade aqui la regla con una ruta relativa. */
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
@font-face {
|
||||
font-family: "Retrolift";
|
||||
src: url("fonts/retrolift.woff2") format("woff2"),
|
||||
url("fonts/retrolift.woff") format("woff");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
/* La familia "Retrolift" no se distribuye con el repo: no hay @font-face.
|
||||
Las reglas que la piden caen a sans-serif. Para usarla, deja el .woff2 en
|
||||
fonts/ y anade aqui la regla con una ruta relativa. */
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
@font-face {
|
||||
font-family: "Retrolift";
|
||||
src: url("fonts/retrolift.woff2") format("woff2"),
|
||||
url("fonts/retrolift.woff") format("woff");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
/* La familia "Retrolift" no se distribuye con el repo: no hay @font-face.
|
||||
Las reglas que la piden caen a sans-serif. Para usarla, deja el .woff2 en
|
||||
fonts/ y anade aqui la regla con una ruta relativa. */
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
@font-face {
|
||||
font-family: "Retrolift";
|
||||
src: url("fonts/retrolift.woff2") format("woff2"),
|
||||
url("fonts/retrolift.woff") format("woff");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
/* La familia "Retrolift" no se distribuye con el repo: no hay @font-face.
|
||||
Las reglas que la piden caen a sans-serif. Para usarla, deja el .woff2 en
|
||||
fonts/ y anade aqui la regla con una ruta relativa. */
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
@font-face {
|
||||
font-family: "Retrolift";
|
||||
src: url("fonts/retrolift.woff2") format("woff2"),
|
||||
url("fonts/retrolift.woff") format("woff");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
/* La familia "Retrolift" no se distribuye con el repo: no hay @font-face.
|
||||
Las reglas que la piden caen a sans-serif. Para usarla, deja el .woff2 en
|
||||
fonts/ y anade aqui la regla con una ruta relativa. */
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
@font-face {
|
||||
font-family: "Retrolift";
|
||||
src: url("fonts/retrolift.woff2") format("woff2"),
|
||||
url("fonts/retrolift.woff") format("woff");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
/* La familia "Retrolift" no se distribuye con el repo: no hay @font-face.
|
||||
Las reglas que la piden caen a sans-serif. Para usarla, deja el .woff2 en
|
||||
fonts/ y anade aqui la regla con una ruta relativa. */
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
@font-face {
|
||||
font-family: "Retrolift";
|
||||
src: url("fonts/retrolift.woff2") format("woff2"),
|
||||
url("fonts/retrolift.woff") format("woff");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
/* La familia "Retrolift" no se distribuye con el repo: no hay @font-face.
|
||||
Las reglas que la piden caen a sans-serif. Para usarla, deja el .woff2 en
|
||||
fonts/ y anade aqui la regla con una ruta relativa. */
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue