diff --git a/gmail-poll.js b/gmail-poll.js index ab9ef8f..92b63f1 100644 --- a/gmail-poll.js +++ b/gmail-poll.js @@ -43,7 +43,7 @@ async function poll(client) { try { pollCount++; if (pollCount % 10 === 0) { - if (totalProcessed > 0) { + if (totalProcessed > 0 || totalSkipped > 0 || totalErrors > 0) { logAutomation('Gmail poll summary', null, `polls: ${pollCount}, processed: ${totalProcessed}, skipped: ${totalSkipped}, errors: ${totalErrors}`).catch(() => {}); } pollCount = 0; totalProcessed = 0; totalSkipped = 0; totalErrors = 0; diff --git a/handlers/buttons.js b/handlers/buttons.js index 7d2ebdc..df847a5 100644 --- a/handlers/buttons.js +++ b/handlers/buttons.js @@ -341,11 +341,7 @@ async function handleClaim(interaction, ticket) { if (renameInfo.ok) { const state = freshTicket.escalated ? 'escalated-claimed' : 'claimed'; const newName = makeTicketName(state, freshTicket, creatorNickname, claimerEmoji); - try { - await enqueueRename(interaction.channel, newName); - } catch (e) { - console.error('Rename error (claim):', e); - } + enqueueRename(interaction.channel, newName).catch(() => {}); } else { const unlockAtMs = Date.now() + renameInfo.waitMs; const unlockAtUnix = Math.floor(unlockAtMs / 1000); @@ -399,11 +395,7 @@ async function handleClaim(interaction, ticket) { const unclaimState = (freshTicket.escalationTier ?? 0) >= 1 ? 'escalated' : 'unclaimed'; const renameInfo = await canRename(freshTicket); if (renameInfo.ok) { - try { - await enqueueRename(interaction.channel, makeTicketName(unclaimState, freshTicket, creatorNicknameUnclaim)); - } catch (e) { - console.error('Rename error (unclaim):', e); - } + enqueueRename(interaction.channel, makeTicketName(unclaimState, freshTicket, creatorNicknameUnclaim)).catch(() => {}); } else { const unlockAtMs = Date.now() + renameInfo.waitMs; const unlockAtUnix = Math.floor(unlockAtMs / 1000); diff --git a/handlers/commands.js b/handlers/commands.js index c0da1af..bc7c989 100644 --- a/handlers/commands.js +++ b/handlers/commands.js @@ -90,11 +90,7 @@ async function runEscalation(interaction, ticket, nextTier, reason) { const renameInfo = await canRename(ticket); if (renameInfo.ok) { const newName = makeTicketName('escalated', ticket, creatorNickname); - try { - await enqueueRename(interaction.channel, newName); - } catch (e) { - console.error('Rename error (escalate):', e); - } + enqueueRename(interaction.channel, newName).catch(() => {}); } else { const unlockAtMs = Date.now() + renameInfo.waitMs; const unlockAtUnix = Math.floor(unlockAtMs / 1000); @@ -204,11 +200,7 @@ async function runDeescalation(interaction, ticket) { const state = newTier === 0 ? 'unclaimed' : 'escalated'; const renameInfo = await canRename(ticket); if (renameInfo.ok) { - try { - await enqueueRename(interaction.channel, makeTicketName(state, ticket, creatorNickname)); - } catch (e) { - console.error('Rename error (deescalate):', e); - } + enqueueRename(interaction.channel, makeTicketName(state, ticket, creatorNickname)).catch(() => {}); } else { const unlockAtMs = Date.now() + renameInfo.waitMs; const unlockAtUnix = Math.floor(unlockAtMs / 1000); diff --git a/services/channelQueue.js b/services/channelQueue.js index 7066ec0..7cf153e 100644 --- a/services/channelQueue.js +++ b/services/channelQueue.js @@ -6,8 +6,9 @@ const RENAME_WINDOW_MS = 9 * 60 * 1000; const RENAME_LIMIT = 2; +const { logWarn } = require('../services/debugLog'); -// Per-channel state: { count, windowStart, queue: [{newName, resolve, reject}], processing } +// Per-channel state: { count, windowStart, queue: [{newName, started}], processing } const renameState = new Map(); function getOrInitState(channelId) { @@ -35,18 +36,17 @@ function processQueue(channel, state) { state.processing = false; // New window if (state.queue.length > 3) { - const { logWarn } = require('../services/debugLog'); logWarn('renameQueue', `Channel ${channel.name} has ${state.queue.length} renames queued`).catch(() => {}); } const item = state.queue.shift(); if (!item) return; + item.started = true; state.count = 1; state.windowStart = Date.now(); try { await executeRename(channel, item.newName); - item.resolve(); } catch (err) { - item.reject(err); + logWarn('renameQueue', `Queued rename failed for ${channel.name}: ${err.message || err}`).catch(() => {}); } // Continue processing remaining queue items processQueue(channel, state); @@ -55,29 +55,42 @@ function processQueue(channel, state) { } function enqueueRename(channel, newName) { - return new Promise((resolve, reject) => { - const state = getOrInitState(channel.id); - const now = Date.now(); + const state = getOrInitState(channel.id); + const now = Date.now(); - // Window expired — reset - if (now - state.windowStart >= RENAME_WINDOW_MS) { - state.count = 1; - state.windowStart = now; - executeRename(channel, newName).then(resolve).catch(reject); - return; - } + // Window expired — reset + if (now - state.windowStart >= RENAME_WINDOW_MS) { + state.count = 1; + state.windowStart = now; + executeRename(channel, newName).catch((err) => { + logWarn('renameQueue', `Immediate rename failed for ${channel.name}: ${err.message || err}`).catch(() => {}); + }); + return Promise.resolve(); + } - // Within window and under limit - if (state.count < RENAME_LIMIT) { - state.count++; - executeRename(channel, newName).then(resolve).catch(reject); - return; - } + // Within window and under limit + if (state.count < RENAME_LIMIT) { + state.count++; + executeRename(channel, newName).catch((err) => { + logWarn('renameQueue', `Immediate rename failed for ${channel.name}: ${err.message || err}`).catch(() => {}); + }); + return Promise.resolve(); + } - // At limit — queue it - state.queue.push({ newName, resolve, reject }); - processQueue(channel, state); - }); + // At limit — queue it + const queueSize = state.queue.length + 1; + const queuedItem = { newName, started: false }; + state.queue.push(queuedItem); + + // Only notify if this rename is still waiting after ~2s. + setTimeout(() => { + if (queuedItem.started) return; + const estMinutes = Math.max(1, Math.ceil((queueSize * RENAME_WINDOW_MS) / 60000)); + channel.send(`⏳ Channel will be renamed in ~${estMinutes} minute${estMinutes === 1 ? '' : 's'}.`).catch(() => {}); + }, 2000); + + processQueue(channel, state); + return Promise.resolve(); } function enqueueMove(channel, categoryId) { diff --git a/services/tickets.js b/services/tickets.js index 7a587d4..f532d09 100644 --- a/services/tickets.js +++ b/services/tickets.js @@ -613,7 +613,9 @@ async function reconcileDeletedTicketChannels(client) { console.error(`reconcileDeletedTicketChannels error for ${ticket.gmailThreadId}:`, err); } } - logAutomation('Reconcile run', null, `checked: ${checked}, reconciled: ${reconciled}`).catch(() => {}); + if (reconciled > 0) { + logAutomation('Reconcile run', null, `checked: ${checked}, reconciled: ${reconciled}`).catch(() => {}); + } return { checked, reconciled }; } diff --git a/settings-site/public/index.html b/settings-site/public/index.html index 86c9b4f..3a279ef 100644 --- a/settings-site/public/index.html +++ b/settings-site/public/index.html @@ -13,19 +13,19 @@ @@ -294,7 +294,7 @@

Logging

Log channel configuration (channels set in Channels section)

-

Log channels are configured in the Channels section. This section shows which logs are active based on configured channels.

+

Log channels are configured in the Channels section. This section shows which logs are active based on configured channels.

diff --git a/settings-site/public/js/app.js b/settings-site/public/js/app.js index e9a73ff..cad3c11 100644 --- a/settings-site/public/js/app.js +++ b/settings-site/public/js/app.js @@ -153,19 +153,6 @@ function setupSectionToggles() { header.closest('.section').classList.toggle('collapsed'); }); }); - // Sidebar navigation - document.querySelectorAll('.sidebar a').forEach(link => { - link.addEventListener('click', (e) => { - e.preventDefault(); - const target = document.getElementById(link.getAttribute('href').slice(1)); - if (target) { - target.scrollIntoView({ behavior: 'smooth', block: 'start' }); - target.classList.remove('collapsed'); - } - document.querySelectorAll('.sidebar a').forEach(l => l.classList.remove('active')); - link.classList.add('active'); - }); - }); } function markChanged(key, value) { @@ -435,4 +422,54 @@ function toHumanLabel(key) { .join(' '); } -document.addEventListener('DOMContentLoaded', init); +const ROUTES = { + '/': 's-core', + '/channels': 's-channels', + '/categories': 's-categories', + '/gmail': 's-gmail', + '/behavior': 's-behavior', + '/threads': 's-threads', + '/pins': 's-pins', + '/notifications': 's-notifications', + '/logging': 's-logging', + '/automation': 's-automation', + '/appearance': 's-appearance', + '/staff': 's-staff', + '/advanced': 's-advanced' +}; + +function navigate(path, updateHistory = true) { + const sectionId = ROUTES[path] || ROUTES['/']; + const normalizedPath = ROUTES[path] ? path : '/'; + if (updateHistory) history.pushState({}, '', normalizedPath); + + document.querySelectorAll('.section').forEach(section => { + section.classList.toggle('hidden', section.id !== sectionId); + }); + + document.querySelectorAll('.sidebar a').forEach(link => { + link.classList.toggle('active', link.getAttribute('href') === normalizedPath); + }); +} + +function setupSidebarRouting() { + const sidebar = document.querySelector('.sidebar'); + if (!sidebar) return; + + sidebar.addEventListener('click', e => { + const a = e.target.closest('a'); + if (!a) return; + e.preventDefault(); + navigate(a.getAttribute('href')); + }); + + window.addEventListener('popstate', () => { + navigate(location.pathname, false); + }); +} + +document.addEventListener('DOMContentLoaded', async () => { + setupSidebarRouting(); + await init(); + navigate(location.pathname, false); +}); diff --git a/settings-site/server.js b/settings-site/server.js index e294e25..8780e94 100644 --- a/settings-site/server.js +++ b/settings-site/server.js @@ -12,7 +12,7 @@ const ADMIN_PASSWORD = process.env.SETTINGS_ADMIN_PASSWORD; app.use(express.json()); app.use(express.urlencoded({ extended: true })); -app.use(express.static(path.join(__dirname, 'public'))); +app.use(express.static(path.join(__dirname, 'public'), { index: false })); app.use(session({ secret: SECRET || 'fallback-secret-change-me', resave: false, @@ -93,6 +93,10 @@ app.get('/api/restart/status', requireAuth, async (req, res) => { catch (e) { res.status(502).json({ error: 'Bot unreachable' }); } }); +app.get('*', requireAuth, (req, res) => { + res.sendFile(path.join(__dirname, 'public', 'index.html')); +}); + app.listen(PORT, '0.0.0.0', () => { console.log(`[settings] running on port ${PORT}`); });