From 5f29cfaf0fd2d52676acaf5bb60ad36412f3880a Mon Sep 17 00:00:00 2001 From: Denozordec Date: Mon, 17 Aug 2026 15:36:56 +0700 Subject: [PATCH] =?UTF-8?q?fix(ui):=20=D0=B2=D1=8B=D1=80=D0=BE=D0=B2=D0=BD?= =?UTF-8?q?=D1=8F=D1=82=D1=8C=20chrome=20Frame=20=D0=B8=20=D1=83=D0=B1?= =?UTF-8?q?=D1=80=D0=B0=D1=82=D1=8C=20Card-=D0=BE=D0=B1=D0=BE=D0=BB=D0=BE?= =?UTF-8?q?=D1=87=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Подключить App Switcher, NavUser, OpsPanel и AlertDialog вместо Card-shell. Co-authored-by: Cursor --- .cursor/rules/ma-global.mdc | 2 +- .cursor/rules/ma-ui-guardian.mdc | 30 +-- .cursor/rules/next-shadcn-production.mdc | 15 +- .gitignore | 6 + AGENTS.md | 6 + app/(main)/alerts/page.tsx | 121 ++++++---- app/(main)/backups/page.tsx | 53 +++-- app/(main)/bgp/page.tsx | 54 ++--- app/(main)/certificates/page.tsx | 50 ++-- app/(main)/communities/page.tsx | 64 +++--- app/(main)/containers/page.tsx | 41 ++-- app/(main)/dashboard/page.tsx | 178 +++++++-------- app/(main)/data-collection/page.tsx | 72 +++--- app/(main)/filters/page.tsx | 1 - app/(main)/firewall/page.tsx | 70 +++--- app/(main)/gre/page.tsx | 29 +-- app/(main)/layout.tsx | 17 +- app/(main)/ospf/page.tsx | 44 ++-- app/(main)/probes/page.tsx | 21 +- app/(main)/recursive-routes/page.tsx | 10 +- app/(main)/releases/page.tsx | 40 ++-- app/(main)/route-optimizer/page.tsx | 100 ++++---- app/(main)/servers/page.tsx | 29 +-- app/(main)/settings/page.tsx | 171 ++++++-------- app/(main)/traffic/page.tsx | 28 ++- app/(main)/uptime/page.tsx | 47 ++-- app/(main)/vxlan/page.tsx | 31 ++- app/(main)/wireguard/page.tsx | 31 ++- components.json | 7 +- components/app-sidebar.tsx | 70 +----- components/app-switcher.tsx | 97 ++++++++ components/apps-menu.tsx | 96 ++++++++ components/dashboard/internet-path-map.tsx | 24 +- components/data-page-card.tsx | 11 +- components/empty-state.tsx | 25 +- components/nav-user.tsx | 173 +++++++++----- components/ops-panel.tsx | 50 ++++ components/page-header.tsx | 18 +- components/placeholder-page.tsx | 10 +- components/reui/icon-stack.tsx | 90 ++++++++ components/reui/icon-tile.tsx | 125 ++++++++++ components/system-monitor-popover.tsx | 252 +++++++++++++++++++++ components/ui/alert-dialog.tsx | 187 +++++++++++++++ components/ui/sheet.tsx | 4 +- components/ui/sidebar.tsx | 19 +- hooks/use-app-switcher.ts | 51 +++++ lib/app-switcher-config.ts | 162 +++++++++++++ 47 files changed, 1972 insertions(+), 860 deletions(-) create mode 100644 components/app-switcher.tsx create mode 100644 components/apps-menu.tsx create mode 100644 components/ops-panel.tsx create mode 100644 components/reui/icon-stack.tsx create mode 100644 components/reui/icon-tile.tsx create mode 100644 components/system-monitor-popover.tsx create mode 100644 components/ui/alert-dialog.tsx create mode 100644 hooks/use-app-switcher.ts create mode 100644 lib/app-switcher-config.ts diff --git a/.cursor/rules/ma-global.mdc b/.cursor/rules/ma-global.mdc index bdfd9c1..07482ab 100644 --- a/.cursor/rules/ma-global.mdc +++ b/.cursor/rules/ma-global.mdc @@ -18,7 +18,7 @@ alwaysApply: false ## Эталон UI -- **`app/(main)/servers`** — главный эталон layout, сетки, типографики, композиции и взаимодействий. Не вводить новый UI-паттерн, если на `/servers` уже есть эквивалент. +- **`app/(main)/servers`** — эталон **поведения** CRUD. Оболочка ops: **Frame + DataGrid**, не Card-shell. Не копировать `DataPageCard` на новые экраны. ## Качество и совместимость diff --git a/.cursor/rules/ma-ui-guardian.mdc b/.cursor/rules/ma-ui-guardian.mdc index 9cdd3f4..7e91542 100644 --- a/.cursor/rules/ma-ui-guardian.mdc +++ b/.cursor/rules/ma-ui-guardian.mdc @@ -1,11 +1,11 @@ --- -description: "[Multi-agent] Роль UI GUARDIAN — сверка UI только с эталоном /servers" +description: "[Multi-agent] Роль UI GUARDIAN — сверка UI с CRUD /servers и оболочкой Frame+DataGrid" alwaysApply: false --- # Агент: UI GUARDIAN (хранитель UI) -Ты отвечаешь **только** за **визуальную и UX-согласованность** с эталоном **`/servers`**. +Ты отвечаешь **только** за **визуальную и UX-согласованность**. ## Язык @@ -13,31 +13,31 @@ alwaysApply: false ## Эталон -- Каталог **`app/(main)/servers`** и связанные компоненты страницы — **единственный** эталон для: - - структуры layout (области страницы, карточки, секции); - - сетки и отступов; - - типографики (заголовки, подписи, плотность текста); - - композиции shadcn-компонентов; - - паттернов взаимодействия (кнопки, таблицы, диалоги, формы — как на `/servers`). +Два слоя (не смешивать): + +1. **Поведение CRUD** — каталог **`app/(main)/servers`**: фильтры, таблица, диалоги, формы, плотность, паттерны кнопок. +2. **Оболочка ops** — **ReUI Frame + DataGrid**, не shadcn `Card` как page shell. KPI — `IconTile` `variant="elevated"` `className="size-10.5"`. Preview: [frame](https://reui.io/docs/components/base/frame) · [data-grid](https://reui.io/docs/components/base/data-grid) · [stats-12](https://reui.io/preview/base/stats-12) · [icon-tile](https://reui.io/docs/components/base/icon-tile). + +`DataPageCard` — ops list shell на ReUI Frame; новые экраны не копируют Card-shell. ## Обязанности -- Сравнить **каждое** спорное UI-решение в изменениях с тем, как сделано на `/servers`. -- Требовать **переиспользование** существующих компонентов/обёрток из этой зоны, если они покрывают задачу. +- Сравнить **каждое** спорное UI-решение: CRUD — с `/servers`; оболочка — с Frame+DataGrid. +- Требовать **переиспользование** существующих компонентов/обёрток, если они покрывают задачу. ## Правила -- **Нельзя** принимать UI, который **системно расходится** с `/servers` без явной технической необходимости (которую нужно назвать). +- **Нельзя** принимать новый Card page-shell без явной технической необходимости (назвать её). - **Нельзя** предлагать «новый стиль» ради вариации — только выравнивание с эталоном или расширение существующих примитивов. ## Формат вывода (строго) -1. **Несоответствия** эталону (конкретно: что именно отличается). +1. **Несоответствия** эталону (конкретно: CRUD vs Frame-shell). 2. **Обязательные правки** (что поменять). -3. **Что переиспользовать** с `/servers`: файлы/компоненты и паттерн. +3. **Что переиспользовать**: файлы/компоненты и паттерн. -Если расхождений нет — явно: **«Расхождений с эталоном /servers не выявлено»** и кратко перечисли проверенные аспекты. +Если расхождений нет — явно: **«Расхождений с эталоном CRUD /servers и Frame+DataGrid не выявлено»** и кратко перечисли проверенные аспекты. ## Граница ответственности -- Не дублируй полный код-ревью Implementer/Reviewer: фокус только на **консистентности с /servers** и **reuse** UI. +- Не дублируй полный код-ревью Implementer/Reviewer: фокус только на **консистентности** и **reuse** UI. diff --git a/.cursor/rules/next-shadcn-production.mdc b/.cursor/rules/next-shadcn-production.mdc index 79d7f58..e10f6b9 100644 --- a/.cursor/rules/next-shadcn-production.mdc +++ b/.cursor/rules/next-shadcn-production.mdc @@ -16,11 +16,12 @@ alwaysApply: true - Решения сверять с **официальной** документацией Next.js и shadcn/ui (актуальные версии проекта). - Не выдумывать API и «недокументированные» паттерны; предпочитать стабильные, описанные в доках решения. -## 2. Эталон UI/UX: страница `/servers` (критично) +## 2. Эталон UI/UX: `/servers` (CRUD) + Frame+DataGrid (оболочка) -- **`app/(main)/servers`** (и связанные компоненты) — **главный эталон** дизайна и поведения. -- Выравнивать: layout, отступы, сетку, типографику, структуру компонентов, паттерны взаимодействия. -- Переиспользовать оттуда же компоненты и паттерны; **не вводить новый UI-паттерн**, если эквивалент уже есть на `/servers`. +- **`app/(main)/servers`** — эталон **поведения** CRUD: фильтры, сортировка, пагинация, диалоги, формы, плотность. +- **Оболочка ops-списков / dashboard / KPI:** **ReUI Frame + DataGrid**, не shadcn `Card` как page shell. Preview: [frame](https://reui.io/docs/components/base/frame) · [data-grid](https://reui.io/docs/components/base/data-grid) · [stats-12](https://reui.io/preview/base/stats-12) · [icon-tile](https://reui.io/docs/components/base/icon-tile). +- `DataPageCard` — ops list shell на ReUI Frame (`components/data-page-card.tsx`). Внутренние панели — `OpsPanel` / Frame, не shadcn Card как page shell. +- Переиспользовать паттерны `/servers` для CRUD; **не** вводить Card-grid как новый SoT. ## 3. Большой репозиторий (критично) @@ -50,7 +51,7 @@ alwaysApply: true В ответе явно указать: -- **Почему** это согласуется с Next.js, shadcn/ui и эталоном **`/servers`**. +- **Почему** это согласуется с Next.js, shadcn/ui, CRUD `/servers` и оболочкой Frame+DataGrid. - **Server vs Client Component** и обоснование. - Что **переиспользовано** из проекта (паттерны/компоненты). - Какие **файлы проанализированы**. @@ -68,7 +69,7 @@ alwaysApply: true 1. **Шаг 0:** список проанализированных файлов. 2. **Шаг 1:** что изменено. -3. **Шаг 2:** почему (Next.js + shadcn/ui + `/servers` + паттерны кодовой базы). +3. **Шаг 2:** почему (Next.js + shadcn/ui + CRUD `/servers` + Frame+DataGrid + паттерны кодовой базы). 4. **Шаг 3:** анализ влияния (что ещё затронуто). 5. **Шаг 4:** код (диффы или несколько файлов). 6. **Шаг 5:** проверка dev/консоли (ошибки, предупреждения, исправления). @@ -80,4 +81,4 @@ alwaysApply: true ## Цель -Вести себя как **senior** в production-кодовой базе: консистентность, переиспользование, выравнивание с `/servers`, безопасные масштабируемые изменения по best practices Next.js и shadcn/ui, готовый к продакшену код без ошибок сборки/рантайма по возможности. +Вести себя как **senior** в production-кодовой базе: консистентность, переиспользование, CRUD как на `/servers`, оболочка Frame+DataGrid (не Card-shell), безопасные масштабируемые изменения по best practices Next.js и shadcn/ui, готовый к продакшену код без ошибок сборки/рантайма по возможности. diff --git a/.gitignore b/.gitignore index 0f41946..ea24376 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,9 @@ Thumbs.db tmp/ temp/ .ci/docker/ +.claude/settings.local.json + +# Local MCP configs (may contain REUI license Bearer) +.cursor/mcp.json +.mcp.json +opencode.json diff --git a/AGENTS.md b/AGENTS.md index 4d2bab5..0fe66d1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,3 +14,9 @@ ## Локальный запуск Два процесса из корня: `npm run dev` (frontend :3000) и `npm --prefix backend run dev` (backend :8000). Перед первым запуском — `npm install`, для backend — `backend/.env` из `backend/.env.example`. Подробности — `.cursor/rules/dev-run-command.mdc` и `README.md`. + +## ReUI + +Skill: `.claude/skills/reui` / `.cursor/skills/reui` — версия `668fb463eb` (20 free components). +Docs: [Introduction](https://reui.io/docs) · [llms.txt](https://reui.io/llms.txt) · [MCP](https://reui.io/docs/mcp) · [Agent Skills](https://reui.io/docs/agent-skills) · [Cursor](https://reui.io/docs/cursor). +Обновление skill: `curl.exe -fsSL https://mcp.reui.io/install | node -` из корня проекта. diff --git a/app/(main)/alerts/page.tsx b/app/(main)/alerts/page.tsx index 908e25a..24cb09b 100644 --- a/app/(main)/alerts/page.tsx +++ b/app/(main)/alerts/page.tsx @@ -4,11 +4,21 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react" import { useDataSource } from "@/lib/data-source" import { PageHeader } from "@/components/page-header" import { FormToggle } from "@/components/form-kit" -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { OpsPanel } from "@/components/ops-panel" import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible" import { Separator } from "@/components/ui/separator" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog" import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetFooter, } from "@/components/ui/sheet" @@ -841,12 +851,9 @@ function TelegramCard({ cfg, onChange, tokenConfigured, liveSaveBusy, onSaveTele } return ( - - - - - ✈️ Telegram - + ✈️ Telegram} + headerRight={ {cfg.connected ? "Подключён" : "Не настроен"} - - - + } + contentClassName="px-4 pb-4 flex flex-col gap-3" + > {/* bot token */}
@@ -943,8 +950,7 @@ function TelegramCard({ cfg, onChange, tokenConfigured, liveSaveBusy, onSaveTele : testResult === "fail" ? "Ошибка отправки" : "Отправить тестовое сообщение"} - - + ) } @@ -952,11 +958,7 @@ function TelegramCard({ cfg, onChange, tokenConfigured, liveSaveBusy, onSaveTele function HistoryCard({ entries }: { entries: HistoryEntry[] }) { return ( - - - Журнал срабатываний - - + {entries.length === 0 ? (

Нет срабатываний

) : ( @@ -983,8 +985,7 @@ function HistoryCard({ entries }: { entries: HistoryEntry[] }) { ))}
)} -
-
+ ) } @@ -2363,6 +2364,7 @@ export default function AlertsPage() { const [meta, setMeta] = useState(null) const [sheetOpen, setSheetOpen] = useState(false) + const [pendingPresetReplaceId, setPendingPresetReplaceId] = useState(null) const [sheetSnap, setSheetSnap] = useState(null) const [sheetEditId, setSheetEditId] = useState(null) const [sheetKey, setSheetKey] = useState(0) @@ -2661,9 +2663,6 @@ export default function AlertsPage() { const applyPresetReplace = useCallback( (presetId: string) => { - if (typeof window !== "undefined" && !window.confirm("Заменить все текущие правила выбранным пресетом?")) { - return - } const def = ALERT_PRESETS.find((p) => p.id === presetId) if (!def) return const built = def.build(serverNamesForPresets) @@ -2947,11 +2946,7 @@ export default function AlertsPage() {
- - - Пресеты - - +

Быстро добавить набор правил из каталога серверов (демо или live meta).

@@ -2979,19 +2974,18 @@ export default function AlertsPage() { variant="outline" className="h-7 text-xs" disabled={rulesSaveBusy || (isLive && backendStatus === false)} - onClick={() => applyPresetReplace(p.id)} + onClick={() => setPendingPresetReplaceId(p.id)} > Заменить все
))} - - + - - - Группы ANY / ALL + Группа - - + } + contentClassName="px-4 pb-4 flex flex-col gap-3" + > {groups.length === 0 ? (

Групп пока нет. Создайте группу и назначьте правилам её в форме правила — движок отправит одно сообщение по логике ANY или ALL. @@ -3074,8 +3069,7 @@ export default function AlertsPage() { )) )} - - + @@ -3085,13 +3079,14 @@ export default function AlertsPage() {

{/* ── rules card ── */} - - -
- - Правила оповещения - {rulesSaveBusy && } - + + Правила оповещения + {rulesSaveBusy && } + + } + headerRight={ -
-
- - + } + contentClassName="px-0 pb-0 pt-2" + > {filteredRules.length === 0 ? (
@@ -3138,8 +3132,7 @@ export default function AlertsPage() { Добавить правило
-
-
+ {/* ── right sidebar ── */}
@@ -3172,6 +3165,34 @@ export default function AlertsPage() { onTestRuleTelegram={isLive ? testRuleTelegramFromForm : undefined} testTelegramDisabled={isLive && backendStatus === false} /> + + { + if (!open) setPendingPresetReplaceId(null) + }} + > + + + Заменить все правила? + + Текущие правила будут заменены выбранным пресетом. Это нельзя отменить. + + + + Отмена + { + if (pendingPresetReplaceId) applyPresetReplace(pendingPresetReplaceId) + setPendingPresetReplaceId(null) + }} + > + Заменить + + + +
) } diff --git a/app/(main)/backups/page.tsx b/app/(main)/backups/page.tsx index 1be708e..bdc6a9a 100644 --- a/app/(main)/backups/page.tsx +++ b/app/(main)/backups/page.tsx @@ -5,10 +5,12 @@ import { PageHeader } from "@/components/page-header" import { DataPageToolbar } from "@/components/data-page-toolbar" import { BackupsDataGrid } from "@/components/data-grids/backups-data-grid" import { FileImportDialog } from "@/components/file-import-dialog" -import { FormField, FormToggle, SectionTitle, SegmentedControl } from "@/components/form-kit" +import { FormField, FormToggle, SegmentedControl } from "@/components/form-kit" import { StatusBadge } from "@/components/status-badge" import type { Backup, Server } from "@/lib/data" -import { Card, CardContent } from "@/components/ui/card" +import { Frame, FramePanel } from "@/components/reui/frame" +import { IconTile } from "@/components/reui/icon-tile" +import { OpsPanel } from "@/components/ops-panel" import { DataPageCard } from "@/components/data-page-card" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" @@ -349,15 +351,17 @@ export default function BackupsPage() { { label: "Вручную", value: manualCount, icon: }, { label: "Серверов охвачено",value: serverCount, icon: }, ].map((s) => ( - - -
-

{s.label}

-

{s.value}

+ + + +
+

{s.label}

+

{s.value}

- {s.icon} - - +
+ ))}
@@ -427,9 +431,7 @@ export default function BackupsPage() {
{/* Расписание */} - - - }>Расписание +
@@ -520,13 +522,10 @@ export default function BackupsPage() {
-
-
+ {/* Хранилище */} - - - }>Хранилище + Файлы: {storage.localPath || "/var/backup/mikrotik"}/{""}_{"{date}"}.{schedule.format}

)}
-
-
+ {/* Серверы */} - - -
- }>Серверы для бэкапа + @@ -628,7 +626,9 @@ export default function BackupsPage() {
-
+ } + contentClassName="px-5 py-5 flex flex-col gap-4" + >
{liveServers.map((s) => { @@ -662,8 +662,7 @@ export default function BackupsPage() {

Выбрано {selectedServers.size} из {liveServers.length} серверов

- - + {/* Save button */}
diff --git a/app/(main)/bgp/page.tsx b/app/(main)/bgp/page.tsx index 3423524..ebd5153 100644 --- a/app/(main)/bgp/page.tsx +++ b/app/(main)/bgp/page.tsx @@ -9,7 +9,8 @@ import { applyReuiFilters } from "@/lib/data-filters/apply-reui-filters" import { BGP_FILTER_FIELDS } from "@/lib/data-filters/bgp-filter-fields" import type { BgpSessionRow, BgpState, BgpType } from "@/lib/bgp/types" import { BGP_AS_NAMES } from "@/lib/bgp/types" -import { Card, CardContent } from "@/components/ui/card" +import { Frame, FramePanel } from "@/components/reui/frame" +import { OpsPanel } from "@/components/ops-panel" import { DataPageCard } from "@/components/data-page-card" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" @@ -359,7 +360,8 @@ function RoutersTab({ sessions }: { sessions: BgpSession[] }) { const totalRx = router.sessions.reduce((a, s) => a + s.prefixesRx, 0) return ( - + + {/* header */}
@@ -424,7 +426,8 @@ function RoutersTab({ sessions }: { sessions: BgpSession[] }) {
)} -
+ + ) })}
@@ -467,20 +470,18 @@ function AnalyticsTab({ sessions }: { sessions: BgpSession[] }) { { label: "eBGP сессий", value: ebgpSessions, color: "" }, { label: "iBGP сессий", value: ibgpSessions, color: "" }, ].map(s => ( - - -

{s.label}

-

{s.value}

-
-
+ + +

{s.label}

+

{s.value}

+
+ ))}
{/* prefixes by peer — horizontal bar chart */} - - -

