phase 5 dynamic alert registry (bot canonical, settings-site with fallback)
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
|
||||
const NOTIFICATION_PRESETS = ['15m', '30m', '1h', '2h', '4h', '8h', '1d'];
|
||||
|
||||
const NOTIFICATION_TAB_KEYS = {
|
||||
const FALLBACK_TAB_KEYS = {
|
||||
surge: [
|
||||
'surge_tickets',
|
||||
'surge_game',
|
||||
@@ -42,7 +42,7 @@
|
||||
chat: ['chat_messages', 'chat_time']
|
||||
};
|
||||
|
||||
const NOTIFICATION_ALERT_DESCRIPTIONS = {
|
||||
const FALLBACK_ALERT_DESCRIPTIONS = {
|
||||
surge_tickets: 'Fires when total active ticket volume exceeds configured surge thresholds, signaling broad queue pressure that needs staffing attention.',
|
||||
surge_game: 'Fires when one game accumulates tickets unusually fast within the configured window, indicating a localized incident that should be triaged.',
|
||||
surge_stale: 'Fires when too many tickets stay unresolved past the stale-time threshold, prompting staff to clear aging backlog.',
|
||||
@@ -80,6 +80,77 @@
|
||||
|
||||
let notificationThresholdsState = {};
|
||||
|
||||
// Active sources. Start as fallback; replaced/merged when the bot-side
|
||||
// registry (GET /api/notifications/alerts) returns successfully. On 404 or
|
||||
// network failure the fallbacks remain authoritative.
|
||||
let activeTabKeys = FALLBACK_TAB_KEYS;
|
||||
let activeAlertDescriptions = FALLBACK_ALERT_DESCRIPTIONS;
|
||||
|
||||
async function fetchAlertRegistry() {
|
||||
try {
|
||||
const res = await fetch('/api/notifications/alerts', { credentials: 'same-origin' });
|
||||
if (!res.ok) return null;
|
||||
const data = await res.json();
|
||||
if (!data || typeof data !== 'object' || Array.isArray(data)) return null;
|
||||
// Accept only if at least one known category is a non-empty array
|
||||
const hasShape = ['surge', 'patterns', 'unclaimed'].some(
|
||||
cat => Array.isArray(data[cat]) && data[cat].length > 0
|
||||
);
|
||||
return hasShape ? data : null;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Merge bot registry with fallback, preserving fallback order for existing
|
||||
// keys (so rapid_t2_t3 and chat keys stay where the UI expects them).
|
||||
// Registry-only keys get appended to their category; registry descriptions
|
||||
// override fallback text.
|
||||
function mergeRegistryWithFallback(registry) {
|
||||
const tabKeys = {};
|
||||
const alertDescriptions = { ...FALLBACK_ALERT_DESCRIPTIONS };
|
||||
Object.keys(FALLBACK_TAB_KEYS).forEach(cat => { tabKeys[cat] = [...FALLBACK_TAB_KEYS[cat]]; });
|
||||
|
||||
Object.entries(registry).forEach(([category, entries]) => {
|
||||
if (!Array.isArray(entries)) return;
|
||||
if (!tabKeys[category]) tabKeys[category] = [];
|
||||
const seen = new Set(tabKeys[category]);
|
||||
for (const e of entries) {
|
||||
if (!e || typeof e.key !== 'string') continue;
|
||||
if (!seen.has(e.key)) {
|
||||
tabKeys[category].push(e.key);
|
||||
seen.add(e.key);
|
||||
}
|
||||
if (typeof e.description === 'string') {
|
||||
alertDescriptions[e.key] = e.description;
|
||||
}
|
||||
}
|
||||
});
|
||||
return { tabKeys, alertDescriptions };
|
||||
}
|
||||
|
||||
function applyMergedRegistry(section, registry) {
|
||||
const merged = mergeRegistryWithFallback(registry);
|
||||
activeTabKeys = merged.tabKeys;
|
||||
activeAlertDescriptions = merged.alertDescriptions;
|
||||
window.Notifications.registry = registry;
|
||||
|
||||
Object.entries(activeTabKeys).forEach(([category, keys]) => {
|
||||
const select = section.querySelector(`[data-notif-category="${category}"]`);
|
||||
if (!select) return;
|
||||
const existing = new Set(Array.from(select.options).map(o => o.value));
|
||||
keys.forEach(key => {
|
||||
if (!existing.has(key)) {
|
||||
const option = document.createElement('option');
|
||||
option.value = key;
|
||||
option.textContent = toHumanLabel(key);
|
||||
select.appendChild(option);
|
||||
}
|
||||
});
|
||||
renderAlertDescription(category);
|
||||
});
|
||||
}
|
||||
|
||||
function initNotificationsEditor(config) {
|
||||
const section = document.getElementById('s-notifications');
|
||||
if (!section) return;
|
||||
@@ -94,7 +165,7 @@
|
||||
btn.addEventListener('click', () => setNotificationTab(btn.dataset.notifTab));
|
||||
});
|
||||
|
||||
Object.entries(NOTIFICATION_TAB_KEYS).forEach(([category, keys]) => {
|
||||
Object.entries(activeTabKeys).forEach(([category, keys]) => {
|
||||
const select = section.querySelector(`[data-notif-category="${category}"]`);
|
||||
const chipsWrap = section.querySelector(`[data-notif-chips="${category}"]`);
|
||||
const input = section.querySelector(`[data-notif-input="${category}"]`);
|
||||
@@ -136,6 +207,15 @@
|
||||
});
|
||||
|
||||
setNotificationTab('surge');
|
||||
|
||||
// Background: pull canonical registry from the bot, merge with fallback,
|
||||
// append any registry-only keys and refresh descriptions. Fallback stays
|
||||
// in use if the endpoint 404s (settings-site deployed ahead of bot) or
|
||||
// the fetch fails (network/proxy error).
|
||||
fetchAlertRegistry().then(registry => {
|
||||
if (!registry) return;
|
||||
applyMergedRegistry(section, registry);
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
function parseNotificationThresholdsConfig(config) {
|
||||
@@ -234,7 +314,7 @@
|
||||
const descriptionEl = document.querySelector(`#s-notifications [data-notif-description="${category}"]`);
|
||||
if (!descriptionEl) return;
|
||||
const alertKey = getSelectedAlertKey(category);
|
||||
descriptionEl.textContent = NOTIFICATION_ALERT_DESCRIPTIONS[alertKey] || 'No description available for this alert key yet.';
|
||||
descriptionEl.textContent = activeAlertDescriptions[alertKey] || 'No description available for this alert key yet.';
|
||||
}
|
||||
|
||||
function syncNotificationThresholdsField() {
|
||||
@@ -270,8 +350,12 @@
|
||||
initNotificationsEditor,
|
||||
isValidThresholdValue,
|
||||
toHumanLabel,
|
||||
fetchAlertRegistry,
|
||||
NOTIFICATION_PRESETS,
|
||||
NOTIFICATION_TAB_KEYS,
|
||||
NOTIFICATION_ALERT_DESCRIPTIONS
|
||||
FALLBACK_TAB_KEYS,
|
||||
FALLBACK_ALERT_DESCRIPTIONS,
|
||||
registry: null,
|
||||
get tabKeys() { return activeTabKeys; },
|
||||
get alertDescriptions() { return activeAlertDescriptions; }
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -182,6 +182,7 @@ 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('/api/notifications/alerts', apiLimiter, requireAuth, proxy('GET', '/notifications/alerts'));
|
||||
|
||||
app.get('/*splat', requireAuth, (req, res) => {
|
||||
res.sendFile(path.join(__dirname, 'public', 'index.html'));
|
||||
|
||||
Reference in New Issue
Block a user