feat: обновить название проекта и улучшить документацию
Docker images / prepare-release (push) Successful in 5s
Docker images / backend-image (push) Successful in 1m32s
Docker images / frontend-image (push) Successful in 1m37s
Docker images / updater-image (push) Successful in 38s
Docker images / notify-webhook (push) Has been skipped
Docker images / publish-release (push) Successful in 6s

- Изменено название проекта с "RouterLists" на "MikrotikManager" во всех соответствующих файлах и компонентах.
- Обновлены ссылки и комментарии в документации, чтобы отразить новое название и улучшить ясность.
- Внесены изменения в правила коммитов, чтобы обеспечить использование русского языка в сообщениях коммитов.
This commit is contained in:
Denozordec
2026-05-12 17:53:53 +07:00
parent 35876fcd6f
commit c2a8a08618
19 changed files with 360 additions and 29 deletions
+1 -1
View File
@@ -18,7 +18,7 @@ alwaysApply: false
- Выравнивать UI/UX с **`/servers`** как с главным эталоном (layout, отступы, сетка, типографика, композиция). - Выравнивать UI/UX с **`/servers`** как с главным эталоном (layout, отступы, сетка, типографика, композиция).
- Предпочитать **Server Components**; `'use client'` — только при необходимости интерактива (состояние, браузерные API, обработчики). - Предпочитать **Server Components**; `'use client'` — только при необходимости интерактива (состояние, браузерные API, обработчики).
- UI: по возможности **shadcn/ui**; не вводить новый визуальный/UX-паттерн, если на `/servers` уже есть эквивалент. - UI: по возможности **shadcn/ui**; не вводить новый визуальный/UX-паттерн, если на `/servers` уже есть эквивалент.
- При завершении задачи предлагать subject коммита по **`release-versioning.mdc`** (`fix:` для patch, `feat:` для minor). - При завершении задачи предлагать subject коммита по **`release-versioning.mdc`** (`fix:` для patch, `feat:` для minor), **описание на русском**.
## Формат вывода (строго) ## Формат вывода (строго)
+2
View File
@@ -9,6 +9,8 @@ alwaysApply: true
**Язык:** все ответы пользователю — **только на русском**. **Язык:** все ответы пользователю — **только на русском**.
**Git / Generate Commit Message:** subject и body коммита — **на русском** (префикс `feat`/`fix`/scope — на английском). Подробности — **`release-versioning.mdc`**.
## 1. Документация в первую очередь ## 1. Документация в первую очередь
- Решения сверять с **официальной** документацией Next.js и shadcn/ui (актуальные версии проекта). - Решения сверять с **официальной** документацией Next.js и shadcn/ui (актуальные версии проекта).
+15 -4
View File
@@ -1,18 +1,28 @@
--- ---
description: "Conventional Commits и semver для patch/minor релизов RouterLists" description: "Conventional Commits, semver релизов MikrotikManager и русские git commit message (Generate Commit Message)"
alwaysApply: true alwaysApply: true
--- ---
# Версионирование релизов # Версионирование релизов
## Generate Commit Message (критично)
Для **Generate Commit Message** в Cursor, предложения subject в чате, `git commit` и любой автогенерации сообщения коммита:
- Формат: `тип(scope): описание на русском` — префикс Conventional Commits и scope **на английском**, текст после `:` **только на русском**.
- Body и footer (если нужны) — **только на русском**.
- Не использовать английские глаголы в subject: `add`, `update`, `fix`, `implement`, `rename` и т.п.
- Примеры: `feat(releases): добавить модалку сводки релиза`; `fix(ci): исправить сборку release manifest`.
- Версию **не** править вручную в UI, `package.json` и git-тегах; bump делает CI через [`.ci/scripts/compute-release.mjs`](.ci/scripts/compute-release.mjs). - Версию **не** править вручную в UI, `package.json` и git-тегах; bump делает CI через [`.ci/scripts/compute-release.mjs`](.ci/scripts/compute-release.mjs).
- Базовая линия: `v1.0.0`. Формат отображения: `v1.2.3`. - Базовая линия: `v1.0.0`. Формат отображения: `v1.2.3`.
- **Язык коммитов:** subject и body — **только на русском**; префикс Conventional Commits (`feat`, `fix`, `chore` и т.д.) и scope — на английском.
## Patch (`1.0.x`) ## Patch (`1.0.x`)
`fix:`, `chore:`, `docs:`, `refactor:`, `style:`, `test:`, `build:`, `ci:` `fix:`, `chore:`, `docs:`, `refactor:`, `style:`, `test:`, `build:`, `ci:`
- Один логический смысл на коммит; subject в повелительном наклонении, до ~72 символов. - Один логический смысл на коммит; subject в повелительном наклонении, до ~72 символов, **на русском**.
- Scope по желанию: `fix(servers):`, `chore(ci):`. - Scope по желанию: `fix(servers):`, `chore(ci):`.
## Minor (`1.x.0`) ## Minor (`1.x.0`)
@@ -23,10 +33,11 @@ alwaysApply: true
- `fix(servers): исправить опрос API после таймаута` — patch - `fix(servers): исправить опрос API после таймаута` — patch
- `feat(filters): добавить синхронизацию BGP in` — minor - `feat(filters): добавить синхронизацию BGP in` — minor
- `chore: мелкие правки` — плохо; `chore(ci): добавить job prepare-release` — хорошо - `chore(ci): добавить job prepare-release` — хорошо
- `feat: add release modal` — плохо (subject не на русском)
## Антипаттерны ## Антипаттерны
WIP, «update», смешение несвязанных изменений, маскировка фич под `fix`/`chore`, ручной bump в sidebar. WIP, «update», subject на английском, смешение несвязанных изменений, маскировка фич под `fix`/`chore`, ручной bump в sidebar.
Подробности — раздел CI/CD в `README.md`. Подробности — раздел CI/CD в `README.md`.
+11
View File
@@ -0,0 +1,11 @@
# MikrotikManager — инструкции для агента
## Сообщения коммитов
При **Generate Commit Message**, предложении subject для `git commit` и любой автогенерации commit message:
- Формат: `feat(scope): описание на русском` (префикс и scope — английский Conventional Commits).
- Текст после двоеточия и body — **только на русском**.
- Плохо: `feat(ui): add release modal`. Хорошо: `feat(ui): добавить модалку сводки релиза`.
Полные правила semver и примеры — `.cursor/rules/release-versioning.mdc`.
+1 -1
View File
@@ -215,7 +215,7 @@ npm --prefix backend run db:studio
- Job **`publish-release`**: annotated tag `v1.2.3`, Gitea Release на `https://git.shts.su` (markdown notes), образы с тегами `:latest`, `:<sha>`, `:<semver>`. - Job **`publish-release`**: annotated tag `v1.2.3`, Gitea Release на `https://git.shts.su` (markdown notes), образы с тегами `:latest`, `:<sha>`, `:<semver>`.
- UI: версия в sidebar и страница **`/releases`**; manifest `public/release-manifest.json` (в CI подставляется из артефакта). - UI: версия в sidebar и страница **`/releases`**; manifest `public/release-manifest.json` (в CI подставляется из артефакта).
- Bootstrap: один раз выставить `1.0.0` в workspace `package.json` и создать тег **`v1.0.0`** на `main` перед первым автоматическим bump. - Bootstrap: один раз выставить `1.0.0` в workspace `package.json` и создать тег **`v1.0.0`** на `main` перед первым автоматическим bump.
- Сообщения коммитов: см. [`.cursor/rules/release-versioning.mdc`](.cursor/rules/release-versioning.mdc). - Сообщения коммитов: см. [`.cursor/rules/release-versioning.mdc`](.cursor/rules/release-versioning.mdc); subject и body — **на русском**, префикс Conventional Commits — на английском.
## Прод-развёртывание Docker ## Прод-развёртывание Docker
+2 -2
View File
@@ -185,7 +185,7 @@ function generateCombinedRouterOSConfig(
const bhCount = rulesets.reduce((s, r) => s + r.rules.filter(x => x.action === "blackhole").length, 0) const bhCount = rulesets.reduce((s, r) => s + r.rules.filter(x => x.action === "blackhole").length, 0)
const lines: string[] = [ const lines: string[] = [
`# RouterLists — конфиг фильтрации`, `# MikrotikManager — конфиг фильтрации`,
`# Дата : ${now}`, `# Дата : ${now}`,
`# Серверов : ${withRules.length}`, `# Серверов : ${withRules.length}`,
`# Communities : ${totalRules - bhCount} route + ${bhCount} blackhole`, `# Communities : ${totalRules - bhCount} route + ${bhCount} blackhole`,
@@ -271,7 +271,7 @@ function generateCombinedRouterOSConfig(
lines.push(`/routing filter rule add \\`) lines.push(`/routing filter rule add \\`)
lines.push(` chain=bgp-in \\`) lines.push(` chain=bgp-in \\`)
lines.push(` comment="RouterLists: ${server.name}" \\`) lines.push(` comment="MikrotikManager: ${server.name}" \\`)
lines.push(` rule="`) lines.push(` rule="`)
lines.push(branches) lines.push(branches)
lines.push(` "`) lines.push(` "`)
+1 -1
View File
@@ -417,7 +417,7 @@ function NativeSelect({ value, onChange, children, className }: {
function generateRsc(rules: FirewallRule[], timestamp?: string): string { function generateRsc(rules: FirewallRule[], timestamp?: string): string {
const lines: string[] = [] const lines: string[] = []
lines.push(`# RouterLists Firewall Export`) lines.push(`# MikrotikManager Firewall Export`)
if (timestamp) lines.push(`# Сгенерировано: ${timestamp}`) if (timestamp) lines.push(`# Сгенерировано: ${timestamp}`)
lines.push(`# Правил: ${rules.length}`) lines.push(`# Правил: ${rules.length}`)
lines.push("") lines.push("")
+1 -1
View File
@@ -54,7 +54,7 @@ function generateRosCommands(t: GreTunnel, serverById: Record<string, Server>):
const srv = serverById[t.serverId] const srv = serverById[t.serverId]
lines.push(`# Сервер: ${srv?.name ?? t.serverId} (${srv?.host ?? ""})`) lines.push(`# Сервер: ${srv?.name ?? t.serverId} (${srv?.host ?? ""})`)
lines.push(`# Сгенерировано RouterLists · ${new Date().toLocaleDateString("ru")}`) lines.push(`# Сгенерировано MikrotikManager · ${new Date().toLocaleDateString("ru")}`)
lines.push("") lines.push("")
// GRE interface // GRE interface
+2
View File
@@ -1,6 +1,7 @@
import { AppSidebar } from "@/components/app-sidebar" import { AppSidebar } from "@/components/app-sidebar"
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar" import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"
import { CommandPalette } from "@/components/command-palette" import { CommandPalette } from "@/components/command-palette"
import { ReleaseNotesModal } from "@/components/release-notes-modal"
import { DataSourceProvider } from "@/lib/data-source" import { DataSourceProvider } from "@/lib/data-source"
import { EvoBGPProvider } from "@/lib/evobgp-context" import { EvoBGPProvider } from "@/lib/evobgp-context"
@@ -12,6 +13,7 @@ export default function MainLayout({ children }: { children: React.ReactNode })
<AppSidebar /> <AppSidebar />
<SidebarInset className="h-svh overflow-hidden">{children}</SidebarInset> <SidebarInset className="h-svh overflow-hidden">{children}</SidebarInset>
<CommandPalette /> <CommandPalette />
<ReleaseNotesModal />
</SidebarProvider> </SidebarProvider>
</EvoBGPProvider> </EvoBGPProvider>
</DataSourceProvider> </DataSourceProvider>
+1 -1
View File
@@ -50,7 +50,7 @@ export default function ReleasesPage() {
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Текущая версия</CardTitle> <CardTitle>Текущая версия</CardTitle>
<CardDescription>Сборка RouterLists, опубликованная через CI/CD.</CardDescription> <CardDescription>Сборка MikrotikManager, опубликованная через CI/CD.</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-3"> <CardContent className="space-y-3">
<div className="flex flex-wrap items-center gap-3"> <div className="flex flex-wrap items-center gap-3">
+1 -1
View File
@@ -16,7 +16,7 @@ const geistMono = Geist_Mono({
}); });
export const metadata: Metadata = { export const metadata: Metadata = {
title: "RouterLists", title: "MikrotikManager",
description: "MikroTik network management platform", description: "MikroTik network management platform",
}; };
+23
View File
@@ -0,0 +1,23 @@
export const PRODUCT_NAME = "MikrotikManager"
export const LEGACY_PRODUCT_NAME = "RouterLists"
export function hasManagedCommentPrefix(comment: string): boolean {
const value = comment.trim()
return value.startsWith(`${PRODUCT_NAME}:`) || value.startsWith(`${LEGACY_PRODUCT_NAME}:`)
}
export function hasManagedRecursiveComment(comment: string): boolean {
const value = comment.trim()
return (
value.startsWith(`${PRODUCT_NAME}:recursive`) ||
value.startsWith(`${LEGACY_PRODUCT_NAME}:recursive`)
)
}
export function managedComment(label: string): string {
return `${PRODUCT_NAME}: ${label}`
}
export function managedRecursiveComment(comment?: string | null): string {
return comment ? `${PRODUCT_NAME}:recursive ${comment}` : `${PRODUCT_NAME}:recursive`
}
+9 -5
View File
@@ -5,6 +5,10 @@ import { filterRules, recursiveRoutes, servers } from "../db/schema.js"
import { MikrotikClient } from "../services/mikrotik.js" import { MikrotikClient } from "../services/mikrotik.js"
import { parseDbServerId } from "../utils/server-id.js" import { parseDbServerId } from "../utils/server-id.js"
import { appendEvent } from "../modules/events/service/events-service.js" import { appendEvent } from "../modules/events/service/events-service.js"
import {
hasManagedCommentPrefix,
managedComment,
} from "../managed-markers.js"
type ServerRow = typeof servers.$inferSelect type ServerRow = typeof servers.$inferSelect
@@ -595,11 +599,11 @@ const filtersRoutes: FastifyPluginAsyncZod = async (app) => {
const isInBgpIn = (r: RosFilterRule) => const isInBgpIn = (r: RosFilterRule) =>
(r.chain ?? "").trim().toLowerCase() === "bgp-in" (r.chain ?? "").trim().toLowerCase() === "bgp-in"
const managedComment = `RouterLists: ${server.name || server.host}` const managedCommentValue = managedComment(server.name || server.host)
// Уже созданное нами правило — будем PATCH'ить, чтобы сохранить ID/позицию в цепочке. // Уже созданное нами правило — будем PATCH'ить, чтобы сохранить ID/позицию в цепочке.
const managedRule = existing.find( const managedRule = existing.find(
r => isInBgpIn(r) && (r.comment ?? "").startsWith("RouterLists:"), r => isInBgpIn(r) && hasManagedCommentPrefix(r.comment ?? ""),
) )
// Конфликтующие легаси-правила в bgp-in (без нашего comment, но с bgp-communities) — // Конфликтующие легаси-правила в bgp-in (без нашего comment, но с bgp-communities) —
@@ -608,7 +612,7 @@ const filtersRoutes: FastifyPluginAsyncZod = async (app) => {
const conflictIds = existing const conflictIds = existing
.filter(r => .filter(r =>
isInBgpIn(r) && isInBgpIn(r) &&
!(r.comment ?? "").startsWith("RouterLists:") && !hasManagedCommentPrefix(r.comment ?? "") &&
/bgp-communities/i.test(r.rule ?? ""), /bgp-communities/i.test(r.rule ?? ""),
) )
.map(r => r[".id"]) .map(r => r[".id"])
@@ -641,7 +645,7 @@ const filtersRoutes: FastifyPluginAsyncZod = async (app) => {
`/routing/filter/rule/${encodeURIComponent(managedRule[".id"])}`, `/routing/filter/rule/${encodeURIComponent(managedRule[".id"])}`,
{ {
chain: "bgp-in", chain: "bgp-in",
comment: managedComment, comment: managedCommentValue,
rule: ruleBody, rule: ruleBody,
disabled: "no", disabled: "no",
}, },
@@ -650,7 +654,7 @@ const filtersRoutes: FastifyPluginAsyncZod = async (app) => {
} else { } else {
await client.post("/routing/filter/rule/add", { await client.post("/routing/filter/rule/add", {
chain: "bgp-in", chain: "bgp-in",
comment: managedComment, comment: managedCommentValue,
rule: ruleBody, rule: ruleBody,
}) })
app.log.info({ serverId: server.id }, "bgp-in rule created") app.log.info({ serverId: server.id }, "bgp-in rule created")
+6 -4
View File
@@ -4,6 +4,10 @@ import { db } from "../db/index.js"
import { recursiveRoutes, servers } from "../db/schema.js" import { recursiveRoutes, servers } from "../db/schema.js"
import { MikrotikClient } from "../services/mikrotik.js" import { MikrotikClient } from "../services/mikrotik.js"
import { parseDbServerId } from "../utils/server-id.js" import { parseDbServerId } from "../utils/server-id.js"
import {
hasManagedRecursiveComment,
managedRecursiveComment,
} from "../managed-markers.js"
type ServerRow = typeof servers.$inferSelect type ServerRow = typeof servers.$inferSelect
@@ -108,9 +112,7 @@ function toRouterPayload(route: RecursiveRouteDto): Record<string, string> {
...(route.targetScope != null ? { "target-scope": String(route.targetScope) } : {}), ...(route.targetScope != null ? { "target-scope": String(route.targetScope) } : {}),
...(route.routingTable ? { "routing-table": route.routingTable } : {}), ...(route.routingTable ? { "routing-table": route.routingTable } : {}),
...(route.checkGateway ? { "check-gateway": route.checkGateway } : {}), ...(route.checkGateway ? { "check-gateway": route.checkGateway } : {}),
comment: route.comment comment: managedRecursiveComment(route.comment),
? `RouterLists:recursive ${route.comment}`
: "RouterLists:recursive",
disabled: route.disabled ? "true" : "false", disabled: route.disabled ? "true" : "false",
} }
} }
@@ -235,7 +237,7 @@ const recursiveRoutesPlugin: FastifyPluginAsyncZod = async (app) => {
try { try {
const client = MikrotikClient.fromServer(server) const client = MikrotikClient.fromServer(server)
const existing = await client.get<RosRoute[]>("/ip/route") const existing = await client.get<RosRoute[]>("/ip/route")
const managed = existing.filter(r => (r.comment ?? "").startsWith("RouterLists:recursive")) const managed = existing.filter(r => hasManagedRecursiveComment(r.comment ?? ""))
for (const r of managed) { for (const r of managed) {
if (!r[".id"]) continue if (!r[".id"]) continue
await client.delete(`/ip/route/${encodeURIComponent(r[".id"])}`) await client.delete(`/ip/route/${encodeURIComponent(r[".id"])}`)
+1 -1
View File
@@ -227,7 +227,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
<RouterIcon className="size-4" /> <RouterIcon className="size-4" />
</div> </div>
<div className="flex flex-col leading-tight group-data-[collapsible=icon]:hidden"> <div className="flex flex-col leading-tight group-data-[collapsible=icon]:hidden">
<span className="font-semibold text-sidebar-foreground text-sm tracking-tight">RouterLists</span> <span className="font-semibold text-sidebar-foreground text-sm tracking-tight">MikrotikManager</span>
<span className="text-[10px] font-mono text-sidebar-foreground/40">{appVersionLabel}</span> <span className="text-[10px] font-mono text-sidebar-foreground/40">{appVersionLabel}</span>
</div> </div>
</div> </div>
+102
View File
@@ -0,0 +1,102 @@
"use client"
import * as React from "react"
import Link from "next/link"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { formatAppVersionLabel, getAppVersion } from "@/lib/app-version"
import type { ReleaseManifest } from "@/lib/release-manifest"
import { hasSeenReleaseModal, markReleaseModalSeen } from "@/lib/release-modal-storage"
export function ReleaseNotesModal() {
const [open, setOpen] = React.useState(false)
const [manifest, setManifest] = React.useState<ReleaseManifest | null>(null)
React.useEffect(() => {
let cancelled = false
async function loadManifest() {
try {
const response = await fetch("/release-manifest.json", { cache: "no-store" })
if (!response.ok) return
const data = (await response.json()) as ReleaseManifest
if (cancelled || !data.commits?.length || hasSeenReleaseModal(data.tag)) return
setManifest(data)
setOpen(true)
} catch {
// Локальная сборка или недоступный manifest — модалку не показываем.
}
}
void loadManifest()
return () => {
cancelled = true
}
}, [])
const handleOpenChange = (nextOpen: boolean) => {
setOpen(nextOpen)
if (!nextOpen && manifest) {
markReleaseModalSeen(manifest.tag)
}
}
const versionLabel = formatAppVersionLabel(manifest?.version ?? getAppVersion())
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="flex max-h-[min(85vh,720px)] flex-col gap-0 overflow-hidden p-0 sm:max-w-[560px]">
<DialogHeader className="gap-1 border-b px-6 py-4">
<DialogTitle>Что нового в {versionLabel}</DialogTitle>
<DialogDescription>
Краткая сводка изменений текущего релиза MikrotikManager.
</DialogDescription>
</DialogHeader>
<div className="flex-1 overflow-y-auto px-6 py-4">
{manifest?.commits.length ? (
<ul className="flex flex-col gap-3">
{manifest.commits.map((commit) => (
<li
key={commit.sha}
className="flex flex-col gap-2 rounded-lg border bg-card px-3 py-2.5"
>
<div className="flex flex-wrap items-center gap-2">
<Badge variant="secondary" className="font-mono uppercase">
{commit.type}
</Badge>
<span className="font-mono text-xs text-muted-foreground">{commit.shortSha}</span>
<span className="text-xs text-muted-foreground">{commit.author}</span>
</div>
<p className="text-sm leading-relaxed">{commit.subject}</p>
</li>
))}
</ul>
) : null}
</div>
<DialogFooter className="border-t bg-muted/30 px-6 py-4 sm:justify-between">
<Button
variant="outline"
render={<Link href="/releases" />}
onClick={() => handleOpenChange(false)}
>
Все релизы
</Button>
<Button onClick={() => handleOpenChange(false)}>Понятно</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
+160
View File
@@ -0,0 +1,160 @@
"use client"
import * as React from "react"
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { XIcon } from "lucide-react"
function Dialog({ ...props }: DialogPrimitive.Root.Props) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}
function DialogTrigger({ ...props }: DialogPrimitive.Trigger.Props) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}
function DialogPortal({ ...props }: DialogPrimitive.Portal.Props) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
function DialogClose({ ...props }: DialogPrimitive.Close.Props) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}
function DialogOverlay({
className,
...props
}: DialogPrimitive.Backdrop.Props) {
return (
<DialogPrimitive.Backdrop
data-slot="dialog-overlay"
className={cn(
"fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
className
)}
{...props}
/>
)
}
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: DialogPrimitive.Popup.Props & {
showCloseButton?: boolean
}) {
return (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Popup
data-slot="dialog-content"
className={cn(
"fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
render={
<Button
variant="ghost"
className="absolute top-2 right-2"
size="icon-sm"
/>
}
>
<XIcon
/>
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Popup>
</DialogPortal>
)
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2", className)}
{...props}
/>
)
}
function DialogFooter({
className,
showCloseButton = false,
children,
...props
}: React.ComponentProps<"div"> & {
showCloseButton?: boolean
}) {
return (
<div
data-slot="dialog-footer"
className={cn(
"-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 sm:flex-row sm:justify-end",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close render={<Button variant="outline" />}>
Close
</DialogPrimitive.Close>
)}
</div>
)
}
function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn(
"font-heading text-base leading-none font-medium",
className
)}
{...props}
/>
)
}
function DialogDescription({
className,
...props
}: DialogPrimitive.Description.Props) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn(
"text-sm text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
className
)}
{...props}
/>
)
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}
+7 -7
View File
@@ -758,15 +758,15 @@ export const routerCertificates: RouterCertificate[] = [
id: "cert1", name: "mt-msk-core-01-server", serverId: "srv1", id: "cert1", name: "mt-msk-core-01-server", serverId: "srv1",
commonName: "mt-msk-core-01.routerlists.io", commonName: "mt-msk-core-01.routerlists.io",
sans: ["mt-msk-core-01.routerlists.io","10.0.0.1"], sans: ["mt-msk-core-01.routerlists.io","10.0.0.1"],
issuedBy: "RouterLists Internal CA", validFrom: "2024-01-15", issuedBy: "MikrotikManager Internal CA", validFrom: "2024-01-15",
validUntil: "2026-01-15", daysLeft: 261, validUntil: "2026-01-15", daysLeft: 261,
keySize: 4096, usage: ["server"], trusted: true, status: "valid", keySize: 4096, usage: ["server"], trusted: true, status: "valid",
}, },
{ {
id: "cert2", name: "routerlists-internal-ca", serverId: "srv1", id: "cert2", name: "routerlists-internal-ca", serverId: "srv1",
commonName: "RouterLists Internal CA", commonName: "MikrotikManager Internal CA",
sans: [], sans: [],
issuedBy: "RouterLists Internal CA (self-signed)", validFrom: "2023-06-01", issuedBy: "MikrotikManager Internal CA (self-signed)", validFrom: "2023-06-01",
validUntil: "2033-06-01", daysLeft: 2589, validUntil: "2033-06-01", daysLeft: 2589,
keySize: 4096, usage: ["ca","crl-sign"], trusted: true, status: "valid", keySize: 4096, usage: ["ca","crl-sign"], trusted: true, status: "valid",
}, },
@@ -774,7 +774,7 @@ export const routerCertificates: RouterCertificate[] = [
id: "cert3", name: "mt-spb-edge-01-server", serverId: "srv2", id: "cert3", name: "mt-spb-edge-01-server", serverId: "srv2",
commonName: "mt-spb-edge-01.routerlists.io", commonName: "mt-spb-edge-01.routerlists.io",
sans: ["mt-spb-edge-01.routerlists.io","10.0.1.1"], sans: ["mt-spb-edge-01.routerlists.io","10.0.1.1"],
issuedBy: "RouterLists Internal CA", validFrom: "2024-01-15", issuedBy: "MikrotikManager Internal CA", validFrom: "2024-01-15",
validUntil: "2026-01-15", daysLeft: 261, validUntil: "2026-01-15", daysLeft: 261,
keySize: 4096, usage: ["server"], trusted: true, status: "valid", keySize: 4096, usage: ["server"], trusted: true, status: "valid",
}, },
@@ -790,7 +790,7 @@ export const routerCertificates: RouterCertificate[] = [
id: "cert5", name: "wg-client-admin", serverId: "srv1", id: "cert5", name: "wg-client-admin", serverId: "srv1",
commonName: "admin.routerlists.io", commonName: "admin.routerlists.io",
sans: ["admin.routerlists.io"], sans: ["admin.routerlists.io"],
issuedBy: "RouterLists Internal CA", validFrom: "2025-06-01", issuedBy: "MikrotikManager Internal CA", validFrom: "2025-06-01",
validUntil: "2026-06-01", daysLeft: 33, validUntil: "2026-06-01", daysLeft: 33,
keySize: 2048, usage: ["client"], trusted: true, status: "valid", keySize: 2048, usage: ["client"], trusted: true, status: "valid",
}, },
@@ -798,7 +798,7 @@ export const routerCertificates: RouterCertificate[] = [
id: "cert6", name: "mt-ams-edge-01-server", serverId: "srv4", id: "cert6", name: "mt-ams-edge-01-server", serverId: "srv4",
commonName: "mt-ams-edge-01.routerlists.io", commonName: "mt-ams-edge-01.routerlists.io",
sans: ["mt-ams-edge-01.routerlists.io"], sans: ["mt-ams-edge-01.routerlists.io"],
issuedBy: "RouterLists Internal CA", validFrom: "2024-04-01", issuedBy: "MikrotikManager Internal CA", validFrom: "2024-04-01",
validUntil: "2026-05-15", daysLeft: 16, validUntil: "2026-05-15", daysLeft: 16,
keySize: 2048, usage: ["server"], trusted: false, status: "valid", keySize: 2048, usage: ["server"], trusted: false, status: "valid",
}, },
@@ -806,7 +806,7 @@ export const routerCertificates: RouterCertificate[] = [
id: "cert7", name: "mt-msk-lab-01-server", serverId: "srv7", id: "cert7", name: "mt-msk-lab-01-server", serverId: "srv7",
commonName: "mt-msk-lab-01.routerlists.io", commonName: "mt-msk-lab-01.routerlists.io",
sans: ["mt-msk-lab-01.routerlists.io","10.0.0.7"], sans: ["mt-msk-lab-01.routerlists.io","10.0.0.7"],
issuedBy: "RouterLists Internal CA", validFrom: "2025-09-01", issuedBy: "MikrotikManager Internal CA", validFrom: "2025-09-01",
validUntil: "2026-09-01", daysLeft: 125, validUntil: "2026-09-01", daysLeft: 125,
keySize: 4096, usage: ["server"], trusted: true, status: "valid", keySize: 4096, usage: ["server"], trusted: true, status: "valid",
}, },
+14
View File
@@ -0,0 +1,14 @@
const STORAGE_PREFIX = "routerlists:release-modal:"
export function getReleaseModalStorageKey(tag: string): string {
return `${STORAGE_PREFIX}${tag}`
}
export function hasSeenReleaseModal(tag: string): boolean {
if (typeof window === "undefined") return true
return window.localStorage.getItem(getReleaseModalStorageKey(tag)) === "1"
}
export function markReleaseModalSeen(tag: string): void {
window.localStorage.setItem(getReleaseModalStorageKey(tag), "1")
}