From 3e2bf919e985101cd5fa95ef93aab3425f570741 Mon Sep 17 00:00:00 2001 From: indifferentketchup Date: Sat, 18 Apr 2026 16:32:37 +0000 Subject: [PATCH] settings-site: phase 2 correctness fixes (proxy helper, /healthz, datetime-local min, health polling) --- settings-site/public/js/app.js | 63 ++++++++++++++++++++++++++++++++-- settings-site/server.js | 62 ++++++++++++++++++++------------- 2 files changed, 99 insertions(+), 26 deletions(-) diff --git a/settings-site/public/js/app.js b/settings-site/public/js/app.js index 2c230ee..c0c6a14 100644 --- a/settings-site/public/js/app.js +++ b/settings-site/public/js/app.js @@ -224,7 +224,7 @@ async function saveConfig(mode) { }); showToast('Restart initiated.', 'warning'); } else if (mode === 'restart' && hasErrors) { - showToast('Restart cancelled due to save errors.', 'warning'); + showToast(`Restart cancelled — save returned errors: ${data.errors.join(', ')}`, 'warning'); } } catch (e) { showToast('Failed to save. Bot may be unreachable.', 'error'); @@ -233,11 +233,16 @@ async function saveConfig(mode) { } } +function formatLocalDateTime(d) { + const pad = n => String(n).padStart(2, '0'); + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`; +} + function openScheduleModal() { const modal = document.getElementById('schedule-modal'); modal.classList.remove('hidden'); const dt = document.getElementById('schedule-datetime'); - const min = new Date(Date.now() + 60000).toISOString().slice(0, 16); + const min = formatLocalDateTime(new Date(Date.now() + 60000)); dt.min = min; dt.value = min; } @@ -550,10 +555,64 @@ function setupMobileNav() { }); } +let healthPollHandle = null; + +function setBotStatus(online) { + const dot = document.getElementById('bot-status-dot'); + const text = document.getElementById('bot-status-text'); + if (!dot || !text) return; + dot.className = online ? 'dot online' : 'dot offline'; + text.textContent = online ? 'Connected' : 'Unreachable'; +} + +async function pollHealth() { + try { + const res = await fetch('/healthz', { credentials: 'same-origin' }); + if (res.ok) { + const data = await res.json(); + setBotStatus(Boolean(data.bot)); + } else { + setBotStatus(false); + } + } catch (_) { + setBotStatus(false); + } +} + +function scheduleNextHealthPoll() { + if (document.hidden) return; + healthPollHandle = setTimeout(async () => { + await pollHealth(); + scheduleNextHealthPoll(); + }, 20000); +} + +function startHealthPolling() { + if (healthPollHandle) clearTimeout(healthPollHandle); + scheduleNextHealthPoll(); +} + +function stopHealthPolling() { + if (healthPollHandle) { + clearTimeout(healthPollHandle); + healthPollHandle = null; + } +} + +function setupHealthPolling() { + document.addEventListener('visibilitychange', () => { + if (document.hidden) stopHealthPolling(); + else startHealthPolling(); + }); + window.addEventListener('pagehide', stopHealthPolling); + startHealthPolling(); +} + document.addEventListener('DOMContentLoaded', async () => { setupSidebarRouting(); setupActionButtons(); setupMobileNav(); await init(); navigate(location.pathname, false); + setupHealthPolling(); }); diff --git a/settings-site/server.js b/settings-site/server.js index 1bfb8aa..db75e23 100644 --- a/settings-site/server.js +++ b/settings-site/server.js @@ -115,8 +115,41 @@ async function callBot(method, apiPath, body) { return res.json(); } +function proxy(method, botPath) { + return async (req, res) => { + try { + const data = await callBot(method, botPath, method === 'POST' ? req.body : undefined); + res.json(data); + } catch (e) { + res.status(502).json({ error: 'Bot unreachable' }); + } + }; +} + +async function pingBot() { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), 2000); + try { + const r = await fetch(`${INTERNAL_URL}/config`, { + method: 'GET', + headers: { 'x-internal-secret': SECRET }, + signal: controller.signal + }); + return r.ok; + } catch (_) { + return false; + } finally { + clearTimeout(timer); + } +} + app.use(express.static(path.join(__dirname, 'public'), { index: false })); +app.get('/healthz', async (req, res) => { + const bot = await pingBot(); + res.json({ ok: true, bot }); +}); + app.get('/api/csrf-token', (req, res) => { const csrfToken = generateCsrfToken(req, res); res.json({ csrfToken }); @@ -145,30 +178,11 @@ app.get('/', requireAuth, (req, res) => { res.sendFile(path.join(__dirname, 'public', 'index.html')); }); -app.get('/api/config', apiLimiter, requireAuth, async (req, res) => { - try { res.json(await callBot('GET', '/config')); } - catch (e) { res.status(502).json({ error: 'Bot unreachable' }); } -}); - -app.post('/api/config', apiLimiter, requireAuth, async (req, res) => { - try { res.json(await callBot('POST', '/config', req.body)); } - catch (e) { res.status(502).json({ error: 'Bot unreachable' }); } -}); - -app.get('/api/discord/guild', apiLimiter, requireAuth, async (req, res) => { - try { res.json(await callBot('GET', '/discord/guild')); } - catch (e) { res.status(502).json({ error: 'Bot unreachable' }); } -}); - -app.post('/api/restart', apiLimiter, requireAuth, async (req, res) => { - try { res.json(await callBot('POST', '/restart', req.body)); } - catch (e) { res.status(502).json({ error: 'Bot unreachable' }); } -}); - -app.get('/api/restart/status', apiLimiter, requireAuth, async (req, res) => { - try { res.json(await callBot('GET', '/restart/status')); } - catch (e) { res.status(502).json({ error: 'Bot unreachable' }); } -}); +app.get('/api/config', apiLimiter, requireAuth, proxy('GET', '/config')); +app.post('/api/config', apiLimiter, requireAuth, proxy('POST', '/config')); +app.get('/api/discord/guild', apiLimiter, requireAuth, proxy('GET', '/discord/guild')); +app.post('/api/restart', apiLimiter, requireAuth, proxy('POST', '/restart')); +app.get('/api/restart/status', apiLimiter, requireAuth, proxy('GET', '/restart/status')); app.get('*', requireAuth, (req, res) => { res.sendFile(path.join(__dirname, 'public', 'index.html'));