Топ-8 пиров по полученным префиксам

+
{topPeers.map((s, i) => { const pct = (s.prefixesRx / maxRx) * 100 @@ -516,15 +517,12 @@ function AnalyticsTab({ sessions }: { sessions: BgpSession[] }) {

Нет данных о префиксах

)}
-
-
+ {/* right column */}
{/* state distribution */} - - -

Распределение состояний

+ {stateCounts.length === 0 ? (

Нет данных

) : ( @@ -545,13 +543,10 @@ function AnalyticsTab({ sessions }: { sessions: BgpSession[] }) { })}
)} - - + {/* eBGP vs iBGP */} - - -

eBGP vs iBGP

+ {sessions.length === 0 ? (

Нет данных

) : ( @@ -592,8 +587,7 @@ function AnalyticsTab({ sessions }: { sessions: BgpSession[] }) {
)} -
-
+ @@ -739,12 +733,12 @@ export default function BgpPage() { { label: "Не установлено", value: notEstab, color: notEstab > 0 ? "text-amber-500" : "text-muted-foreground" }, { label: "Получено префиксов", value: fmtNum(totalRx),color: "" }, ].map(s => ( - - -

{s.label}

-

{s.value}

-
-
+ + +

{s.label}

+

{s.value}

+
+ ))} diff --git a/app/(main)/certificates/page.tsx b/app/(main)/certificates/page.tsx index 3a06904..a06dacf 100644 --- a/app/(main)/certificates/page.tsx +++ b/app/(main)/certificates/page.tsx @@ -7,7 +7,9 @@ import { FileImportDialog } from "@/components/file-import-dialog" import { routerCertificates, servers as mockServers } from "@/lib/data" import type { CertStatus, Server } from "@/lib/data" import type { CertificateDto } from "@mmapp/contracts/certificates" -import { Card, CardContent } from "@/components/ui/card" +import { Frame, FramePanel } from "@/components/reui/frame" +import { IconTile } from "@/components/reui/icon-tile" +import { OpsPanel } from "@/components/ops-panel" import { DataPageCard } from "@/components/data-page-card" import { DataPageToolbar } from "@/components/data-page-toolbar" import { CertificatesDataGrid } from "@/components/data-grids/certificates-data-grid" @@ -140,15 +142,17 @@ function CertPartKpi({ icon: , }, ].map((s) => ( - - -
-

{s.label}

-

{s.value}

+ + + +
+

{s.label}

+

{s.value}

-
{s.icon}
- - +
+ ))}
) @@ -178,12 +182,11 @@ function CertPartAcmeSettings({ onSave: () => void }) { return ( - - -

ACME · Cloudflare DNS-01

-

- Публичные Let's Encrypt для зон в Cloudflare выпускаются на backend и импортируются на RouterOS 7.22+. -

+
- - + ) } function CertPartReference() { return ( - - -

- RouterOS 7.22+ · публичные LE для Cloudflare через backend DNS-01, не через /certificate add-acme на устройстве. -

-

RouterOS 7 · /certificate — справка CLI

+
{[ { @@ -284,8 +285,7 @@ function CertPartReference() {
))} -
-
+ ) } diff --git a/app/(main)/communities/page.tsx b/app/(main)/communities/page.tsx index 5ca0e30..5044505 100644 --- a/app/(main)/communities/page.tsx +++ b/app/(main)/communities/page.tsx @@ -10,9 +10,8 @@ import { ACTION_LABELS, ACTION_COLOR, } from "@/components/data-grids/communities-data-grid" -import { - Card, CardContent, CardHeader, CardTitle, CardDescription, -} from "@/components/ui/card" +import { Frame, FramePanel } from "@/components/reui/frame" +import { OpsPanel } from "@/components/ops-panel" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { @@ -169,12 +168,12 @@ export default function CommunitiesPage() { { label: "Стандартных", value: String(listData.filter(c => c.type === "standard").length) }, { label: "Использует фильтры",value: String(new Set(listData.flatMap(c => c.filterIds)).size) }, ].map(({ label, value }) => ( - - -

{label}

-

{value}

-
-
+ + +

{label}

+

{value}

+
+ ))} @@ -211,30 +210,28 @@ export default function CommunitiesPage() { {/* ── detail panel ── */} {selected ? ( - - -
-
-
- - {selected.enabled ? "Активен" : "Выключен"} - -
- {selected.value} - {selected.name} -
+ {selected.value}} + description={selected.name} + headerRight={
+ } + contentClassName="flex flex-col gap-4 text-sm px-5 pb-5" + > +
+ + {selected.enabled ? "Активен" : "Выключен"} +
- -

{selected.description}

@@ -272,15 +269,14 @@ export default function CommunitiesPage() { Скопировать значение
-
- +
) : ( - - + +

Выберите community
для просмотра деталей

-
-
+ + )}
diff --git a/app/(main)/containers/page.tsx b/app/(main)/containers/page.tsx index 86c7f61..2c6339a 100644 --- a/app/(main)/containers/page.tsx +++ b/app/(main)/containers/page.tsx @@ -5,7 +5,9 @@ import { PageHeader } from "@/components/page-header" import { routerContainers, servers } from "@/lib/data" import type { RouterContainer } from "@/lib/data" import { Flag } from "@/components/flag" -import { Card, CardContent } from "@/components/ui/card" +import { Frame, FramePanel } from "@/components/reui/frame" +import { IconTile } from "@/components/reui/icon-tile" +import { OpsPanel } from "@/components/ops-panel" import { Button } from "@/components/ui/button" import { cn } from "@/lib/utils" import { @@ -169,7 +171,8 @@ function ContainerCard({ const cfg = statusConfig(container.status) return ( - + +
@@ -201,7 +204,7 @@ function ContainerCard({
- +
{/* image */}
@@ -273,8 +276,9 @@ function ContainerCard({ {container.comment && (

{container.comment}

)} - - +
+ + ) } @@ -323,15 +327,17 @@ export default function ContainersPage() { { label: "Stopped", value: stopped, icon: }, { label: "Ошибок", value: errors, icon: }, ].map((s) => ( - - -
-

{s.label}

-

{s.value}

+ + + +
+

{s.label}

+

{s.value}

-
{s.icon}
- - +
+ ))}
@@ -397,11 +403,7 @@ export default function ContainersPage() { )} {/* RouterOS reference */} - - -

- RouterOS 7.4+ · /container — быстрые команды -

+
{[ { @@ -457,8 +459,7 @@ export default function ContainersPage() {
))}
-
- +
diff --git a/app/(main)/dashboard/page.tsx b/app/(main)/dashboard/page.tsx index d8dd0cc..10b44db 100644 --- a/app/(main)/dashboard/page.tsx +++ b/app/(main)/dashboard/page.tsx @@ -1,10 +1,12 @@ "use client" -import { useCallback, useEffect, useMemo, useState } from "react" +import { useCallback, useEffect, useMemo, useState, type ReactNode } from "react" import { usePathname } from "next/navigation" import Link from "next/link" import { PageHeader } from "@/components/page-header" -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { OpsPanel } from "@/components/ops-panel" +import { Frame, FramePanel } from "@/components/reui/frame" +import { IconTile } from "@/components/reui/icon-tile" import { StatusDot } from "@/components/status-dot" import { StatusBadge } from "@/components/status-badge" import { Sparkline } from "@/components/sparkline" @@ -22,7 +24,7 @@ import type { PingProbe, Server, ServerStatus, ServerType } from "@/lib/data" import type { GreTunnel } from "@/lib/data" import { useDataSource } from "@/lib/data-source" import { Flag } from "@/components/flag" -import { AlertCircleIcon, AlertTriangleIcon, InfoIcon, DownloadIcon } from "lucide-react" +import { AlertCircleIcon, AlertTriangleIcon, BellIcon, FilterIcon, GitMergeIcon, InfoIcon, DownloadIcon, ServerIcon } from "lucide-react" import { Button, buttonVariants } from "@/components/ui/button" import { DataPageCard } from "@/components/data-page-card" import { DashboardActiveProbesDataGrid } from "@/components/data-grids/dashboard-active-probes-data-grid" @@ -46,31 +48,45 @@ function makeApiFetch(backendUrl: string) { } function StatCard({ - label, value, unit, delta, deltaDir, spark, sparkColor, + label, value, unit, delta, deltaDir, spark, sparkColor, icon, }: { label: string; value: string; unit?: string; delta?: string deltaDir?: "up" | "down"; spark?: number[]; sparkColor?: string + icon?: ReactNode }) { return ( - - -

{label}

-
- {value} - {unit && {unit}} + + +
+ {icon ? ( + + ) : null} +
+

{label}

+
+ {value} + {unit ? {unit} : null} +
+ {delta ? ( +

+ {delta} +

+ ) : null} +
- {delta && ( -

- {delta} -

- )} - {spark && spark.length > 1 && ( + {spark && spark.length > 1 ? (
- )} - - + ) : null} +
+ ) } @@ -719,6 +735,7 @@ export default function DashboardPage() { deltaDir={dashboardKpi.servers.deltaDir} spark={dashboardKpi.servers.spark} sparkColor={dashboardKpi.servers.sparkColor} + icon={} /> } /> } /> } />
{/* Latency chart + Events */}
- - -
-
- Задержка до серверов -

- {latencyChartBlock.kind === "mock" && latencyChartBlock.subtitle} - {latencyChartBlock.kind === "live" && latencyChartBlock.subtitle} - {latencyChartBlock.kind === "loading" && "Загрузка…"} - {latencyChartBlock.kind === "empty" && "Нет серии RTT для графика"} -

-
-
-
- + {latencyChartBlock.kind === "loading" && (
)} - - + - - -
- Последние события + Все → -
-

Система и BGP-активность

-
- + } + >
{eventsLoading && recentEvents.length === 0 && (
Загрузка событий...
@@ -825,48 +838,41 @@ export default function DashboardPage() {
))}
-
-
+
{/* Bandwidth + Server status */}
- - -
-
- Пропускная способность -

Суммарный RX / TX по всем серверам

-
+ RX 318 Мбит/с TX 244 Мбит/с
-
-
- + } + contentClassName="px-3 pb-3" + > - -
+ - - -
-
- Состояние серверов -

- {serverStatusModel.subtitle} -

-
+ + {serverStatusModel.subtitle} + + } + headerRight={ Управление → -
-
- + } + >
{serverStatusModel.kind === "loading" && ( <> @@ -916,8 +922,7 @@ export default function DashboardPage() {
))} -
-
+
@@ -933,28 +938,22 @@ export default function DashboardPage() {
{/* Ping probes table */} - - -
-
- Активные пробы -

- {probesSubtitle} -

-
+ + {probesSubtitle} + + } + headerRight={ Открыть монитор → -
-
- + } + > - -
+ diff --git a/app/(main)/data-collection/page.tsx b/app/(main)/data-collection/page.tsx index da7a22b..d03085e 100644 --- a/app/(main)/data-collection/page.tsx +++ b/app/(main)/data-collection/page.tsx @@ -6,7 +6,9 @@ import { PageHeader } from "@/components/page-header" import { FormToggle } from "@/components/form-kit" import { Badge } from "@/components/ui/badge" import { Button, buttonVariants } from "@/components/ui/button" -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Frame, FramePanel } from "@/components/reui/frame" +import { IconTile } from "@/components/reui/icon-tile" +import { OpsPanel } from "@/components/ops-panel" import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" import { Collapsible, @@ -1032,30 +1034,24 @@ export default function DataCollectionPage() {
{!prefsHydrated && ( - - - Загрузка настроек подключения - - Читаем режим данных и адрес API из локальных настроек. - - - + +
+ )} {prefsHydrated && !isLive && ( - - - Нужен live-режим - - Планировщик и журнал читаются только с бекенда. Включите «Живые» данные и проверьте URL бекенда в настройках. - - - + Открыть настройки - - + )} {isLive && collectorError && ( @@ -1070,16 +1066,18 @@ export default function DataCollectionPage() { <>
{stats.map((s) => ( - - -
-

{s.label}

-

{s.value}

+ + + +
+

{s.label}

+

{s.value}

{s.sub}

-
{s.icon}
- - +
+ ))}
@@ -1203,14 +1201,10 @@ export default function DataCollectionPage() {
- - -
- Журнал прогонов - - SQLite `scheduler_runs` — до 80 записей; раскройте строку для полей и текста ошибки. - -
+
- - + } + contentClassName="px-0 pb-0" + > {schedulerRuns.length === 0 ? (

Пока нет прогонов

) : ( @@ -1287,8 +1282,7 @@ export default function DataCollectionPage() { ))}
)} - - + )}
diff --git a/app/(main)/filters/page.tsx b/app/(main)/filters/page.tsx index 25d66ed..ddf5da2 100644 --- a/app/(main)/filters/page.tsx +++ b/app/(main)/filters/page.tsx @@ -10,7 +10,6 @@ import { type RecursiveRouteLite, } from "@/components/data-grids/filters-data-grid" import { DataPageCard } from "@/components/data-page-card" -import { Card } from "@/components/ui/card" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Separator } from "@/components/ui/separator" diff --git a/app/(main)/firewall/page.tsx b/app/(main)/firewall/page.tsx index b203dfb..e5434e9 100644 --- a/app/(main)/firewall/page.tsx +++ b/app/(main)/firewall/page.tsx @@ -12,7 +12,9 @@ import { DataPageCard } from "@/components/data-page-card" import { DataPageToolbarFrame } from "@/components/data-page-toolbar" import { FormField, FormToggle, SectionTitle, SegmentedControl } from "@/components/form-kit" import { firewallRules, type FirewallRule } from "@/lib/data" -import { Card, CardContent } from "@/components/ui/card" +import { Frame, FramePanel } from "@/components/reui/frame" +import { IconTile } from "@/components/reui/icon-tile" +import { OpsPanel } from "@/components/ops-panel" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { @@ -36,7 +38,7 @@ import { CheckIcon, PowerIcon, CheckCircleIcon, PlayIcon, SquareIcon, RotateCcwIcon, ZapIcon, CheckCircle2Icon, XCircleIcon, MinusCircleIcon, SkipForwardIcon, - PackageIcon, SlidersHorizontalIcon, + SlidersHorizontalIcon, } from "lucide-react" // ─── Types ──────────────────────────────────────────────────────────────────── @@ -725,7 +727,8 @@ function AddressListsTab({ entries, onAdd }: { {/* groups */}
{Object.entries(grouped).map(([listName, listEntries]) => ( - + +
@@ -765,7 +768,8 @@ function AddressListsTab({ entries, onAdd }: {
))}
-
+ + ))}
@@ -1309,14 +1313,7 @@ function SimulatorTab({ rules: allRules }: { rules: FirewallRule[] }) {
{/* ── Presets + Scenarios card ─────────────────────────────────────────── */} - - - - {/* Built-in traffic presets */} -
-

- Пресеты трафика -

+
{SIM_PRESETS.map(p => ( ))}
-
@@ -1400,17 +1396,11 @@ function SimulatorTab({ rules: allRules }: { rules: FirewallRule[] }) {
)}
- - + {/* ── Packet editor ───────────────────────────────────────────────────── */} - - - -
-

