CI / changes (push) Successful in 17s
CI / commitlint (push) Has been skipped
CI / openapi (push) Successful in 26s
CI / web (push) Successful in 46s
CI / go (push) Successful in 1m1s
CI / bird2 (push) Successful in 17s
CI / release (push) Failing after 2m22s
Web UI полностью переведён с SvelteKit на новый стек: React 19, TanStack Router/Query/Table/Virtual, shadcn/ui (base-nova) и ReUI enterprise-компоненты (data-grid, filters, autocomplete). Новый код разложен по слоям: packages/ui (shadcn-примитивы), apps/web (роуты, shared-обёртки, ReUI-адаптации). BREAKING CHANGE: меняется структура и инструментинг фронтенда. - apps/web/ — новый Vite + React-проект (@evobgp/web), file-based роуты TanStack Router; экраны dashboard, modules, monitoring, network, operations, schedule, settings, tenant-settings, access, directories. - packages/ui/ — shadcn/ui-примитивы (@evobgp/ui) с общими стилями globals.css и cn-утилитой; CLI shadcn запускается из apps/web. - apps/web/src/components/reui/ — enterprise-паттерны ReUI. - pnpm workspace (pnpm-workspace.yaml, pnpm-lock.yaml, tsconfig.base.json) заменяет npm-проект в web/. - web/ переименован в web-legacy-svelte/ (архив-референс для миграции); импорты оттуда запрещены правилом WEB-22. - CI (.gitea/workflows/ci.yaml): job web переведён на Node 22 + pnpm 10 (typecheck/lint/build через pnpm --filter @evobgp/web); пути триггеров обновлены под apps/web|packages/ui. - deploy/docker/evobgp-web/Dockerfile: сборка из корня репозитория, pnpm install --frozen-lockfile, выход dist из apps/web/dist. - .cursor/rules/web-shadcn.mdc, context7-stack.mdc, engineering.mdc, AGENTS.md — обновлены под React-стек (WEB-01..WEB-22, DOC-SYNC-06/07). Проверки WEB-19 локально: typecheck, lint, build — exit 0. Co-authored-by: Cursor <[email protected]>
182 lines
5.9 KiB
TypeScript
182 lines
5.9 KiB
TypeScript
/** Режимы расписания (5-field cron, UTC) — совместимы с robfig/cron в scheduler. */
|
||
export type ScheduleMode = 'minutes' | 'hours' | 'daily' | 'weekly' | 'custom';
|
||
|
||
export type ScheduleEditor = {
|
||
mode: ScheduleMode;
|
||
/** Интервал в минутах (режим minutes). */
|
||
intervalMinutes: string;
|
||
/** Интервал в часах (режим hours). */
|
||
intervalHours: string;
|
||
minute: string;
|
||
hour: string;
|
||
/** 0 = воскресенье … 6 = суббота */
|
||
weekday: string;
|
||
customCron: string;
|
||
};
|
||
|
||
export const scheduleModeOptions: { value: ScheduleMode; label: string }[] = [
|
||
{ value: 'minutes', label: 'Каждые N минут' },
|
||
{ value: 'hours', label: 'Каждые N часов' },
|
||
{ value: 'daily', label: 'Ежедневно в указанное время' },
|
||
{ value: 'weekly', label: 'Еженедельно в указанный день' },
|
||
{ value: 'custom', label: 'Cron вручную (расширенный)' }
|
||
];
|
||
|
||
export const weekdayOptions = [
|
||
{ value: '0', label: 'Воскресенье' },
|
||
{ value: '1', label: 'Понедельник' },
|
||
{ value: '2', label: 'Вторник' },
|
||
{ value: '3', label: 'Среда' },
|
||
{ value: '4', label: 'Четверг' },
|
||
{ value: '5', label: 'Пятница' },
|
||
{ value: '6', label: 'Суббота' }
|
||
] as const;
|
||
|
||
export function defaultScheduleEditor(cron = '0 3 * * *'): ScheduleEditor {
|
||
return cronToEditor(cron);
|
||
}
|
||
|
||
function splitCron(cron: string): [string, string, string, string, string] | null {
|
||
const parts = cron.trim().split(/\s+/);
|
||
if (parts.length !== 5) return null;
|
||
return [parts[0], parts[1], parts[2], parts[3], parts[4]];
|
||
}
|
||
|
||
function clampInt(raw: string, min: number, max: number): number {
|
||
const n = Math.floor(Number(String(raw).trim()));
|
||
if (!Number.isFinite(n)) return min;
|
||
return Math.min(max, Math.max(min, n));
|
||
}
|
||
|
||
function pad2(n: number): string {
|
||
return String(n).padStart(2, '0');
|
||
}
|
||
|
||
/** Пытается разобрать cron в редактор; неизвестные выражения → custom. */
|
||
export function cronToEditor(cron: string): ScheduleEditor {
|
||
const base: ScheduleEditor = {
|
||
mode: 'custom',
|
||
intervalMinutes: '30',
|
||
intervalHours: '2',
|
||
minute: '0',
|
||
hour: '3',
|
||
weekday: '0',
|
||
customCron: cron.trim() || '0 3 * * *'
|
||
};
|
||
|
||
const parts = splitCron(cron);
|
||
if (!parts) return base;
|
||
|
||
const [min, hour, dom, month, dow] = parts;
|
||
|
||
const minEvery = min.match(/^\*\/(\d+)$/);
|
||
if (minEvery && hour === '*' && dom === '*' && month === '*' && dow === '*') {
|
||
return { ...base, mode: 'minutes', intervalMinutes: minEvery[1], customCron: cron.trim() };
|
||
}
|
||
|
||
const hourEvery = hour.match(/^\*\/(\d+)$/);
|
||
if (hourEvery && !min.includes('*') && dom === '*' && month === '*' && dow === '*') {
|
||
return {
|
||
...base,
|
||
mode: 'hours',
|
||
intervalHours: hourEvery[1],
|
||
minute: min,
|
||
customCron: cron.trim()
|
||
};
|
||
}
|
||
|
||
if (!min.includes('*') && !hour.includes('*') && dom === '*' && month === '*' && dow === '*') {
|
||
return { ...base, mode: 'daily', minute: min, hour, customCron: cron.trim() };
|
||
}
|
||
|
||
if (!min.includes('*') && !hour.includes('*') && dom === '*' && month === '*' && dow !== '*') {
|
||
return { ...base, mode: 'weekly', minute: min, hour, weekday: dow, customCron: cron.trim() };
|
||
}
|
||
|
||
return base;
|
||
}
|
||
|
||
/** Собирает 5-field cron из редактора. */
|
||
export function editorToCron(editor: ScheduleEditor): string {
|
||
switch (editor.mode) {
|
||
case 'minutes': {
|
||
const n = clampInt(editor.intervalMinutes, 1, 59);
|
||
return `*/${n} * * * *`;
|
||
}
|
||
case 'hours': {
|
||
const n = clampInt(editor.intervalHours, 1, 23);
|
||
const m = clampInt(editor.minute, 0, 59);
|
||
return `${m} */${n} * * *`;
|
||
}
|
||
case 'daily': {
|
||
const m = clampInt(editor.minute, 0, 59);
|
||
const h = clampInt(editor.hour, 0, 23);
|
||
return `${m} ${h} * * *`;
|
||
}
|
||
case 'weekly': {
|
||
const m = clampInt(editor.minute, 0, 59);
|
||
const h = clampInt(editor.hour, 0, 23);
|
||
const d = clampInt(editor.weekday, 0, 6);
|
||
return `${m} ${h} * * ${d}`;
|
||
}
|
||
case 'custom':
|
||
return editor.customCron.trim() || '0 3 * * *';
|
||
}
|
||
}
|
||
|
||
/** Человекочитаемое описание расписания для таблицы и подсказок. */
|
||
export function describeCron(cron: string): string {
|
||
const ed = cronToEditor(cron);
|
||
switch (ed.mode) {
|
||
case 'minutes': {
|
||
const n = clampInt(ed.intervalMinutes, 1, 59);
|
||
return `Каждые ${n} ${minutesLabel(n)} (UTC)`;
|
||
}
|
||
case 'hours': {
|
||
const n = clampInt(ed.intervalHours, 1, 23);
|
||
const m = clampInt(ed.minute, 0, 59);
|
||
return `Каждые ${n} ${hoursLabel(n)}, в :${pad2(m)} (UTC)`;
|
||
}
|
||
case 'daily': {
|
||
const h = clampInt(ed.hour, 0, 23);
|
||
const m = clampInt(ed.minute, 0, 59);
|
||
return `Ежедневно в ${pad2(h)}:${pad2(m)} UTC`;
|
||
}
|
||
case 'weekly': {
|
||
const wd =
|
||
weekdayOptions.find((w) => w.value === String(clampInt(ed.weekday, 0, 6)))?.label ??
|
||
ed.weekday;
|
||
const h = clampInt(ed.hour, 0, 23);
|
||
const m = clampInt(ed.minute, 0, 59);
|
||
return `Каждую ${wd.toLowerCase()} в ${pad2(h)}:${pad2(m)} UTC`;
|
||
}
|
||
case 'custom':
|
||
return `Cron: ${cron.trim()}`;
|
||
}
|
||
}
|
||
|
||
/** Синхронизирует form.schedule из редактора. */
|
||
export function applyScheduleEditor(editor: ScheduleEditor): {
|
||
editor: ScheduleEditor;
|
||
cron: string;
|
||
} {
|
||
if (editor.mode === 'custom') {
|
||
const cron = editor.customCron.trim() || '0 3 * * *';
|
||
return { editor: { ...editor, customCron: cron }, cron };
|
||
}
|
||
const cron = editorToCron(editor);
|
||
return { editor: { ...editor, customCron: cron }, cron };
|
||
}
|
||
|
||
function minutesLabel(n: number): string {
|
||
if (n % 10 === 1 && n % 100 !== 11) return 'минуту';
|
||
if (n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20)) return 'минуты';
|
||
return 'минут';
|
||
}
|
||
|
||
function hoursLabel(n: number): string {
|
||
if (n % 10 === 1 && n % 100 !== 11) return 'час';
|
||
if (n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20)) return 'часа';
|
||
return 'часов';
|
||
}
|