Docker images / prepare-release (push) Successful in 10s
Docker images / backend-test (push) Successful in 2m2s
Docker images / frontend-image (push) Successful in 3m23s
Docker images / updater-image (push) Successful in 44s
Docker images / backend-image (push) Successful in 2m47s
Docker images / notify-webhook (push) Skipped
Docker images / publish-release (push) Successful in 11s
При первом рестарте wipe всех таблиц и импорт из SQLite в компактную схему. Retention через DROP PARTITION, lz4 и AIO worker. Co-authored-by: Cursor <cursoragent@cursor.com>
706 lines
33 KiB
TypeScript
706 lines
33 KiB
TypeScript
import { sql } from "drizzle-orm"
|
|
import {
|
|
bigint,
|
|
boolean,
|
|
date,
|
|
doublePrecision,
|
|
index,
|
|
inet,
|
|
integer,
|
|
jsonb,
|
|
pgTable,
|
|
primaryKey,
|
|
smallint,
|
|
text,
|
|
timestamp,
|
|
uniqueIndex,
|
|
} from "drizzle-orm/pg-core"
|
|
|
|
function ts(name: string) {
|
|
return timestamp(name, { withTimezone: true, mode: "string" as const })
|
|
}
|
|
|
|
function idIdentity() {
|
|
return bigint("id", { mode: "number" }).generatedByDefaultAsIdentity()
|
|
}
|
|
|
|
function idSingleton() {
|
|
return bigint("id", { mode: "number" }).primaryKey()
|
|
}
|
|
|
|
function intPkRef() {
|
|
return bigint("server_id", { mode: "number" }).notNull()
|
|
}
|
|
|
|
// ── servers ────────────────────────────────────────────────────────────────────
|
|
|
|
export const servers = pgTable("servers", {
|
|
id: idIdentity().primaryKey(),
|
|
name: text("name").notNull().default(""),
|
|
host: text("host").notNull(),
|
|
port: integer("port").notNull().default(443),
|
|
username: text("username").notNull().default("admin"),
|
|
password: text("password").notNull().default(""),
|
|
useSsl: boolean("use_ssl").notNull().default(true),
|
|
verifySsl: boolean("verify_ssl").notNull().default(false),
|
|
type: text("type", { enum: ["jump-host", "exit-node", "home-router"] })
|
|
.notNull().default("home-router"),
|
|
site: text("site").notNull().default(""),
|
|
country: text("country").notNull().default(""),
|
|
asn: text("asn").notNull().default(""),
|
|
comment: text("comment").notNull().default(""),
|
|
enabled: boolean("enabled").notNull().default(true),
|
|
lanSubnet: text("lan_subnet").notNull().default(""),
|
|
wanUplinks: jsonb("wan_uplinks").notNull().default(sql`'[]'::jsonb`),
|
|
mgmtTunnelIp: text("mgmt_tunnel_ip").notNull().default(""),
|
|
createdAt: ts("created_at").notNull().defaultNow(),
|
|
updatedAt: ts("updated_at").notNull().defaultNow(),
|
|
})
|
|
|
|
export const serverSnapshots = pgTable("server_snapshots", {
|
|
id: idIdentity(),
|
|
serverId: intPkRef().references(() => servers.id, { onDelete: "cascade" }),
|
|
polledAt: ts("polled_at").notNull(),
|
|
status: text("status", { enum: ["online", "offline"] }).notNull(),
|
|
latencyMs: doublePrecision("latency_ms"),
|
|
rosVersion: text("ros_version"),
|
|
boardName: text("board_name"),
|
|
uptime: text("uptime"),
|
|
cpuLoad: integer("cpu_load"),
|
|
freeMemory: bigint("free_memory", { mode: "number" }),
|
|
totalMemory: bigint("total_memory", { mode: "number" }),
|
|
identityName: text("identity_name"),
|
|
rawInterfaces: jsonb("raw_interfaces"),
|
|
rawIpAddresses: jsonb("raw_ip_addresses"),
|
|
}, (t) => [
|
|
primaryKey({ columns: [t.id, t.polledAt] }),
|
|
index("idx_server_snapshots_server_time").on(t.serverId, t.polledAt),
|
|
])
|
|
|
|
export const filterRules = pgTable("filter_rules", {
|
|
id: idIdentity().primaryKey(),
|
|
serverId: intPkRef().references(() => servers.id, { onDelete: "cascade" }),
|
|
sortOrder: integer("sort_order").notNull().default(0),
|
|
community: text("community").notNull(),
|
|
communityName: text("community_name"),
|
|
action: text("action", { enum: ["route", "blackhole"] }).notNull().default("route"),
|
|
gateway: text("gateway").notNull().default(""),
|
|
gatewayTunnelId: text("gateway_tunnel_id").notNull().default(""),
|
|
description: text("description").notNull().default(""),
|
|
createdAt: ts("created_at").notNull().defaultNow(),
|
|
updatedAt: ts("updated_at").notNull().defaultNow(),
|
|
}, (t) => [
|
|
index("idx_filter_rules_server_sort").on(t.serverId, t.sortOrder),
|
|
])
|
|
|
|
export const recursiveRoutes = pgTable("recursive_routes", {
|
|
id: idIdentity().primaryKey(),
|
|
serverId: intPkRef().references(() => servers.id, { onDelete: "cascade" }),
|
|
sortOrder: integer("sort_order").notNull().default(0),
|
|
dstAddress: text("dst_address").notNull(),
|
|
gateway: text("gateway").notNull(),
|
|
distance: integer("distance").notNull().default(1),
|
|
scope: integer("scope"),
|
|
targetScope: integer("target_scope"),
|
|
routingTable: text("routing_table").notNull().default("main"),
|
|
checkGateway: text("check_gateway").notNull().default(""),
|
|
country: text("country").notNull().default(""),
|
|
comment: text("comment").notNull().default(""),
|
|
disabled: boolean("disabled").notNull().default(false),
|
|
createdAt: ts("created_at").notNull().defaultNow(),
|
|
updatedAt: ts("updated_at").notNull().defaultNow(),
|
|
}, (t) => [
|
|
index("idx_recursive_routes_server_sort").on(t.serverId, t.sortOrder),
|
|
])
|
|
|
|
export const trafficSettings = pgTable("traffic_settings", {
|
|
id: idSingleton(),
|
|
enabled: boolean("enabled").notNull().default(true),
|
|
intervalSec: integer("interval_sec").notNull().default(30),
|
|
retentionDays: integer("retention_days").notNull().default(14),
|
|
lastCollectedAt: ts("last_collected_at"),
|
|
lastDurationMs: integer("last_duration_ms"),
|
|
lastError: text("last_error"),
|
|
createdAt: ts("created_at").notNull().defaultNow(),
|
|
updatedAt: ts("updated_at").notNull().defaultNow(),
|
|
})
|
|
|
|
export const serversApiPingSettings = pgTable("servers_api_ping_settings", {
|
|
id: idSingleton(),
|
|
enabled: boolean("enabled").notNull().default(false),
|
|
intervalSec: integer("interval_sec").notNull().default(120),
|
|
lastCollectedAt: ts("last_collected_at"),
|
|
lastDurationMs: integer("last_duration_ms"),
|
|
lastError: text("last_error"),
|
|
createdAt: ts("created_at").notNull().defaultNow(),
|
|
updatedAt: ts("updated_at").notNull().defaultNow(),
|
|
})
|
|
|
|
export const serversRestPingSamples = pgTable("servers_rest_ping_samples", {
|
|
serverId: intPkRef().references(() => servers.id, { onDelete: "cascade" }),
|
|
sampledAt: ts("sampled_at").notNull(),
|
|
ok: boolean("ok").notNull(),
|
|
latencyMs: integer("latency_ms"),
|
|
error: text("error"),
|
|
}, (t) => [
|
|
primaryKey({ columns: [t.serverId, t.sampledAt] }),
|
|
])
|
|
|
|
export const trafficFlowSettings = pgTable("traffic_flow_settings", {
|
|
id: idSingleton(),
|
|
enabled: boolean("enabled").notNull().default(false),
|
|
collectorIp: text("collector_ip").notNull().default("10.255.254.1"),
|
|
flowListenPort: integer("flow_listen_port").notNull().default(4739),
|
|
wgListenPort: integer("wg_listen_port").notNull().default(51821),
|
|
prefix: text("prefix").notNull().default("10.255.254.0/24"),
|
|
publicEndpoint: text("public_endpoint").notNull().default(""),
|
|
hostPublicKey: text("host_public_key").notNull().default(""),
|
|
hostPrivateKey: text("host_private_key").notNull().default(""),
|
|
hubServerId: bigint("hub_server_id", { mode: "number" }),
|
|
retentionHours: integer("retention_hours").notNull().default(24),
|
|
topN: integer("top_n").notNull().default(200),
|
|
mapServiceMinSharePct: doublePrecision("map_service_min_share_pct").notNull().default(5),
|
|
lastDatagramAt: ts("last_datagram_at"),
|
|
lastExporterIp: text("last_exporter_ip"),
|
|
lastError: text("last_error"),
|
|
packetsReceived: bigint("packets_received", { mode: "number" }).notNull().default(0),
|
|
peersJson: jsonb("peers_json").notNull().default(sql`'[]'::jsonb`),
|
|
createdAt: ts("created_at").notNull().defaultNow(),
|
|
updatedAt: ts("updated_at").notNull().defaultNow(),
|
|
})
|
|
|
|
export const flowMinuteStats = pgTable("flow_minute_stats", {
|
|
serverId: bigint("server_id", { mode: "number" }).notNull()
|
|
.references(() => servers.id, { onDelete: "cascade" }),
|
|
bucketAt: ts("bucket_at").notNull(),
|
|
bytes: bigint("bytes", { mode: "number" }).notNull().default(0),
|
|
packets: bigint("packets", { mode: "number" }).notNull().default(0),
|
|
uniqueSrc: integer("unique_src").notNull().default(0),
|
|
uniqueDst: integer("unique_dst").notNull().default(0),
|
|
conversations: integer("conversations").notNull().default(0),
|
|
}, (t) => [
|
|
primaryKey({ columns: [t.serverId, t.bucketAt] }),
|
|
])
|
|
|
|
export const flowMinuteDims = pgTable("flow_minute_dims", {
|
|
serverId: bigint("server_id", { mode: "number" }).notNull()
|
|
.references(() => servers.id, { onDelete: "cascade" }),
|
|
bucketAt: ts("bucket_at").notNull(),
|
|
dim: text("dim").notNull(),
|
|
key: text("key").notNull(),
|
|
bytes: bigint("bytes", { mode: "number" }).notNull().default(0),
|
|
packets: bigint("packets", { mode: "number" }).notNull().default(0),
|
|
}, (t) => [
|
|
primaryKey({ columns: [t.serverId, t.bucketAt, t.dim, t.key] }),
|
|
index("idx_flow_minute_dims_time").on(t.bucketAt, t.dim),
|
|
])
|
|
|
|
export const flowDailyDims = pgTable("flow_daily_dims", {
|
|
serverId: bigint("server_id", { mode: "number" }).notNull()
|
|
.references(() => servers.id, { onDelete: "cascade" }),
|
|
day: date("day", { mode: "string" }).notNull(),
|
|
dim: text("dim").notNull(),
|
|
key: text("key").notNull(),
|
|
bytes: bigint("bytes", { mode: "number" }).notNull().default(0),
|
|
packets: bigint("packets", { mode: "number" }).notNull().default(0),
|
|
}, (t) => [
|
|
primaryKey({ columns: [t.serverId, t.day, t.dim, t.key] }),
|
|
index("idx_flow_daily_dims_day").on(t.day, t.dim),
|
|
])
|
|
|
|
export const flowBuckets = pgTable("flow_buckets", {
|
|
serverId: intPkRef().references(() => servers.id, { onDelete: "cascade" }),
|
|
bucketAt: ts("bucket_at").notNull(),
|
|
src: inet("src").notNull(),
|
|
dst: inet("dst").notNull(),
|
|
proto: smallint("proto").notNull().default(0),
|
|
srcPort: integer("src_port").notNull().default(0),
|
|
dstPort: integer("dst_port").notNull().default(0),
|
|
bytes: bigint("bytes", { mode: "number" }).notNull().default(0),
|
|
packets: bigint("packets", { mode: "number" }).notNull().default(0),
|
|
inIface: text("in_iface").notNull().default(""),
|
|
outIface: text("out_iface").notNull().default(""),
|
|
nextHop: inet("next_hop"),
|
|
flowStartMs: bigint("flow_start_ms", { mode: "number" }).notNull().default(0),
|
|
flowEndMs: bigint("flow_end_ms", { mode: "number" }).notNull().default(0),
|
|
}, (t) => [
|
|
primaryKey({
|
|
name: "flow_buckets_pkey",
|
|
columns: [t.serverId, t.bucketAt, t.src, t.dst, t.proto, t.srcPort, t.dstPort, t.inIface],
|
|
}),
|
|
index("idx_flow_buckets_server_time").on(t.serverId, t.bucketAt),
|
|
])
|
|
|
|
export const flowIpMeta = pgTable("flow_ip_meta", {
|
|
prefix: text("prefix").primaryKey(),
|
|
asn: integer("asn").notNull().default(0),
|
|
country: text("country").notNull().default(""),
|
|
lat: doublePrecision("lat"),
|
|
lng: doublePrecision("lng"),
|
|
holder: text("holder").notNull().default(""),
|
|
ok: integer("ok").notNull().default(1),
|
|
fetchedAt: ts("fetched_at").notNull(),
|
|
})
|
|
|
|
export const flowAsnMeta = pgTable("flow_asn_meta", {
|
|
asn: integer("asn").primaryKey(),
|
|
holder: text("holder").notNull().default(""),
|
|
fetchedAt: ts("fetched_at").notNull(),
|
|
})
|
|
|
|
export const trafficSamples = pgTable("traffic_samples", {
|
|
serverId: intPkRef().references(() => servers.id, { onDelete: "cascade" }),
|
|
interfaceName: text("interface_name").notNull(),
|
|
peerPublicKey: text("peer_public_key").notNull().default(""),
|
|
sampledAt: ts("sampled_at").notNull(),
|
|
rxBytes: bigint("rx_bytes", { mode: "number" }).notNull().default(0),
|
|
txBytes: bigint("tx_bytes", { mode: "number" }).notNull().default(0),
|
|
rxBps: bigint("rx_bps", { mode: "number" }).notNull().default(0),
|
|
txBps: bigint("tx_bps", { mode: "number" }).notNull().default(0),
|
|
flags: smallint("flags").notNull().default(0),
|
|
}, (t) => [
|
|
primaryKey({ columns: [t.serverId, t.sampledAt, t.interfaceName, t.peerPublicKey] }),
|
|
index("idx_traffic_samples_server_iface_time").on(t.serverId, t.interfaceName, t.sampledAt),
|
|
])
|
|
|
|
export const uptimeSettings = pgTable("uptime_settings", {
|
|
id: idSingleton(),
|
|
enabled: boolean("enabled").notNull().default(true),
|
|
resourcesEnabled: boolean("resources_enabled").notNull().default(true),
|
|
pingEnabled: boolean("ping_enabled").notNull().default(true),
|
|
speedEnabled: boolean("speed_enabled").notNull().default(true),
|
|
intervalSec: integer("interval_sec").notNull().default(15),
|
|
probeIntervalSec: integer("probe_interval_sec").notNull().default(15),
|
|
speedIntervalSec: integer("speed_interval_sec").notNull().default(60),
|
|
retentionDays: integer("retention_days").notNull().default(14),
|
|
lastCollectedAt: ts("last_collected_at"),
|
|
lastDurationMs: integer("last_duration_ms"),
|
|
lastError: text("last_error"),
|
|
createdAt: ts("created_at").notNull().defaultNow(),
|
|
updatedAt: ts("updated_at").notNull().defaultNow(),
|
|
})
|
|
|
|
export const uptimeProbes = pgTable("uptime_probes", {
|
|
id: text("id").primaryKey(),
|
|
srcServerId: bigint("src_server_id", { mode: "number" }).notNull()
|
|
.references(() => servers.id, { onDelete: "cascade" }),
|
|
srcInterface: text("src_interface").notNull().default(""),
|
|
name: text("name").notNull(),
|
|
target: text("target").notNull(),
|
|
probeFilter: text("probe_filter").notNull().default("—"),
|
|
enabled: boolean("enabled").notNull().default(true),
|
|
intervalSec: integer("interval_sec").notNull().default(0),
|
|
showOnDashboard: boolean("show_on_dashboard").notNull().default(false),
|
|
sortOrder: integer("sort_order").notNull().default(0),
|
|
createdAt: ts("created_at").notNull().defaultNow(),
|
|
updatedAt: ts("updated_at").notNull().defaultNow(),
|
|
}, (t) => [
|
|
index("idx_uptime_probes_server_sort").on(t.srcServerId, t.sortOrder),
|
|
])
|
|
|
|
export const uptimeProbeSamples = pgTable("uptime_probe_samples", {
|
|
probeId: text("probe_id").notNull().references(() => uptimeProbes.id, { onDelete: "cascade" }),
|
|
sampledAt: ts("sampled_at").notNull(),
|
|
rttMs: integer("rtt_ms"),
|
|
lossPct: integer("loss_pct").notNull().default(0),
|
|
status: text("status", { enum: ["up", "warn", "down"] }).notNull().default("down"),
|
|
}, (t) => [
|
|
primaryKey({ columns: [t.probeId, t.sampledAt] }),
|
|
])
|
|
|
|
export const uptimeResourceSamples = pgTable("uptime_resource_samples", {
|
|
serverId: intPkRef().references(() => servers.id, { onDelete: "cascade" }),
|
|
sampledAt: ts("sampled_at").notNull(),
|
|
status: text("status", { enum: ["online", "offline"] }).notNull().default("offline"),
|
|
cpuLoad: integer("cpu_load").notNull().default(0),
|
|
freeMemory: bigint("free_memory", { mode: "number" }).notNull().default(0),
|
|
totalMemory: bigint("total_memory", { mode: "number" }).notNull().default(0),
|
|
freeHddSpace: bigint("free_hdd_space", { mode: "number" }).notNull().default(0),
|
|
totalHddSpace: bigint("total_hdd_space", { mode: "number" }).notNull().default(0),
|
|
uptimeSeconds: bigint("uptime_seconds", { mode: "number" }).notNull().default(0),
|
|
}, (t) => [
|
|
primaryKey({ columns: [t.serverId, t.sampledAt] }),
|
|
])
|
|
|
|
export const uptimeSpeedProbes = pgTable("uptime_speed_probes", {
|
|
id: text("id").primaryKey(),
|
|
srcServerId: bigint("src_server_id", { mode: "number" }).notNull()
|
|
.references(() => servers.id, { onDelete: "cascade" }),
|
|
dstServerId: bigint("dst_server_id", { mode: "number" }).notNull()
|
|
.references(() => servers.id, { onDelete: "cascade" }),
|
|
srcInterface: text("src_interface").notNull().default(""),
|
|
dstInterface: text("dst_interface").notNull().default(""),
|
|
protocol: text("protocol", { enum: ["tcp", "udp"] }).notNull().default("tcp"),
|
|
direction: text("direction", { enum: ["transmit", "receive", "both"] }).notNull().default("both"),
|
|
durationSec: integer("duration_sec").notNull().default(10),
|
|
enabled: boolean("enabled").notNull().default(true),
|
|
lastRunAt: ts("last_run_at"),
|
|
lastTxAvgMbps: doublePrecision("last_tx_avg_mbps"),
|
|
lastRxAvgMbps: doublePrecision("last_rx_avg_mbps"),
|
|
lastStatus: text("last_status", { enum: ["done", "error"] }),
|
|
lastError: text("last_error"),
|
|
lastPingRttMs: integer("last_ping_rtt_ms"),
|
|
lastPingLossPct: integer("last_ping_loss_pct"),
|
|
lastPingAt: ts("last_ping_at"),
|
|
lastPingError: text("last_ping_error"),
|
|
sortOrder: integer("sort_order").notNull().default(0),
|
|
createdAt: ts("created_at").notNull().defaultNow(),
|
|
updatedAt: ts("updated_at").notNull().defaultNow(),
|
|
}, (t) => [
|
|
index("idx_uptime_speed_probes_src_sort").on(t.srcServerId, t.sortOrder),
|
|
])
|
|
|
|
export const evobgpSettings = pgTable("evobgp_settings", {
|
|
id: idSingleton(),
|
|
baseUrl: text("base_url").notNull().default(""),
|
|
apiKey: text("api_key").notNull().default(""),
|
|
enabled: boolean("enabled").notNull().default(false),
|
|
updatedAt: ts("updated_at").notNull().defaultNow(),
|
|
})
|
|
|
|
export const alertTelegramSettings = pgTable("alert_telegram_settings", {
|
|
id: idSingleton(),
|
|
botToken: text("bot_token").notNull().default(""),
|
|
chatId: text("chat_id").notNull().default(""),
|
|
messageThreadId: integer("message_thread_id"),
|
|
updatedAt: ts("updated_at").notNull().defaultNow(),
|
|
})
|
|
|
|
export const acmeSettings = pgTable("acme_settings", {
|
|
id: idSingleton(),
|
|
directoryUrl: text("directory_url").notNull().default("https://acme-v02.api.letsencrypt.org/directory"),
|
|
cloudflareApiToken: text("cloudflare_api_token").notNull().default(""),
|
|
defaultZoneId: text("default_zone_id").notNull().default(""),
|
|
accountPrivateKey: text("account_private_key").notNull().default(""),
|
|
updatedAt: ts("updated_at").notNull().defaultNow(),
|
|
})
|
|
|
|
export const certificateIssueJobs = pgTable("certificate_issue_jobs", {
|
|
id: text("id").primaryKey(),
|
|
status: text("status", { enum: ["queued", "running", "done", "failed"] }).notNull().default("queued"),
|
|
step: text("step").notNull().default("queued"),
|
|
source: text("source", { enum: ["manual", "scheduler"] }).notNull().default("manual"),
|
|
serverId: bigint("server_id", { mode: "number" }).notNull()
|
|
.references(() => servers.id, { onDelete: "cascade" }),
|
|
certName: text("cert_name").notNull(),
|
|
domainNames: jsonb("domain_names").notNull(),
|
|
keyType: text("key_type").notNull().default("rsa2048"),
|
|
trustStore: text("trust_store").notNull().default("www,api"),
|
|
requestedAt: ts("requested_at").notNull().defaultNow(),
|
|
startedAt: ts("started_at"),
|
|
finishedAt: ts("finished_at"),
|
|
error: text("error"),
|
|
})
|
|
|
|
export const certificateRenewSettings = pgTable("certificate_renew_settings", {
|
|
id: idSingleton(),
|
|
enabled: boolean("enabled").notNull().default(true),
|
|
intervalSec: integer("interval_sec").notNull().default(21600),
|
|
renewBeforeDays: integer("renew_before_days").notNull().default(30),
|
|
lastCollectedAt: ts("last_collected_at"),
|
|
lastDurationMs: integer("last_duration_ms"),
|
|
lastError: text("last_error"),
|
|
updatedAt: ts("updated_at").notNull().defaultNow(),
|
|
})
|
|
|
|
export const backupScheduleSettings = pgTable("backup_schedule_settings", {
|
|
id: idSingleton(),
|
|
enabled: boolean("enabled").notNull().default(true),
|
|
frequency: text("frequency", { enum: ["daily", "weekly", "monthly"] }).notNull().default("daily"),
|
|
hour: integer("hour").notNull().default(3),
|
|
minute: integer("minute").notNull().default(0),
|
|
weekDay: integer("week_day").notNull().default(0),
|
|
monthDay: integer("month_day").notNull().default(1),
|
|
keepCount: integer("keep_count").notNull().default(7),
|
|
format: text("format", { enum: ["rsc", "backup"] }).notNull().default("rsc"),
|
|
serverIdsJson: jsonb("server_ids_json").notNull().default(sql`'[]'::jsonb`),
|
|
lastRunAt: ts("last_run_at"),
|
|
lastDurationMs: integer("last_duration_ms"),
|
|
lastError: text("last_error"),
|
|
updatedAt: ts("updated_at").notNull().defaultNow(),
|
|
})
|
|
|
|
export const backupEntries = pgTable("backup_entries", {
|
|
id: text("id").primaryKey(),
|
|
serverId: bigint("server_id", { mode: "number" })
|
|
.references(() => servers.id, { onDelete: "set null" }),
|
|
serverName: text("server_name").notNull(),
|
|
filename: text("filename").notNull(),
|
|
sizeBytes: bigint("size_bytes", { mode: "number" }).notNull(),
|
|
kind: text("kind", { enum: ["manual", "auto"] }).notNull().default("manual"),
|
|
notes: text("notes"),
|
|
createdAt: ts("created_at").notNull(),
|
|
}, (t) => [
|
|
uniqueIndex("idx_backup_entries_filename").on(t.filename),
|
|
index("idx_backup_entries_server_created").on(t.serverId, t.createdAt),
|
|
])
|
|
|
|
export const alertGroups = pgTable("alert_groups", {
|
|
id: text("id").primaryKey(),
|
|
name: text("name").notNull(),
|
|
combineMode: text("combine_mode", { enum: ["any", "all"] }).notNull().default("any"),
|
|
enabled: boolean("enabled").notNull().default(true),
|
|
cooldownOverride: text("cooldown_override", {
|
|
enum: ["1м", "5м", "15м", "1ч", "4ч", "24ч"],
|
|
}),
|
|
createdAt: ts("created_at").notNull().defaultNow(),
|
|
updatedAt: ts("updated_at").notNull().defaultNow(),
|
|
})
|
|
|
|
export const alertRules = pgTable("alert_rules", {
|
|
id: text("id").primaryKey(),
|
|
name: text("name").notNull(),
|
|
type: text("type", {
|
|
enum: ["gre-tunnel", "bgp-peer", "bgp-prefix", "gre-client", "server", "rtt", "loss", "traffic"],
|
|
}).notNull(),
|
|
target: text("target").notNull(),
|
|
condition: text("condition").notNull(),
|
|
severity: text("severity", { enum: ["critical", "warning", "info"] }).notNull(),
|
|
enabled: boolean("enabled").notNull().default(true),
|
|
cooldown: text("cooldown", {
|
|
enum: ["1м", "5м", "15м", "1ч", "4ч", "24ч"],
|
|
}).notNull().default("5м"),
|
|
chatId: text("rule_chat_id").notNull().default(""),
|
|
confirmStabilitySec: integer("confirm_stability_sec"),
|
|
recoveryMode: text("recovery_mode", {
|
|
enum: ["always", "never", "conditional"],
|
|
}).notNull().default("always"),
|
|
recoveryStabilitySec: integer("recovery_stability_sec"),
|
|
groupId: text("group_id").references(() => alertGroups.id, { onDelete: "set null" }),
|
|
createdAt: ts("created_at").notNull().defaultNow(),
|
|
updatedAt: ts("updated_at").notNull().defaultNow(),
|
|
})
|
|
|
|
export const alertRuleTargets = pgTable("alert_rule_targets", {
|
|
id: text("id").primaryKey(),
|
|
ruleId: text("rule_id").notNull().references(() => alertRules.id, { onDelete: "cascade" }),
|
|
target: text("target").notNull(),
|
|
sortIndex: integer("sort_index").notNull().default(0),
|
|
}, (t) => [
|
|
index("idx_alert_rule_targets_rule").on(t.ruleId),
|
|
])
|
|
|
|
export const alertRuleConditions = pgTable("alert_rule_conditions", {
|
|
id: text("id").primaryKey(),
|
|
ruleId: text("rule_id").notNull().references(() => alertRules.id, { onDelete: "cascade" }),
|
|
conditionLine: text("condition_line").notNull(),
|
|
sortIndex: integer("sort_index").notNull().default(0),
|
|
}, (t) => [
|
|
index("idx_alert_rule_conditions_rule").on(t.ruleId),
|
|
])
|
|
|
|
export const alertEngineState = pgTable("alert_engine_state", {
|
|
scopeKey: text("scope_key").primaryKey(),
|
|
lastFiredAt: text("last_fired_at").notNull().default(""),
|
|
lastPayloadHash: text("last_payload_hash"),
|
|
})
|
|
|
|
export const alertEnginePrevLive = pgTable("alert_engine_prev_live", {
|
|
kind: text("kind", { enum: ["gre", "bgp"] }).primaryKey(),
|
|
payloadJson: jsonb("payload_json").notNull().default(sql`'{}'::jsonb`),
|
|
updatedAt: ts("updated_at").notNull().defaultNow(),
|
|
})
|
|
|
|
export const alertEngineConfirmPending = pgTable("alert_engine_confirm_pending", {
|
|
ruleId: text("rule_id").primaryKey(),
|
|
payloadHash: text("payload_hash").notNull(),
|
|
sinceAt: ts("since_at").notNull(),
|
|
})
|
|
|
|
export const alertHistory = pgTable("alert_history", {
|
|
id: text("id").primaryKey(),
|
|
ruleId: text("rule_id"),
|
|
groupId: text("group_id"),
|
|
ruleName: text("rule_name").notNull(),
|
|
severity: text("severity", { enum: ["critical", "warning", "info"] }).notNull(),
|
|
message: text("message").notNull(),
|
|
sentOk: boolean("sent_ok").notNull().default(true),
|
|
firedAt: ts("fired_at").notNull(),
|
|
}, (t) => [
|
|
index("idx_alert_history_fired_at").on(t.firedAt),
|
|
index("idx_alert_history_rule_id").on(t.ruleId),
|
|
])
|
|
|
|
export const alertOutbox = pgTable("alert_outbox", {
|
|
id: text("id").primaryKey(),
|
|
dedupeKey: text("dedupe_key").notNull(),
|
|
channel: text("channel", { enum: ["telegram"] }).notNull().default("telegram"),
|
|
status: text("status", { enum: ["pending", "sent", "failed"] }).notNull().default("pending"),
|
|
retryCount: integer("retry_count").notNull().default(0),
|
|
maxRetries: integer("max_retries").notNull().default(3),
|
|
nextAttemptAt: ts("next_attempt_at").notNull(),
|
|
payloadJson: jsonb("payload_json").notNull(),
|
|
createdAt: ts("created_at").notNull().defaultNow(),
|
|
sentAt: ts("sent_at"),
|
|
lastError: text("last_error"),
|
|
}, (t) => [
|
|
uniqueIndex("idx_alert_outbox_dedupe").on(t.dedupeKey),
|
|
index("idx_alert_outbox_status_next_attempt").on(t.status, t.nextAttemptAt),
|
|
])
|
|
|
|
export const alertEngineCursor = pgTable("alert_engine_cursor", {
|
|
id: idSingleton(),
|
|
lastSourceFinishedAt: ts("last_source_finished_at"),
|
|
updatedAt: ts("updated_at").notNull().defaultNow(),
|
|
})
|
|
|
|
export const schedulerRuns = pgTable("scheduler_runs", {
|
|
id: text("id").primaryKey(),
|
|
jobKey: text("job_key").notNull(),
|
|
startedAt: ts("started_at").notNull(),
|
|
finishedAt: ts("finished_at").notNull(),
|
|
status: text("status", { enum: ["ok", "error"] }).notNull(),
|
|
error: text("error"),
|
|
durationMs: integer("duration_ms").notNull().default(0),
|
|
resultJson: jsonb("result_json"),
|
|
}, (t) => [
|
|
index("idx_scheduler_runs_job_time").on(t.jobKey, t.startedAt),
|
|
])
|
|
|
|
export const events = pgTable("events", {
|
|
id: text("id").primaryKey(),
|
|
createdAt: ts("created_at").notNull(),
|
|
level: text("level", { enum: ["critical", "warning", "info"] }).notNull(),
|
|
eventType: text("event_type").notNull(),
|
|
sourceModule: text("source_module").notNull(),
|
|
title: text("title").notNull(),
|
|
message: text("message").notNull(),
|
|
entityType: text("entity_type"),
|
|
entityId: text("entity_id"),
|
|
payloadJson: jsonb("payload_json"),
|
|
}, (t) => [
|
|
index("idx_events_created_at").on(t.createdAt),
|
|
index("idx_events_level_created_at").on(t.level, t.createdAt),
|
|
index("idx_events_source_created_at").on(t.sourceModule, t.createdAt),
|
|
index("idx_events_event_type_created_at").on(t.eventType, t.createdAt),
|
|
])
|
|
|
|
export const uptimeSpeedTestRuns = pgTable("uptime_speed_test_runs", {
|
|
id: text("id").primaryKey(),
|
|
probeId: text("probe_id"),
|
|
srcServerId: bigint("src_server_id", { mode: "number" }).notNull()
|
|
.references(() => servers.id, { onDelete: "cascade" }),
|
|
dstServerId: bigint("dst_server_id", { mode: "number" }).notNull()
|
|
.references(() => servers.id, { onDelete: "cascade" }),
|
|
srcInterface: text("src_interface").notNull().default(""),
|
|
dstInterface: text("dst_interface").notNull().default(""),
|
|
srcAddress: text("src_address"),
|
|
dstAddress: text("dst_address"),
|
|
srcInterfaceAddress:text("src_interface_address"),
|
|
dstInterfaceAddress:text("dst_interface_address"),
|
|
protocol: text("protocol", { enum: ["tcp", "udp"] }).notNull().default("tcp"),
|
|
direction: text("direction", { enum: ["transmit", "receive", "both"] }).notNull().default("both"),
|
|
durationSec: integer("duration_sec").notNull().default(10),
|
|
txAvgMbps: doublePrecision("tx_avg_mbps"),
|
|
rxAvgMbps: doublePrecision("rx_avg_mbps"),
|
|
pingRttMs: integer("ping_rtt_ms"),
|
|
pingLossPct: integer("ping_loss_pct"),
|
|
pingError: text("ping_error"),
|
|
status: text("status", { enum: ["done", "error"] }).notNull().default("done"),
|
|
error: text("error"),
|
|
createdAt: ts("created_at").notNull().defaultNow(),
|
|
}, (t) => [
|
|
index("idx_uptime_speed_test_runs_created_at").on(t.createdAt),
|
|
])
|
|
|
|
export const internetPathSettings = pgTable("internet_path_settings", {
|
|
id: idSingleton(),
|
|
enabled: boolean("enabled").notNull().default(true),
|
|
intervalSec: integer("interval_sec").notNull().default(300),
|
|
retentionDays: integer("retention_days").notNull().default(14),
|
|
lastCollectedAt: ts("last_collected_at"),
|
|
lastDurationMs: integer("last_duration_ms"),
|
|
lastError: text("last_error"),
|
|
createdAt: ts("created_at").notNull().defaultNow(),
|
|
updatedAt: ts("updated_at").notNull().defaultNow(),
|
|
})
|
|
|
|
export const appUsers = pgTable("app_users", {
|
|
id: text("id").primaryKey(),
|
|
name: text("name").notNull().default(""),
|
|
login: text("login").notNull().unique(),
|
|
email: text("email").notNull().default(""),
|
|
role: text("role", { enum: ["admin", "operator", "viewer"] }).notNull().default("viewer"),
|
|
active: boolean("active").notNull().default(true),
|
|
avatar: text("avatar").notNull().default(""),
|
|
lastSeen: ts("last_seen"),
|
|
sectionsJson: jsonb("sections_json").notNull().default(sql`'[]'::jsonb`),
|
|
serversJson: jsonb("servers_json").notNull().default(sql`'[]'::jsonb`),
|
|
createdAt: ts("created_at").notNull().defaultNow(),
|
|
updatedAt: ts("updated_at").notNull().defaultNow(),
|
|
})
|
|
|
|
export const userInterfaceBindings = pgTable("user_interface_bindings", {
|
|
id: text("id").primaryKey(),
|
|
userId: text("user_id").notNull().references(() => appUsers.id, { onDelete: "cascade" }),
|
|
serverId: intPkRef().references(() => servers.id, { onDelete: "cascade" }),
|
|
interfaceName: text("interface_name").notNull(),
|
|
interfaceType: text("interface_type", { enum: ["ether", "gre", "wg", "other"] })
|
|
.notNull().default("other"),
|
|
peerPublicKey: text("peer_public_key").notNull().default(""),
|
|
peerName: text("peer_name").notNull().default(""),
|
|
comment: text("comment").notNull().default(""),
|
|
createdAt: ts("created_at").notNull().defaultNow(),
|
|
updatedAt: ts("updated_at").notNull().defaultNow(),
|
|
}, (t) => [
|
|
uniqueIndex("idx_user_iface_bind_server_name_peer").on(t.serverId, t.interfaceName, t.peerPublicKey),
|
|
index("idx_user_iface_bind_user").on(t.userId),
|
|
])
|
|
|
|
export const internetPathSnapshots = pgTable("internet_path_snapshots", {
|
|
id: idIdentity(),
|
|
sampledAt: ts("sampled_at").notNull(),
|
|
payloadJson: jsonb("payload_json").notNull(),
|
|
}, (t) => [
|
|
primaryKey({ columns: [t.id, t.sampledAt] }),
|
|
index("idx_internet_path_snapshots_sampled").on(t.sampledAt),
|
|
])
|
|
|
|
export const dataMigration = pgTable("data_migration", {
|
|
id: integer("id").primaryKey(),
|
|
sqliteImportedAt: ts("sqlite_imported_at"),
|
|
sqlitePath: text("sqlite_path"),
|
|
sqliteSha256: text("sqlite_sha256"),
|
|
reportJson: jsonb("report_json"),
|
|
})
|
|
|
|
export type Server = typeof servers.$inferSelect
|
|
export type ServerInsert = typeof servers.$inferInsert
|
|
export type Snapshot = typeof serverSnapshots.$inferSelect
|
|
export type SnapshotInsert = typeof serverSnapshots.$inferInsert
|
|
export type FilterRuleRow = typeof filterRules.$inferSelect
|
|
export type RecursiveRouteRow = typeof recursiveRoutes.$inferSelect
|
|
export type TrafficSettingsRow = typeof trafficSettings.$inferSelect
|
|
export type TrafficFlowSettingsRow = typeof trafficFlowSettings.$inferSelect
|
|
export type FlowBucketRow = typeof flowBuckets.$inferSelect
|
|
export type FlowIpMetaRow = typeof flowIpMeta.$inferSelect
|
|
export type FlowAsnMetaRow = typeof flowAsnMeta.$inferSelect
|
|
export type ServersApiPingSettingsRow = typeof serversApiPingSettings.$inferSelect
|
|
export type TrafficSampleRow = typeof trafficSamples.$inferSelect
|
|
export type UptimeSettingsRow = typeof uptimeSettings.$inferSelect
|
|
export type UptimeProbeRow = typeof uptimeProbes.$inferSelect
|
|
export type UptimeProbeSampleRow = typeof uptimeProbeSamples.$inferSelect
|
|
export type UptimeResourceSampleRow = typeof uptimeResourceSamples.$inferSelect
|
|
export type UptimeSpeedProbeRow = typeof uptimeSpeedProbes.$inferSelect
|
|
export type UptimeSpeedTestRunRow = typeof uptimeSpeedTestRuns.$inferSelect
|
|
export type InternetPathSettingsRow = typeof internetPathSettings.$inferSelect
|
|
export type InternetPathSnapshotRow = typeof internetPathSnapshots.$inferSelect
|
|
export type SchedulerRunRow = typeof schedulerRuns.$inferSelect
|
|
export type EventRow = typeof events.$inferSelect
|
|
export type EvobgpSettingsRow = typeof evobgpSettings.$inferSelect
|
|
export type AlertTelegramSettingsRow = typeof alertTelegramSettings.$inferSelect
|
|
export type AcmeSettingsRow = typeof acmeSettings.$inferSelect
|
|
export type CertificateIssueJobRow = typeof certificateIssueJobs.$inferSelect
|
|
export type CertificateRenewSettingsRow = typeof certificateRenewSettings.$inferSelect
|
|
export type BackupScheduleSettingsRow = typeof backupScheduleSettings.$inferSelect
|
|
export type BackupEntryRow = typeof backupEntries.$inferSelect
|
|
export type AlertGroupRow = typeof alertGroups.$inferSelect
|
|
export type AlertRuleRow = typeof alertRules.$inferSelect
|
|
export type AlertRuleTargetRow = typeof alertRuleTargets.$inferSelect
|
|
export type AlertEngineStateRow = typeof alertEngineState.$inferSelect
|
|
export type AlertHistoryRow = typeof alertHistory.$inferSelect
|
|
export type AlertOutboxRow = typeof alertOutbox.$inferSelect
|
|
export type AlertEngineCursorRow = typeof alertEngineCursor.$inferSelect
|
|
export type AppUserRow = typeof appUsers.$inferSelect
|
|
export type UserInterfaceBindingRow = typeof userInterfaceBindings.$inferSelect
|