Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bfceb07a23 | ||
|
|
e7a8ad0910 |
@@ -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"])
|
||||||
@@ -139,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())
|
||||||
@@ -210,7 +228,7 @@ function main() {
|
|||||||
if (commits.length === 0) {
|
if (commits.length === 0) {
|
||||||
const deployTag = latestTag ?? `v${baseFromTag}`
|
const deployTag = latestTag ?? `v${baseFromTag}`
|
||||||
const deployVersion = baseFromTag
|
const deployVersion = baseFromTag
|
||||||
const displayCommits = latestTag ? getCommitsSince(getPreviousTag(latestTag)) : []
|
const displayCommits = getRecentCommits()
|
||||||
const publishedAt = latestTag ? getTagPublishedAt(latestTag) : null
|
const publishedAt = latestTag ? getTagPublishedAt(latestTag) : null
|
||||||
|
|
||||||
mkdirSync(outputDir, { recursive: true })
|
mkdirSync(outputDir, { recursive: true })
|
||||||
@@ -244,7 +262,7 @@ function main() {
|
|||||||
tag,
|
tag,
|
||||||
publishedAt,
|
publishedAt,
|
||||||
releaseUrl,
|
releaseUrl,
|
||||||
commits: toManifestCommits(commits),
|
commits: toManifestCommits(getRecentCommits()),
|
||||||
}
|
}
|
||||||
|
|
||||||
mkdirSync(outputDir, { recursive: true })
|
mkdirSync(outputDir, { recursive: 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)
|
||||||
@@ -7,9 +7,10 @@ alwaysApply: true
|
|||||||
|
|
||||||
При **Generate Commit Message**, автогенерации subject и предложении `git commit`:
|
При **Generate Commit Message**, автогенерации subject и предложении `git commit`:
|
||||||
|
|
||||||
## Язык
|
## Обязательно
|
||||||
|
|
||||||
- После `:` — **только русский**. Английский в subject/body запрещён.
|
- Subject и body — **только на русском** (после `:` в conventional commit).
|
||||||
|
- Локальный hook `.githooks/commit-msg` **отклоняет** коммиты без кириллицы в subject.
|
||||||
- Префикс и scope — английский: `fix(ui):`, `chore(rules):`.
|
- Префикс и scope — английский: `fix(ui):`, `chore(rules):`.
|
||||||
|
|
||||||
## Тип: сначала не `feat`
|
## Тип: сначала не `feat`
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -7,5 +7,6 @@
|
|||||||
1. Subject после `:` — **только на русском**.
|
1. Subject после `:` — **только на русском**.
|
||||||
2. **`feat` не по умолчанию.** Правка UI/бага/CI → `fix`; правила/README/workflow → `chore`/`ci`; новая пользовательская фича → `feat`.
|
2. **`feat` не по умолчанию.** Правка UI/бага/CI → `fix`; правила/README/workflow → `chore`/`ci`; новая пользовательская фича → `feat`.
|
||||||
3. Формат: `fix(ui): выровнять модалку релиза`.
|
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`.
|
Полные правила: `.cursor/rules/commit-messages-ru.mdc`, semver — `.cursor/rules/release-versioning.mdc`.
|
||||||
|
|||||||
@@ -826,8 +826,8 @@ const TABS: Array<{ id: BgpTab; label: string; icon: React.ReactNode }> = [
|
|||||||
export default function BgpPage() {
|
export default function BgpPage() {
|
||||||
const [activeTab, setActiveTab] = useState<BgpTab>("sessions")
|
const [activeTab, setActiveTab] = useState<BgpTab>("sessions")
|
||||||
|
|
||||||
const { mode, backendUrl, backendStatus } = useDataSource()
|
const { mode, backendUrl } = useDataSource()
|
||||||
const isLive = mode === "live" && backendStatus === true
|
const isLive = mode === "live"
|
||||||
|
|
||||||
const [liveSessions, setLiveSessions] = useState<BgpSession[]>([])
|
const [liveSessions, setLiveSessions] = useState<BgpSession[]>([])
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
|
|||||||
@@ -289,8 +289,8 @@ function ProbeSourceCell({ probe, catalog }: { probe: PingProbe; catalog: Server
|
|||||||
|
|
||||||
export default function DashboardPage() {
|
export default function DashboardPage() {
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
const { mode, backendUrl, backendStatus } = useDataSource()
|
const { mode, backendUrl } = useDataSource()
|
||||||
const isLive = mode === "live" && backendStatus === true
|
const isLive = mode === "live"
|
||||||
const apiFetch = useMemo(() => makeApiFetch(backendUrl), [backendUrl])
|
const apiFetch = useMemo(() => makeApiFetch(backendUrl), [backendUrl])
|
||||||
|
|
||||||
const [liveProbes, setLiveProbes] = useState<PingProbe[] | null>(null)
|
const [liveProbes, setLiveProbes] = useState<PingProbe[] | null>(null)
|
||||||
|
|||||||
@@ -661,8 +661,8 @@ function RunRowDetail({ r }: { r: SchedulerRunRowDto }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function DataCollectionPage() {
|
export default function DataCollectionPage() {
|
||||||
const { mode, backendUrl, backendStatus } = useDataSource()
|
const { mode, backendUrl } = useDataSource()
|
||||||
const isLive = mode === "live" && backendStatus === true
|
const isLive = mode === "live"
|
||||||
const apiFetch = useMemo(() => makeApiFetch(backendUrl), [backendUrl])
|
const apiFetch = useMemo(() => makeApiFetch(backendUrl), [backendUrl])
|
||||||
|
|
||||||
const [trafficCollector, setTrafficCollector] = useState<CollectorSettingsDto | null>(null)
|
const [trafficCollector, setTrafficCollector] = useState<CollectorSettingsDto | null>(null)
|
||||||
|
|||||||
@@ -272,8 +272,8 @@ type PageTab = "tunnels" | "pools"
|
|||||||
|
|
||||||
// ════════════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
export default function GrePage() {
|
export default function GrePage() {
|
||||||
const { mode, backendUrl, backendStatus } = useDataSource()
|
const { mode, backendUrl } = useDataSource()
|
||||||
const isLive = mode === "live" && backendStatus === true
|
const isLive = mode === "live"
|
||||||
const apiFetch = useMemo(() => makeApiFetch(backendUrl), [backendUrl])
|
const apiFetch = useMemo(() => makeApiFetch(backendUrl), [backendUrl])
|
||||||
|
|
||||||
const [liveServers, setLiveServers] = useState<Server[]>([])
|
const [liveServers, setLiveServers] = useState<Server[]>([])
|
||||||
|
|||||||
@@ -777,12 +777,12 @@ function ZoomControls({ zoom, onZoomIn, onZoomOut, onFit }: {
|
|||||||
// ════════════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
export default function NetworkMapPage() {
|
export default function NetworkMapPage() {
|
||||||
const { mode, backendUrl, backendStatus } = useDataSource()
|
const { mode, backendUrl } = useDataSource()
|
||||||
const useLiveData = mode === "live" && backendStatus === true
|
const useLiveData = mode === "live"
|
||||||
const apiFetch = useMemo(() => makeApiFetch(backendUrl), [backendUrl])
|
const apiFetch = useMemo(() => makeApiFetch(backendUrl), [backendUrl])
|
||||||
|
|
||||||
const [mapServers, setMapServers] = useState<Server[]>(mockServers)
|
const [mapServers, setMapServers] = useState<Server[]>([])
|
||||||
const [mapGreTunnels, setMapGreTunnels] = useState<GreTunnel[]>(mockGreTunnels)
|
const [mapGreTunnels, setMapGreTunnels] = useState<GreTunnel[]>([])
|
||||||
const [speedProbes, setSpeedProbes] = useState<GreSpeedProbeSnapshot[]>([])
|
const [speedProbes, setSpeedProbes] = useState<GreSpeedProbeSnapshot[]>([])
|
||||||
/** FQDN из GRE outer → IPv4 (ответ POST /api/network/resolve-hosts), для матчинга с WAN. */
|
/** FQDN из GRE outer → IPv4 (ответ POST /api/network/resolve-hosts), для матчинга с WAN. */
|
||||||
const [greResolvedIpv4ByHost, setGreResolvedIpv4ByHost] = useState<Record<string, string>>({})
|
const [greResolvedIpv4ByHost, setGreResolvedIpv4ByHost] = useState<Record<string, string>>({})
|
||||||
|
|||||||
@@ -1227,8 +1227,8 @@ export default function OspfPage() {
|
|||||||
const [activeTab, setActiveTab] = useState<OspfTab>("interfaces")
|
const [activeTab, setActiveTab] = useState<OspfTab>("interfaces")
|
||||||
const [filterServerId, setFilterServerId] = useState<string>("all")
|
const [filterServerId, setFilterServerId] = useState<string>("all")
|
||||||
|
|
||||||
const { mode, backendUrl, backendStatus } = useDataSource()
|
const { mode, backendUrl } = useDataSource()
|
||||||
const isLive = mode === "live" && backendStatus === true
|
const isLive = mode === "live"
|
||||||
|
|
||||||
const [liveData, setLiveData] = useState<BackendOspfAll | null>(null)
|
const [liveData, setLiveData] = useState<BackendOspfAll | null>(null)
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
@@ -1291,6 +1291,16 @@ export default function OspfPage() {
|
|||||||
const { nodes: graphNodes, edges: graphEdges } = buildLiveGraph(neighbors)
|
const { nodes: graphNodes, edges: graphEdges } = buildLiveGraph(neighbors)
|
||||||
return { items, neighbors, graphNodes, graphEdges, routerIds, bfdSessions }
|
return { items, neighbors, graphNodes, graphEdges, routerIds, bfdSessions }
|
||||||
}
|
}
|
||||||
|
if (isLive) {
|
||||||
|
return {
|
||||||
|
items: [],
|
||||||
|
neighbors: [],
|
||||||
|
graphNodes: [],
|
||||||
|
graphEdges: [],
|
||||||
|
routerIds: {},
|
||||||
|
bfdSessions: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
items: MOCK_ITEMS,
|
items: MOCK_ITEMS,
|
||||||
neighbors: MOCK_NEIGHBORS,
|
neighbors: MOCK_NEIGHBORS,
|
||||||
@@ -1318,6 +1328,7 @@ export default function OspfPage() {
|
|||||||
}
|
}
|
||||||
return [...seen.values()]
|
return [...seen.values()]
|
||||||
}
|
}
|
||||||
|
if (isLive) return []
|
||||||
// Mock: derive from MOCK_GRAPH_NODES that actually have interfaces
|
// Mock: derive from MOCK_GRAPH_NODES that actually have interfaces
|
||||||
const activeIds = new Set(MOCK_ITEMS.map(i => i.routerKey))
|
const activeIds = new Set(MOCK_ITEMS.map(i => i.routerKey))
|
||||||
const siteCountry: Record<string, string> = { MSK: "RU", SPB: "RU", FRA: "DE", AMS: "NL", SGP: "SG" }
|
const siteCountry: Record<string, string> = { MSK: "RU", SPB: "RU", FRA: "DE", AMS: "NL", SGP: "SG" }
|
||||||
|
|||||||
@@ -729,10 +729,8 @@ export default function ProbesPage() {
|
|||||||
|
|
||||||
const allServers = useMemo(() => {
|
const allServers = useMemo(() => {
|
||||||
if (!isLive) return servers
|
if (!isLive) return servers
|
||||||
if (liveServers.length > 0) return liveServers
|
return liveServers
|
||||||
if (liveLoad === "error") return servers
|
}, [isLive, liveServers])
|
||||||
return []
|
|
||||||
}, [isLive, liveServers, liveLoad])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isLive) {
|
if (!isLive) {
|
||||||
|
|||||||
@@ -498,12 +498,12 @@ function TypeChip({ type }: { type: "jump-host" | "exit-node" | "home-router" })
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function RecursiveRoutesPage() {
|
export default function RecursiveRoutesPage() {
|
||||||
const { mode, backendUrl, backendStatus } = useDataSource()
|
const { mode, backendUrl } = useDataSource()
|
||||||
const isLive = mode === "live" && backendStatus === true
|
const isLive = mode === "live"
|
||||||
const apiFetch = useMemo(() => makeApiFetch(backendUrl), [backendUrl])
|
const apiFetch = useMemo(() => makeApiFetch(backendUrl), [backendUrl])
|
||||||
|
|
||||||
const [servers, setServers] = useState<Server[]>(mockServers)
|
const [servers, setServers] = useState<Server[]>([])
|
||||||
const [selectedServerId, setSelectedServerId] = useState<string>(mockServers[0]?.id ?? "")
|
const [selectedServerId, setSelectedServerId] = useState<string>("")
|
||||||
const [rows, setRows] = useState<RecursiveRouteRow[]>([])
|
const [rows, setRows] = useState<RecursiveRouteRow[]>([])
|
||||||
const [busy, setBusy] = useState<"load" | "save" | "from" | "to" | null>(null)
|
const [busy, setBusy] = useState<"load" | "save" | "from" | "to" | null>(null)
|
||||||
const [search, setSearch] = useState("")
|
const [search, setSearch] = useState("")
|
||||||
|
|||||||
@@ -3,6 +3,11 @@ 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"
|
||||||
@@ -24,7 +29,7 @@ export default function ReleasesPage() {
|
|||||||
const version = getAppVersion()
|
const version = getAppVersion()
|
||||||
const isProdBuild = !version.endsWith("-dev")
|
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">
|
||||||
@@ -68,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>
|
||||||
@@ -84,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>
|
||||||
|
|||||||
@@ -321,10 +321,10 @@ type SheetMode = "add" | "edit"
|
|||||||
|
|
||||||
// ════════════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
export default function ServersPage() {
|
export default function ServersPage() {
|
||||||
const { mode, backendUrl, backendStatus } = useDataSource()
|
const { mode, backendUrl } = useDataSource()
|
||||||
const isLive = mode === "live" && backendStatus === true
|
const isLive = mode === "live"
|
||||||
|
|
||||||
const [serverList, setServerList] = useState<Server[]>(initialServers)
|
const [serverList, setServerList] = useState<Server[]>([])
|
||||||
const [_backendOk, setBackendOk] = useState(false)
|
const [_backendOk, setBackendOk] = useState(false)
|
||||||
const [search, setSearch] = useState("")
|
const [search, setSearch] = useState("")
|
||||||
const [typeFilter, setTypeFilter] = useState<TypeFilter>("all")
|
const [typeFilter, setTypeFilter] = useState<TypeFilter>("all")
|
||||||
@@ -358,7 +358,7 @@ export default function ServersPage() {
|
|||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setBackendOk(false)
|
setBackendOk(false)
|
||||||
setServerList(initialServers)
|
setServerList([])
|
||||||
})
|
})
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [mode, backendUrl])
|
}, [mode, backendUrl])
|
||||||
|
|||||||
@@ -427,8 +427,8 @@ interface BackendServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function TerminalPage() {
|
export default function TerminalPage() {
|
||||||
const { mode, backendUrl, backendStatus } = useDataSource()
|
const { mode, backendUrl } = useDataSource()
|
||||||
const isLive = mode === "live" && backendStatus === true
|
const isLive = mode === "live"
|
||||||
|
|
||||||
// Server list state
|
// Server list state
|
||||||
const [liveServers, setLiveServers] = useState<TermServer[]>([])
|
const [liveServers, setLiveServers] = useState<TermServer[]>([])
|
||||||
|
|||||||
@@ -758,8 +758,8 @@ const SORT_FIELDS: Array<{ field: SortField; label: string; modesOnly?: GroupMod
|
|||||||
]
|
]
|
||||||
|
|
||||||
export default function TrafficPage() {
|
export default function TrafficPage() {
|
||||||
const { mode, backendUrl, backendStatus } = useDataSource()
|
const { mode, backendUrl } = useDataSource()
|
||||||
const isLive = mode === "live" && backendStatus === true
|
const isLive = mode === "live"
|
||||||
const apiFetch = useMemo(() => makeApiFetch(backendUrl), [backendUrl])
|
const apiFetch = useMemo(() => makeApiFetch(backendUrl), [backendUrl])
|
||||||
|
|
||||||
const [groupMode, setGroupMode] = useState<GroupMode>("servers")
|
const [groupMode, setGroupMode] = useState<GroupMode>("servers")
|
||||||
|
|||||||
@@ -1293,15 +1293,15 @@ function ResourcesTab({ resources, serversList, liveApi }: { resources: ServerRe
|
|||||||
// ── page ───────────────────────────────────────────────────────────────────────
|
// ── page ───────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export default function UptimePage() {
|
export default function UptimePage() {
|
||||||
const [allServers, setAllServers] = useState<Server[]>(mockServers)
|
|
||||||
const { mode, backendUrl, backendStatus, checkBackend } = useDataSource()
|
const { mode, backendUrl, backendStatus, checkBackend } = useDataSource()
|
||||||
/** При mode=live всегда ходим на backend. Нельзя требовать backendStatus===true: до ответа /health там undefined — иначе обзор/«Обновить» молчат. */
|
/** При mode=live всегда ходим на backend. Нельзя требовать backendStatus===true: до ответа /health там undefined — иначе обзор/«Обновить» молчат. */
|
||||||
const liveApi = mode === "live"
|
const liveApi = mode === "live"
|
||||||
const apiFetch = useMemo(() => makeApiFetch(backendUrl), [backendUrl])
|
const apiFetch = useMemo(() => makeApiFetch(backendUrl), [backendUrl])
|
||||||
|
|
||||||
|
const [allServers, setAllServers] = useState<Server[]>([])
|
||||||
const [tab, setTab] = useState<"probes" | "resources" | "speed">("probes")
|
const [tab, setTab] = useState<"probes" | "resources" | "speed">("probes")
|
||||||
const [probes, setProbes] = useState<PingProbe[]>(INIT_PROBES)
|
const [probes, setProbes] = useState<PingProbe[]>([])
|
||||||
const [resources, setResources] = useState<ServerResource[]>(INIT_RESOURCES)
|
const [resources, setResources] = useState<ServerResource[]>([])
|
||||||
const [isPaused, setIsPaused] = useState(false)
|
const [isPaused, setIsPaused] = useState(false)
|
||||||
const [search, setSearch] = useState("")
|
const [search, setSearch] = useState("")
|
||||||
const [statusFilter, setStatusFilter] = useState<"all" | "up" | "warn" | "down">("all")
|
const [statusFilter, setStatusFilter] = useState<"all" | "up" | "warn" | "down">("all")
|
||||||
@@ -1334,18 +1334,12 @@ export default function UptimePage() {
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!liveApi) {
|
|
||||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
||||||
setAllServers(mockServers)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
/** В live каталог серверов подгружается вместе с overview (см. loadLiveOverview), иначе после «Серверы» строки ресурсов пропадают из-за .filter(r => r.server). */
|
|
||||||
}, [liveApi])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (liveApi) return
|
if (liveApi) return
|
||||||
queueMicrotask(() => setProbes(mockProbesWithSavedStars(INIT_PROBES)))
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
|
setAllServers(mockServers)
|
||||||
|
setProbes(mockProbesWithSavedStars(INIT_PROBES))
|
||||||
|
setResources(INIT_RESOURCES)
|
||||||
}, [liveApi])
|
}, [liveApi])
|
||||||
|
|
||||||
const isPausedRef = useRef(isPaused)
|
const isPausedRef = useRef(isPaused)
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ type LiveSidebarCounts = SidebarCountsDto & { greTunnels?: number }
|
|||||||
|
|
||||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||||
const { resolvedTheme, setTheme } = useTheme()
|
const { resolvedTheme, setTheme } = useTheme()
|
||||||
const { mode, backendUrl, backendStatus } = useDataSource()
|
const { mode, backendUrl } = useDataSource()
|
||||||
const evo = useEvoBGP()
|
const evo = useEvoBGP()
|
||||||
const [mounted, setMounted] = React.useState(false)
|
const [mounted, setMounted] = React.useState(false)
|
||||||
const [liveCounts, setLiveCounts] = React.useState<LiveSidebarCounts | null>(null)
|
const [liveCounts, setLiveCounts] = React.useState<LiveSidebarCounts | null>(null)
|
||||||
@@ -142,7 +142,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (mode !== "live" || backendStatus !== true) {
|
if (mode !== "live") {
|
||||||
setLiveCounts(null)
|
setLiveCounts(null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -176,15 +176,14 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|||||||
cancelled = true
|
cancelled = true
|
||||||
window.clearInterval(id)
|
window.clearInterval(id)
|
||||||
}
|
}
|
||||||
}, [mode, backendStatus, backendUrl])
|
}, [mode, backendUrl])
|
||||||
|
|
||||||
const navGroups = React.useMemo((): NavGroup[] => {
|
const navGroups = React.useMemo((): NavGroup[] => {
|
||||||
function badgeFor(url: string): string | undefined {
|
function badgeFor(url: string): string | undefined {
|
||||||
if (mode !== "live") return mockBadges[url]
|
if (mode !== "live") return mockBadges[url]
|
||||||
if (backendStatus !== true) return undefined
|
|
||||||
|
|
||||||
if (url === "/domains" || url === "/ip-ranges" || url === "/asns") {
|
if (url === "/domains" || url === "/ip-ranges" || url === "/asns") {
|
||||||
if (!evo.enabled) return mockBadges[url]
|
if (!evo.enabled) return undefined
|
||||||
if (!evo.snapshot) return undefined
|
if (!evo.snapshot) return undefined
|
||||||
if (url === "/domains") return formatSidebarBadgeCount(evo.snapshot.domains.length)
|
if (url === "/domains") return formatSidebarBadgeCount(evo.snapshot.domains.length)
|
||||||
if (url === "/ip-ranges") return formatSidebarBadgeCount(evo.snapshot.ipRanges.length)
|
if (url === "/ip-ranges") return formatSidebarBadgeCount(evo.snapshot.ipRanges.length)
|
||||||
@@ -214,7 +213,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|||||||
badge: mounted ? badgeFor(it.url) : undefined,
|
badge: mounted ? badgeFor(it.url) : undefined,
|
||||||
})),
|
})),
|
||||||
}))
|
}))
|
||||||
}, [mounted, mode, backendStatus, liveCounts, mockBadges, evo.enabled, evo.snapshot])
|
}, [mounted, mode, liveCounts, mockBadges, evo.enabled, evo.snapshot])
|
||||||
|
|
||||||
const isDark = (resolvedTheme ?? "dark") === "dark"
|
const isDark = (resolvedTheme ?? "dark") === "dark"
|
||||||
const appVersionLabel = formatAppVersionLabel(getAppVersion())
|
const appVersionLabel = formatAppVersionLabel(getAppVersion())
|
||||||
|
|||||||
@@ -13,16 +13,14 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog"
|
} from "@/components/ui/dialog"
|
||||||
import { formatAppVersionLabel, getAppVersion } from "@/lib/app-version"
|
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 type { ReleaseManifest } from "@/lib/release-manifest"
|
||||||
import { hasSeenReleaseModal, markReleaseModalSeen } from "@/lib/release-modal-storage"
|
import { hasSeenReleaseModal, markReleaseModalSeen } from "@/lib/release-modal-storage"
|
||||||
|
|
||||||
function formatCommitSubject(subject: string): string {
|
|
||||||
return subject.replace(
|
|
||||||
/^(feat|fix|chore|docs|refactor|style|test|build|ci)(\([^)]+\))?!?:\s*/i,
|
|
||||||
"",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ReleaseNotesModal() {
|
export function ReleaseNotesModal() {
|
||||||
const [open, setOpen] = React.useState(false)
|
const [open, setOpen] = React.useState(false)
|
||||||
const [manifest, setManifest] = React.useState<ReleaseManifest | null>(null)
|
const [manifest, setManifest] = React.useState<ReleaseManifest | null>(null)
|
||||||
@@ -60,6 +58,7 @@ export function ReleaseNotesModal() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const versionLabel = formatAppVersionLabel(manifest?.version ?? getAppVersion())
|
const versionLabel = formatAppVersionLabel(manifest?.version ?? getAppVersion())
|
||||||
|
const commits = getReleaseCommitPreview(manifest?.commits ?? [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||||
@@ -69,14 +68,14 @@ export function ReleaseNotesModal() {
|
|||||||
Что нового в {versionLabel}
|
Что нового в {versionLabel}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription className="text-sm leading-relaxed">
|
<DialogDescription className="text-sm leading-relaxed">
|
||||||
Краткая сводка изменений текущего релиза MikrotikManager.
|
Последние {RELEASE_COMMIT_PREVIEW_LIMIT} коммитов текущей сборки MikrotikManager.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="min-h-0 flex-1 overflow-y-auto px-6 py-4">
|
<div className="min-h-0 flex-1 overflow-y-auto px-6 py-4">
|
||||||
{manifest?.commits.length ? (
|
{commits.length ? (
|
||||||
<ul className="flex flex-col gap-2">
|
<ul className="flex flex-col gap-2">
|
||||||
{manifest.commits.map((commit) => (
|
{commits.map((commit) => (
|
||||||
<li
|
<li
|
||||||
key={commit.sha}
|
key={commit.sha}
|
||||||
className="rounded-lg border bg-card/80 p-3"
|
className="rounded-lg border bg-card/80 p-3"
|
||||||
|
|||||||
@@ -14,6 +14,11 @@ import {
|
|||||||
|
|
||||||
export type DataSourceMode = "mock" | "live"
|
export type DataSourceMode = "mock" | "live"
|
||||||
|
|
||||||
|
/** Режим «Живые» в настройках: моковые данные не подставлять. */
|
||||||
|
export function isLiveDataSourceMode(mode: DataSourceMode): boolean {
|
||||||
|
return mode === "live"
|
||||||
|
}
|
||||||
|
|
||||||
interface DataSourceContextValue {
|
interface DataSourceContextValue {
|
||||||
mode: DataSourceMode
|
mode: DataSourceMode
|
||||||
setMode: (m: DataSourceMode) => void
|
setMode: (m: DataSourceMode) => void
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user