From 267b7575abbf3671dc1866caaab902603038d597 Mon Sep 17 00:00:00 2001 From: deploy-migration Date: Thu, 2 Jul 2026 12:54:02 +0000 Subject: [PATCH] sync server.js from production (entitlements-dirty IPC + SHARED-relative asset paths) --- server.js | 40 ++++++++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/server.js b/server.js index 8153da8..bd18180 100644 --- a/server.js +++ b/server.js @@ -8,7 +8,7 @@ const compression = require('compression'); // Load environment variables if dotenv is available try { - require('dotenv').config({ path: process.env.DOTENV_PATH || require('path').join(__dirname, '.env') }); + require('dotenv').config({ path: require('path').join(__dirname, '..', '.env') }); } catch (err) { // dotenv not installed, continue without it (production uses env vars) } @@ -96,11 +96,24 @@ if (!STORAGE_ROOT) { } const REPLAYS_ROOT = path.join(STORAGE_ROOT, 'REPLAYS'); fs.mkdirSync(REPLAYS_ROOT, { recursive: true }); -const LEGACY_REPLAYS_ROOT = process.env.LEGACY_REPLAYS_ROOT || path.join(__dirname, 'replays'); +const LEGACY_REPLAYS_ROOT = path.join(__dirname, '..', 'replays'); const ENTITLEMENTS_DB_PATH = path.join(STORAGE_ROOT, 'entitlements.db'); +const ENTITLEMENTS_DIRTY_PATH = path.join(STORAGE_ROOT, 'entitlements.dirty'); const SQUADRONS_DB_PATH = path.join(STORAGE_ROOT, 'squadrons.db'); const SQUADRONS_JSON_PATH = path.join(STORAGE_ROOT, 'SQUADRONS.json'); const entitlementsDb = new sqlite3.Database(ENTITLEMENTS_DB_PATH); + +// Bump the mtime on a marker file so the bot process (which caches +// entitlements in memory, see BOT/utils.py refresh_entitled_guilds) picks up +// writes to guild_entitlements within a few seconds instead of waiting for +// its hourly fallback refresh. The bot polls this file's mtime rather than +// us calling into it directly, since the bot and web/webhook run as +// separate pm2 processes with no other IPC between them. +function touchEntitlementsDirty() { + fs.writeFile(ENTITLEMENTS_DIRTY_PATH, String(Date.now()), (err) => { + if (err) log.error('[WHOP] Failed to write entitlements dirty flag:', err); + }); +} const squadronsDb = fs.existsSync(SQUADRONS_DB_PATH) ? new sqlite3.Database(SQUADRONS_DB_PATH, sqlite3.OPEN_READONLY) : null; @@ -152,12 +165,13 @@ const log = { debug: (...args) => isDev && console.log('[DEBUG]', ...args) }; +const REPO_ROOT = path.join(__dirname, '..'); const SQUADRON_RECAP_CACHE_DIR = path.join(STORAGE_ROOT, 'RECAPS', 'squadrons'); const PLAYER_RECAP_CACHE_DIR = path.join(STORAGE_ROOT, 'RECAPS', 'players'); const RECAP_TTL_MS = 24 * 60 * 60 * 1000; // in-progress season TTL const RECAP_RENDER_TIMEOUT_MS = 30_000; -const PYTHON_BIN = process.env.PYTHON_BIN || 'python3'; -const RECAP_SCRIPT = process.env.RECAP_SCRIPT || ''; +const PYTHON_BIN = path.join(REPO_ROOT, '..', 'SHARED', '.venv', 'bin', 'python'); +const RECAP_SCRIPT = path.join(REPO_ROOT, 'BOT', 'render_recap.py'); function resolveReplaySessionDir(sessionId) { const sid = String(sessionId).toLowerCase(); @@ -1913,10 +1927,10 @@ app.get('/api/match/:sessionId/video', async (req, res) => { _videoRenderCount++; try { await new Promise((resolve, reject) => { - const pythonBin = PYTHON_BIN; + const pythonBin = path.join(__dirname, '..', '..', 'SHARED', '.venv', 'bin', 'python'); execFile(pythonBin, ['-m', 'BOT.render_replay', replayPath, videoPath], { timeout: 120000, - cwd: process.env.BOT_REPO_ROOT || __dirname + cwd: path.join(__dirname, '..') }, (error, stdout, stderr) => { if (error) { log.error('[Video] Generation stderr:', stderr); @@ -1976,10 +1990,10 @@ app.get('/api/match/:sessionId/replay-canvas', async (req, res) => { _canvasRenderCount++; try { await new Promise((resolve, reject) => { - const pythonBin = PYTHON_BIN; + const pythonBin = path.join(__dirname, '..', '..', 'SHARED', '.venv', 'bin', 'python'); execFile(pythonBin, ['-m', 'BOT.render_replay', replayPath, jsonPath], { timeout: 30000, - cwd: process.env.BOT_REPO_ROOT || __dirname + cwd: path.join(__dirname, '..') }, (error, stdout, stderr) => { if (error) { log.error('[ReplayCanvas] Generation stderr:', stderr); @@ -2012,7 +2026,7 @@ app.get('/api/icons/type/:name', (req, res) => { if (!name || !/^[a-zA-Z0-9_\-]+$/.test(name)) { return res.status(400).json({ error: 'Invalid icon name' }); } - const iconsBase = process.env.SHARED_ICONS_DIR || path.join(__dirname, 'ICONS'); + const iconsBase = path.join(__dirname, '..', '..', 'SHARED', 'ICONS'); // Check: ICONS/ → FALLBACKS/ → MINIS/ const candidates = [ path.join(iconsBase, name + '.png'), @@ -2039,7 +2053,7 @@ app.get('/api/match/minimap/:level', (req, res) => { if (!level || !/^[a-zA-Z0-9_]+$/.test(level)) { return res.status(400).json({ error: 'Invalid level name' }); } - const minimapsDir = process.env.SHARED_MINIMAPS_DIR || path.join(__dirname, 'MAPS', 'MINIMAPS'); + const minimapsDir = path.join(__dirname, '..', '..', 'SHARED', 'MAPS', 'MINIMAPS'); const names = req.query.type === 'full' ? [level + '.png', level + '_map.png'] : [level + '_tankmap.png', level + '.png', level + '_map.png']; @@ -2848,7 +2862,7 @@ app.post('/webhook/whop', (req, res) => { [guildId], (err) => { if (err) log.error(`[WHOP] DB write failed for guild ${guildId}:`, err); - else log.info(`[WHOP] Guild ${guildId} deactivated via ${reason}`); + else { log.info(`[WHOP] Guild ${guildId} deactivated via ${reason}`); touchEntitlementsDirty(); } res.status(200).json({ success: true, guild_id: guildId, action: reason }); } ); @@ -2873,7 +2887,7 @@ app.post('/webhook/whop', (req, res) => { [row.guild_id], (err2) => { if (err2) log.error(`[WHOP] DB write failed for guild ${row.guild_id}:`, err2); - else log.info(`[WHOP] Guild ${row.guild_id} deactivated via ${reason} (membership ${membershipId})`); + else { log.info(`[WHOP] Guild ${row.guild_id} deactivated via ${reason} (membership ${membershipId})`); touchEntitlementsDirty(); } res.status(200).json({ success: true, guild_id: row.guild_id, action: reason }); } ); @@ -2903,6 +2917,7 @@ app.post('/webhook/whop', (req, res) => { return res.status(500).json({ success: false, error: 'DB write failed' }); } log.info(`[WHOP] Guild ${guildId} activated (membership ${data.id}, tier=${tier}, product_or_plan=${whopTierId || 'unknown'})`); + touchEntitlementsDirty(); res.status(200).json({ success: true, guild_id: guildId, action: 'activated', tier }); } ); @@ -3078,6 +3093,7 @@ async function syncWhopEntitlements() { } } + if (activated > 0 || deactivated > 0) touchEntitlementsDirty(); console.log(`[WHOP-SYNC] Sync complete: ${activated} activated, ${deactivated} deactivated, ${unchanged} unchanged`); } catch (err) { console.error('[WHOP-SYNC] Sync failed:', err);