chore: synchronize pending app/backend updates and repository hygiene
Includes current frontend and backend work in progress and removes generated artifacts from tracking to keep the repository clean for дальнейшая разработка. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
import Database from "better-sqlite3"
|
||||
|
||||
type SqliteHandle = InstanceType<typeof Database>
|
||||
import { drizzle } from "drizzle-orm/better-sqlite3"
|
||||
import { env } from "../config.js"
|
||||
import * as schema from "./schema.js"
|
||||
@@ -190,6 +192,60 @@ CREATE TABLE IF NOT EXISTS uptime_speed_test_runs (
|
||||
CREATE INDEX IF NOT EXISTS idx_uptime_speed_test_runs_created_at
|
||||
ON uptime_speed_test_runs(created_at);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS scheduler_runs (
|
||||
id TEXT PRIMARY KEY,
|
||||
job_key TEXT NOT NULL,
|
||||
started_at TEXT NOT NULL,
|
||||
finished_at TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
error TEXT,
|
||||
duration_ms INTEGER NOT NULL DEFAULT 0,
|
||||
result_json TEXT
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_scheduler_runs_job_time
|
||||
ON scheduler_runs(job_key, finished_at);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS servers_api_ping_settings (
|
||||
id INTEGER PRIMARY KEY,
|
||||
enabled INTEGER NOT NULL DEFAULT 0,
|
||||
interval_sec INTEGER NOT NULL DEFAULT 120,
|
||||
last_collected_at TEXT,
|
||||
last_duration_ms INTEGER,
|
||||
last_error TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS servers_rest_ping_samples (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
server_id INTEGER NOT NULL,
|
||||
sampled_at TEXT NOT NULL,
|
||||
ok INTEGER NOT NULL DEFAULT 0,
|
||||
latency_ms INTEGER,
|
||||
error TEXT,
|
||||
FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_servers_rest_ping_samples_server_id
|
||||
ON servers_rest_ping_samples(server_id, id DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS alert_gre_tunnel_samples (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sampled_at TEXT NOT NULL,
|
||||
target_label TEXT NOT NULL,
|
||||
status TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_alert_gre_tunnel_samples_label_id
|
||||
ON alert_gre_tunnel_samples(target_label, id DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS alert_bgp_peer_samples (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sampled_at TEXT NOT NULL,
|
||||
peer_key TEXT NOT NULL,
|
||||
state TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_alert_bgp_peer_samples_key_id
|
||||
ON alert_bgp_peer_samples(peer_key, id DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS evobgp_settings (
|
||||
id INTEGER PRIMARY KEY,
|
||||
base_url TEXT NOT NULL DEFAULT '',
|
||||
@@ -197,6 +253,120 @@ CREATE TABLE IF NOT EXISTS evobgp_settings (
|
||||
enabled INTEGER NOT NULL DEFAULT 0,
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS alert_telegram_settings (
|
||||
id INTEGER PRIMARY KEY,
|
||||
bot_token TEXT NOT NULL DEFAULT '',
|
||||
chat_id TEXT NOT NULL DEFAULT '',
|
||||
message_thread_id INTEGER,
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS alert_rules (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
target TEXT NOT NULL,
|
||||
condition TEXT NOT NULL,
|
||||
severity TEXT NOT NULL,
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
cooldown TEXT NOT NULL DEFAULT '5м',
|
||||
rule_chat_id TEXT NOT NULL DEFAULT '',
|
||||
recovery_mode TEXT NOT NULL DEFAULT 'always',
|
||||
recovery_stability_sec INTEGER,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS alert_history (
|
||||
id TEXT PRIMARY KEY,
|
||||
rule_id TEXT,
|
||||
rule_name TEXT NOT NULL,
|
||||
severity TEXT NOT NULL,
|
||||
message TEXT NOT NULL,
|
||||
sent_ok INTEGER NOT NULL DEFAULT 1,
|
||||
fired_at TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_alert_history_fired_at ON alert_history(fired_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_alert_history_rule_id ON alert_history(rule_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS alert_groups (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
combine_mode TEXT NOT NULL DEFAULT 'any',
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
cooldown_override TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS alert_rule_targets (
|
||||
id TEXT PRIMARY KEY,
|
||||
rule_id TEXT NOT NULL,
|
||||
target TEXT NOT NULL,
|
||||
sort_index INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_alert_rule_targets_rule ON alert_rule_targets(rule_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS alert_rule_conditions (
|
||||
id TEXT PRIMARY KEY,
|
||||
rule_id TEXT NOT NULL,
|
||||
condition_line TEXT NOT NULL,
|
||||
sort_index INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_alert_rule_conditions_rule ON alert_rule_conditions(rule_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS alert_engine_state (
|
||||
scope_key TEXT PRIMARY KEY,
|
||||
last_fired_at TEXT NOT NULL DEFAULT '',
|
||||
last_payload_hash TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS alert_engine_prev_live (
|
||||
kind TEXT PRIMARY KEY,
|
||||
payload_json TEXT NOT NULL DEFAULT '{}',
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS alert_engine_confirm_pending (
|
||||
rule_id TEXT PRIMARY KEY,
|
||||
payload_hash TEXT NOT NULL,
|
||||
since_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS alert_destinations (
|
||||
id TEXT PRIMARY KEY,
|
||||
kind TEXT NOT NULL DEFAULT 'telegram',
|
||||
label TEXT NOT NULL DEFAULT '',
|
||||
telegram_chat_id TEXT NOT NULL DEFAULT '',
|
||||
message_thread_id INTEGER,
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS alert_outbox (
|
||||
id TEXT PRIMARY KEY,
|
||||
dedupe_key TEXT NOT NULL,
|
||||
channel TEXT NOT NULL DEFAULT 'telegram',
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
retry_count INTEGER NOT NULL DEFAULT 0,
|
||||
max_retries INTEGER NOT NULL DEFAULT 3,
|
||||
next_attempt_at TEXT NOT NULL,
|
||||
payload_json TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
sent_at TEXT,
|
||||
last_error TEXT
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_alert_outbox_status_next_attempt
|
||||
ON alert_outbox(status, next_attempt_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_alert_outbox_dedupe
|
||||
ON alert_outbox(dedupe_key);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS alert_engine_cursor (
|
||||
id INTEGER PRIMARY KEY,
|
||||
last_source_finished_at TEXT,
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
`)
|
||||
|
||||
// Lightweight schema evolution for existing databases without migrations
|
||||
@@ -215,6 +385,10 @@ const hasShowOnDashboardColumn = uptimeProbeCols.some((c) => c.name === "show_on
|
||||
if (!hasShowOnDashboardColumn) {
|
||||
sqlite.exec(`ALTER TABLE uptime_probes ADD COLUMN show_on_dashboard INTEGER NOT NULL DEFAULT 0`)
|
||||
}
|
||||
const hasProbeIntervalSecColumn = uptimeProbeCols.some((c) => c.name === "interval_sec")
|
||||
if (!hasProbeIntervalSecColumn) {
|
||||
sqlite.exec(`ALTER TABLE uptime_probes ADD COLUMN interval_sec INTEGER NOT NULL DEFAULT 0`)
|
||||
}
|
||||
|
||||
const uptimeSettingsCols = sqlite.prepare(`PRAGMA table_info('uptime_settings')`).all() as Array<{ name?: string }>
|
||||
const hasProbeIntervalColumn = uptimeSettingsCols.some((c) => c.name === "probe_interval_sec")
|
||||
@@ -226,6 +400,16 @@ if (!hasSpeedIntervalColumn) {
|
||||
sqlite.exec(`ALTER TABLE uptime_settings ADD COLUMN speed_interval_sec INTEGER NOT NULL DEFAULT 60`)
|
||||
}
|
||||
|
||||
const hasUptimeJobFlags = uptimeSettingsCols.some((c) => c.name === "resources_enabled")
|
||||
if (!hasUptimeJobFlags) {
|
||||
sqlite.exec(`ALTER TABLE uptime_settings ADD COLUMN resources_enabled INTEGER NOT NULL DEFAULT 1`)
|
||||
sqlite.exec(`ALTER TABLE uptime_settings ADD COLUMN ping_enabled INTEGER NOT NULL DEFAULT 1`)
|
||||
sqlite.exec(`ALTER TABLE uptime_settings ADD COLUMN speed_enabled INTEGER NOT NULL DEFAULT 1`)
|
||||
sqlite.exec(
|
||||
`UPDATE uptime_settings SET resources_enabled = enabled, ping_enabled = enabled, speed_enabled = enabled WHERE id = 1`,
|
||||
)
|
||||
}
|
||||
|
||||
const uptimeSpeedProbeCols = sqlite.prepare(`PRAGMA table_info('uptime_speed_probes')`).all() as Array<{ name?: string }>
|
||||
const ensureSpeedProbeCol = (name: string, ddl: string) => {
|
||||
if (!uptimeSpeedProbeCols.some((c) => c.name === name)) sqlite.exec(ddl)
|
||||
@@ -240,6 +424,11 @@ ensureSpeedProbeCol("last_ping_loss_pct", `ALTER TABLE uptime_speed_probes ADD C
|
||||
ensureSpeedProbeCol("last_ping_at", `ALTER TABLE uptime_speed_probes ADD COLUMN last_ping_at TEXT`)
|
||||
ensureSpeedProbeCol("last_ping_error", `ALTER TABLE uptime_speed_probes ADD COLUMN last_ping_error TEXT`)
|
||||
|
||||
const schedulerRunCols = sqlite.prepare(`PRAGMA table_info('scheduler_runs')`).all() as Array<{ name?: string }>
|
||||
if (!schedulerRunCols.some((c) => c.name === "result_json")) {
|
||||
sqlite.exec(`ALTER TABLE scheduler_runs ADD COLUMN result_json TEXT`)
|
||||
}
|
||||
|
||||
const serverCols = sqlite.prepare(`PRAGMA table_info('servers')`).all() as Array<{ name?: string }>
|
||||
if (!serverCols.some((c) => c.name === "lan_subnet")) {
|
||||
sqlite.exec(`ALTER TABLE servers ADD COLUMN lan_subnet TEXT NOT NULL DEFAULT ''`)
|
||||
@@ -248,6 +437,44 @@ if (!serverCols.some((c) => c.name === "wan_uplinks")) {
|
||||
sqlite.exec(`ALTER TABLE servers ADD COLUMN wan_uplinks TEXT NOT NULL DEFAULT '[]'`)
|
||||
}
|
||||
|
||||
const alertTgCols = sqlite.prepare(`PRAGMA table_info('alert_telegram_settings')`).all() as Array<{ name?: string }>
|
||||
if (!alertTgCols.some((c) => c.name === "message_thread_id")) {
|
||||
sqlite.exec(`ALTER TABLE alert_telegram_settings ADD COLUMN message_thread_id INTEGER`)
|
||||
}
|
||||
|
||||
const alertRulesCols = sqlite.prepare(`PRAGMA table_info('alert_rules')`).all() as Array<{ name?: string }>
|
||||
if (!alertRulesCols.some((c) => c.name === "group_id")) {
|
||||
sqlite.exec(`ALTER TABLE alert_rules ADD COLUMN group_id TEXT`)
|
||||
}
|
||||
if (!alertRulesCols.some((c) => c.name === "confirm_stability_sec")) {
|
||||
sqlite.exec(`ALTER TABLE alert_rules ADD COLUMN confirm_stability_sec INTEGER`)
|
||||
}
|
||||
if (!alertRulesCols.some((c) => c.name === "recovery_mode")) {
|
||||
sqlite.exec(`ALTER TABLE alert_rules ADD COLUMN recovery_mode TEXT NOT NULL DEFAULT 'always'`)
|
||||
}
|
||||
if (!alertRulesCols.some((c) => c.name === "recovery_stability_sec")) {
|
||||
sqlite.exec(`ALTER TABLE alert_rules ADD COLUMN recovery_stability_sec INTEGER`)
|
||||
}
|
||||
|
||||
const alertHistoryCols = sqlite.prepare(`PRAGMA table_info('alert_history')`).all() as Array<{ name?: string }>
|
||||
if (!alertHistoryCols.some((c) => c.name === "group_id")) {
|
||||
sqlite.exec(`ALTER TABLE alert_history ADD COLUMN group_id TEXT`)
|
||||
}
|
||||
|
||||
/** Одна строка target на правило из legacy-колонки `alert_rules.target` */
|
||||
sqlite.exec(`
|
||||
INSERT OR IGNORE INTO alert_rule_targets (id, rule_id, target, sort_index)
|
||||
SELECT 'rt-' || id || '-0', id, target, 0 FROM alert_rules
|
||||
WHERE id NOT IN (SELECT rule_id FROM alert_rule_targets)
|
||||
`)
|
||||
|
||||
/** Одна строка условия из legacy `alert_rules.condition` */
|
||||
sqlite.exec(`
|
||||
INSERT OR IGNORE INTO alert_rule_conditions (id, rule_id, condition_line, sort_index)
|
||||
SELECT 'rc-' || id || '-0', id, condition, 0 FROM alert_rules
|
||||
WHERE id NOT IN (SELECT rule_id FROM alert_rule_conditions)
|
||||
`)
|
||||
|
||||
sqlite.exec(`
|
||||
INSERT INTO traffic_settings (id, enabled, interval_sec, retention_days)
|
||||
SELECT 1, 1, 30, 14
|
||||
@@ -266,4 +493,25 @@ SELECT 1, '', '', 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM evobgp_settings WHERE id = 1);
|
||||
`)
|
||||
|
||||
sqlite.exec(`
|
||||
INSERT INTO servers_api_ping_settings (id, enabled, interval_sec)
|
||||
SELECT 1, 0, 120
|
||||
WHERE NOT EXISTS (SELECT 1 FROM servers_api_ping_settings WHERE id = 1);
|
||||
`)
|
||||
|
||||
sqlite.exec(`
|
||||
INSERT INTO alert_telegram_settings (id, bot_token, chat_id)
|
||||
SELECT 1, '', ''
|
||||
WHERE NOT EXISTS (SELECT 1 FROM alert_telegram_settings WHERE id = 1);
|
||||
`)
|
||||
|
||||
sqlite.exec(`
|
||||
INSERT INTO alert_engine_cursor (id, last_source_finished_at)
|
||||
SELECT 1, NULL
|
||||
WHERE NOT EXISTS (SELECT 1 FROM alert_engine_cursor WHERE id = 1);
|
||||
`)
|
||||
|
||||
export const db = drizzle(sqlite, { schema })
|
||||
|
||||
/** Прямой доступ к better-sqlite3 для сложных read-only запросов (напр. /api/alerts). */
|
||||
export const sqliteDatabase: SqliteHandle = sqlite
|
||||
|
||||
@@ -115,6 +115,46 @@ export const trafficSettings = sqliteTable("traffic_settings", {
|
||||
updatedAt: text("updated_at").notNull().default(sql`(datetime('now'))`),
|
||||
})
|
||||
|
||||
/** Настройки фоновой проверки доступности RouterOS REST API по серверам каталога. */
|
||||
export const serversApiPingSettings = sqliteTable("servers_api_ping_settings", {
|
||||
id: integer("id").primaryKey(),
|
||||
enabled: integer("enabled", { mode: "boolean" }).notNull().default(false),
|
||||
intervalSec: integer("interval_sec").notNull().default(120),
|
||||
lastCollectedAt: text("last_collected_at"),
|
||||
lastDurationMs: integer("last_duration_ms"),
|
||||
lastError: text("last_error"),
|
||||
createdAt: text("created_at").notNull().default(sql`(datetime('now'))`),
|
||||
updatedAt: text("updated_at").notNull().default(sql`(datetime('now'))`),
|
||||
})
|
||||
|
||||
/** Сырые пробы REST `/system/identity` по серверам — для UI сбора и для `buildSignalSnapshot` (алерты). */
|
||||
export const serversRestPingSamples = sqliteTable("servers_rest_ping_samples", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
serverId: integer("server_id")
|
||||
.notNull()
|
||||
.references(() => servers.id, { onDelete: "cascade" }),
|
||||
sampledAt: text("sampled_at").notNull(),
|
||||
ok: integer("ok", { mode: "boolean" }).notNull(),
|
||||
latencyMs: integer("latency_ms"),
|
||||
error: text("error"),
|
||||
})
|
||||
|
||||
/** Сырые снимки GRE для алертов — пишет джоба `gre_bgp`. */
|
||||
export const alertGreTunnelSamples = sqliteTable("alert_gre_tunnel_samples", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
sampledAt: text("sampled_at").notNull(),
|
||||
targetLabel: text("target_label").notNull(),
|
||||
status: text("status").notNull(),
|
||||
})
|
||||
|
||||
/** Сырые снимки BGP-сессий для алертов — пишет джоба `gre_bgp`. */
|
||||
export const alertBgpPeerSamples = sqliteTable("alert_bgp_peer_samples", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
sampledAt: text("sampled_at").notNull(),
|
||||
peerKey: text("peer_key").notNull(),
|
||||
state: text("state").notNull(),
|
||||
})
|
||||
|
||||
// ── raw traffic samples (per server/interface/timepoint) ──────────────────────
|
||||
|
||||
export const trafficSamples = sqliteTable("traffic_samples", {
|
||||
@@ -136,7 +176,11 @@ export const trafficSamples = sqliteTable("traffic_samples", {
|
||||
|
||||
export const uptimeSettings = sqliteTable("uptime_settings", {
|
||||
id: integer("id").primaryKey(),
|
||||
/** Устаревший агрегат: синхронизируется как resources ∨ ping ∨ speed (для совместимости). */
|
||||
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
||||
resourcesEnabled: integer("resources_enabled", { mode: "boolean" }).notNull().default(true),
|
||||
pingEnabled: integer("ping_enabled", { mode: "boolean" }).notNull().default(true),
|
||||
speedEnabled: integer("speed_enabled", { mode: "boolean" }).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),
|
||||
@@ -158,6 +202,8 @@ export const uptimeProbes = sqliteTable("uptime_probes", {
|
||||
target: text("target").notNull(),
|
||||
probeFilter: text("probe_filter").notNull().default("—"),
|
||||
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
||||
/** 0 — брать глобальный probe_interval_sec из uptime_settings */
|
||||
intervalSec: integer("interval_sec").notNull().default(0),
|
||||
/** Выводить пробу в блоке «Активные пробы» на дашборде */
|
||||
showOnDashboard: integer("show_on_dashboard", { mode: "boolean" }).notNull().default(false),
|
||||
sortOrder: integer("sort_order").notNull().default(0),
|
||||
@@ -231,6 +277,155 @@ export const evobgpSettings = sqliteTable("evobgp_settings", {
|
||||
updatedAt: text("updated_at").notNull().default(sql`(datetime('now'))`),
|
||||
})
|
||||
|
||||
/** Единственная строка id=1: токен и чат Telegram для оповещений. */
|
||||
export const alertTelegramSettings = sqliteTable("alert_telegram_settings", {
|
||||
id: integer("id").primaryKey(),
|
||||
botToken: text("bot_token").notNull().default(""),
|
||||
chatId: text("chat_id").notNull().default(""),
|
||||
/** Тема супергруппы (forum): `message_thread_id` в Bot API; NULL — общий чат */
|
||||
messageThreadId: integer("message_thread_id"),
|
||||
updatedAt: text("updated_at").notNull().default(sql`(datetime('now'))`),
|
||||
})
|
||||
|
||||
/** Группы правил: ANY = хотя бы одно; ALL = все одновременно в окне тика. */
|
||||
export const alertGroups = sqliteTable("alert_groups", {
|
||||
id: text("id").primaryKey(),
|
||||
name: text("name").notNull(),
|
||||
combineMode: text("combine_mode", { enum: ["any", "all"] }).notNull().default("any"),
|
||||
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
||||
/** Переопределение cooldown для агрегата группы; NULL — по умолчанию 5м */
|
||||
cooldownOverride: text("cooldown_override", {
|
||||
enum: ["1м", "5м", "15м", "1ч", "4ч", "24ч"],
|
||||
}),
|
||||
createdAt: text("created_at").notNull().default(sql`(datetime('now'))`),
|
||||
updatedAt: text("updated_at").notNull().default(sql`(datetime('now'))`),
|
||||
})
|
||||
|
||||
/** Правила оповещений (UI /alerts). */
|
||||
export const alertRules = sqliteTable("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 или join); для совместимости и поиска */
|
||||
target: text("target").notNull(),
|
||||
condition: text("condition").notNull(),
|
||||
severity: text("severity", { enum: ["critical", "warning", "info"] }).notNull(),
|
||||
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
||||
cooldown: text("cooldown", {
|
||||
enum: ["1м", "5м", "15м", "1ч", "4ч", "24ч"],
|
||||
}).notNull().default("5м"),
|
||||
chatId: text("rule_chat_id").notNull().default(""),
|
||||
/** NULL / 0 — выкл. Секунды: уведомление только если условие держится столько времени без «отмены» (восстановление сигнала). */
|
||||
confirmStabilitySec: integer("confirm_stability_sec"),
|
||||
/** Политика recovery-уведомления для правила. */
|
||||
recoveryMode: text("recovery_mode", {
|
||||
enum: ["always", "never", "conditional"],
|
||||
}).notNull().default("always"),
|
||||
/** Доп. задержка подтверждения для recovery при `conditional`; NULL — без доп. ожидания. */
|
||||
recoveryStabilitySec: integer("recovery_stability_sec"),
|
||||
/** NULL — правило вне группы (отдельные отправки по cooldown правила) */
|
||||
groupId: text("group_id"),
|
||||
createdAt: text("created_at").notNull().default(sql`(datetime('now'))`),
|
||||
updatedAt: text("updated_at").notNull().default(sql`(datetime('now'))`),
|
||||
})
|
||||
|
||||
/** Несколько объектов на правило; срабатывание по OR (хотя бы один). */
|
||||
export const alertRuleTargets = sqliteTable("alert_rule_targets", {
|
||||
id: text("id").primaryKey(),
|
||||
ruleId: text("rule_id").notNull(),
|
||||
target: text("target").notNull(),
|
||||
sortIndex: integer("sort_index").notNull().default(0),
|
||||
})
|
||||
|
||||
/** Несколько условий на правило; срабатывание по OR (любое из выбранных). */
|
||||
export const alertRuleConditions = sqliteTable("alert_rule_conditions", {
|
||||
id: text("id").primaryKey(),
|
||||
ruleId: text("rule_id").notNull(),
|
||||
conditionLine: text("condition_line").notNull(),
|
||||
sortIndex: integer("sort_index").notNull().default(0),
|
||||
})
|
||||
|
||||
/** Состояние движка: cooldown / дедуп по ключу rule:id или group:id */
|
||||
export const alertEngineState = sqliteTable("alert_engine_state", {
|
||||
scopeKey: text("scope_key").primaryKey(),
|
||||
lastFiredAt: text("last_fired_at").notNull().default(""),
|
||||
lastPayloadHash: text("last_payload_hash"),
|
||||
})
|
||||
|
||||
/** Снимок прошлого тика для live-сигналов (GRE/BGP): переходы «offline» / «восстановился». */
|
||||
export const alertEnginePrevLive = sqliteTable("alert_engine_prev_live", {
|
||||
kind: text("kind", { enum: ["gre", "bgp"] }).primaryKey(),
|
||||
payloadJson: text("payload_json").notNull().default("{}"),
|
||||
updatedAt: text("updated_at").notNull().default(sql`(datetime('now'))`),
|
||||
})
|
||||
|
||||
/** Ожидание стабильности срабатывания по правилу (антидребезг). */
|
||||
export const alertEngineConfirmPending = sqliteTable("alert_engine_confirm_pending", {
|
||||
ruleId: text("rule_id").primaryKey(),
|
||||
payloadHash: text("payload_hash").notNull(),
|
||||
sinceAt: text("since_at").notNull(),
|
||||
})
|
||||
|
||||
/** Задел: несколько получателей Telegram (пока UI не подключён) */
|
||||
export const alertDestinations = sqliteTable("alert_destinations", {
|
||||
id: text("id").primaryKey(),
|
||||
kind: text("kind", { enum: ["telegram"] }).notNull().default("telegram"),
|
||||
label: text("label").notNull().default(""),
|
||||
telegramChatId: text("telegram_chat_id").notNull().default(""),
|
||||
messageThreadId: integer("message_thread_id"),
|
||||
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
||||
createdAt: text("created_at").notNull().default(sql`(datetime('now'))`),
|
||||
})
|
||||
|
||||
/** Журнал отправок / срабатываний (персист + при необходимости пополняется воркером). */
|
||||
export const alertHistory = sqliteTable("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: integer("sent_ok", { mode: "boolean" }).notNull().default(true),
|
||||
firedAt: text("fired_at").notNull(),
|
||||
})
|
||||
|
||||
/** Outbox доставки уведомлений: ретраи и идемпотентность отправки каналов. */
|
||||
export const alertOutbox = sqliteTable("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:text("next_attempt_at").notNull(),
|
||||
payloadJson: text("payload_json").notNull(),
|
||||
createdAt: text("created_at").notNull().default(sql`(datetime('now'))`),
|
||||
sentAt: text("sent_at"),
|
||||
lastError: text("last_error"),
|
||||
})
|
||||
|
||||
/** Cursor движка: watermark последней обработанной точки snapshot-источников. */
|
||||
export const alertEngineCursor = sqliteTable("alert_engine_cursor", {
|
||||
id: integer("id").primaryKey(),
|
||||
lastSourceFinishedAt: text("last_source_finished_at"),
|
||||
updatedAt: text("updated_at").notNull().default(sql`(datetime('now'))`),
|
||||
})
|
||||
|
||||
/** Журнал прогонов планировщика (аудит). */
|
||||
export const schedulerRuns = sqliteTable("scheduler_runs", {
|
||||
id: text("id").primaryKey(),
|
||||
jobKey: text("job_key").notNull(),
|
||||
startedAt: text("started_at").notNull(),
|
||||
finishedAt: text("finished_at").notNull(),
|
||||
status: text("status", { enum: ["ok", "error"] }).notNull(),
|
||||
error: text("error"),
|
||||
durationMs: integer("duration_ms").notNull().default(0),
|
||||
/** JSON: см. `SchedulerRunSnapshot` в types/scheduler-run-snapshot.ts */
|
||||
resultJson: text("result_json"),
|
||||
})
|
||||
|
||||
export const uptimeSpeedTestRuns = sqliteTable("uptime_speed_test_runs", {
|
||||
id: text("id").primaryKey(),
|
||||
probeId: text("probe_id"),
|
||||
@@ -264,6 +459,7 @@ 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 ServersApiPingSettingsRow = typeof serversApiPingSettings.$inferSelect
|
||||
export type TrafficSampleRow = typeof trafficSamples.$inferSelect
|
||||
export type UptimeSettingsRow = typeof uptimeSettings.$inferSelect
|
||||
export type UptimeProbeRow = typeof uptimeProbes.$inferSelect
|
||||
@@ -271,4 +467,14 @@ 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 SchedulerRunRow = typeof schedulerRuns.$inferSelect
|
||||
export type EvobgpSettingsRow = typeof evobgpSettings.$inferSelect
|
||||
export type AlertTelegramSettingsRow = typeof alertTelegramSettings.$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 AlertDestinationRow = typeof alertDestinations.$inferSelect
|
||||
export type AlertHistoryRow = typeof alertHistory.$inferSelect
|
||||
export type AlertOutboxRow = typeof alertOutbox.$inferSelect
|
||||
export type AlertEngineCursorRow = typeof alertEngineCursor.$inferSelect
|
||||
|
||||
Reference in New Issue
Block a user