fix(db): bootstrap полной схемы на чистой установке и логи Docker
Docker / build (push) Failing after 18s

Пустой volume больше не отдаёт 500 на /api/data из-за отсутствующих таблиц. В compose добавлены json-file логи с ротацией и LOG_LEVEL для Fastify.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-07-19 04:27:42 +07:00
co-authored by Cursor
parent 5060b78875
commit 2931d6ee0f
7 changed files with 321 additions and 45 deletions
+3 -1
View File
@@ -43,7 +43,9 @@ export async function buildApp(opts: BuildAppOptions = {}) {
getDb()
const app = Fastify({
logger: process.env.NODE_ENV !== 'production',
logger: {
level: process.env.LOG_LEVEL ?? 'info',
},
})
await app.register(cors, { origin: true })
+3
View File
@@ -47,6 +47,7 @@ services:
options:
max-size: "10m"
max-file: "3"
tag: "{{.Name}}"
app:
image: ${VPS_TRACKER_IMAGE:-ghcr.io/denozordec/vps-tracker}:${VPS_TRACKER_IMAGE_TAG:-latest}
@@ -60,6 +61,7 @@ services:
PORT: "3001"
RUNTIME: "fastify"
NODE_ENV: production
LOG_LEVEL: ${LOG_LEVEL:-info}
AUTH_REQUIRED: ${AUTH_REQUIRED:-false}
AUTH_JWT_SECRET: ${AUTH_JWT_SECRET:-}
AUTH_ISSUER: ${AUTH_ISSUER:-https://auth.shnt.top}
@@ -93,6 +95,7 @@ services:
options:
max-size: "10m"
max-file: "3"
tag: "{{.Name}}"
volumes:
traefik_letsencrypt:
+1
View File
@@ -9,6 +9,7 @@ VPS_DOMAIN=vps.example.com
# TRAEFIK_HTTP_PORT=80
# TRAEFIK_HTTPS_PORT=443
# TRAEFIK_LOG_LEVEL=INFO
# LOG_LEVEL=info
# --- Image ---
# Default: GHCR. For Gitea registry, e.g.:
+13
View File
@@ -8,12 +8,20 @@ services:
environment:
PORT: "3001"
RUNTIME: "fastify"
NODE_ENV: production
LOG_LEVEL: ${LOG_LEVEL:-info}
AUTH_REQUIRED: ${AUTH_REQUIRED:-false}
AUTH_JWT_SECRET: ${AUTH_JWT_SECRET:-dev-secret-change-me}
AUTH_ISSUER: ${AUTH_ISSUER:-https://auth.shnt.top}
AUTH_PORTAL_URL: ${AUTH_PORTAL_URL:-http://localhost:8080}
volumes:
- ./data:/app/data
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
tag: "{{.Name}}"
# Раскомментируйте для локальной сборки вместо pull образа:
# app-build:
@@ -27,3 +35,8 @@ services:
# RUNTIME: "express"
# volumes:
# - ./data:/app/data
# logging:
# driver: json-file
# options:
# max-size: "10m"
# max-file: "3"
+4
View File
@@ -196,14 +196,18 @@ docker run --rm -v vps_tracker_traefik_letsencrypt:/data -v "$PWD:/backup" alpin
| ACME / браузер ругается на сертификат | `CF_DNS_API_TOKEN`, Zone:DNS:Edit, DNS only (не orange cloud), логи Traefik |
| `invalid credentials` | Не Global Key; зона в scope токена; нет пробелов/кавычек в `.env` |
| Gateway Timeout / 404 | `docker compose ps`; labels; сеть `vps-tracker`; `VPS_DOMAIN` |
| `/health` OK, `/api/data` 500 | Старые образы без bootstrap схемы — `docker compose pull && up -d`; смотрите `docker compose logs app` (`no such table`) |
| `/health` OK, UI пустой | образ / кэш CDN; смотрите `docker compose logs app` |
| SSO 401 / issuer mismatch | `AUTH_JWT_SECRET` = `JWT_SECRET` портала; `AUTH_ISSUER` |
```bash
docker compose logs traefik 2>&1 | grep -iE 'acme|certificate|cloudflare|error'
docker compose logs app --tail=80
docker compose logs -f app # follow
```
Логи контейнеров: драйвер `json-file`, ротация `max-size=10m`, `max-file=3`. Уровень приложения — `LOG_LEVEL` (по умолчанию `info`).
---
## Простой compose без Traefik
+44
View File
@@ -0,0 +1,44 @@
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { closeDb, getDb, getSqlite } from './index.js'
import { resetRuntimeMigrate } from './runtime-migrate.js'
import { getSnapshot } from './repositories/snapshot.js'
import { runWithSpace, MAIN_SPACE_ID } from './space-context.js'
describe('ensureRuntimeSchema (empty DB)', () => {
let dir: string
afterEach(() => {
closeDb()
resetRuntimeMigrate()
delete process.env.DB_PATH
if (dir) rmSync(dir, { recursive: true, force: true })
})
it('bootstraps core tables and serves empty snapshot', () => {
dir = mkdtempSync(join(tmpdir(), 'vps-tracker-db-'))
process.env.DB_PATH = join(dir, 'fresh.db')
resetRuntimeMigrate()
closeDb()
getDb()
const tables = getSqlite()
.prepare(
`SELECT name FROM sqlite_master WHERE type='table' AND name IN ('vps','settings','providers','spaces')`,
)
.all() as { name: string }[]
expect(tables.map((t) => t.name).sort()).toEqual([
'providers',
'settings',
'spaces',
'vps',
])
const snap = runWithSpace(MAIN_SPACE_ID, () => getSnapshot())
expect(snap.spaceId).toBe(MAIN_SPACE_ID)
expect(snap.vps).toEqual([])
expect(snap.settings.length).toBeGreaterThanOrEqual(1)
})
})
+253 -44
View File
@@ -2,41 +2,11 @@ import type Database from 'better-sqlite3'
const MAIN_SPACE_ID = 'space-main'
const COLUMN_MIGRATIONS: string[] = [
`ALTER TABLE vps ADD COLUMN customData TEXT`,
`ALTER TABLE vps ADD COLUMN last_health_status TEXT`,
`ALTER TABLE vps ADD COLUMN last_health_checked_at TEXT`,
`ALTER TABLE settings ADD COLUMN notifyVpsDownEnabled INTEGER`,
`ALTER TABLE settings ADD COLUMN webhookUrl TEXT`,
`ALTER TABLE settings ADD COLUMN webhookEnabled INTEGER`,
`ALTER TABLE settings ADD COLUMN notifyIntervalMinutes INTEGER`,
`ALTER TABLE settings ADD COLUMN uptimeCheckIntervalMinutes INTEGER`,
`ALTER TABLE settings ADD COLUMN appSwitcherJson TEXT`,
`ALTER TABLE settings ADD COLUMN integrationToken TEXT`,
`ALTER TABLE settings ADD COLUMN integrationEnabled INTEGER`,
`ALTER TABLE settings ADD COLUMN integrationLastSyncAt TEXT`,
`ALTER TABLE settings ADD COLUMN cfdmApiUrl TEXT`,
`ALTER TABLE settings ADD COLUMN showQuickActions INTEGER`,
`ALTER TABLE vps_domains ADD COLUMN targetIps TEXT`,
`ALTER TABLE providers ADD COLUMN spaceId TEXT`,
`ALTER TABLE provider_accounts ADD COLUMN spaceId TEXT`,
`ALTER TABLE server_projects ADD COLUMN spaceId TEXT`,
`ALTER TABLE vps ADD COLUMN spaceId TEXT`,
`ALTER TABLE payments ADD COLUMN spaceId TEXT`,
`ALTER TABLE balance_ledger ADD COLUMN spaceId TEXT`,
`ALTER TABLE settings ADD COLUMN spaceId TEXT`,
`ALTER TABLE vps_domains ADD COLUMN spaceId TEXT`,
`ALTER TABLE notification_log ADD COLUMN spaceId TEXT`,
`ALTER TABLE notification_state ADD COLUMN spaceId TEXT`,
`ALTER TABLE vps_health_checks ADD COLUMN spaceId TEXT`,
`ALTER TABLE audit_log ADD COLUMN spaceId TEXT`,
`ALTER TABLE audit_log ADD COLUMN actorUserId TEXT`,
`ALTER TABLE sync_log ADD COLUMN spaceId TEXT`,
`ALTER TABLE active_tariffs ADD COLUMN spaceId TEXT`,
`ALTER TABLE tariff_sync_options ADD COLUMN spaceId TEXT`,
]
const TABLE_MIGRATIONS: string[] = [
/**
* Full bootstrap for empty DB (clean Docker install).
* CREATE IF NOT EXISTS — safe on existing databases; column upgrades stay in COLUMN_MIGRATIONS.
*/
const CORE_TABLE_MIGRATIONS: string[] = [
`CREATE TABLE IF NOT EXISTS spaces (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
@@ -52,8 +22,195 @@ const TABLE_MIGRATIONS: string[] = [
createdAt TEXT NOT NULL,
UNIQUE(spaceId, userId)
)`,
`CREATE TABLE IF NOT EXISTS providers (
id TEXT PRIMARY KEY,
spaceId TEXT NOT NULL DEFAULT 'space-main',
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,
spaceId TEXT NOT NULL DEFAULT 'space-main',
providerId TEXT NOT NULL,
name TEXT NOT NULL,
panelUrl TEXT,
currency TEXT,
billingMode TEXT,
notes TEXT,
apiType TEXT,
apiBaseUrl TEXT,
apiCredentials TEXT,
balance_api REAL,
balance_currency TEXT,
balance_updated_at TEXT,
enoughmoneyto TEXT,
balance_alert_below REAL,
FOREIGN KEY (providerId) REFERENCES providers(id)
)`,
`CREATE TABLE IF NOT EXISTS server_projects (
id TEXT PRIMARY KEY,
spaceId TEXT NOT NULL DEFAULT 'space-main',
name TEXT NOT NULL,
color TEXT,
sortOrder INTEGER DEFAULT 0,
notes TEXT,
createdAt TEXT
)`,
`CREATE TABLE IF NOT EXISTS vps (
id TEXT PRIMARY KEY,
spaceId TEXT NOT NULL DEFAULT 'space-main',
ip TEXT,
ipv6 TEXT,
additionalIps TEXT,
dns TEXT,
providerId TEXT,
providerAccountId TEXT,
country TEXT,
city TEXT,
datacenter TEXT,
os TEXT,
vcpu INTEGER,
ramGb REAL,
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,
customData TEXT,
last_health_status TEXT,
last_health_checked_at TEXT,
FOREIGN KEY (providerId) REFERENCES providers(id),
FOREIGN KEY (providerAccountId) REFERENCES provider_accounts(id)
)`,
`CREATE TABLE IF NOT EXISTS payments (
id TEXT PRIMARY KEY,
spaceId TEXT NOT NULL DEFAULT 'space-main',
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,
spaceId TEXT NOT NULL DEFAULT 'space-main',
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,
spaceId TEXT NOT NULL DEFAULT 'space-main',
baseCurrency TEXT,
ratesUrl TEXT,
autoConvert INTEGER,
ratesUpdatedAt TEXT,
syncEnabled INTEGER,
syncIntervalMinutes INTEGER,
syncTariffsIntervalMinutes INTEGER,
customFields TEXT,
telegramBotToken TEXT,
telegramChatId TEXT,
notifyPaymentExpiryEnabled INTEGER,
notifyNewTariffsEnabled INTEGER,
telegramMessageThreadId TEXT,
notifyLowBalanceEnabled INTEGER,
notifySyncDigestEnabled INTEGER,
notifyVpsDownEnabled INTEGER,
webhookUrl TEXT,
webhookEnabled INTEGER,
notifyIntervalMinutes INTEGER,
uptimeCheckIntervalMinutes INTEGER,
appSwitcherJson TEXT,
integrationToken TEXT,
integrationEnabled INTEGER,
integrationLastSyncAt TEXT,
cfdmApiUrl TEXT,
showQuickActions INTEGER
)`,
`CREATE TABLE IF NOT EXISTS sync_log (
id TEXT PRIMARY KEY,
spaceId TEXT NOT NULL DEFAULT 'space-main',
accountId TEXT NOT NULL,
startedAt TEXT NOT NULL,
finishedAt TEXT,
status TEXT,
vpsCount INTEGER,
paymentsCount INTEGER,
error TEXT,
summary TEXT,
FOREIGN KEY (accountId) REFERENCES provider_accounts(id)
)`,
`CREATE TABLE IF NOT EXISTS active_tariffs (
id TEXT PRIMARY KEY,
spaceId TEXT NOT NULL DEFAULT 'space-main',
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,
spaceId TEXT NOT NULL DEFAULT 'space-main',
datacenters TEXT,
periods TEXT,
syncedAt TEXT,
FOREIGN KEY (providerAccountId) REFERENCES provider_accounts(id)
)`,
`CREATE TABLE IF NOT EXISTS vps_health_checks (
id TEXT PRIMARY KEY,
spaceId TEXT NOT NULL DEFAULT 'space-main',
vpsId TEXT NOT NULL REFERENCES vps(id),
checkedAt TEXT NOT NULL,
status TEXT NOT NULL,
@@ -62,14 +219,17 @@ const TABLE_MIGRATIONS: string[] = [
)`,
`CREATE TABLE IF NOT EXISTS audit_log (
id TEXT PRIMARY KEY,
spaceId TEXT NOT NULL DEFAULT 'space-main',
entity TEXT NOT NULL,
entityId TEXT NOT NULL,
action TEXT NOT NULL,
diff TEXT,
actorUserId TEXT,
createdAt TEXT NOT NULL
)`,
`CREATE TABLE IF NOT EXISTS notification_log (
id TEXT PRIMARY KEY,
spaceId TEXT NOT NULL DEFAULT 'space-main',
event TEXT NOT NULL,
channel TEXT NOT NULL,
status TEXT NOT NULL,
@@ -80,20 +240,14 @@ const TABLE_MIGRATIONS: string[] = [
)`,
`CREATE TABLE IF NOT EXISTS notification_state (
key TEXT PRIMARY KEY,
spaceId TEXT NOT NULL DEFAULT 'space-main',
lastFingerprint TEXT,
lastSentAt TEXT,
lastStatus TEXT
)`,
`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_domains (
id TEXT PRIMARY KEY,
spaceId TEXT NOT NULL DEFAULT 'space-main',
vpsId TEXT REFERENCES vps(id) ON DELETE SET NULL,
fqdn TEXT NOT NULL,
zoneName TEXT NOT NULL,
@@ -128,6 +282,54 @@ const TABLE_MIGRATIONS: string[] = [
)`,
]
/** Additive columns for DBs created before spaces / notifications / etc. */
const COLUMN_MIGRATIONS: string[] = [
`ALTER TABLE vps ADD COLUMN customData TEXT`,
`ALTER TABLE vps ADD COLUMN last_health_status TEXT`,
`ALTER TABLE vps ADD COLUMN last_health_checked_at TEXT`,
`ALTER TABLE settings ADD COLUMN notifyVpsDownEnabled INTEGER`,
`ALTER TABLE settings ADD COLUMN webhookUrl TEXT`,
`ALTER TABLE settings ADD COLUMN webhookEnabled INTEGER`,
`ALTER TABLE settings ADD COLUMN notifyIntervalMinutes INTEGER`,
`ALTER TABLE settings ADD COLUMN uptimeCheckIntervalMinutes INTEGER`,
`ALTER TABLE settings ADD COLUMN appSwitcherJson TEXT`,
`ALTER TABLE settings ADD COLUMN integrationToken TEXT`,
`ALTER TABLE settings ADD COLUMN integrationEnabled INTEGER`,
`ALTER TABLE settings ADD COLUMN integrationLastSyncAt TEXT`,
`ALTER TABLE settings ADD COLUMN cfdmApiUrl TEXT`,
`ALTER TABLE settings ADD COLUMN showQuickActions INTEGER`,
`ALTER TABLE settings ADD COLUMN telegramBotToken TEXT`,
`ALTER TABLE settings ADD COLUMN telegramChatId TEXT`,
`ALTER TABLE settings ADD COLUMN notifyPaymentExpiryEnabled INTEGER`,
`ALTER TABLE settings ADD COLUMN notifyNewTariffsEnabled INTEGER`,
`ALTER TABLE settings ADD COLUMN telegramMessageThreadId TEXT`,
`ALTER TABLE settings ADD COLUMN notifyLowBalanceEnabled INTEGER`,
`ALTER TABLE settings ADD COLUMN notifySyncDigestEnabled INTEGER`,
`ALTER TABLE vps_domains ADD COLUMN targetIps TEXT`,
`ALTER TABLE providers ADD COLUMN spaceId TEXT`,
`ALTER TABLE provider_accounts ADD COLUMN spaceId TEXT`,
`ALTER TABLE provider_accounts ADD COLUMN balance_api REAL`,
`ALTER TABLE provider_accounts ADD COLUMN balance_currency TEXT`,
`ALTER TABLE provider_accounts ADD COLUMN balance_updated_at TEXT`,
`ALTER TABLE provider_accounts ADD COLUMN enoughmoneyto TEXT`,
`ALTER TABLE provider_accounts ADD COLUMN balance_alert_below REAL`,
`ALTER TABLE server_projects ADD COLUMN spaceId TEXT`,
`ALTER TABLE vps ADD COLUMN spaceId TEXT`,
`ALTER TABLE payments ADD COLUMN spaceId TEXT`,
`ALTER TABLE balance_ledger ADD COLUMN spaceId TEXT`,
`ALTER TABLE settings ADD COLUMN spaceId TEXT`,
`ALTER TABLE vps_domains ADD COLUMN spaceId TEXT`,
`ALTER TABLE notification_log ADD COLUMN spaceId TEXT`,
`ALTER TABLE notification_state ADD COLUMN spaceId TEXT`,
`ALTER TABLE vps_health_checks ADD COLUMN spaceId TEXT`,
`ALTER TABLE audit_log ADD COLUMN spaceId TEXT`,
`ALTER TABLE audit_log ADD COLUMN actorUserId TEXT`,
`ALTER TABLE sync_log ADD COLUMN spaceId TEXT`,
`ALTER TABLE sync_log ADD COLUMN summary TEXT`,
`ALTER TABLE active_tariffs ADD COLUMN spaceId TEXT`,
`ALTER TABLE tariff_sync_options ADD COLUMN spaceId TEXT`,
]
const SPACE_BACKFILL_TABLES = [
'providers',
'provider_accounts',
@@ -165,6 +367,13 @@ function ensureMainSpace(sqlite: Database.Database): void {
)
.run(MAIN_SPACE_ID, owner, now)
}
sqlite
.prepare(
`INSERT OR IGNORE INTO settings (id, spaceId, baseCurrency, autoConvert, syncEnabled)
VALUES ('settings-main', ?, 'USD', 1, 0)`,
)
.run(MAIN_SPACE_ID)
}
function backfillSpaceIds(sqlite: Database.Database): void {
@@ -189,7 +398,7 @@ export function resetRuntimeMigrate(): void {
export function ensureRuntimeSchema(sqlite: Database.Database): void {
if (migrated) return
for (const sql of TABLE_MIGRATIONS) {
for (const sql of CORE_TABLE_MIGRATIONS) {
sqlite.exec(sql)
}
for (const sql of COLUMN_MIGRATIONS) {