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:
Denozordec
2026-05-07 12:29:04 +07:00
co-authored by Cursor
parent bdb9b72fac
commit 5f31bb47fb
81 changed files with 11976 additions and 1239 deletions
+248
View File
@@ -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
+206
View File
@@ -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