// ════════════════════════════════════════════════════════════════
// /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;