Files
EvoBGP/web-legacy-svelte/src/lib/maintenance/policy-schedule.ts
T
DenozordecandCursor c144b49acf
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
feat!(web): migrate UI from SvelteKit to React + shadcn/ui + ReUI
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]>
2026-07-02 17:59:59 +07:00

182 lines
5.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/** Режимы расписания (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 'часов';
}