diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..1926998 --- /dev/null +++ b/.env.example @@ -0,0 +1,15 @@ +# 환경 설정 +# development: 로컬 개발 (스케줄러 수동) +# production: 운영 서버 (스케줄러 자동) +NODE_ENV=development + +# PostgreSQL 연결 정보 +DB_HOST=localhost +DB_PORT=5432 +DB_NAME=osolit_monitor +DB_USER=postgres +DB_PASSWORD=your_password + +# 스케줄러 자동시작 여부 (true/false) +# 생략 시: production=true, development=false +AUTO_START_SCHEDULER=false diff --git a/backend/api/server/scheduler/start.post.ts b/backend/api/server/scheduler/start.post.ts index bac5f1e..110a4d0 100644 --- a/backend/api/server/scheduler/start.post.ts +++ b/backend/api/server/scheduler/start.post.ts @@ -1,6 +1,6 @@ import { startServerScheduler } from '../../../utils/server-scheduler' -export default defineEventHandler(() => { - startServerScheduler() +export default defineEventHandler(async () => { + await startServerScheduler() return { success: true, message: 'Server scheduler started' } }) diff --git a/backend/api/server/status.get.ts b/backend/api/server/status.get.ts index 1ba2092..7b35507 100644 --- a/backend/api/server/status.get.ts +++ b/backend/api/server/status.get.ts @@ -1,5 +1,5 @@ import { getServerSchedulerStatus } from '../../utils/server-scheduler' -export default defineEventHandler(() => { - return getServerSchedulerStatus() +export default defineEventHandler(async () => { + return await getServerSchedulerStatus() }) diff --git a/backend/api/server/targets/index.get.ts b/backend/api/server/targets/index.get.ts index 271969e..062c6cf 100644 --- a/backend/api/server/targets/index.get.ts +++ b/backend/api/server/targets/index.get.ts @@ -1,12 +1,10 @@ -import { getDb } from '../../../utils/db' +import { query } from '../../../utils/db' -export default defineEventHandler(() => { - const db = getDb() - - const targets = db.prepare(` +export default defineEventHandler(async () => { + const targets = await query(` SELECT * FROM server_targets ORDER BY target_id ASC - `).all() + `) return targets }) diff --git a/backend/api/server/targets/index.post.ts b/backend/api/server/targets/index.post.ts index ba2faee..f67258b 100644 --- a/backend/api/server/targets/index.post.ts +++ b/backend/api/server/targets/index.post.ts @@ -1,4 +1,4 @@ -import { getDb } from '../../../utils/db' +import { queryOne } from '../../../utils/db' import { refreshServerTimer } from '../../../utils/server-scheduler' export default defineEventHandler(async (event) => { @@ -12,18 +12,17 @@ export default defineEventHandler(async (event) => { }) } - const db = getDb() - - const result = db.prepare(` + const result = await queryOne<{ target_id: number }>(` INSERT INTO server_targets (server_name, server_ip, glances_url, is_active, collect_interval) - VALUES (?, ?, ?, ?, ?) - `).run(server_name, server_ip, glances_url, is_active ? 1 : 0, collect_interval) + VALUES ($1, $2, $3, $4, $5) + RETURNING target_id + `, [server_name, server_ip, glances_url, is_active ? 1 : 0, collect_interval]) - const targetId = result.lastInsertRowid as number + const targetId = result?.target_id // 스케줄러에 반영 - if (is_active) { - refreshServerTimer(targetId) + if (is_active && targetId) { + await refreshServerTimer(targetId) } return { diff --git a/backend/plugins/privnet-init.ts b/backend/plugins/privnet-init.ts index 0e57f78..a1597e8 100644 --- a/backend/plugins/privnet-init.ts +++ b/backend/plugins/privnet-init.ts @@ -1,14 +1,19 @@ -import { initPrivnetTables } from '../utils/db' +import { initPrivnetTables, shouldAutoStartScheduler } from '../utils/db' import { privnetScheduler } from '../utils/privnet-scheduler' -export default defineNitroPlugin((nitroApp) => { +export default defineNitroPlugin(async (nitroApp) => { console.log('[Plugin] privnet-init starting...') // DB 테이블 초기화 - initPrivnetTables() + await initPrivnetTables() - // 스케줄러 자동 시작 - privnetScheduler.start() + // 스케줄러 자동 시작 (환경에 따라) + if (shouldAutoStartScheduler()) { + privnetScheduler.start() + console.log('[Plugin] privnet scheduler auto-started (production mode)') + } else { + console.log('[Plugin] privnet scheduler NOT started (development mode - use API to start)') + } // 서버 종료 시 클린업 nitroApp.hooks.hook('close', () => { diff --git a/backend/plugins/pubnet-init.ts b/backend/plugins/pubnet-init.ts index 23def63..370892f 100644 --- a/backend/plugins/pubnet-init.ts +++ b/backend/plugins/pubnet-init.ts @@ -1,14 +1,19 @@ -import { initPubnetTables } from '../utils/db' +import { initPubnetTables, shouldAutoStartScheduler } from '../utils/db' import { pubnetScheduler } from '../utils/pubnet-scheduler' -export default defineNitroPlugin((nitroApp) => { +export default defineNitroPlugin(async (nitroApp) => { console.log('[Plugin] pubnet-init starting...') // DB 테이블 초기화 - initPubnetTables() + await initPubnetTables() - // 스케줄러 자동 시작 - pubnetScheduler.start() + // 스케줄러 자동 시작 (환경에 따라) + if (shouldAutoStartScheduler()) { + pubnetScheduler.start() + console.log('[Plugin] pubnet scheduler auto-started (production mode)') + } else { + console.log('[Plugin] pubnet scheduler NOT started (development mode - use API to start)') + } // 서버 종료 시 클린업 nitroApp.hooks.hook('close', () => { diff --git a/backend/plugins/server-init.ts b/backend/plugins/server-init.ts index 097eb1a..82077c2 100644 --- a/backend/plugins/server-init.ts +++ b/backend/plugins/server-init.ts @@ -1,8 +1,12 @@ +import { shouldAutoStartScheduler } from '../utils/db' import { startServerScheduler } from '../utils/server-scheduler' export default defineNitroPlugin(() => { - // 서버 시작 시 스케줄러 자동 시작 - startServerScheduler() - - console.log('[Server] Plugin initialized - scheduler auto-started') + // 스케줄러 자동 시작 (환경에 따라) + if (shouldAutoStartScheduler()) { + startServerScheduler() + console.log('[Server] Plugin initialized - scheduler auto-started (production mode)') + } else { + console.log('[Server] Plugin initialized - scheduler NOT started (development mode - use API to start)') + } }) diff --git a/backend/utils/db.ts b/backend/utils/db.ts index 4b91350..31dbee9 100644 --- a/backend/utils/db.ts +++ b/backend/utils/db.ts @@ -1,44 +1,109 @@ -import Database from 'better-sqlite3' -import { resolve } from 'path' +import pg from 'pg' -// 싱글톤 DB 인스턴스 -let db: Database.Database | null = null +const { Pool } = pg -export function getDb(): Database.Database { - if (!db) { - const dbPath = resolve(process.cwd(), 'database/osolit-monitor.db') - db = new Database(dbPath) - db.pragma('journal_mode = WAL') +// 환경 설정 +const isDev = process.env.NODE_ENV !== 'production' + +// PostgreSQL 연결 풀 (싱글톤) +let pool: pg.Pool | null = null + +/** + * PostgreSQL 연결 풀 가져오기 + */ +export function getPool(): pg.Pool { + if (!pool) { + pool = new Pool({ + host: process.env.DB_HOST || 'localhost', + port: parseInt(process.env.DB_PORT || '5432'), + database: process.env.DB_NAME || 'osolit_monitor', + user: process.env.DB_USER || 'postgres', + password: process.env.DB_PASSWORD || '', + max: 10, + idleTimeoutMillis: 30000, + connectionTimeoutMillis: 2000, + }) + + pool.on('error', (err) => { + console.error('[DB] Unexpected pool error:', err) + }) + + console.log(`[DB] PostgreSQL pool created (${isDev ? 'development' : 'production'})`) } - return db + return pool } -export function initPubnetTables(): void { - const db = getDb() +/** + * 단일 쿼리 실행 (자동 커넥션 반환) + */ +export async function query(sql: string, params?: any[]): Promise { + const pool = getPool() + const result = await pool.query(sql, params) + return result.rows as T[] +} - db.exec(` +/** + * 단일 행 조회 + */ +export async function queryOne(sql: string, params?: any[]): Promise { + const rows = await query(sql, params) + return rows[0] || null +} + +/** + * INSERT/UPDATE/DELETE 실행 + */ +export async function execute(sql: string, params?: any[]): Promise { + const pool = getPool() + const result = await pool.query(sql, params) + return result.rowCount || 0 +} + +/** + * 스케줄러 자동시작 여부 + */ +export function shouldAutoStartScheduler(): boolean { + // 환경변수로 명시적 설정된 경우 + const envValue = process.env.AUTO_START_SCHEDULER + if (envValue !== undefined) { + return envValue === 'true' + } + // 기본값: production=true, development=false + return !isDev +} + +/** + * pubnet 테이블 초기화 (PostgreSQL용) + */ +export async function initPubnetTables(): Promise { + const pool = getPool() + + // pubnet_targets + await pool.query(` CREATE TABLE IF NOT EXISTS pubnet_targets ( - id INTEGER PRIMARY KEY AUTOINCREMENT, + id SERIAL PRIMARY KEY, name TEXT NOT NULL, url TEXT NOT NULL UNIQUE, is_active INTEGER DEFAULT 1, sort_order INTEGER DEFAULT 0, - created_at TEXT DEFAULT (datetime('now', 'localtime')), - updated_at TEXT DEFAULT (datetime('now', 'localtime')) + created_at TEXT DEFAULT to_char(NOW(), 'YYYY-MM-DD HH24:MI:SS'), + updated_at TEXT DEFAULT to_char(NOW(), 'YYYY-MM-DD HH24:MI:SS') ) `) - db.exec(` + // pubnet_logs + await pool.query(` CREATE TABLE IF NOT EXISTS pubnet_logs ( - id INTEGER PRIMARY KEY AUTOINCREMENT, + id SERIAL PRIMARY KEY, target_id INTEGER NOT NULL, is_success INTEGER NOT NULL, - checked_at TEXT DEFAULT (datetime('now', 'localtime')), + checked_at TEXT DEFAULT to_char(NOW(), 'YYYY-MM-DD HH24:MI:SS'), FOREIGN KEY (target_id) REFERENCES pubnet_targets(id) ) `) - db.exec(` + // pubnet_status (싱글톤 패턴) + await pool.query(` CREATE TABLE IF NOT EXISTS pubnet_status ( id INTEGER PRIMARY KEY CHECK (id = 1), current_index INTEGER DEFAULT 0, @@ -47,44 +112,51 @@ export function initPubnetTables(): void { last_target_id INTEGER, last_checked_at TEXT, scheduler_running INTEGER DEFAULT 0, - updated_at TEXT DEFAULT (datetime('now', 'localtime')) + updated_at TEXT DEFAULT to_char(NOW(), 'YYYY-MM-DD HH24:MI:SS') ) `) - const statusExists = db.prepare('SELECT COUNT(*) as cnt FROM pubnet_status').get() as { cnt: number } - if (statusExists.cnt === 0) { - db.prepare('INSERT INTO pubnet_status (id) VALUES (1)').run() + // 상태 행 존재 확인 및 생성 + const statusExists = await queryOne<{ cnt: number }>('SELECT COUNT(*) as cnt FROM pubnet_status') + if (!statusExists || statusExists.cnt === 0) { + await pool.query('INSERT INTO pubnet_status (id) VALUES (1)') } console.log('[DB] pubnet tables initialized') } -export function initPrivnetTables(): void { - const db = getDb() +/** + * privnet 테이블 초기화 (PostgreSQL용) + */ +export async function initPrivnetTables(): Promise { + const pool = getPool() - db.exec(` + // privnet_targets + await pool.query(` CREATE TABLE IF NOT EXISTS privnet_targets ( - id INTEGER PRIMARY KEY AUTOINCREMENT, + id SERIAL PRIMARY KEY, name TEXT NOT NULL, url TEXT NOT NULL UNIQUE, is_active INTEGER DEFAULT 1, sort_order INTEGER DEFAULT 0, - created_at TEXT DEFAULT (datetime('now', 'localtime')), - updated_at TEXT DEFAULT (datetime('now', 'localtime')) + created_at TEXT DEFAULT to_char(NOW(), 'YYYY-MM-DD HH24:MI:SS'), + updated_at TEXT DEFAULT to_char(NOW(), 'YYYY-MM-DD HH24:MI:SS') ) `) - db.exec(` + // privnet_logs + await pool.query(` CREATE TABLE IF NOT EXISTS privnet_logs ( - id INTEGER PRIMARY KEY AUTOINCREMENT, + id SERIAL PRIMARY KEY, target_id INTEGER NOT NULL, is_success INTEGER NOT NULL, - checked_at TEXT DEFAULT (datetime('now', 'localtime')), + checked_at TEXT DEFAULT to_char(NOW(), 'YYYY-MM-DD HH24:MI:SS'), FOREIGN KEY (target_id) REFERENCES privnet_targets(id) ) `) - db.exec(` + // privnet_status (싱글톤 패턴) + await pool.query(` CREATE TABLE IF NOT EXISTS privnet_status ( id INTEGER PRIMARY KEY CHECK (id = 1), current_index INTEGER DEFAULT 0, @@ -93,21 +165,26 @@ export function initPrivnetTables(): void { last_target_id INTEGER, last_checked_at TEXT, scheduler_running INTEGER DEFAULT 0, - updated_at TEXT DEFAULT (datetime('now', 'localtime')) + updated_at TEXT DEFAULT to_char(NOW(), 'YYYY-MM-DD HH24:MI:SS') ) `) - const statusExists = db.prepare('SELECT COUNT(*) as cnt FROM privnet_status').get() as { cnt: number } - if (statusExists.cnt === 0) { - db.prepare('INSERT INTO privnet_status (id) VALUES (1)').run() + // 상태 행 존재 확인 및 생성 + const statusExists = await queryOne<{ cnt: number }>('SELECT COUNT(*) as cnt FROM privnet_status') + if (!statusExists || statusExists.cnt === 0) { + await pool.query('INSERT INTO privnet_status (id) VALUES (1)') } console.log('[DB] privnet tables initialized') } -export function closeDb(): void { - if (db) { - db.close() - db = null +/** + * 연결 풀 종료 + */ +export async function closeDb(): Promise { + if (pool) { + await pool.end() + pool = null + console.log('[DB] PostgreSQL pool closed') } } diff --git a/backend/utils/privnet-scheduler.ts b/backend/utils/privnet-scheduler.ts index bce80f0..e9dfd60 100644 --- a/backend/utils/privnet-scheduler.ts +++ b/backend/utils/privnet-scheduler.ts @@ -1,4 +1,4 @@ -import { getDb } from './db' +import { query, queryOne, execute } from './db' // 상수 정의 const INTERVAL_SUCCESS = 5 * 60 * 1000 // 5분 @@ -84,10 +84,10 @@ class PrivnetScheduler { const nextInterval = result.isSuccess ? INTERVAL_SUCCESS : INTERVAL_FAILURE // 로그 저장 - this.saveLog(result) + await this.saveLog(result) // 상태 업데이트 (인덱스 증가) - this.updateStatus(result, nextInterval) + await this.updateStatus(result, nextInterval) console.log( `[PrivnetScheduler] ${result.targetName} (${result.url}) - ` + @@ -109,21 +109,23 @@ class PrivnetScheduler { * 현재 타겟 URL 체크 */ private async checkCurrentTarget(): Promise { - const db = getDb() - // 활성화된 타겟 목록 조회 - const targets = db.prepare(` + const targets = await query(` SELECT * FROM privnet_targets WHERE is_active = 1 ORDER BY id ASC - `).all() as PrivnetTarget[] + `) if (targets.length === 0) { throw new Error('No active targets found') } // 현재 인덱스 조회 - const status = db.prepare('SELECT * FROM privnet_status WHERE id = 1').get() as PrivnetStatus + const status = await queryOne('SELECT * FROM privnet_status WHERE id = 1') + if (!status) { + throw new Error('Status not found') + } + const currentIndex = status.current_index % targets.length const target = targets[currentIndex] @@ -160,60 +162,50 @@ class PrivnetScheduler { /** * 체크 결과 로그 저장 */ - private saveLog(result: CheckResult): void { - const db = getDb() - - db.prepare(` - INSERT INTO privnet_logs (target_id, is_success) - VALUES (@targetId, @isSuccess) - `).run({ - targetId: result.targetId, - isSuccess: result.isSuccess ? 1 : 0 - }) + private async saveLog(result: CheckResult): Promise { + await execute( + `INSERT INTO privnet_logs (target_id, is_success) VALUES ($1, $2)`, + [result.targetId, result.isSuccess ? 1 : 0] + ) } /** * 상태 업데이트 (인덱스 순환) */ - private updateStatus(result: CheckResult, nextInterval: number): void { - const db = getDb() - + private async updateStatus(result: CheckResult, nextInterval: number): Promise { // 활성 타겟 수 조회 - const countResult = db.prepare(` - SELECT COUNT(*) as cnt FROM privnet_targets WHERE is_active = 1 - `).get() as { cnt: number } + const countResult = await queryOne<{ cnt: string }>( + `SELECT COUNT(*) as cnt FROM privnet_targets WHERE is_active = 1` + ) + const totalCount = parseInt(countResult?.cnt || '0') - const status = db.prepare('SELECT current_index FROM privnet_status WHERE id = 1').get() as { current_index: number } - const nextIndex = (status.current_index + 1) % countResult.cnt + const status = await queryOne<{ current_index: number }>( + 'SELECT current_index FROM privnet_status WHERE id = 1' + ) + const nextIndex = ((status?.current_index || 0) + 1) % totalCount - db.prepare(` + await execute(` UPDATE privnet_status SET - current_index = @nextIndex, - check_interval = @checkInterval, - is_healthy = @isHealthy, - last_target_id = @lastTargetId, - last_checked_at = datetime('now', 'localtime'), - updated_at = datetime('now', 'localtime') + current_index = $1, + check_interval = $2, + is_healthy = $3, + last_target_id = $4, + last_checked_at = to_char(NOW(), 'YYYY-MM-DD HH24:MI:SS'), + updated_at = to_char(NOW(), 'YYYY-MM-DD HH24:MI:SS') WHERE id = 1 - `).run({ - nextIndex, - checkInterval: nextInterval, - isHealthy: result.isSuccess ? 1 : 0, - lastTargetId: result.targetId - }) + `, [nextIndex, nextInterval, result.isSuccess ? 1 : 0, result.targetId]) } /** * 스케줄러 실행 상태 업데이트 */ - private updateSchedulerRunning(running: number): void { - const db = getDb() - db.prepare(` + private async updateSchedulerRunning(running: number): Promise { + await execute(` UPDATE privnet_status SET - scheduler_running = @running, - updated_at = datetime('now', 'localtime') + scheduler_running = $1, + updated_at = to_char(NOW(), 'YYYY-MM-DD HH24:MI:SS') WHERE id = 1 - `).run({ running }) + `, [running]) } } diff --git a/backend/utils/pubnet-scheduler.ts b/backend/utils/pubnet-scheduler.ts index 008245e..716725e 100644 --- a/backend/utils/pubnet-scheduler.ts +++ b/backend/utils/pubnet-scheduler.ts @@ -1,4 +1,4 @@ -import { getDb } from './db' +import { query, queryOne, execute } from './db' // 상수 정의 const INTERVAL_SUCCESS = 5 * 60 * 1000 // 5분 @@ -84,10 +84,10 @@ class PubnetScheduler { const nextInterval = result.isSuccess ? INTERVAL_SUCCESS : INTERVAL_FAILURE // 로그 저장 (1개만) - this.saveLog(result) + await this.saveLog(result) // 상태 업데이트 (인덱스 +1) - this.updateStatus(result, nextInterval) + await this.updateStatus(result, nextInterval) console.log( `[PubnetScheduler] ${result.targetName} (${result.url}) - ` + @@ -109,21 +109,22 @@ class PubnetScheduler { * 현재 타겟 1개 체크 */ private async checkCurrentTargets(): Promise { - const db = getDb() - // 활성화된 타겟 목록 조회 - const targets = db.prepare(` + const targets = await query(` SELECT * FROM pubnet_targets WHERE is_active = 1 ORDER BY id ASC - `).all() as PubnetTarget[] + `) if (targets.length === 0) { throw new Error('No active targets found') } // 현재 인덱스 조회 - const status = db.prepare('SELECT * FROM pubnet_status WHERE id = 1').get() as PubnetStatus + const status = await queryOne('SELECT * FROM pubnet_status WHERE id = 1') + if (!status) { + throw new Error('Status not found') + } // 1개 타겟 선택 const idx = status.current_index % targets.length @@ -160,62 +161,52 @@ class PubnetScheduler { /** * 체크 결과 로그 저장 */ - private saveLog(result: CheckResult): void { - const db = getDb() - - db.prepare(` - INSERT INTO pubnet_logs (target_id, is_success) - VALUES (@targetId, @isSuccess) - `).run({ - targetId: result.targetId, - isSuccess: result.isSuccess ? 1 : 0 - }) + private async saveLog(result: CheckResult): Promise { + await execute( + `INSERT INTO pubnet_logs (target_id, is_success) VALUES ($1, $2)`, + [result.targetId, result.isSuccess ? 1 : 0] + ) } /** * 상태 업데이트 (인덱스 +1 순환) */ - private updateStatus(result: CheckResult, nextInterval: number): void { - const db = getDb() - + private async updateStatus(result: CheckResult, nextInterval: number): Promise { // 활성 타겟 수 조회 - const countResult = db.prepare(` - SELECT COUNT(*) as cnt FROM pubnet_targets WHERE is_active = 1 - `).get() as { cnt: number } + const countResult = await queryOne<{ cnt: string }>( + `SELECT COUNT(*) as cnt FROM pubnet_targets WHERE is_active = 1` + ) + const totalCount = parseInt(countResult?.cnt || '0') - const status = db.prepare('SELECT current_index FROM pubnet_status WHERE id = 1').get() as { current_index: number } + const status = await queryOne<{ current_index: number }>( + 'SELECT current_index FROM pubnet_status WHERE id = 1' + ) // 인덱스 +1 (순환) - const nextIndex = (status.current_index + 1) % countResult.cnt + const nextIndex = ((status?.current_index || 0) + 1) % totalCount - db.prepare(` + await execute(` UPDATE pubnet_status SET - current_index = @nextIndex, - check_interval = @checkInterval, - is_healthy = @isHealthy, - last_target_id = @lastTargetId, - last_checked_at = datetime('now', 'localtime'), - updated_at = datetime('now', 'localtime') + current_index = $1, + check_interval = $2, + is_healthy = $3, + last_target_id = $4, + last_checked_at = to_char(NOW(), 'YYYY-MM-DD HH24:MI:SS'), + updated_at = to_char(NOW(), 'YYYY-MM-DD HH24:MI:SS') WHERE id = 1 - `).run({ - nextIndex, - checkInterval: nextInterval, - isHealthy: result.isSuccess ? 1 : 0, - lastTargetId: result.targetId - }) + `, [nextIndex, nextInterval, result.isSuccess ? 1 : 0, result.targetId]) } /** * 스케줄러 실행 상태 업데이트 */ - private updateSchedulerRunning(running: number): void { - const db = getDb() - db.prepare(` + private async updateSchedulerRunning(running: number): Promise { + await execute(` UPDATE pubnet_status SET - scheduler_running = @running, - updated_at = datetime('now', 'localtime') + scheduler_running = $1, + updated_at = to_char(NOW(), 'YYYY-MM-DD HH24:MI:SS') WHERE id = 1 - `).run({ running }) + `, [running]) } } diff --git a/backend/utils/server-scheduler.ts b/backend/utils/server-scheduler.ts index f318c7a..bf64575 100644 --- a/backend/utils/server-scheduler.ts +++ b/backend/utils/server-scheduler.ts @@ -1,4 +1,4 @@ -import { getDb } from './db' +import { query, queryOne, execute, getPool } from './db' interface ServerTarget { target_id: number @@ -60,20 +60,19 @@ async function detectApiVersion(baseUrl: string, serverName: string): Promise(` SELECT cpu_percent, memory_percent FROM server_snapshots - WHERE target_id = ? AND is_online = 1 + WHERE target_id = $1 AND is_online = 1 ORDER BY collected_at DESC LIMIT 20 - `).all(targetId) as any[] + `, [targetId]) if (snapshots.length >= 4) { const half = Math.floor(snapshots.length / 2) @@ -88,39 +87,46 @@ async function detectAnomalies(targetId: number, serverName: string) { const cpuChange = prevCpuAvg > 1 ? ((currCpuAvg - prevCpuAvg) / prevCpuAvg) * 100 : currCpuAvg - prevCpuAvg const memChange = prevMemAvg > 1 ? ((currMemAvg - prevMemAvg) / prevMemAvg) * 100 : currMemAvg - prevMemAvg - // 중복 체크용 - const recentLogExists = db.prepare(` - SELECT 1 FROM anomaly_logs - WHERE target_id = ? AND detect_type = ? AND metric = ? - AND detected_at > datetime('now', '-1 minute', 'localtime') - LIMIT 1 - `) - - const insertLog = db.prepare(` - INSERT INTO anomaly_logs (target_id, server_name, detect_type, metric, level, current_value, threshold_value, message) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - `) - // CPU 단기 변화율 체크 if (Math.abs(cpuChange) >= SHORT_TERM_THRESHOLD) { - const level = Math.abs(cpuChange) >= 100 ? 'danger' : 'warning' - const direction = cpuChange >= 0 ? '증가' : '감소' - const message = `CPU ${direction} 감지 (${prevCpuAvg.toFixed(1)}% → ${currCpuAvg.toFixed(1)}%)` + const recentExists = await queryOne(` + SELECT 1 FROM anomaly_logs + WHERE target_id = $1 AND detect_type = 'short-term' AND metric = 'CPU' + AND detected_at > NOW() - INTERVAL '1 minute' + LIMIT 1 + `, [targetId]) - if (!recentLogExists.get(targetId, 'short-term', 'CPU')) { - insertLog.run(targetId, serverName, 'short-term', 'CPU', level, currCpuAvg, cpuChange, message) + if (!recentExists) { + const level = Math.abs(cpuChange) >= 100 ? 'danger' : 'warning' + const direction = cpuChange >= 0 ? '증가' : '감소' + const message = `CPU ${direction} 감지 (${prevCpuAvg.toFixed(1)}% → ${currCpuAvg.toFixed(1)}%)` + + await execute(` + INSERT INTO anomaly_logs (target_id, server_name, detect_type, metric, level, current_value, threshold_value, message) + VALUES ($1, $2, 'short-term', 'CPU', $3, $4, $5, $6) + `, [targetId, serverName, level, currCpuAvg, cpuChange, message]) console.log(`[${now}] 🚨 [${serverName}] 단기변화율 이상감지: CPU ${cpuChange.toFixed(1)}% (${level})`) } } // Memory 단기 변화율 체크 if (Math.abs(memChange) >= SHORT_TERM_THRESHOLD) { - const level = Math.abs(memChange) >= 100 ? 'danger' : 'warning' - const direction = memChange >= 0 ? '증가' : '감소' - const message = `Memory ${direction} 감지 (${prevMemAvg.toFixed(1)}% → ${currMemAvg.toFixed(1)}%)` + const recentExists = await queryOne(` + SELECT 1 FROM anomaly_logs + WHERE target_id = $1 AND detect_type = 'short-term' AND metric = 'Memory' + AND detected_at > NOW() - INTERVAL '1 minute' + LIMIT 1 + `, [targetId]) - if (!recentLogExists.get(targetId, 'short-term', 'Memory')) { - insertLog.run(targetId, serverName, 'short-term', 'Memory', level, currMemAvg, memChange, message) + if (!recentExists) { + const level = Math.abs(memChange) >= 100 ? 'danger' : 'warning' + const direction = memChange >= 0 ? '증가' : '감소' + const message = `Memory ${direction} 감지 (${prevMemAvg.toFixed(1)}% → ${currMemAvg.toFixed(1)}%)` + + await execute(` + INSERT INTO anomaly_logs (target_id, server_name, detect_type, metric, level, current_value, threshold_value, message) + VALUES ($1, $2, 'short-term', 'Memory', $3, $4, $5, $6) + `, [targetId, serverName, level, currMemAvg, memChange, message]) console.log(`[${now}] 🚨 [${serverName}] 단기변화율 이상감지: Memory ${memChange.toFixed(1)}% (${level})`) } } @@ -130,13 +136,13 @@ async function detectAnomalies(targetId: number, serverName: string) { const WARNING_Z = 2.0 const DANGER_Z = 3.0 - const hourSnapshots = db.prepare(` + const hourSnapshots = await query(` SELECT cpu_percent, memory_percent FROM server_snapshots - WHERE target_id = ? AND is_online = 1 - AND collected_at >= datetime('now', '-1 hour', 'localtime') + WHERE target_id = $1 AND is_online = 1 + AND collected_at >= NOW() - INTERVAL '1 hour' ORDER BY collected_at DESC - `).all(targetId) as any[] + `, [targetId]) if (hourSnapshots.length >= 10) { const current = hourSnapshots[0] @@ -157,43 +163,51 @@ async function detectAnomalies(targetId: number, serverName: string) { const cpuZscore = cpuStd > 0.1 ? (currCpu - cpuAvg) / cpuStd : 0 const memZscore = memStd > 0.1 ? (currMem - memAvg) / memStd : 0 - const recentLogExists = db.prepare(` - SELECT 1 FROM anomaly_logs - WHERE target_id = ? AND detect_type = 'zscore' AND metric = ? - AND detected_at > datetime('now', '-1 minute', 'localtime') - LIMIT 1 - `) - - const insertLog = db.prepare(` - INSERT INTO anomaly_logs (target_id, server_name, detect_type, metric, level, current_value, threshold_value, message) - VALUES (?, ?, 'zscore', ?, ?, ?, ?, ?) - `) - // CPU Z-Score 체크 if (Math.abs(cpuZscore) >= WARNING_Z) { - const level = Math.abs(cpuZscore) >= DANGER_Z ? 'danger' : 'warning' - const direction = cpuZscore >= 0 ? '높음' : '낮음' - const message = `CPU 평균 대비 ${Math.abs(cpuZscore).toFixed(1)}σ ${direction} (평균: ${cpuAvg.toFixed(1)}%, 현재: ${currCpu.toFixed(1)}%)` + const recentExists = await queryOne(` + SELECT 1 FROM anomaly_logs + WHERE target_id = $1 AND detect_type = 'zscore' AND metric = 'CPU' + AND detected_at > NOW() - INTERVAL '1 minute' + LIMIT 1 + `, [targetId]) - if (!recentLogExists.get(targetId, 'CPU')) { - insertLog.run(targetId, serverName, 'CPU', level, currCpu, cpuZscore, message) + if (!recentExists) { + const level = Math.abs(cpuZscore) >= DANGER_Z ? 'danger' : 'warning' + const direction = cpuZscore >= 0 ? '높음' : '낮음' + const message = `CPU 평균 대비 ${Math.abs(cpuZscore).toFixed(1)}σ ${direction} (평균: ${cpuAvg.toFixed(1)}%, 현재: ${currCpu.toFixed(1)}%)` + + await execute(` + INSERT INTO anomaly_logs (target_id, server_name, detect_type, metric, level, current_value, threshold_value, message) + VALUES ($1, $2, 'zscore', 'CPU', $3, $4, $5, $6) + `, [targetId, serverName, level, currCpu, cpuZscore, message]) console.log(`[${now}] 🚨 [${serverName}] Z-Score 이상감지: CPU Z=${cpuZscore.toFixed(2)} (${level})`) } } // Memory Z-Score 체크 if (Math.abs(memZscore) >= WARNING_Z) { - const level = Math.abs(memZscore) >= DANGER_Z ? 'danger' : 'warning' - const direction = memZscore >= 0 ? '높음' : '낮음' - const message = `Memory 평균 대비 ${Math.abs(memZscore).toFixed(1)}σ ${direction} (평균: ${memAvg.toFixed(1)}%, 현재: ${currMem.toFixed(1)}%)` + const recentExists = await queryOne(` + SELECT 1 FROM anomaly_logs + WHERE target_id = $1 AND detect_type = 'zscore' AND metric = 'Memory' + AND detected_at > NOW() - INTERVAL '1 minute' + LIMIT 1 + `, [targetId]) - if (!recentLogExists.get(targetId, 'Memory')) { - insertLog.run(targetId, serverName, 'Memory', level, currMem, memZscore, message) + if (!recentExists) { + const level = Math.abs(memZscore) >= DANGER_Z ? 'danger' : 'warning' + const direction = memZscore >= 0 ? '높음' : '낮음' + const message = `Memory 평균 대비 ${Math.abs(memZscore).toFixed(1)}σ ${direction} (평균: ${memAvg.toFixed(1)}%, 현재: ${currMem.toFixed(1)}%)` + + await execute(` + INSERT INTO anomaly_logs (target_id, server_name, detect_type, metric, level, current_value, threshold_value, message) + VALUES ($1, $2, 'zscore', 'Memory', $3, $4, $5, $6) + `, [targetId, serverName, level, currMem, memZscore, message]) console.log(`[${now}] 🚨 [${serverName}] Z-Score 이상감지: Memory Z=${memZscore.toFixed(2)} (${level})`) } } } - + // === 시간대별 베이스라인 감지 === const DEVIATION_THRESHOLD = 2.0 const currentHour = new Date().getHours() @@ -201,25 +215,25 @@ async function detectAnomalies(targetId: number, serverName: string) { const isWeekend = currentDayOfWeek === 0 || currentDayOfWeek === 6 const dayType = isWeekend ? 'weekend' : 'weekday' - const baselineData = db.prepare(` + const baselineData = await query(` SELECT cpu_percent, memory_percent FROM server_snapshots - WHERE target_id = ? AND is_online = 1 - AND collected_at >= datetime('now', '-14 days', 'localtime') - AND strftime('%H', collected_at) = ? + WHERE target_id = $1 AND is_online = 1 + AND collected_at >= NOW() - INTERVAL '14 days' + AND EXTRACT(HOUR FROM collected_at) = $2 AND ( - (? = 'weekend' AND strftime('%w', collected_at) IN ('0', '6')) + ($3 = 'weekend' AND EXTRACT(DOW FROM collected_at) IN (0, 6)) OR - (? = 'weekday' AND strftime('%w', collected_at) NOT IN ('0', '6')) + ($3 = 'weekday' AND EXTRACT(DOW FROM collected_at) NOT IN (0, 6)) ) - `).all(targetId, currentHour.toString().padStart(2, '0'), dayType, dayType) as any[] + `, [targetId, currentHour, dayType]) - const currentSnapshot = db.prepare(` + const currentSnapshot = await queryOne(` SELECT cpu_percent, memory_percent FROM server_snapshots - WHERE target_id = ? AND is_online = 1 + WHERE target_id = $1 AND is_online = 1 ORDER BY collected_at DESC LIMIT 1 - `).get(targetId) as any + `, [targetId]) if (baselineData.length >= 5 && currentSnapshot) { const currCpu = currentSnapshot.cpu_percent ?? 0 @@ -239,38 +253,46 @@ async function detectAnomalies(targetId: number, serverName: string) { const cpuDeviation = cpuStd > 0.1 ? (currCpu - cpuAvg) / cpuStd : 0 const memDeviation = memStd > 0.1 ? (currMem - memAvg) / memStd : 0 - const baselineLogExists = db.prepare(` - SELECT 1 FROM anomaly_logs - WHERE target_id = ? AND detect_type = 'baseline' AND metric = ? - AND detected_at > datetime('now', '-1 minute', 'localtime') - LIMIT 1 - `) - - const baselineInsertLog = db.prepare(` - INSERT INTO anomaly_logs (target_id, server_name, detect_type, metric, level, current_value, threshold_value, message) - VALUES (?, ?, 'baseline', ?, ?, ?, ?, ?) - `) - if (Math.abs(cpuDeviation) >= DEVIATION_THRESHOLD) { - const level = Math.abs(cpuDeviation) >= 3.0 ? 'danger' : 'warning' - const direction = cpuDeviation >= 0 ? '높음' : '낮음' - const dayLabel = isWeekend ? '주말' : '평일' - const message = `CPU ${dayLabel} ${currentHour}시 베이스라인 대비 ${Math.abs(cpuDeviation).toFixed(1)}σ ${direction}` + const recentExists = await queryOne(` + SELECT 1 FROM anomaly_logs + WHERE target_id = $1 AND detect_type = 'baseline' AND metric = 'CPU' + AND detected_at > NOW() - INTERVAL '1 minute' + LIMIT 1 + `, [targetId]) - if (!baselineLogExists.get(targetId, 'CPU')) { - baselineInsertLog.run(targetId, serverName, 'CPU', level, currCpu, cpuDeviation, message) + if (!recentExists) { + const level = Math.abs(cpuDeviation) >= 3.0 ? 'danger' : 'warning' + const direction = cpuDeviation >= 0 ? '높음' : '낮음' + const dayLabel = isWeekend ? '주말' : '평일' + const message = `CPU ${dayLabel} ${currentHour}시 베이스라인 대비 ${Math.abs(cpuDeviation).toFixed(1)}σ ${direction}` + + await execute(` + INSERT INTO anomaly_logs (target_id, server_name, detect_type, metric, level, current_value, threshold_value, message) + VALUES ($1, $2, 'baseline', 'CPU', $3, $4, $5, $6) + `, [targetId, serverName, level, currCpu, cpuDeviation, message]) console.log(`[${now}] 🚨 [${serverName}] 베이스라인 이상감지: CPU σ=${cpuDeviation.toFixed(2)} (${level})`) } } if (Math.abs(memDeviation) >= DEVIATION_THRESHOLD) { - const level = Math.abs(memDeviation) >= 3.0 ? 'danger' : 'warning' - const direction = memDeviation >= 0 ? '높음' : '낮음' - const dayLabel = isWeekend ? '주말' : '평일' - const message = `Memory ${dayLabel} ${currentHour}시 베이스라인 대비 ${Math.abs(memDeviation).toFixed(1)}σ ${direction}` + const recentExists = await queryOne(` + SELECT 1 FROM anomaly_logs + WHERE target_id = $1 AND detect_type = 'baseline' AND metric = 'Memory' + AND detected_at > NOW() - INTERVAL '1 minute' + LIMIT 1 + `, [targetId]) - if (!baselineLogExists.get(targetId, 'Memory')) { - baselineInsertLog.run(targetId, serverName, 'Memory', level, currMem, memDeviation, message) + if (!recentExists) { + const level = Math.abs(memDeviation) >= 3.0 ? 'danger' : 'warning' + const direction = memDeviation >= 0 ? '높음' : '낮음' + const dayLabel = isWeekend ? '주말' : '평일' + const message = `Memory ${dayLabel} ${currentHour}시 베이스라인 대비 ${Math.abs(memDeviation).toFixed(1)}σ ${direction}` + + await execute(` + INSERT INTO anomaly_logs (target_id, server_name, detect_type, metric, level, current_value, threshold_value, message) + VALUES ($1, $2, 'baseline', 'Memory', $3, $4, $5, $6) + `, [targetId, serverName, level, currMem, memDeviation, message]) console.log(`[${now}] 🚨 [${serverName}] 베이스라인 이상감지: Memory σ=${memDeviation.toFixed(2)} (${level})`) } } @@ -280,13 +302,13 @@ async function detectAnomalies(targetId: number, serverName: string) { const SLOPE_THRESHOLD = 0.5 const WINDOW_MINUTES = 30 - const trendSnapshots = db.prepare(` + const trendSnapshots = await query(` SELECT cpu_percent, memory_percent FROM server_snapshots - WHERE target_id = ? AND is_online = 1 - AND collected_at >= datetime('now', '-${WINDOW_MINUTES} minutes', 'localtime') + WHERE target_id = $1 AND is_online = 1 + AND collected_at >= NOW() - INTERVAL '${WINDOW_MINUTES} minutes' ORDER BY collected_at ASC - `).all(targetId) as any[] + `, [targetId]) if (trendSnapshots.length >= 10) { const n = trendSnapshots.length @@ -315,34 +337,42 @@ async function detectAnomalies(targetId: number, serverName: string) { const cpuResult = calcSlope(trendSnapshots.map(s => s.cpu_percent ?? 0)) const memResult = calcSlope(trendSnapshots.map(s => s.memory_percent ?? 0)) - const trendLogExists = db.prepare(` - SELECT 1 FROM anomaly_logs - WHERE target_id = ? AND detect_type = 'trend' AND metric = ? - AND detected_at > datetime('now', '-1 minute', 'localtime') - LIMIT 1 - `) - - const trendInsertLog = db.prepare(` - INSERT INTO anomaly_logs (target_id, server_name, detect_type, metric, level, current_value, threshold_value, message) - VALUES (?, ?, 'trend', ?, ?, ?, ?, ?) - `) - if (cpuResult.slope >= SLOPE_THRESHOLD && cpuResult.r2 >= 0.3) { - const level = cpuResult.slope >= 1.0 ? 'danger' : 'warning' - const message = `CPU 지속 상승 중 (분당 +${cpuResult.slope.toFixed(2)}%, R²=${cpuResult.r2.toFixed(2)})` + const recentExists = await queryOne(` + SELECT 1 FROM anomaly_logs + WHERE target_id = $1 AND detect_type = 'trend' AND metric = 'CPU' + AND detected_at > NOW() - INTERVAL '1 minute' + LIMIT 1 + `, [targetId]) - if (!trendLogExists.get(targetId, 'CPU')) { - trendInsertLog.run(targetId, serverName, 'CPU', level, currCpu, cpuResult.slope, message) + if (!recentExists) { + const level = cpuResult.slope >= 1.0 ? 'danger' : 'warning' + const message = `CPU 지속 상승 중 (분당 +${cpuResult.slope.toFixed(2)}%, R²=${cpuResult.r2.toFixed(2)})` + + await execute(` + INSERT INTO anomaly_logs (target_id, server_name, detect_type, metric, level, current_value, threshold_value, message) + VALUES ($1, $2, 'trend', 'CPU', $3, $4, $5, $6) + `, [targetId, serverName, level, currCpu, cpuResult.slope, message]) console.log(`[${now}] 🚨 [${serverName}] 추세 이상감지: CPU +${cpuResult.slope.toFixed(2)}/분 (${level})`) } } if (memResult.slope >= SLOPE_THRESHOLD && memResult.r2 >= 0.3) { - const level = memResult.slope >= 1.0 ? 'danger' : 'warning' - const message = `Memory 지속 상승 중 (분당 +${memResult.slope.toFixed(2)}%, R²=${memResult.r2.toFixed(2)})` + const recentExists = await queryOne(` + SELECT 1 FROM anomaly_logs + WHERE target_id = $1 AND detect_type = 'trend' AND metric = 'Memory' + AND detected_at > NOW() - INTERVAL '1 minute' + LIMIT 1 + `, [targetId]) - if (!trendLogExists.get(targetId, 'Memory')) { - trendInsertLog.run(targetId, serverName, 'Memory', level, currMem, memResult.slope, message) + if (!recentExists) { + const level = memResult.slope >= 1.0 ? 'danger' : 'warning' + const message = `Memory 지속 상승 중 (분당 +${memResult.slope.toFixed(2)}%, R²=${memResult.r2.toFixed(2)})` + + await execute(` + INSERT INTO anomaly_logs (target_id, server_name, detect_type, metric, level, current_value, threshold_value, message) + VALUES ($1, $2, 'trend', 'Memory', $3, $4, $5, $6) + `, [targetId, serverName, level, currMem, memResult.slope, message]) console.log(`[${now}] 🚨 [${serverName}] 추세 이상감지: Memory +${memResult.slope.toFixed(2)}/분 (${level})`) } } @@ -353,9 +383,9 @@ async function detectAnomalies(targetId: number, serverName: string) { } } + // 서버 데이터 수집 async function collectServerData(target: ServerTarget) { - const db = getDb() const now = timestamp() console.log(`[${now}] 📡 [${target.server_name}] 수집 시작... (${target.glances_url})`) @@ -373,10 +403,10 @@ async function collectServerData(target: ServerTarget) { if (!apiVersion) { console.log(`[${now}] ❌ [${target.server_name}] 연결 실패 - Offline 기록`) - db.prepare(` + await execute(` INSERT INTO server_snapshots (target_id, is_online, collected_at) - VALUES (?, 0, ?) - `).run(target.target_id, now) + VALUES ($1, 0, $2) + `, [target.target_id, now]) return } @@ -403,10 +433,10 @@ async function collectServerData(target: ServerTarget) { // 캐시 클리어 후 재시도 위해 apiVersionCache.delete(target.target_id) console.log(`[${now}] ❌ [${target.server_name}] 연결 실패 - Offline 기록`) - db.prepare(` + await execute(` INSERT INTO server_snapshots (target_id, is_online, collected_at) - VALUES (?, 0, ?) - `).run(target.target_id, now) + VALUES ($1, 0, $2) + `, [target.target_id, now]) return } @@ -423,16 +453,17 @@ async function collectServerData(target: ServerTarget) { cpuTemp = tempSensor?.value ?? null } - // server_snapshots INSERT (api_version 포함) + // server_snapshots INSERT console.log(`[${now}] 💾 [${target.server_name}] snapshot 저장 (API v${apiVersion}, CPU: ${cpu?.total?.toFixed(1) || 0}%, MEM: ${mem?.percent?.toFixed(1) || 0}%, TEMP: ${cpuTemp ?? 'N/A'}°C, LOAD: ${quicklook?.load?.toFixed(1) ?? 'N/A'}%)`) - db.prepare(` + + await execute(` INSERT INTO server_snapshots ( target_id, os_name, os_version, host_name, uptime_seconds, uptime_str, ip_address, cpu_name, cpu_count, cpu_percent, memory_total, memory_used, memory_percent, swap_total, swap_used, swap_percent, is_online, api_version, cpu_temp, load_1, load_5, load_15, load_percent, collected_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `).run( + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24) + `, [ target.target_id, system?.os_name || system?.linux_distro || null, system?.os_version || null, @@ -457,19 +488,18 @@ async function collectServerData(target: ServerTarget) { load?.min15 ?? null, quicklook?.load ?? null, now - ) + ]) // server_disks INSERT (배열) if (Array.isArray(fs) && fs.length > 0) { console.log(`[${now}] 💾 [${target.server_name}] disk 저장 (${fs.length}개 파티션)`) - const diskStmt = db.prepare(` - INSERT INTO server_disks ( - target_id, device_name, mount_point, fs_type, - disk_total, disk_used, disk_percent, collected_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) - `) for (const disk of fs) { - diskStmt.run( + await execute(` + INSERT INTO server_disks ( + target_id, device_name, mount_point, fs_type, + disk_total, disk_used, disk_percent, collected_at + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + `, [ target.target_id, disk.device_name || null, disk.mnt_point || null, @@ -478,22 +508,21 @@ async function collectServerData(target: ServerTarget) { disk.used || null, disk.percent || null, now - ) + ]) } } // server_containers INSERT (배열) if (Array.isArray(docker) && docker.length > 0) { console.log(`[${now}] 🐳 [${target.server_name}] container 저장 (${docker.length}개 컨테이너)`) - const containerStmt = db.prepare(` - INSERT INTO server_containers ( - target_id, docker_id, container_name, container_image, - container_status, cpu_percent, memory_usage, memory_limit, - memory_percent, uptime, network_rx, network_tx, collected_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `) for (const container of docker) { - containerStmt.run( + await execute(` + INSERT INTO server_containers ( + target_id, docker_id, container_name, container_image, + container_status, cpu_percent, memory_usage, memory_limit, + memory_percent, uptime, network_rx, network_tx, collected_at + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) + `, [ target.target_id, container.id || null, container.name || null, @@ -509,22 +538,21 @@ async function collectServerData(target: ServerTarget) { container.network?.rx ?? container.network_rx ?? null, container.network?.tx ?? container.network_tx ?? null, now - ) + ]) } } // server_networks INSERT (배열) if (Array.isArray(network) && network.length > 0) { console.log(`[${now}] 🌐 [${target.server_name}] network 저장 (${network.length}개 인터페이스)`) - const netStmt = db.prepare(` - INSERT INTO server_networks ( - target_id, interface_name, bytes_recv, bytes_sent, - packets_recv, packets_sent, speed_recv, speed_sent, - is_up, collected_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `) for (const iface of network) { - netStmt.run( + await execute(` + INSERT INTO server_networks ( + target_id, interface_name, bytes_recv, bytes_sent, + packets_recv, packets_sent, speed_recv, speed_sent, + is_up, collected_at + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) + `, [ target.target_id, iface.interface_name || null, iface.bytes_recv || iface.cumulative_rx || null, @@ -535,7 +563,7 @@ async function collectServerData(target: ServerTarget) { iface.bytes_sent_rate_per_sec || iface.tx || iface.bytes_sent_rate || null, iface.is_up ? 1 : 0, now - ) + ]) } } @@ -551,13 +579,14 @@ async function collectServerData(target: ServerTarget) { apiVersionCache.delete(target.target_id) // 오프라인 기록 - db.prepare(` + await execute(` INSERT INTO server_snapshots (target_id, is_online, collected_at) - VALUES (?, 0, ?) - `).run(target.target_id, now) + VALUES ($1, 0, $2) + `, [target.target_id, now]) } } + // 서버별 타이머 시작 function startServerTimer(target: ServerTarget) { const now = timestamp() @@ -591,7 +620,7 @@ function stopServerTimer(targetId: number) { } // 스케줄러 시작 (모든 활성 서버) -export function startServerScheduler() { +export async function startServerScheduler() { const now = timestamp() if (isRunning) { @@ -601,10 +630,9 @@ export function startServerScheduler() { console.log(`[${now}] 🚀 [Server Scheduler] ========== 스케줄러 시작 ==========`) - const db = getDb() - const targets = db.prepare(` + const targets = await query(` SELECT * FROM server_targets WHERE is_active = 1 - `).all() as ServerTarget[] + `) console.log(`[${now}] 📋 [Server Scheduler] 활성 서버: ${targets.length}개`) @@ -632,12 +660,11 @@ export function stopServerScheduler() { } // 스케줄러 상태 -export function getServerSchedulerStatus() { - const db = getDb() +export async function getServerSchedulerStatus() { const activeServers = serverTimers.size - const targets = db.prepare(` + const targets = await query(` SELECT * FROM server_targets WHERE is_active = 1 - `).all() as ServerTarget[] + `) return { is_running: isRunning, @@ -655,12 +682,11 @@ export function getServerSchedulerStatus() { } // 특정 서버 타이머 갱신 (설정 변경 시) -export function refreshServerTimer(targetId: number) { +export async function refreshServerTimer(targetId: number) { const now = timestamp() - const db = getDb() - const target = db.prepare(` - SELECT * FROM server_targets WHERE target_id = ? AND is_active = 1 - `).get(targetId) as ServerTarget | undefined + const target = await queryOne(` + SELECT * FROM server_targets WHERE target_id = $1 AND is_active = 1 + `, [targetId]) if (target && isRunning) { console.log(`[${now}] 🔄 [${target.server_name}] 타이머 갱신`) diff --git a/nuxt.config.ts b/nuxt.config.ts index 34f3f1f..fbd4bd8 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -53,7 +53,7 @@ export default defineNuxtConfig({ }, // rollup에서 external로 처리 rollupConfig: { - external: ['better-sqlite3'] + external: ['pg'] }, // 플러그인 등록 plugins: [ @@ -65,7 +65,22 @@ export default defineNuxtConfig({ // Vite 설정 (네이티브 모듈) vite: { optimizeDeps: { - exclude: ['better-sqlite3'] + exclude: ['pg'] + } + }, + + // 환경변수 설정 + runtimeConfig: { + // 서버 전용 (NUXT_로 시작하는 환경변수 자동 로드) + dbHost: process.env.DB_HOST || 'localhost', + dbPort: process.env.DB_PORT || '5432', + dbName: process.env.DB_NAME || 'osolit_monitor', + dbUser: process.env.DB_USER || 'postgres', + dbPassword: process.env.DB_PASSWORD || '', + autoStartScheduler: process.env.AUTO_START_SCHEDULER || 'false', + // 클라이언트 공개 + public: { + nodeEnv: process.env.NODE_ENV || 'development' } } }) diff --git a/package-lock.json b/package-lock.json index 5bb9183..f9a0e01 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,16 +9,16 @@ "version": "1.0.0", "hasInstallScript": true, "dependencies": { - "better-sqlite3": "^11.0.0", "chart.js": "^4.5.1", "nuxt": "^3.13.0", + "pg": "^8.11.0", "vue": "^3.4.0", "vue-router": "^4.4.0" }, "devDependencies": { "@nuxt/devtools": "latest", - "@types/better-sqlite3": "^7.6.11", "@types/node": "^20.0.0", + "@types/pg": "^8.11.0", "typescript": "^5.3.0" } }, @@ -3188,16 +3188,6 @@ "tslib": "^2.4.0" } }, - "node_modules/@types/better-sqlite3": { - "version": "7.6.13", - "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", - "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -3221,6 +3211,18 @@ "integrity": "sha512-LriObC2+KYZD3FzCrgWGv/qufdUy4eXrxcLgQMfYXgPbLIecKIsVBaQgUPmxSSLcjmYbDTQbMgr6qr6l/eb7Bg==", "license": "MIT" }, + "node_modules/@types/pg": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.16.0.tgz", + "integrity": "sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, "node_modules/@types/resolve": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", @@ -3889,18 +3891,6 @@ "baseline-browser-mapping": "dist/cli.js" } }, - "node_modules/better-sqlite3": { - "version": "11.10.0", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", - "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", - "hasInstallScript": true, - "license": "MIT", - "peer": true, - "dependencies": { - "bindings": "^1.5.0", - "prebuild-install": "^7.1.1" - } - }, "node_modules/bindings": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", @@ -3924,6 +3914,7 @@ "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", "license": "MIT", + "optional": true, "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", @@ -3949,6 +3940,7 @@ } ], "license": "MIT", + "optional": true, "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" @@ -3959,6 +3951,7 @@ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "license": "MIT", + "optional": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -4746,6 +4739,7 @@ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", "license": "MIT", + "optional": true, "dependencies": { "mimic-response": "^3.1.0" }, @@ -4761,6 +4755,7 @@ "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "license": "MIT", + "optional": true, "engines": { "node": ">=4.0.0" } @@ -5006,6 +5001,7 @@ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", "license": "MIT", + "optional": true, "dependencies": { "once": "^1.4.0" } @@ -5194,6 +5190,7 @@ "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", "license": "(MIT OR WTFPL)", + "optional": true, "engines": { "node": ">=6" } @@ -5339,7 +5336,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/fsevents": { "version": "2.3.3", @@ -5449,7 +5447,8 @@ "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/glob": { "version": "10.5.0", @@ -6350,6 +6349,7 @@ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", "license": "MIT", + "optional": true, "engines": { "node": ">=10" }, @@ -6377,6 +6377,7 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "license": "MIT", + "optional": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -6412,7 +6413,8 @@ "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/mlly": { "version": "1.8.0", @@ -6498,7 +6500,8 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/nitropack": { "version": "2.12.9", @@ -7099,6 +7102,7 @@ "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.85.0.tgz", "integrity": "sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==", "license": "MIT", + "optional": true, "dependencies": { "semver": "^7.3.5" }, @@ -7424,6 +7428,7 @@ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "license": "ISC", + "optional": true, "dependencies": { "wrappy": "1" } @@ -7700,6 +7705,96 @@ "integrity": "sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow==", "license": "MIT" }, + "node_modules/pg": { + "version": "8.16.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", + "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", + "license": "MIT", + "peer": true, + "dependencies": { + "pg-connection-string": "^2.9.1", + "pg-pool": "^3.10.1", + "pg-protocol": "^1.10.3", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.2.7" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz", + "integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", + "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz", + "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz", + "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -8206,11 +8301,51 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/prebuild-install": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", "license": "MIT", + "optional": true, "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", @@ -8237,6 +8372,7 @@ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "license": "Apache-2.0", + "optional": true, "engines": { "node": ">=8" } @@ -8292,6 +8428,7 @@ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", "license": "MIT", + "optional": true, "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -8362,6 +8499,7 @@ "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "optional": true, "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", @@ -8376,7 +8514,8 @@ "version": "1.3.8", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "license": "ISC" + "license": "ISC", + "optional": true }, "node_modules/rc9": { "version": "2.1.2", @@ -8820,7 +8959,8 @@ "url": "https://feross.org/support" } ], - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/simple-get": { "version": "4.0.1", @@ -8841,6 +8981,7 @@ } ], "license": "MIT", + "optional": true, "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", @@ -8946,6 +9087,15 @@ "node": ">=0.10.0" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/srvx": { "version": "0.9.8", "resolved": "https://registry.npmjs.org/srvx/-/srvx-0.9.8.tgz", @@ -9112,6 +9262,7 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", "license": "MIT", + "optional": true, "engines": { "node": ">=0.10.0" } @@ -9284,6 +9435,7 @@ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", "license": "MIT", + "optional": true, "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", @@ -9295,13 +9447,15 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "license": "ISC" + "license": "ISC", + "optional": true }, "node_modules/tar-fs/node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "license": "MIT", + "optional": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -9316,6 +9470,7 @@ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", "license": "MIT", + "optional": true, "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", @@ -9459,6 +9614,7 @@ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", "license": "Apache-2.0", + "optional": true, "dependencies": { "safe-buffer": "^5.0.1" }, @@ -10476,7 +10632,8 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" + "license": "ISC", + "optional": true }, "node_modules/ws": { "version": "8.18.3", @@ -10514,6 +10671,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 4ca0f7f..61723b7 100644 --- a/package.json +++ b/package.json @@ -11,16 +11,16 @@ "postinstall": "nuxt prepare" }, "dependencies": { - "better-sqlite3": "^11.0.0", "chart.js": "^4.5.1", "nuxt": "^3.13.0", + "pg": "^8.11.0", "vue": "^3.4.0", "vue-router": "^4.4.0" }, "devDependencies": { "@nuxt/devtools": "latest", - "@types/better-sqlite3": "^7.6.11", "@types/node": "^20.0.0", + "@types/pg": "^8.11.0", "typescript": "^5.3.0" } }