diff --git a/app/(main)/data-collection/page.tsx b/app/(main)/data-collection/page.tsx index d03085e..75f14de 100644 --- a/app/(main)/data-collection/page.tsx +++ b/app/(main)/data-collection/page.tsx @@ -4,7 +4,7 @@ import Link from "next/link" import { useCallback, useEffect, useMemo, useState } from "react" import { PageHeader } from "@/components/page-header" import { FormToggle } from "@/components/form-kit" -import { Badge } from "@/components/ui/badge" +import { Badge } from "@/components/reui/badge" import { Button, buttonVariants } from "@/components/ui/button" import { Frame, FramePanel } from "@/components/reui/frame" import { IconTile } from "@/components/reui/icon-tile" @@ -104,7 +104,7 @@ function SnapshotTables({ snap }: { snap: SchedulerRunSnapshot }) { if (snap.job === "traffic") { const t = snap as TrafficRunSnapshot return ( -
+
{t.skipped ? (

Прогон пропущен: предыдущий сбор трафика ещё выполнялся.

) : null} @@ -126,7 +126,7 @@ function SnapshotTables({ snap }: { snap: SchedulerRunSnapshot }) { if (snap.job === "uptime_resources") { const u = snap as ResourcesRunSnapshot return ( -
+
{u.skipped ? (

Прогон пропущен: сбор ресурсов уже выполняется.

) : null} @@ -148,7 +148,7 @@ function SnapshotTables({ snap }: { snap: SchedulerRunSnapshot }) { if (snap.job === "servers_rest_ping") { const s = snap as ServersRestPingRunSnapshot return ( -
+
{s.skipped ? (

Прогон пропущен: предыдущая проверка API ещё выполнялась.

) : null} @@ -171,7 +171,7 @@ function SnapshotTables({ snap }: { snap: SchedulerRunSnapshot }) { if (snap.job === "uptime_ping") { const p = snap as PingRunSnapshot return ( -
+
{p.skipped ? (

Прогон пропущен: сбор ping уже выполняется.

) : null} @@ -196,7 +196,7 @@ function SnapshotTables({ snap }: { snap: SchedulerRunSnapshot }) { if (snap.job === "uptime_speed") { const s = snap as SpeedScheduledRunSnapshot return ( -
+

Прогоны speed на {new Date(s.sampledAt).toLocaleString("ru-RU")} — по очереди для каждой включённой пробы

@@ -209,7 +209,7 @@ function SnapshotTables({ snap }: { snap: SchedulerRunSnapshot }) { if (snap.job === "gre_bgp") { const g = snap as GreBgpSnapshotRunSnapshot return ( -
+
{g.skipped ? (

Прогон пропущен: предыдущий сбор GRE/BGP ещё выполнялся.

) : null} @@ -235,7 +235,7 @@ function SnapshotTables({ snap }: { snap: SchedulerRunSnapshot }) {
{g.errors?.length ? ( -
+
{g.errors.map((e, i) => (

{e} @@ -249,7 +249,7 @@ function SnapshotTables({ snap }: { snap: SchedulerRunSnapshot }) { if (snap.job === "internet_path") { const p = snap as InternetPathRunSnapshot return ( -

+

Снимок internet-path на{" "} {new Date(p.sampledAt).toLocaleString("ru-RU")} @@ -276,7 +276,7 @@ function SnapshotTables({ snap }: { snap: SchedulerRunSnapshot }) { if (snap.job === "certificates_renew") { const c = snap as CertificatesRenewRunSnapshot return ( -

+
{c.skipped ? (

Прогон пропущен: предыдущая проверка ещё выполнялась или задача отключена.

) : null} @@ -299,7 +299,7 @@ function SnapshotTables({ snap }: { snap: SchedulerRunSnapshot }) {
{c.errors.length ? ( -
+
{c.errors.map((e, i) => (

{e}

))} @@ -311,7 +311,7 @@ function SnapshotTables({ snap }: { snap: SchedulerRunSnapshot }) { if (snap.job === "backups") { const b = snap as BackupsRunSnapshot return ( -
+
Слот расписания
@@ -331,7 +331,7 @@ function SnapshotTables({ snap }: { snap: SchedulerRunSnapshot }) {
{b.errors?.length ? ( -
+
{b.errors.map((e, i) => (

{e}

))} @@ -343,7 +343,7 @@ function SnapshotTables({ snap }: { snap: SchedulerRunSnapshot }) { if (snap.job === "alert_engine") { const a = snap as AlertEngineRunSnapshot return ( -
+

Снимок на{" "} {new Date(a.sampledAt).toLocaleString("ru-RU")} @@ -370,7 +370,7 @@ function SnapshotTables({ snap }: { snap: SchedulerRunSnapshot }) { {a.errors?.length ? ( - + {a.errors.map((e, i) => (

{e} @@ -380,7 +380,7 @@ function SnapshotTables({ snap }: { snap: SchedulerRunSnapshot }) { ) : null} {a.ruleDiag && a.ruleDiag.length > 0 ? ( -

+

По правилам (почему не ушло в Telegram)

@@ -398,39 +398,39 @@ function RunRowDetail({ r }: { r: SchedulerRunRowDto }) { const jobDesc = SCHEDULER_JOB_DESCRIPTIONS[r.jobKey] ?? "—" const snapshot = useMemo(() => parseSchedulerRunSnapshot(r.resultJson ?? null), [r.resultJson]) return ( -
+
-
+
ID записи
{r.id}
-
+
Ключ задачи
{r.jobKey}
-
+
Название и назначение
{jobTitle}

{jobDesc}

-
+
Старт
{new Date(r.startedAt).toLocaleString("ru-RU")}
-
+
Завершение
{new Date(r.finishedAt).toLocaleString("ru-RU")}
-
+
Длительность
{r.durationMs} мс ({fmtMs(r.durationMs)})
-
+
Результат
{r.error ? ( -
+

Текст ошибки

 
-      
+

Результаты измерений

{snapshot ? ( @@ -1090,7 +1090,7 @@ export default function DataCollectionPage() {
-
+

Хранение сэмплов трафика (дней)

@@ -1142,7 +1142,7 @@ export default function DataCollectionPage() { {schedulerSaveBusy ? : null} Сохранить настройки планировщика -
+

Трафик — последний сбор:{" "} {trafficCollector?.lastCollectedAt diff --git a/app/(main)/filters/page.tsx b/app/(main)/filters/page.tsx index ddf5da2..b7c9805 100644 --- a/app/(main)/filters/page.tsx +++ b/app/(main)/filters/page.tsx @@ -35,6 +35,7 @@ import { import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" import { toast } from "sonner" +import { CodeExportSheet } from "@/components/reui-kit/code-export-sheet" // ── helpers ──────────────────────────────────────────────────────────────────── @@ -754,13 +755,11 @@ function PreviewModal({ open, serverId, rulesets, onClose, serversList, tunnelsL tunnelsList: GreTunnel[] recRoutesByServer: Record }) { - const [copied, setCopied] = useState(false) - const singleRuleset = useMemo( () => rulesets.filter(r => r.serverId === serverId), [rulesets, serverId], ) - const server = serversList.find(s => s.id === serverId) + const server = serversList.find(s => s.id === serverId) const totalRules = singleRuleset.reduce((s, r) => s + r.rules.length, 0) const config = useMemo( @@ -771,93 +770,25 @@ function PreviewModal({ open, serverId, rulesets, onClose, serversList, tunnelsL [open, singleRuleset, serversList, tunnelsList, recRoutesByServer], ) - const handleCopy = () => { - navigator.clipboard.writeText(config).catch(() => {}) - setCopied(true); setTimeout(() => setCopied(false), 2000) - } - if (!open) return null - return ( -

-
-
- - {/* header */} -
-
- - RouterOS config - {server && ( - - - {server.name} - · {totalRules} правил - - )} -
- -
- - {/* code */} -
-
-            {config.split("\n").map((line, i) => {
-              const trimmed = line.trimStart()
-              const cls =
-                // section dividers
-                trimmed.startsWith("# ═") || trimmed.startsWith("# ─")
-                  ? "text-[#444c56]" :
-                // inline comments inside rule body
-                trimmed.startsWith("# ")
-                  ? "text-[#8b949e]" :
-                // RouterOS scripting keywords
-                trimmed.startsWith(":local") || trimmed.startsWith(":log") || trimmed.startsWith(":foreach")
-                  ? "text-[#d2a8ff]" :
-                // :if identity branch / closing brace
-                trimmed.startsWith(":if") || (trimmed === "}" && line.length < 3)
-                  ? "text-[#ff7b72] font-semibold" :
-                // filter rule add command
-                trimmed.startsWith("/routing filter rule")
-                  ? "text-[#79c0ff]" :
-                // rule body: if/else if branches
-                trimmed.startsWith("if (") || trimmed.startsWith("} else if")
-                  ? "text-[#ff7b72]" :
-                // rule body: blackhole action
-                trimmed.startsWith("set type blackhole")
-                  ? "text-[#ff7b72] font-semibold" :
-                // rule body: set actions
-                trimmed.startsWith("set gw") || trimmed.startsWith("set gateway") || trimmed.startsWith("set out-interface")
-                  ? "text-[#a5d6ff]" :
-                // rule body: accept / rule close
-                trimmed.startsWith("accept") || trimmed === `}"` || trimmed.startsWith(`rule="`)
-                  ? "text-[#79c0ff]" :
-                // named params
-                trimmed.match(/^(chain|comment|bgp-communities)=/)
-                  ? "text-[#a5d6ff]" :
-                  "text-[#c9d1d9]"
-              return {line || " "}
-            })}
-          
-
- - {/* footer */} -
-
-

1. Скопируйте скрипт в буфер обмена

-

3. Вставьте в терминал и нажмите Enter

-

2. Подключитесь к любому MikroTik (SSH / Winbox)

-

4. Скрипт сам определит свои правила по identity

-
-
- - -
-
-
-
+ ) } diff --git a/app/(main)/settings/page.tsx b/app/(main)/settings/page.tsx index 5c70698..8793e13 100644 --- a/app/(main)/settings/page.tsx +++ b/app/(main)/settings/page.tsx @@ -16,6 +16,17 @@ import { Separator } from "@/components/ui/separator" import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetFooter, } from "@/components/ui/sheet" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogMedia, + AlertDialogTitle, +} from "@/components/ui/alert-dialog" import { Flag } from "@/components/flag" import { StatusDot } from "@/components/status-dot" import { servers } from "@/lib/data" @@ -152,7 +163,7 @@ const PERM_OPTS: { v: PermLevel; label: string }[] = [ const PERM_COLOR: Record = { none: "bg-muted text-muted-foreground", read: "bg-sky-500/10 text-sky-600 dark:text-sky-400", - write: "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400", + write: "bg-success/10 text-success", } const SECTIONS_NAV = ["Общие", "EvoBGP", "Уведомления", "Пользователи", "API-ключи", "Безопасность"] as const @@ -188,8 +199,8 @@ function PermPills({ value, onChange, disabled }: { value: PermLevel; onChange: "px-2 text-[11px] font-medium transition-colors", i < PERM_OPTS.length - 1 && "border-r border-input", value === o.v - ? o.v === "write" ? "bg-emerald-600 text-white dark:bg-emerald-500" - : o.v === "read" ? "bg-sky-600 text-white dark:bg-sky-500" + ? o.v === "write" ? "bg-success text-white" + : o.v === "read" ? "bg-info text-white" : "bg-muted-foreground/70 text-white" : "text-muted-foreground hover:bg-muted", )} @@ -685,68 +696,79 @@ function UserSheet({ open, user, onSave, onClose }: { // ─── delete confirm ─────────────────────────────────────────────────────────── function DatabaseRestoreConfirm({ + open, filename, busy, onConfirm, onCancel, }: { + open: boolean filename: string busy?: boolean onConfirm: () => void onCancel: () => void }) { return ( -
-
-
-
-
- -
-
-

Восстановить базу приложения?

-

{filename}

-
-
-

- Текущие данные SQLite на бекенде будут полностью заменены содержимым файла. Рекомендуется сначала скачать - актуальный бэкап. -

-
- - -
-
-
+ + + + ) } -function DeleteConfirm({ user, onConfirm, onCancel }: { user: User; onConfirm: () => void; onCancel: () => void }) { +function DeleteConfirm({ + open, + user, + onConfirm, + onCancel, +}: { + open: boolean + user: User + onConfirm: () => void + onCancel: () => void +}) { return ( -
-
-
-
-
- -
-
-

Удалить пользователя?

-

{user.name} · @{user.login}

-
-
-

- Это действие нельзя отменить. Пользователь потеряет доступ немедленно. -

-
- - -
-
-
+ { if (!v) onCancel() }}> + + + + + + Удалить пользователя? + + {user.name} · @{user.login}. Это действие нельзя отменить. + Пользователь потеряет доступ немедленно. + + + + Отмена + + Удалить + + + + ) } @@ -937,7 +959,7 @@ export default function SettingsPage() { // ── Общие ── if (section === "Общие") return ( -
+
{/* ── Источник данных ── */} {l} @@ -972,7 +992,7 @@ export default function SettingsPage() { ) : ( - + Живые @@ -1173,7 +1193,7 @@ export default function SettingsPage() { // ── EvoBGP ── if (section === "EvoBGP") return ( -
+
{(mode !== "live" || backendStatus !== true) && ( @@ -1357,7 +1377,7 @@ export default function SettingsPage() { // ── Уведомления ── if (section === "Уведомления") return ( -
+
@@ -1479,11 +1499,11 @@ export default function SettingsPage() { // ── API-ключи ── if (section === "API-ключи") return ( -
+
-
+
{apiKeys.map(k => ( @@ -1546,7 +1566,7 @@ export default function SettingsPage() { // ── Безопасность ── if (section === "Безопасность") return ( -
+
@@ -1647,6 +1667,7 @@ export default function SettingsPage() { {/* delete confirm */} {dbRestoreFile && ( { void handleSystemDatabaseRestoreConfirm() }} @@ -1658,6 +1679,7 @@ export default function SettingsPage() { )} {deleteTarget && ( { setUsers(p => p.filter(u => u.id !== deleteTarget.id)); setDeleteTarget(null) }} onCancel={() => setDeleteTarget(null)} diff --git a/components/data-grids/data-collection-scheduler-data-grid.tsx b/components/data-grids/data-collection-scheduler-data-grid.tsx index c34b14d..40bf28f 100644 --- a/components/data-grids/data-collection-scheduler-data-grid.tsx +++ b/components/data-grids/data-collection-scheduler-data-grid.tsx @@ -8,7 +8,7 @@ import { } from "@tanstack/react-table" import type { SchedulerJobStatusDto } from "@/lib/scheduler-settings" import { FormToggle } from "@/components/form-kit" -import { Badge } from "@/components/ui/badge" +import { Badge } from "@/components/reui/badge" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { DataGridShell } from "@/components/data-grids/shared/data-grid-shell" diff --git a/components/data-grids/snapshot-data-grid.tsx b/components/data-grids/snapshot-data-grid.tsx index c6c669e..a7240f9 100644 --- a/components/data-grids/snapshot-data-grid.tsx +++ b/components/data-grids/snapshot-data-grid.tsx @@ -1,6 +1,6 @@ "use client" -import { Badge } from "@/components/ui/badge" +import { Badge } from "@/components/reui/badge" import { cn } from "@/lib/utils" import { CompactDataGrid, @@ -35,11 +35,11 @@ function SnapshotOkBadge({ errLabel?: string }) { return ok ? ( - + {okLabel} ) : ( - + {errLabel} ) diff --git a/components/release-notes-modal.tsx b/components/release-notes-modal.tsx index 79b6dab..336b310 100644 --- a/components/release-notes-modal.tsx +++ b/components/release-notes-modal.tsx @@ -2,7 +2,7 @@ import * as React from "react" import Link from "next/link" -import { Badge } from "@/components/ui/badge" +import { Badge } from "@/components/reui/badge" import { Button } from "@/components/ui/button" import { Dialog, diff --git a/docs/ui-design-contract.md b/docs/ui-design-contract.md new file mode 100644 index 0000000..f191438 --- /dev/null +++ b/docs/ui-design-contract.md @@ -0,0 +1,79 @@ +# UI Design Contract (MikrotikManager) + +Surface: **ReUI Frame**. Kit: `components/reui-kit/`. +Иерархия: **ReUI PRO > shadcn primitives**. +Стек: Next.js 16 App Router + Turbopack + shadcn **base-nova** + ReUI `@reui`. + +Карта: [docs](https://reui.io/docs) · [llms.txt](https://reui.io/llms.txt) · [Get Started](https://reui.io/docs/get-started) · [Styling](https://reui.io/docs/styling) · [Blocks](https://reui.io/blocks) · [MCP](https://reui.io/docs/mcp) + +## Surface + +Project lock: **`surface: frame`**. Ops / list / dashboard / detail / settings — только **Frame**, не shadcn Card как page shell. Не смешивать Card и Frame на одном ops-экране. + +Эталон CRUD: [`app/(main)/servers`](../app/(main)/servers). +Оболочка списков: `DataPageCard` → Frame. Панели: `OpsPanel` → Frame. + +## Canonical PRO references + +| Зона | Preview | +|------|---------| +| Shell | https://reui.io/preview/base/app-shell-12 | +| KPI | https://reui.io/preview/base/stats-12 · https://reui.io/docs/components/base/icon-tile | +| Lists | https://reui.io/preview/base/data-grid-filtering-2 | +| Settings | https://reui.io/preview/base/settings-16 · https://reui.io/preview/base/settings-3 | +| Forms / Sheet | https://reui.io/preview/base/form-7 · https://reui.io/preview/base/sheet-8 | +| Alert / Badge | https://reui.io/docs/components/base/alert · https://reui.io/docs/components/base/badge | +| Empty | https://reui.io/preview/base/empty-state-12 | + +## Kit (`components/reui-kit/`) + +| Component | Role | +|-----------|------| +| `KpiStatGrid` | Hybrid KPI (IconTile elevated `size-10.5`) | +| `CodeExportSheet` | Экспорт кода (.rsc / .conf) — Sheet + Frame + ScrollArea | + +Слои: + +| Слой | Путь | +|------|------| +| shadcn | `components/ui/` | +| ReUI CLI | `components/reui/` | +| Kit | `components/reui-kit/` | +| Domain grids | `components/data-grids/` | + +## Shared App Shell chrome + +| Токен / зона | Значение | +|--------------|----------| +| `--sidebar-width` | `240px` | +| Header right | AppsMenu → SystemMonitorPopover (тема — в NavUser) | +| Search | ⌘K / Ctrl+K only | +| `main` | `gap-4 md:gap-6`, `px-4 py-4 md:px-6 md:py-5` | + +## Spacing & tokens + +- `flex` + `gap-*` — не `space-y-*` / `space-x-*` +- Semantic tokens / ReUI Badge variants — не raw `bg-emerald-*` / hex +- Max 1 primary CTA на экран + +## Exceptions (не Frame ops shell) + +- `network-map` — canvas / topology UX +- `terminal` — terminal chrome + +## Forbidden + +- Card как ops list/dashboard shell +- Hand-roll data-grid / KPI / code-export при наличии kit +- Дубль Copy в header + footer export sheet +- Mixing Card и Frame на одном ops-экране +- Radix-варианты docs — только Base UI (`base-nova`) + +## License + +```env +# .env.local (gitignored) +REUI_LICENSE_KEY= +``` + +`components.json` → `@reui` с `Authorization: Bearer ${REUI_LICENSE_KEY}`.