queue
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -13,19 +13,19 @@
|
||||
<!-- Sidebar -->
|
||||
<nav class="sidebar">
|
||||
<div class="logo">Broccolini Settings</div>
|
||||
<a href="#s-core" class="active">Core</a>
|
||||
<a href="#s-channels">Channels</a>
|
||||
<a href="#s-categories">Categories</a>
|
||||
<a href="#s-gmail">Gmail</a>
|
||||
<a href="#s-behavior">Ticket Behavior</a>
|
||||
<a href="#s-threads">Staff Threads</a>
|
||||
<a href="#s-pins">Pin Messages</a>
|
||||
<a href="#s-notifications">Notifications</a>
|
||||
<a href="#s-logging">Logging</a>
|
||||
<a href="#s-automation">Automation</a>
|
||||
<a href="#s-appearance">Appearance</a>
|
||||
<a href="#s-staff">Staff</a>
|
||||
<a href="#s-advanced">Advanced</a>
|
||||
<a href="/" class="active">Core</a>
|
||||
<a href="/channels">Channels</a>
|
||||
<a href="/categories">Categories</a>
|
||||
<a href="/gmail">Gmail</a>
|
||||
<a href="/behavior">Ticket Behavior</a>
|
||||
<a href="/threads">Staff Threads</a>
|
||||
<a href="/pins">Pin Messages</a>
|
||||
<a href="/notifications">Notifications</a>
|
||||
<a href="/logging">Logging</a>
|
||||
<a href="/automation">Automation</a>
|
||||
<a href="/appearance">Appearance</a>
|
||||
<a href="/staff">Staff</a>
|
||||
<a href="/advanced">Advanced</a>
|
||||
</nav>
|
||||
|
||||
<!-- Top bar -->
|
||||
@@ -294,7 +294,7 @@
|
||||
<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">▼</span></div>
|
||||
<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>
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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}`);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user