Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7a8ad0910 | ||
|
|
0435608ef9 | ||
|
|
c2a8a08618 | ||
|
|
35876fcd6f |
@@ -1,9 +1,37 @@
|
|||||||
{
|
{
|
||||||
"version": "1.1.0",
|
"version": "1.3.1",
|
||||||
"tag": "v1.1.0",
|
"tag": "v1.3.1",
|
||||||
"publishedAt": "2026-05-12T10:30:14.398Z",
|
"publishedAt": "2026-05-12T18:06:29+07:00",
|
||||||
"releaseUrl": "https://git.shts.su/denozord/MikrotikManager-3/releases/tag/v1.1.0",
|
"releaseUrl": "https://git.shts.su/denozord/MikrotikManager-3/releases/tag/v1.3.1",
|
||||||
"commits": [
|
"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",
|
"sha": "4b92a6eb81ce64fad2ca61f5fa94342959a45349",
|
||||||
"shortSha": "4b92a6e",
|
"shortSha": "4b92a6e",
|
||||||
@@ -17,174 +45,6 @@
|
|||||||
"subject": "feat: update application versioning and enhance release management",
|
"subject": "feat: update application versioning and enhance release management",
|
||||||
"author": "Denozordec",
|
"author": "Denozordec",
|
||||||
"type": "feat"
|
"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"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const outputDir = process.env.RELEASE_OUTPUT_DIR
|
|||||||
const repository = process.env.GITEA_REPOSITORY ?? process.env.GITHUB_REPOSITORY ?? ""
|
const repository = process.env.GITEA_REPOSITORY ?? process.env.GITHUB_REPOSITORY ?? ""
|
||||||
const serverUrl = (process.env.GITEA_SERVER_URL ?? "https://git.shts.su").replace(/\/$/, "")
|
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 =
|
const CONVENTIONAL_RE =
|
||||||
/^(feat|fix|chore|docs|refactor|style|test|build|ci)(\([^)]+\))?!?:\s*(.+)$/i
|
/^(feat|fix|chore|docs|refactor|style|test|build|ci)(\([^)]+\))?!?:\s*(.+)$/i
|
||||||
const MINOR_TYPES = new Set(["feat"])
|
const MINOR_TYPES = new Set(["feat"])
|
||||||
@@ -86,16 +87,50 @@ function parseCommit(subject, body = "") {
|
|||||||
return { type, scope, description, bump }
|
return { type, scope, description, bump }
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLatestTag() {
|
function getAllTags() {
|
||||||
try {
|
try {
|
||||||
const tags = git("tag", "--list", "v*.*.*", "--sort=-v:refname")
|
return git("tag", "--list", "v*.*.*", "--sort=-v:refname")
|
||||||
const first = tags.split("\n").map((line) => line.trim()).find(Boolean)
|
.split("\n")
|
||||||
return first ?? null
|
.map((line) => line.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPreviousTag(tag) {
|
||||||
|
const tags = getAllTags()
|
||||||
|
const index = tags.indexOf(tag)
|
||||||
|
return index >= 0 && index + 1 < tags.length ? tags[index + 1] : null
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTagPublishedAt(tag) {
|
||||||
|
try {
|
||||||
|
return git("log", "-1", "--format=%aI", tag)
|
||||||
} catch {
|
} catch {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toManifestCommits(commits) {
|
||||||
|
return commits.map((commit) => ({
|
||||||
|
sha: commit.sha,
|
||||||
|
shortSha: commit.shortSha,
|
||||||
|
subject: commit.subject,
|
||||||
|
author: commit.author,
|
||||||
|
type: commit.type,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeManifest(manifest) {
|
||||||
|
mkdirSync(outputDir, { recursive: true })
|
||||||
|
writeFileSync(join(outputDir, "release-manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`, "utf8")
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLatestTag() {
|
||||||
|
return getAllTags()[0] ?? null
|
||||||
|
}
|
||||||
|
|
||||||
function getCommitsSince(tag) {
|
function getCommitsSince(tag) {
|
||||||
const range = tag ? `${tag}..HEAD` : "HEAD"
|
const range = tag ? `${tag}..HEAD` : "HEAD"
|
||||||
const raw = git(
|
const raw = git(
|
||||||
@@ -105,6 +140,23 @@ function getCommitsSince(tag) {
|
|||||||
)
|
)
|
||||||
if (!raw) return []
|
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
|
return raw
|
||||||
.split("\x1e")
|
.split("\x1e")
|
||||||
.map((entry) => entry.trim())
|
.map((entry) => entry.trim())
|
||||||
@@ -174,28 +226,25 @@ function main() {
|
|||||||
const commits = getCommitsSince(latestTag)
|
const commits = getCommitsSince(latestTag)
|
||||||
|
|
||||||
if (commits.length === 0) {
|
if (commits.length === 0) {
|
||||||
|
const deployTag = latestTag ?? `v${baseFromTag}`
|
||||||
|
const deployVersion = baseFromTag
|
||||||
|
const displayCommits = getRecentCommits()
|
||||||
|
const publishedAt = latestTag ? getTagPublishedAt(latestTag) : null
|
||||||
|
|
||||||
mkdirSync(outputDir, { recursive: true })
|
mkdirSync(outputDir, { recursive: true })
|
||||||
writeFileSync(join(outputDir, "release_notes.md"), "", "utf8")
|
writeFileSync(join(outputDir, "release_notes.md"), "", "utf8")
|
||||||
writeFileSync(
|
writeManifest({
|
||||||
join(outputDir, "release-manifest.json"),
|
version: deployVersion,
|
||||||
`${JSON.stringify(
|
tag: deployTag,
|
||||||
{
|
publishedAt,
|
||||||
version: baseFromTag,
|
releaseUrl: latestTag ? buildReleaseUrl(latestTag) : "",
|
||||||
tag: latestTag ?? `v${baseFromTag}`,
|
commits: toManifestCommits(displayCommits),
|
||||||
publishedAt: null,
|
})
|
||||||
releaseUrl: latestTag ? buildReleaseUrl(latestTag) : "",
|
|
||||||
commits: [],
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
)}\n`,
|
|
||||||
"utf8",
|
|
||||||
)
|
|
||||||
|
|
||||||
writeGithubOutput({
|
writeGithubOutput({
|
||||||
should_release: "false",
|
should_release: "false",
|
||||||
version: baseFromTag,
|
version: deployVersion,
|
||||||
tag: latestTag ?? `v${baseFromTag}`,
|
tag: deployTag,
|
||||||
bump: "none",
|
bump: "none",
|
||||||
release_url: latestTag ? buildReleaseUrl(latestTag) : "",
|
release_url: latestTag ? buildReleaseUrl(latestTag) : "",
|
||||||
})
|
})
|
||||||
@@ -213,18 +262,12 @@ function main() {
|
|||||||
tag,
|
tag,
|
||||||
publishedAt,
|
publishedAt,
|
||||||
releaseUrl,
|
releaseUrl,
|
||||||
commits: commits.map((commit) => ({
|
commits: toManifestCommits(getRecentCommits()),
|
||||||
sha: commit.sha,
|
|
||||||
shortSha: commit.shortSha,
|
|
||||||
subject: commit.subject,
|
|
||||||
author: commit.author,
|
|
||||||
type: commit.type,
|
|
||||||
})),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mkdirSync(outputDir, { recursive: true })
|
mkdirSync(outputDir, { recursive: true })
|
||||||
writeFileSync(join(outputDir, "release_notes.md"), releaseNotes, "utf8")
|
writeFileSync(join(outputDir, "release_notes.md"), releaseNotes, "utf8")
|
||||||
writeFileSync(join(outputDir, "release-manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`, "utf8")
|
writeManifest(manifest)
|
||||||
|
|
||||||
writeGithubOutput({
|
writeGithubOutput({
|
||||||
should_release: "true",
|
should_release: "true",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
})
|
||||||
@@ -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)
|
||||||
@@ -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, правил, документации и багфиксов.
|
||||||
@@ -27,4 +27,4 @@ alwaysApply: false
|
|||||||
## Связь с проектом
|
## Связь с проектом
|
||||||
|
|
||||||
- Полные стандарты проекта (формат ответа, чеклисты) — в правиле **`next-shadcn-production.mdc`** (обычно уже подключено). Эти правила **дополняют** его ролью в пайплайне, не отменяют.
|
- Полные стандарты проекта (формат ответа, чеклисты) — в правиле **`next-shadcn-production.mdc`** (обычно уже подключено). Эти правила **дополняют** его ролью в пайплайне, не отменяют.
|
||||||
- Сообщения коммитов и ожидаемый semver bump — в **`release-versioning.mdc`**.
|
- Сообщения коммитов: **`commit-messages-ru.mdc`** (язык и `fix` vs `feat`), semver bump — **`release-versioning.mdc`**.
|
||||||
|
|||||||
@@ -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 по **`commit-messages-ru.mdc`**: правки UI/багов → `fix`, инфраструктура/правила → `chore`, новая фича → `feat`; описание **на русском**.
|
||||||
|
|
||||||
## Формат вывода (строго)
|
## Формат вывода (строго)
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ alwaysApply: true
|
|||||||
|
|
||||||
**Язык:** все ответы пользователю — **только на русском**.
|
**Язык:** все ответы пользователю — **только на русском**.
|
||||||
|
|
||||||
|
**Git / Generate Commit Message:** subject и body — **на русском**; `feat` только для новой UX-фичи, иначе `fix`/`chore`. См. **`commit-messages-ru.mdc`** и **`release-versioning.mdc`**.
|
||||||
|
|
||||||
## 1. Документация в первую очередь
|
## 1. Документация в первую очередь
|
||||||
|
|
||||||
- Решения сверять с **официальной** документацией Next.js и shadcn/ui (актуальные версии проекта).
|
- Решения сверять с **официальной** документацией Next.js и shadcn/ui (актуальные версии проекта).
|
||||||
|
|||||||
@@ -1,18 +1,30 @@
|
|||||||
---
|
---
|
||||||
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 (если нужны) — **только на русском**.
|
||||||
|
- В описании после `:` не использовать английские слова: `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).
|
- Версию **не** править вручную в 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 +35,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`.
|
||||||
|
|||||||
@@ -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`.
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
node "$(dirname "$0")/../.ci/scripts/validate-commit-message.mjs" "$1"
|
||||||
@@ -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
@@ -27,7 +27,7 @@ COPY components components
|
|||||||
COPY lib lib
|
COPY lib lib
|
||||||
COPY shared shared
|
COPY shared shared
|
||||||
COPY entities entities
|
COPY entities entities
|
||||||
RUN mkdir -p public
|
COPY public public
|
||||||
COPY hooks hooks
|
COPY hooks hooks
|
||||||
ENV NEXT_TELEMETRY_DISABLED=1
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
RUN npm run build -w @mmapp/contracts \
|
RUN npm run build -w @mmapp/contracts \
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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(` "`)
|
||||||
|
|||||||
@@ -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("")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -3,23 +3,33 @@ import { PageHeader } from "@/components/page-header"
|
|||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { buttonVariants } from "@/components/ui/button"
|
import { buttonVariants } from "@/components/ui/button"
|
||||||
import { formatAppVersionLabel, getAppVersion, getReleaseUrl } from "@/lib/app-version"
|
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 { readReleaseManifest } from "@/lib/release-manifest"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { ExternalLinkIcon } from "lucide-react"
|
import { ExternalLinkIcon } from "lucide-react"
|
||||||
|
|
||||||
function formatPublishedAt(value: string | null): string {
|
export const dynamic = "force-dynamic"
|
||||||
if (!value) return "Локальная сборка"
|
|
||||||
return new Intl.DateTimeFormat("ru-RU", {
|
function formatPublishedAt(value: string | null, isProdBuild: boolean): string {
|
||||||
dateStyle: "long",
|
if (value) {
|
||||||
timeStyle: "short",
|
return new Intl.DateTimeFormat("ru-RU", {
|
||||||
}).format(new Date(value))
|
dateStyle: "long",
|
||||||
|
timeStyle: "short",
|
||||||
|
}).format(new Date(value))
|
||||||
|
}
|
||||||
|
return isProdBuild ? "Прод-сборка" : "Локальная сборка"
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ReleasesPage() {
|
export default function ReleasesPage() {
|
||||||
const manifest = readReleaseManifest()
|
const manifest = readReleaseManifest()
|
||||||
const version = getAppVersion()
|
const version = getAppVersion()
|
||||||
|
const isProdBuild = !version.endsWith("-dev")
|
||||||
const releaseUrl = getReleaseUrl() || manifest.releaseUrl || null
|
const releaseUrl = getReleaseUrl() || manifest.releaseUrl || null
|
||||||
const commits = manifest.commits
|
const commits = getReleaseCommitPreview(manifest.commits)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
@@ -45,12 +55,12 @@ 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">
|
||||||
<span className="text-2xl font-semibold tracking-tight">{formatAppVersionLabel(version)}</span>
|
<span className="text-2xl font-semibold tracking-tight">{formatAppVersionLabel(version)}</span>
|
||||||
<span className="text-sm text-muted-foreground">{formatPublishedAt(manifest.publishedAt)}</span>
|
<span className="text-sm text-muted-foreground">{formatPublishedAt(manifest.publishedAt, isProdBuild)}</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Версия формируется автоматически от базы <span className="font-mono">v1.0.0</span> по Conventional Commits.
|
Версия формируется автоматически от базы <span className="font-mono">v1.0.0</span> по Conventional Commits.
|
||||||
@@ -63,7 +73,7 @@ export default function ReleasesPage() {
|
|||||||
<CardTitle>Сводка коммитов</CardTitle>
|
<CardTitle>Сводка коммитов</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
{commits.length > 0
|
{commits.length > 0
|
||||||
? `Изменения, вошедшие в ${formatAppVersionLabel(manifest.version)}.`
|
? `Последние ${RELEASE_COMMIT_PREVIEW_LIMIT} коммитов в сборке ${formatAppVersionLabel(manifest.version)}.`
|
||||||
: "Для локальной разработки список коммитов пуст. В прод-сборке он заполняется CI."}
|
: "Для локальной разработки список коммитов пуст. В прод-сборке он заполняется CI."}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -79,7 +89,7 @@ export default function ReleasesPage() {
|
|||||||
<span className="font-mono">{commit.shortSha}</span>
|
<span className="font-mono">{commit.shortSha}</span>
|
||||||
<span>{commit.author}</span>
|
<span>{commit.author}</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-1 text-sm">{commit.subject}</p>
|
<p className="mt-1 text-sm">{formatCommitSubject(commit.subject)}</p>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
+1
-1
@@ -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",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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 { 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")
|
||||||
|
|||||||
@@ -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"])}`)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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",
|
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",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "npm run build -w @mmapp/contracts && next build",
|
"build": "npm run build -w @mmapp/contracts && next build",
|
||||||
"postinstall": "npm run build -w @mmapp/contracts",
|
"postinstall": "npm run build -w @mmapp/contracts",
|
||||||
|
"prepare": "node .ci/scripts/ensure-git-hooks.mjs",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint"
|
"lint": "eslint"
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user