/* ===========================================================
Views (set 1): Overview, Pipeline, Runs, RunDetail
=========================================================== */
const { useState: useState1, useEffect: useEffect1, useMemo: useMemo1, useRef: useRef1, Fragment } = React;
/* ------------------------- OVERVIEW ------------------------- */
function OverviewView({ runs, nav }) {
const today = runs.length;
const published = runs.filter(r => r.publisher.success).length;
const succRate = Math.round((runs.filter(r => r.status === 'success').length / runs.length) * 100);
const avgDur = runs.reduce((a, r) => a + r.duration_seconds, 0) / runs.length;
const avgDurY = avgDur * 0.92;
// Build funnel from all runs aggregated
const totalFound = runs.reduce((a, r) => a + r.collector.total_found, 0);
const dedup = runs.reduce((a, r) => a + r.cooldown.candidates_after_cooldown, 0);
const editorSel = runs.length;
const qualOK = runs.filter(r => r.quality.passed).length;
const pubOK = runs.filter(r => r.publisher.success).length;
const stages = [
{ name: 'Collected', value: totalFound, tip: 'RSS scrape · 9 sources' },
{ name: 'Deduplicated', value: dedup, tip: 'Jaccard ≥ 0.78 + cooldown' },
{ name: 'Editor-selected', value: editorSel, tip: 'Top-1 per run' },
{ name: 'Quality-passed', value: qualOK, tip: 'Rúbrica editorial OK' },
{ name: 'Published', value: pubOK, tip: 'WordPress 201' },
];
// Live pipeline (latest run)
const latest = runs[0];
const stageList = [
{ name: 'RSS Collector', status: 'ok', model: 'qwen2.5:14b', dur: '3.2s' },
{ name: 'Deduplication', status: 'ok', model: 'local jaccard', dur: '0.4s' },
{ name: 'Cooldown', status: 'ok', model: 'local', dur: '0.1s' },
{ name: 'Trends agent', status: 'ok', model: 'qwen2.5:14b', dur: '4.8s' },
{ name: 'Editor', status: 'ok', model: 'qwen2.5:14b', dur: '6.1s' },
{ name: 'Extractor', status: 'ok', model: 'http/scrape', dur: '2.0s' },
{ name: 'Dual writer', status: 'run', model: 'mistral · gemma',dur: '12.4s' },
{ name: 'Selector', status: 'idle', model: 'qwen2.5:14b', dur: '—' },
{ name: 'Fact-checker', status: 'idle', model: 'qwen2.5:14b', dur: '—' },
{ name: 'Quality gate', status: 'idle', model: 'local rúbrica', dur: '—' },
{ name: 'SEO agent', status: 'idle', model: 'gemma4:e4b', dur: '—' },
{ name: 'WordPress', status: 'idle', model: 'wp-rest', dur: '—' },
];
// anomaly: longest run that exceeds 1.5x mean
const anomalous = runs.find(r => r.duration_seconds > avgDur * 1.5);
const durSparkData = runs.map(r => r.duration_seconds).reverse();
const pubSparkData = runs.map(r => r.publisher.success ? 1 : 0).reverse();
return (
Sala de redacción
Overview
Pipeline activo
actualizado {fmtAgo(latest.timestamp)}
{/* KPI strip */}
Hoy
{today}runs
+2 vs ayer
runs.length - i)} color="var(--info)" />
Publicados
{published}/ {today}
+1 vs ayer
v + i*0.1)} color="var(--ok)" />
Tasa de éxito
{succRate}%
— sin cambio
Duración media
{fmtDuration(avgDur)}min
+{((avgDur/avgDurY - 1)*100).toFixed(0)}% vs ayer
{/* Funnel + Live status */}
FLOW Pipeline funnel
últimas {runs.length} ejecuciones · agregado
LIVE Pipeline status
{fmtClock(latest.timestamp)}
{stageList.map(s => (
))}
{/* Recent runs + Anomaly */}
HISTORY Últimos 10 ciclos
click para abrir
Hora
Estado
Titular seleccionado
Fuente
Writer
Att.
Duración
{runs.map(r => (
nav(`/runs/${r.id}`)}>
{fmtClock(r.timestamp)}
{r.editor.selected_titulo}
{r.editor.selected_fuente}
{r.writer.winner_model}
1 ? 'var(--warn)' : 'var(--text2)'}}>{r.writer.attempts}
{fmtDuration(r.duration_seconds)}
))}
{anomalous && (
Anomalía detectada
{anomalous.editor.selected_titulo.slice(0, 70)}…
Run a las
{fmtClock(anomalous.timestamp)} tardó
{fmtDuration(anomalous.duration_seconds)} — un
{Math.round((anomalous.duration_seconds/avgDur - 1)*100)}% sobre la media. Cuello de botella:
Writer ({anomalous.writer.winner_model}) con
{anomalous.writer.attempts} intentos .
nav(`/runs/${anomalous.id}/visor`)} style={{cursor: 'pointer'}}>
abrir visor de reintentos →
)}
Lo más leído (mock)
El BCE recorta los tipos al 2,5%
Real Madrid, decimosexta Champions
OpenAI presenta GPT-6
El alquiler en España, récord
Sistema
Ollama bridge localhost:11434
Modelos cargados 3 / 3
vRAM GPU 14.8 / 24 GiB
Last heartbeat 2s ago
Bridge WS CONNECTED
);
}
/* ------------------------- PIPELINE ------------------------- */
function PipelineView({ runs, nav }) {
const latest = runs[0];
const totalDur = latest.duration_seconds;
// build a fake but plausible gantt for the latest run
const lanes = [
{ name: 'Collector', bars: [{start: 0, duration: 8, cls: 'collector', label: '8.1s', tip: 'qwen2.5:14b · 9 fuentes RSS'}] },
{ name: 'Cooldown', bars: [{start: 8, duration: 0.4, cls: 'cooldown', label: '0.4s', tip: '6 candidatos penalizados'}] },
{ name: 'Trends', bars: [{start: 8.4, duration: 5.2, cls: 'trends', label: '5.2s', tip: 'Gemini hints + Google trends'}] },
{ name: 'Editor', bars: [{start: 13.6, duration: 7.4, cls: 'editor', label: '7.4s', tip: 'qwen2.5:14b · 73 cand.'}] },
{ name: 'Extractor', bars: [{start: 21, duration: 2.1, cls: 'extractor', label: '2.1s', tip: 'http scrape · 562 palabras'}] },
{ name: 'Writer A', bars: [{start: 23.1, duration: 11.4, cls: 'writer-a', label: 'mistral 11.4s', tip: 'mistral-nemo:12b · 1.342 tokens'}] },
{ name: 'Writer B', bars: [{start: 23.1, duration: 9.8, cls: 'writer-b', label: 'gemma 9.8s', tip: 'gemma4:e4b · 1.188 tokens'}] },
{ name: 'Selector', bars: [{start: 34.5, duration: 4.0, cls: 'selector', label: '4.0s', tip: 'A wins (0.78 vs 0.61)'}] },
{ name: 'Fact-checker', bars: [{start: 38.5, duration: 6.2, cls: 'fact', label: '6.2s', tip: '17/17 claims ✓'}] },
{ name: 'Quality', bars: [{start: 44.7, duration: 1.8, cls: 'quality', label: '1.8s', tip: latest.writer.attempts > 1 ? `${latest.writer.attempts} intentos` : 'OK first try'}] },
{ name: 'SEO', bars: [{start: 46.5, duration: 3.4, cls: 'seo', label: '3.4s', tip: 'gemma4:e4b'}] },
{ name: 'Publisher', bars: [{start: 49.9, duration: 2.6, cls: latest.publisher.success ? 'publisher' : 'failed', label: '2.6s', tip: latest.publisher.success ? `WP 201 → ${latest.publisher.post_id}` : 'WP 504'}] },
];
const ganttTotal = 53;
// heatmap: topics x last 15 runs
const allRuns = runs.slice(0, 15);
const topicRows = window.TOPICS;
const maxCellVal = 10;
// Funnel from latest run
const lr = latest;
const stages = [
{ name: 'Collected', value: lr.collector.total_found, tip: 'RSS scrape' },
{ name: 'Deduplicated', value: lr.collector.candidates_after_dedup, tip: 'Jaccard 0.78' },
{ name: 'Post-cooldown', value: lr.cooldown.candidates_after_cooldown, tip: 'Factor 0.65' },
{ name: 'Editor-selected', value: 1, tip: 'Top-1' },
{ name: 'Published', value: lr.publisher.success ? 1 : 0, tip: 'WP REST' },
];
return (
Diagnóstico
Pipeline detail
Run actual: {lr.id}
{fmtClock(lr.timestamp)}
FUNNEL Run actual — drop-off por etapa
Top razones de descarte
{[
{ label: 'Duplicado (Jaccard ≥ 0.78)', count: 12, color: '#5b8def' },
{ label: 'Penalización cooldown', count: 9, color: '#ad7eea' },
{ label: 'Fuente fuera de catálogo', count: 4, color: 'var(--warn)' },
{ label: 'Idioma no detectado', count: 2, color: 'var(--text3)' },
{ label: 'Sin texto extraíble', count: 1, color: 'var(--err)' },
].map(r => (
))}
GANTT Cronograma — etapas del último run
duración total {fmtDuration(lr.duration_seconds)}
HEATMAP Penalización por cooldown — temas × runs recientes
color más intenso = más penalización
{allRuns.map((r,i) =>
{String(i+1).padStart(2,'0')}
)}
{topicRows.map(t => (
{t.replace(/_/g, ' ')}
{allRuns.map((r,i) => {
const v = r.cooldown.penalized_topics[t] || 0;
const op = v === 0 ? 0.08 : 0.18 + (v/maxCellVal) * 0.82;
return (
);
})}
))}
bajo
medio
alto
);
}
/* ------------------------- RUNS HISTORY ------------------------- */
function RunsView({ runs, nav }) {
const [filter, setFilter] = useState1('all');
const [search, setSearch] = useState1('');
const filtered = runs.filter(r => {
if (filter !== 'all' && r.status !== filter) return false;
if (search && !r.editor.selected_titulo.toLowerCase().includes(search.toLowerCase())) return false;
return true;
});
return (
Archivo
Historial de runs
{filtered.length} de {runs.length} runs
{['all', 'success', 'partial', 'error'].map(s => (
setFilter(s)}
style={{
cursor: 'pointer',
background: filter === s ? 'var(--surface3)' : 'var(--surface2)',
color: filter === s ? 'var(--text1)' : 'var(--text3)',
borderColor: filter === s ? 'var(--border-strong)' : 'var(--border)',
}}>
{s}
))}
setSearch(e.target.value)}
placeholder="buscar por titular…"
style={{background:'transparent', border:'none', outline:'none', color:'var(--text1)', fontSize: 12.5, width:'100%', fontFamily:'var(--sans)'}}
/>
Timestamp
Estado
Titular
Fuente
Writer
Att.
Duración
Fact
Quality
WP
{filtered.map(r => (
nav(`/runs/${r.id}`)}>
{fmtClock(r.timestamp)} · {fmtAgo(r.timestamp)}
{r.editor.selected_titulo}
{r.editor.selected_fuente}
{r.writer.winner_model}
1 ? 'var(--warn)' : 'var(--text2)'}}>{r.writer.attempts}
{fmtDuration(r.duration_seconds)}
{r.fact_checker.passed ? : }
{r.quality.passed ? : }
{r.publisher.success
? e.stopPropagation()} style={{textDecoration:'none'}}> #{r.publisher.post_id}
: no pub. }
))}
);
}
/* ------------------------- RUN DETAIL ------------------------- */
function RunDetailView({ run, nav }) {
const writerAttempt = run.writer.attempts_history[run.writer.attempts_history.length - 1];
const breakdown = run.editor.breakdown;
const donutData = [
{ label: 'Source weight', value: breakdown.source, color: '#5b8def' },
{ label: 'Freshness', value: breakdown.freshness, color: 'var(--gold)' },
{ label: 'Relevance', value: breakdown.relevance, color: '#4ec9b0' },
{ label: 'Trends boost', value: breakdown.trends, color: '#d685c3' },
{ label: 'Gemini boost', value: breakdown.gemini, color: '#c98c4e' },
];
const totalDur = run.duration_seconds;
const compactLanes = [
{ name: 'Collector', bars: [{start: 0, duration: 10, cls: 'collector', label: '', tip: 'qwen2.5:14b'}] },
{ name: 'Editor', bars: [{start: 12, duration: 9, cls: 'editor', label: '', tip: 'qwen2.5:14b'}] },
{ name: 'Writer', bars: [{start: 22, duration: 22, cls: 'writer-a', label: `${run.writer.attempts}×`, tip: `${run.writer.winner_model}`}] },
{ name: 'Quality', bars: [{start: 45, duration: 4, cls: run.quality.passed ? 'quality' : 'failed', label: '', tip: run.quality.passed ? 'OK' : 'failed'}] },
{ name: 'Publisher', bars: [{start: 50, duration: 3, cls: run.publisher.success ? 'publisher' : 'failed', label: '', tip: run.publisher.success ? `#${run.publisher.post_id}` : 'failed'}] },
];
// parse the article html with regex placeholders into a clean version (no flaws)
const articleHtml = renderArticleHtmlClean(run);
return (
nav('/runs')} style={{cursor:'pointer'}}> Runs
Run {run.id}
{run.editor.selected_titulo}
{fmtClock(run.timestamp)}
•
{fmtDuration(run.duration_seconds)}
{/* LEFT: SEO + article */}
slug
{run.seo.slug}
categoría
{run.seo.category}
focus keyword
{run.seo.focus_keyword}
tags
{run.seo.tags.map(t => {t} )}
El Cronista Automático
Edición digital
{new Date(run.timestamp).toLocaleDateString('es-ES', {weekday:'long', day:'numeric', month:'long', year:'numeric'})}
Sección · {run.seo.category}
{run.editor.selected_titulo}
{getStoryDeck(run.editor.selected_titulo)}
Por la redacción automatizada · Fuente original: {run.editor.selected_fuente}
nav(`/runs/${run.id}/visor`)} style={{cursor:'pointer'}}>
abrir visor de reintentos
{/* RIGHT: editor reasoning */}
Desglose del editorial score
EDITOR Top 3 candidatos considerados
{run.editor.top_3_candidates.map((c, i) => {
const isSelected = c.titulo === run.editor.selected_titulo;
return (
{isSelected ? seleccionado : `candidato #${i+1}`}
{c.editorial_score.toFixed(3)}
{c.titulo}
{c.fuente}
);
})}
TIMELINE Cronograma compacto
{run.errors.length > 0 && (
Errores
{run.errors.map((e, i) => (
0 ? '1px dashed var(--border)' : 'none', fontSize: 12}}>
{e.stage}
{fmtClock(e.timestamp)}
{e.message}
))}
)}
Métricas de ejecución
LLM calls {run.llm_calls}
Input chars {run.input_chars.toLocaleString()}
Writer attempts {run.writer.attempts}
Final length {run.writer.final_length} palabras
Fact claims {run.fact_checker.claim_count} ({run.fact_checker.uncertain_claims.length} dudosas)
);
}
/* helpers reused */
function getStoryDeck(titulo) {
const story = window.STORY_BANK.find(s => s.titulo === titulo);
return story ? story.deck : '';
}
function renderArticleHtmlClean(run) {
// rebuild the article body without flaws (final approved version)
const story = window.STORY_BANK.find(s => s.titulo === run.editor.selected_titulo);
if (!story) return 'Article body not available.
';
const parts = [];
parts.push(`${story.body[0]}
`);
parts.push(`${story.body[1]}
`);
parts.push(`${story.body[2]}
`);
parts.push(`${story.h2} `);
for (const p of story.body2) parts.push(`${p}
`);
return parts.join('\n');
}
Object.assign(window, {
OverviewView, PipelineView, RunsView, RunDetailView,
getStoryDeck, renderArticleHtmlClean,
});