Compare commits

...
3 Commits
Author SHA1 Message Date
Denozordec e7a8ad0910 chore(rules): добавить локальный hook для проверки сообщений коммитов
Docker images / prepare-release (push) Successful in 5s
Docker images / backend-image (push) Successful in 2m36s
Docker images / frontend-image (push) Successful in 2m25s
Docker images / updater-image (push) Successful in 37s
Docker images / notify-webhook (push) Has been skipped
Docker images / publish-release (push) Successful in 6s
2026-05-12 18:18:30 +07:00
Denozordec 0435608ef9 chore: обновить правила коммитов и улучшить форматирование релизных заметок
Docker images / prepare-release (push) Successful in 5s
Docker images / backend-image (push) Successful in 1m33s
Docker images / frontend-image (push) Successful in 1m39s
Docker images / updater-image (push) Successful in 37s
Docker images / notify-webhook (push) Has been skipped
Docker images / publish-release (push) Successful in 6s
- Уточнены правила формирования сообщений коммитов, включая использование русского языка и уточнение типов изменений (fix/chore/feat).
- Добавлена функция для форматирования заголовков коммитов в компоненте модалки релизов, улучшая читаемость.
- Обновлены ссылки на правила коммитов в документации для большей ясности.
2026-05-12 18:06:29 +07:00
Denozordec c2a8a08618 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" во всех соответствующих файлах и компонентах.
- Обновлены ссылки и комментарии в документации, чтобы отразить новое название и улучшить ясность.
- Внесены изменения в правила коммитов, чтобы обеспечить использование русского языка в сообщениях коммитов.
2026-05-12 17:53:53 +07:00
30 changed files with 577 additions and 248 deletions
+32 -172
View File
@@ -1,9 +1,37 @@
{
"version": "1.1.0",
"tag": "v1.1.0",
"publishedAt": "2026-05-12T10:30:14.398Z",
"releaseUrl": "https://git.shts.su/denozord/MikrotikManager-3/releases/tag/v1.1.0",
"version": "1.3.1",
"tag": "v1.3.1",
"publishedAt": "2026-05-12T18:06:29+07:00",
"releaseUrl": "https://git.shts.su/denozord/MikrotikManager-3/releases/tag/v1.3.1",
"commits": [
{
"sha": "0435608ef9b0aa792afb7d678856c60058a40659",
"shortSha": "0435608",
"subject": "chore: обновить правила коммитов и улучшить форматирование релизных заметок",
"author": "Denozordec",
"type": "chore"
},
{
"sha": "c2a8a08618db0ba7a98635637d88be9d3f1d8ce1",
"shortSha": "c2a8a08",
"subject": "feat: обновить название проекта и улучшить документацию",
"author": "Denozordec",
"type": "feat"
},
{
"sha": "35876fcd6fd0d13e0419c30c9d112b5a4017ae67",
"shortSha": "35876fc",
"subject": "feat: update Dockerfile and release management scripts",
"author": "Denozordec",
"type": "feat"
},
{
"sha": "7207d1b74992dabaf35cfa9208aa3c7becf42cbd",
"shortSha": "7207d1b",
"subject": "feat: update release notes and manifest for v1.1.0",
"author": "Denozordec",
"type": "feat"
},
{
"sha": "4b92a6eb81ce64fad2ca61f5fa94342959a45349",
"shortSha": "4b92a6e",
@@ -17,174 +45,6 @@
"subject": "feat: update application versioning and enhance release management",
"author": "Denozordec",
"type": "feat"
},
{
"sha": "f282585b525fe787ab6bdbd2f83c9c848b376771",
"shortSha": "f282585",
"subject": "feat: enhance network configuration handling in updater script",
"author": "Denozordec",
"type": "feat"
},
{
"sha": "40e2040fb05717283d0ee2b7d0a6b3315abecf73",
"shortSha": "40e2040",
"subject": "feat: refactor theme management and remove next-themes dependency",
"author": "Denozordec",
"type": "feat"
},
{
"sha": "bb07ba0f69966e4b5533f6b4f0d7d96cef0916f1",
"shortSha": "bb07ba0",
"subject": "feat: implement database backup and restore functionality in settings page",
"author": "Denozordec",
"type": "feat"
},
{
"sha": "1fefed2aa0dbd51f893750e2e179f65bd9386372",
"shortSha": "1fefed2",
"subject": "chore: enhance data source configuration and mock data handling",
"author": "Denozordec",
"type": "chore"
},
{
"sha": "4489dfb2d62e836b5415b9ead409851bef995eaf",
"shortSha": "4489dfb",
"subject": "chore: enhance frontend configuration for backend integration",
"author": "Denozordec",
"type": "chore"
},
{
"sha": "4b240d70d0fbc2143cc657f78f932d003ad5d406",
"shortSha": "4b240d7",
"subject": "chore: add server and server_snapshots tables to database schema",
"author": "Denozordec",
"type": "chore"
},
{
"sha": "63252ae85ed03b772ede2be97a27f1605cc16d0d",
"shortSha": "63252ae",
"subject": "chore: enhance image reference resolution in updater script",
"author": "Denozordec",
"type": "chore"
},
{
"sha": "a359874a42add6b0f4f2f4844ba6e19268bdc05f",
"shortSha": "a359874",
"subject": "chore: enhance Dockerfile for backend to rebuild better-sqlite3",
"author": "Denozordec",
"type": "chore"
},
{
"sha": "4ec247f2c02edfccd775c3e5c644c28844358c90",
"shortSha": "4ec247f",
"subject": "chore: update Docker Compose configuration for data volume management",
"author": "Denozordec",
"type": "chore"
},
{
"sha": "2697a35c0a719396fbe5fdaf6045cb9ca49c28fa",
"shortSha": "2697a35",
"subject": "chore: streamline Docker workflow and enhance build efficiency",
"author": "Denozordec",
"type": "chore"
},
{
"sha": "7295545cde2fc7b0674ba17b3447d89be2f85218",
"shortSha": "7295545",
"subject": "chore: add updater image configuration to Docker workflow",
"author": "Denozordec",
"type": "chore"
},
{
"sha": "a5b5a2a3d390bf4f6fb0986a917d43532844a346",
"shortSha": "a5b5a2a",
"subject": "chore: enhance Dockerfiles and package.json for improved build process",
"author": "Denozordec",
"type": "chore"
},
{
"sha": "01596864dc7801275eefcb67aad67b58785d3a28",
"shortSha": "0159686",
"subject": "chore: update Dockerfiles to ignore scripts during npm install",
"author": "Denozordec",
"type": "chore"
},
{
"sha": "02119a3acde256a92edd06cf4d00337bb53088ee",
"shortSha": "02119a3",
"subject": "chore: configure dynamic image naming for frontend and backend in Docker workflow",
"author": "Denozordec",
"type": "chore"
},
{
"sha": "c36994c36b78063068aff5fa509d1d26c522de51",
"shortSha": "c36994c",
"subject": "chore: update configuration and dependencies",
"author": "Denozordec",
"type": "chore"
},
{
"sha": "dfbaa859ad7616159e373ca50632efd2ad77e8e3",
"shortSha": "dfbaa85",
"subject": "chore: isolate frontend and backend via npm workspaces and @mmapp/contracts",
"author": "Denozordec",
"type": "chore"
},
{
"sha": "b9a75b6831e4ef27726bedbab37f385366c33067",
"shortSha": "b9a75b6",
"subject": "feat: implement internet path functionality with backend support",
"author": "Denozordec",
"type": "feat"
},
{
"sha": "11ad94f67d4a3f5567d3e6e9cd8fe7a587e86df1",
"shortSha": "11ad94f",
"subject": "feat: integrate sonner for toast notifications and enhance UI feedback",
"author": "Denozordec",
"type": "feat"
},
{
"sha": "84ecd4f0619cb434918e3d36d1d2d8d61dc0c759",
"shortSha": "84ecd4f",
"subject": "feat: enhance backups page with live data loading and backup job management",
"author": "Denozordec",
"type": "feat"
},
{
"sha": "6d8379501caf263fa2def609c91601d8b4bb4298",
"shortSha": "6d83795",
"subject": "refactor: replace custom API fetch logic with requestJson utility across multiple pages",
"author": "Denozordec",
"type": "refactor"
},
{
"sha": "5f31bb47fb11592ef1dd1c576105efb07cff584e",
"shortSha": "5f31bb4",
"subject": "chore: synchronize pending app/backend updates and repository hygiene",
"author": "Denozordec",
"type": "chore"
},
{
"sha": "bdb9b72fac5e9cf03faa6e8df463759a9defbf24",
"shortSha": "bdb9b72",
"subject": "Init 2",
"author": "Denozordec",
"type": "other"
},
{
"sha": "ce00c4c671834df0c3277480ecf1a7bbd03cf3f4",
"shortSha": "ce00c4c",
"subject": "Update .gitignore to include additional build and log files",
"author": "Denozordec",
"type": "other"
},
{
"sha": "f3f831653f48c24ae02d15cf10b3e929d4d63e14",
"shortSha": "f3f8316",
"subject": "Init commit",
"author": "Denozordec",
"type": "other"
}
]
}
-41
View File
@@ -1,41 +0,0 @@
# v1.1.0
Версия **1.1.0**.
## feat
- feat: release v1.1.0 with enhancements and refactoring (`4b92a6e`, Denozordec)
- feat: update application versioning and enhance release management (`9ccc845`, Denozordec)
- feat: enhance network configuration handling in updater script (`f282585`, Denozordec)
- feat: refactor theme management and remove next-themes dependency (`40e2040`, Denozordec)
- feat: implement database backup and restore functionality in settings page (`bb07ba0`, Denozordec)
- feat: implement internet path functionality with backend support (`b9a75b6`, Denozordec)
- feat: integrate sonner for toast notifications and enhance UI feedback (`11ad94f`, Denozordec)
- feat: enhance backups page with live data loading and backup job management (`84ecd4f`, Denozordec)
## chore
- chore: enhance data source configuration and mock data handling (`1fefed2`, Denozordec)
- chore: enhance frontend configuration for backend integration (`4489dfb`, Denozordec)
- chore: add server and server_snapshots tables to database schema (`4b240d7`, Denozordec)
- chore: enhance image reference resolution in updater script (`63252ae`, Denozordec)
- chore: enhance Dockerfile for backend to rebuild better-sqlite3 (`a359874`, Denozordec)
- chore: update Docker Compose configuration for data volume management (`4ec247f`, Denozordec)
- chore: streamline Docker workflow and enhance build efficiency (`2697a35`, Denozordec)
- chore: add updater image configuration to Docker workflow (`7295545`, Denozordec)
- chore: enhance Dockerfiles and package.json for improved build process (`a5b5a2a`, Denozordec)
- chore: update Dockerfiles to ignore scripts during npm install (`0159686`, Denozordec)
- chore: configure dynamic image naming for frontend and backend in Docker workflow (`02119a3`, Denozordec)
- chore: update configuration and dependencies (`c36994c`, Denozordec)
- chore: isolate frontend and backend via npm workspaces and @mmapp/contracts (`dfbaa85`, Denozordec)
- chore: synchronize pending app/backend updates and repository hygiene (`5f31bb4`, Denozordec)
## refactor
- refactor: replace custom API fetch logic with requestJson utility across multiple pages (`6d83795`, Denozordec)
## other
- Init 2 (`bdb9b72`, Denozordec)
- Update .gitignore to include additional build and log files (`ce00c4c`, Denozordec)
- Init commit (`f3f8316`, Denozordec)
+20 -2
View File
@@ -13,6 +13,7 @@ const outputDir = process.env.RELEASE_OUTPUT_DIR
const repository = process.env.GITEA_REPOSITORY ?? process.env.GITHUB_REPOSITORY ?? ""
const serverUrl = (process.env.GITEA_SERVER_URL ?? "https://git.shts.su").replace(/\/$/, "")
const MANIFEST_COMMIT_LIMIT = Number(process.env.RELEASE_MANIFEST_COMMIT_LIMIT ?? 6)
const CONVENTIONAL_RE =
/^(feat|fix|chore|docs|refactor|style|test|build|ci)(\([^)]+\))?!?:\s*(.+)$/i
const MINOR_TYPES = new Set(["feat"])
@@ -139,6 +140,23 @@ function getCommitsSince(tag) {
)
if (!raw) return []
return parseCommitLog(raw)
}
function getRecentCommits(limit = MANIFEST_COMMIT_LIMIT) {
const raw = git(
"log",
"-n",
String(limit),
"HEAD",
"--pretty=format:%H%x1f%h%x1f%an%x1f%s%x1f%b%x1e",
)
if (!raw) return []
return parseCommitLog(raw)
}
function parseCommitLog(raw) {
return raw
.split("\x1e")
.map((entry) => entry.trim())
@@ -210,7 +228,7 @@ function main() {
if (commits.length === 0) {
const deployTag = latestTag ?? `v${baseFromTag}`
const deployVersion = baseFromTag
const displayCommits = latestTag ? getCommitsSince(getPreviousTag(latestTag)) : []
const displayCommits = getRecentCommits()
const publishedAt = latestTag ? getTagPublishedAt(latestTag) : null
mkdirSync(outputDir, { recursive: true })
@@ -244,7 +262,7 @@ function main() {
tag,
publishedAt,
releaseUrl,
commits: toManifestCommits(commits),
commits: toManifestCommits(getRecentCommits()),
}
mkdirSync(outputDir, { recursive: true })
+21
View File
@@ -0,0 +1,21 @@
#!/usr/bin/env node
import { execFileSync, spawnSync } from "node:child_process"
import path from "node:path"
import { fileURLToPath } from "node:url"
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..")
const gitCheck = spawnSync("git", ["rev-parse", "--show-toplevel"], {
cwd: repoRoot,
encoding: "utf8",
})
if (gitCheck.status !== 0) {
process.exit(0)
}
const topLevel = gitCheck.stdout.trim()
execFileSync("git", ["config", "core.hooksPath", ".githooks"], {
cwd: topLevel,
stdio: "ignore",
})
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env node
import fs from "node:fs"
const messagePath = process.argv[2]
if (!messagePath) {
console.error("validate-commit-message: не указан файл сообщения коммита")
process.exit(2)
}
if (process.env.SKIP_COMMIT_MESSAGE_RU === "1") {
process.exit(0)
}
const content = fs.readFileSync(messagePath, "utf8")
const subject = content
.split(/\r?\n/)
.map((line) => line.trim())
.find((line) => line.length > 0 && !line.startsWith("#"))
if (!subject) {
process.exit(0)
}
const skipPatterns = [
/^Merge\b/i,
/^Revert\b/i,
/^fixup!/i,
/^squash!/i,
/^amend!/i,
]
if (skipPatterns.some((pattern) => pattern.test(subject))) {
process.exit(0)
}
const cyrillic = /[\u0400-\u04FF\u0500-\u052F]/
if (!cyrillic.test(subject)) {
console.error("")
console.error("Коммит отклонён: subject должен быть на русском.")
console.error("Формат: fix(scope): краткое описание на русском")
console.error("Правила: .cursor/rules/commit-messages-ru.mdc")
console.error("Обход только для аварий: SKIP_COMMIT_MESSAGE_RU=1")
console.error("")
console.error(`Subject: ${subject}`)
process.exit(1)
}
const conventional =
/^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([^)]+\))?!?:\s*(.+)$/
const match = subject.match(conventional)
if (match && !cyrillic.test(match[3])) {
console.error("")
console.error("Коммит отклонён: текст после «:» должен быть на русском.")
console.error(`Subject: ${subject}`)
process.exit(1)
}
process.exit(0)
+34
View File
@@ -0,0 +1,34 @@
---
description: "Generate Commit Message: русский subject, fix vs feat, Conventional Commits"
alwaysApply: true
---
# Git commit message (Generate Commit Message)
При **Generate Commit Message**, автогенерации subject и предложении `git commit`:
## Обязательно
- Subject и body — **только на русском** (после `:` в conventional commit).
- Локальный hook `.githooks/commit-msg` **отклоняет** коммиты без кириллицы в subject.
- Префикс и scope — английский: `fix(ui):`, `chore(rules):`.
## Тип: сначала не `feat`
- **`fix`** — баг, регрессия, правка вёрстки/модалки, некорректное поведение, CI/CD, мелкий дефект.
- **`chore` / `docs` / `ci` / `build`** — правила Cursor, README, workflow, зависимости, рефакторинг без новой UX-фичи.
- **`feat`** — только **новая** пользовательская возможность (страница, API, сценарий), которой не было.
Если сомневаешься между `feat` и `fix` — выбирай **`fix`** или **`chore`**, не `feat`.
## Примеры
- `fix(ui): выровнять модалку сводки релиза`
- `fix(ci): исправить сборку release manifest`
- `chore(rules): усилить правила русских commit message`
- `feat(releases): добавить страницу релизов`
## Запрещено
- `feat: update layout`, `fix: add modal`, `chore: update rules` — английский subject.
- `feat` для правки UI, правил, документации и багфиксов.
+1 -1
View File
@@ -27,4 +27,4 @@ alwaysApply: false
## Связь с проектом
- Полные стандарты проекта (формат ответа, чеклисты) — в правиле **`next-shadcn-production.mdc`** (обычно уже подключено). Эти правила **дополняют** его ролью в пайплайне, не отменяют.
- Сообщения коммитов и ожидаемый semver bump — в **`release-versioning.mdc`**.
- Сообщения коммитов: **`commit-messages-ru.mdc`** (язык и `fix` vs `feat`), semver bump — **`release-versioning.mdc`**.
+1 -1
View File
@@ -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 по **`commit-messages-ru.mdc`**: правки UI/багов → `fix`, инфраструктура/правила → `chore`, новая фича → `feat`; описание **на русском**.
## Формат вывода (строго)
+2
View File
@@ -9,6 +9,8 @@ alwaysApply: true
**Язык:** все ответы пользователю — **только на русском**.
**Git / Generate Commit Message:** subject и body — **на русском**; `feat` только для новой UX-фичи, иначе `fix`/`chore`. См. **`commit-messages-ru.mdc`** и **`release-versioning.mdc`**.
## 1. Документация в первую очередь
- Решения сверять с **официальной** документацией Next.js и shadcn/ui (актуальные версии проекта).
+17 -4
View File
@@ -1,18 +1,30 @@
---
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 (если нужны) — **только на русском**.
- В описании после `:` не использовать английские слова: `add`, `update`, `implement`, `rename` и т.п.
- **`feat` не по умолчанию.** Сначала проверь `fix` / `chore` / `ci` / `docs`; `feat` — только новая пользовательская возможность.
- Примеры: `fix(ui): выровнять модалку релиза`; `chore(rules): усилить правила commit message`; `feat(releases): добавить страницу релизов`.
- Детальный чеклист типов — **`commit-messages-ru.mdc`**.
- Версию **не** править вручную в 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 +35,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`.
+10
View File
@@ -0,0 +1,10 @@
# Git commit message (Generate Commit Message)
Всегда генерируй subject коммита на русском.
- Формат: `тип(scope): описание на русском` — тип и scope на английском, текст после `:` только на русском.
- По умолчанию не `feat`: правки UI/багов/CI → `fix`; правила/README/workflow → `chore`/`ci`; `feat` только для новой пользовательской возможности.
- Примеры: `fix(ui): выровнять модалку релиза`; `chore(rules): усилить правила commit message`.
- Запрещено: `feat: update layout`, `fix: add modal`, любой английский subject.
Подробнее: `.cursor/rules/commit-messages-ru.mdc`, semver: `.cursor/rules/release-versioning.mdc`.
+3
View File
@@ -0,0 +1,3 @@
#!/bin/sh
set -e
node "$(dirname "$0")/../.ci/scripts/validate-commit-message.mjs" "$1"
+12
View File
@@ -0,0 +1,12 @@
# MikrotikManager — инструкции для агента
## Сообщения коммитов
При **Generate Commit Message** и любой автогенерации commit message:
1. Subject после `:`**только на русском**.
2. **`feat` не по умолчанию.** Правка UI/бага/CI → `fix`; правила/README/workflow → `chore`/`ci`; новая пользовательская фича → `feat`.
3. Формат: `fix(ui): выровнять модалку релиза`.
4. Локальный hook `.githooks/commit-msg` отклоняет subject без кириллицы; подключение — `npm install` / `npm run prepare`.
Полные правила: `.cursor/rules/commit-messages-ru.mdc`, 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>`.
- 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
+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 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(` "`)
+1 -1
View File
@@ -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("")
+1 -1
View File
@@ -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
+2
View File
@@ -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>
+9 -4
View File
@@ -3,6 +3,11 @@ import { PageHeader } from "@/components/page-header"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { buttonVariants } from "@/components/ui/button"
import { formatAppVersionLabel, getAppVersion, getReleaseUrl } from "@/lib/app-version"
import {
formatCommitSubject,
getReleaseCommitPreview,
RELEASE_COMMIT_PREVIEW_LIMIT,
} from "@/lib/release-commits"
import { readReleaseManifest } from "@/lib/release-manifest"
import { cn } from "@/lib/utils"
import { ExternalLinkIcon } from "lucide-react"
@@ -24,7 +29,7 @@ export default function ReleasesPage() {
const version = getAppVersion()
const isProdBuild = !version.endsWith("-dev")
const releaseUrl = getReleaseUrl() || manifest.releaseUrl || null
const commits = manifest.commits
const commits = getReleaseCommitPreview(manifest.commits)
return (
<div className="flex flex-col h-full">
@@ -50,7 +55,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">
@@ -68,7 +73,7 @@ export default function ReleasesPage() {
<CardTitle>Сводка коммитов</CardTitle>
<CardDescription>
{commits.length > 0
? `Изменения, вошедшие в ${formatAppVersionLabel(manifest.version)}.`
? `Последние ${RELEASE_COMMIT_PREVIEW_LIMIT} коммитов в сборке ${formatAppVersionLabel(manifest.version)}.`
: "Для локальной разработки список коммитов пуст. В прод-сборке он заполняется CI."}
</CardDescription>
</CardHeader>
@@ -84,7 +89,7 @@ export default function ReleasesPage() {
<span className="font-mono">{commit.shortSha}</span>
<span>{commit.author}</span>
</div>
<p className="mt-1 text-sm">{commit.subject}</p>
<p className="mt-1 text-sm">{formatCommitSubject(commit.subject)}</p>
</li>
))}
</ul>
+1 -1
View File
@@ -16,7 +16,7 @@ const geistMono = Geist_Mono({
});
export const metadata: Metadata = {
title: "RouterLists",
title: "MikrotikManager",
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 { 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")
+6 -4
View File
@@ -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"])}`)
+1 -1
View File
@@ -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>
+112
View File
@@ -0,0 +1,112 @@
"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 {
formatCommitSubject,
getReleaseCommitPreview,
RELEASE_COMMIT_PREVIEW_LIMIT,
} from "@/lib/release-commits"
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())
const commits = getReleaseCommitPreview(manifest?.commits ?? [])
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="flex max-h-[min(85vh,720px)] w-full max-w-[calc(100%-2rem)] flex-col gap-0 overflow-hidden p-0 sm:max-w-lg">
<DialogHeader className="gap-1.5 border-b px-6 py-5 pr-12 text-left">
<DialogTitle className="text-base leading-snug">
Что нового в {versionLabel}
</DialogTitle>
<DialogDescription className="text-sm leading-relaxed">
Последние {RELEASE_COMMIT_PREVIEW_LIMIT} коммитов текущей сборки MikrotikManager.
</DialogDescription>
</DialogHeader>
<div className="min-h-0 flex-1 overflow-y-auto px-6 py-4">
{commits.length ? (
<ul className="flex flex-col gap-2">
{commits.map((commit) => (
<li
key={commit.sha}
className="rounded-lg border bg-card/80 p-3"
>
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
<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="mt-2 text-sm leading-relaxed text-foreground">
{formatCommitSubject(commit.subject)}
</p>
</li>
))}
</ul>
) : null}
</div>
<DialogFooter className="mx-0 mb-0 shrink-0 gap-3 border-t bg-muted/30 px-6 py-4 sm:flex-row sm:items-center 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",
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",
},
+12
View File
@@ -0,0 +1,12 @@
export const RELEASE_COMMIT_PREVIEW_LIMIT = 6
export function formatCommitSubject(subject: string): string {
return subject.replace(
/^(feat|fix|chore|docs|refactor|style|test|build|ci)(\([^)]+\))?!?:\s*/i,
"",
)
}
export function getReleaseCommitPreview<T>(commits: T[], limit = RELEASE_COMMIT_PREVIEW_LIMIT): T[] {
return commits.slice(0, limit)
}
+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")
}
+1
View File
@@ -10,6 +10,7 @@
"dev": "next dev",
"build": "npm run build -w @mmapp/contracts && next build",
"postinstall": "npm run build -w @mmapp/contracts",
"prepare": "node .ci/scripts/ensure-git-hooks.mjs",
"start": "next start",
"lint": "eslint"
},