(function () { 'use strict'; const NOTIFICATION_PRESETS = ['15m', '30m', '1h', '2h', '4h', '8h', '1d']; const NOTIFICATION_TAB_KEYS = { surge: [ 'surge_tickets', 'surge_game', 'surge_stale', 'surge_needs_response', 'surge_unclaimed', 'surge_tier3_unclaimed', 'surge_no_staff' ], patterns: [ 'user_tickets', 'user_reopen', 'user_crossgame', 'game_surge', 'game_backlog', 'game_resolution', 'game_spike', 'tag_top', 'tag_escalation', 'untagged_closes', 'tag_game_corr', 'user_esc', 'game_esc_rate', 'rapid_t2_t3', 'staff_no_close', 'staff_overloaded', 'staff_stale', 'staff_transfer_rate', 'staff_esc', 'staff_game_esc', 'game_tag_spike', 'overnight_gap', 'staff_always_esc' ], unclaimed: ['unclaimed_reminder'], chat: ['chat_messages', 'chat_time'] }; const NOTIFICATION_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.', surge_needs_response: 'Fires when tickets needing a staff reply exceed count and age limits, indicating response latency is building.', surge_unclaimed: 'Fires when the unclaimed queue crosses configured count/age thresholds, signaling ownership gaps that need pickup.', surge_tier3_unclaimed: "Fires when Tier 3 tickets have been sitting unclaimed past each threshold. Escalating intervals prevent spam while ensuring critical tickets don't go unnoticed.", surge_no_staff: 'Fires when open-ticket load is high while no staff are detected as available, prompting immediate coverage.', user_tickets: 'Detects users opening unusually high ticket counts in the active window, suggesting repeat-issue or abuse patterns.', user_reopen: 'Detects users who repeatedly reopen or recreate issues after closure, signaling unresolved root-cause patterns.', user_crossgame: 'Detects users reporting similar issues across multiple games in a short period, indicating broader account-level impact.', game_surge: 'Detects game-specific ticket spikes crossing thresholds in the pattern window, signaling service instability for that title.', game_backlog: 'Detects games accumulating unresolved backlog above threshold, implying triage capacity is lagging for that queue.', game_resolution: 'Detects unusual drops in resolution rate for a game, indicating tickets are staying open longer than expected.', game_spike: 'Detects abrupt short-window jumps in ticket volume for a game, flagging incidents that may need escalation.', tag_top: 'Detects tag frequency leaders above threshold so recurring issue types can be prioritized for fixes or macros.', tag_escalation: 'Detects tags with unusually high escalation rates, indicating categories that routinely require higher-tier handling.', untagged_closes: 'Detects elevated counts of closed tickets without tags, prompting cleanup to preserve reporting quality.', tag_game_corr: 'Detects strong tag-to-game concentration patterns, highlighting issue types tightly linked to specific games.', user_esc: 'Detects users whose tickets escalate unusually often, implying complex cases that may need proactive follow-up.', game_esc_rate: 'Detects games with escalating ticket-rate thresholds exceeded, signaling deeper technical issues for that title.', rapid_t2_t3: 'Fires at ticket count milestones (e.g. 3, 5, 10) when tickets have reached Tier 3 this week. Each milestone fires once per week.', staff_no_close: 'Detects staff with prolonged periods of claims but few closes, suggesting overloaded ownership or stuck work.', staff_overloaded: 'Detects staff carrying ticket loads beyond threshold, indicating balancing or reassignment may be needed.', staff_stale: 'Detects staff-owned tickets aging beyond stale limits, prompting review and unblock actions.', staff_transfer_rate: 'Detects unusually high transfer/reassignment rates by staff, signaling ownership churn that may hurt throughput.', staff_esc: 'Detects staff escalation counts above threshold, highlighting where extra support or training may be needed.', staff_game_esc: 'Detects high escalation concentration for specific staff/game combinations, indicating targeted expertise gaps.', game_tag_spike: 'Detects sudden spikes of specific tags within a game, flagging focused incident signatures.', overnight_gap: 'Detects recurring unattended overnight windows with active demand, suggesting staffing coverage gaps.', staff_always_esc: 'Detects staff whose handled tickets escalate at consistently high rates, implying sustained tier-fit issues.', unclaimed_reminder: 'Reminds all staff notification channels about unclaimed tickets. Thresholds are per-ticket age — each threshold fires once per ticket and resets on escalation.', chat_messages: 'Fires when pending user message volume in monitored chat channels crosses configured count thresholds without staff replies.', chat_time: 'Fires when a monitored chat channel has had no staff response for the given duration with pending user messages. Resets when staff responds.' }; let notificationThresholdsState = {}; function initNotificationsEditor(config) { const section = document.getElementById('s-notifications'); if (!section) return; const hiddenField = section.querySelector('[data-key="NOTIFICATION_THRESHOLDS_JSON"]'); if (!hiddenField) return; notificationThresholdsState = parseNotificationThresholdsConfig(config); hiddenField.value = serializeNotificationThresholds(notificationThresholdsState); section.querySelectorAll('.notif-tab-btn').forEach(btn => { btn.addEventListener('click', () => setNotificationTab(btn.dataset.notifTab)); }); Object.entries(NOTIFICATION_TAB_KEYS).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}"]`); const addBtn = section.querySelector(`[data-notif-add="${category}"]`); const presetsWrap = section.querySelector(`[data-notif-presets="${category}"]`); if (!select || !chipsWrap || !input || !addBtn || !presetsWrap) return; keys.forEach(key => { const option = document.createElement('option'); option.value = key; option.textContent = toHumanLabel(key); select.appendChild(option); }); if (keys.length) select.value = keys[0]; select.addEventListener('change', () => { renderThresholdChips(category); renderAlertDescription(category); }); addBtn.addEventListener('click', () => addThresholdFromInput(category)); input.addEventListener('keydown', (evt) => { if (evt.key === 'Enter') { evt.preventDefault(); addThresholdFromInput(category); } }); NOTIFICATION_PRESETS.forEach(preset => { const btn = document.createElement('button'); btn.type = 'button'; btn.textContent = preset; btn.addEventListener('click', () => addThresholdValue(category, preset)); presetsWrap.appendChild(btn); }); renderThresholdChips(category); renderAlertDescription(category); }); setNotificationTab('surge'); } function parseNotificationThresholdsConfig(config) { const rawJson = config.NOTIFICATION_THRESHOLDS_JSON; if (rawJson && String(rawJson).trim()) { try { const parsed = JSON.parse(rawJson); if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed; } catch (_) {} } if (config.NOTIFICATION_THRESHOLDS && typeof config.NOTIFICATION_THRESHOLDS === 'object' && !Array.isArray(config.NOTIFICATION_THRESHOLDS)) { return config.NOTIFICATION_THRESHOLDS; } return {}; } function serializeNotificationThresholds(obj) { const ordered = {}; Object.keys(obj).sort().forEach(key => { const arr = Array.isArray(obj[key]) ? obj[key].map(v => String(v).trim()).filter(Boolean) : []; ordered[key] = arr; }); return JSON.stringify(ordered); } function setNotificationTab(category) { document.querySelectorAll('#s-notifications .notif-tab-btn').forEach(btn => { btn.classList.toggle('active', btn.dataset.notifTab === category); }); document.querySelectorAll('#s-notifications .notif-panel').forEach(panel => { panel.classList.toggle('hidden', panel.dataset.notifPanel !== category); }); } function addThresholdFromInput(category) { const input = document.querySelector(`#s-notifications [data-notif-input="${category}"]`); if (!input) return; const value = input.value.trim().toLowerCase(); if (addThresholdValue(category, value)) input.value = ''; } function addThresholdValue(category, rawValue) { const value = String(rawValue || '').trim().toLowerCase(); if (!isValidThresholdValue(value)) { Util.showToast('Invalid threshold format. Use 15m, 1h, 1d6h, or whole numbers.', 'error'); return false; } const alertKey = getSelectedAlertKey(category); if (!alertKey) return false; const current = Array.isArray(notificationThresholdsState[alertKey]) ? [...notificationThresholdsState[alertKey]] : []; if (current.includes(value)) return false; current.push(value); notificationThresholdsState[alertKey] = current; syncNotificationThresholdsField(); renderThresholdChips(category); return true; } function removeThresholdValue(category, valueToRemove) { const alertKey = getSelectedAlertKey(category); if (!alertKey) return; const current = Array.isArray(notificationThresholdsState[alertKey]) ? [...notificationThresholdsState[alertKey]] : []; notificationThresholdsState[alertKey] = current.filter(v => String(v) !== String(valueToRemove)); syncNotificationThresholdsField(); renderThresholdChips(category); } function renderThresholdChips(category) { const chipsWrap = document.querySelector(`#s-notifications [data-notif-chips="${category}"]`); if (!chipsWrap) return; const alertKey = getSelectedAlertKey(category); const thresholds = alertKey && Array.isArray(notificationThresholdsState[alertKey]) ? notificationThresholdsState[alertKey] : []; chipsWrap.replaceChildren(); thresholds.forEach(value => { const chip = document.createElement('span'); chip.className = 'notif-chip'; chip.textContent = value; const remove = document.createElement('button'); remove.type = 'button'; remove.title = `Remove ${value}`; remove.textContent = '×'; remove.addEventListener('click', () => removeThresholdValue(category, value)); chip.appendChild(remove); chipsWrap.appendChild(chip); }); } function renderAlertDescription(category) { 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.'; } function syncNotificationThresholdsField() { const hiddenField = document.querySelector('#s-notifications [data-key="NOTIFICATION_THRESHOLDS_JSON"]'); if (!hiddenField) return; const serialized = serializeNotificationThresholds(notificationThresholdsState); hiddenField.value = serialized; Fields.markChanged('NOTIFICATION_THRESHOLDS_JSON', serialized); hiddenField.classList.toggle('changed', Fields.isChanged('NOTIFICATION_THRESHOLDS_JSON')); } function getSelectedAlertKey(category) { const select = document.querySelector(`#s-notifications [data-notif-category="${category}"]`); return select ? select.value : ''; } function isValidThresholdValue(value) { if (!value) return false; if (/^\d+$/.test(value)) return true; return /^(\d+[mhd])+$/.test(value); } function toHumanLabel(key) { return String(key) .split('_') .map(part => part.toUpperCase() === 'T2' || part.toUpperCase() === 'T3' ? part.toUpperCase() : part.charAt(0).toUpperCase() + part.slice(1)) .join(' '); } window.Notifications = { initNotificationsEditor, isValidThresholdValue, toHumanLabel, NOTIFICATION_PRESETS, NOTIFICATION_TAB_KEYS, NOTIFICATION_ALERT_DESCRIPTIONS }; })();