- Параметры пакета -

+ +
- - + {/* ── Verdict banner ──────────────────────────────────────────────────── */} {verdict && simState === "done" && verdictColors && ( @@ -1568,7 +1557,8 @@ function SimulatorTab({ rules: allRules }: { rules: FirewallRule[] }) { {/* ── Trace list ──────────────────────────────────────────────────────── */} {steps.length > 0 && ( - + +

Трассировка · цепочка: {packet.chain} @@ -1608,7 +1598,8 @@ function SimulatorTab({ rules: allRules }: { rules: FirewallRule[] }) { ) })}

-
+ + )} {/* ── Empty state ─────────────────────────────────────────────────────── */} @@ -1745,15 +1736,17 @@ export default function FirewallPage() { { label: "Блокирующих", value: dropRules, icon: }, { label: "Срабатываний", value: fmtHits(totalHits), icon: }, ].map((s) => ( - - -
-

{s.label}

-

{s.value}

+ + + +
+

{s.label}

+

{s.value}

-
{s.icon}
- - +
+ ))}
@@ -1829,11 +1822,7 @@ export default function FirewallPage() { )} {/* RouterOS reference */} - - -

- RouterOS 7.20+ · /ip firewall + /ipv6 firewall — цепочки и новые матчеры -

+
{[ { @@ -1874,8 +1863,7 @@ export default function FirewallPage() {
))}
- - +
diff --git a/app/(main)/gre/page.tsx b/app/(main)/gre/page.tsx index 5b0e7b2..75e7e59 100644 --- a/app/(main)/gre/page.tsx +++ b/app/(main)/gre/page.tsx @@ -13,7 +13,9 @@ import { useDataSource } from "@/lib/data-source" import { requestJson } from "@/shared/api/http-client" import { cn } from "@/lib/utils" import { toast } from "sonner" -import { Card, CardContent } from "@/components/ui/card" +import { Frame, FramePanel } from "@/components/reui/frame" +import { IconTile } from "@/components/reui/icon-tile" +import { OpsPanel } from "@/components/ops-panel" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { @@ -430,15 +432,17 @@ export default function GrePage() { { label: "Защищены IPsec", value: ipsecCount, icon: }, { label: "IP-пулов", value: displayPools.length, icon: }, ].map((s) => ( - - -
-

{s.label}

-

{s.value}

+ + + +
+

{s.label}

+

{s.value}

-
{s.icon}
- - +
+ ))}
@@ -519,9 +523,7 @@ export default function GrePage() { )} {/* RouterOS reference */} - - -

