From c2a8a08618db0ba7a98635637d88be9d3f1d8ce1 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Tue, 12 May 2026 17:53:53 +0700 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D0=BD=D0=B0=D0=B7=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5?= =?UTF-8?q?=20=D0=BF=D1=80=D0=BE=D0=B5=D0=BA=D1=82=D0=B0=20=D0=B8=20=D1=83?= =?UTF-8?q?=D0=BB=D1=83=D1=87=D1=88=D0=B8=D1=82=D1=8C=20=D0=B4=D0=BE=D0=BA?= =?UTF-8?q?=D1=83=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=D1=86=D0=B8=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Изменено название проекта с "RouterLists" на "MikrotikManager" во всех соответствующих файлах и компонентах. - Обновлены ссылки и комментарии в документации, чтобы отразить новое название и улучшить ясность. - Внесены изменения в правила коммитов, чтобы обеспечить использование русского языка в сообщениях коммитов. --- .cursor/rules/ma-implementer.mdc | 2 +- .cursor/rules/next-shadcn-production.mdc | 2 + .cursor/rules/release-versioning.mdc | 19 ++- AGENTS.md | 11 ++ README.md | 2 +- app/(main)/filters/page.tsx | 4 +- app/(main)/firewall/page.tsx | 2 +- app/(main)/gre/page.tsx | 2 +- app/(main)/layout.tsx | 2 + app/(main)/releases/page.tsx | 2 +- app/layout.tsx | 2 +- backend/src/managed-markers.ts | 23 ++++ backend/src/routes/filters.ts | 14 +- backend/src/routes/recursive-routes.ts | 10 +- components/app-sidebar.tsx | 2 +- components/release-notes-modal.tsx | 102 +++++++++++++++ components/ui/dialog.tsx | 160 +++++++++++++++++++++++ lib/data.ts | 14 +- lib/release-modal-storage.ts | 14 ++ 19 files changed, 360 insertions(+), 29 deletions(-) create mode 100644 AGENTS.md create mode 100644 backend/src/managed-markers.ts create mode 100644 components/release-notes-modal.tsx create mode 100644 components/ui/dialog.tsx create mode 100644 lib/release-modal-storage.ts diff --git a/.cursor/rules/ma-implementer.mdc b/.cursor/rules/ma-implementer.mdc index 1420e27..da975ea 100644 --- a/.cursor/rules/ma-implementer.mdc +++ b/.cursor/rules/ma-implementer.mdc @@ -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), **описание на русском**. ## Формат вывода (строго) diff --git a/.cursor/rules/next-shadcn-production.mdc b/.cursor/rules/next-shadcn-production.mdc index 77e9c7e..ed03aa4 100644 --- a/.cursor/rules/next-shadcn-production.mdc +++ b/.cursor/rules/next-shadcn-production.mdc @@ -9,6 +9,8 @@ alwaysApply: true **Язык:** все ответы пользователю — **только на русском**. +**Git / Generate Commit Message:** subject и body коммита — **на русском** (префикс `feat`/`fix`/scope — на английском). Подробности — **`release-versioning.mdc`**. + ## 1. Документация в первую очередь - Решения сверять с **официальной** документацией Next.js и shadcn/ui (актуальные версии проекта). diff --git a/.cursor/rules/release-versioning.mdc b/.cursor/rules/release-versioning.mdc index d9d5217..9335c2b 100644 --- a/.cursor/rules/release-versioning.mdc +++ b/.cursor/rules/release-versioning.mdc @@ -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`. diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..1fb06cb --- /dev/null +++ b/AGENTS.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`. diff --git a/README.md b/README.md index 98c7a4f..fca87a2 100644 --- a/README.md +++ b/README.md @@ -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`, `:`, `:`. - 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 diff --git a/app/(main)/filters/page.tsx b/app/(main)/filters/page.tsx index ea73933..c77700c 100644 --- a/app/(main)/filters/page.tsx +++ b/app/(main)/filters/page.tsx @@ -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(` "`) diff --git a/app/(main)/firewall/page.tsx b/app/(main)/firewall/page.tsx index 64ad946..895c256 100644 --- a/app/(main)/firewall/page.tsx +++ b/app/(main)/firewall/page.tsx @@ -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("") diff --git a/app/(main)/gre/page.tsx b/app/(main)/gre/page.tsx index faff9d7..ed5eccc 100644 --- a/app/(main)/gre/page.tsx +++ b/app/(main)/gre/page.tsx @@ -54,7 +54,7 @@ function generateRosCommands(t: GreTunnel, serverById: Record): 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 diff --git a/app/(main)/layout.tsx b/app/(main)/layout.tsx index e8199b0..1da8fba 100644 --- a/app/(main)/layout.tsx +++ b/app/(main)/layout.tsx @@ -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 }) {children} + diff --git a/app/(main)/releases/page.tsx b/app/(main)/releases/page.tsx index 86bd37c..a04f385 100644 --- a/app/(main)/releases/page.tsx +++ b/app/(main)/releases/page.tsx @@ -50,7 +50,7 @@ export default function ReleasesPage() { Текущая версия - Сборка RouterLists, опубликованная через CI/CD. + Сборка MikrotikManager, опубликованная через CI/CD.
diff --git a/app/layout.tsx b/app/layout.tsx index 328b9b5..1cf9b6a 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -16,7 +16,7 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { - title: "RouterLists", + title: "MikrotikManager", description: "MikroTik network management platform", }; diff --git a/backend/src/managed-markers.ts b/backend/src/managed-markers.ts new file mode 100644 index 0000000..18b4812 --- /dev/null +++ b/backend/src/managed-markers.ts @@ -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` +} diff --git a/backend/src/routes/filters.ts b/backend/src/routes/filters.ts index d648ccf..86444a0 100644 --- a/backend/src/routes/filters.ts +++ b/backend/src/routes/filters.ts @@ -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") diff --git a/backend/src/routes/recursive-routes.ts b/backend/src/routes/recursive-routes.ts index 39caa8b..dbc0f3a 100644 --- a/backend/src/routes/recursive-routes.ts +++ b/backend/src/routes/recursive-routes.ts @@ -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 { ...(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("/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"])}`) diff --git a/components/app-sidebar.tsx b/components/app-sidebar.tsx index 30cd66c..e014e6a 100644 --- a/components/app-sidebar.tsx +++ b/components/app-sidebar.tsx @@ -227,7 +227,7 @@ export function AppSidebar({ ...props }: React.ComponentProps) {
- RouterLists + MikrotikManager {appVersionLabel}
diff --git a/components/release-notes-modal.tsx b/components/release-notes-modal.tsx new file mode 100644 index 0000000..693a2f1 --- /dev/null +++ b/components/release-notes-modal.tsx @@ -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(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 ( + + + + Что нового в {versionLabel} + + Краткая сводка изменений текущего релиза MikrotikManager. + + + +
+ {manifest?.commits.length ? ( +
    + {manifest.commits.map((commit) => ( +
  • +
    + + {commit.type} + + {commit.shortSha} + {commit.author} +
    +

    {commit.subject}

    +
  • + ))} +
+ ) : null} +
+ + + + + +
+
+ ) +} diff --git a/components/ui/dialog.tsx b/components/ui/dialog.tsx new file mode 100644 index 0000000..014f5aa --- /dev/null +++ b/components/ui/dialog.tsx @@ -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 +} + +function DialogTrigger({ ...props }: DialogPrimitive.Trigger.Props) { + return +} + +function DialogPortal({ ...props }: DialogPrimitive.Portal.Props) { + return +} + +function DialogClose({ ...props }: DialogPrimitive.Close.Props) { + return +} + +function DialogOverlay({ + className, + ...props +}: DialogPrimitive.Backdrop.Props) { + return ( + + ) +} + +function DialogContent({ + className, + children, + showCloseButton = true, + ...props +}: DialogPrimitive.Popup.Props & { + showCloseButton?: boolean +}) { + return ( + + + + {children} + {showCloseButton && ( + + } + > + + Close + + )} + + + ) +} + +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogFooter({ + className, + showCloseButton = false, + children, + ...props +}: React.ComponentProps<"div"> & { + showCloseButton?: boolean +}) { + return ( +
+ {children} + {showCloseButton && ( + }> + Close + + )} +
+ ) +} + +function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) { + return ( + + ) +} + +function DialogDescription({ + className, + ...props +}: DialogPrimitive.Description.Props) { + return ( + + ) +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +} diff --git a/lib/data.ts b/lib/data.ts index 16f17d8..d216be8 100644 --- a/lib/data.ts +++ b/lib/data.ts @@ -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", }, diff --git a/lib/release-modal-storage.ts b/lib/release-modal-storage.ts new file mode 100644 index 0000000..1b5e387 --- /dev/null +++ b/lib/release-modal-storage.ts @@ -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") +}