diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 650d28f..acc8d9f 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -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 }) diff --git a/deploy/docker-compose.traefik.yml b/deploy/docker-compose.traefik.yml index 2fd34b3..807d3f8 100644 --- a/deploy/docker-compose.traefik.yml +++ b/deploy/docker-compose.traefik.yml @@ -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: diff --git a/deploy/env.traefik.example b/deploy/env.traefik.example index 35715bb..62dafe6 100644 --- a/deploy/env.traefik.example +++ b/deploy/env.traefik.example @@ -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.: diff --git a/docker-compose.yml b/docker-compose.yml index fbb6441..643fd43 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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" diff --git a/docs/deploy-traefik.md b/docs/deploy-traefik.md index dae1c5a..5d77627 100644 --- a/docs/deploy-traefik.md +++ b/docs/deploy-traefik.md @@ -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 diff --git a/packages/db/src/runtime-migrate.test.ts b/packages/db/src/runtime-migrate.test.ts new file mode 100644 index 0000000..4caf4ad --- /dev/null +++ b/packages/db/src/runtime-migrate.test.ts @@ -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) + }) +}) diff --git a/packages/db/src/runtime-migrate.ts b/packages/db/src/runtime-migrate.ts index 3561073..8c4ca56 100644 --- a/packages/db/src/runtime-migrate.ts +++ b/packages/db/src/runtime-migrate.ts @@ -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) {