// Page 4 — Rules // Загружает правила из /api/rules. Создание/удаление/toggle идут через backend. const RULE_YAML_TEMPLATE = `name: my_new_rule description: | Короткое описание для UI. severity: warning # warning | critical role: coder claudeMdRef: agents/coder/CLAUDE.md active: true trigger: event: poll # poll | state_changed | new_task | comment | heartbeat label: for-coder # фильтр по name лейбла (опц.) task_state: [Todo, Backlog] # state names (опц.) condition: age_min: 5 # держится в указанных state > N минут dedup_hours: 1.0 # не алертить чаще раз в N часов respect_baseline_cutoff: true message: "Шаблон: PB-{seq} «{title}» висит в {state} {age_min}+ минут" `; function RulesPage({ pushToast }) { const [rules, setRules] = useState(RULES); const [openRow, setOpenRow] = useState(null); const [creating, setCreating] = useState(false); // Перетягивает свежий список с backend (после изменений) const reload = async () => { try { const r = await fetch("/api/rules"); if (r.ok) { const data = await r.json(); if (Array.isArray(data)) { setRules(data); window.RULES = data; } } } catch (e) { console.warn("[rules] reload failed", e); } }; const toggle = async (name) => { const rule = rules.find((r) => r.name === name); if (!rule) return; const wasActive = rule.active; // optimistic update setRules((rs) => rs.map((r) => (r.name === name ? { ...r, active: !r.active } : r))); try { const resp = await fetch(`/api/rules/${encodeURIComponent(name)}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ active: !wasActive }), }); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); pushToast(`Правило ${name} ${wasActive ? "приостановлено" : "включено"}`, "info"); } catch (e) { // rollback setRules((rs) => rs.map((r) => (r.name === name ? { ...r, active: wasActive } : r))); pushToast(`Не удалось переключить ${name}: ${e.message}`, "crit"); } }; const remove = async (name) => { if (!window.confirm(`Удалить правило "${name}"?`)) return; try { const resp = await fetch(`/api/rules/${encodeURIComponent(name)}`, { method: "DELETE" }); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); pushToast(`Правило ${name} удалено`, "info"); setOpenRow(null); reload(); } catch (e) { pushToast(`Не удалось удалить ${name}: ${e.message}`, "crit"); } }; return (

Правила

Правила watchdog'а отслеживают активность команды. Включай / приостанавливай и смотри статистику.
{rules.map((r) => ( setOpenRow(openRow === r.name ? null : r.name)}> {openRow === r.name && ( )} ))}
Имя Описание Severity Active За 24 часа Последний trigger
{openRow === r.name ? "▾" : "▸"} {r.name} {r.description} e.stopPropagation()}> {r.active ? "active" : "paused"} 0 ? (r.severity === "critical" ? "var(--crit)" : r.severity === "warning" ? "var(--warn)" : "var(--info)") : "var(--text-muted)" }}> {r.triggers24h} {r.triggers24h > 0 && сработ.} {r.lastTriggered ? fmtAgo(r.lastTriggered) : "—"}
remove(r.name)} />
{creating && ( setCreating(false)} onCreated={(name) => { setCreating(false); pushToast(`Правило ${name} создано`, "info"); reload(); }} onError={(msg) => pushToast(msg, "crit")} /> )}
); } function NewRuleDialog({ onCancel, onCreated, onError }) { const [name, setName] = useState("my_new_rule"); const [yamlText, setYamlText] = useState(RULE_YAML_TEMPLATE); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const submit = async () => { setBusy(true); setError(null); try { const r = await fetch("/api/rules", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, yaml: yamlText }), }); const data = await r.json().catch(() => ({})); if (!r.ok) { throw new Error(data.detail || `HTTP ${r.status}`); } onCreated(name); } catch (e) { setError(e.message); onError(`Не удалось создать правило: ${e.message}`); } finally { setBusy(false); } }; return (
e.stopPropagation()} style={{ background: "var(--bg-elev)", border: "1px solid var(--border)", borderRadius: 8, padding: 20, width: "min(720px, 92vw)", maxHeight: "90vh", display: "flex", flexDirection: "column", gap: 14, }} >

Новое правило

POST /api/rules · валидация на backend через pydantic