settings-site: phase 2 correctness fixes (proxy helper, /healthz, datetime-local min, health polling)
This commit is contained in:
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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'));
|
||||
|
||||
Reference in New Issue
Block a user