This commit is contained in:
indifferentketchup
2026-04-09 09:49:19 -05:00
parent a4fb82620a
commit 7fff9192b4
8 changed files with 115 additions and 75 deletions

View File

@@ -43,7 +43,7 @@ async function poll(client) {
try { try {
pollCount++; pollCount++;
if (pollCount % 10 === 0) { 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(() => {}); logAutomation('Gmail poll summary', null, `polls: ${pollCount}, processed: ${totalProcessed}, skipped: ${totalSkipped}, errors: ${totalErrors}`).catch(() => {});
} }
pollCount = 0; totalProcessed = 0; totalSkipped = 0; totalErrors = 0; pollCount = 0; totalProcessed = 0; totalSkipped = 0; totalErrors = 0;

View File

@@ -341,11 +341,7 @@ async function handleClaim(interaction, ticket) {
if (renameInfo.ok) { if (renameInfo.ok) {
const state = freshTicket.escalated ? 'escalated-claimed' : 'claimed'; const state = freshTicket.escalated ? 'escalated-claimed' : 'claimed';
const newName = makeTicketName(state, freshTicket, creatorNickname, claimerEmoji); const newName = makeTicketName(state, freshTicket, creatorNickname, claimerEmoji);
try { enqueueRename(interaction.channel, newName).catch(() => {});
await enqueueRename(interaction.channel, newName);
} catch (e) {
console.error('Rename error (claim):', e);
}
} else { } else {
const unlockAtMs = Date.now() + renameInfo.waitMs; const unlockAtMs = Date.now() + renameInfo.waitMs;
const unlockAtUnix = Math.floor(unlockAtMs / 1000); const unlockAtUnix = Math.floor(unlockAtMs / 1000);
@@ -399,11 +395,7 @@ async function handleClaim(interaction, ticket) {
const unclaimState = (freshTicket.escalationTier ?? 0) >= 1 ? 'escalated' : 'unclaimed'; const unclaimState = (freshTicket.escalationTier ?? 0) >= 1 ? 'escalated' : 'unclaimed';
const renameInfo = await canRename(freshTicket); const renameInfo = await canRename(freshTicket);
if (renameInfo.ok) { if (renameInfo.ok) {
try { enqueueRename(interaction.channel, makeTicketName(unclaimState, freshTicket, creatorNicknameUnclaim)).catch(() => {});
await enqueueRename(interaction.channel, makeTicketName(unclaimState, freshTicket, creatorNicknameUnclaim));
} catch (e) {
console.error('Rename error (unclaim):', e);
}
} else { } else {
const unlockAtMs = Date.now() + renameInfo.waitMs; const unlockAtMs = Date.now() + renameInfo.waitMs;
const unlockAtUnix = Math.floor(unlockAtMs / 1000); const unlockAtUnix = Math.floor(unlockAtMs / 1000);

View File

@@ -90,11 +90,7 @@ async function runEscalation(interaction, ticket, nextTier, reason) {
const renameInfo = await canRename(ticket); const renameInfo = await canRename(ticket);
if (renameInfo.ok) { if (renameInfo.ok) {
const newName = makeTicketName('escalated', ticket, creatorNickname); const newName = makeTicketName('escalated', ticket, creatorNickname);
try { enqueueRename(interaction.channel, newName).catch(() => {});
await enqueueRename(interaction.channel, newName);
} catch (e) {
console.error('Rename error (escalate):', e);
}
} else { } else {
const unlockAtMs = Date.now() + renameInfo.waitMs; const unlockAtMs = Date.now() + renameInfo.waitMs;
const unlockAtUnix = Math.floor(unlockAtMs / 1000); const unlockAtUnix = Math.floor(unlockAtMs / 1000);
@@ -204,11 +200,7 @@ async function runDeescalation(interaction, ticket) {
const state = newTier === 0 ? 'unclaimed' : 'escalated'; const state = newTier === 0 ? 'unclaimed' : 'escalated';
const renameInfo = await canRename(ticket); const renameInfo = await canRename(ticket);
if (renameInfo.ok) { if (renameInfo.ok) {
try { enqueueRename(interaction.channel, makeTicketName(state, ticket, creatorNickname)).catch(() => {});
await enqueueRename(interaction.channel, makeTicketName(state, ticket, creatorNickname));
} catch (e) {
console.error('Rename error (deescalate):', e);
}
} else { } else {
const unlockAtMs = Date.now() + renameInfo.waitMs; const unlockAtMs = Date.now() + renameInfo.waitMs;
const unlockAtUnix = Math.floor(unlockAtMs / 1000); const unlockAtUnix = Math.floor(unlockAtMs / 1000);

View File

@@ -6,8 +6,9 @@
const RENAME_WINDOW_MS = 9 * 60 * 1000; const RENAME_WINDOW_MS = 9 * 60 * 1000;
const RENAME_LIMIT = 2; 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(); const renameState = new Map();
function getOrInitState(channelId) { function getOrInitState(channelId) {
@@ -35,18 +36,17 @@ function processQueue(channel, state) {
state.processing = false; state.processing = false;
// New window // New window
if (state.queue.length > 3) { if (state.queue.length > 3) {
const { logWarn } = require('../services/debugLog');
logWarn('renameQueue', `Channel ${channel.name} has ${state.queue.length} renames queued`).catch(() => {}); logWarn('renameQueue', `Channel ${channel.name} has ${state.queue.length} renames queued`).catch(() => {});
} }
const item = state.queue.shift(); const item = state.queue.shift();
if (!item) return; if (!item) return;
item.started = true;
state.count = 1; state.count = 1;
state.windowStart = Date.now(); state.windowStart = Date.now();
try { try {
await executeRename(channel, item.newName); await executeRename(channel, item.newName);
item.resolve();
} catch (err) { } catch (err) {
item.reject(err); logWarn('renameQueue', `Queued rename failed for ${channel.name}: ${err.message || err}`).catch(() => {});
} }
// Continue processing remaining queue items // Continue processing remaining queue items
processQueue(channel, state); processQueue(channel, state);
@@ -55,7 +55,6 @@ function processQueue(channel, state) {
} }
function enqueueRename(channel, newName) { function enqueueRename(channel, newName) {
return new Promise((resolve, reject) => {
const state = getOrInitState(channel.id); const state = getOrInitState(channel.id);
const now = Date.now(); const now = Date.now();
@@ -63,21 +62,35 @@ function enqueueRename(channel, newName) {
if (now - state.windowStart >= RENAME_WINDOW_MS) { if (now - state.windowStart >= RENAME_WINDOW_MS) {
state.count = 1; state.count = 1;
state.windowStart = now; state.windowStart = now;
executeRename(channel, newName).then(resolve).catch(reject); executeRename(channel, newName).catch((err) => {
return; logWarn('renameQueue', `Immediate rename failed for ${channel.name}: ${err.message || err}`).catch(() => {});
});
return Promise.resolve();
} }
// Within window and under limit // Within window and under limit
if (state.count < RENAME_LIMIT) { if (state.count < RENAME_LIMIT) {
state.count++; state.count++;
executeRename(channel, newName).then(resolve).catch(reject); executeRename(channel, newName).catch((err) => {
return; logWarn('renameQueue', `Immediate rename failed for ${channel.name}: ${err.message || err}`).catch(() => {});
});
return Promise.resolve();
} }
// At limit — queue it // At limit — queue it
state.queue.push({ newName, resolve, reject }); 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); processQueue(channel, state);
}); return Promise.resolve();
} }
function enqueueMove(channel, categoryId) { function enqueueMove(channel, categoryId) {

View File

@@ -613,7 +613,9 @@ async function reconcileDeletedTicketChannels(client) {
console.error(`reconcileDeletedTicketChannels error for ${ticket.gmailThreadId}:`, err); console.error(`reconcileDeletedTicketChannels error for ${ticket.gmailThreadId}:`, err);
} }
} }
if (reconciled > 0) {
logAutomation('Reconcile run', null, `checked: ${checked}, reconciled: ${reconciled}`).catch(() => {}); logAutomation('Reconcile run', null, `checked: ${checked}, reconciled: ${reconciled}`).catch(() => {});
}
return { checked, reconciled }; return { checked, reconciled };
} }

View File

@@ -13,19 +13,19 @@
<!-- Sidebar --> <!-- Sidebar -->
<nav class="sidebar"> <nav class="sidebar">
<div class="logo">Broccolini Settings</div> <div class="logo">Broccolini Settings</div>
<a href="#s-core" class="active">Core</a> <a href="/" class="active">Core</a>
<a href="#s-channels">Channels</a> <a href="/channels">Channels</a>
<a href="#s-categories">Categories</a> <a href="/categories">Categories</a>
<a href="#s-gmail">Gmail</a> <a href="/gmail">Gmail</a>
<a href="#s-behavior">Ticket Behavior</a> <a href="/behavior">Ticket Behavior</a>
<a href="#s-threads">Staff Threads</a> <a href="/threads">Staff Threads</a>
<a href="#s-pins">Pin Messages</a> <a href="/pins">Pin Messages</a>
<a href="#s-notifications">Notifications</a> <a href="/notifications">Notifications</a>
<a href="#s-logging">Logging</a> <a href="/logging">Logging</a>
<a href="#s-automation">Automation</a> <a href="/automation">Automation</a>
<a href="#s-appearance">Appearance</a> <a href="/appearance">Appearance</a>
<a href="#s-staff">Staff</a> <a href="/staff">Staff</a>
<a href="#s-advanced">Advanced</a> <a href="/advanced">Advanced</a>
</nav> </nav>
<!-- Top bar --> <!-- Top bar -->
@@ -294,7 +294,7 @@
<div class="section" id="s-logging"> <div class="section" id="s-logging">
<div class="section-header"><h2>Logging</h2><p>Log channel configuration (channels set in Channels section)</p><span class="chevron">&#9660;</span></div> <div class="section-header"><h2>Logging</h2><p>Log channel configuration (channels set in Channels section)</p><span class="chevron">&#9660;</span></div>
<div class="section-body"><div class="field-grid"> <div class="section-body"><div class="field-grid">
<div class="field full-width"><p style="color:var(--text-muted);font-size:13px;">Log channels are configured in the <a href="#s-channels" style="color:var(--accent);">Channels</a> section. This section shows which logs are active based on configured channels.</p></div> <div class="field full-width"><p style="color:var(--text-muted);font-size:13px;">Log channels are configured in the <a href="/channels" style="color:var(--accent);">Channels</a> section. This section shows which logs are active based on configured channels.</p></div>
</div></div> </div></div>
</div> </div>

View File

@@ -153,19 +153,6 @@ function setupSectionToggles() {
header.closest('.section').classList.toggle('collapsed'); 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) { function markChanged(key, value) {
@@ -435,4 +422,54 @@ function toHumanLabel(key) {
.join(' '); .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);
});

View File

@@ -12,7 +12,7 @@ const ADMIN_PASSWORD = process.env.SETTINGS_ADMIN_PASSWORD;
app.use(express.json()); app.use(express.json());
app.use(express.urlencoded({ extended: true })); 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({ app.use(session({
secret: SECRET || 'fallback-secret-change-me', secret: SECRET || 'fallback-secret-change-me',
resave: false, resave: false,
@@ -93,6 +93,10 @@ app.get('/api/restart/status', requireAuth, async (req, res) => {
catch (e) { res.status(502).json({ error: 'Bot unreachable' }); } 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', () => { app.listen(PORT, '0.0.0.0', () => {
console.log(`[settings] running on port ${PORT}`); console.log(`[settings] running on port ${PORT}`);
}); });