로컬(개발)용과 운영용 설정분리

This commit is contained in:
2025-12-28 13:45:41 +09:00
parent a871ec8008
commit 716f4f8791
15 changed files with 661 additions and 368 deletions

View File

@@ -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' }
})

View File

@@ -1,5 +1,5 @@
import { getServerSchedulerStatus } from '../../utils/server-scheduler'
export default defineEventHandler(() => {
return getServerSchedulerStatus()
export default defineEventHandler(async () => {
return await getServerSchedulerStatus()
})

View File

@@ -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
})

View File

@@ -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 {

View File

@@ -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', () => {

View File

@@ -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', () => {

View File

@@ -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)')
}
})

View File

@@ -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<T = any>(sql: string, params?: any[]): Promise<T[]> {
const pool = getPool()
const result = await pool.query(sql, params)
return result.rows as T[]
}
db.exec(`
/**
* 단일 행 조회
*/
export async function queryOne<T = any>(sql: string, params?: any[]): Promise<T | null> {
const rows = await query<T>(sql, params)
return rows[0] || null
}
/**
* INSERT/UPDATE/DELETE 실행
*/
export async function execute(sql: string, params?: any[]): Promise<number> {
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<void> {
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<void> {
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<void> {
if (pool) {
await pool.end()
pool = null
console.log('[DB] PostgreSQL pool closed')
}
}

View File

@@ -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<CheckResult> {
const db = getDb()
// 활성화된 타겟 목록 조회
const targets = db.prepare(`
const targets = await query<PrivnetTarget>(`
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<PrivnetStatus>('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<void> {
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<void> {
// 활성 타겟 수 조회
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<void> {
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])
}
}

View File

@@ -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<CheckResult> {
const db = getDb()
// 활성화된 타겟 목록 조회
const targets = db.prepare(`
const targets = await query<PubnetTarget>(`
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<PubnetStatus>('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<void> {
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<void> {
// 활성 타겟 수 조회
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<void> {
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])
}
}

View File

@@ -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<st
// 이상감지 실행
async function detectAnomalies(targetId: number, serverName: string) {
const db = getDb()
const now = timestamp()
try {
// === 단기 변화율 감지 ===
const SHORT_TERM_THRESHOLD = 30
const snapshots = db.prepare(`
const snapshots = await query<any>(`
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<any>(`
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<any>(`
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<any>(`
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<any>(`
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<ServerTarget>(`
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<ServerTarget>(`
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<ServerTarget>(`
SELECT * FROM server_targets WHERE target_id = $1 AND is_active = 1
`, [targetId])
if (target && isRunning) {
console.log(`[${now}] 🔄 [${target.server_name}] 타이머 갱신`)