Init commit

This commit is contained in:
Denozordec committed 2026-05-02 01:17:08 +07:00
commit f3f831653f
104 files changed
+43827

No files matched your search

+36
View File
@@ -0,0 +1,36 @@
/**
* API mode feature-flag.
*
* Set NEXT_PUBLIC_API_MODE=live in .env.local to switch from static mock data
* to real RouterOS REST API calls.
*
* Default: "mock"
*/
export const API_MODE = (
typeof process !== "undefined"
? (process.env.NEXT_PUBLIC_API_MODE ?? "mock")
: "mock"
) as "mock" | "live"
export const IS_MOCK = API_MODE !== "live"
/**
* Wrap an async data-fetch so it transparently returns mock data in "mock" mode
* or calls the live resolver in "live" mode.
*
* Example:
* export const fetchServers = apiOr(
* () => servers, // mock
* async (cfg) => apiClient(cfg).getAll() // live
* )
*/
export function apiOr<T, Args extends unknown[]>(
mock: (...args: Args) => T | Promise<T>,
live: (...args: Args) => T | Promise<T>,
): (...args: Args) => Promise<T> {
return async (...args: Args): Promise<T> => {
if (IS_MOCK) return mock(...args)
return live(...args)
}
}
+256
View File
@@ -0,0 +1,256 @@
/**
* RouterOS 7.x REST API client
*
* RouterOS 7.1+ exposes a REST API at https://<router>/rest/
* All responses are JSON; authentication is HTTP Basic or Bearer token.
*
* Usage (browser / server-component):
* const client = new RouterOsApiClient({ host: "10.0.0.1", port: 443, ... })
* const ifaces = await client.getInterfaces()
*/
export interface ApiClientConfig {
host: string // "10.0.0.1"
port: number // 443 (HTTPS) or 80 (HTTP)
tls: boolean // use HTTPS
username: string
password: string
tlsVerify: boolean // verify TLS certificate (false for self-signed)
timeout: number // ms, default 8000
}
// ─── RouterOS REST response shapes ───────────────────────────────────────────
export interface RosSystemIdentity {
name: string
}
export interface RosSystemResource {
uptime: string
version: string
"build-time": string
"free-memory": string
"total-memory": string
"cpu": string
"cpu-count": string
"cpu-load": string
"board-name": string
"platform": string
"architecture-name": string
"bad-blocks": string
}
export interface RosInterface {
".id": string
name: string
type: string
mtu: string
"actual-mtu": string
"mac-address": string
running: string
disabled: string
comment?: string
}
export interface RosBgpSession {
".id": string
name: string
"remote.address": string
"remote.as": string
state: string
"uptime": string
"prefix-count": string
disabled: string
}
export interface RosWireGuardInterface {
".id": string
name: string
"listen-port": string
mtu: string
"public-key": string
running: string
disabled: string
comment?: string
}
export interface RosWireGuardPeer {
".id": string
interface: string
"public-key": string
"endpoint-address"?: string
"endpoint-port"?: string
"allowed-address": string
"last-handshake"?: string
"rx": string
"tx": string
disabled: string
comment?: string
}
// ─── Error class ─────────────────────────────────────────────────────────────
export class RouterOsApiError extends Error {
constructor(
public readonly status: number,
public readonly detail: string,
message: string,
) {
super(message)
this.name = "RouterOsApiError"
}
}
// ─── Client class ─────────────────────────────────────────────────────────────
export class RouterOsApiClient {
private readonly baseUrl: string
private readonly headers: Record<string, string>
private readonly timeout: number
constructor(private readonly cfg: ApiClientConfig) {
const scheme = cfg.tls ? "https" : "http"
this.baseUrl = `${scheme}://${cfg.host}:${cfg.port}/rest`
const creds = btoa(`${cfg.username}:${cfg.password}`)
this.headers = {
"Authorization": `Basic ${creds}`,
"Content-Type": "application/json",
}
this.timeout = cfg.timeout ?? 8000
}
// ── low-level fetch ──────────────────────────────────────────────────────
private async request<T>(
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE",
path: string,
body?: unknown,
): Promise<T> {
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), this.timeout)
try {
const res = await fetch(`${this.baseUrl}${path}`, {
method,
headers: this.headers,
body: body !== undefined ? JSON.stringify(body) : undefined,
signal: controller.signal,
})
clearTimeout(timer)
if (!res.ok) {
let detail = ""
try { detail = (await res.json())?.message ?? "" } catch { /* ignore */ }
throw new RouterOsApiError(res.status, detail, `RouterOS API ${method} ${path} → ${res.status}: ${detail}`)
}
if (res.status === 204) return undefined as T
return res.json() as Promise<T>
} catch (e) {
clearTimeout(timer)
throw e
}
}
async get<T>(path: string): Promise<T> {
return this.request<T>("GET", path)
}
async post<T>(path: string, body: unknown): Promise<T> {
return this.request<T>("POST", path, body)
}
/** RouterOS uses PUT for updates on a specific item by /.id */
async put<T>(path: string, id: string, body: unknown): Promise<T> {
return this.request<T>("PUT", `${path}/${encodeURIComponent(id)}`, body)
}
async patch<T>(path: string, id: string, body: unknown): Promise<T> {
return this.request<T>("PATCH", `${path}/${encodeURIComponent(id)}`, body)
}
async delete(path: string, id: string): Promise<void> {
return this.request<void>("DELETE", `${path}/${encodeURIComponent(id)}`)
}
// ── high-level helpers ───────────────────────────────────────────────────
async getSystemIdentity(): Promise<RosSystemIdentity> {
return this.get<RosSystemIdentity>("/system/identity")
}
async getSystemResource(): Promise<RosSystemResource> {
return this.get<RosSystemResource>("/system/resource")
}
async getInterfaces(): Promise<RosInterface[]> {
return this.get<RosInterface[]>("/interface")
}
async getBgpSessions(): Promise<RosBgpSession[]> {
return this.get<RosBgpSession[]>("/routing/bgp/session")
}
async getWireGuardInterfaces(): Promise<RosWireGuardInterface[]> {
return this.get<RosWireGuardInterface[]>("/interface/wireguard")
}
async getWireGuardPeers(): Promise<RosWireGuardPeer[]> {
return this.get<RosWireGuardPeer[]>("/interface/wireguard/peers")
}
/** Add a WireGuard interface */
async addWireGuardInterface(params: {
name: string
listenPort: number
mtu?: number
comment?: string
}): Promise<RosWireGuardInterface> {
return this.post<RosWireGuardInterface>("/interface/wireguard", {
name: params.name,
"listen-port": String(params.listenPort),
mtu: String(params.mtu ?? 1420),
comment: params.comment ?? "",
})
}
/** Add a WireGuard peer */
async addWireGuardPeer(params: {
iface: string
publicKey: string
allowedAddresses: string[]
endpoint?: string
endpointPort?: number
persistentKeepalive?: number
comment?: string
}): Promise<RosWireGuardPeer> {
return this.post<RosWireGuardPeer>("/interface/wireguard/peers", {
interface: params.iface,
"public-key": params.publicKey,
"allowed-address": params.allowedAddresses.join(","),
...(params.endpoint ? { "endpoint-address": params.endpoint } : {}),
...(params.endpointPort ? { "endpoint-port": String(params.endpointPort) } : {}),
...(params.persistentKeepalive ? { "persistent-keepalive": String(params.persistentKeepalive) } : {}),
comment: params.comment ?? "",
})
}
/** Ping test from the router — uses /tool/ping */
async ping(address: string, count = 4): Promise<{ "avg-rtt": string; "packet-loss": string }[]> {
return this.post("/tool/ping", { address, count: String(count) })
}
}
// ─── Factory helper ───────────────────────────────────────────────────────────
export function createClient(overrides: Partial<ApiClientConfig> & { host: string }): RouterOsApiClient {
return new RouterOsApiClient({
port: 443,
tls: true,
username: "admin",
password: "",
tlsVerify: false,
timeout: 8000,
...overrides,
})
}
+76
View File
@@ -0,0 +1,76 @@
"use client"
import { createContext, useContext, useEffect, useState, useCallback } from "react"
// ── types ─────────────────────────────────────────────────────────────────────
export type DataSourceMode = "mock" | "live"
interface DataSourceContextValue {
mode: DataSourceMode
setMode: (m: DataSourceMode) => void
backendUrl: string
setBackendUrl: (url: string) => void
/** undefined = не проверялось, true = OK, false = недоступен */
backendStatus: boolean | undefined
checkBackend: () => Promise<void>
}
// ── context ───────────────────────────────────────────────────────────────────
const DataSourceContext = createContext<DataSourceContextValue | null>(null)
const LS_MODE = "routerlists:data-source"
const LS_BACKEND = "routerlists:backend-url"
const DEFAULT_URL = "http://localhost:8000"
export function DataSourceProvider({ children }: { children: React.ReactNode }) {
const [mode, setModeState] = useState<DataSourceMode>("mock")
const [backendUrl, setBackendUrlState] = useState(DEFAULT_URL)
const [backendStatus, setBackendStatus] = useState<boolean | undefined>(undefined)
// Restore from localStorage on mount
useEffect(() => {
const storedMode = localStorage.getItem(LS_MODE) as DataSourceMode | null
const storedUrl = localStorage.getItem(LS_BACKEND)
if (storedMode === "mock" || storedMode === "live") setModeState(storedMode)
if (storedUrl) setBackendUrlState(storedUrl)
}, [])
const setMode = useCallback((m: DataSourceMode) => {
setModeState(m)
localStorage.setItem(LS_MODE, m)
}, [])
const setBackendUrl = useCallback((url: string) => {
setBackendUrlState(url)
localStorage.setItem(LS_BACKEND, url)
}, [])
const checkBackend = useCallback(async () => {
const url = (localStorage.getItem(LS_BACKEND) ?? DEFAULT_URL).replace(/\/$/, "")
try {
const res = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3000) })
setBackendStatus(res.ok)
} catch {
setBackendStatus(false)
}
}, [])
// Auto-check when mode switches to live
useEffect(() => {
if (mode === "live") checkBackend()
}, [mode, checkBackend])
return (
<DataSourceContext.Provider value={{ mode, setMode, backendUrl, setBackendUrl, backendStatus, checkBackend }}>
{children}
</DataSourceContext.Provider>
)
}
export function useDataSource(): DataSourceContextValue {
const ctx = useContext(DataSourceContext)
if (!ctx) throw new Error("useDataSource must be used inside <DataSourceProvider>")
return ctx
}
+807
View File
@@ -0,0 +1,807 @@
export type ServerStatus = "online" | "offline" | "degraded"
export type ServerType = "jump-host" | "exit-node" | "home-router"
export interface WanUplink {
id: string
name: string // "WAN1-RT"
isp: string // "Rostelecom"
iface: string // "ether1"
ip: string // external IP
maxDl: number // Mbps
maxUl: number // Mbps
}
// ─── WireGuard ───────────────────────────────────────────────────────────────
export interface WireGuardPeer {
publicKey: string
allowedIps: string[]
endpoint?: string // "1.2.3.4:13231"
latestHandshake?: string // "2 минуты назад"
transferRx?: number // bytes
transferTx?: number // bytes
persistent?: boolean
comment?: string
}
export interface WireGuardInterface {
id: string
name: string // e.g. "wg-msk-spb"
listenPort: number // default 13231
mtu: number // 1420 default in ROS 7.x
peers: WireGuardPeer[]
comment: string
enabled: boolean
status: "up" | "down"
}
export interface Server {
id: string
name: string
host: string // management IP / API address
model: string
os: string
site: string
country: string // ISO 3166-1 alpha-2, e.g. "RU", "DE"
asn: string
type: ServerType
enabled: boolean
status: ServerStatus
latency: number | null
sessions: number
comment?: string
// RouterOS 7.x extras
ipv6Address?: string // global or link-local
wireGuardIfaces?: WireGuardInterface[]
vrfNames?: string[] // VRF instances on this router
rpkiEnabled?: boolean
// home-router specific
wanUplinks?: WanUplink[]
lanSubnet?: string // e.g. "192.168.10.0/24"
// snapshot / poll data
uptime?: string
cpuLoad?: number
polledAt?: string
}
export interface FlowspecRule {
id: string
dstPrefix?: string
srcPrefix?: string
ipProto?: string
dstPort?: string
action: "rate-limit" | "discard" | "redirect"
rate?: number // Mbps, for rate-limit
comment?: string
}
export interface Filter {
id: string
name: string
domains: number
ips: number
asns: number
servers: string[]
enabled: boolean
updated: string
communities: string[]
gateway: string
// RouterOS 7.x extras
largeCommunities?: string[] // RFC 8092 format "AS:VALUE1:VALUE2"
rpkiValid?: boolean // filter only RPKI-valid routes
flowspecRules?: FlowspecRule[]
}
export interface Domain {
id: string
domain: string
resolvedIp: string
asn: string
purpose: string
filter: string
updated: string
enabled: boolean
}
export interface IpRange {
id: string
cidr: string
asn: string
country: string
purpose: string
filter: string
updated: string
enabled: boolean
}
export interface Asn {
id: string
asn: string
org: string
country: string
prefixes: number
filter: string
updated: string
enabled: boolean
}
export type FirewallAction =
| "accept" | "drop" | "reject" | "mark-routing" | "mark-conn"
| "masquerade" | "src-nat" | "dst-nat"
| "fasttrack-connection" | "nfqueue" | "passthrough" | "return"
| "add-src-to-address-list" | "add-dst-to-address-list"
export interface FirewallRule {
id: string
chain: string
action: FirewallAction | string
proto: string
src: string
dst: string
port: string
iface: string
comment: string
enabled: boolean
hits: number
// RouterOS 7.x extras
family?: "ip" | "ip6" | "any"
tlsHost?: string // tls-host matcher (SNI)
connRateLimit?: string // connection-rate e.g. "100/s"
layer7Proto?: string // /ip firewall layer7-protocol
nfqueueId?: number
log?: boolean
logPrefix?: string
}
export interface Backup {
id: string
server: string
filename: string
size: string
created: string
kind: "auto" | "manual"
notes: string
}
// ─── VXLAN ───────────────────────────────────────────────────────────────────
export interface VxlanTunnel {
id: string
name: string // "vxlan-overlay-1"
vni: number // 0–16777215
port: number // source port (default 0 = auto)
dstPort: number // destination port (default 8472)
serverId: string
vtepIp: string // VTEP address on the server
remoteVteps: string[] // remote VTEP IPs (unicast-flood)
l2mtu: number
arpProxy: boolean
macLearning: boolean
comment: string
enabled: boolean
status: "up" | "down"
}
// ─── VRF ─────────────────────────────────────────────────────────────────────
export interface VrfInstance {
id: string
name: string // VRF name
serverId: string
interfaces: string[] // list of interfaces in this VRF
routingTable: string // associated routing table
comment: string
}
// ─── Container (RouterOS 7.4+) ───────────────────────────────────────────────
export interface RouterContainer {
id: string
name: string
serverId: string
image: string // e.g. "nginx:alpine"
tag: string
status: "running" | "stopped" | "error"
envs: { key: string; value: string }[]
mounts: { dst: string; src?: string }[]
interfaces: string[] // veth interface names
cmd?: string
startOnBoot: boolean
comment: string
uptime?: string
cpu?: number // CPU % 0-100
memMb?: number // MB
}
// ─── Certificate ─────────────────────────────────────────────────────────────
export type CertUsage = "server" | "client" | "ca" | "crl-sign"
export type CertStatus = "valid" | "expired" | "revoked"
export interface RouterCertificate {
id: string
name: string
serverId: string
commonName: string
sans: string[]
issuedBy: string
validFrom: string
validUntil: string
daysLeft: number
keySize: number
usage: CertUsage[]
trusted: boolean
status: CertStatus
}
export interface PingProbe {
id: string
srcServerId: string // which server sends the ping
srcInterface?: string // optional interface name on source router
name: string
target: string
filter: string
rtt: number | null
loss: number
status: "up" | "warn" | "down"
series: number[]
enabled: boolean
}
export interface SystemEvent {
id: string
sev: "destructive" | "warning" | "info"
title: string
message: string
when: string
}
function spark(n: number, base: number, range: number): number[] {
let s = base * 1000 + range
return Array.from({ length: n }, () => {
s = (s * 9301 + 49297) % 233280
const r = s / 233280
return Math.max(1, Math.round(base + (r - 0.5) * range * 2))
})
}
export const servers: Server[] = [
{
id: "srv1", name: "mt-msk-core-01", host: "10.0.0.1", model: "CCR2004-1G-12S+2XS", os: "7.20.1",
site: "MSK", country: "RU", asn: "AS65001", type: "jump-host", enabled: true, status: "online", latency: 12, sessions: 4,
ipv6Address: "2a01:4f8:1c1c:a1b2::1",
rpkiEnabled: true,
vrfNames: ["mgmt", "bypass"],
wireGuardIfaces: [
{
id: "wg1", name: "wg-msk-spb", listenPort: 13231, mtu: 1420, comment: "MSK → SPB overlay", enabled: true, status: "up",
peers: [
{ publicKey: "SPBPublicKeyBase64AAAAAAAAAAAAAAAAAAAAAA=", allowedIps: ["10.210.0.2/32","192.168.20.0/24"], endpoint: "10.0.1.1:13231", latestHandshake: "2 мин назад", transferRx: 182_400_000, transferTx: 64_800_000, persistent: true },
],
},
{
id: "wg2", name: "wg-msk-fra", listenPort: 13232, mtu: 1420, comment: "MSK → FRA overlay", enabled: true, status: "up",
peers: [
{ publicKey: "FRAPublicKeyBase64AAAAAAAAAAAAAAAAAAAAAA=", allowedIps: ["10.210.0.6/32"], endpoint: "10.0.2.1:13231", latestHandshake: "1 мин назад", transferRx: 421_000_000, transferTx: 210_000_000, persistent: true },
],
},
],
},
{
id: "srv2", name: "mt-spb-edge-01", host: "10.0.1.1", model: "CCR2116-12G-4S+", os: "7.20.1",
site: "SPB", country: "RU", asn: "AS65002", type: "exit-node", enabled: true, status: "online", latency: 28, sessions: 6,
ipv6Address: "2a01:4f8:1c2c:b2c3::1",
rpkiEnabled: true,
wireGuardIfaces: [
{
id: "wg3", name: "wg-spb-msk", listenPort: 13231, mtu: 1420, comment: "SPB → MSK overlay", enabled: true, status: "up",
peers: [
{ publicKey: "MSKPublicKeyBase64AAAAAAAAAAAAAAAAAAAAAA=", allowedIps: ["10.210.0.1/32"], endpoint: "10.0.0.1:13231", latestHandshake: "2 мин назад", transferRx: 64_800_000, transferTx: 182_400_000, persistent: true },
],
},
],
},
{
id: "srv3", name: "mt-fra-edge-01", host: "10.0.2.1", model: "CCR2004-16G-2S+", os: "7.20.1",
site: "FRA", country: "DE", asn: "AS65003", type: "exit-node", enabled: true, status: "online", latency: 41, sessions: 3,
ipv6Address: "2a01:4f8:1c3c:c3d4::1",
rpkiEnabled: true,
wireGuardIfaces: [
{
id: "wg4", name: "wg-fra-msk", listenPort: 13231, mtu: 1420, comment: "FRA → MSK overlay", enabled: true, status: "up",
peers: [
{ publicKey: "MSKPublicKeyBase64AAAAAAAAAAAAAAAAAAAAAA=", allowedIps: ["10.210.0.1/32"], endpoint: "10.0.0.1:13232", latestHandshake: "1 мин назад", transferRx: 210_000_000, transferTx: 421_000_000, persistent: true },
],
},
],
},
{
id: "srv4", name: "mt-ams-edge-01", host: "10.0.3.1", model: "RB5009UG+S+IN", os: "7.19.3",
site: "AMS", country: "NL", asn: "AS65004", type: "exit-node", enabled: true, status: "degraded", latency: 88, sessions: 2,
},
{
id: "srv5", name: "mt-sgp-edge-01", host: "10.0.4.1", model: "CCR2004-1G-12S+2XS", os: "7.14.2",
site: "SGP", country: "SG", asn: "AS65005", type: "exit-node", enabled: false, status: "offline", latency: null, sessions: 0,
},
{
id: "srv6", name: "mt-ams-test-01", host: "10.0.3.2", model: "hEX S", os: "7.20.1",
site: "AMS", country: "NL", asn: "AS65006", type: "jump-host", enabled: true, status: "online", latency: 92, sessions: 1,
},
{
id: "srv7", name: "mt-msk-lab-01", host: "10.0.0.7", model: "RB5009UG+S+IN", os: "7.20.1",
site: "MSK", country: "RU", asn: "AS65007", type: "jump-host", enabled: true, status: "online", latency: 14, sessions: 2,
vrfNames: ["lab"],
},
{
id: "home1", name: "home-msk-01", host: "192.168.10.1", model: "hEX S (RB760iGS)", os: "7.20.1",
site: "MSK", country: "RU", asn: "", type: "home-router", enabled: true, status: "online", latency: 3, sessions: 0,
comment: "MSK офис — основной роутер",
lanSubnet: "192.168.10.0/24",
wanUplinks: [
{ id: "w1a", name: "WAN1-RT", isp: "Rostelecom", iface: "ether1", ip: "94.25.168.1", maxDl: 500, maxUl: 500 },
{ id: "w1b", name: "WAN2-BL", isp: "Beeline", iface: "ether2", ip: "85.174.12.1", maxDl: 100, maxUl: 100 },
],
},
{
id: "home2", name: "home-spb-01", host: "192.168.20.1", model: "RB5009UG+S+IN", os: "7.20.1",
site: "SPB", country: "RU", asn: "", type: "home-router", enabled: true, status: "online", latency: 5, sessions: 0,
comment: "SPB офис — основной роутер",
lanSubnet: "192.168.20.0/24",
wanUplinks: [
{ id: "w2a", name: "WAN1-MTS", isp: "МТС", iface: "ether1", ip: "178.214.4.1", maxDl: 300, maxUl: 300 },
{ id: "w2b", name: "WAN2-ERT", isp: "ЭР-Телеком", iface: "ether2", ip: "95.31.14.1", maxDl: 200, maxUl: 150 },
],
},
]
export const filters: Filter[] = [
{
id: "f1", name: "youtube-bypass", domains: 1842, ips: 4218, asns: 12,
servers: ["srv1","srv2","srv3","srv4"], enabled: true, updated: "12 мин назад",
communities: ["65001:100","65001:200"], gateway: "10.0.0.1",
largeCommunities: ["65001:100:1","65001:100:2"],
rpkiValid: true,
},
{
id: "f2", name: "streaming-eu", domains: 612, ips: 2104, asns: 8,
servers: ["srv2","srv3","srv4"], enabled: true, updated: "2 ч назад",
communities: ["65001:200"], gateway: "10.0.1.1",
largeCommunities: ["65001:200:1"],
rpkiValid: true,
},
{
id: "f3", name: "cdn-bypass", domains: 4218, ips: 18211, asns: 22,
servers: ["srv1","srv2","srv3","srv4","srv6"], enabled: true, updated: "1 ч назад",
communities: ["65001:300","65002:100"], gateway: "10.0.0.1",
flowspecRules: [
{ id: "fs1", dstPrefix: "104.16.0.0/12", ipProto: "tcp", dstPort: "443", action: "rate-limit", rate: 100, comment: "CF rate-limit" },
],
},
{ id: "f4", name: "social-block-school", domains: 84, ips: 412, asns: 4, servers: ["srv1","srv7"], enabled: false, updated: "3 дн назад", communities: ["65007:999"], gateway: "" },
{ id: "f5", name: "office-direct", domains: 142, ips: 884, asns: 6, servers: ["srv1","srv2","srv3","srv4","srv6","srv7"], enabled: true, updated: "4 ч назад", communities: ["65001:400"], gateway: "10.0.0.1" },
{ id: "f6", name: "gaming-low-latency", domains: 88, ips: 1212, asns: 14, servers: ["srv1","srv2","srv3","srv7"], enabled: true, updated: "24 мин назад", communities: ["65001:500","65002:200"], gateway: "10.0.2.1" },
]
export const domains: Domain[] = [
{ id: "d1", domain: "youtube.com", purpose: "streaming", filter: "youtube-bypass", resolvedIp: "142.250.74.110", asn: "AS15169", updated: "12 мин", enabled: true },
{ id: "d2", domain: "googlevideo.com", purpose: "streaming", filter: "youtube-bypass", resolvedIp: "142.250.74.142", asn: "AS15169", updated: "12 мин", enabled: true },
{ id: "d3", domain: "ytimg.com", purpose: "streaming", filter: "youtube-bypass", resolvedIp: "172.217.16.46", asn: "AS15169", updated: "12 мин", enabled: true },
{ id: "d4", domain: "netflix.com", purpose: "streaming", filter: "streaming-eu", resolvedIp: "54.246.79.9", asn: "AS2906", updated: "2 ч", enabled: true },
{ id: "d5", domain: "nflxvideo.net", purpose: "streaming", filter: "streaming-eu", resolvedIp: "45.57.32.18", asn: "AS2906", updated: "2 ч", enabled: true },
{ id: "d6", domain: "cloudflare.com", purpose: "cdn", filter: "cdn-bypass", resolvedIp: "104.16.132.229", asn: "AS13335", updated: "1 ч", enabled: true },
{ id: "d7", domain: "akamaized.net", purpose: "cdn", filter: "cdn-bypass", resolvedIp: "23.46.18.94", asn: "AS20940", updated: "1 ч", enabled: true },
{ id: "d8", domain: "fastly.net", purpose: "cdn", filter: "cdn-bypass", resolvedIp: "151.101.1.95", asn: "AS54113", updated: "1 ч", enabled: true },
{ id: "d9", domain: "twitch.tv", purpose: "streaming", filter: "streaming-eu", resolvedIp: "52.223.241.72", asn: "AS16509", updated: "2 ч", enabled: false },
{ id: "d10", domain: "discord.com", purpose: "voip", filter: "gaming-low-latency", resolvedIp: "162.159.135.232", asn: "AS13335", updated: "24 мин", enabled: true },
{ id: "d11", domain: "steam.com", purpose: "gaming", filter: "gaming-low-latency", resolvedIp: "23.45.139.40", asn: "AS20940", updated: "24 мин", enabled: true },
{ id: "d12", domain: "epicgames.com", purpose: "gaming", filter: "gaming-low-latency", resolvedIp: "34.205.247.51", asn: "AS16509", updated: "24 мин", enabled: true },
]
export const ipRanges: IpRange[] = [
{ id: "i1", cidr: "142.250.0.0/15", purpose: "streaming", filter: "youtube-bypass", asn: "AS15169", country: "US", updated: "12 мин", enabled: true },
{ id: "i2", cidr: "172.217.0.0/16", purpose: "streaming", filter: "youtube-bypass", asn: "AS15169", country: "US", updated: "12 мин", enabled: true },
{ id: "i3", cidr: "104.16.0.0/12", purpose: "cdn", filter: "cdn-bypass", asn: "AS13335", country: "US", updated: "1 ч", enabled: true },
{ id: "i4", cidr: "23.32.0.0/11", purpose: "cdn", filter: "cdn-bypass", asn: "AS20940", country: "US", updated: "1 ч", enabled: true },
{ id: "i5", cidr: "151.101.0.0/16", purpose: "cdn", filter: "cdn-bypass", asn: "AS54113", country: "US", updated: "1 ч", enabled: true },
{ id: "i6", cidr: "54.246.0.0/16", purpose: "streaming", filter: "streaming-eu", asn: "AS2906", country: "IE", updated: "2 ч", enabled: true },
{ id: "i7", cidr: "45.57.0.0/16", purpose: "streaming", filter: "streaming-eu", asn: "AS2906", country: "NL", updated: "2 ч", enabled: true },
{ id: "i8", cidr: "162.159.128.0/19", purpose: "voip", filter: "gaming-low-latency", asn: "AS13335", country: "US", updated: "24 мин", enabled: true },
{ id: "i9", cidr: "52.223.240.0/20", purpose: "streaming", filter: "streaming-eu", asn: "AS16509", country: "IE", updated: "2 ч", enabled: false },
]
export const asns: Asn[] = [
{ id: "a1", asn: "AS15169", org: "GOOGLE", country: "US", prefixes: 842, filter: "youtube-bypass", updated: "12 мин", enabled: true },
{ id: "a2", asn: "AS13335", org: "CLOUDFLARENET", country: "US", prefixes: 1284, filter: "cdn-bypass", updated: "1 ч", enabled: true },
{ id: "a3", asn: "AS16509", org: "AMAZON-02", country: "US", prefixes: 4128, filter: "streaming-eu", updated: "2 ч", enabled: true },
{ id: "a4", asn: "AS20940", org: "AKAMAI-ASN1", country: "US", prefixes: 614, filter: "cdn-bypass", updated: "1 ч", enabled: true },
{ id: "a5", asn: "AS54113", org: "FASTLY", country: "US", prefixes: 122, filter: "cdn-bypass", updated: "1 ч", enabled: true },
{ id: "a6", asn: "AS2906", org: "AS-SSI (Netflix)", country: "US", prefixes: 88, filter: "streaming-eu", updated: "2 ч", enabled: true },
{ id: "a7", asn: "AS32934", org: "FACEBOOK", country: "US", prefixes: 412, filter: "social-block-school",updated: "3 дн", enabled: false },
{ id: "a8", asn: "AS8075", org: "MICROSOFT-CORP", country: "US", prefixes: 882, filter: "office-direct", updated: "4 ч", enabled: true },
]
export const firewallRules: FirewallRule[] = [
{ id: "fw1", chain: "forward", action: "accept", proto: "tcp", src: "youtube-bypass", dst: "0.0.0.0/0", port: "443", iface: "wan-bgp", comment: "YouTube bypass traffic", enabled: true, hits: 188421 },
{ id: "fw2", chain: "forward", action: "mark-routing", proto: "tcp", src: "10.10.0.0/24", dst: "cdn-bypass", port: "—", iface: "lan", comment: "CDN routing mark", enabled: true, hits: 4128821 },
{ id: "fw3", chain: "srcnat", action: "masquerade", proto: "all", src: "10.10.0.0/16", dst: "0.0.0.0/0", port: "—", iface: "wan-msk", comment: "Main NAT rule", enabled: true, hits: 12889241 },
{ id: "fw4", chain: "forward", action: "drop", proto: "tcp", src: "0.0.0.0/0", dst: "social-block-school", port: "—", iface: "lan-school", comment: "School social block", enabled: true, hits: 4218 },
{ id: "fw5", chain: "input", action: "accept", proto: "tcp", src: "10.10.0.0/16", dst: "—", port: "22,443,8291",iface: "—", comment: "Management access", enabled: true, hits: 18412 },
{ id: "fw6", chain: "input", action: "drop", proto: "tcp", src: "0.0.0.0/0", dst: "—", port: "22", iface: "wan-msk", comment: "Block SSH from WAN", enabled: true, hits: 882412 },
]
export const backups: Backup[] = [
{ id: "b1", server: "mt-msk-core-01", filename: "mt-msk-core-01_2026-04-26_03-00.rsc", size: "124 КБ", created: "Сегодня, 03:00", kind: "auto", notes: "снапшот перед обновлением" },
{ id: "b2", server: "mt-msk-core-01", filename: "mt-msk-core-01_2026-04-25_03-00.rsc", size: "124 КБ", created: "Вчера, 03:00", kind: "auto", notes: "" },
{ id: "b3", server: "mt-spb-edge-01", filename: "mt-spb-edge-01_2026-04-26_03-00.rsc", size: "88 КБ", created: "Сегодня, 03:00", kind: "auto", notes: "" },
{ id: "b4", server: "mt-fra-edge-01", filename: "mt-fra-edge-01_2026-04-26_manual.rsc",size: "92 КБ", created: "Сегодня, 14:18", kind: "manual", notes: "перед изменением BGP" },
{ id: "b5", server: "mt-ams-edge-01", filename: "mt-ams-edge-01_2026-04-25_03-00.rsc", size: "64 КБ", created: "Вчера, 03:00", kind: "auto", notes: "" },
]
export const pingProbes: PingProbe[] = [
// srv1 — mt-msk-core-01 (JH, MSK) → основной мониторинг
{ id: "p1", srcServerId: "srv1", name: "youtube.com", target: "142.250.74.110", filter: "youtube-bypass", rtt: 18, loss: 0, status: "up", series: spark(40, 18, 4), enabled: true },
{ id: "p2", srcServerId: "srv1", name: "cloudflare.com", target: "104.16.132.229", filter: "cdn-bypass", rtt: 4, loss: 0, status: "up", series: spark(40, 4, 1), enabled: true },
{ id: "p3", srcServerId: "srv1", name: "netflix-eu", target: "54.246.79.9", filter: "streaming-eu", rtt: 28, loss: 0, status: "up", series: spark(40, 28, 5), enabled: true },
{ id: "p4", srcServerId: "srv1", name: "fastly-edge", target: "151.101.1.95", filter: "cdn-bypass", rtt: 8, loss: 0, status: "up", series: spark(40, 8, 2), enabled: true },
// srv7 — mt-msk-lab-01 (JH, MSK) → лаборатория
{ id: "p5", srcServerId: "srv7", name: "discord-voip", target: "162.159.135.232", filter: "gaming-low-latency", rtt: 12, loss: 0, status: "up", series: spark(40, 12, 2), enabled: true },
{ id: "p6", srcServerId: "srv7", name: "steam-cdn", target: "23.45.139.40", filter: "gaming-low-latency", rtt: 38, loss: 1.2, status: "warn", series: spark(40, 38, 12), enabled: true },
{ id: "p7", srcServerId: "srv7", name: "akamai-edge", target: "23.46.18.94", filter: "cdn-bypass", rtt: 22, loss: 0, status: "up", series: spark(40, 22, 4), enabled: true },
// srv2 — mt-spb-edge-01 (EN, SPB) → исходящий трафик SPB
{ id: "p8", srcServerId: "srv2", name: "youtube.com", target: "142.250.74.110", filter: "youtube-bypass", rtt: 22, loss: 0, status: "up", series: spark(40, 22, 5), enabled: true },
{ id: "p9", srcServerId: "srv2", name: "office-365", target: "104.47.0.22", filter: "office-direct", rtt: 31, loss: 0, status: "up", series: spark(40, 31, 6), enabled: false },
// srv3 — mt-fra-edge-01 (EN, FRA) → исходящий трафик Frankfurt
{ id: "p10", srcServerId: "srv3", name: "youtube.com", target: "142.250.74.110", filter: "youtube-bypass", rtt: 12, loss: 0, status: "up", series: spark(40, 12, 3), enabled: true },
{ id: "p11", srcServerId: "srv3", name: "netflix-eu", target: "54.246.79.9", filter: "streaming-eu", rtt: 9, loss: 0, status: "up", series: spark(40, 9, 2), enabled: true },
// srv4 — mt-ams-edge-01 (EN, AMS, degraded) → деградировавший узел
{ id: "p12", srcServerId: "srv4", name: "cloudflare.com", target: "104.16.132.229", filter: "cdn-bypass", rtt: 88, loss: 3.4, status: "warn", series: spark(40, 88, 22), enabled: true },
{ id: "p13", srcServerId: "srv4", name: "sgp-probe", target: "139.180.22.4", filter: "—", rtt: null,loss: 100, status: "down", series: Array(40).fill(0), enabled: true },
]
export const systemEvents: SystemEvent[] = [
{ id: "e1", sev: "destructive", title: "BGP-сессия упала", message: "mt-sgp-edge-01 · пир 139.180.22.4 · простой 2ч", when: "2ч" },
{ id: "e2", sev: "destructive", title: "Ошибка синхронизации фильтра", message: "cdn-bypass → mt-ams-edge-01 · таймаут", when: "14м" },
{ id: "e3", sev: "warning", title: "Высокая задержка на AMS", message: "среднее 88мс · порог 60мс", when: "8м" },
{ id: "e4", sev: "warning", title: "Бэкап просрочен", message: "mt-msk-lab-01 · последний 5д назад", when: "1ч" },
{ id: "e5", sev: "info", title: "Обновление BGP завершено", message: "+412 префиксов · −188 отозвано", when: "12м" },
{ id: "e6", sev: "info", title: "Фильтр youtube-bypass обновлён", message: "+88 доменов · ↑ 24 сервера", when: "12м" },
]
export const dashLatency = {
msk: spark(60, 12, 4),
spb: spark(60, 28, 6),
fra: spark(60, 41, 9),
ams: spark(60, 88, 18),
}
export const traffic = {
rx: Array.from({ length: 60 }, (_, i) => Math.round(280 + Math.sin(i / 6) * 60 + ((i * 7919) % 31))),
tx: Array.from({ length: 60 }, (_, i) => Math.round(220 + Math.sin(i / 5) * 50 + ((i * 6271) % 21))),
}
// ─── GRE ────────────────────────────────────────────────────────────────────
export type GreStatus = "up" | "down" | "degraded"
export type IpsecEncAlg = "aes-128" | "aes-192" | "aes-256"
export type IpsecAuthAlg = "sha1" | "sha256" | "sha512"
export type IpsecDhGroup = "modp1024" | "modp2048" | "modp4096" | "ecp256" | "ecp384" | "ecp521"
export type IkeVersion = "ikev1" | "ikev2"
export type DscpMode = "inherit" | number
export interface GreIpsec {
secret: string // ipsec-secret → auto-creates peer+policy+proposal
encAlg: IpsecEncAlg // proposal enc-algorithms
authAlg: IpsecAuthAlg // proposal auth-algorithms
dhGroup: IpsecDhGroup // proposal pfs-group / peer dh-group
ikeVersion: IkeVersion // peer exchange-mode
lifetime: string // proposal lifetime (e.g. "1d 00:00:00")
pfs: boolean // perfect forward secrecy
}
export interface GreTunnel {
id: string
name: string // interface name, e.g. gre-msk-spb
serverId: string // which MikroTik runs this tunnel
localAddress: string // local GRE endpoint ("0.0.0.0" = auto)
remoteAddress: string // remote GRE endpoint (required)
localInnerIp: string // /ip address on this side, e.g. 10.200.0.1/30
remoteInnerIp: string // /ip address on remote side, e.g. 10.200.0.2/30
poolId: string // which IP pool the inner IPs come from
ipsec: GreIpsec | null // null = no IPsec (plain GRE)
mtu: number // default 1476
keepaliveInterval: number // seconds, 0 = disabled
keepaliveRetries: number
dscp: DscpMode // "inherit" or 0-63
clampTcpMss: boolean
allowFastPath: boolean
comment: string
enabled: boolean
status: GreStatus
}
export interface GrePool {
id: string
name: string
cidr: string
allocated: number
total: number
comment: string
}
const defaultIpsec = (secret: string, ikeVersion: IkeVersion = "ikev2"): GreIpsec => ({
secret,
encAlg: "aes-256",
authAlg: "sha256",
dhGroup: "modp2048",
ikeVersion,
lifetime: "1d 00:00:00",
pfs: true,
})
export const grePools: GrePool[] = [
{ id: "gp1", name: "pool-gre-core", cidr: "10.200.0.0/24", allocated: 16, total: 64, comment: "Ядровые межузловые туннели" },
{ id: "gp2", name: "pool-gre-mgmt", cidr: "10.201.0.0/24", allocated: 8, total: 64, comment: "Управляющие туннели (lab, test)" },
]
export const greTunnels: GreTunnel[] = [
{
id: "gre1", name: "gre-msk-spb", serverId: "srv1",
localAddress: "10.0.0.1", remoteAddress: "10.0.1.1",
localInnerIp: "10.200.0.1/30", remoteInnerIp: "10.200.0.2/30", poolId: "gp1",
ipsec: defaultIpsec("xK9#mQ2$vLp8nR4"),
mtu: 1476, keepaliveInterval: 10, keepaliveRetries: 10,
dscp: "inherit", clampTcpMss: true, allowFastPath: true,
comment: "MSK → SPB core interconnect", enabled: true, status: "up",
},
{
id: "gre2", name: "gre-msk-fra", serverId: "srv1",
localAddress: "10.0.0.1", remoteAddress: "10.0.2.1",
localInnerIp: "10.200.0.5/30", remoteInnerIp: "10.200.0.6/30", poolId: "gp1",
ipsec: defaultIpsec("pW3@jT7!hD5kYe1"),
mtu: 1476, keepaliveInterval: 10, keepaliveRetries: 10,
dscp: "inherit", clampTcpMss: true, allowFastPath: true,
comment: "MSK → FRA core interconnect", enabled: true, status: "up",
},
{
id: "gre3", name: "gre-msk-ams", serverId: "srv1",
localAddress: "10.0.0.1", remoteAddress: "10.0.3.1",
localInnerIp: "10.200.0.9/30", remoteInnerIp: "10.200.0.10/30", poolId: "gp1",
ipsec: defaultIpsec("rN6%bF2*cM8sZa0"),
mtu: 1476, keepaliveInterval: 10, keepaliveRetries: 10,
dscp: "inherit", clampTcpMss: true, allowFastPath: false,
comment: "MSK → AMS core interconnect", enabled: true, status: "degraded",
},
{
id: "gre4", name: "gre-msk-sgp", serverId: "srv1",
localAddress: "10.0.0.1", remoteAddress: "10.0.4.1",
localInnerIp: "10.200.0.13/30", remoteInnerIp: "10.200.0.14/30", poolId: "gp1",
ipsec: defaultIpsec("qA4^eU9#wG1oXi6"),
mtu: 1476, keepaliveInterval: 10, keepaliveRetries: 10,
dscp: "inherit", clampTcpMss: true, allowFastPath: true,
comment: "MSK → SGP core interconnect", enabled: false, status: "down",
},
{
id: "gre5", name: "gre-lab-core", serverId: "srv7",
localAddress: "10.0.0.7", remoteAddress: "10.0.0.1",
localInnerIp: "10.201.0.1/30", remoteInnerIp: "10.201.0.2/30", poolId: "gp2",
ipsec: null,
mtu: 1500, keepaliveInterval: 0, keepaliveRetries: 0,
dscp: "inherit", clampTcpMss: false, allowFastPath: true,
comment: "Lab → core (internal, no IPsec)", enabled: true, status: "up",
},
{
id: "gre6", name: "gre-ams-test", serverId: "srv6",
localAddress: "10.0.3.2", remoteAddress: "10.0.3.1",
localInnerIp: "10.201.0.5/30", remoteInnerIp: "10.201.0.6/30", poolId: "gp2",
ipsec: null,
mtu: 1476, keepaliveInterval: 5, keepaliveRetries: 5,
dscp: 0, clampTcpMss: true, allowFastPath: true,
comment: "AMS test → AMS edge", enabled: true, status: "up",
},
]
// ─── Filter Rules (Community → Gateway per server) ────────────────────────────
export interface FilterRule {
id: string
community: string // BGP community, e.g. "65001:100"
communityName?: string // optional human-readable name
action?: "route" | "blackhole" // default "route"; "blackhole" → null-route (RTBH)
gateway: string // remote inner IP of GRE tunnel; empty for blackhole
gatewayTunnelId: string // reference to greTunnel.id; empty for blackhole
description: string
}
export interface ServerFilterRuleset {
serverId: string
rules: FilterRule[]
}
export const serverFilterRulesets: ServerFilterRuleset[] = [
// srv1 — mt-msk-core-01 (jump-host, MSK): gre1=SPB, gre2=FRA, gre3=AMS(deg), gre4=SGP(down)
{
serverId: "srv1",
rules: [
{ id: "r1", community: "65001:100", communityName: "youtube-bypass", gateway: "10.200.0.6", gatewayTunnelId: "gre2", description: "YouTube/Google Video → Frankfurt (IX)" },
{ id: "r2", community: "65001:200", communityName: "streaming-eu", gateway: "10.200.0.6", gatewayTunnelId: "gre2", description: "Netflix, Twitch EU → Frankfurt" },
{ id: "r3", community: "65001:300", communityName: "cdn-bypass", gateway: "10.200.0.2", gatewayTunnelId: "gre1", description: "Cloudflare / Akamai / Fastly → SPB" },
{ id: "r4", community: "65002:100", communityName: "cdn-secondary", gateway: "10.200.0.10", gatewayTunnelId: "gre3", description: "CDN резерв → Amsterdam (degraded)" },
{ id: "r5", community: "65001:400", communityName: "office-direct", gateway: "10.200.0.2", gatewayTunnelId: "gre1", description: "MS Office 365 / Teams → SPB direct" },
{ id: "r6", community: "65001:500", communityName: "gaming-ll", gateway: "10.200.0.2", gatewayTunnelId: "gre1", description: "Discord / Steam / Epic — низкая задержка → SPB" },
{ id: "r10", community: "65535:666", communityName: "BLACKHOLE (RFC 7999)", action: "blackhole", gateway: "", gatewayTunnelId: "", description: "RTBH — null-route входящих маршрутов" },
{ id: "r11", community: "65001:666", communityName: "blackhole-local", action: "blackhole", gateway: "", gatewayTunnelId: "", description: "Локальная блокировка — сброс трафика" },
],
},
// srv7 — mt-msk-lab-01 (jump-host, MSK lab): gre5=core
{
serverId: "srv7",
rules: [
{ id: "r7", community: "65001:100", communityName: "youtube-bypass", gateway: "10.201.0.2", gatewayTunnelId: "gre5", description: "Lab: YouTube → core" },
{ id: "r8", community: "65007:999", communityName: "school-block", gateway: "10.201.0.2", gatewayTunnelId: "gre5", description: "Lab: школьная блокировка" },
],
},
// srv6 — mt-ams-test-01 (jump-host, AMS test): gre6=AMS edge
{
serverId: "srv6",
rules: [
{ id: "r9", community: "65001:300", communityName: "cdn-bypass", gateway: "10.201.0.6", gatewayTunnelId: "gre6", description: "AMS test: CDN → AMS edge" },
],
},
// Exit nodes — нет правил по умолчанию
{ serverId: "srv2", rules: [] },
{ serverId: "srv3", rules: [] },
{ serverId: "srv4", rules: [] },
{ serverId: "srv5", rules: [] },
]
// ─── VXLAN mock data ─────────────────────────────────────────────────────────
export const vxlanTunnels: VxlanTunnel[] = [
{
id: "vx1", name: "vxlan-overlay-msk", vni: 10010, port: 0, dstPort: 8472,
serverId: "srv1", vtepIp: "10.0.0.1", remoteVteps: ["10.0.1.1","10.0.2.1"],
l2mtu: 1500, arpProxy: true, macLearning: true,
comment: "MSK overlay — сегмент bypass", enabled: true, status: "up",
},
{
id: "vx2", name: "vxlan-overlay-spb", vni: 10010, port: 0, dstPort: 8472,
serverId: "srv2", vtepIp: "10.0.1.1", remoteVteps: ["10.0.0.1","10.0.2.1"],
l2mtu: 1500, arpProxy: true, macLearning: true,
comment: "SPB overlay — сегмент bypass", enabled: true, status: "up",
},
{
id: "vx3", name: "vxlan-overlay-fra", vni: 10010, port: 0, dstPort: 8472,
serverId: "srv3", vtepIp: "10.0.2.1", remoteVteps: ["10.0.0.1","10.0.1.1"],
l2mtu: 1500, arpProxy: true, macLearning: false,
comment: "FRA overlay — сегмент bypass", enabled: true, status: "up",
},
{
id: "vx4", name: "vxlan-mgmt-lab", vni: 20001, port: 0, dstPort: 4789,
serverId: "srv7", vtepIp: "10.0.0.7", remoteVteps: ["10.0.0.1"],
l2mtu: 1500, arpProxy: false, macLearning: true,
comment: "Lab management VXLAN", enabled: true, status: "up",
},
{
id: "vx5", name: "vxlan-overlay-ams", vni: 10010, port: 0, dstPort: 8472,
serverId: "srv4", vtepIp: "10.0.3.1", remoteVteps: ["10.0.0.1"],
l2mtu: 1500, arpProxy: true, macLearning: true,
comment: "AMS overlay (degraded)", enabled: true, status: "down",
},
]
// ─── Container mock data ─────────────────────────────────────────────────────
export const routerContainers: RouterContainer[] = [
{
id: "c1", name: "adguard-msk", serverId: "srv1",
image: "adguard/adguardhome", tag: "latest",
status: "running",
envs: [{ key: "AGH_CONFIG", value: "/opt/adguardhome/conf/AdGuardHome.yaml" }],
mounts: [{ dst: "/opt/adguardhome/conf", src: "/disk1/adguard" }, { dst: "/opt/adguardhome/work", src: "/disk1/adguard-work" }],
interfaces: ["veth-adguard"],
startOnBoot: true, comment: "AdGuard Home — DNS фильтрация",
uptime: "12д 4ч", cpu: 3, memMb: 64,
},
{
id: "c2", name: "prometheus-msk", serverId: "srv1",
image: "prom/prometheus", tag: "v2.52.0",
status: "running",
envs: [],
mounts: [{ dst: "/prometheus", src: "/disk1/prom-data" }],
interfaces: ["veth-prom"],
cmd: "--config.file=/etc/prometheus/prometheus.yml --storage.tsdb.retention.time=30d",
startOnBoot: true, comment: "Prometheus метрики",
uptime: "5д 11ч", cpu: 8, memMb: 256,
},
{
id: "c3", name: "grafana-msk", serverId: "srv1",
image: "grafana/grafana", tag: "10.4.2",
status: "running",
envs: [{ key: "GF_SECURITY_ADMIN_PASSWORD", value: "*** (hidden)" }],
mounts: [{ dst: "/var/lib/grafana", src: "/disk1/grafana" }],
interfaces: ["veth-grafana"],
startOnBoot: true, comment: "Grafana — дашборды мониторинга",
uptime: "5д 11ч", cpu: 5, memMb: 128,
},
{
id: "c4", name: "wg-portal-spb", serverId: "srv2",
image: "wgportal/wg-portal", tag: "v1.3.1",
status: "running",
envs: [{ key: "WG_PORTAL_ADMIN_USER", value: "admin" }],
mounts: [{ dst: "/app/data", src: "/disk1/wgportal" }],
interfaces: ["veth-wgportal"],
startOnBoot: true, comment: "WireGuard Portal — управление клиентами",
uptime: "3д 7ч", cpu: 1, memMb: 48,
},
{
id: "c5", name: "unbound-fra", serverId: "srv3",
image: "mvance/unbound", tag: "1.19.3",
status: "stopped",
envs: [],
mounts: [],
interfaces: ["veth-unbound"],
startOnBoot: false, comment: "Unbound DNS резолвер (в резерве)",
cpu: 0, memMb: 0,
},
]
// ─── Certificate mock data ────────────────────────────────────────────────────
export const routerCertificates: RouterCertificate[] = [
{
id: "cert1", name: "mt-msk-core-01-server", serverId: "srv1",
commonName: "mt-msk-core-01.routerlists.io",
sans: ["mt-msk-core-01.routerlists.io","10.0.0.1"],
issuedBy: "RouterLists Internal CA", validFrom: "2024-01-15",
validUntil: "2026-01-15", daysLeft: 261,
keySize: 4096, usage: ["server"], trusted: true, status: "valid",
},
{
id: "cert2", name: "routerlists-internal-ca", serverId: "srv1",
commonName: "RouterLists Internal CA",
sans: [],
issuedBy: "RouterLists Internal CA (self-signed)", validFrom: "2023-06-01",
validUntil: "2033-06-01", daysLeft: 2589,
keySize: 4096, usage: ["ca","crl-sign"], trusted: true, status: "valid",
},
{
id: "cert3", name: "mt-spb-edge-01-server", serverId: "srv2",
commonName: "mt-spb-edge-01.routerlists.io",
sans: ["mt-spb-edge-01.routerlists.io","10.0.1.1"],
issuedBy: "RouterLists Internal CA", validFrom: "2024-01-15",
validUntil: "2026-01-15", daysLeft: 261,
keySize: 4096, usage: ["server"], trusted: true, status: "valid",
},
{
id: "cert4", name: "mt-fra-edge-01-server", serverId: "srv3",
commonName: "mt-fra-edge-01.routerlists.io",
sans: ["mt-fra-edge-01.routerlists.io","10.0.2.1"],
issuedBy: "Let's Encrypt", validFrom: "2026-01-10",
validUntil: "2026-04-10", daysLeft: -19,
keySize: 2048, usage: ["server"], trusted: true, status: "expired",
},
{
id: "cert5", name: "wg-client-admin", serverId: "srv1",
commonName: "admin.routerlists.io",
sans: ["admin.routerlists.io"],
issuedBy: "RouterLists Internal CA", validFrom: "2025-06-01",
validUntil: "2026-06-01", daysLeft: 33,
keySize: 2048, usage: ["client"], trusted: true, status: "valid",
},
{
id: "cert6", name: "mt-ams-edge-01-server", serverId: "srv4",
commonName: "mt-ams-edge-01.routerlists.io",
sans: ["mt-ams-edge-01.routerlists.io"],
issuedBy: "RouterLists Internal CA", validFrom: "2024-04-01",
validUntil: "2026-05-15", daysLeft: 16,
keySize: 2048, usage: ["server"], trusted: false, status: "valid",
},
{
id: "cert7", name: "mt-msk-lab-01-server", serverId: "srv7",
commonName: "mt-msk-lab-01.routerlists.io",
sans: ["mt-msk-lab-01.routerlists.io","10.0.0.7"],
issuedBy: "RouterLists Internal CA", validFrom: "2025-09-01",
validUntil: "2026-09-01", daysLeft: 125,
keySize: 4096, usage: ["server"], trusted: true, status: "valid",
},
]
+6
View File
@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}