feat(mcp): подготовить сервер и доступ к приёмке

This commit is contained in:
Denozordec committed 2026-11-03 03:12:03 +07:00
commit bf3d50e1ff
3835 files changed
+537122

No files matched your search

+8
View File
@@ -0,0 +1,8 @@
export const emptyAlerts={revision:"1".repeat(64),telegram:{chatId:"",tokenConfigured:false,connected:false,messageThreadId:null},groups:[],rules:[],history:[],meta:{servers:[],greClients:[],probeTargets:[],rttLossTargets:[],trafficServers:[],serversDetail:[]}}
export const alertsRule={id:"rule-fixture",name:"Fixture rule",type:"server",target:"router-fixture",targets:["router-fixture"],groupId:"group-fixture",condition:"перешёл в offline",conditions:["перешёл в offline"],severity:"critical",enabled:true,cooldown:"5м",confirmStabilitySec:5,recoveryMode:"conditional",recoveryStabilitySec:17,chatId:"",lastFiredAt:null}
export const alertsGroup={id:"group-fixture",name:"Fixture group",combineMode:"any",enabled:true,cooldownOverride:null}
export const alertsWithRule={...emptyAlerts,rules:[alertsRule],groups:[alertsGroup],meta:{...emptyAlerts.meta,servers:["router-fixture"],serversDetail:[{name:"router-fixture",host:"192.0.2.1",site:"",country:"",type:""}]}}
export const emptyOutbox={counts:{total:0,pending:0,processing:0,unknown:0,sent:0,failed:0,unrecognized:0},items:[],itemsLimit:20}
export const heldOutbox={...emptyOutbox,counts:{...emptyOutbox.counts,total:1,unknown:1},items:[{id:"held-fixture",resolutionVersion:3,status:"unknown",ruleName:"Held retry fixture",createdAt:"2026-10-03T00:00:00Z",attemptedAt:"2026-10-03T01:00:00Z"}]}
+14
View File
@@ -0,0 +1,14 @@
import { expect, type Page } from "@playwright/test"
import { backup } from "./backups"
export async function openBackup(page: Page) {
await page.goto("/backups"); await page.getByRole("button", { name: "Новый бэкап", exact: true }).first().click()
const dialog = page.getByRole("dialog")
await dialog.getByRole("checkbox", { name: /Выбрать Роутер/ }).check()
await dialog.getByRole("button", { name: "Далее", exact: true }).click()
await dialog.getByRole("textbox", { name: "Заметка резервной копии", exact: true }).fill("manual")
await dialog.getByRole("button", { name: "Далее", exact: true }).click()
await expect(dialog.getByRole("button", { name: /Снять бэкап/ })).toBeEnabled()
return dialog
}
export function backupJob(id: string, patch: Record<string, unknown> = {}) { return { id, status: "done", serverIds: ["1"], notes: "manual", total: 1, completed: 1, created: [backup], failures: [], requestedAt: "2026-10-01T00:00:00Z", finishedAt: "2026-10-01T00:01:00Z", ...patch } }
export function backupAck(id: string) { return { jobId: id, status: "queued", serverIds: ["1"], total: 1, completed: 0 } }
+13
View File
@@ -0,0 +1,13 @@
import type { Page } from "@playwright/test"
export { server } from "./users"
import { server } from "./users"
export const backup = { id: "backup-1", serverId: 1, serverName: "router", filename: "live-config.rsc", sizeBytes: 0, createdAt: "2026-10-01T00:00:00Z", kind: "manual", storage: "local" }
export const schedule = { enabled: false, frequency: "daily", hour: 0, minute: 0, weekDay: 0, monthDay: 1, keepCount: 7, format: "rsc", serverIds: ["1"] }
export const storage = { provider: "local", s3Endpoint: "", s3Region: "", s3Bucket: "", s3Prefix: "", s3AccessKeyId: "", secretConfigured: false, s3ForcePathStyle: false, keepLocalCopy: false }
export async function setupBackups(page: Page, headers: Record<string, string> = {}) {
await page.route("**/health", r => r.fulfill({ headers, json: { ok: true } }))
await page.route("**/api/**", r => {
const path = new URL(r.request().url()).pathname
return r.fulfill({ headers, json: path === "/api/auth/config" ? { required: false } : path === "/api/servers" ? [server] : path === "/api/backups" ? [backup] : path === "/api/backups/schedule" ? schedule : path === "/api/backups/storage" ? storage : {} })
})
}
+2
View File
@@ -0,0 +1,2 @@
import type {BackendBgpSession} from "@/lib/bgp/response"
export function bgpSession(serverId=1,prefixesRx:number|null=321):BackendBgpSession{return {id:"*1",serverId,serverName:"Роутер "+serverId,serverSite:"TEST",serverCountry:"RU",name:"Сессия "+serverId,peerIp:"192.0.2."+serverId,remoteAs:64513,localAs:64512,localId:"192.0.2.100",remoteId:"192.0.2."+serverId,state:"Established",type:"eBGP",uptime:"1h",holdTime:90,keepalive:30,prefixesRx,prefixesTx:2,inputMessages:20,outputMessages:10,capabilities:[],lastError:null}}
+23
View File
@@ -0,0 +1,23 @@
import { expect, type Page } from "@playwright/test"
import { certificateState, server } from "./certificates"
export async function certificateRoutes(page: Page) {
await page.route("**/health", r => r.fulfill({ json: { ok: true } }))
await page.route("**/api/**", r => {
const path = new URL(r.request().url()).pathname
return r.fulfill({ json: path === "/api/auth/config" ? { required: false } : path === "/api/servers" ? [server] : path === "/api/certificates/state" ? certificateState() : path === "/api/certificates/renew-settings" ? { enabled: false, intervalSec: 300, renewBeforeDays: 30 } : { directoryUrl: "https://acme.example.test/directory", tokenConfigured: true } })
})
}
export function issueJob(request: Record<string, unknown>, overrides: Record<string, unknown> = {}) {
return { id: request.requestId, serverId: String(request.serverId), certName: request.certName, domainNames: request.domainNames, status: "done", step: "done", requestedAt: "2026-10-01T00:00:00Z", finishedAt: "2026-10-01T00:01:00Z", ...overrides }
}
export async function openIssue(page: Page) {
await page.goto("/certificates"); const action = page.getByRole("button", { name: "Выпустить сертификат", exact: true }); await expect(action).toBeEnabled(); await action.click()
const dialog = page.getByRole("dialog", { name: "Выпуск сертификата", exact: true })
await dialog.getByRole("combobox", { name: "Сервер выпуска", exact: true }).selectOption("1")
await dialog.getByRole("textbox", { name: "Имя сертификата", exact: true }).fill("new-cert")
await dialog.getByRole("button", { name: "Далее", exact: true }).click()
await dialog.getByRole("textbox", { name: "Основной домен", exact: true }).fill("test.example")
await dialog.getByRole("button", { name: "Далее", exact: true }).click()
await dialog.getByRole("button", { name: "Далее", exact: true }).click()
return dialog
}
+5
View File
@@ -0,0 +1,5 @@
export { server } from "./users"
export const certificate = { id: "1:*A", rosId: "*A", name: "certificate-live", serverId: "1", commonName: "test.example", sans: [], issuedBy: "—", validFrom: null, validUntil: null, rawValidFrom: null, rawValidUntil: null, daysLeft: null, keySize: null, trusted: null, status: "unknown", usage: [] }
export function certificateState(certificates: unknown[] = [certificate], overrides: Record<string, unknown> = {}) {
return { collectedAt: "2026-10-01T00:00:00Z", servers: [{ serverId: "1", serverName: "Роутер", collectedAt: "2026-10-01T00:00:00Z", certificates, error: null, ...overrides }] }
}
+6
View File
@@ -0,0 +1,6 @@
import { server } from "./users"
export { server }
export const container = {id:"1-*A",rosId:"*A",name:"container-live",serverId:"1",image:"nginx",tag:"alpine",status:"running",envs:[],mounts:[],interfaces:["veth1"],startOnBoot:false,comment:"",cpu:0,memMb:0}
export function state(containers:unknown[]=[container], overrides:Record<string,unknown>={}) {
return {collectedAt:"2026-10-01T00:00:00Z",servers:[{serverId:1,collectedAt:"2026-10-01T00:00:00Z",availability:"available",containers,failures:[],...overrides}]}
}
+4
View File
@@ -0,0 +1,4 @@
import type {DashboardGreTunnel} from "@/lib/dashboard-overlays"
export const dashboardGre:DashboardGreTunnel={id:"g1",name:"GRE сохранён",serverId:"1",status:"down",localAddress:"192.0.2.1",remoteAddress:"192.0.2.2",localInnerIp:"10.0.0.1/30",remoteInnerIp:"10.0.0.2/30",poolId:"live",ipsec:null,mtu:1400,keepaliveInterval:10,keepaliveRetries:3,dscp:"inherit",clampTcpMss:true,allowFastPath:true,comment:"",enabled:true}
export const dashboardVxlan={id:"v1",name:"VXLAN fixture",serverId:"1",vni:1,port:4789,dstPort:4789,vtepIp:"192.0.2.1",remoteVteps:["192.0.2.2"],l2mtu:1400,arpProxy:false,macLearning:true,comment:"",enabled:true,status:"up"}
export const dashboardWg={id:"w1",rosId:"*1",name:"WG fixture",serverId:"1",serverName:"Узел",listenPort:51820,mtu:1420,peers:[],comment:"",enabled:true,status:"up"}
+2
View File
@@ -0,0 +1,2 @@
export const evoSettings={baseUrl:"https://evo.example.test",enabled:true,secretConfigured:true}
export const evoCatalog={fetchedAt:"2026-10-03T00:00:00Z",modules:[],domains:[{id:"fixture-domain",domain:"kept.example.test",resolvedIp:"192.0.2.1",asn:"AS64512",purpose:"fixture",filter:"fixture",updated:"сейчас",enabled:true}],ipRanges:[],asns:[],communities:[]}
+1
View File
@@ -0,0 +1 @@
export const geoip = { ready: false, countryLoaded: false, asnLoaded: false, countryFile: "country.mmdb", asnFile: "asn.mmdb", dir: "geoip", running: false, settings: { enabled: false, updateIntervalSec: 604800, lastCheckAt: null, lastSuccessAt: null, lastError: null, countryBuildAt: null, asnBuildAt: null, updatedAt: "2026-10-01T00:00:00Z" } }
+2
View File
@@ -0,0 +1,2 @@
export const netflowSettings = { enabled: false, collectorIp: "10.255.254.1", flowListenPort: 4739, wgListenPort: 51821, prefix: "10.255.254.0/24", publicEndpoint: "", hostPublicKey: "", hasHostPrivateKey: false, hubServerId: null, retentionHours: 24, topN: 200, mapServiceMinSharePct: 0, lastDatagramAt: null, lastExporterIp: null, lastError: null, packetsReceived: 0, listenerBound: false, listenerAddress: null, peers: [] }
export const keyedNetflowSettings = {...netflowSettings,hostPublicKey:"fixture-public-only",hasHostPrivateKey:true}
+11
View File
@@ -0,0 +1,11 @@
export const user={id:"u-test",name:"Настоящий пользователь",login:"test@example.test",email:"test@example.test",role:"viewer",active:true,avatar:"НП",lastSeen:null,sections:[],servers:[],bindings:[],createdAt:"2026-10-01T00:00:00Z",updatedAt:"2026-10-01T00:00:00Z"}
export const server={id:1,name:"Роутер",host:"192.0.2.1",port:443,useSsl:true,verifySsl:false,username:"admin",password:"",type:"exit-node",site:"",country:"RU",asn:"",comment:"",enabled:true,lanSubnet:"",wanUplinks:[],createdAt:"",updatedAt:"",status:null,latency:null,os:null,model:null,uptime:null,cpuLoad:null,freeMemory:null,totalMemory:null,identityName:null,sessions:0,polledAt:null}
import type {Route} from "@playwright/test"
export function catalogState(interfaces:unknown[]=[],serverId=1,overrides:Record<string,unknown>={}) {
return {serverId,source:"snapshot",sampledAt:"2026-10-01T00:00:00Z",inventoryComplete:true,interfaces,failures:[],...overrides}
}
export function catalogResponse(route:Route,options:NonNullable<Parameters<Route["fulfill"]>[0]>){
const json=options.json as Record<string,unknown>|undefined
return route.fulfill({...options,...(json&&typeof json==="object"&&"interfaces" in json?{json:catalogState(json.interfaces as unknown[],Number(new URL(route.request().url()).searchParams.get("serverId")??1),json)}:{})})
}