/* =========================================================== 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 */}
FLOWPipeline funnel
últimas {runs.length} ejecuciones · agregado
LIVEPipeline status
{fmtClock(latest.timestamp)}
{stageList.map(s => (
{s.name}
{s.model}
{s.dur}
))}
{/* Recent runs + Anomaly */}
HISTORYÚltimos 10 ciclos
click para abrir
{runs.map(r => ( nav(`/runs/${r.id}`)}> ))}
Hora Estado Titular seleccionado Fuente Writer Att. Duración
{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.
)}
Lo más leído (mock)
  1. El BCE recorta los tipos al 2,5%
  2. Real Madrid, decimosexta Champions
  3. OpenAI presenta GPT-6
  4. El alquiler en España, récord
Sistema
Ollama bridgelocalhost:11434 Modelos cargados3 / 3 vRAM GPU14.8 / 24 GiB Last heartbeat2s ago Bridge WSCONNECTED
); } /* ------------------------- 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)}
FUNNELRun 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 => (
{r.label} {r.count}
))}
GANTTCronograma — etapas del último run
duración total {fmtDuration(lr.duration_seconds)}
HEATMAPPenalizació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 => ( ))}
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)'}} />
{filtered.map(r => ( nav(`/runs/${r.id}`)}> ))}
Timestamp Estado Titular Fuente Writer Att. Duración Fact Quality WP
{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 (
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}
Publicado en {run.publisher.success ? {run.publisher.post_url} (#{run.publisher.post_id}) : no publicado}
{/* RIGHT: editor reasoning */}
Desglose del editorial score
EDITORTop 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}
); })}
TIMELINECronograma 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, });