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
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:
@@ -18,7 +18,7 @@ alwaysApply: false
|
||||
- Выравнивать UI/UX с **`/servers`** как с главным эталоном (layout, отступы, сетка, типографика, композиция).
|
||||
- Предпочитать **Server Components**; `'use client'` — только при необходимости интерактива (состояние, браузерные API, обработчики).
|
||||
- UI: по возможности **shadcn/ui**; не вводить новый визуальный/UX-паттерн, если на `/servers` уже есть эквивалент.
|
||||
- При завершении задачи предлагать subject коммита по **`release-versioning.mdc`** (`fix:` для patch, `feat:` для minor).
|
||||
- При завершении задачи предлагать subject коммита по **`release-versioning.mdc`** (`fix:` для patch, `feat:` для minor), **описание на русском**.
|
||||
|
||||
## Формат вывода (строго)
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ alwaysApply: true
|
||||
|
||||
**Язык:** все ответы пользователю — **только на русском**.
|
||||
|
||||
**Git / Generate Commit Message:** subject и body коммита — **на русском** (префикс `feat`/`fix`/scope — на английском). Подробности — **`release-versioning.mdc`**.
|
||||
|
||||
## 1. Документация в первую очередь
|
||||
|
||||
- Решения сверять с **официальной** документацией Next.js и shadcn/ui (актуальные версии проекта).
|
||||
|
||||
@@ -1,18 +1,28 @@
|
||||
---
|
||||
description: "Conventional Commits и semver для patch/minor релизов RouterLists"
|
||||
description: "Conventional Commits, semver релизов MikrotikManager и русские git commit message (Generate Commit Message)"
|
||||
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).
|
||||
- Базовая линия: `v1.0.0`. Формат отображения: `v1.2.3`.
|
||||
- **Язык коммитов:** subject и body — **только на русском**; префикс Conventional Commits (`feat`, `fix`, `chore` и т.д.) и scope — на английском.
|
||||
|
||||
## Patch (`1.0.x`)
|
||||
|
||||
`fix:`, `chore:`, `docs:`, `refactor:`, `style:`, `test:`, `build:`, `ci:`
|
||||
|
||||
- Один логический смысл на коммит; subject в повелительном наклонении, до ~72 символов.
|
||||
- Один логический смысл на коммит; subject в повелительном наклонении, до ~72 символов, **на русском**.
|
||||
- Scope по желанию: `fix(servers):`, `chore(ci):`.
|
||||
|
||||
## Minor (`1.x.0`)
|
||||
@@ -23,10 +33,11 @@ alwaysApply: true
|
||||
|
||||
- `fix(servers): исправить опрос API после таймаута` — patch
|
||||
- `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`.
|
||||
|
||||
@@ -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`.
|
||||
@@ -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>`.
|
||||
- UI: версия в sidebar и страница **`/releases`**; manifest `public/release-manifest.json` (в CI подставляется из артефакта).
|
||||
- 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
|
||||
|
||||
|
||||
@@ -185,7 +185,7 @@ function generateCombinedRouterOSConfig(
|
||||
const bhCount = rulesets.reduce((s, r) => s + r.rules.filter(x => x.action === "blackhole").length, 0)
|
||||
|
||||
const lines: string[] = [
|
||||
`# RouterLists — конфиг фильтрации`,
|
||||
`# MikrotikManager — конфиг фильтрации`,
|
||||
`# Дата : ${now}`,
|
||||
`# Серверов : ${withRules.length}`,
|
||||
`# Communities : ${totalRules - bhCount} route + ${bhCount} blackhole`,
|
||||
@@ -271,7 +271,7 @@ function generateCombinedRouterOSConfig(
|
||||
|
||||
lines.push(`/routing filter rule add \\`)
|
||||
lines.push(` chain=bgp-in \\`)
|
||||
lines.push(` comment="RouterLists: ${server.name}" \\`)
|
||||
lines.push(` comment="MikrotikManager: ${server.name}" \\`)
|
||||
lines.push(` rule="`)
|
||||
lines.push(branches)
|
||||
lines.push(` "`)
|
||||
|
||||
@@ -417,7 +417,7 @@ function NativeSelect({ value, onChange, children, className }: {
|
||||
|
||||
function generateRsc(rules: FirewallRule[], timestamp?: string): string {
|
||||
const lines: string[] = []
|
||||
lines.push(`# RouterLists Firewall Export`)
|
||||
lines.push(`# MikrotikManager Firewall Export`)
|
||||
if (timestamp) lines.push(`# Сгенерировано: ${timestamp}`)
|
||||
lines.push(`# Правил: ${rules.length}`)
|
||||
lines.push("")
|
||||
|
||||
@@ -54,7 +54,7 @@ function generateRosCommands(t: GreTunnel, serverById: Record<string, Server>):
|
||||
const srv = serverById[t.serverId]
|
||||
|
||||
lines.push(`# Сервер: ${srv?.name ?? t.serverId} (${srv?.host ?? ""})`)
|
||||
lines.push(`# Сгенерировано RouterLists · ${new Date().toLocaleDateString("ru")}`)
|
||||
lines.push(`# Сгенерировано MikrotikManager · ${new Date().toLocaleDateString("ru")}`)
|
||||
lines.push("")
|
||||
|
||||
// GRE interface
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { AppSidebar } from "@/components/app-sidebar"
|
||||
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"
|
||||
import { CommandPalette } from "@/components/command-palette"
|
||||
import { ReleaseNotesModal } from "@/components/release-notes-modal"
|
||||
import { DataSourceProvider } from "@/lib/data-source"
|
||||
import { EvoBGPProvider } from "@/lib/evobgp-context"
|
||||
|
||||
@@ -12,6 +13,7 @@ export default function MainLayout({ children }: { children: React.ReactNode })
|
||||
<AppSidebar />
|
||||
<SidebarInset className="h-svh overflow-hidden">{children}</SidebarInset>
|
||||
<CommandPalette />
|
||||
<ReleaseNotesModal />
|
||||
</SidebarProvider>
|
||||
</EvoBGPProvider>
|
||||
</DataSourceProvider>
|
||||
|
||||
@@ -50,7 +50,7 @@ export default function ReleasesPage() {
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Текущая версия</CardTitle>
|
||||
<CardDescription>Сборка RouterLists, опубликованная через CI/CD.</CardDescription>
|
||||
<CardDescription>Сборка MikrotikManager, опубликованная через CI/CD.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ const geistMono = Geist_Mono({
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "RouterLists",
|
||||
title: "MikrotikManager",
|
||||
description: "MikroTik network management platform",
|
||||
};
|
||||
|
||||
|
||||
@@ -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`
|
||||
}
|
||||
@@ -5,6 +5,10 @@ import { filterRules, recursiveRoutes, servers } from "../db/schema.js"
|
||||
import { MikrotikClient } from "../services/mikrotik.js"
|
||||
import { parseDbServerId } from "../utils/server-id.js"
|
||||
import { appendEvent } from "../modules/events/service/events-service.js"
|
||||
import {
|
||||
hasManagedCommentPrefix,
|
||||
managedComment,
|
||||
} from "../managed-markers.js"
|
||||
|
||||
type ServerRow = typeof servers.$inferSelect
|
||||
|
||||
@@ -595,11 +599,11 @@ const filtersRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
|
||||
const isInBgpIn = (r: RosFilterRule) =>
|
||||
(r.chain ?? "").trim().toLowerCase() === "bgp-in"
|
||||
const managedComment = `RouterLists: ${server.name || server.host}`
|
||||
const managedCommentValue = managedComment(server.name || server.host)
|
||||
|
||||
// Уже созданное нами правило — будем PATCH'ить, чтобы сохранить ID/позицию в цепочке.
|
||||
const managedRule = existing.find(
|
||||
r => isInBgpIn(r) && (r.comment ?? "").startsWith("RouterLists:"),
|
||||
r => isInBgpIn(r) && hasManagedCommentPrefix(r.comment ?? ""),
|
||||
)
|
||||
|
||||
// Конфликтующие легаси-правила в bgp-in (без нашего comment, но с bgp-communities) —
|
||||
@@ -608,7 +612,7 @@ const filtersRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
const conflictIds = existing
|
||||
.filter(r =>
|
||||
isInBgpIn(r) &&
|
||||
!(r.comment ?? "").startsWith("RouterLists:") &&
|
||||
!hasManagedCommentPrefix(r.comment ?? "") &&
|
||||
/bgp-communities/i.test(r.rule ?? ""),
|
||||
)
|
||||
.map(r => r[".id"])
|
||||
@@ -641,7 +645,7 @@ const filtersRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
`/routing/filter/rule/${encodeURIComponent(managedRule[".id"])}`,
|
||||
{
|
||||
chain: "bgp-in",
|
||||
comment: managedComment,
|
||||
comment: managedCommentValue,
|
||||
rule: ruleBody,
|
||||
disabled: "no",
|
||||
},
|
||||
@@ -650,7 +654,7 @@ const filtersRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
} else {
|
||||
await client.post("/routing/filter/rule/add", {
|
||||
chain: "bgp-in",
|
||||
comment: managedComment,
|
||||
comment: managedCommentValue,
|
||||
rule: ruleBody,
|
||||
})
|
||||
app.log.info({ serverId: server.id }, "bgp-in rule created")
|
||||
|
||||
@@ -4,6 +4,10 @@ import { db } from "../db/index.js"
|
||||
import { recursiveRoutes, servers } from "../db/schema.js"
|
||||
import { MikrotikClient } from "../services/mikrotik.js"
|
||||
import { parseDbServerId } from "../utils/server-id.js"
|
||||
import {
|
||||
hasManagedRecursiveComment,
|
||||
managedRecursiveComment,
|
||||
} from "../managed-markers.js"
|
||||
|
||||
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.routingTable ? { "routing-table": route.routingTable } : {}),
|
||||
...(route.checkGateway ? { "check-gateway": route.checkGateway } : {}),
|
||||
comment: route.comment
|
||||
? `RouterLists:recursive ${route.comment}`
|
||||
: "RouterLists:recursive",
|
||||
comment: managedRecursiveComment(route.comment),
|
||||
disabled: route.disabled ? "true" : "false",
|
||||
}
|
||||
}
|
||||
@@ -235,7 +237,7 @@ const recursiveRoutesPlugin: FastifyPluginAsyncZod = async (app) => {
|
||||
try {
|
||||
const client = MikrotikClient.fromServer(server)
|
||||
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) {
|
||||
if (!r[".id"]) continue
|
||||
await client.delete(`/ip/route/${encodeURIComponent(r[".id"])}`)
|
||||
|
||||
@@ -227,7 +227,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
<RouterIcon className="size-4" />
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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
@@ -758,15 +758,15 @@ export const routerCertificates: RouterCertificate[] = [
|
||||
id: "cert1", name: "mt-msk-core-01-server", serverId: "srv1",
|
||||
commonName: "mt-msk-core-01.routerlists.io",
|
||||
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,
|
||||
keySize: 4096, usage: ["server"], trusted: true, status: "valid",
|
||||
},
|
||||
{
|
||||
id: "cert2", name: "routerlists-internal-ca", serverId: "srv1",
|
||||
commonName: "RouterLists Internal CA",
|
||||
commonName: "MikrotikManager Internal CA",
|
||||
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,
|
||||
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",
|
||||
commonName: "mt-spb-edge-01.routerlists.io",
|
||||
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,
|
||||
keySize: 4096, usage: ["server"], trusted: true, status: "valid",
|
||||
},
|
||||
@@ -790,7 +790,7 @@ export const routerCertificates: RouterCertificate[] = [
|
||||
id: "cert5", name: "wg-client-admin", serverId: "srv1",
|
||||
commonName: "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,
|
||||
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",
|
||||
commonName: "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,
|
||||
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",
|
||||
commonName: "mt-msk-lab-01.routerlists.io",
|
||||
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,
|
||||
keySize: 4096, usage: ["server"], trusted: true, status: "valid",
|
||||
},
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
Reference in New Issue
Block a user