Ребренд Мониторинг Орбита -> Железный Егорин; accent green->amber #F59E0B; LoginScreen правая половина = Lightning (WebGL, hue 35 тёплый) под стеклянной формой; левая панель — тёплый градиент. Доступ агентам: репо публичный, push german-токеном. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
79 lines
2.8 KiB
TypeScript
79 lines
2.8 KiB
TypeScript
import type { NotifyRule, NotifySchedule } from '@/data/notify';
|
|
import type { Severity } from '@/core/types';
|
|
|
|
// Движок правил уведомлений: сопоставляет событие с правилами, объясняет ПОЧЕМУ,
|
|
// и собирает объединённый набор каналов. Чистые функции — тестируемо и предсказуемо.
|
|
|
|
export interface NEvent {
|
|
section: string;
|
|
severity: Severity;
|
|
objectId?: string;
|
|
objectName?: string;
|
|
text?: string;
|
|
}
|
|
|
|
export interface RuleMatch {
|
|
rule: NotifyRule;
|
|
reasons: string[];
|
|
}
|
|
export interface RuleMiss {
|
|
rule: NotifyRule;
|
|
why: string;
|
|
}
|
|
|
|
// Активно ли расписание правила в момент now
|
|
export function scheduleActive(schedule: NotifySchedule, now: Date): boolean {
|
|
const h = now.getHours();
|
|
const day = now.getDay(); // 0=вс … 6=сб
|
|
const workday = day >= 1 && day <= 5;
|
|
if (schedule === 'always') return true;
|
|
if (schedule === 'work') return workday && h >= 9 && h < 18;
|
|
if (schedule === 'night') return !workday || h < 9 || h >= 18;
|
|
return true;
|
|
}
|
|
|
|
export function evaluate(ev: NEvent, rules: NotifyRule[], now: Date = new Date()): {
|
|
matched: RuleMatch[];
|
|
missed: RuleMiss[];
|
|
channels: string[];
|
|
} {
|
|
const matched: RuleMatch[] = [];
|
|
const missed: RuleMiss[] = [];
|
|
|
|
for (const r of rules) {
|
|
if (!r.enabled) {
|
|
missed.push({ rule: r, why: 'правило выключено' });
|
|
continue;
|
|
}
|
|
const secOk = r.sections.length === 0 || r.sections.includes(ev.section);
|
|
const sevOk = r.severities.length === 0 || r.severities.includes(ev.severity);
|
|
const schedOk = scheduleActive(r.schedule, now);
|
|
|
|
if (!secOk) {
|
|
missed.push({ rule: r, why: `раздел «${ev.section}» не в списке правила` });
|
|
continue;
|
|
}
|
|
if (!sevOk) {
|
|
missed.push({ rule: r, why: `важность «${ev.severity}» не в списке правила` });
|
|
continue;
|
|
}
|
|
if (!schedOk) {
|
|
missed.push({ rule: r, why: 'вне расписания правила' });
|
|
continue;
|
|
}
|
|
const reasons: string[] = [
|
|
r.sections.length ? `раздел ∈ {${r.sections.join(', ')}}` : 'любой раздел',
|
|
r.severities.length ? `важность ∈ {${r.severities.join(', ')}}` : 'любая важность',
|
|
'расписание активно',
|
|
];
|
|
matched.push({ rule: r, reasons });
|
|
}
|
|
|
|
// Объединённый набор каналов (по приоритету правил)
|
|
const order = matched.slice().sort((a, b) => b.rule.priority - a.rule.priority);
|
|
const channels: string[] = [];
|
|
for (const m of order) for (const c of m.rule.channels) if (!channels.includes(c)) channels.push(c);
|
|
|
|
return { matched, missed, channels };
|
|
}
|