Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e558570967 | ||
|
|
b6b081b04f | ||
|
|
13b4c20148 | ||
|
|
700d415422 |
+25
-37
@@ -1,65 +1,53 @@
|
|||||||
# Gitea Actions
|
# Gitea Actions
|
||||||
|
|
||||||
Workflow-файлы в [workflows/](workflows/):
|
Workflow: [workflows/ci.yaml](workflows/ci.yaml).
|
||||||
|
|
||||||
| Workflow | Триггер | Назначение |
|
| Job | PR | push в main |
|
||||||
|----------|---------|------------|
|
|-----|-----|-------------|
|
||||||
| [ci.yaml](workflows/ci.yaml) | push/PR в `main`/`master` | openapi, web, go, bird2, commitlint (PR) |
|
| changes, openapi, web, go, bird2 | quality gates | quality gates |
|
||||||
| [release.yaml](workflows/release.yaml) | после успешного CI (push в main) | semantic-release → tag, CHANGELOG, Gitea Release |
|
| commitlint | да | — |
|
||||||
| [publish.yaml](workflows/publish.yaml) | push тега `v*` | docker buildx bake → Container Registry |
|
| **release** | — | semantic-release + docker push (один run) |
|
||||||
|
|
||||||
Подробнее о версионировании: [docs/releasing.md](../docs/releasing.md).
|
Подробнее: [docs/releasing.md](../docs/releasing.md).
|
||||||
|
|
||||||
## CI (quality gates)
|
## CI (quality gates)
|
||||||
|
|
||||||
Job **changes** вычисляет флаги по путям в diff (`openapi`, `go`, `web`, `bird_conf`, …). Изменение `.gitea/workflows/*` поднимает полный прогон.
|
Job **changes** вычисляет флаги по путям в diff. Изменение `.gitea/workflows/*` поднимает полный прогон.
|
||||||
|
|
||||||
На **pull request** дополнительно запускается **commitlint** (Conventional Commits).
|
На **pull request** — **commitlint** (Conventional Commits).
|
||||||
|
|
||||||
Требования к runner:
|
Runner: `ubuntu-latest`, **bird2** из apt, Docker для job **release**.
|
||||||
|
|
||||||
- Метка `ubuntu-latest`.
|
## Release (job в ci.yaml)
|
||||||
- Job `bird2` ставит **bird2** из репозитория Ubuntu и вызывает `bird -c … -p`.
|
|
||||||
- Job **publish** требует Docker и доступ к Container Registry.
|
|
||||||
|
|
||||||
## Release (semantic-release)
|
После успешных quality gates на **push в main** job **release**:
|
||||||
|
|
||||||
После успешного CI на push в `main`/`master` workflow **Release** запускает `npx semantic-release` (корневой `package.json`, [.releaserc.json](../.releaserc.json)).
|
1. `npx semantic-release` — тег `vX.Y.Z` на **текущий commit** (без дополнительного commit в main).
|
||||||
|
2. Gitea Release + `CHANGELOG.md` как attachment (не в git).
|
||||||
|
3. `docker buildx bake default --push` с `VERSION=X.Y.Z` — в том же job.
|
||||||
|
|
||||||
Результат при releasable-коммитах:
|
Если releasable-коммитов нет — semantic-release no-op, образы не публикуются.
|
||||||
|
|
||||||
- git tag `vX.Y.Z`;
|
|
||||||
- commit `CHANGELOG.md` с `[skip ci]`;
|
|
||||||
- Gitea Release с release notes.
|
|
||||||
|
|
||||||
### Секреты
|
### Секреты
|
||||||
|
|
||||||
Один PAT — **`ACTIONS_PAT`**: push/t/tags, releases, запись в Container Registry. Если не задан — fallback на **`gitea.token`** (нужны права на releases и packages в Gitea).
|
**`ACTIONS_PAT`**: push tags, releases, Container Registry. Fallback: **`gitea.token`**.
|
||||||
|
|
||||||
`GITEA_URL`: `https://git.shts.su` (задано в workflow).
|
|
||||||
|
|
||||||
## Publish (образы)
|
|
||||||
|
|
||||||
Запускается только при **push git-тега** `vX.Y.Z` (не при каждом push в main).
|
|
||||||
|
|
||||||
Собираются все образы из `deploy/docker/*` (`docker buildx bake default --push`). Semver из тега передаётся в Go-бинарники (`VERSION`, `GIT_SHA`, `BUILD_TIME`).
|
|
||||||
|
|
||||||
Registry: **`git.shts.su`**. Login: `${{ gitea.actor }}` / `${{ secrets.ACTIONS_PAT || gitea.token }}` (тот же **`ACTIONS_PAT`**, что и для release).
|
|
||||||
|
|
||||||
### Теги образов
|
### Теги образов
|
||||||
|
|
||||||
```text
|
```text
|
||||||
git.shts.su/<owner_lowercase>/<имя>:<тег>
|
git.shts.su/<owner>/<имя>:latest
|
||||||
|
git.shts.su/<owner>/<имя>:v1.2.3
|
||||||
|
git.shts.su/<owner>/<имя>:1.2.3
|
||||||
|
git.shts.su/<owner>/<имя>:<short-sha>
|
||||||
|
git.shts.su/<owner>/<имя>:sha-<full-sha>
|
||||||
```
|
```
|
||||||
|
|
||||||
Имена: `evobgp-api`, `evobgp-all`, `evobgp-scheduler`, `evobgp-ingest`, `evobgp-render`, `evobgp-deploy`, `evobgp-node`, `evobgp-web`, `evobgp-web-all`, `evobgp-agent`, `evobgp-bird2`.
|
Имена образов: `evobgp-api`, `evobgp-all`, `evobgp-scheduler`, `evobgp-ingest`, `evobgp-render`, `evobgp-deploy`, `evobgp-node`, `evobgp-web`, `evobgp-web-all`, `evobgp-agent`, `evobgp-bird2`.
|
||||||
|
|
||||||
Теги при релизе `v1.2.3`: `latest`, `v1.2.3`, короткий SHA, `sha-<full_commit>`.
|
|
||||||
|
|
||||||
Пример:
|
Пример:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker pull git.shts.su/myuser/evobgp-api:v1.2.3
|
docker pull git.shts.su/myuser/evobgp-api:1.2.3
|
||||||
```
|
```
|
||||||
|
|
||||||
Подробности — [docs/quickstart.md](../docs/quickstart.md), [deploy/docker/README.md](../deploy/docker/README.md).
|
См. [deploy/docker/README.md](../deploy/docker/README.md), [docs/quickstart.md](../docs/quickstart.md).
|
||||||
|
|||||||
@@ -225,3 +225,88 @@ jobs:
|
|||||||
set -euxo pipefail
|
set -euxo pipefail
|
||||||
npm ci
|
npm ci
|
||||||
npx commitlint --from "${{ github.event.pull_request.base.sha }}" --to "${{ github.event.pull_request.head.sha }}"
|
npx commitlint --from "${{ github.event.pull_request.base.sha }}" --to "${{ github.event.pull_request.head.sha }}"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Один push в main: semantic-release (тег на текущий commit, без доп. commit) + docker push.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
release:
|
||||||
|
needs: [changes, openapi, web, go, bird2]
|
||||||
|
if: >-
|
||||||
|
always() &&
|
||||||
|
github.event_name == 'push' &&
|
||||||
|
(github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master') &&
|
||||||
|
needs.changes.result == 'success' &&
|
||||||
|
(needs.openapi.result == 'success' || needs.openapi.result == 'skipped') &&
|
||||||
|
(needs.web.result == 'success' || needs.web.result == 'skipped') &&
|
||||||
|
(needs.go.result == 'success' || needs.go.result == 'skipped') &&
|
||||||
|
(needs.bird2.result == 'success' || needs.bird2.result == 'skipped')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ secrets.ACTIONS_PAT || gitea.token }}
|
||||||
|
persist-credentials: true
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "22"
|
||||||
|
cache: npm
|
||||||
|
cache-dependency-path: package-lock.json
|
||||||
|
- name: Install release tooling
|
||||||
|
run: npm ci
|
||||||
|
- name: Semantic release
|
||||||
|
run: npx semantic-release
|
||||||
|
env:
|
||||||
|
GITEA_URL: https://git.shts.su
|
||||||
|
GITEA_TOKEN: ${{ secrets.ACTIONS_PAT || gitea.token }}
|
||||||
|
- name: Detect new release
|
||||||
|
id: rel
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
if [ -f .release-version ]; then
|
||||||
|
echo "version=$(tr -d '[:space:]' < .release-version)" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "released=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "released=false" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "No releasable commits — skipping image publish"
|
||||||
|
fi
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
if: steps.rel.outputs.released == 'true'
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
- name: Prepare image metadata
|
||||||
|
if: steps.rel.outputs.released == 'true'
|
||||||
|
id: meta
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
echo "version=${{ steps.rel.outputs.version }}" >> "$GITHUB_OUTPUT"
|
||||||
|
owner_lc="$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')"
|
||||||
|
echo "owner_lc=$owner_lc" >> "$GITHUB_OUTPUT"
|
||||||
|
short_sha="$(echo '${{ github.sha }}' | cut -c1-7)"
|
||||||
|
echo "short_sha=$short_sha" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "build_time=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
|
||||||
|
- name: Log in to Gitea Registry
|
||||||
|
if: steps.rel.outputs.released == 'true'
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: git.shts.su
|
||||||
|
username: ${{ gitea.actor }}
|
||||||
|
password: ${{ secrets.ACTIONS_PAT || gitea.token }}
|
||||||
|
- name: Build and push images (bake)
|
||||||
|
if: steps.rel.outputs.released == 'true'
|
||||||
|
env:
|
||||||
|
REGISTRY: git.shts.su/${{ steps.meta.outputs.owner_lc }}
|
||||||
|
IMAGE_TAG: latest
|
||||||
|
VERSION: ${{ steps.meta.outputs.version }}
|
||||||
|
SHORT_SHA: ${{ steps.meta.outputs.short_sha }}
|
||||||
|
SHA_FULL: ${{ github.sha }}
|
||||||
|
BUILD_TIME: ${{ steps.meta.outputs.build_time }}
|
||||||
|
CACHE_REF_GO: git.shts.su/${{ steps.meta.outputs.owner_lc }}/evobgp-buildcache:go-buildcache
|
||||||
|
CACHE_REF_WEB: git.shts.su/${{ steps.meta.outputs.owner_lc }}/evobgp-buildcache:web-buildcache
|
||||||
|
BUILDX_BAKE_ENTITLEMENTS_FS: "0"
|
||||||
|
run: |
|
||||||
|
set -euxo pipefail
|
||||||
|
cd "${{ github.workspace }}/deploy/docker"
|
||||||
|
sh write-bake-override.sh
|
||||||
|
docker buildx bake --allow=fs.read="${{ github.workspace }}" \
|
||||||
|
-f docker-bake.hcl -f docker-bake.override.hcl default --push
|
||||||
|
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
name: Publish
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags: ["v*"]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
publish-images:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
- name: Prepare image metadata
|
|
||||||
id: meta
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
ref="${GITHUB_REF_NAME:-}"
|
|
||||||
version="${ref#v}"
|
|
||||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
|
||||||
owner_lc="$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')"
|
|
||||||
echo "owner_lc=$owner_lc" >> "$GITHUB_OUTPUT"
|
|
||||||
short_sha="$(echo '${{ github.sha }}' | cut -c1-7)"
|
|
||||||
echo "short_sha=$short_sha" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "build_time=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
|
|
||||||
- name: Log in to Gitea Registry
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: git.shts.su
|
|
||||||
username: ${{ gitea.actor }}
|
|
||||||
password: ${{ secrets.ACTIONS_PAT || gitea.token }}
|
|
||||||
- name: Build and push images (bake)
|
|
||||||
env:
|
|
||||||
REGISTRY: git.shts.su/${{ steps.meta.outputs.owner_lc }}
|
|
||||||
IMAGE_TAG: latest
|
|
||||||
VERSION: ${{ steps.meta.outputs.version }}
|
|
||||||
SHORT_SHA: ${{ steps.meta.outputs.short_sha }}
|
|
||||||
SHA_FULL: ${{ github.sha }}
|
|
||||||
BUILD_TIME: ${{ steps.meta.outputs.build_time }}
|
|
||||||
CACHE_REF_GO: git.shts.su/${{ steps.meta.outputs.owner_lc }}/evobgp-buildcache:go-buildcache
|
|
||||||
CACHE_REF_WEB: git.shts.su/${{ steps.meta.outputs.owner_lc }}/evobgp-buildcache:web-buildcache
|
|
||||||
BUILDX_BAKE_ENTITLEMENTS_FS: "0"
|
|
||||||
run: |
|
|
||||||
set -euxo pipefail
|
|
||||||
cd "${{ github.workspace }}/deploy/docker"
|
|
||||||
sh write-bake-override.sh
|
|
||||||
docker buildx bake --allow=fs.read="${{ github.workspace }}" \
|
|
||||||
-f docker-bake.hcl -f docker-bake.override.hcl default --push
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
name: Release
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_run:
|
|
||||||
workflows: [CI]
|
|
||||||
types: [completed]
|
|
||||||
branches: [main, master]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release:
|
|
||||||
if: >-
|
|
||||||
github.event.workflow_run.conclusion == 'success' &&
|
|
||||||
github.event.workflow_run.event == 'push'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
ref: ${{ github.event.workflow_run.head_sha }}
|
|
||||||
fetch-depth: 0
|
|
||||||
token: ${{ secrets.ACTIONS_PAT || gitea.token }}
|
|
||||||
persist-credentials: true
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: "22"
|
|
||||||
cache: npm
|
|
||||||
cache-dependency-path: package-lock.json
|
|
||||||
- name: Install release tooling
|
|
||||||
run: npm ci
|
|
||||||
- name: Semantic release
|
|
||||||
run: npx semantic-release
|
|
||||||
env:
|
|
||||||
GITEA_URL: https://git.shts.su
|
|
||||||
GITEA_TOKEN: ${{ secrets.ACTIONS_PAT || gitea.token }}
|
|
||||||
@@ -4,6 +4,10 @@ node_modules/
|
|||||||
# Generated by deploy/docker/write-bake-override.sh (CI/local bake)
|
# Generated by deploy/docker/write-bake-override.sh (CI/local bake)
|
||||||
deploy/docker/docker-bake.override.hcl
|
deploy/docker/docker-bake.override.hcl
|
||||||
|
|
||||||
|
# CI-only markers (not committed)
|
||||||
|
.release-version
|
||||||
|
CHANGELOG.md
|
||||||
|
|
||||||
# OS
|
# OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|||||||
+2
-3
@@ -27,10 +27,9 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"@semantic-release/git",
|
"@semantic-release/exec",
|
||||||
{
|
{
|
||||||
"assets": ["CHANGELOG.md"],
|
"successCmd": "echo ${nextRelease.version} > .release-version"
|
||||||
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -50,6 +50,6 @@ docker compose --profile reference up -d
|
|||||||
|
|
||||||
- [docs/releasing.md](docs/releasing.md) — пайплайн, commit conventions, секреты CI
|
- [docs/releasing.md](docs/releasing.md) — пайплайн, commit conventions, секреты CI
|
||||||
- API: `GET /version` и `GET /v1/version` (поле `version`)
|
- API: `GET /version` и `GET /v1/version` (поле `version`)
|
||||||
- Docker-образы публикуются по git-тегу `vX.Y.Z` (workflow **Publish**)
|
- Docker-образы: теги `latest`, `vX.Y.Z`, `X.Y.Z` — в **том же CI run**, что и релиз (job `release`)
|
||||||
|
|
||||||
Лицензия и условия использования — по политике владельца репозитория.
|
Лицензия и условия использования — по политике владельца репозитория.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
Сборка образов — **`docker buildx bake`** (`deploy/docker/docker-bake.hcl`), не отдельные `docker build`.
|
Сборка образов — **`docker buildx bake`** (`deploy/docker/docker-bake.hcl`), не отдельные `docker build`.
|
||||||
|
|
||||||
**Publish:** push git-тега `vX.Y.Z` запускает [.gitea/workflows/publish.yaml](../.gitea/workflows/publish.yaml) — образы с semver-тегом, `latest`, `sha-*`. Переменная `VERSION` из тега попадает в Go-бинарники через `-ldflags`.
|
**Publish:** push в `main` после quality gates — job **release** в [.gitea/workflows/ci.yaml](../.gitea/workflows/ci.yaml): semantic-release + bake с `VERSION` из релиза.
|
||||||
|
|
||||||
Локально BuildKit также кэширует `/go/pkg/mod` и `~/.cache/go-build` через `RUN --mount=type=cache`.
|
Локально BuildKit также кэширует `/go/pkg/mod` и `~/.cache/go-build` через `RUN --mount=type=cache`.
|
||||||
|
|
||||||
|
|||||||
@@ -149,7 +149,10 @@ function "image-tags" {
|
|||||||
"${REGISTRY}/${name}:${IMAGE_TAG}",
|
"${REGISTRY}/${name}:${IMAGE_TAG}",
|
||||||
"${REGISTRY}/${name}:${SHORT_SHA}",
|
"${REGISTRY}/${name}:${SHORT_SHA}",
|
||||||
],
|
],
|
||||||
notequal(VERSION, "dev") && notequal(VERSION, "") ? ["${REGISTRY}/${name}:v${VERSION}"] : []
|
notequal(VERSION, "dev") && notequal(VERSION, "") ? [
|
||||||
|
"${REGISTRY}/${name}:v${VERSION}",
|
||||||
|
"${REGISTRY}/${name}:${VERSION}",
|
||||||
|
] : []
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+22
-33
@@ -15,39 +15,35 @@ EvoBGP использует [Conventional Commits](https://www.conventionalcommi
|
|||||||
|
|
||||||
Подробные правила сообщений коммитов: [.cursor/rules/conventional-commits.mdc](../.cursor/rules/conventional-commits.mdc).
|
Подробные правила сообщений коммитов: [.cursor/rules/conventional-commits.mdc](../.cursor/rules/conventional-commits.mdc).
|
||||||
|
|
||||||
## CI-пайплайн
|
## CI-пайплайн (один push в main)
|
||||||
|
|
||||||
```text
|
```text
|
||||||
push/merge в main
|
push/merge в main
|
||||||
→ CI (openapi, web, go, bird2, commitlint на PR)
|
→ CI: openapi, web, go, bird2 (параллельно)
|
||||||
→ Release (semantic-release после успешного CI)
|
→ job release (в том же workflow, после quality gates):
|
||||||
→ git tag vX.Y.Z
|
→ semantic-release: git tag vX.Y.Z на текущий commit (без доп. commit)
|
||||||
→ CHANGELOG.md + commit [skip ci]
|
→ Gitea Release + CHANGELOG.md как attachment
|
||||||
→ Gitea Release с notes
|
→ docker buildx bake с VERSION=X.Y.Z
|
||||||
→ Publish (push тега v*)
|
→ образы: latest, vX.Y.Z, X.Y.Z, sha-*, короткий SHA
|
||||||
→ docker buildx bake с VERSION из тега
|
|
||||||
→ образы: latest, vX.Y.Z, sha-*, короткий SHA
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Workflow-файлы:
|
Pull request: только quality gates + commitlint; релиз и образы **не** публикуются.
|
||||||
|
|
||||||
- [.gitea/workflows/ci.yaml](../.gitea/workflows/ci.yaml) — quality gates
|
Workflow: [.gitea/workflows/ci.yaml](../.gitea/workflows/ci.yaml) (job **release**).
|
||||||
- [.gitea/workflows/release.yaml](../.gitea/workflows/release.yaml) — semantic-release
|
|
||||||
- [.gitea/workflows/publish.yaml](../.gitea/workflows/publish.yaml) — публикация образов
|
|
||||||
|
|
||||||
Конфиг semantic-release: [.releaserc.json](../.releaserc.json).
|
Конфиг semantic-release: [.releaserc.json](../.releaserc.json) — без `@semantic-release/git` (CHANGELOG не коммитится в репозиторий).
|
||||||
|
|
||||||
## Секреты Gitea
|
## Секреты Gitea
|
||||||
|
|
||||||
Один PAT в репозитории — **`ACTIONS_PAT`** (Settings → Actions → Secrets). Используется для semantic-release, push тегов/CHANGELOG и docker login в registry.
|
Один PAT — **`ACTIONS_PAT`** (Settings → Actions → Secrets).
|
||||||
|
|
||||||
| Право PAT | Зачем |
|
| Право PAT | Зачем |
|
||||||
|-----------|--------|
|
|-----------|--------|
|
||||||
| push / write repository | commit `CHANGELOG.md`, push тегов |
|
| push tags | git tag `vX.Y.Z` на commit merge |
|
||||||
| releases | Gitea Release через semantic-release |
|
| releases | Gitea Release + notes |
|
||||||
| packages (Container Registry) | workflow **Publish** |
|
| packages (Container Registry) | push образов |
|
||||||
|
|
||||||
Если `ACTIONS_PAT` не задан, workflow пробует **`gitea.token`** job-токен (нужны права на releases и packages в настройках Gitea).
|
Fallback: **`gitea.token`** (нужны права на releases и packages).
|
||||||
|
|
||||||
## Источник правды для версии в runtime
|
## Источник правды для версии в runtime
|
||||||
|
|
||||||
@@ -56,28 +52,21 @@ Semver из git-тега пробрасывается в Go-бинарники
|
|||||||
- локально (`go run`) — `version: "dev"`
|
- локально (`go run`) — `version: "dev"`
|
||||||
- в образе после релиза — совпадает с тегом (например `1.2.3`)
|
- в образе после релиза — совпадает с тегом (например `1.2.3`)
|
||||||
|
|
||||||
API:
|
API: `GET /version`, `GET /v1/version` — поля `version`, `git_sha`, `build_time`.
|
||||||
|
|
||||||
- `GET /version`
|
|
||||||
- `GET /v1/version`
|
|
||||||
|
|
||||||
Ответ включает `version`, `git_sha`, `build_time`; поле `api_version` — deprecated alias.
|
|
||||||
|
|
||||||
Web UI показывает версию из API (footer sidebar, страница «Мониторинг»).
|
Web UI показывает версию из API (footer sidebar, страница «Мониторинг»).
|
||||||
|
|
||||||
## CHANGELOG
|
## CHANGELOG
|
||||||
|
|
||||||
Файл [`CHANGELOG.md`](../CHANGELOG.md) создаётся и обновляется semantic-release. Копия прикрепляется к Gitea Release.
|
Release notes — в Gitea Release; файл `CHANGELOG.md` генерируется в CI и прикрепляется как asset, **не** попадает в git history.
|
||||||
|
|
||||||
## Проверка после релиза
|
## Проверка после релиза
|
||||||
|
|
||||||
1. В Gitea: тег `vX.Y.Z` и Release с notes.
|
1. Один run workflow **CI** на push в main: job **release** зелёный.
|
||||||
2. Container Registry: образы с тегом `vX.Y.Z`.
|
2. Gitea: тег `vX.Y.Z` на том же commit, что и merge; Release с notes.
|
||||||
3. `curl http://localhost:8080/version` → `"version":"X.Y.Z"`.
|
3. Container Registry: `evobgp-api:vX.Y.Z`, `evobgp-api:X.Y.Z`, `evobgp-api:latest`.
|
||||||
4. Footer Web UI → `vX.Y.Z`.
|
4. `curl http://localhost:8080/version` → `"version":"X.Y.Z"`.
|
||||||
|
|
||||||
## Первый релиз (bootstrap)
|
## Первый релиз (bootstrap)
|
||||||
|
|
||||||
Merge PR в `main` с conventional commit типа `feat(release): ...` (не `chore` — иначе релиз не создастся). Ожидаемый результат: **v1.0.0**.
|
Merge в `main` с `feat(release): ...` → **v1.0.0** в том же CI run.
|
||||||
|
|
||||||
После merge убедитесь, что workflow **Release** завершился успешно и workflow **Publish** собрал образы по тегу.
|
|
||||||
|
|||||||
Generated
+168
-139
@@ -11,7 +11,7 @@
|
|||||||
"@markwylde/semantic-release-gitea": "^2.2.0",
|
"@markwylde/semantic-release-gitea": "^2.2.0",
|
||||||
"@semantic-release/changelog": "^6.0.3",
|
"@semantic-release/changelog": "^6.0.3",
|
||||||
"@semantic-release/commit-analyzer": "^13.0.1",
|
"@semantic-release/commit-analyzer": "^13.0.1",
|
||||||
"@semantic-release/git": "^10.0.1",
|
"@semantic-release/exec": "^7.0.0",
|
||||||
"@semantic-release/release-notes-generator": "^14.0.3",
|
"@semantic-release/release-notes-generator": "^14.0.3",
|
||||||
"semantic-release": "^25.0.2"
|
"semantic-release": "^25.0.2"
|
||||||
}
|
}
|
||||||
@@ -761,40 +761,28 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@semantic-release/git": {
|
"node_modules/@semantic-release/exec": {
|
||||||
"version": "10.0.1",
|
"version": "7.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@semantic-release/git/-/git-10.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@semantic-release/exec/-/exec-7.1.0.tgz",
|
||||||
"integrity": "sha512-eWrx5KguUcU2wUPaO6sfvZI0wPafUKAMNC18aXY4EnNcrZL86dEmpNVnC9uMpGZkmZJ9EfCVJBQx4pV4EMGT1w==",
|
"integrity": "sha512-4ycZ2atgEUutspPZ2hxO6z8JoQt4+y/kkHvfZ1cZxgl9WKJId1xPj+UadwInj+gMn2Gsv+fLnbrZ4s+6tK2TFQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@semantic-release/error": "^3.0.0",
|
"@semantic-release/error": "^4.0.0",
|
||||||
"aggregate-error": "^3.0.0",
|
"aggregate-error": "^3.0.0",
|
||||||
"debug": "^4.0.0",
|
"debug": "^4.0.0",
|
||||||
"dir-glob": "^3.0.0",
|
"execa": "^9.0.0",
|
||||||
"execa": "^5.0.0",
|
"lodash-es": "^4.17.21",
|
||||||
"lodash": "^4.17.4",
|
"parse-json": "^8.0.0"
|
||||||
"micromatch": "^4.0.0",
|
|
||||||
"p-reduce": "^2.0.0"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.17"
|
"node": ">=20.8.1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"semantic-release": ">=18.0.0"
|
"semantic-release": ">=24.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@semantic-release/git/node_modules/@semantic-release/error": {
|
"node_modules/@semantic-release/exec/node_modules/aggregate-error": {
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@semantic-release/error/-/error-3.0.0.tgz",
|
|
||||||
"integrity": "sha512-5hiM4Un+tpl4cKw3lV4UgzJj+SmfNIDCLLw0TepzQxz9ZGV5ixnqkzIVF+3tp0ZHgcMKE+VNGHJjEeyFG2dcSw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.17"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@semantic-release/git/node_modules/aggregate-error": {
|
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
|
||||||
"integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
|
"integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
|
||||||
@@ -808,7 +796,7 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@semantic-release/git/node_modules/clean-stack": {
|
"node_modules/@semantic-release/exec/node_modules/clean-stack": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
|
||||||
"integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
|
"integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
|
||||||
@@ -818,7 +806,61 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@semantic-release/git/node_modules/indent-string": {
|
"node_modules/@semantic-release/exec/node_modules/execa": {
|
||||||
|
"version": "9.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz",
|
||||||
|
"integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@sindresorhus/merge-streams": "^4.0.0",
|
||||||
|
"cross-spawn": "^7.0.6",
|
||||||
|
"figures": "^6.1.0",
|
||||||
|
"get-stream": "^9.0.0",
|
||||||
|
"human-signals": "^8.0.1",
|
||||||
|
"is-plain-obj": "^4.1.0",
|
||||||
|
"is-stream": "^4.0.1",
|
||||||
|
"npm-run-path": "^6.0.0",
|
||||||
|
"pretty-ms": "^9.2.0",
|
||||||
|
"signal-exit": "^4.1.0",
|
||||||
|
"strip-final-newline": "^4.0.0",
|
||||||
|
"yoctocolors": "^2.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.19.0 || >=20.5.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@semantic-release/exec/node_modules/get-stream": {
|
||||||
|
"version": "9.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz",
|
||||||
|
"integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@sec-ant/readable-stream": "^0.4.1",
|
||||||
|
"is-stream": "^4.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@semantic-release/exec/node_modules/human-signals": {
|
||||||
|
"version": "8.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz",
|
||||||
|
"integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.18.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@semantic-release/exec/node_modules/indent-string": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
|
||||||
"integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
|
"integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
|
||||||
@@ -828,6 +870,106 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@semantic-release/exec/node_modules/is-stream": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@semantic-release/exec/node_modules/npm-run-path": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"path-key": "^4.0.0",
|
||||||
|
"unicorn-magic": "^0.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@semantic-release/exec/node_modules/parse-json": {
|
||||||
|
"version": "8.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz",
|
||||||
|
"integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/code-frame": "^7.26.2",
|
||||||
|
"index-to-position": "^1.1.0",
|
||||||
|
"type-fest": "^4.39.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@semantic-release/exec/node_modules/path-key": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@semantic-release/exec/node_modules/signal-exit": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@semantic-release/exec/node_modules/strip-final-newline": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@semantic-release/exec/node_modules/unicorn-magic": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@semantic-release/github": {
|
"node_modules/@semantic-release/github": {
|
||||||
"version": "12.0.8",
|
"version": "12.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/@semantic-release/github/-/github-12.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@semantic-release/github/-/github-12.0.8.tgz",
|
||||||
@@ -2335,30 +2477,6 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/execa": {
|
|
||||||
"version": "5.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
|
|
||||||
"integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"cross-spawn": "^7.0.3",
|
|
||||||
"get-stream": "^6.0.0",
|
|
||||||
"human-signals": "^2.1.0",
|
|
||||||
"is-stream": "^2.0.0",
|
|
||||||
"merge-stream": "^2.0.0",
|
|
||||||
"npm-run-path": "^4.0.1",
|
|
||||||
"onetime": "^5.1.2",
|
|
||||||
"signal-exit": "^3.0.3",
|
|
||||||
"strip-final-newline": "^2.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/fast-content-type-parse": {
|
"node_modules/fast-content-type-parse": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz",
|
||||||
@@ -2970,16 +3088,6 @@
|
|||||||
"node": ">= 20"
|
"node": ">= 20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/human-signals": {
|
|
||||||
"version": "2.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
|
|
||||||
"integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10.17.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ignore": {
|
"node_modules/ignore": {
|
||||||
"version": "7.0.5",
|
"version": "7.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
|
||||||
@@ -3171,19 +3279,6 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/is-stream": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
|
||||||
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/is-text-path": {
|
"node_modules/is-text-path": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-2.0.0.tgz",
|
||||||
@@ -3683,16 +3778,6 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mimic-fn": {
|
|
||||||
"version": "2.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
|
|
||||||
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/mimic-response": {
|
"node_modules/mimic-response": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz",
|
||||||
@@ -3961,19 +4046,6 @@
|
|||||||
"node": "^20.17.0 || >=22.9.0"
|
"node": "^20.17.0 || >=22.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/npm-run-path": {
|
|
||||||
"version": "4.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
|
|
||||||
"integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"path-key": "^3.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/npm/node_modules/@gar/promise-retry": {
|
"node_modules/npm/node_modules/@gar/promise-retry": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -5736,22 +5808,6 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/onetime": {
|
|
||||||
"version": "5.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
|
|
||||||
"integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"mimic-fn": "^2.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/p-cancelable": {
|
"node_modules/p-cancelable": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-4.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-4.0.1.tgz",
|
||||||
@@ -5852,16 +5908,6 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/p-reduce": {
|
|
||||||
"version": "2.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/p-reduce/-/p-reduce-2.1.0.tgz",
|
|
||||||
"integrity": "sha512-2USApvnsutq8uoxZBGbbWM0JIYLiEMJ9RlaN7fAzVNb9OZN0SHjjTTfIcb667XynS5Y1VhwDJVDa72TnPzAYWw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/p-timeout": {
|
"node_modules/p-timeout": {
|
||||||
"version": "6.1.4",
|
"version": "6.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-6.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-6.1.4.tgz",
|
||||||
@@ -6850,13 +6896,6 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/signal-exit": {
|
|
||||||
"version": "3.0.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
|
|
||||||
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "ISC"
|
|
||||||
},
|
|
||||||
"node_modules/signale": {
|
"node_modules/signale": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/signale/-/signale-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/signale/-/signale-1.4.0.tgz",
|
||||||
@@ -7121,16 +7160,6 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/strip-final-newline": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/strip-json-comments": {
|
"node_modules/strip-json-comments": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@
|
|||||||
"@markwylde/semantic-release-gitea": "^2.2.0",
|
"@markwylde/semantic-release-gitea": "^2.2.0",
|
||||||
"@semantic-release/changelog": "^6.0.3",
|
"@semantic-release/changelog": "^6.0.3",
|
||||||
"@semantic-release/commit-analyzer": "^13.0.1",
|
"@semantic-release/commit-analyzer": "^13.0.1",
|
||||||
"@semantic-release/git": "^10.0.1",
|
"@semantic-release/exec": "^7.0.0",
|
||||||
"@semantic-release/release-notes-generator": "^14.0.3",
|
"@semantic-release/release-notes-generator": "^14.0.3",
|
||||||
"semantic-release": "^25.0.2"
|
"semantic-release": "^25.0.2"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/** Ответ GET /v1/version (см. docs/openapi.yaml VersionInfo). */
|
||||||
|
export type VersionInfo = {
|
||||||
|
version?: string;
|
||||||
|
api_version?: string;
|
||||||
|
git_sha?: string;
|
||||||
|
build_time?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PLACEHOLDER_SHA = new Set(['', 'unknown']);
|
||||||
|
|
||||||
|
function nonEmptyString(value: unknown): string {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const s = value.trim();
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
if (value == null) return '';
|
||||||
|
return String(value).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Semver или legacy api_version; пустая строка, если полей нет. */
|
||||||
|
export function resolveVersionString(info: VersionInfo | null | undefined): string {
|
||||||
|
if (!info) return '';
|
||||||
|
return nonEmptyString(info.version) || nonEmptyString(info.api_version);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Короткий git SHA; пусто для placeholder «unknown». */
|
||||||
|
export function resolveGitSha(info: VersionInfo | null | undefined): string {
|
||||||
|
if (!info) return '';
|
||||||
|
const sha = nonEmptyString(info.git_sha);
|
||||||
|
if (!sha || PLACEHOLDER_SHA.has(sha.toLowerCase())) return '';
|
||||||
|
return sha;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Заголовок карточки «Версия» на /monitoring. */
|
||||||
|
export function formatVersionHeadline(info: VersionInfo | null | undefined): string {
|
||||||
|
if (!info) return '—';
|
||||||
|
const ver = resolveVersionString(info);
|
||||||
|
const sha = resolveGitSha(info);
|
||||||
|
if (ver && sha) return `${ver} (${sha.slice(0, 12)})`;
|
||||||
|
if (ver) return ver;
|
||||||
|
if (sha) return sha.slice(0, 12);
|
||||||
|
return '—';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Подпись в сайдбаре (v1.2.3). */
|
||||||
|
export function formatVersionSidebarLabel(info: VersionInfo | null | undefined): string | null {
|
||||||
|
const ver = resolveVersionString(info);
|
||||||
|
return ver ? `v${ver}` : null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
/** Pure-helpers для страницы /monitoring. */
|
||||||
|
|
||||||
|
export type OverallStatus = 'ok' | 'warn' | 'error' | 'unknown';
|
||||||
|
|
||||||
|
export type JobsKpi = { running: number; failed: number; total: number };
|
||||||
|
|
||||||
|
export type ReadyStatus = { status: string; checks?: Record<string, unknown> };
|
||||||
|
|
||||||
|
export type HealthStatus = { ok: boolean; status?: string; error?: string };
|
||||||
|
|
||||||
|
export type CheckBadge = {
|
||||||
|
label: string;
|
||||||
|
variant: 'default' | 'secondary' | 'destructive' | 'outline';
|
||||||
|
class?: string;
|
||||||
|
hint?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function toErrorMessage(error: unknown): string {
|
||||||
|
if (error instanceof Error && error.message.trim() !== '') return error.message;
|
||||||
|
return 'Ошибка запроса';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function summarizeJobsStatuses(items: Array<{ status?: string }>): JobsKpi {
|
||||||
|
let running = 0;
|
||||||
|
let failed = 0;
|
||||||
|
for (const row of items) {
|
||||||
|
const status = String(row.status ?? '').toLowerCase();
|
||||||
|
if (status === 'queued' || status === 'running' || status === 'cancel_requested') {
|
||||||
|
running += 1;
|
||||||
|
}
|
||||||
|
if (status === 'failed' || status === 'error' || status === 'canceled') {
|
||||||
|
failed += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { running, failed, total: items.length };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deriveOverallStatus(input: {
|
||||||
|
health: HealthStatus | null;
|
||||||
|
ready: ReadyStatus | null;
|
||||||
|
jobs: JobsKpi | null;
|
||||||
|
birdConfigured: boolean;
|
||||||
|
birdHealthy: boolean | null;
|
||||||
|
}): OverallStatus {
|
||||||
|
const { health, ready, jobs, birdConfigured, birdHealthy } = input;
|
||||||
|
if (health === null && ready === null && jobs === null) return 'unknown';
|
||||||
|
if (!health?.ok) return 'error';
|
||||||
|
if (ready !== null && ready.status !== 'ready') return 'error';
|
||||||
|
if (jobs !== null && jobs.failed > 0) return 'warn';
|
||||||
|
if (birdConfigured && birdHealthy === false) return 'warn';
|
||||||
|
return 'ok';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function overallStatusLabel(status: OverallStatus): string {
|
||||||
|
switch (status) {
|
||||||
|
case 'ok':
|
||||||
|
return 'В норме';
|
||||||
|
case 'warn':
|
||||||
|
return 'Внимание';
|
||||||
|
case 'error':
|
||||||
|
return 'Ошибка';
|
||||||
|
default:
|
||||||
|
return 'Нет данных';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function overallStatusHint(
|
||||||
|
status: OverallStatus,
|
||||||
|
input: {
|
||||||
|
healthOk: boolean | undefined;
|
||||||
|
jobsFailed: number;
|
||||||
|
}
|
||||||
|
): string {
|
||||||
|
if (status === 'unknown') return 'Нет данных. Запустите обновление.';
|
||||||
|
if (status === 'error') {
|
||||||
|
if (!input.healthOk) return 'Проверьте доступность API и логи сервиса.';
|
||||||
|
return 'Readiness не в норме: проверьте postgres/store/jobs.';
|
||||||
|
}
|
||||||
|
if (status === 'warn') {
|
||||||
|
if (input.jobsFailed > 0) return 'Есть ошибки в задачах: откройте операции и последние jobs.';
|
||||||
|
return 'Проверьте BGP-сессии и вывод birdc.';
|
||||||
|
}
|
||||||
|
return 'Критичных отклонений не обнаружено.';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function overallBadgeVariant(status: OverallStatus): CheckBadge['variant'] {
|
||||||
|
if (status === 'ok') return 'default';
|
||||||
|
if (status === 'warn') return 'secondary';
|
||||||
|
if (status === 'error') return 'destructive';
|
||||||
|
return 'outline';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function overallBadgeClass(status: OverallStatus): string | undefined {
|
||||||
|
if (status === 'ok') return 'border-success/30 bg-success/15 text-success';
|
||||||
|
if (status === 'warn') return 'border-warning/30 bg-warning/15 text-warning';
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Нормализация значений ready.checks и health/readiness в badge. */
|
||||||
|
export function checkStatusBadge(value: unknown): CheckBadge {
|
||||||
|
const raw = String(value ?? '').trim();
|
||||||
|
const lower = raw.toLowerCase();
|
||||||
|
|
||||||
|
if (lower === 'ok' || lower === 'ready' || lower === 'true' || lower === 'up') {
|
||||||
|
return {
|
||||||
|
label: 'OK',
|
||||||
|
variant: 'default',
|
||||||
|
class: 'border-success/30 bg-success/15 text-success'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (lower === 'memory') {
|
||||||
|
return {
|
||||||
|
label: 'In-memory',
|
||||||
|
variant: 'secondary',
|
||||||
|
hint: 'Очередь задач в памяти процесса, не shared между воркерами.'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
lower === 'failed' ||
|
||||||
|
lower === 'false' ||
|
||||||
|
lower === 'error' ||
|
||||||
|
lower === 'down' ||
|
||||||
|
lower === 'unavailable'
|
||||||
|
) {
|
||||||
|
return { label: 'Ошибка', variant: 'destructive' };
|
||||||
|
}
|
||||||
|
if (raw === '') {
|
||||||
|
return { label: '—', variant: 'outline' };
|
||||||
|
}
|
||||||
|
return { label: raw, variant: 'outline' };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Человекочитаемое имя проверки readiness. */
|
||||||
|
export function checkDisplayName(key: string): string {
|
||||||
|
switch (key) {
|
||||||
|
case 'postgres':
|
||||||
|
return 'PostgreSQL';
|
||||||
|
case 'store':
|
||||||
|
return 'Хранилище';
|
||||||
|
case 'jobs':
|
||||||
|
return 'Очередь задач';
|
||||||
|
default:
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function livenessBadge(health: HealthStatus | null): CheckBadge {
|
||||||
|
if (health === null) return { label: '—', variant: 'outline' };
|
||||||
|
if (health.ok) {
|
||||||
|
return {
|
||||||
|
label: 'В норме',
|
||||||
|
variant: 'default',
|
||||||
|
class: 'border-success/30 bg-success/15 text-success'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { label: 'Недоступен', variant: 'destructive' };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readinessBadge(ready: ReadyStatus | null): CheckBadge {
|
||||||
|
if (ready === null) return { label: '—', variant: 'outline' };
|
||||||
|
if (ready.status === 'ready') {
|
||||||
|
return {
|
||||||
|
label: 'Готов',
|
||||||
|
variant: 'default',
|
||||||
|
class: 'border-success/30 bg-success/15 text-success'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { label: ready.status || 'Не готов', variant: 'destructive' };
|
||||||
|
}
|
||||||
@@ -1,11 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { apiJSON } from '$lib/api/client.js';
|
import { apiJSON } from '$lib/api/client.js';
|
||||||
|
import { formatVersionSidebarLabel, type VersionInfo } from '$lib/api/version-info.js';
|
||||||
type VersionInfo = {
|
|
||||||
version?: string;
|
|
||||||
api_version?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
let label = $state<string | null>(null);
|
let label = $state<string | null>(null);
|
||||||
|
|
||||||
@@ -13,11 +9,7 @@
|
|||||||
void (async () => {
|
void (async () => {
|
||||||
try {
|
try {
|
||||||
const info = await apiJSON<VersionInfo>('/v1/version');
|
const info = await apiJSON<VersionInfo>('/v1/version');
|
||||||
const ver =
|
label = formatVersionSidebarLabel(info);
|
||||||
(typeof info.version === 'string' && info.version) ||
|
|
||||||
(typeof info.api_version === 'string' && info.api_version) ||
|
|
||||||
'';
|
|
||||||
label = ver ? `v${ver}` : null;
|
|
||||||
} catch {
|
} catch {
|
||||||
label = null;
|
label = null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,54 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
import { resolve } from '$app/paths';
|
||||||
import { apiJSON, apiFetch } from '$lib/api/client.js';
|
import { apiJSON, apiFetch } from '$lib/api/client.js';
|
||||||
import type { BirdStatus, JobsResponse } from '$lib/api/types.js';
|
import type { BirdStatus, JobRow, JobsResponse } from '$lib/api/types.js';
|
||||||
import { Badge } from '$lib/components/ui/badge/index.js';
|
import {
|
||||||
import { Button } from '$lib/components/ui/button/index.js';
|
formatVersionHeadline,
|
||||||
|
resolveVersionString,
|
||||||
|
type VersionInfo
|
||||||
|
} from '$lib/api/version-info.js';
|
||||||
|
import { jobKindTitle } from '$lib/operations/job-kind-label.js';
|
||||||
|
import { jobStatusRu } from '$lib/ui-labels.js';
|
||||||
|
import {
|
||||||
|
checkDisplayName,
|
||||||
|
checkStatusBadge,
|
||||||
|
deriveOverallStatus,
|
||||||
|
livenessBadge,
|
||||||
|
overallBadgeClass,
|
||||||
|
overallBadgeVariant,
|
||||||
|
overallStatusHint,
|
||||||
|
overallStatusLabel,
|
||||||
|
readinessBadge,
|
||||||
|
summarizeJobsStatuses,
|
||||||
|
toErrorMessage,
|
||||||
|
type HealthStatus,
|
||||||
|
type JobsKpi,
|
||||||
|
type ReadyStatus
|
||||||
|
} from '$lib/monitoring/status.js';
|
||||||
|
import { Badge } from '$lib/ui/core/badge/index.js';
|
||||||
|
import { Button } from '$lib/ui/core/button/index.js';
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
CardDescription
|
CardDescription
|
||||||
} from '$lib/components/ui/card/index.js';
|
} from '$lib/ui/core/card/index.js';
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from '$lib/ui/core/alert/index.js';
|
||||||
|
import { Separator } from '$lib/ui/core/separator/index.js';
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow
|
||||||
|
} from '$lib/ui/core/table/index.js';
|
||||||
|
import CardSkeleton from '$lib/ui/patterns/feedback/card-skeleton.svelte';
|
||||||
|
import ScrollPreBlock from '$lib/components/app/scroll-pre-block.svelte';
|
||||||
|
import PageHeader from '$lib/ui/app/page-header/page-header.svelte';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
import RefreshCw from '@lucide/svelte/icons/refresh-cw';
|
import RefreshCw from '@lucide/svelte/icons/refresh-cw';
|
||||||
import Gauge from '@lucide/svelte/icons/gauge';
|
import Gauge from '@lucide/svelte/icons/gauge';
|
||||||
import Activity from '@lucide/svelte/icons/activity';
|
import Activity from '@lucide/svelte/icons/activity';
|
||||||
@@ -18,138 +56,248 @@
|
|||||||
import Server from '@lucide/svelte/icons/server';
|
import Server from '@lucide/svelte/icons/server';
|
||||||
import Hash from '@lucide/svelte/icons/hash';
|
import Hash from '@lucide/svelte/icons/hash';
|
||||||
import AlertTriangle from '@lucide/svelte/icons/alert-triangle';
|
import AlertTriangle from '@lucide/svelte/icons/alert-triangle';
|
||||||
import PageHeader from '$lib/ui/app/page-header/page-header.svelte';
|
import CheckCircle from '@lucide/svelte/icons/check-circle';
|
||||||
|
import XCircle from '@lucide/svelte/icons/x-circle';
|
||||||
|
import ArrowRight from '@lucide/svelte/icons/arrow-right';
|
||||||
|
import HeartPulse from '@lucide/svelte/icons/heart-pulse';
|
||||||
|
import ShieldCheck from '@lucide/svelte/icons/shield-check';
|
||||||
|
import Database from '@lucide/svelte/icons/database';
|
||||||
|
import HardDrive from '@lucide/svelte/icons/hard-drive';
|
||||||
|
import ListTodo from '@lucide/svelte/icons/list-todo';
|
||||||
|
import type { Component } from 'svelte';
|
||||||
|
|
||||||
type ReadyStatus = { status: string; checks?: Record<string, unknown> };
|
let health = $state<HealthStatus | null>(null);
|
||||||
type VersionInfo = {
|
|
||||||
version?: string;
|
|
||||||
git_sha?: string;
|
|
||||||
build_time?: string;
|
|
||||||
[k: string]: unknown;
|
|
||||||
};
|
|
||||||
type JobsKpi = { running: number; failed: number; total: number };
|
|
||||||
|
|
||||||
let health = $state<{ ok: boolean; status?: string; error?: string } | null>(null);
|
|
||||||
let ready = $state<ReadyStatus | null>(null);
|
let ready = $state<ReadyStatus | null>(null);
|
||||||
let version = $state<VersionInfo | null>(null);
|
let version = $state<VersionInfo | null>(null);
|
||||||
let bird = $state<BirdStatus | null>(null);
|
let bird = $state<BirdStatus | null>(null);
|
||||||
let jobs = $state<JobsKpi | null>(null);
|
let jobs = $state<JobsKpi | null>(null);
|
||||||
|
let jobItems = $state<JobRow[]>([]);
|
||||||
let readyError = $state<string | null>(null);
|
let readyError = $state<string | null>(null);
|
||||||
let versionError = $state<string | null>(null);
|
let versionError = $state<string | null>(null);
|
||||||
let birdError = $state<string | null>(null);
|
let birdError = $state<string | null>(null);
|
||||||
let jobsError = $state<string | null>(null);
|
let jobsError = $state<string | null>(null);
|
||||||
let lastUpdated = $state<Date | null>(null);
|
let lastUpdated = $state<Date | null>(null);
|
||||||
let loading = $state(false);
|
let initialLoading = $state(true);
|
||||||
|
let refreshing = $state(false);
|
||||||
|
|
||||||
function toErrorMessage(error: unknown): string {
|
const statAccents = [
|
||||||
if (error instanceof Error && error.message.trim() !== '') return error.message;
|
{
|
||||||
return 'Ошибка запроса';
|
border: 'border-l-chart-1',
|
||||||
}
|
bg: 'bg-chart-1/5',
|
||||||
|
iconBg: 'bg-chart-1/15',
|
||||||
function summarizeJobsStatuses(items: Array<{ status?: string }>): JobsKpi {
|
iconText: 'text-chart-1'
|
||||||
let running = 0;
|
},
|
||||||
let failed = 0;
|
{
|
||||||
for (const row of items) {
|
border: 'border-l-chart-2',
|
||||||
const status = String(row.status ?? '').toLowerCase();
|
bg: 'bg-chart-2/5',
|
||||||
if (status === 'queued' || status === 'running' || status === 'cancel_requested') {
|
iconBg: 'bg-chart-2/15',
|
||||||
running += 1;
|
iconText: 'text-chart-2'
|
||||||
}
|
},
|
||||||
if (status === 'failed' || status === 'error' || status === 'canceled') {
|
{
|
||||||
failed += 1;
|
border: 'border-l-chart-4',
|
||||||
}
|
bg: 'bg-chart-4/5',
|
||||||
|
iconBg: 'bg-chart-4/15',
|
||||||
|
iconText: 'text-chart-4'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
border: 'border-l-info',
|
||||||
|
bg: 'bg-info/10',
|
||||||
|
iconBg: 'bg-info/15',
|
||||||
|
iconText: 'text-info'
|
||||||
}
|
}
|
||||||
return { running, failed, total: items.length };
|
] as const;
|
||||||
}
|
|
||||||
|
|
||||||
function overallBadgeVariant(status: 'ok' | 'warn' | 'error' | 'unknown') {
|
const overallStatus = $derived.by(() =>
|
||||||
if (status === 'ok') return 'default';
|
deriveOverallStatus({
|
||||||
if (status === 'warn') return 'secondary';
|
health,
|
||||||
if (status === 'error') return 'destructive';
|
ready,
|
||||||
return 'outline';
|
jobs,
|
||||||
}
|
birdConfigured: bird?.birdc_configured ?? false,
|
||||||
|
birdHealthy: bird?.healthy ?? null
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const overallStatus = $derived.by(() => {
|
const overallHint = $derived.by(() =>
|
||||||
if (health === null && ready === null && bird === null && jobs === null) return 'unknown';
|
overallStatusHint(overallStatus, {
|
||||||
if (!health?.ok) return 'error';
|
healthOk: health?.ok,
|
||||||
if (ready !== null && ready.status !== 'ready') return 'error';
|
jobsFailed: jobs?.failed ?? 0
|
||||||
if (jobs !== null && jobs.failed > 0) return 'warn';
|
})
|
||||||
if (bird !== null && bird.birdc_configured && bird.healthy === false) return 'warn';
|
);
|
||||||
return 'ok';
|
|
||||||
});
|
|
||||||
|
|
||||||
const overallHint = $derived.by(() => {
|
|
||||||
if (overallStatus === 'unknown') return 'Нет данных. Запустите обновление.';
|
|
||||||
if (overallStatus === 'error') {
|
|
||||||
if (!health?.ok) return 'Проверьте доступность API и логи сервиса.';
|
|
||||||
return 'Readiness не в норме: проверьте postgres/store/jobs.';
|
|
||||||
}
|
|
||||||
if (overallStatus === 'warn') {
|
|
||||||
if (jobs && jobs.failed > 0)
|
|
||||||
return 'Есть ошибки в задачах: откройте операции и последние jobs.';
|
|
||||||
return 'Проверьте BGP-сессии и вывод birdc.';
|
|
||||||
}
|
|
||||||
return 'Критичных отклонений не обнаружено.';
|
|
||||||
});
|
|
||||||
|
|
||||||
const bgpText = $derived.by(() => {
|
const bgpText = $derived.by(() => {
|
||||||
if (!bird) return '—';
|
if (!bird) return '—';
|
||||||
if (!bird.birdc_configured) return 'birdc не настроен';
|
if (!bird.birdc_configured) return '—';
|
||||||
if (bird.error) return 'ошибка birdc';
|
if (bird.error) return '—';
|
||||||
return `${bird.bgp_established}/${bird.bgp_sessions_total}`;
|
return `${bird.bgp_established}/${bird.bgp_sessions_total}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
const versionText = $derived.by(() => {
|
const bgpRatio = $derived.by(() => {
|
||||||
if (!version) return '—';
|
if (!bird?.birdc_configured || bird.bgp_sessions_total <= 0) return null;
|
||||||
const ver =
|
return Math.round((bird.bgp_established / bird.bgp_sessions_total) * 100);
|
||||||
(typeof version.version === 'string' && version.version) ||
|
|
||||||
(typeof version.api_version === 'string' && version.api_version) ||
|
|
||||||
'';
|
|
||||||
const sha =
|
|
||||||
typeof version.git_sha === 'string' && version.git_sha ? version.git_sha : 'unknown';
|
|
||||||
if (ver) return `${ver} (${sha.slice(0, 12)})`;
|
|
||||||
return sha.slice(0, 12);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const versionText = $derived(formatVersionHeadline(version));
|
||||||
|
|
||||||
|
const versionFooter = $derived.by(() => {
|
||||||
|
if (!version) return '';
|
||||||
|
const parts: string[] = [];
|
||||||
|
const ver = resolveVersionString(version);
|
||||||
|
if (ver) parts.push(`version: ${ver}`);
|
||||||
|
if (version.git_sha) parts.push(`git_sha: ${version.git_sha}`);
|
||||||
|
if (version.build_time) parts.push(`build: ${version.build_time}`);
|
||||||
|
return parts.join(' · ');
|
||||||
|
});
|
||||||
|
|
||||||
|
const failedJobs = $derived.by(() =>
|
||||||
|
jobItems
|
||||||
|
.filter((j) => {
|
||||||
|
const s = String(j.status ?? '').toLowerCase();
|
||||||
|
return s === 'failed' || s === 'error' || s === 'canceled';
|
||||||
|
})
|
||||||
|
.slice(0, 5)
|
||||||
|
);
|
||||||
|
|
||||||
|
const checkIconByKey: Record<string, Component> = {
|
||||||
|
postgres: Database,
|
||||||
|
store: HardDrive,
|
||||||
|
jobs: ListTodo
|
||||||
|
};
|
||||||
|
|
||||||
|
const kpiCards = $derived.by(() => [
|
||||||
|
{
|
||||||
|
id: 'overall',
|
||||||
|
label: 'Общий статус',
|
||||||
|
value: initialLoading ? '—' : overallStatusLabel(overallStatus),
|
||||||
|
description: overallHint,
|
||||||
|
icon: Server,
|
||||||
|
accent: statAccents[0],
|
||||||
|
badge: overallStatusLabel(overallStatus),
|
||||||
|
badgeVariant: overallBadgeVariant(overallStatus),
|
||||||
|
badgeClass: overallBadgeClass(overallStatus),
|
||||||
|
error: null as string | null,
|
||||||
|
href: undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bgp',
|
||||||
|
label: 'BGP сессии',
|
||||||
|
value: initialLoading ? '—' : bgpText,
|
||||||
|
description: bird?.birdc_configured
|
||||||
|
? 'Established / total на API-хосте'
|
||||||
|
: (bird?.message ?? 'birdc не настроен на API-хосте'),
|
||||||
|
icon: Bird,
|
||||||
|
accent: statAccents[1],
|
||||||
|
badge: !bird ? '—' : !bird.birdc_configured ? 'N/A' : bird.healthy ? 'В норме' : 'Деградация',
|
||||||
|
badgeVariant: !bird?.birdc_configured
|
||||||
|
? ('outline' as const)
|
||||||
|
: bird?.healthy
|
||||||
|
? ('default' as const)
|
||||||
|
: ('secondary' as const),
|
||||||
|
badgeClass:
|
||||||
|
bird?.birdc_configured && bird?.healthy
|
||||||
|
? 'border-success/30 bg-success/15 text-success'
|
||||||
|
: undefined,
|
||||||
|
error: birdError ?? bird?.error ?? null,
|
||||||
|
href: '/network' as const
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'jobs',
|
||||||
|
label: 'Задачи',
|
||||||
|
value: initialLoading ? '—' : String(jobs?.running ?? '—'),
|
||||||
|
description: `Активных из ${jobs?.total ?? '—'} последних`,
|
||||||
|
icon: Activity,
|
||||||
|
accent: statAccents[2],
|
||||||
|
badge: jobs && jobs.failed > 0 ? `ошибок ${jobs.failed}` : 'без ошибок',
|
||||||
|
badgeVariant: jobs && jobs.failed > 0 ? ('secondary' as const) : ('default' as const),
|
||||||
|
badgeClass:
|
||||||
|
jobs && jobs.failed === 0 ? 'border-success/30 bg-success/15 text-success' : undefined,
|
||||||
|
error: jobsError,
|
||||||
|
href: jobs && jobs.failed > 0 ? ('/operations' as const) : undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'version',
|
||||||
|
label: 'Версия',
|
||||||
|
value: initialLoading ? '—' : versionText,
|
||||||
|
description: versionFooter || 'GET /v1/version',
|
||||||
|
icon: Hash,
|
||||||
|
accent: statAccents[3],
|
||||||
|
badge: version ? 'Загружена' : '—',
|
||||||
|
badgeVariant: version ? ('outline' as const) : ('secondary' as const),
|
||||||
|
badgeClass: undefined,
|
||||||
|
error: versionError,
|
||||||
|
href: undefined
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
async function fetchHealth(): Promise<HealthStatus> {
|
||||||
|
const h = await apiFetch('/v1/health');
|
||||||
|
const hj = (await h.json().catch(() => ({}))) as { status?: string };
|
||||||
|
return { ok: h.ok, status: hj?.status, error: h.ok ? undefined : `HTTP ${h.status}` };
|
||||||
|
}
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
loading = true;
|
if (initialLoading) {
|
||||||
|
/* keep skeleton */
|
||||||
|
} else {
|
||||||
|
refreshing = true;
|
||||||
|
}
|
||||||
readyError = null;
|
readyError = null;
|
||||||
versionError = null;
|
versionError = null;
|
||||||
birdError = null;
|
birdError = null;
|
||||||
jobsError = null;
|
jobsError = null;
|
||||||
try {
|
|
||||||
const h = await apiFetch('/v1/health');
|
const [healthRes, readyRes, versionRes, birdRes, jobsRes] = await Promise.allSettled([
|
||||||
const hj = await h.json().catch(() => ({}));
|
fetchHealth(),
|
||||||
health = { ok: h.ok, status: hj?.status, error: h.ok ? undefined : `HTTP ${h.status}` };
|
apiJSON<ReadyStatus>('/v1/ready'),
|
||||||
} catch (error) {
|
apiJSON<VersionInfo>('/v1/version'),
|
||||||
health = { ok: false, error: toErrorMessage(error) };
|
apiJSON<BirdStatus>('/v1/bird/status'),
|
||||||
|
apiJSON<JobsResponse>('/v1/jobs?limit=100')
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (healthRes.status === 'fulfilled') {
|
||||||
|
health = healthRes.value;
|
||||||
|
} else {
|
||||||
|
health = { ok: false, error: toErrorMessage(healthRes.reason) };
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
ready = await apiJSON<ReadyStatus>('/v1/ready');
|
if (readyRes.status === 'fulfilled') {
|
||||||
} catch (error) {
|
ready = readyRes.value;
|
||||||
|
} else {
|
||||||
ready = null;
|
ready = null;
|
||||||
readyError = toErrorMessage(error);
|
readyError = toErrorMessage(readyRes.reason);
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
version = await apiJSON<VersionInfo>('/v1/version');
|
if (versionRes.status === 'fulfilled') {
|
||||||
} catch (error) {
|
version = versionRes.value;
|
||||||
|
} else {
|
||||||
version = null;
|
version = null;
|
||||||
versionError = toErrorMessage(error);
|
versionError = toErrorMessage(versionRes.reason);
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
bird = await apiJSON<BirdStatus>('/v1/bird/status');
|
if (birdRes.status === 'fulfilled') {
|
||||||
} catch (error) {
|
bird = birdRes.value;
|
||||||
|
} else {
|
||||||
bird = null;
|
bird = null;
|
||||||
birdError = toErrorMessage(error);
|
birdError = toErrorMessage(birdRes.reason);
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
const jobsPage = await apiJSON<JobsResponse>('/v1/jobs?limit=100');
|
if (jobsRes.status === 'fulfilled') {
|
||||||
jobs = summarizeJobsStatuses(jobsPage.items ?? []);
|
const items = jobsRes.value.items ?? [];
|
||||||
} catch (error) {
|
jobItems = items;
|
||||||
|
jobs = summarizeJobsStatuses(items);
|
||||||
|
} else {
|
||||||
|
jobItems = [];
|
||||||
jobs = null;
|
jobs = null;
|
||||||
jobsError = toErrorMessage(error);
|
jobsError = toErrorMessage(jobsRes.reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
lastUpdated = new Date();
|
lastUpdated = new Date();
|
||||||
loading = false;
|
initialLoading = false;
|
||||||
|
refreshing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function truncateError(error: string | null | undefined, max = 120): string {
|
||||||
|
if (!error) return '';
|
||||||
|
return error.length > max ? `${error.slice(0, max)}…` : error;
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(load);
|
onMount(load);
|
||||||
@@ -165,191 +313,394 @@
|
|||||||
iconClass="bg-info/15 text-info"
|
iconClass="bg-info/15 text-info"
|
||||||
>
|
>
|
||||||
{#snippet actions()}
|
{#snippet actions()}
|
||||||
<Button variant="outline" size="sm" onclick={load} disabled={loading}>
|
<Button variant="outline" size="sm" onclick={load} disabled={refreshing}>
|
||||||
<RefreshCw class={loading ? 'animate-spin' : ''} />
|
<RefreshCw class={refreshing ? 'animate-spin' : ''} />
|
||||||
Обновить
|
Обновить
|
||||||
</Button>
|
</Button>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
<div class="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
|
{#if !initialLoading}
|
||||||
<Card class="border-l-4 border-l-chart-1 bg-chart-1/5 shadow-sm">
|
{#if overallStatus === 'ok'}
|
||||||
<CardHeader>
|
<Alert class="border-success/30 bg-success/5">
|
||||||
<CardTitle class="flex items-center gap-2 text-base">
|
<CheckCircle class="text-success" />
|
||||||
<Server class="size-4" />
|
<AlertTitle>Система в норме</AlertTitle>
|
||||||
Общий статус
|
<AlertDescription>{overallHint}</AlertDescription>
|
||||||
</CardTitle>
|
</Alert>
|
||||||
<CardDescription>Liveness + readiness + jobs/BGP</CardDescription>
|
{:else if overallStatus === 'warn'}
|
||||||
</CardHeader>
|
<Alert class="border-warning/30 bg-warning/5">
|
||||||
<CardContent>
|
<AlertTriangle class="text-warning" />
|
||||||
<div class="flex items-center justify-between gap-2">
|
<AlertTitle>Требуется внимание</AlertTitle>
|
||||||
<p class="text-2xl font-semibold uppercase">{overallStatus}</p>
|
<AlertDescription>{overallHint}</AlertDescription>
|
||||||
<Badge variant={overallBadgeVariant(overallStatus)}>{overallStatus}</Badge>
|
</Alert>
|
||||||
</div>
|
{:else if overallStatus === 'error'}
|
||||||
<p class="mt-2 text-xs text-muted-foreground">{overallHint}</p>
|
<Alert variant="destructive">
|
||||||
</CardContent>
|
<XCircle />
|
||||||
</Card>
|
<AlertTitle>Обнаружена проблема</AlertTitle>
|
||||||
|
<AlertDescription>{overallHint}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
|
||||||
<Card class="border-l-4 border-l-chart-2 bg-chart-2/5 shadow-sm">
|
<div class="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
||||||
<CardHeader>
|
{#if initialLoading}
|
||||||
<CardTitle class="flex items-center gap-2 text-base">
|
{#each Array(4) as _, i (i)}
|
||||||
<Bird class="size-4" />
|
<CardSkeleton />
|
||||||
BGP сессии
|
{/each}
|
||||||
</CardTitle>
|
{:else}
|
||||||
<CardDescription>GET /v1/bird/status</CardDescription>
|
{#each kpiCards as card (card.id)}
|
||||||
</CardHeader>
|
{@const Icon = card.icon}
|
||||||
<CardContent>
|
{@const a = card.accent}
|
||||||
<div class="flex items-center justify-between gap-2">
|
<Card
|
||||||
<p class="text-2xl font-semibold">{bgpText}</p>
|
class={cn(
|
||||||
{#if bird?.birdc_configured}
|
'overflow-hidden border-l-4 shadow-sm transition-colors',
|
||||||
<Badge variant={bird.healthy ? 'default' : 'secondary'}
|
a.border,
|
||||||
>{bird.healthy ? 'healthy' : 'degraded'}</Badge
|
a.bg,
|
||||||
|
card.href ? 'hover:border-primary/35' : ''
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<CardHeader class="pb-2">
|
||||||
|
<div class="flex items-center justify-between gap-2">
|
||||||
|
<CardDescription class="flex min-w-0 items-center gap-2">
|
||||||
|
<span
|
||||||
|
class={cn(
|
||||||
|
'flex size-9 shrink-0 items-center justify-center rounded-lg',
|
||||||
|
a.iconBg
|
||||||
|
)}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<Icon class={cn('size-4', a.iconText)} />
|
||||||
|
</span>
|
||||||
|
<span class="truncate">{card.label}</span>
|
||||||
|
</CardDescription>
|
||||||
|
{#if card.href}
|
||||||
|
<Button variant="ghost" size="icon-sm" href={resolve(card.href)}>
|
||||||
|
<ArrowRight class="size-3.5" aria-hidden="true" />
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<CardTitle
|
||||||
|
class={cn(
|
||||||
|
'font-bold tabular-nums',
|
||||||
|
card.id === 'version' ? 'font-mono text-xl' : 'text-3xl'
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{:else}
|
{card.value}
|
||||||
<Badge variant="outline">n/a</Badge>
|
</CardTitle>
|
||||||
{/if}
|
</CardHeader>
|
||||||
</div>
|
<CardContent class="space-y-2">
|
||||||
{#if birdError}
|
<div class="flex items-center justify-between gap-2">
|
||||||
<p class="mt-2 text-xs text-destructive">{birdError}</p>
|
<Badge variant={card.badgeVariant} class={card.badgeClass}>{card.badge}</Badge>
|
||||||
{:else if bird && !bird.birdc_configured}
|
</div>
|
||||||
<p class="mt-2 text-xs text-muted-foreground">
|
{#if card.error}
|
||||||
{bird.message ?? 'birdc не настроен на API-хосте.'}
|
<p class="text-xs text-destructive">{card.error}</p>
|
||||||
</p>
|
{:else}
|
||||||
{:else if bird?.error}
|
<p class="text-xs text-muted-foreground">{card.description}</p>
|
||||||
<p class="mt-2 text-xs text-destructive">{bird.error}</p>
|
{/if}
|
||||||
{:else}
|
</CardContent>
|
||||||
<p class="mt-2 text-xs text-muted-foreground">Established / total на API-хосте.</p>
|
</Card>
|
||||||
{/if}
|
{/each}
|
||||||
</CardContent>
|
{/if}
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card class="border-l-4 border-l-chart-4 bg-chart-4/5 shadow-sm">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle class="flex items-center gap-2 text-base">
|
|
||||||
<Activity class="size-4" />
|
|
||||||
Задачи
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>GET /v1/jobs?limit=100</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div class="flex items-center justify-between gap-2">
|
|
||||||
<p class="text-2xl font-semibold">{jobs ? jobs.running : '—'}</p>
|
|
||||||
<Badge variant={jobs && jobs.failed > 0 ? 'secondary' : 'default'}>
|
|
||||||
{jobs && jobs.failed > 0 ? `ошибок ${jobs.failed}` : 'без ошибок'}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
{#if jobsError}
|
|
||||||
<p class="mt-2 text-xs text-destructive">{jobsError}</p>
|
|
||||||
{:else}
|
|
||||||
<p class="mt-2 text-xs text-muted-foreground">
|
|
||||||
Активных: {jobs?.running ?? '—'} из {jobs?.total ?? '—'} последних.
|
|
||||||
</p>
|
|
||||||
{/if}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card class="border-l-4 border-l-info bg-info/10 shadow-sm">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle class="flex items-center gap-2 text-base">
|
|
||||||
<Hash class="size-4" />
|
|
||||||
Версия
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>GET /v1/version</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div class="flex items-center justify-between gap-2">
|
|
||||||
<p class="font-mono text-2xl font-semibold">{versionText}</p>
|
|
||||||
<Badge variant={version ? 'outline' : 'secondary'}>{version ? 'loaded' : '—'}</Badge>
|
|
||||||
</div>
|
|
||||||
{#if versionError}
|
|
||||||
<p class="mt-2 text-xs text-destructive">{versionError}</p>
|
|
||||||
{:else if version}
|
|
||||||
<p class="mt-2 truncate text-xs text-muted-foreground">
|
|
||||||
api_version: {String(version.api_version ?? 'unknown')}
|
|
||||||
</p>
|
|
||||||
{/if}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-4 lg:grid-cols-2">
|
<div class="grid gap-4 lg:grid-cols-2">
|
||||||
<Card>
|
{#if initialLoading}
|
||||||
<CardHeader>
|
<CardSkeleton />
|
||||||
<CardTitle class="text-base">Доступность и готовность</CardTitle>
|
<CardSkeleton />
|
||||||
<CardDescription>GET /v1/health и GET /v1/ready</CardDescription>
|
{:else}
|
||||||
</CardHeader>
|
<Card>
|
||||||
<CardContent class="space-y-3">
|
<CardHeader>
|
||||||
<div class="flex items-center gap-2 text-sm">
|
<CardTitle class="text-base">Доступность и готовность</CardTitle>
|
||||||
<span class="text-muted-foreground">Liveness:</span>
|
<CardDescription>GET /v1/health · GET /v1/ready</CardDescription>
|
||||||
{#if health === null}
|
</CardHeader>
|
||||||
<Badge variant="outline">—</Badge>
|
<CardContent class="space-y-4">
|
||||||
{:else if health.ok}
|
{#if health?.error || readyError}
|
||||||
<Badge variant="default">ok</Badge>
|
<Alert variant="destructive">
|
||||||
{:else}
|
<XCircle />
|
||||||
<Badge variant="destructive">failed</Badge>
|
<AlertTitle>Ошибка проверки</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
{#if health?.error}{health.error}{/if}
|
||||||
|
{#if health?.error && readyError}<br />{/if}
|
||||||
|
{#if readyError}{readyError}{/if}
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
{/if}
|
{/if}
|
||||||
{#if health?.status}
|
|
||||||
<span class="text-muted-foreground">{health.status}</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-2 text-sm">
|
<Table>
|
||||||
<span class="text-muted-foreground">Readiness:</span>
|
<TableHeader>
|
||||||
{#if ready === null}
|
<TableRow>
|
||||||
<Badge variant="outline">—</Badge>
|
<TableHead class="w-[55%]">Проверка</TableHead>
|
||||||
{:else}
|
<TableHead>Статус</TableHead>
|
||||||
<Badge variant={ready.status === 'ready' ? 'default' : 'destructive'}
|
</TableRow>
|
||||||
>{ready.status}</Badge
|
</TableHeader>
|
||||||
>
|
<TableBody>
|
||||||
{/if}
|
{@const liveBadge = livenessBadge(health)}
|
||||||
</div>
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<HeartPulse class="size-4 shrink-0 text-muted-foreground" aria-hidden="true" />
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium">Liveness</p>
|
||||||
|
<p class="text-xs text-muted-foreground">/v1/health</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge variant={liveBadge.variant} class={liveBadge.class}
|
||||||
|
>{liveBadge.label}</Badge
|
||||||
|
>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
{@const readyBadge = readinessBadge(ready)}
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<ShieldCheck class="size-4 shrink-0 text-muted-foreground" aria-hidden="true" />
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium">Readiness</p>
|
||||||
|
<p class="text-xs text-muted-foreground">/v1/ready</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge variant={readyBadge.variant} class={readyBadge.class}
|
||||||
|
>{readyBadge.label}</Badge
|
||||||
|
>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
{#if ready?.checks && Object.keys(ready.checks).length > 0}
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colspan={2} class="bg-muted/30 py-2">
|
||||||
|
<p class="text-xs font-medium text-muted-foreground">Зависимости</p>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
{#each Object.entries(ready.checks) as [key, value] (key)}
|
||||||
|
{@const badge = checkStatusBadge(value)}
|
||||||
|
{@const CheckIcon = checkIconByKey[key] ?? ListTodo}
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<CheckIcon
|
||||||
|
class="size-4 shrink-0 text-muted-foreground"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium">{checkDisplayName(key)}</p>
|
||||||
|
<p class="text-xs text-muted-foreground">{key}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<Badge variant={badge.variant} class={badge.class}>{badge.label}</Badge>
|
||||||
|
{#if badge.hint}
|
||||||
|
<p class="text-xs text-muted-foreground">{badge.hint}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
{#if health?.error}
|
<p class="text-xs text-muted-foreground">
|
||||||
<p class="text-xs text-destructive">{health.error}</p>
|
HTTP 503 на readiness означает недоступность одной из зависимостей в checks.
|
||||||
{/if}
|
</p>
|
||||||
{#if readyError}
|
</CardContent>
|
||||||
<p class="text-xs text-destructive">{readyError}</p>
|
</Card>
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if ready?.checks}
|
<Card>
|
||||||
<div class="space-y-1">
|
<CardHeader>
|
||||||
{#each Object.entries(ready.checks) as [k, v] (k)}
|
<div class="flex items-start justify-between gap-2">
|
||||||
<div class="grid grid-cols-[auto_1fr] items-center gap-2 text-xs">
|
<div>
|
||||||
<span class="text-muted-foreground">{k}</span>
|
<CardTitle class="flex items-center gap-2 text-base">
|
||||||
<Badge variant="outline" class="justify-start font-normal">{String(v)}</Badge>
|
<Bird class="size-4" />
|
||||||
</div>
|
BGP на API-хосте
|
||||||
{/each}
|
</CardTitle>
|
||||||
|
<CardDescription>GET /v1/bird/status</CardDescription>
|
||||||
|
</div>
|
||||||
|
<Button variant="outline" size="sm" href={resolve('/network')}>Пиры и спикеры</Button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
</CardHeader>
|
||||||
</CardContent>
|
<CardContent class="space-y-4">
|
||||||
</Card>
|
{#if birdError}
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<XCircle />
|
||||||
|
<AlertTitle>Ошибка birdc</AlertTitle>
|
||||||
|
<AlertDescription>{birdError}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
{:else if bird && !bird.birdc_configured}
|
||||||
|
<p class="text-sm text-muted-foreground">
|
||||||
|
{bird.message ?? 'birdc не настроен на API-хосте (EVOBGP_BIRDC_SOCKET).'}
|
||||||
|
</p>
|
||||||
|
{:else if bird}
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="flex items-center justify-between text-sm">
|
||||||
|
<span class="text-muted-foreground">Established / total</span>
|
||||||
|
<span class="font-medium tabular-nums">
|
||||||
|
{bird.bgp_established} / {bird.bgp_sessions_total}
|
||||||
|
{#if bgpRatio !== null}
|
||||||
|
<span class="text-muted-foreground">({bgpRatio}%)</span>
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{#if bgpRatio !== null}
|
||||||
|
<div class="h-2 overflow-hidden rounded-full bg-muted">
|
||||||
|
<div
|
||||||
|
class={cn(
|
||||||
|
'h-full rounded-full transition-all',
|
||||||
|
bgpRatio >= 100
|
||||||
|
? 'bg-success'
|
||||||
|
: bgpRatio >= 50
|
||||||
|
? 'bg-warning'
|
||||||
|
: 'bg-destructive'
|
||||||
|
)}
|
||||||
|
style="width: {bgpRatio}%"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if bird.error}
|
||||||
|
<p class="text-xs text-destructive">{bird.error}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if bird.protocols_excerpt}
|
||||||
|
<Separator />
|
||||||
|
<div class="space-y-2">
|
||||||
|
<p class="text-sm font-medium">Вывод birdc (protocols)</p>
|
||||||
|
<ScrollPreBlock variant="preserve" text={bird.protocols_excerpt} class="max-h-48" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
<Card>
|
<div class="grid gap-4 lg:grid-cols-2">
|
||||||
<CardHeader>
|
{#if initialLoading}
|
||||||
<CardTitle class="flex items-center gap-2 text-base">
|
<CardSkeleton />
|
||||||
<AlertTriangle class="size-4 text-muted-foreground" />
|
<CardSkeleton />
|
||||||
Что проверять при деградации
|
{:else}
|
||||||
</CardTitle>
|
<Card>
|
||||||
<CardDescription>Короткая шпаргалка для triage</CardDescription>
|
<CardHeader>
|
||||||
</CardHeader>
|
<div class="flex items-start justify-between gap-2">
|
||||||
<CardContent class="space-y-2 text-sm">
|
<div>
|
||||||
<p>
|
<CardTitle class="flex items-center gap-2 text-base">
|
||||||
<span class="text-muted-foreground">Если </span><code>health=failed</code> — проверьте доступность
|
<Activity class="size-4" />
|
||||||
процесса API и его логи.
|
Задачи
|
||||||
</p>
|
</CardTitle>
|
||||||
<p>
|
<CardDescription>Последние 100 задач · GET /v1/jobs</CardDescription>
|
||||||
<span class="text-muted-foreground">Если </span><code>ready!=ready</code> — сначала `postgres`,
|
</div>
|
||||||
затем `store/jobs` в checks.
|
<Button variant="outline" size="sm" href={resolve('/operations')}>Все операции</Button>
|
||||||
</p>
|
</div>
|
||||||
<p>
|
</CardHeader>
|
||||||
<span class="text-muted-foreground">Если low BGP ratio</span> — проверьте `bird/status`, затем
|
<CardContent class="space-y-4">
|
||||||
BGP peer состояния.
|
{#if jobsError}
|
||||||
</p>
|
<Alert variant="destructive">
|
||||||
<p>
|
<XCircle />
|
||||||
<span class="text-muted-foreground">Если ошибки jobs</span> — откройте операции и проверьте
|
<AlertTitle>Не удалось загрузить задачи</AlertTitle>
|
||||||
последние неуспешные задачи.
|
<AlertDescription>{jobsError}</AlertDescription>
|
||||||
</p>
|
</Alert>
|
||||||
</CardContent>
|
{:else if jobs}
|
||||||
</Card>
|
<div class="flex flex-wrap gap-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<p class="text-muted-foreground">Активных</p>
|
||||||
|
<p class="text-2xl font-bold tabular-nums">{jobs.running}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-muted-foreground">С ошибками</p>
|
||||||
|
<p
|
||||||
|
class={cn(
|
||||||
|
'text-2xl font-bold tabular-nums',
|
||||||
|
jobs.failed > 0 ? 'text-warning' : 'text-success'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{jobs.failed}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-muted-foreground">В выборке</p>
|
||||||
|
<p class="text-2xl font-bold tabular-nums">{jobs.total}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
{#if failedJobs.length > 0}
|
||||||
|
<div class="space-y-3">
|
||||||
|
<p class="text-sm font-medium">Последние ошибки</p>
|
||||||
|
<ul class="space-y-2">
|
||||||
|
{#each failedJobs as job (job.job_id)}
|
||||||
|
<li class="rounded-lg border px-3 py-2 text-sm">
|
||||||
|
<div class="flex items-start justify-between gap-2">
|
||||||
|
<p class="font-medium">{jobKindTitle(job)}</p>
|
||||||
|
<Badge variant="destructive">{jobStatusRu(job.status)}</Badge>
|
||||||
|
</div>
|
||||||
|
{#if job.error}
|
||||||
|
<p class="mt-1 text-xs text-muted-foreground">
|
||||||
|
{truncateError(job.error)}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<p class="text-sm text-muted-foreground">
|
||||||
|
Критичных сбоев в последних 100 задачах нет.
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle class="flex items-center gap-2 text-base">
|
||||||
|
<AlertTriangle class="size-4 text-muted-foreground" />
|
||||||
|
Что проверять при деградации
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>Короткая шпаргалка для triage</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent class="space-y-3">
|
||||||
|
<Alert>
|
||||||
|
<HeartPulse class="size-4" />
|
||||||
|
<AlertTitle>API недоступен</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
Если <code class="text-xs">/v1/health</code> возвращает ошибку — проверьте процесс API и
|
||||||
|
его логи.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
<Alert>
|
||||||
|
<Database class="size-4" />
|
||||||
|
<AlertTitle>Readiness не «Готов»</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
Сначала <code class="text-xs">postgres</code>, затем
|
||||||
|
<code class="text-xs">store</code> и <code class="text-xs">jobs</code> в checks.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
<Alert>
|
||||||
|
<Bird class="size-4" />
|
||||||
|
<AlertTitle>Низкий ratio BGP</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
Проверьте <code class="text-xs">/v1/bird/status</code>, затем состояние пиров в
|
||||||
|
<Button variant="link" class="h-auto p-0" href={resolve('/network')}>Сети</Button>.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
<Alert>
|
||||||
|
<ListTodo class="size-4" />
|
||||||
|
<AlertTitle>Ошибки задач</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
Откройте
|
||||||
|
<Button variant="link" class="h-auto p-0" href={resolve('/operations')}
|
||||||
|
>Операции</Button
|
||||||
|
>
|
||||||
|
и проверьте последние неуспешные jobs.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user