// ════════════════════════════════════════════════════════════════ // /admin/pipeline — настройка cron + источники // ════════════════════════════════════════════════════════════════ function PipelineConfig({ currentUser }) { const A = window.SDH_ADMIN; const [pipeline, setPipeline] = React.useState(A.pipeline); const [running, setRunning] = React.useState(false); function toggleCron() { setPipeline(p => ({ ...p, cron_enabled: !p.cron_enabled })); } function setFreq(h) { setPipeline(p => ({ ...p, cron_frequency_hours: h })); } function toggleSource(id) { setPipeline(p => ({ ...p, sources: p.sources.map(s => s.id === id ? { ...s, enabled: !s.enabled } : s) })); } function runAll() { setRunning(true); setTimeout(() => setRunning(false), 2400); } // Build hour-by-hour timeseries (7 days) const hourSeries = React.useMemo(() => { const m = {}; pipeline.history.forEach(h => { const d = new Date(h.hour).toISOString().slice(0, 10); m[d] = (m[d] || 0) + h.count; }); return Object.entries(m).slice(-7).map(([date, count]) => ({ date, count })); }, [pipeline.history]); const totalNew = hourSeries.reduce((s, d) => s + d.count, 0); return (
{/* Status row */}
состояние cron
{running ? 'идёт ручной прогон' : pipeline.cron_enabled ? `работает · каждые ${pipeline.cron_frequency_hours} часов` : 'остановлен'}
последний прогон: {fmtAgo2(pipeline.last_run_at)} · добавил {pipeline.last_run_count} стартапов
s.enabled).length} / ${pipeline.sources.length}`} suffix="вкл" hue="info" />
{/* 7-day chart */}

новые стартапы за 7 дней

среднее: {Math.round(totalNew / 7)} / день
{/* Cron schedule (super_admin only) */} {currentUser.role === 'super_admin' && (

частота автозапуска

{[1, 3, 6, 12, 24].map(h => ( ))}
)} {/* Sources */}

источники сигнала

{pipeline.sources.map(s => toggleSource(s.id)} />)}
{/* Errors */} {pipeline.errors_24h.length > 0 && (

ошибки за 24 часа

{pipeline.errors_24h.map((e, i) => (
{fmtAgo2(e.time)} {e.source} {e.message}
))}
)}
); } function MiniMetric({ label, value, suffix, hue }) { return (
{label}
{value} {suffix && {suffix}}
); } function DayBars({ days }) { const max = Math.max(...days.map(d => d.count), 1); return (
{days.map(d => { const h = (d.count / max) * 100; return (
{d.count}
{d.date.slice(5)}
); })}
); } function SourceRow({ source, onToggle }) { const healthCfg = { green: { hue: 'emerald', label: 'работает' }, amber: { hue: 'amber', label: 'медленно' }, red: { hue: 'rose', label: 'ошибка' }, grey: { hue: 'info', label: 'выключен' }, }[source.health] || { hue: 'info', label: 'нет данных' }; return (
{source.name}
{source.note && (
{source.note}
)}
{source.last_run ? fmtAgo2(source.last_run) : '—'}
); } window.PipelineConfig = PipelineConfig;