diff --git a/.env.example b/.env.example index 0a78d24..090d759 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,5 @@ AFFILIATE_SOURCE=GambleCodez +DEVICE=vps ADMIN_IDS=6668510825 BOT_TOKEN= DISCORD_CODE_GENERATION_IMAGE_URL=https://github.com/gamblecodezcom/Runewager/blob/38c1bbab2d954238829852981514ce92c49d8b10/images/discord_code_generation.png diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 409c0c7..3325f99 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -151,13 +151,16 @@ Mode: \`${DEPLOY_MODE}\` By: \`${{ github.actor }}\`" # --------------------------------------------------------------------------- - # Job 2: Deploy to VPS (skipped on dry-run) + # Job 2: Deploy to VPS — DISABLED + # Deployments are performed directly on the VPS by running deploy.sh. + # GitHub Actions must NEVER launch, restart, or host the bot. + # To deploy: SSH into the VPS and run: bash /var/www/html/Runewager/deploy.sh # --------------------------------------------------------------------------- deploy: - name: Deploy to VPS + name: Deploy to VPS (DISABLED — run deploy.sh on VPS directly) runs-on: ubuntu-latest needs: quality-gates - if: ${{ github.event_name != 'workflow_dispatch' || github.event.inputs.dry_run != 'true' }} + if: false # GitHub must never trigger VPS restarts or bot launches environment: name: production diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..4b54587 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# ============================================================= +# Runewager — VPS Deployment Script +# +# Run this directly on the VPS to deploy the latest code. +# GitHub Actions does NOT run this script — it is VPS-only. +# +# Usage: +# bash /var/www/html/Runewager/deploy.sh +# +# What it does: +# 1. git fetch + reset to origin/main (clean, no merge conflicts) +# 2. npm ci --omit=dev (install/update production deps) +# 3. systemctl restart runewager +# 4. Confirm service status +# ============================================================= +set -euo pipefail + +PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +APP_NAME="runewager" + +say() { printf '[%s] %s\n' "$APP_NAME" "$*"; } +warn() { printf '[%s][warn] %s\n' "$APP_NAME" "$*" >&2; } + +say "=== Runewager VPS Deployment ===" +say "Project: $PROJECT_DIR" +say "Date: $(date -u '+%Y-%m-%dT%H:%M:%SZ')" + +cd "$PROJECT_DIR" + +# --------------------------------------------------------- +# 1) Pull latest code from origin/main +# --------------------------------------------------------- +say "Step 1/4 — Fetching latest code from origin/main…" +git fetch --all +git reset --hard origin/main +say "Now at: $(git rev-parse --short HEAD)" + +# --------------------------------------------------------- +# 2) Install / update production dependencies +# --------------------------------------------------------- +say "Step 2/4 — Installing production dependencies…" +if [[ -f package-lock.json ]]; then + npm ci --omit=dev +else + npm install --omit=dev +fi +say "Dependencies installed." + +# --------------------------------------------------------- +# 3) Restart via systemctl +# --------------------------------------------------------- +say "Step 3/4 — Restarting bot via systemctl…" +if command -v systemctl >/dev/null 2>&1; then + systemctl restart "${APP_NAME}.service" + say "Bot restarted." +else + warn "systemctl not found. Restart manually: node $PROJECT_DIR/index.js" +fi + +# --------------------------------------------------------- +# 4) Confirm service status +# --------------------------------------------------------- +say "Step 4/4 — Confirming bot status…" +sleep 3 +ACTIVE="$(systemctl is-active "${APP_NAME}.service" 2>/dev/null || echo unknown)" +say "Service status: $ACTIVE" + +say "" +say "=== Deployment complete. ===" diff --git a/index.js b/index.js index 25f8a6c..3667184 100644 --- a/index.js +++ b/index.js @@ -47,6 +47,9 @@ const DEFAULT_BONUS_RULE = `Wager 3,000 SC in any rolling 7-day period → 30 SC // Channel username (or chat_id) for admin announcements via /announce const ANNOUNCE_CHANNEL = process.env.ANNOUNCE_CHANNEL || '@GambleCodezDrops'; +// Group username (or chat_id) for automatic silent tips +const TIPS_GROUP = process.env.TIPS_GROUP || '@GambleCodezPrizeHub'; + // ── Images ───────────────────────────────────────────────────────────────── // Images live in /images/ next to index.js. // If HOSTED_*_URL env vars are set they take priority (CDN / public URL). @@ -140,6 +143,37 @@ const giveawayStore = { counter: 1, }; +// ========================= +// Tips Store — rotating silent group posts +// ========================= +const DEFAULT_TIPS_LIST = [ + { id: 1, text: '💡 *Did you know?* Runewager gives *instant crypto redemptions* on all sweepstakes prizes. No delays, no stress — just claim and flex.', enabled: true }, + { id: 2, text: '👋 New here? Tap /start in the bot to unlock your *FREE SC bonuses*, link your account, and join our giveaways.', enabled: true }, + { id: 3, text: '🏆 Runewager\'s weekly leaderboard is *LIVE* — climb the ranks, earn bragging rights, and win real prizes. All 100% free to play.', enabled: true }, + { id: 4, text: '🔗 *Pro tip:* Linking your Runewager username in the bot unlocks bonuses, giveaways, and faster support.', enabled: true }, + { id: 5, text: '🎁 Want more SC? Join our Telegram channel for surprise drops, flash bonuses, and exclusive codes.', enabled: true }, + { id: 6, text: '🎮 Runewager features top providers like *Hacksaw*, *BGaming*, *Betsoft*, and more — all sweepstakes‑safe and free to play.', enabled: true }, + { id: 7, text: '🔒 Make sure you\'re Discord‑verified in the bot — it unlocks extra rewards and keeps your account secure.', enabled: true }, + { id: 8, text: '🎉 Tap *Giveaways* in the bot menu to join our SC giveaways with automatic eligibility checks.', enabled: true }, + { id: 9, text: '🌍 Runewager accepts players worldwide — no region blocks, no deposit requirements, just pure sweepstakes fun.', enabled: true }, + { id: 10, text: '💰 Your bonuses refresh often — check the bot for new user bonuses, 30 SC rewards, and seasonal promos.', enabled: true }, + { id: 11, text: '🤝 Want to help the community grow? Share the bot link with a friend — more players means bigger giveaways.', enabled: true }, + { id: 12, text: '❓ If you ever get stuck, tap *Help / Commands* in the bot for a full guide, tooltips, and troubleshooting.', enabled: true }, + { id: 13, text: '📱 Runewager\'s mini‑app loads instantly inside Telegram — or switch to browser mode in *Settings* if you prefer.', enabled: true }, + { id: 14, text: '👀 Stay active in the group — admins drop surprise SC codes and leaderboard boosts at random times.', enabled: true }, + { id: 15, text: '🆓 Reminder: Runewager is *100% free to play*. No deposits, no wagering, no risk — just sweepstakes fun and instant crypto prizes.', enabled: true }, +]; + +const tipsStore = { + tips: DEFAULT_TIPS_LIST.map((t) => ({ ...t })), + systemEnabled: true, + intervalHours: 4, + targetGroup: TIPS_GROUP, + nextTipId: 16, +}; + +let tipsTimerRef = null; + const dataDir = path.join(__dirname, 'data'); const promoHistoryFile = path.join(dataDir, 'promo-history.json'); const analyticsFile = path.join(dataDir, 'analytics.json'); @@ -287,6 +321,13 @@ function createRuntimeStateSnapshot() { running: Array.from(giveawayStore.running.entries()).map(([id, g]) => ([id, serializeGiveaway(g)])), history: Array.from(giveawayStore.history.entries()).map(([id, g]) => ([id, serializeGiveaway(g)])), }, + tipsStore: { + tips: tipsStore.tips, + systemEnabled: tipsStore.systemEnabled, + intervalHours: tipsStore.intervalHours, + targetGroup: tipsStore.targetGroup, + nextTipId: tipsStore.nextTipId, + }, }; } @@ -383,6 +424,20 @@ function loadRuntimeState() { } } } + + if (raw.tipsStore) { + if (Array.isArray(raw.tipsStore.tips) && raw.tipsStore.tips.length > 0) { + tipsStore.tips = raw.tipsStore.tips; + } + if (typeof raw.tipsStore.systemEnabled === 'boolean') tipsStore.systemEnabled = raw.tipsStore.systemEnabled; + if (typeof raw.tipsStore.intervalHours === 'number' && raw.tipsStore.intervalHours > 0) { + tipsStore.intervalHours = raw.tipsStore.intervalHours; + } + if (typeof raw.tipsStore.targetGroup === 'string' && raw.tipsStore.targetGroup) { + tipsStore.targetGroup = raw.tipsStore.targetGroup; + } + if (typeof raw.tipsStore.nextTipId === 'number') tipsStore.nextTipId = raw.tipsStore.nextTipId; + } } function persistRuntimeState() { @@ -5119,6 +5174,59 @@ bot.on('text', async (ctx) => { return; } + // Tips system: await new tip text + if (action.type === 'await_tip_add_text') { + if (!requireAdmin(ctx)) return; + if (!text) { + await ctx.reply('Tip text cannot be empty. Please send the tip you want to add.'); + return; + } + const newTip = { id: tipsStore.nextTipId, text, enabled: true }; + tipsStore.tips.push(newTip); + tipsStore.nextTipId += 1; + user.pendingAction = null; + persistRuntimeState(); + await ctx.reply(`Added as Tip #${newTip.id}.`); + return; + } + + // Tips system: await edited tip text + if (action.type === 'await_tip_edit_text') { + if (!requireAdmin(ctx)) return; + if (!text) { + await ctx.reply('Tip text cannot be empty. Please send the updated tip text.'); + return; + } + const tipId = action.data && action.data.tipId; + const tip = tipsStore.tips.find((t) => t.id === tipId); + if (!tip) { + user.pendingAction = null; + await ctx.reply('Tip not found — it may have been removed.'); + return; + } + tip.text = text; + user.pendingAction = null; + persistRuntimeState(); + await ctx.reply(`Tip #${tipId} updated.`); + return; + } + + // Tips system: await new interval (hours) + if (action.type === 'await_tip_settings_interval') { + if (!requireAdmin(ctx)) return; + const hours = Number(text); + if (!Number.isFinite(hours) || hours <= 0) { + await ctx.reply('Please send a valid number of hours (e.g. `4`).', { parse_mode: 'Markdown' }); + return; + } + tipsStore.intervalHours = hours; + user.pendingAction = null; + persistRuntimeState(); + startTipsScheduler(); // re-arm with new interval + await ctx.reply(`✅ Tip interval updated to every ${hours} hour(s). Scheduler restarted.`); + return; + } + // Admin announce: await announcement text if (action.type === 'await_announcement_text') { if (!requireAdmin(ctx)) return; @@ -5289,6 +5397,243 @@ bot.action('announce_send_channel', async (ctx) => { } }); +// ========================= +// Tips System — scheduler + admin commands + callbacks +// ========================= + +/** + * Start (or restart) the tips scheduler. + * Clears any existing timer then arms a fresh one using the current interval. + */ +function startTipsScheduler() { + if (tipsTimerRef) clearInterval(tipsTimerRef); + const ms = (tipsStore.intervalHours || 4) * 60 * 60 * 1000; + tipsTimerRef = setInterval(async () => { + if (!tipsStore.systemEnabled) return; + const enabled = tipsStore.tips.filter((t) => t.enabled); + if (!enabled.length) return; + const tip = enabled[Math.floor(Math.random() * enabled.length)]; + try { + await bot.telegram.sendMessage(tipsStore.targetGroup, tip.text, { + parse_mode: 'Markdown', + disable_notification: true, + }); + logEvent('info', 'Group tip posted', { tipId: tip.id }); + } catch (e) { + logEvent('warn', 'Failed to post group tip', { tipId: tip.id, error: e.message }); + } + }, ms); +} + +/** Build the Tips Manager dashboard keyboard */ +function tipsDashboardKeyboard() { + return Markup.inlineKeyboard([ + [Markup.button.callback('➕ Add Tip', 'tips_cmd_add'), Markup.button.callback('✏️ Edit Tip', 'tips_cmd_edit')], + [Markup.button.callback('❌ Remove Tip', 'tips_cmd_remove'), Markup.button.callback('🔁 Toggle System', 'tips_cmd_toggle')], + [Markup.button.callback('📋 View All Tips', 'tips_cmd_list'), Markup.button.callback('🧪 Test Random Tip', 'tips_cmd_test')], + [Markup.button.callback('⚙️ Settings', 'tips_cmd_settings')], + [Markup.button.callback('↩ Admin Menu', 'pamenu_back_admin')], + ]); +} + +/** Build a keyboard with one button per tip (for select-to-edit or select-to-remove) */ +function tipSelectKeyboard(action) { + const rows = []; + let row = []; + for (const tip of tipsStore.tips) { + row.push(Markup.button.callback(`#${tip.id}${tip.enabled ? '' : ' 🔇'}`, `${action}_${tip.id}`)); + if (row.length === 5) { rows.push(row); row = []; } + } + if (row.length) rows.push(row); + rows.push([Markup.button.callback('❌ Cancel', 'tips_select_cancel')]); + return Markup.inlineKeyboard(rows); +} + +/** Send (or re-send) the Tips Manager dashboard */ +async function sendTipsDashboard(ctx) { + const total = tipsStore.tips.length; + const enabled = tipsStore.tips.filter((t) => t.enabled).length; + const status = tipsStore.systemEnabled ? '🟢 Enabled' : '🔴 Disabled'; + const text = `📝 *Tips Manager*\n\nStatus: ${status}\nTotal Tips: ${total} (${enabled} active)\nInterval: every ${tipsStore.intervalHours}h\nTarget: ${tipsStore.targetGroup}`; + await ctx.reply(text, { parse_mode: 'Markdown', ...tipsDashboardKeyboard() }); +} + +// ── /tips /tiplist /tipadd /tipremove /tipedit /tiptoggle /tiptest /tipsettings ── + +async function handleTipsCommand(ctx) { + if (!requireAdmin(ctx)) return; + await sendTipsDashboard(ctx); +} + +bot.command('tips', handleTipsCommand); +bot.command('t', handleTipsCommand); +bot.command('tp', handleTipsCommand); + +bot.command('tiplist', async (ctx) => { + if (!requireAdmin(ctx)) return; + const lines = ['📋 *Current Tips:*\n']; + for (const tip of tipsStore.tips) { + const preview = tip.text.replace(/\*/g, '').slice(0, 80); + lines.push(`${tip.id}. ${tip.enabled ? '' : '🔇 '}${preview}${tip.text.length > 80 ? '…' : ''}`); + } + await ctx.reply(lines.join('\n'), { parse_mode: 'Markdown' }); +}); + +bot.command('tipadd', async (ctx) => { + if (!requireAdmin(ctx)) return; + const user = getUser(ctx); + clearPendingAction(user); + user.pendingAction = { type: 'await_tip_add_text' }; + await ctx.reply('Send the new tip text. HTML and Markdown are both allowed.'); +}); + +bot.command('tipremove', async (ctx) => { + if (!requireAdmin(ctx)) return; + if (!tipsStore.tips.length) { await ctx.reply('No tips to remove.'); return; } + await ctx.reply('Remove which tip?', tipSelectKeyboard('tip_remove')); +}); + +bot.command('tipedit', async (ctx) => { + if (!requireAdmin(ctx)) return; + if (!tipsStore.tips.length) { await ctx.reply('No tips to edit.'); return; } + await ctx.reply('Edit which tip?', tipSelectKeyboard('tip_edit_select')); +}); + +bot.command('tiptoggle', async (ctx) => { + if (!requireAdmin(ctx)) return; + tipsStore.systemEnabled = !tipsStore.systemEnabled; + persistRuntimeState(); + const status = tipsStore.systemEnabled ? '🟢 Tips System Enabled' : '🔴 Tips System Disabled'; + await ctx.reply(status); +}); + +bot.command('tiptest', async (ctx) => { + if (!requireAdmin(ctx)) return; + const enabled = tipsStore.tips.filter((t) => t.enabled); + if (!enabled.length) { await ctx.reply('No enabled tips to preview.'); return; } + const tip = enabled[Math.floor(Math.random() * enabled.length)]; + await ctx.reply(`Here is a random tip preview:\n\n${tip.text}`, { parse_mode: 'Markdown' }); +}); + +bot.command('tipsettings', async (ctx) => { + if (!requireAdmin(ctx)) return; + const text = `⚙️ *Tips Settings*\n\nInterval: every ${tipsStore.intervalHours} hours\nSilent mode: Always ON\nTarget group: ${tipsStore.targetGroup}\n\nTo change the interval, reply with the number of hours (e.g. \`4\`).`; + const user = getUser(ctx); + clearPendingAction(user); + user.pendingAction = { type: 'await_tip_settings_interval' }; + await ctx.reply(text, { parse_mode: 'Markdown' }); +}); + +// ── Tips Dashboard inline button actions ── + +bot.action('tips_cmd_add', async (ctx) => { + if (!requireAdmin(ctx)) return; + const user = getUser(ctx); + await ctx.answerCbQuery(); + clearPendingAction(user); + user.pendingAction = { type: 'await_tip_add_text' }; + await ctx.reply('Send the new tip text. HTML and Markdown are both allowed.'); +}); + +bot.action('tips_cmd_edit', async (ctx) => { + if (!requireAdmin(ctx)) return; + await ctx.answerCbQuery(); + if (!tipsStore.tips.length) { await ctx.reply('No tips to edit.'); return; } + await ctx.reply('Edit which tip?', tipSelectKeyboard('tip_edit_select')); +}); + +bot.action('tips_cmd_remove', async (ctx) => { + if (!requireAdmin(ctx)) return; + await ctx.answerCbQuery(); + if (!tipsStore.tips.length) { await ctx.reply('No tips to remove.'); return; } + await ctx.reply('Remove which tip?', tipSelectKeyboard('tip_remove')); +}); + +bot.action('tips_cmd_toggle', async (ctx) => { + if (!requireAdmin(ctx)) return; + await ctx.answerCbQuery(); + tipsStore.systemEnabled = !tipsStore.systemEnabled; + persistRuntimeState(); + const status = tipsStore.systemEnabled ? '🟢 Tips System Enabled' : '🔴 Tips System Disabled'; + await ctx.reply(status); + await sendTipsDashboard(ctx); +}); + +bot.action('tips_cmd_list', async (ctx) => { + if (!requireAdmin(ctx)) return; + await ctx.answerCbQuery(); + const lines = ['📋 *Current Tips:*\n']; + for (const tip of tipsStore.tips) { + const preview = tip.text.replace(/\*/g, '').slice(0, 80); + lines.push(`${tip.id}. ${tip.enabled ? '' : '🔇 '}${preview}${tip.text.length > 80 ? '…' : ''}`); + } + await ctx.reply(lines.join('\n'), { parse_mode: 'Markdown' }); +}); + +bot.action('tips_cmd_test', async (ctx) => { + if (!requireAdmin(ctx)) return; + await ctx.answerCbQuery(); + const enabled = tipsStore.tips.filter((t) => t.enabled); + if (!enabled.length) { await ctx.reply('No enabled tips to preview.'); return; } + const tip = enabled[Math.floor(Math.random() * enabled.length)]; + await ctx.reply(`Here is a random tip preview:\n\n${tip.text}`, { parse_mode: 'Markdown' }); +}); + +bot.action('tips_cmd_settings', async (ctx) => { + if (!requireAdmin(ctx)) return; + await ctx.answerCbQuery(); + const user = getUser(ctx); + clearPendingAction(user); + user.pendingAction = { type: 'await_tip_settings_interval' }; + const text = `⚙️ *Tips Settings*\n\nInterval: every ${tipsStore.intervalHours} hours\nSilent mode: Always ON\nTarget group: ${tipsStore.targetGroup}\n\nSend the new interval in hours (e.g. \`4\`).`; + await ctx.reply(text, { parse_mode: 'Markdown' }); +}); + +bot.action('tips_select_cancel', async (ctx) => { + if (!requireAdmin(ctx)) return; + const user = getUser(ctx); + await ctx.answerCbQuery('Cancelled'); + clearPendingAction(user); + await ctx.reply('Cancelled.'); +}); + +// tip_remove_ +bot.action(/^tip_remove_(\d+)$/, async (ctx) => { + if (!requireAdmin(ctx)) return; + await ctx.answerCbQuery(); + const tipId = Number(ctx.match[1]); + const idx = tipsStore.tips.findIndex((t) => t.id === tipId); + if (idx === -1) { await ctx.reply('Tip not found.'); return; } + tipsStore.tips.splice(idx, 1); + persistRuntimeState(); + await ctx.reply(`Tip #${tipId} removed.`); +}); + +// tip_edit_select_ +bot.action(/^tip_edit_select_(\d+)$/, async (ctx) => { + if (!requireAdmin(ctx)) return; + await ctx.answerCbQuery(); + const tipId = Number(ctx.match[1]); + const tip = tipsStore.tips.find((t) => t.id === tipId); + if (!tip) { await ctx.reply('Tip not found.'); return; } + const user = getUser(ctx); + clearPendingAction(user); + user.pendingAction = { type: 'await_tip_edit_text', data: { tipId } }; + await ctx.reply(`Send the updated text for Tip #${tipId}.\n\nCurrent text:\n${tip.text}`); +}); + +// tip_toggle_ (per-tip enable/disable, accessible from tiplist) +bot.action(/^tip_toggle_(\d+)$/, async (ctx) => { + if (!requireAdmin(ctx)) return; + await ctx.answerCbQuery(); + const tipId = Number(ctx.match[1]); + const tip = tipsStore.tips.find((t) => t.id === tipId); + if (!tip) { await ctx.reply('Tip not found.'); return; } + tip.enabled = !tip.enabled; + persistRuntimeState(); + await ctx.reply(`Tip #${tipId} is now ${tip.enabled ? '✅ enabled' : '🔇 disabled'}.`); +}); + // ========================= // Giveaway Wizard action handlers // ========================= @@ -6645,54 +6990,76 @@ async function startBot() { for (const adminId of ADMIN_IDS) { bot.telegram.sendMessage(adminId, onlineMsg, { parse_mode: 'Markdown', disable_notification: true }).catch(() => {}); } + + // Start the rotating group tips scheduler + startTipsScheduler(); + logEvent('info', 'Tips scheduler started', { intervalHours: tipsStore.intervalHours, target: tipsStore.targetGroup }); } catch (e) { logEvent('error', 'Failed to launch bot', { error: e.message }); process.exit(1); } } -const healthServer = startHealthServer(); -startBot(); - -const WEEK_MS = 7 * 24 * 60 * 60 * 1000; -setInterval(() => { - runWeeklyWagerReminder().catch(() => { - // ignore reminder errors to keep bot alive - }); -}, WEEK_MS); +// ========================= +// Runtime environment guard +// +// GitHub CI → CI=true or DISABLE_RUNTIME=1 → log and skip (no process.exit so +// smoke tests can require() this file and verify it loads cleanly). +// Non-VPS → DEVICE !== "vps" → exit(0) immediately. +// Set DEVICE=vps in the VPS .env to allow the bot to start. +// +// All bot handler registrations above this block are pure in-memory operations +// and are safe to run in any environment. +// ========================= +if (process.env.CI === 'true' || process.env.DISABLE_RUNTIME === '1') { + console.log('CI mode detected — bot runtime disabled.'); +} else if (process.env.DEVICE !== 'vps') { + console.log('Bot runtime disabled — not running on VPS. Set DEVICE=vps in .env to enable.'); + process.exit(0); +} else { + const healthServer = startHealthServer(); + startBot(); + + const WEEK_MS = 7 * 24 * 60 * 60 * 1000; + setInterval(() => { + runWeeklyWagerReminder().catch(() => { + // ignore reminder errors to keep bot alive + }); + }, WEEK_MS); -setInterval(() => { - persistAnalytics(); - persistRuntimeState(); - buildDashboard(); -}, 60 * 1000); + setInterval(() => { + persistAnalytics(); + persistRuntimeState(); + buildDashboard(); + }, 60 * 1000); -setInterval(() => { - referralStore.weekly.clear(); -}, WEEK_MS); + setInterval(() => { + referralStore.weekly.clear(); + }, WEEK_MS); -setInterval(() => { - try { - if (fs.existsSync(runtimeStateFile)) { - const backupPath = createRuntimeBackup(); - logEvent('info', 'Runtime backup created', { backupPath }); + setInterval(() => { + try { + if (fs.existsSync(runtimeStateFile)) { + const backupPath = createRuntimeBackup(); + logEvent('info', 'Runtime backup created', { backupPath }); + } + } catch (e) { + logEvent('error', 'Runtime backup failed', { error: e.message }); } - } catch (e) { - logEvent('error', 'Runtime backup failed', { error: e.message }); - } -}, 24 * 60 * 60 * 1000); + }, 24 * 60 * 60 * 1000); -process.once('SIGINT', () => { - persistAnalytics(); - persistRuntimeState(); - buildDashboard(); - healthServer.close(); - bot.stop('SIGINT'); -}); -process.once('SIGTERM', () => { - persistAnalytics(); - persistRuntimeState(); - buildDashboard(); - healthServer.close(); - bot.stop('SIGTERM'); -}); + process.once('SIGINT', () => { + persistAnalytics(); + persistRuntimeState(); + buildDashboard(); + healthServer.close(); + bot.stop('SIGINT'); + }); + process.once('SIGTERM', () => { + persistAnalytics(); + persistRuntimeState(); + buildDashboard(); + healthServer.close(); + bot.stop('SIGTERM'); + }); +}