// Page 6 — Tasks History (timeline) function TasksPage() { const [moduleFilter, setModuleFilter] = useState("all"); const [agentFilter, setAgentFilter] = useState("all"); const [periodFilter, setPeriodFilter] = useState("week"); const [problemsOnly, setProblemsOnly] = useState(false); const [selected, setSelected] = useState(49); // PB-49 selected by default const modules = useMemo(() => { const s = new Set(TASKS.map(t => t.module)); return Array.from(s); }, []); const filtered = useMemo(() => { const now = NOW.getTime(); const cutoff = periodFilter === "day" ? now - 24 * 3600 * 1000 : periodFilter === "week" ? now - 7 * 24 * 3600 * 1000 : periodFilter === "month" ? now - 30 * 24 * 3600 * 1000 : 0; return TASKS.filter(t => { if (cutoff && new Date(t.createdAt).getTime() < cutoff) return false; if (moduleFilter !== "all" && t.module !== moduleFilter) return false; if (agentFilter !== "all" && !t.agents.includes(agentFilter)) return false; if (problemsOnly && t.status === "ok") return false; return true; }); }, [moduleFilter, agentFilter, periodFilter, problemsOnly]); const sortedByCreated = [...filtered].sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt)); const selectedTask = TASKS.find(t => t.seq === selected) || filtered[0]; return (

История задач

Жизненный цикл задач Plane. Кружки — задачи, цвет — наличие алертов.
{/* Filters */}
{["day","week","month","all"].map(p => ( ))}
В выборке: {filtered.length} / {TASKS.length}
{/* Timeline strip */}

Timeline · {periodFilter === "day" ? "за сутки" : periodFilter === "week" ? "за неделю" : periodFilter === "month" ? "за месяц" : "за всё время"}

{/* Detail */}

Задачи в выборке

{filtered.slice().reverse().map(t => ( setSelected(t.seq)} style={{ background: selected === t.seq ? "var(--surface-hi)" : "transparent", }}> ))}
PB-{t.seq}
{t.title}
{fmtDate(t.createdAt)}
{t.state}
{selectedTask && }
); } function FilterGroup({ label, children }) { return (
{label}
{children}
); } function Sep() { return
; } function Legend({ color, label }) { return ( {label} ); } function statusColor(s) { return s === "ok" ? "var(--ok)" : s === "info" ? "var(--info)" : s === "warn" ? "var(--warn)" : s === "crit" ? "var(--crit)" : "#52525b"; } function TaskTimeline({ tasks, selected, onSelect }) { if (tasks.length === 0) return
Нет задач в выборке.
; // Group by date const groups = {}; tasks.forEach(t => { const day = t.createdAt.slice(0, 10); (groups[day] = groups[day] || []).push(t); }); const days = Object.keys(groups).sort(); return (
{days.map((d) => (
{groups[d].map((t) => ( onSelect(t.seq)} /> ))}
{new Date(d).toLocaleDateString("ru-RU", { day: "2-digit", month: "short" })}
))}
); } function TimelineBubble({ task, selected, onSelect }) { const color = statusColor(task.status === "open" ? "open" : task.status); const size = task.status === "crit" ? 28 : task.status === "warn" ? 24 : 20; return ( ); } function TaskDetail({ task }) { // Mock state transitions const transitions = useMemo(() => { const created = new Date(task.createdAt); const closed = task.closedAt ? new Date(task.closedAt) : null; const span = closed ? closed - created : NOW - created; const t = (ratio) => new Date(created.getTime() + span * ratio).toISOString(); if (task.state === "Done") { return [ { state: "Backlog", at: task.createdAt, by: task.agents[0] }, { state: "Todo", at: t(0.1), by: task.agents[0] }, { state: "In Progress", at: t(0.25), by: task.agents[1] || task.agents[0] }, { state: "In Review", at: t(0.7), by: task.agents[2] || task.agents[0] }, { state: "Done", at: task.closedAt, by: task.agents[2] || task.agents[0] }, ]; } return [ { state: "Backlog", at: task.createdAt, by: task.agents[0] }, { state: "Todo", at: t(0.3), by: task.agents[0] }, { state: "In Progress", at: t(0.7), by: task.agents[1] || task.agents[0] }, ]; }, [task.seq]); const taskAlerts = ALERTS.filter(a => a.seq === task.seq); const taskEvents = EVENTS.filter(e => e.seq === task.seq); return (

PB-{task.seq} · {task.title}

Создана: {fmtDateTime(task.createdAt)} {task.closedAt && Закрыта: {fmtDateTime(task.closedAt)}} state: {task.state}
Переходы состояний
{transitions.map((tr, i) => (
{tr.state}
{fmtTime(tr.at)}
{i < transitions.length - 1 && (
)}
))}
Кто работал
{task.agents.map(aid => { const a = teamById(aid); if (!a) return null; return (
{a.name} {a.roleLabel}
); })}
Сработавшие алерты
{taskAlerts.length === 0 ? (
● Без инцидентов
) : (
{taskAlerts.map(a => (
{a.ruleTitle} {fmtTime(a.ts)}
))}
)}
{taskEvents.length > 0 && (
События
{taskEvents.map((ev, i) => (
{fmtTime(ev.ts)} {ev.text} {kindLabel(ev.kind)}
))}
)}
); } window.TasksPage = TasksPage;