RouterOS 7.20+ — параметры GRE-интерфейса

+
{[ ["/interface gre add", ""], @@ -544,8 +546,7 @@ export default function GrePage() {
))} -
-
+ diff --git a/app/(main)/layout.tsx b/app/(main)/layout.tsx index 1da8fba..d6e09ba 100644 --- a/app/(main)/layout.tsx +++ b/app/(main)/layout.tsx @@ -1,3 +1,4 @@ +import type { CSSProperties, ReactNode } from "react" import { AppSidebar } from "@/components/app-sidebar" import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar" import { CommandPalette } from "@/components/command-palette" @@ -5,13 +6,23 @@ import { ReleaseNotesModal } from "@/components/release-notes-modal" import { DataSourceProvider } from "@/lib/data-source" import { EvoBGPProvider } from "@/lib/evobgp-context" -export default function MainLayout({ children }: { children: React.ReactNode }) { +const SKIP_TO_CONTENT_CLASS = + "bg-background text-foreground ring-ring sr-only focus:not-sr-only focus:absolute focus:top-2 focus:left-2 focus:z-50 focus:rounded-md focus:px-3 focus:py-2 focus:text-sm focus:font-medium focus:shadow-sm focus:ring-2" + +export default function MainLayout({ children }: { children: ReactNode }) { return ( - + + + К содержимому + - {children} + + {children} + diff --git a/app/(main)/ospf/page.tsx b/app/(main)/ospf/page.tsx index 9c55885..1a22801 100644 --- a/app/(main)/ospf/page.tsx +++ b/app/(main)/ospf/page.tsx @@ -6,7 +6,7 @@ import { DataPageCard } from "@/components/data-page-card" import { OspfNeighborsDataGrid } from "@/components/data-grids/ospf-neighbors-data-grid" import { OspfRoutesDataGrid, routeTypeClass } from "@/components/data-grids/ospf-routes-data-grid" import { OspfBfdDataGrid } from "@/components/data-grids/ospf-bfd-data-grid" -import { Card, CardContent } from "@/components/ui/card" +import { Frame, FramePanel } from "@/components/reui/frame" import { Button } from "@/components/ui/button" import { Separator } from "@/components/ui/separator" import { toast } from "sonner" @@ -809,7 +809,8 @@ function InterfacesTab({ {grouped.map(router => { const totalIfaces = router.areas.reduce((s, a) => s + a.items.length, 0) return ( - + +
@@ -873,7 +874,8 @@ function InterfacesTab({
))} -
+ + ) })} @@ -923,12 +925,12 @@ function NeighborsTab({ { label: "Full", value: fullCount, color: "text-[var(--status-online-fg)]" }, { label: "Не Full", value: neighbors.length - fullCount, color: neighbors.length - fullCount > 0 ? "text-[var(--status-degraded-fg)]" : "text-muted-foreground" }, ].map(s => ( - - -

{s.label}

-

{s.value}

-
-
+ + +

{s.label}

+

{s.value}

+
+ ))} @@ -1047,12 +1049,12 @@ function BfdTab({ sessions }: { sessions: BfdSession[] }) { { label: "Down / Admin", value: downCount, color: downCount > 0 ? "text-[var(--status-offline-fg)]" : "text-muted-foreground" }, { label: "Init / другие", value: initCount, color: initCount > 0 ? "text-[var(--status-degraded-fg)]" : "text-muted-foreground" }, ].map(s => ( - - -

{s.label}

-

{s.value}

-
-
+ + +

{s.label}

+

{s.value}

+
+ ))} @@ -1350,12 +1352,12 @@ export default function OspfPage() { { label: "Интерфейсов", value: totalInterfaces }, { label: "Зон (Area)", value: totalAreas }, ].map(s => ( - - -

{s.label}

-

{s.value}

-
-
+ + +

{s.label}

+

{s.value}

+
+ ))} diff --git a/app/(main)/probes/page.tsx b/app/(main)/probes/page.tsx index aa6020d..d80f7e8 100644 --- a/app/(main)/probes/page.tsx +++ b/app/(main)/probes/page.tsx @@ -3,7 +3,8 @@ import { useEffect, useRef, useState, useMemo, useCallback } from "react" import { PageHeader } from "@/components/page-header" import { FormToggle } from "@/components/form-kit" -import { Card, CardContent } from "@/components/ui/card" +import { Frame, FramePanel } from "@/components/reui/frame" +import { OpsPanel } from "@/components/ops-panel" import { DataPageCard } from "@/components/data-page-card" import { ProbesScheduleDataGrid, @@ -516,7 +517,8 @@ function ScheduleTab({ /> {showAdd ? ( - + +
Новое правило
@@ -552,7 +554,8 @@ function ScheduleTab({ -
+ + ) : ( -
-
+ {/* ── tabs ── */}
@@ -1102,7 +1103,8 @@ export default function ProbesPage() { {tests.map(test => { const { Icon, color, label } = TOOL_META[test.tool] return ( - + + {/* header */}
@@ -1149,7 +1151,8 @@ export default function ProbesPage() {
- + + ) })}
diff --git a/app/(main)/recursive-routes/page.tsx b/app/(main)/recursive-routes/page.tsx index b501b42..c6c98c9 100644 --- a/app/(main)/recursive-routes/page.tsx +++ b/app/(main)/recursive-routes/page.tsx @@ -9,7 +9,7 @@ import { } from "@/components/data-grids/recursive-routes-data-grid" import { FormField, SectionTitle } from "@/components/form-kit" import { DataPageCard } from "@/components/data-page-card" -import { Card } from "@/components/ui/card" +import { Frame, FramePanel } from "@/components/reui/frame" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Sheet, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle } from "@/components/ui/sheet" @@ -651,9 +651,11 @@ export default function RecursiveRoutesPage() {
{!isLive ? ( - - Раздел работает в режиме "Живые данные". Переключи источник данных в настройках. - + + + Раздел работает в режиме "Живые данные". Переключи источник данных в настройках. + + ) : ( {currentServer && ( diff --git a/app/(main)/releases/page.tsx b/app/(main)/releases/page.tsx index 69878bf..0be3001 100644 --- a/app/(main)/releases/page.tsx +++ b/app/(main)/releases/page.tsx @@ -1,6 +1,6 @@ import Link from "next/link" import { PageHeader } from "@/components/page-header" -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { OpsPanel } from "@/components/ops-panel" import { buttonVariants } from "@/components/ui/button" import { formatAppVersionLabel, getAppVersion, getReleaseUrl } from "@/lib/app-version" import { @@ -52,12 +52,11 @@ export default function ReleasesPage() {
- - - Текущая версия - Сборка MikrotikManager, опубликованная через CI/CD. - - +
{formatAppVersionLabel(version)} {formatPublishedAt(manifest.publishedAt, isProdBuild)} @@ -65,23 +64,21 @@ export default function ReleasesPage() {

Версия формируется автоматически от базы v1.0.0 по Conventional Commits.

- - + - - - Сводка коммитов - - {commits.length > 0 - ? `Последние ${RELEASE_COMMIT_PREVIEW_LIMIT} коммитов в сборке ${formatAppVersionLabel(manifest.version)}.` - : "Для локальной разработки список коммитов пуст. В прод-сборке он заполняется CI."} - - - + 0 + ? `Последние ${RELEASE_COMMIT_PREVIEW_LIMIT} коммитов в сборке ${formatAppVersionLabel(manifest.version)}.` + : "Для локальной разработки список коммитов пуст. В прод-сборке он заполняется CI." + } + contentClassName="px-5 pb-5" + > {commits.length === 0 ? (

Коммитов для отображения пока нет.

) : ( -
    +
      {commits.map((commit) => (
    • @@ -94,8 +91,7 @@ export default function ReleasesPage() { ))}
    )} - - +
diff --git a/app/(main)/route-optimizer/page.tsx b/app/(main)/route-optimizer/page.tsx index 77ef102..4b80d10 100644 --- a/app/(main)/route-optimizer/page.tsx +++ b/app/(main)/route-optimizer/page.tsx @@ -9,7 +9,9 @@ import { RouteOptimizerFullRoutesDataGrid } from "@/components/data-grids/route- import { RouteOptimizerCommRecsDataGrid } from "@/components/data-grids/route-optimizer-comm-recs-data-grid" import { RouteOptimizerOspfPreviewDataGrid } from "@/components/data-grids/route-optimizer-ospf-preview-data-grid" import { FormToggle } from "@/components/form-kit" -import { Card, CardContent } from "@/components/ui/card" +import { Frame, FramePanel } from "@/components/reui/frame" +import { IconTile } from "@/components/reui/icon-tile" +import { OpsPanel } from "@/components/ops-panel" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Flag } from "@/components/flag" @@ -41,7 +43,7 @@ import { RefreshCwIcon, AlertCircleIcon, ArrowRightIcon, SettingsIcon, ChevronDownIcon, ChevronUpIcon, PinIcon, ZapIcon, PlayIcon, CheckCircleIcon, NetworkIcon, WifiIcon, - MonitorIcon, ServerIcon, GitMergeIcon, ShieldIcon, LayersIcon, + MonitorIcon, ServerIcon, InfoIcon, } from "lucide-react" @@ -290,7 +292,8 @@ function HomeRouterCard({ entry, jumpHosts, settings, pinned, applied, applying, const switchCount = commRecs.filter(r => r.shouldSwitch && !pinned.has(`${home.id}::${r.community}`)).length return ( - + + {/* Header */}
@@ -366,7 +369,8 @@ function HomeRouterCard({ entry, jumpHosts, settings, pinned, applied, applying, onPin={onPin} onApply={onApply} /> )} - + + ) } @@ -761,16 +765,18 @@ export default function RouteOptimizerPage() { {/* Stats chips */}
{statsChips.map((s) => ( - - -
-

{s.label}

-

{s.value}

-

{s.sub}

+ + + +
+

{s.label}

+

{s.value}

+

{s.sub}

-
{s.icon}
- - +
+ ))}
@@ -787,7 +793,8 @@ export default function RouteOptimizerPage() { )} {/* Settings */} - + +
setShowSettings(v => !v)} @@ -801,7 +808,7 @@ export default function RouteOptimizerPage() {
{showSettings && ( - +

Пороги переключения

@@ -866,20 +873,17 @@ export default function RouteOptimizerPage() { Настройки → Route AI .

- +
)} - + + {/* OSPF optimization from Route Optimizer */} - - -
- - OSPF - - Route AI weight: ping {settings.pingWeight}% - -
+ {!useLiveData && (

@@ -960,20 +964,16 @@ export default function RouteOptimizerPage() { )} )} - - + {/* ─── ECMP / RPF / VRF section ──────────────────────────────── */}

{/* ECMP Card */} - - -
- - ECMP - Equal-Cost Multi-Path -
+ setEcmpEnabled(v => !v)} className={cn("relative inline-flex h-5 w-9 shrink-0 rounded-full border-2 border-transparent transition-colors", @@ -981,8 +981,9 @@ export default function RouteOptimizerPage() { -
-
+ } + contentClassName="px-5 py-4 flex flex-col gap-4" + >
@@ -1013,17 +1014,10 @@ export default function RouteOptimizerPage() { RouterOS 7.x: /routing/rule add ecmp=yes
-
-
+ {/* RPF Card */} - - -
- - RPF - Reverse Path Forwarding -
+
Режим проверки источника @@ -1057,17 +1051,10 @@ export default function RouteOptimizerPage() { /ip settings set rp-filter={rpfMode}
)} -
-
+ {/* VRF Card */} - - -
- - VRF - Virtual Routing -
+
Контекст оптимизации маршрутов @@ -1099,8 +1086,7 @@ export default function RouteOptimizerPage() { Оптимизатор работает в VRF {selectedVrf}
-
-
+
diff --git a/app/(main)/servers/page.tsx b/app/(main)/servers/page.tsx index 16ca202..40f8fe0 100644 --- a/app/(main)/servers/page.tsx +++ b/app/(main)/servers/page.tsx @@ -27,7 +27,8 @@ import { import { useDataSource } from "@/lib/data-source" import { Flag } from "@/components/flag" import { cn } from "@/lib/utils" -import { Card, CardContent } from "@/components/ui/card" +import { Frame, FramePanel } from "@/components/reui/frame" +import { IconTile } from "@/components/reui/icon-tile" import { DataPageCard } from "@/components/data-page-card" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" @@ -493,20 +494,22 @@ export default function ServersPage() { {/* Stats */}
{[ - { label: "Всего серверов", value: counts.all, icon: }, - { label: "Онлайн", value: counts.online, icon: }, - { label: "JH + Exit Node", value: counts["jump-host"] + counts["exit-node"], icon: }, - { label: "Home Router", value: counts["home-router"],icon: }, + { label: "Всего серверов", value: counts.all, icon: , iconClass: "text-muted-foreground" }, + { label: "Онлайн", value: counts.online, icon: , iconClass: "text-[var(--status-online-fg)]" }, + { label: "JH + Exit Node", value: counts["jump-host"] + counts["exit-node"], icon: , iconClass: "text-muted-foreground" }, + { label: "Home Router", value: counts["home-router"],icon: , iconClass: "text-muted-foreground" }, ].map(s => ( - - -
-

{s.label}

-

{s.value}

+ + + +
+

{s.label}

+

{s.value}

-
{s.icon}
- - +
+ ))}
diff --git a/app/(main)/settings/page.tsx b/app/(main)/settings/page.tsx index bfe3b9b..a995244 100644 --- a/app/(main)/settings/page.tsx +++ b/app/(main)/settings/page.tsx @@ -5,9 +5,8 @@ import Link from "next/link" import { PageHeader } from "@/components/page-header" import { FormField, FormToggle } from "@/components/form-kit" import { FileImportDialog } from "@/components/file-import-dialog" -import { - Card, CardContent, CardHeader, CardTitle, CardDescription, -} from "@/components/ui/card" +import { OpsPanel } from "@/components/ops-panel" +import { Frame, FramePanel } from "@/components/reui/frame" import { Button, buttonVariants } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { useDataSource } from "@/lib/data-source" @@ -938,16 +937,15 @@ export default function SettingsPage() {
{/* ── Источник данных ── */} - - - Источник данных - - {mockModeAvailable + - - + : "В продакшен-образе доступны только живые данные от бекенда" + } + contentClassName="divide-y px-5" + > {/* mode toggle */} {mockModeAvailable ? ( @@ -1042,13 +1040,10 @@ export default function SettingsPage() { Бекенд недоступен — данные будут отображаться из кеша или моков. Запустите бекенд: cd backend && npm run dev
)} -
-
+ {/* ── Основные настройки ── */} - - Основные настройки - + setProbeTimeout(e.target.value)} /> - - + - - - База данных приложения - - Резервная копия SQLite бекенда: серверы, мониторинг, оповещения, EvoBGP. На время операции планировщик - сбора данных приостанавливается. - - - + {!systemDbAvailable && (
@@ -1136,18 +1126,15 @@ export default function SettingsPage() { )}
-
-
+ - - - Route AI - - Параметры оптимизации маршрутов по умолчанию (тот же набор, что на странице «Оптимизатор маршрутов»). Пороги и - веса настраиваются в инструменте, не через отдельный API. - - - +
Мин. выигрыш (переключение)
@@ -1176,8 +1163,7 @@ export default function SettingsPage() { > Открыть оптимизатор маршрутов - - +
) @@ -1186,20 +1172,20 @@ export default function SettingsPage() { if (section === "EvoBGP") return (
{(mode !== "live" || backendStatus !== true) && ( - - + +

Интеграция доступна в live-режиме

Включите «Живые» данные и убедитесь, что локальный бекенд доступен (раздел «Общие»).

-
-
+ + )} - - - EvoBGP API - + Control plane EvoBGP: Bearer-ключ и роль viewer+ — см.{" "} evobgp_settings ). Каталог —{" "} GET /v1/router-lists/catalog через прокси. - - - + + } + contentClassName="divide-y px-5 flex flex-col gap-4 pb-5" + > {evo.error}

)} -
-
+
) // ── Уведомления ── if (section === "Уведомления") return (
- - Каналы уведомлений - + @@ -1384,11 +1368,8 @@ export default function SettingsPage() { {notifWh &&
} -
-
- - Триггеры - + + @@ -1401,8 +1382,7 @@ export default function SettingsPage() { - - +
) @@ -1419,7 +1399,8 @@ export default function SettingsPage() {
- + + {/* table header */}
Пользователь @@ -1475,7 +1456,8 @@ export default function SettingsPage() { ) })}
-
+ + {/* access summary */} @@ -1500,8 +1482,8 @@ export default function SettingsPage() {
{apiKeys.map(k => ( - - + +

{k.name}

@@ -1529,16 +1511,15 @@ export default function SettingsPage() {
- - + + ))}
- - - Документация API - Base URL: https://api.routerlists.io/v1 - - +
{[ ["GET", "/servers", "Список серверов"], @@ -1556,17 +1537,14 @@ export default function SettingsPage() {
))}
- - +
) // ── Безопасность ── if (section === "Безопасность") return (
- - Аутентификация - + @@ -1574,30 +1552,22 @@ export default function SettingsPage() { setSessMin(e.target.value)} /> - - - - - Allowlist IP-адресов - Доступ разрешён только с этих сетей. Одна запись на строку. - - + +