diff --git a/.gitea/pull_request_template.md b/.gitea/pull_request_template.md new file mode 100644 index 0000000..fb85c82 --- /dev/null +++ b/.gitea/pull_request_template.md @@ -0,0 +1,28 @@ +## Summary + + + +## Checklist + +### General + +- [ ] Docs updated if behavior/API changed (DOC-02) +- [ ] Conventional Commits (EN title / RU body) + +### Backend (if Go / OpenAPI / migrations) + +- [ ] `go test ./... -race -count=1` (or scoped packages) when touching jobs/pipeline/httpapi +- [ ] OpenAPI lint + regenerate `apps/web/src/types/api.gen.ts` if `docs/openapi.yaml` changed +- [ ] Migration pairs postgres+sqlite (DEP-03) + +### Frontend (if `apps/web` / `packages/ui`) — WEB-19 + +- [ ] `pnpm --filter @evobgp/web run typecheck` +- [ ] `pnpm --filter @evobgp/web run lint` +- [ ] `pnpm --filter @evobgp/web run build` +- [ ] `pnpm --filter @evobgp/web run test` +- [ ] UI follows ReUI PRO + `docs/ui-design-contract.md` (surface `frame`, kit; cite previewUrl) + +### KPI / Quick Actions + +- [ ] If changing `kpi-stat-grid.tsx` / `quick-action-grid.tsx`, note sibling-app sync (vps / CFDM / fw / auth-portal) diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index ae00a47..70ac16e 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -180,12 +180,13 @@ jobs: node-version: "22" - name: Enable pnpm via corepack run: corepack enable - - name: pnpm install, typecheck, lint, build + - name: pnpm install, typecheck, lint, test, build run: | set -euxo pipefail pnpm install --frozen-lockfile pnpm --filter @evobgp/web run typecheck pnpm --filter @evobgp/web run lint + pnpm --filter @evobgp/web run test pnpm --filter @evobgp/web run build # --------------------------------------------------------------------------- diff --git a/apps/web/src/lib/api-client.test.ts b/apps/web/src/lib/api-client.test.ts new file mode 100644 index 0000000..a42f69d --- /dev/null +++ b/apps/web/src/lib/api-client.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, it } from 'vitest' + +import { normalizeApiToken, DEV_API_TOKEN } from '@/lib/api-client' +import { + aggregateNetworkMetrics, + moduleNameById, + runningJobCount, +} from '@/queries/overview' +import type { JobRow, ModuleRow, PeerRow, SpeakerRow } from '@/types/api' + +describe('normalizeApiToken', () => { + it('trims and strips Bearer prefix', () => { + expect(normalizeApiToken(' Bearer dev ')).toBe('dev') + expect(normalizeApiToken(DEV_API_TOKEN)).toBe('dev') + }) +}) + +describe('overview selectors', () => { + it('runningJobCount counts queued and running', () => { + const jobs = [ + { status: 'queued' }, + { status: 'running' }, + { status: 'succeeded' }, + ] as JobRow[] + expect(runningJobCount(jobs)).toBe(2) + }) + + it('moduleNameById maps ids', () => { + const modules = [ + { id: 'a', name: 'Alpha' }, + { id: 'b', name: 'Beta' }, + ] as ModuleRow[] + expect(moduleNameById(modules).get('a')).toBe('Alpha') + }) + + it('aggregateNetworkMetrics sums peers and speakers', () => { + const peers = [ + { enabled: true, session_state: 'Established', session_mismatch: false }, + { enabled: false, session_state: 'Idle', session_mismatch: true }, + ] as PeerRow[] + const speakers = [{ live: { agent_ok: true } }, { live: { agent_ok: false } }] as SpeakerRow[] + const m = aggregateNetworkMetrics(peers, speakers) + expect(m.peersTotal).toBe(2) + expect(m.peersEnabled).toBe(1) + expect(m.peersEstablished).toBe(1) + expect(m.peersMismatch).toBe(1) + expect(m.speakersOnline).toBe(1) + }) +}) diff --git a/docs/README.md b/docs/README.md index ccdd0e3..8b4b3d3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -23,6 +23,8 @@ | [remote-speakers.md](remote-speakers.md) | Удалённые BGP-реплики: Traefik, agent sync, compose | | [releasing.md](releasing.md) | Автоматические релизы, Conventional Commits, CI | | [ui-design-contract.md](ui-design-contract.md) | UI SoT: ReUI Frame, kit, KPI hybrid | +| [kpi-cross-app-sync.md](kpi-cross-app-sync.md) | Синхронизация KPI/QuickActions DNA с sibling-apps | +| [adr/](adr/) | Architecture Decision Records | | [openapi.yaml](openapi.yaml) | Источник правды по контракту API | | [OPENAPI-GITEA.md](OPENAPI-GITEA.md) | Как открыть HTML-документацию API (в т.ч. из Gitea) | | [evobgp-api-sketches.md](evobgp-api-sketches.md) | Ранний черновик идей API (контекст, не замена OpenAPI) | diff --git a/docs/adr/001-durable-job-queue.md b/docs/adr/001-durable-job-queue.md new file mode 100644 index 0000000..79bf522 --- /dev/null +++ b/docs/adr/001-durable-job-queue.md @@ -0,0 +1,24 @@ +# ADR-001: Durable job queue via PostgreSQL job_audit + +## Status + +Accepted (2026-07) + +## Context + +`jobs.Registry` is in-process. ARCH-04 notes workers do not share memory across API replicas. Jobs were already audited to `job_audit`, but orphans after restart were not reclaimed. + +## Decision + +Keep in-process execution for latency; add durable reclaim: + +1. Persist lifecycle via existing `SetPersistHooks` → `job_audit`. +2. Periodically `ReclaimStaleRunning` + `ClaimQueued` with `FOR UPDATE SKIP LOCKED`. +3. `Registry.Adopt` injects claimed rows into the local worker pool. +4. Claim grace (`EVOBGP_JOB_CLAIM_GRACE`, default 30s) avoids double-run of fresh local enqueues. + +NATS JetStream remains optional (`EVOBGP_BROKER_URL` logged only). + +## Consequences + +Two `evobgp-api` processes can reclaim orphaned work. Fresh jobs still run on the enqueueing process. Full broker-based fan-out is out of scope for this ADR. diff --git a/docs/adr/002-openapi-codegen.md b/docs/adr/002-openapi-codegen.md new file mode 100644 index 0000000..dd52305 --- /dev/null +++ b/docs/adr/002-openapi-codegen.md @@ -0,0 +1,20 @@ +# ADR-002: OpenAPI → TypeScript codegen + +## Status + +Accepted (2026-07) + +## Context + +HTTP SoT is `docs/openapi.yaml`. Frontend DTOs in `apps/web/src/types/api.ts` were hand-maintained and drifted. + +## Decision + +1. Generate `apps/web/src/types/api.gen.ts` with `openapi-typescript` (`pnpm --filter @evobgp/web run openapi:gen`). +2. Keep `api.ts` as UI-facing aliases; re-export `OpenAPISchemas` for gradual adoption. +3. CI (`scripts/check-openapi-gen.sh`) fails when gen is stale vs OpenAPI. +4. `packages/shared` deferred until Zod DTOs are needed cross-app. + +## Consequences + +PRs that change OpenAPI must regenerate types. Hand DTOs remain until routes migrate to `api.gen` schemas. diff --git a/docs/adr/003-legacy-svelte-removal.md b/docs/adr/003-legacy-svelte-removal.md new file mode 100644 index 0000000..1ccd3be --- /dev/null +++ b/docs/adr/003-legacy-svelte-removal.md @@ -0,0 +1,17 @@ +# ADR-003: Removal of web-legacy-svelte + +## Status + +Accepted (2026-07) + +## Context + +`web-legacy-svelte/` (~294 files) confused agents/codegraph after the React + ReUI migration to `apps/web/`. + +## Decision + +Delete `web-legacy-svelte/` from `main`. Historical reference remains in git history. Docs and `AGENTS.md` point only to `apps/web/`. + +## Consequences + +No Svelte imports allowed. Restore from git history if a one-off migration reference is needed. diff --git a/docs/kpi-cross-app-sync.md b/docs/kpi-cross-app-sync.md new file mode 100644 index 0000000..d8c3612 --- /dev/null +++ b/docs/kpi-cross-app-sync.md @@ -0,0 +1,19 @@ +# Cross-app KPI / Quick Actions sync + +EvoBGP is the **source of truth** for hybrid KPI and Quick Actions markup: + +- `apps/web/src/components/reui-kit/kpi-stat-grid.tsx` +- `apps/web/src/components/reui-kit/quick-action-grid.tsx` + +Preview: https://reui.io/preview/base/stats-12 · https://reui.io/preview/base/card-12 + +## Process + +1. Change markup/tokens only in EvoBGP first. +2. Diff against sibling apps (`@cfdm/ui` / `@evofw/ui` / `@authportal/ui` import prefixes only): + - `vps-tracker/apps/web/src/components/reui-kit/kpi-stat-grid.tsx` + - `cloudflare-domain-manager/.../kpi-stat-grid.tsx` + - `EvoFirewall/.../kpi-stat-grid.tsx` + - `auth-portal/.../kpi-stat-grid.tsx` +3. Port structural/class changes; keep package alias (`@evobgp/ui` → `@scope/ui`). +4. Mention sync in the PR checklist when EvoBGP kit DNA changes.