refactor(repo): переход на pnpm monorepo с shadcn/ui и Fastify+Drizzle

Frontend:
- apps/web (Vite+TS, TanStack Router/Query, shadcn/ui @cfdm/ui base-nova)
- 10 страниц в routes/_auth/, Recharts через shadcn Chart, lucide-react
- формы на RHF + Zod (FormSheet/FormField)
- удалены Tabler, Chart.js, react-router-dom

Backend (параллельный трек):
- apps/api (Fastify 5 + Drizzle + better-sqlite3)
- packages/db: Drizzle-схема и repositories по сущностям
- packages/shared: Zod-контракты
- роуты с валидацией и единым форматом ошибок { error: { code, message } }
- sync/backup — заглушки 501 (billmanager-адаптеры переносятся отдельно)
- legacy Express оставлен как runtime по умолчанию (RUNTIME=express)

Infra:
- Dockerfile multi-stage под pnpm workspaces
- .dockerignore и docker-compose обновлены под monorepo

Rules:
- удалены нерелевантные правила (rust, cloudflare, server/frontend-conventions)
- project-structure.mdc и AGENTS.md переписаны под monorepo
- frontend-shadcn.mdc, shadcn-ui-production.mdc, sqlite.mdc обновлены

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-06-26 13:42:05 +07:00
co-authored by Cursor
parent 8408155be6
commit 6fbd1a9113
202 changed files with 16734 additions and 12145 deletions
+139
View File
@@ -0,0 +1,139 @@
/**
* Database initialization and access
*/
import initSqlJs from 'sql.js'
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs'
import { join, dirname } from 'path'
import { fileURLToPath } from 'url'
import { SCHEMA } from './schema.js'
import { MIGRATIONS } from './migrations.js'
import { seed, isDbEmpty } from './seed.js'
const __dirname = dirname(fileURLToPath(import.meta.url))
// db/ is in server/, so .. = server, .. again = project root
export const DB_PATH = join(__dirname, '..', '..', 'data', 'vps-tracker.db')
const SEED_DIR = join(__dirname, '..', '..', 'public', 'data')
let dbInstance = null
/** @type {import('sql.js').SqlJsStatic | null} */
let sqlJsFactory = null
export async function initDb() {
const dataDir = join(__dirname, '..', '..', 'data')
if (!existsSync(dataDir)) {
mkdirSync(dataDir, { recursive: true })
}
const SQL = await initSqlJs()
sqlJsFactory = SQL
let db
if (existsSync(DB_PATH)) {
const fileBuffer = readFileSync(DB_PATH)
db = new SQL.Database(fileBuffer)
} else {
db = new SQL.Database()
}
db.exec(SCHEMA)
for (const m of MIGRATIONS) {
try {
m.run(db)
} catch (err) {
console.warn(`Migration ${m.name} failed:`, err.message)
}
}
if (isDbEmpty(db)) {
seed(db, SEED_DIR)
const data = db.export()
writeFileSync(DB_PATH, Buffer.from(data))
}
dbInstance = db
if (existsSync(DB_PATH)) {
saveDb()
}
return db
}
export function getDb() {
if (!dbInstance) throw new Error('Database not initialized')
return createDbWrapper(dbInstance)
}
function createDbWrapper(db) {
return {
/** One-off run: prepare, bind, step, free. Use for statements that run once. */
run(sql, ...params) {
const stmt = db.prepare(sql)
stmt.bind(params)
stmt.step()
const changes = db.getRowsModified()
stmt.free()
saveDb()
return { changes }
},
prepare(sql) {
const stmt = db.prepare(sql)
return {
all(...params) {
stmt.bind(params)
const rows = []
while (stmt.step()) {
rows.push(stmt.getAsObject())
}
stmt.free()
return rows
},
get(...params) {
stmt.bind(params)
const row = stmt.step() ? stmt.getAsObject() : null
stmt.free()
return row
},
run(...params) {
stmt.bind(params)
stmt.step()
const changes = db.getRowsModified()
stmt.free()
saveDb()
return { changes }
},
}
},
}
}
export function saveDb() {
if (!dbInstance) return
const data = dbInstance.export()
writeFileSync(DB_PATH, Buffer.from(data))
}
/**
* Заменить in-memory БД из буфера SQLite и сохранить на диск.
* @param {Buffer|Uint8Array} buffer
*/
export async function reloadDatabaseFromBuffer(buffer) {
const SQL = sqlJsFactory || (await initSqlJs())
sqlJsFactory = SQL
if (dbInstance) {
dbInstance.close()
dbInstance = null
}
const u8 = buffer instanceof Buffer ? new Uint8Array(buffer) : buffer
dbInstance = new SQL.Database(u8)
dbInstance.exec(SCHEMA)
for (const m of MIGRATIONS) {
try {
m.run(dbInstance)
} catch (err) {
console.warn(`Migration ${m.name} after reload failed:`, err.message)
}
}
saveDb()
}
+388
View File
@@ -0,0 +1,388 @@
/**
* Database migrations — add columns to existing tables
*/
import { randomUUID } from 'node:crypto'
/**
* Перенос apiType/apiBaseUrl с аккаунтов на хостера (idempotent).
* Вызывается из миграции и после импорта бэкапа / старого migrate API.
* @param {import('sql.js').Database} db
*/
function selectAllObjects(db, sql, params = []) {
const prepared = db.prepare(sql)
if (typeof prepared.all === 'function') {
return prepared.all(...params)
}
const stmt = prepared
stmt.bind(params)
const rows = []
while (stmt.step()) {
rows.push(stmt.getAsObject())
}
stmt.free()
return rows
}
export function consolidateProviderApiFromAccounts(db) {
const provRows = selectAllObjects(db, 'SELECT id, apiType, apiBaseUrl FROM providers')
for (const prov of provRows) {
const pid = prov.id
if (String(prov.apiType || '').trim() || String(prov.apiBaseUrl || '').trim()) {
continue
}
const accRows = selectAllObjects(
db,
`SELECT apiBaseUrl FROM provider_accounts
WHERE providerId = ?
AND length(trim(COALESCE(apiBaseUrl, ''))) > 0
AND (
lower(trim(COALESCE(apiType, ''))) = 'billmanager'
OR instr(lower(trim(COALESCE(apiBaseUrl, ''))), 'billmgr') > 0
)
ORDER BY id`,
[pid],
)
if (!accRows.length) continue
const urls = [...new Set(accRows.map((r) => String(r.apiBaseUrl || '').trim()).filter(Boolean))]
if (urls.length > 1) {
console.warn(
`[vps-tracker] У хостера ${pid} у нескольких аккаунтов разный URL BILLmanager — в настройках хостера взят первый.`,
)
}
const apiBaseUrl = urls[0]
db.run(`UPDATE providers SET apiType = ?, apiBaseUrl = ? WHERE id = ?`, 'billmanager', apiBaseUrl, pid)
db.run(
`UPDATE provider_accounts SET apiType = '', apiBaseUrl = ''
WHERE providerId = ?
AND length(trim(COALESCE(apiBaseUrl, ''))) > 0
AND (
lower(trim(COALESCE(apiType, ''))) = 'billmanager'
OR instr(lower(trim(COALESCE(apiBaseUrl, ''))), 'billmgr') > 0
)`,
pid,
)
}
}
/**
* Раньше URL BILLmanager часто указывали в «Сайт» хостера или в panelUrl аккаунта, без apiType/apiBaseUrl.
*/
export function heuristicBillmanagerProviderApi(db) {
const tryBillmgrUrl = (raw) => {
const t = String(raw || '').trim()
if (!t) return ''
if (!/^https?:\/\//i.test(t)) return ''
if (!/billmgr/i.test(t)) return ''
return t.replace(/\/+$/, '')
}
const provRows = selectAllObjects(db, 'SELECT id, website, apiType, apiBaseUrl FROM providers')
for (const prov of provRows) {
if (String(prov.apiType || '').trim() || String(prov.apiBaseUrl || '').trim()) continue
let url = tryBillmgrUrl(prov.website)
if (!url) {
const accRows = selectAllObjects(
db,
`SELECT panelUrl FROM provider_accounts
WHERE providerId = ? AND length(trim(COALESCE(panelUrl, ''))) > 0
ORDER BY id`,
[prov.id],
)
for (const row of accRows) {
url = tryBillmgrUrl(row.panelUrl)
if (url) break
}
}
if (url) {
db.run(`UPDATE providers SET apiType = ?, apiBaseUrl = ? WHERE id = ?`, 'billmanager', url, prov.id)
}
}
}
/** Перенос API на хостера: с аккаунтов + эвристика по website/panelUrl. */
export function consolidateAllProviderApiSources(db) {
consolidateProviderApiFromAccounts(db)
heuristicBillmanagerProviderApi(db)
}
export const MIGRATIONS = [
{
name: 'provider_accounts_api',
run(db) {
try {
db.exec('ALTER TABLE provider_accounts ADD COLUMN apiType TEXT')
} catch (e) {
if (!e.message?.includes('duplicate column')) throw e
}
try {
db.exec('ALTER TABLE provider_accounts ADD COLUMN apiBaseUrl TEXT')
} catch (e) {
if (!e.message?.includes('duplicate column')) throw e
}
try {
db.exec('ALTER TABLE provider_accounts ADD COLUMN apiCredentials TEXT')
} catch (e) {
if (!e.message?.includes('duplicate column')) throw e
}
},
},
{
name: 'settings_sync',
run(db) {
try {
db.exec('ALTER TABLE settings ADD COLUMN syncEnabled INTEGER')
} catch (e) {
if (!e.message?.includes('duplicate column')) throw e
}
try {
db.exec('ALTER TABLE settings ADD COLUMN syncIntervalMinutes INTEGER')
} catch (e) {
if (!e.message?.includes('duplicate column')) throw e
}
},
},
{
name: 'vps_paidUntil',
run(db) {
try {
db.exec('ALTER TABLE vps ADD COLUMN paidUntil TEXT')
} catch (e) {
if (!e.message?.includes('duplicate column')) throw e
}
},
},
{
name: 'provider_accounts_balance_api',
run(db) {
try {
db.exec('ALTER TABLE provider_accounts ADD COLUMN balance_api REAL')
} catch (e) {
if (!e.message?.includes('duplicate column')) throw e
}
try {
db.exec('ALTER TABLE provider_accounts ADD COLUMN balance_currency TEXT')
} catch (e) {
if (!e.message?.includes('duplicate column')) throw e
}
try {
db.exec('ALTER TABLE provider_accounts ADD COLUMN balance_updated_at TEXT')
} catch (e) {
if (!e.message?.includes('duplicate column')) throw e
}
try {
db.exec('ALTER TABLE provider_accounts ADD COLUMN enoughmoneyto TEXT')
} catch (e) {
if (!e.message?.includes('duplicate column')) throw e
}
},
},
{
name: 'vps_userOverrides',
run(db) {
try {
db.exec('ALTER TABLE vps ADD COLUMN userOverrides TEXT')
} catch (e) {
if (!e.message?.includes('duplicate column')) throw e
}
},
},
{
name: 'active_tariffs_location_cpu',
run(db) {
try {
db.exec('ALTER TABLE active_tariffs ADD COLUMN location TEXT')
} catch (e) {
if (!e.message?.includes('duplicate column')) throw e
}
try {
db.exec('ALTER TABLE active_tariffs ADD COLUMN cpuModel TEXT')
} catch (e) {
if (!e.message?.includes('duplicate column')) throw e
}
},
},
{
name: 'settings_customFields',
run(db) {
try {
db.exec('ALTER TABLE settings ADD COLUMN customFields TEXT')
} catch (e) {
if (!e.message?.includes('duplicate column')) throw e
}
},
},
{
name: 'settings_syncTariffsInterval',
run(db) {
try {
db.exec('ALTER TABLE settings ADD COLUMN syncTariffsIntervalMinutes INTEGER')
} catch (e) {
if (!e.message?.includes('duplicate column')) throw e
}
},
},
{
name: 'active_tariffs_country_datacenter',
run(db) {
try {
db.exec('ALTER TABLE active_tariffs ADD COLUMN country TEXT')
} catch (e) {
if (!e.message?.includes('duplicate column')) throw e
}
try {
db.exec('ALTER TABLE active_tariffs ADD COLUMN datacenterKey TEXT')
} catch (e) {
if (!e.message?.includes('duplicate column')) throw e
}
try {
db.exec('ALTER TABLE active_tariffs ADD COLUMN datacenterName TEXT')
} catch (e) {
if (!e.message?.includes('duplicate column')) throw e
}
},
},
{
name: 'settings_telegram',
run(db) {
try {
db.exec('ALTER TABLE settings ADD COLUMN telegramBotToken TEXT')
} catch (e) {
if (!e.message?.includes('duplicate column')) throw e
}
try {
db.exec('ALTER TABLE settings ADD COLUMN telegramChatId TEXT')
} catch (e) {
if (!e.message?.includes('duplicate column')) throw e
}
try {
db.exec('ALTER TABLE settings ADD COLUMN notifyPaymentExpiryEnabled INTEGER')
} catch (e) {
if (!e.message?.includes('duplicate column')) throw e
}
try {
db.exec('ALTER TABLE settings ADD COLUMN notifyNewTariffsEnabled INTEGER')
} catch (e) {
if (!e.message?.includes('duplicate column')) throw e
}
},
},
{
name: 'settings_telegram_thread',
run(db) {
try {
db.exec('ALTER TABLE settings ADD COLUMN telegramMessageThreadId TEXT')
} catch (e) {
if (!e.message?.includes('duplicate column')) throw e
}
},
},
{
name: 'server_projects',
run(db) {
db.run(
`CREATE TABLE IF NOT EXISTS server_projects (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
color TEXT,
sortOrder INTEGER DEFAULT 0,
notes TEXT,
createdAt TEXT
)`,
)
try {
db.run('ALTER TABLE vps ADD COLUMN projectId TEXT')
} catch (e) {
if (!String(e.message || e).includes('duplicate column')) throw e
}
const distinctStmt = db.prepare(
`SELECT DISTINCT trim(project) AS n FROM vps WHERE length(trim(COALESCE(project, ''))) > 0`,
)
const seenLower = new Set()
const findStmt = db.prepare(
'SELECT id FROM server_projects WHERE LOWER(name) = LOWER(?) LIMIT 1',
)
while (distinctStmt.step()) {
const row = distinctStmt.getAsObject()
const t = String(row.n ?? '').trim()
if (!t) continue
const lk = t.toLowerCase()
if (seenLower.has(lk)) continue
seenLower.add(lk)
findStmt.bind([t])
const exists = Boolean(findStmt.step())
findStmt.reset()
if (!exists) {
const id = `proj-${randomUUID()}`
const now = new Date().toISOString()
db.run(
`INSERT INTO server_projects (id, name, color, sortOrder, notes, createdAt) VALUES (?, ?, NULL, 0, NULL, ?)`,
[id, t, now],
)
}
}
distinctStmt.free()
findStmt.free()
db.run(`UPDATE vps SET projectId = (
SELECT sp.id FROM server_projects sp
WHERE LOWER(sp.name) = LOWER(trim(COALESCE(vps.project, '')))
LIMIT 1
) WHERE length(trim(COALESCE(vps.project, ''))) > 0`)
},
},
{
name: 'sync_log_summary',
run(db) {
try {
db.exec('ALTER TABLE sync_log ADD COLUMN summary TEXT')
} catch (e) {
if (!String(e.message || e).includes('duplicate column')) throw e
}
},
},
{
name: 'settings_notify_balance_digest',
run(db) {
try {
db.exec('ALTER TABLE settings ADD COLUMN notifyLowBalanceEnabled INTEGER')
} catch (e) {
if (!String(e.message || e).includes('duplicate column')) throw e
}
try {
db.exec('ALTER TABLE settings ADD COLUMN notifySyncDigestEnabled INTEGER')
} catch (e) {
if (!String(e.message || e).includes('duplicate column')) throw e
}
},
},
{
name: 'provider_accounts_balance_alert_below',
run(db) {
try {
db.exec('ALTER TABLE provider_accounts ADD COLUMN balance_alert_below REAL')
} catch (e) {
if (!String(e.message || e).includes('duplicate column')) throw e
}
},
},
{
name: 'providers_api_integration',
run(db) {
try {
db.exec('ALTER TABLE providers ADD COLUMN apiType TEXT')
} catch (e) {
if (!String(e.message || e).includes('duplicate column')) throw e
}
try {
db.exec('ALTER TABLE providers ADD COLUMN apiBaseUrl TEXT')
} catch (e) {
if (!String(e.message || e).includes('duplicate column')) throw e
}
consolidateAllProviderApiSources(db)
},
},
]
+165
View File
@@ -0,0 +1,165 @@
/**
* SQLite schema for vps-tracker
*/
export const SCHEMA = `
CREATE TABLE IF NOT EXISTS providers (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
website TEXT,
contact TEXT,
baseCurrency TEXT,
usdRate TEXT,
eurRate TEXT,
notes TEXT,
apiType TEXT,
apiBaseUrl TEXT
);
CREATE TABLE IF NOT EXISTS provider_accounts (
id TEXT PRIMARY KEY,
providerId TEXT NOT NULL,
name TEXT NOT NULL,
panelUrl TEXT,
currency TEXT,
billingMode TEXT,
notes TEXT,
apiType TEXT,
apiBaseUrl TEXT,
apiCredentials TEXT,
FOREIGN KEY (providerId) REFERENCES providers(id)
);
CREATE TABLE IF NOT EXISTS server_projects (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
color TEXT,
sortOrder INTEGER DEFAULT 0,
notes TEXT,
createdAt TEXT
);
CREATE TABLE IF NOT EXISTS vps (
id TEXT PRIMARY KEY,
ip TEXT,
ipv6 TEXT,
additionalIps TEXT,
dns TEXT,
providerId TEXT,
providerAccountId TEXT,
country TEXT,
city TEXT,
datacenter TEXT,
os TEXT,
vcpu INTEGER,
ramGb INTEGER,
diskGb INTEGER,
diskType TEXT,
virtualization TEXT,
bandwidthTb INTEGER,
sshPort INTEGER,
rootUser TEXT,
purpose TEXT,
environment TEXT,
project TEXT,
projectId TEXT,
monitoringEnabled INTEGER,
backupEnabled INTEGER,
status TEXT,
tariffType TEXT,
currency TEXT,
dailyRate REAL,
monthlyRate REAL,
createdAt TEXT,
paidUntil TEXT,
notes TEXT,
userOverrides TEXT,
FOREIGN KEY (providerId) REFERENCES providers(id),
FOREIGN KEY (providerAccountId) REFERENCES provider_accounts(id),
FOREIGN KEY (projectId) REFERENCES server_projects(id)
);
CREATE TABLE IF NOT EXISTS payments (
id TEXT PRIMARY KEY,
type TEXT NOT NULL,
date TEXT NOT NULL,
amount REAL NOT NULL,
currency TEXT,
providerAccountId TEXT,
vpsId TEXT,
note TEXT,
FOREIGN KEY (providerAccountId) REFERENCES provider_accounts(id),
FOREIGN KEY (vpsId) REFERENCES vps(id)
);
CREATE TABLE IF NOT EXISTS balance_ledger (
id TEXT PRIMARY KEY,
type TEXT NOT NULL,
date TEXT NOT NULL,
amount REAL NOT NULL,
currency TEXT,
direction TEXT,
providerAccountId TEXT,
vpsId TEXT,
note TEXT,
FOREIGN KEY (providerAccountId) REFERENCES provider_accounts(id),
FOREIGN KEY (vpsId) REFERENCES vps(id)
);
CREATE TABLE IF NOT EXISTS settings (
id TEXT PRIMARY KEY,
baseCurrency TEXT,
ratesUrl TEXT,
autoConvert INTEGER,
ratesUpdatedAt TEXT,
syncEnabled INTEGER,
syncIntervalMinutes INTEGER,
syncTariffsIntervalMinutes INTEGER,
customFields TEXT
);
CREATE TABLE IF NOT EXISTS sync_log (
id TEXT PRIMARY KEY,
accountId TEXT NOT NULL,
startedAt TEXT NOT NULL,
finishedAt TEXT,
status TEXT,
vpsCount INTEGER,
paymentsCount INTEGER,
error TEXT,
FOREIGN KEY (accountId) REFERENCES provider_accounts(id)
);
CREATE TABLE IF NOT EXISTS active_tariffs (
id TEXT PRIMARY KEY,
providerAccountId TEXT NOT NULL,
providerId TEXT NOT NULL,
externalId TEXT NOT NULL,
datacenterKey TEXT,
datacenterName TEXT,
name TEXT,
desc TEXT,
vcpu INTEGER,
ramGb REAL,
diskGb INTEGER,
diskType TEXT,
virtualization TEXT,
channel TEXT,
location TEXT,
country TEXT,
cpuModel TEXT,
orderAvailable INTEGER,
price TEXT,
syncedAt TEXT,
FOREIGN KEY (providerAccountId) REFERENCES provider_accounts(id),
FOREIGN KEY (providerId) REFERENCES providers(id)
);
CREATE TABLE IF NOT EXISTS tariff_sync_options (
providerAccountId TEXT PRIMARY KEY,
datacenters TEXT,
periods TEXT,
syncedAt TEXT,
FOREIGN KEY (providerAccountId) REFERENCES provider_accounts(id)
);
`
+146
View File
@@ -0,0 +1,146 @@
/**
* Seed database with initial data from public/data/*.json
*/
import { readFileSync, existsSync } from 'fs'
import { join } from 'path'
/**
* @param {string} path - path to JSON file
* @returns {Array}
*/
function loadJson(path) {
if (!existsSync(path)) return []
const raw = readFileSync(path, 'utf-8')
try {
const data = JSON.parse(raw)
return Array.isArray(data) ? data : [data]
} catch {
return []
}
}
/**
* @param {object} db - raw sql.js database (not wrapper)
* @param {string} seedDir - path to public/data
*/
export function seed(db, seedDir) {
const providers = loadJson(join(seedDir, 'providers.json'))
if (providers.length === 0) return
const run = db.run.bind(db)
for (const r of providers) {
run(
'INSERT OR IGNORE INTO providers (id, name, website, contact, baseCurrency, usdRate, eurRate, notes, apiType, apiBaseUrl) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
[
r.id,
r.name ?? '',
r.website ?? '',
r.contact ?? '',
r.baseCurrency ?? '',
r.usdRate ?? '',
r.eurRate ?? '',
r.notes ?? '',
r.apiType ?? '',
r.apiBaseUrl ?? '',
],
)
}
const providerAccounts = loadJson(join(seedDir, 'provider-accounts.json'))
for (const r of providerAccounts) {
const legacyType = r.apiType ?? ''
const legacyUrl = r.apiBaseUrl ?? ''
run(
'INSERT OR IGNORE INTO provider_accounts (id, providerId, name, panelUrl, currency, billingMode, notes, apiType, apiBaseUrl, apiCredentials) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
[r.id, r.providerId ?? '', r.name ?? '', r.panelUrl ?? '', r.currency ?? '', r.billingMode ?? '', r.notes ?? '', '', '', r.apiCredentials ?? ''],
)
if (legacyType === 'billmanager' && String(legacyUrl).trim()) {
run(
`UPDATE providers SET apiType = 'billmanager', apiBaseUrl = ? WHERE id = ? AND length(trim(COALESCE(apiBaseUrl,''))) = 0`,
String(legacyUrl).trim(),
r.providerId ?? '',
)
}
}
const vpsList = loadJson(join(seedDir, 'vps.json'))
for (const r of vpsList) {
const additionalIps = Array.isArray(r.additionalIps) ? JSON.stringify(r.additionalIps) : '[]'
const dailyRate = r.dailyRate === '' || r.dailyRate == null ? null : Number(r.dailyRate)
const monthlyRate = r.monthlyRate === '' || r.monthlyRate == null ? null : Number(r.monthlyRate)
run(
`INSERT OR IGNORE INTO vps (id, ip, ipv6, additionalIps, dns, providerId, providerAccountId, country, city, datacenter, os, vcpu, ramGb, diskGb, diskType, virtualization, bandwidthTb, sshPort, rootUser, purpose, environment, project, projectId, monitoringEnabled, backupEnabled, status, tariffType, currency, dailyRate, monthlyRate, createdAt, paidUntil, notes, userOverrides) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
r.id,
r.ip ?? '',
r.ipv6 ?? '',
additionalIps,
r.dns ?? '',
r.providerId ?? '',
r.providerAccountId ?? '',
r.country ?? '',
r.city ?? '',
r.datacenter ?? '',
r.os ?? '',
r.vcpu ?? 0,
r.ramGb ?? 0,
r.diskGb ?? 0,
r.diskType ?? '',
r.virtualization ?? '',
r.bandwidthTb ?? 0,
r.sshPort ?? 22,
r.rootUser ?? '',
r.purpose ?? '',
r.environment ?? '',
r.project ?? '',
r.projectId ?? null,
r.monitoringEnabled ? 1 : 0,
r.backupEnabled ? 1 : 0,
r.status ?? 'active',
r.tariffType ?? '',
r.currency ?? '',
dailyRate,
monthlyRate,
r.createdAt ?? '',
r.paidUntil ?? '',
r.notes ?? '',
'[]',
],
)
}
const payments = loadJson(join(seedDir, 'payments.json'))
for (const r of payments) {
run(
'INSERT OR IGNORE INTO payments (id, type, date, amount, currency, providerAccountId, vpsId, note) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
[r.id, r.type ?? '', r.date ?? '', Number(r.amount) || 0, r.currency ?? '', r.providerAccountId ?? '', r.vpsId ?? '', r.note ?? ''],
)
}
const ledger = loadJson(join(seedDir, 'balance-ledger.json'))
for (const r of ledger) {
run(
'INSERT OR IGNORE INTO balance_ledger (id, type, date, amount, currency, direction, providerAccountId, vpsId, note) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
[r.id, r.type ?? '', r.date ?? '', Number(r.amount) || 0, r.currency ?? '', r.direction ?? '', r.providerAccountId ?? '', r.vpsId ?? '', r.note ?? ''],
)
}
const settingsList = loadJson(join(seedDir, 'settings.json'))
for (const r of settingsList) {
run(
'INSERT OR IGNORE INTO settings (id, baseCurrency, ratesUrl, autoConvert, ratesUpdatedAt, syncEnabled, syncIntervalMinutes) VALUES (?, ?, ?, ?, ?, ?, ?)',
[r.id ?? 'settings-main', r.baseCurrency ?? 'RUB', r.ratesUrl ?? '', r.autoConvert !== false ? 1 : 0, r.ratesUpdatedAt ?? '', r.syncEnabled ? 1 : 0, r.syncIntervalMinutes ?? 60],
)
}
}
/**
* @param {object} db - raw sql.js database
* @returns {boolean}
*/
export function isDbEmpty(db) {
const result = db.exec('SELECT COUNT(*) as c FROM providers')
if (!result.length || !result[0].values.length) return true
return result[0].values[0][0] === 0
}