diff --git a/server/src/core/persistence.ts b/server/src/core/persistence.ts index 04d7b10..a31e867 100644 --- a/server/src/core/persistence.ts +++ b/server/src/core/persistence.ts @@ -7,19 +7,45 @@ const backupFile = stateFile + '.bak'; const tmpFile = stateFile + '.tmp'; let state: Record = {}; +let lastSaveOk = false; export function loadState(): void { + console.log(`[State] DATA_DIR=${DATA_DIR}, stateFile=${stateFile}`); + + // Check if DATA_DIR exists and is writable + try { + fs.mkdirSync(DATA_DIR, { recursive: true }); + const testFile = path.join(DATA_DIR, '.write-test'); + fs.writeFileSync(testFile, 'ok'); + fs.unlinkSync(testFile); + console.log(`[State] DATA_DIR is writable`); + } catch (e) { + console.error(`[State] ⚠ DATA_DIR is NOT writable:`, e); + } + + // List existing files in DATA_DIR for diagnostics + try { + const files = fs.readdirSync(DATA_DIR); + console.log(`[State] Files in DATA_DIR: ${files.join(', ') || '(empty)'}`); + } catch (e) { + console.error(`[State] Cannot list DATA_DIR:`, e); + } + // Try main file first, fall back to backup if corrupted for (const file of [stateFile, backupFile]) { try { if (fs.existsSync(file)) { - state = JSON.parse(fs.readFileSync(file, 'utf-8')); + const raw = fs.readFileSync(file, 'utf-8'); + state = JSON.parse(raw); const keys = Object.keys(state); - console.log(`[State] Loaded from ${path.basename(file)} (${keys.length} keys: ${keys.join(', ')})`); + const favCount = Array.isArray(state.radio_favorites) ? state.radio_favorites.length : 0; + console.log(`[State] ✓ Loaded from ${path.basename(file)} (${keys.length} keys: ${keys.join(', ')})`); + console.log(`[State] radio_favorites: ${favCount} entries`); // Create backup on successful load from main file if (file === stateFile) { try { fs.copyFileSync(stateFile, backupFile); } catch {} } + lastSaveOk = true; return; } } catch (e) { @@ -32,12 +58,29 @@ export function loadState(): void { export function saveState(): void { try { fs.mkdirSync(path.dirname(stateFile), { recursive: true }); + const json = JSON.stringify(state, null, 2); // Atomic write: write to tmp file, then rename - // rename() is atomic on POSIX filesystems, prevents corruption on crash - fs.writeFileSync(tmpFile, JSON.stringify(state, null, 2)); + fs.writeFileSync(tmpFile, json); fs.renameSync(tmpFile, stateFile); + // Verify write by reading back + const verify = fs.readFileSync(stateFile, 'utf-8'); + if (verify.length !== json.length) { + console.error(`[State] ⚠ Write verification FAILED: wrote ${json.length} bytes, read back ${verify.length}`); + lastSaveOk = false; + } else { + lastSaveOk = true; + } } catch (e) { - console.error('[State] Failed to save:', e); + console.error('[State] ✗ Failed to save:', e); + lastSaveOk = false; + // Fallback: try direct write if rename failed + try { + fs.writeFileSync(stateFile, JSON.stringify(state, null, 2)); + console.log('[State] Fallback direct write succeeded'); + lastSaveOk = true; + } catch (e2) { + console.error('[State] ✗ Fallback write also failed:', e2); + } } } @@ -53,3 +96,33 @@ export function setState(key: string, value: any): void { export function getFullState(): Record { return { ...state }; } + +/** Diagnostic info for health endpoint */ +export function getStateDiag(): Record { + let fileExists = false; + let fileSize = 0; + let fileKeys: string[] = []; + let fileFavCount = 0; + try { + if (fs.existsSync(stateFile)) { + fileExists = true; + const stat = fs.statSync(stateFile); + fileSize = stat.size; + const parsed = JSON.parse(fs.readFileSync(stateFile, 'utf-8')); + fileKeys = Object.keys(parsed); + fileFavCount = Array.isArray(parsed.radio_favorites) ? parsed.radio_favorites.length : 0; + } + } catch {} + const memKeys = Object.keys(state); + const memFavCount = Array.isArray(state.radio_favorites) ? state.radio_favorites.length : 0; + return { + stateFile, + fileExists, + fileSize, + fileKeys, + fileFavCount, + memoryKeys: memKeys, + memoryFavCount: memFavCount, + lastSaveOk, + }; +} diff --git a/server/src/index.ts b/server/src/index.ts index d48adff..84bcbc5 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -3,7 +3,7 @@ import path from 'node:path'; import { Client } from 'discord.js'; import { createClient } from './core/discord.js'; import { addSSEClient, removeSSEClient, sseBroadcast, getSSEClientCount } from './core/sse.js'; -import { loadState, getFullState } from './core/persistence.js'; +import { loadState, getFullState, getStateDiag } from './core/persistence.js'; import { getPlugins, registerPlugin, getPluginCtx, type PluginContext } from './core/plugin.js'; import radioPlugin from './plugins/radio/index.js'; import soundboardPlugin from './plugins/soundboard/index.js'; @@ -77,6 +77,7 @@ app.get('/api/health', (_req, res) => { plugins: getPlugins().map(p => ({ name: p.name, version: p.version })), bots: clients.map(b => ({ name: b.name, user: b.client.user?.tag ?? 'offline', guilds: b.client.guilds.cache.size })), sseClients: getSSEClientCount(), + state: getStateDiag(), }); });