Docker images / prepare-release (push) Successful in 8s
Docker images / backend-image (push) Successful in 2m1s
Docker images / frontend-image (push) Successful in 3m56s
Docker images / notify-webhook (push) Skipped
Docker images / updater-image (push) Successful in 46s
Docker images / publish-release (push) Successful in 12s
Чтобы видеть «кто с кем», а не только объём порта: IPFIX внутри WG на хосте Docker MM, REST-счётчики не трогаем. Co-authored-by: Cursor <cursoragent@cursor.com>
870 lines
32 KiB
TypeScript
870 lines
32 KiB
TypeScript
import Database from "better-sqlite3"
|
|
import { existsSync, readFileSync } from "node:fs"
|
|
import path from "node:path"
|
|
|
|
type SqliteHandle = InstanceType<typeof Database>
|
|
import { drizzle } from "drizzle-orm/better-sqlite3"
|
|
import { env } from "../config.js"
|
|
import * as schema from "./schema.js"
|
|
|
|
const sqlite = new Database(env.DATABASE_PATH)
|
|
|
|
// WAL mode for better concurrent read performance
|
|
sqlite.pragma("journal_mode = WAL")
|
|
sqlite.pragma("foreign_keys = ON")
|
|
sqlite.exec(`
|
|
CREATE TABLE IF NOT EXISTS servers (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL DEFAULT '',
|
|
host TEXT NOT NULL,
|
|
port INTEGER NOT NULL DEFAULT 443,
|
|
username TEXT NOT NULL DEFAULT 'admin',
|
|
password TEXT NOT NULL DEFAULT '',
|
|
use_ssl INTEGER NOT NULL DEFAULT 1,
|
|
verify_ssl INTEGER NOT NULL DEFAULT 0,
|
|
type TEXT NOT NULL DEFAULT 'home-router',
|
|
site TEXT NOT NULL DEFAULT '',
|
|
country TEXT NOT NULL DEFAULT '',
|
|
asn TEXT NOT NULL DEFAULT '',
|
|
comment TEXT NOT NULL DEFAULT '',
|
|
enabled INTEGER NOT NULL DEFAULT 1,
|
|
lan_subnet TEXT NOT NULL DEFAULT '',
|
|
wan_uplinks TEXT NOT NULL DEFAULT '[]',
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS server_snapshots (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
server_id INTEGER NOT NULL,
|
|
polled_at TEXT NOT NULL,
|
|
status TEXT NOT NULL,
|
|
latency_ms REAL,
|
|
ros_version TEXT,
|
|
board_name TEXT,
|
|
uptime TEXT,
|
|
cpu_load INTEGER,
|
|
free_memory INTEGER,
|
|
total_memory INTEGER,
|
|
identity_name TEXT,
|
|
raw_interfaces TEXT,
|
|
raw_ip_addresses TEXT,
|
|
FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS filter_rules (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
server_id INTEGER NOT NULL,
|
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
community TEXT NOT NULL,
|
|
community_name TEXT,
|
|
action TEXT NOT NULL DEFAULT 'route',
|
|
gateway TEXT NOT NULL DEFAULT '',
|
|
gateway_tunnel_id TEXT NOT NULL DEFAULT '',
|
|
description TEXT NOT NULL DEFAULT '',
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_filter_rules_server_sort
|
|
ON filter_rules(server_id, sort_order);
|
|
|
|
CREATE TABLE IF NOT EXISTS recursive_routes (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
server_id INTEGER NOT NULL,
|
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
dst_address TEXT NOT NULL,
|
|
gateway TEXT NOT NULL,
|
|
distance INTEGER NOT NULL DEFAULT 1,
|
|
scope INTEGER,
|
|
target_scope INTEGER,
|
|
routing_table TEXT NOT NULL DEFAULT 'main',
|
|
check_gateway TEXT NOT NULL DEFAULT '',
|
|
country TEXT NOT NULL DEFAULT '',
|
|
comment TEXT NOT NULL DEFAULT '',
|
|
disabled INTEGER NOT NULL DEFAULT 0,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_recursive_routes_server_sort
|
|
ON recursive_routes(server_id, sort_order);
|
|
|
|
CREATE TABLE IF NOT EXISTS traffic_settings (
|
|
id INTEGER PRIMARY KEY,
|
|
enabled INTEGER NOT NULL DEFAULT 1,
|
|
interval_sec INTEGER NOT NULL DEFAULT 30,
|
|
retention_days INTEGER NOT NULL DEFAULT 14,
|
|
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 traffic_samples (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
server_id INTEGER NOT NULL,
|
|
interface_name TEXT NOT NULL,
|
|
peer_public_key TEXT NOT NULL DEFAULT '',
|
|
sampled_at TEXT NOT NULL,
|
|
rx_bytes INTEGER NOT NULL DEFAULT 0,
|
|
tx_bytes INTEGER NOT NULL DEFAULT 0,
|
|
rx_bps INTEGER NOT NULL DEFAULT 0,
|
|
tx_bps INTEGER NOT NULL DEFAULT 0,
|
|
running INTEGER NOT NULL DEFAULT 0,
|
|
disabled INTEGER NOT NULL DEFAULT 0,
|
|
FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_traffic_samples_server_time
|
|
ON traffic_samples(server_id, sampled_at);
|
|
CREATE INDEX IF NOT EXISTS idx_traffic_samples_server_iface_time
|
|
ON traffic_samples(server_id, interface_name, sampled_at);
|
|
|
|
CREATE TABLE IF NOT EXISTS traffic_flow_settings (
|
|
id INTEGER PRIMARY KEY,
|
|
enabled INTEGER NOT NULL DEFAULT 0,
|
|
collector_ip TEXT NOT NULL DEFAULT '10.255.254.1',
|
|
flow_listen_port INTEGER NOT NULL DEFAULT 4739,
|
|
wg_listen_port INTEGER NOT NULL DEFAULT 51821,
|
|
prefix TEXT NOT NULL DEFAULT '10.255.254.0/24',
|
|
public_endpoint TEXT NOT NULL DEFAULT '',
|
|
host_public_key TEXT NOT NULL DEFAULT '',
|
|
host_private_key TEXT NOT NULL DEFAULT '',
|
|
hub_server_id INTEGER,
|
|
retention_hours INTEGER NOT NULL DEFAULT 24,
|
|
top_n INTEGER NOT NULL DEFAULT 200,
|
|
last_datagram_at TEXT,
|
|
last_exporter_ip TEXT,
|
|
last_error TEXT,
|
|
packets_received INTEGER NOT NULL DEFAULT 0,
|
|
peers_json TEXT NOT NULL DEFAULT '[]',
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS flow_buckets (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
server_id INTEGER NOT NULL,
|
|
bucket_at TEXT NOT NULL,
|
|
src TEXT NOT NULL,
|
|
dst TEXT NOT NULL,
|
|
proto INTEGER NOT NULL DEFAULT 0,
|
|
src_port INTEGER NOT NULL DEFAULT 0,
|
|
dst_port INTEGER NOT NULL DEFAULT 0,
|
|
bytes INTEGER NOT NULL DEFAULT 0,
|
|
packets INTEGER NOT NULL DEFAULT 0,
|
|
in_iface TEXT NOT NULL DEFAULT '',
|
|
FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE
|
|
);
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_flow_buckets_unique
|
|
ON flow_buckets(server_id, bucket_at, src, dst, proto, src_port, dst_port);
|
|
CREATE INDEX IF NOT EXISTS idx_flow_buckets_server_time
|
|
ON flow_buckets(server_id, bucket_at);
|
|
|
|
CREATE TABLE IF NOT EXISTS uptime_settings (
|
|
id INTEGER PRIMARY KEY,
|
|
enabled INTEGER NOT NULL DEFAULT 1,
|
|
interval_sec INTEGER NOT NULL DEFAULT 15,
|
|
probe_interval_sec INTEGER NOT NULL DEFAULT 15,
|
|
speed_interval_sec INTEGER NOT NULL DEFAULT 60,
|
|
retention_days INTEGER NOT NULL DEFAULT 14,
|
|
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 uptime_probes (
|
|
id TEXT PRIMARY KEY,
|
|
src_server_id INTEGER NOT NULL,
|
|
src_interface TEXT NOT NULL DEFAULT '',
|
|
name TEXT NOT NULL,
|
|
target TEXT NOT NULL,
|
|
probe_filter TEXT NOT NULL DEFAULT '—',
|
|
enabled INTEGER NOT NULL DEFAULT 1,
|
|
show_on_dashboard INTEGER NOT NULL DEFAULT 0,
|
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
FOREIGN KEY (src_server_id) REFERENCES servers(id) ON DELETE CASCADE
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_uptime_probes_server_sort
|
|
ON uptime_probes(src_server_id, sort_order);
|
|
|
|
CREATE TABLE IF NOT EXISTS uptime_probe_samples (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
probe_id TEXT NOT NULL,
|
|
sampled_at TEXT NOT NULL,
|
|
rtt_ms INTEGER,
|
|
loss_pct INTEGER NOT NULL DEFAULT 0,
|
|
status TEXT NOT NULL DEFAULT 'down',
|
|
FOREIGN KEY (probe_id) REFERENCES uptime_probes(id) ON DELETE CASCADE
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_uptime_probe_samples_probe_time
|
|
ON uptime_probe_samples(probe_id, sampled_at);
|
|
|
|
CREATE TABLE IF NOT EXISTS uptime_resource_samples (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
server_id INTEGER NOT NULL,
|
|
sampled_at TEXT NOT NULL,
|
|
status TEXT NOT NULL DEFAULT 'offline',
|
|
cpu_load INTEGER NOT NULL DEFAULT 0,
|
|
free_memory INTEGER NOT NULL DEFAULT 0,
|
|
total_memory INTEGER NOT NULL DEFAULT 0,
|
|
free_hdd_space INTEGER NOT NULL DEFAULT 0,
|
|
total_hdd_space INTEGER NOT NULL DEFAULT 0,
|
|
uptime_seconds INTEGER NOT NULL DEFAULT 0,
|
|
board_name TEXT NOT NULL DEFAULT '',
|
|
ros_version TEXT NOT NULL DEFAULT '',
|
|
FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_uptime_resource_samples_server_time
|
|
ON uptime_resource_samples(server_id, sampled_at);
|
|
|
|
CREATE TABLE IF NOT EXISTS uptime_speed_probes (
|
|
id TEXT PRIMARY KEY,
|
|
src_server_id INTEGER NOT NULL,
|
|
dst_server_id INTEGER NOT NULL,
|
|
src_interface TEXT NOT NULL DEFAULT '',
|
|
dst_interface TEXT NOT NULL DEFAULT '',
|
|
protocol TEXT NOT NULL DEFAULT 'tcp',
|
|
direction TEXT NOT NULL DEFAULT 'both',
|
|
duration_sec INTEGER NOT NULL DEFAULT 10,
|
|
enabled INTEGER NOT NULL DEFAULT 1,
|
|
last_run_at TEXT,
|
|
last_tx_avg_mbps REAL,
|
|
last_rx_avg_mbps REAL,
|
|
last_status TEXT,
|
|
last_error TEXT,
|
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
FOREIGN KEY (src_server_id) REFERENCES servers(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (dst_server_id) REFERENCES servers(id) ON DELETE CASCADE
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_uptime_speed_probes_src_sort
|
|
ON uptime_speed_probes(src_server_id, sort_order);
|
|
|
|
CREATE TABLE IF NOT EXISTS uptime_speed_test_runs (
|
|
id TEXT PRIMARY KEY,
|
|
probe_id TEXT,
|
|
src_server_id INTEGER NOT NULL,
|
|
dst_server_id INTEGER NOT NULL,
|
|
src_interface TEXT NOT NULL DEFAULT '',
|
|
dst_interface TEXT NOT NULL DEFAULT '',
|
|
src_address TEXT,
|
|
dst_address TEXT,
|
|
src_interface_address TEXT,
|
|
dst_interface_address TEXT,
|
|
protocol TEXT NOT NULL DEFAULT 'tcp',
|
|
direction TEXT NOT NULL DEFAULT 'both',
|
|
duration_sec INTEGER NOT NULL DEFAULT 10,
|
|
tx_avg_mbps REAL,
|
|
rx_avg_mbps REAL,
|
|
ping_rtt_ms INTEGER,
|
|
ping_loss_pct INTEGER,
|
|
ping_error TEXT,
|
|
status TEXT NOT NULL DEFAULT 'done',
|
|
error TEXT,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
FOREIGN KEY (src_server_id) REFERENCES servers(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (dst_server_id) REFERENCES servers(id) ON DELETE CASCADE
|
|
);
|
|
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 internet_path_settings (
|
|
id INTEGER PRIMARY KEY,
|
|
enabled INTEGER NOT NULL DEFAULT 1,
|
|
interval_sec INTEGER NOT NULL DEFAULT 300,
|
|
retention_days INTEGER NOT NULL DEFAULT 14,
|
|
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 internet_path_snapshots (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
sampled_at TEXT NOT NULL,
|
|
payload_json TEXT NOT NULL
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_internet_path_snapshots_sampled
|
|
ON internet_path_snapshots(sampled_at DESC);
|
|
|
|
CREATE TABLE IF NOT EXISTS events (
|
|
id TEXT PRIMARY KEY,
|
|
created_at TEXT NOT NULL,
|
|
level TEXT NOT NULL,
|
|
event_type TEXT NOT NULL,
|
|
source_module TEXT NOT NULL,
|
|
title TEXT NOT NULL,
|
|
message TEXT NOT NULL,
|
|
entity_type TEXT,
|
|
entity_id TEXT,
|
|
payload_json TEXT
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_events_created_at ON events(created_at DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_events_level_created_at ON events(level, created_at DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_events_source_created_at ON events(source_module, created_at DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_events_event_type_created_at ON events(event_type, created_at DESC);
|
|
|
|
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 '',
|
|
api_key TEXT NOT NULL DEFAULT '',
|
|
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 acme_settings (
|
|
id INTEGER PRIMARY KEY,
|
|
directory_url TEXT NOT NULL DEFAULT 'https://acme-v02.api.letsencrypt.org/directory',
|
|
cloudflare_api_token TEXT NOT NULL DEFAULT '',
|
|
default_zone_id TEXT NOT NULL DEFAULT '',
|
|
account_private_key TEXT NOT NULL DEFAULT '',
|
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS certificate_issue_jobs (
|
|
id TEXT PRIMARY KEY,
|
|
status TEXT NOT NULL DEFAULT 'queued',
|
|
step TEXT NOT NULL DEFAULT 'queued',
|
|
source TEXT NOT NULL DEFAULT 'manual',
|
|
server_id TEXT NOT NULL,
|
|
cert_name TEXT NOT NULL,
|
|
domain_names TEXT NOT NULL,
|
|
key_type TEXT NOT NULL DEFAULT 'rsa2048',
|
|
trust_store TEXT NOT NULL DEFAULT 'www,api',
|
|
requested_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
started_at TEXT,
|
|
finished_at TEXT,
|
|
error TEXT
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS certificate_renew_settings (
|
|
id INTEGER PRIMARY KEY,
|
|
enabled INTEGER NOT NULL DEFAULT 1,
|
|
interval_sec INTEGER NOT NULL DEFAULT 21600,
|
|
renew_before_days INTEGER NOT NULL DEFAULT 30,
|
|
last_collected_at TEXT,
|
|
last_duration_ms INTEGER,
|
|
last_error TEXT,
|
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS backup_schedule_settings (
|
|
id INTEGER PRIMARY KEY,
|
|
enabled INTEGER NOT NULL DEFAULT 1,
|
|
frequency TEXT NOT NULL DEFAULT 'daily',
|
|
hour INTEGER NOT NULL DEFAULT 3,
|
|
minute INTEGER NOT NULL DEFAULT 0,
|
|
week_day INTEGER NOT NULL DEFAULT 0,
|
|
month_day INTEGER NOT NULL DEFAULT 1,
|
|
keep_count INTEGER NOT NULL DEFAULT 7,
|
|
format TEXT NOT NULL DEFAULT 'rsc',
|
|
server_ids_json TEXT NOT NULL DEFAULT '[]',
|
|
last_run_at TEXT,
|
|
last_duration_ms INTEGER,
|
|
last_error TEXT,
|
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS backup_entries (
|
|
id TEXT PRIMARY KEY,
|
|
server_id TEXT NOT NULL,
|
|
server_name TEXT NOT NULL,
|
|
filename TEXT NOT NULL,
|
|
size_bytes INTEGER NOT NULL,
|
|
kind TEXT NOT NULL DEFAULT 'manual',
|
|
notes TEXT,
|
|
created_at TEXT NOT NULL
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_backup_entries_server_created ON backup_entries(server_id, created_at);
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_backup_entries_filename ON backup_entries(filename);
|
|
|
|
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'))
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS app_users (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL DEFAULT '',
|
|
login TEXT NOT NULL UNIQUE,
|
|
email TEXT NOT NULL DEFAULT '',
|
|
role TEXT NOT NULL DEFAULT 'viewer',
|
|
active INTEGER NOT NULL DEFAULT 1,
|
|
avatar TEXT NOT NULL DEFAULT '',
|
|
last_seen TEXT,
|
|
sections_json TEXT NOT NULL DEFAULT '[]',
|
|
servers_json TEXT NOT NULL DEFAULT '[]',
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS user_interface_bindings (
|
|
id TEXT PRIMARY KEY,
|
|
user_id TEXT NOT NULL,
|
|
server_id INTEGER NOT NULL,
|
|
interface_name TEXT NOT NULL,
|
|
interface_type TEXT NOT NULL DEFAULT 'other',
|
|
peer_public_key TEXT NOT NULL DEFAULT '',
|
|
peer_name TEXT NOT NULL DEFAULT '',
|
|
comment TEXT NOT NULL DEFAULT '',
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
FOREIGN KEY (user_id) REFERENCES app_users(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE,
|
|
UNIQUE (server_id, interface_name, peer_public_key)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_user_iface_bind_user
|
|
ON user_interface_bindings(user_id);
|
|
`)
|
|
|
|
// Lightweight schema evolution for existing databases without migrations
|
|
{
|
|
const sampleCols = sqlite.prepare(`PRAGMA table_info('traffic_samples')`).all() as Array<{ name?: string }>
|
|
if (!sampleCols.some((c) => c.name === "peer_public_key")) {
|
|
sqlite.exec(`ALTER TABLE traffic_samples ADD COLUMN peer_public_key TEXT NOT NULL DEFAULT ''`)
|
|
}
|
|
}
|
|
|
|
{
|
|
const bindCols = sqlite.prepare(`PRAGMA table_info('user_interface_bindings')`).all() as Array<{ name?: string }>
|
|
if (!bindCols.some((c) => c.name === "peer_public_key")) {
|
|
sqlite.exec(`ALTER TABLE user_interface_bindings ADD COLUMN peer_public_key TEXT NOT NULL DEFAULT ''`)
|
|
}
|
|
if (!bindCols.some((c) => c.name === "peer_name")) {
|
|
sqlite.exec(`ALTER TABLE user_interface_bindings ADD COLUMN peer_name TEXT NOT NULL DEFAULT ''`)
|
|
}
|
|
|
|
const indexes = sqlite.prepare(`PRAGMA index_list('user_interface_bindings')`).all() as Array<{
|
|
name?: string
|
|
unique?: number
|
|
}>
|
|
let hasPeerUnique = false
|
|
for (const idx of indexes) {
|
|
if (!idx.name || !idx.unique) continue
|
|
const info = sqlite.prepare(`PRAGMA index_info(${JSON.stringify(idx.name)})`).all() as Array<{ name?: string }>
|
|
const names = info.map((c) => c.name)
|
|
if (names.includes("server_id") && names.includes("interface_name") && names.includes("peer_public_key")) {
|
|
hasPeerUnique = true
|
|
}
|
|
}
|
|
if (!hasPeerUnique) {
|
|
sqlite.exec(`PRAGMA foreign_keys = OFF`)
|
|
sqlite.exec(`
|
|
CREATE TABLE user_interface_bindings_new (
|
|
id TEXT PRIMARY KEY,
|
|
user_id TEXT NOT NULL,
|
|
server_id INTEGER NOT NULL,
|
|
interface_name TEXT NOT NULL,
|
|
interface_type TEXT NOT NULL DEFAULT 'other',
|
|
peer_public_key TEXT NOT NULL DEFAULT '',
|
|
peer_name TEXT NOT NULL DEFAULT '',
|
|
comment TEXT NOT NULL DEFAULT '',
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
FOREIGN KEY (user_id) REFERENCES app_users(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE,
|
|
UNIQUE (server_id, interface_name, peer_public_key)
|
|
);
|
|
INSERT INTO user_interface_bindings_new
|
|
(id, user_id, server_id, interface_name, interface_type, peer_public_key, peer_name, comment, created_at, updated_at)
|
|
SELECT id, user_id, server_id, interface_name, interface_type,
|
|
COALESCE(peer_public_key, ''), COALESCE(peer_name, ''), comment, created_at, updated_at
|
|
FROM user_interface_bindings;
|
|
DROP TABLE user_interface_bindings;
|
|
ALTER TABLE user_interface_bindings_new RENAME TO user_interface_bindings;
|
|
CREATE INDEX IF NOT EXISTS idx_user_iface_bind_user ON user_interface_bindings(user_id);
|
|
`)
|
|
sqlite.exec(`PRAGMA foreign_keys = ON`)
|
|
}
|
|
}
|
|
|
|
const recursiveCols = sqlite.prepare(`PRAGMA table_info('recursive_routes')`).all() as Array<{ name?: string }>
|
|
const hasCountryColumn = recursiveCols.some((c) => c.name === "country")
|
|
if (!hasCountryColumn) {
|
|
sqlite.exec(`ALTER TABLE recursive_routes ADD COLUMN country TEXT NOT NULL DEFAULT ''`)
|
|
}
|
|
|
|
const uptimeProbeCols = sqlite.prepare(`PRAGMA table_info('uptime_probes')`).all() as Array<{ name?: string }>
|
|
const hasSrcInterfaceColumn = uptimeProbeCols.some((c) => c.name === "src_interface")
|
|
if (!hasSrcInterfaceColumn) {
|
|
sqlite.exec(`ALTER TABLE uptime_probes ADD COLUMN src_interface TEXT NOT NULL DEFAULT ''`)
|
|
}
|
|
const hasShowOnDashboardColumn = uptimeProbeCols.some((c) => c.name === "show_on_dashboard")
|
|
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")
|
|
if (!hasProbeIntervalColumn) {
|
|
sqlite.exec(`ALTER TABLE uptime_settings ADD COLUMN probe_interval_sec INTEGER NOT NULL DEFAULT 15`)
|
|
}
|
|
const hasSpeedIntervalColumn = uptimeSettingsCols.some((c) => c.name === "speed_interval_sec")
|
|
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)
|
|
}
|
|
ensureSpeedProbeCol("last_run_at", `ALTER TABLE uptime_speed_probes ADD COLUMN last_run_at TEXT`)
|
|
ensureSpeedProbeCol("last_tx_avg_mbps", `ALTER TABLE uptime_speed_probes ADD COLUMN last_tx_avg_mbps REAL`)
|
|
ensureSpeedProbeCol("last_rx_avg_mbps", `ALTER TABLE uptime_speed_probes ADD COLUMN last_rx_avg_mbps REAL`)
|
|
ensureSpeedProbeCol("last_status", `ALTER TABLE uptime_speed_probes ADD COLUMN last_status TEXT`)
|
|
ensureSpeedProbeCol("last_error", `ALTER TABLE uptime_speed_probes ADD COLUMN last_error TEXT`)
|
|
ensureSpeedProbeCol("last_ping_rtt_ms", `ALTER TABLE uptime_speed_probes ADD COLUMN last_ping_rtt_ms INTEGER`)
|
|
ensureSpeedProbeCol("last_ping_loss_pct", `ALTER TABLE uptime_speed_probes ADD COLUMN last_ping_loss_pct INTEGER`)
|
|
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 ''`)
|
|
}
|
|
if (!serverCols.some((c) => c.name === "wan_uplinks")) {
|
|
sqlite.exec(`ALTER TABLE servers ADD COLUMN wan_uplinks TEXT NOT NULL DEFAULT '[]'`)
|
|
}
|
|
if (!serverCols.some((c) => c.name === "mgmt_tunnel_ip")) {
|
|
sqlite.exec(`ALTER TABLE servers ADD COLUMN mgmt_tunnel_ip 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
|
|
WHERE NOT EXISTS (SELECT 1 FROM traffic_settings WHERE id = 1);
|
|
`)
|
|
|
|
sqlite.exec(`
|
|
INSERT INTO traffic_flow_settings (id, enabled, collector_ip, flow_listen_port, wg_listen_port, prefix)
|
|
SELECT 1, 0, '10.255.254.1', 4739, 51821, '10.255.254.0/24'
|
|
WHERE NOT EXISTS (SELECT 1 FROM traffic_flow_settings WHERE id = 1);
|
|
`)
|
|
|
|
sqlite.exec(`
|
|
INSERT INTO uptime_settings (id, enabled, interval_sec, retention_days)
|
|
SELECT 1, 1, 15, 14
|
|
WHERE NOT EXISTS (SELECT 1 FROM uptime_settings WHERE id = 1);
|
|
`)
|
|
|
|
sqlite.exec(`
|
|
INSERT INTO evobgp_settings (id, base_url, api_key, enabled)
|
|
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 internet_path_settings (id, enabled, interval_sec, retention_days)
|
|
SELECT 1, 1, 300, 14
|
|
WHERE NOT EXISTS (SELECT 1 FROM internet_path_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 acme_settings (id, directory_url, cloudflare_api_token, default_zone_id, account_private_key)
|
|
SELECT 1, 'https://acme-v02.api.letsencrypt.org/directory', '', '', ''
|
|
WHERE NOT EXISTS (SELECT 1 FROM acme_settings WHERE id = 1);
|
|
`)
|
|
|
|
const certIssueJobCols = sqlite.prepare(`PRAGMA table_info('certificate_issue_jobs')`).all() as Array<{ name?: string }>
|
|
if (!certIssueJobCols.some((c) => c.name === "source")) {
|
|
sqlite.exec(`ALTER TABLE certificate_issue_jobs ADD COLUMN source TEXT NOT NULL DEFAULT 'manual'`)
|
|
}
|
|
|
|
sqlite.exec(`
|
|
INSERT INTO certificate_renew_settings (id, enabled, interval_sec, renew_before_days)
|
|
SELECT 1, 1, 21600, 30
|
|
WHERE NOT EXISTS (SELECT 1 FROM certificate_renew_settings WHERE id = 1);
|
|
`)
|
|
|
|
sqlite.exec(`
|
|
INSERT INTO backup_schedule_settings (id, enabled, frequency, hour, minute, week_day, month_day, keep_count, format, server_ids_json)
|
|
SELECT 1, 1, 'daily', 3, 0, 0, 1, 7, 'rsc', '[]'
|
|
WHERE NOT EXISTS (SELECT 1 FROM backup_schedule_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);
|
|
`)
|
|
|
|
const backupEntryCount = sqlite.prepare(`SELECT COUNT(*) AS c FROM backup_entries`).get() as { c: number }
|
|
if (backupEntryCount.c === 0) {
|
|
const legacyIndexPath = path.resolve(process.cwd(), "storage", "backups", "index.json")
|
|
if (existsSync(legacyIndexPath)) {
|
|
try {
|
|
const parsed = JSON.parse(readFileSync(legacyIndexPath, "utf8")) as unknown
|
|
if (Array.isArray(parsed)) {
|
|
const insert = sqlite.prepare(`
|
|
INSERT OR IGNORE INTO backup_entries (id, server_id, server_name, filename, size_bytes, kind, notes, created_at)
|
|
VALUES (@id, @serverId, @serverName, @filename, @sizeBytes, @kind, @notes, @createdAt)
|
|
`)
|
|
for (const row of parsed) {
|
|
if (!row || typeof row !== "object") continue
|
|
const item = row as Record<string, unknown>
|
|
const id = String(item.id ?? "").trim()
|
|
const filename = String(item.filename ?? "").trim()
|
|
if (!id || !filename) continue
|
|
insert.run({
|
|
id,
|
|
serverId: String(item.serverId ?? ""),
|
|
serverName: String(item.serverName ?? ""),
|
|
filename,
|
|
sizeBytes: Number(item.sizeBytes ?? 0) || 0,
|
|
kind: item.kind === "auto" ? "auto" : "manual",
|
|
notes: item.notes == null ? null : String(item.notes),
|
|
createdAt: String(item.createdAt ?? new Date().toISOString()),
|
|
})
|
|
}
|
|
}
|
|
} catch {
|
|
/* legacy index.json не читается — пропускаем */
|
|
}
|
|
}
|
|
}
|
|
|
|
export const db = drizzle(sqlite, { schema })
|
|
|
|
/** Прямой доступ к better-sqlite3 для сложных read-only запросов (напр. /api/alerts). */
|
|
export const sqliteDatabase: SqliteHandle = sqlite
|