phase 9 notification toggles (per-alert, per-category, master; default-disabled)
This commit is contained in:
@@ -8,6 +8,7 @@ const { mongoose } = require('../db-connection');
|
||||
const { getAll, get, shouldFireThreshold, onWeeklyReset } = require('./patternStore');
|
||||
const { enqueueSend } = require('./channelQueue');
|
||||
const { assertKeysRegistered } = require('./notificationRegistry');
|
||||
const { isEnabled } = require('./notificationEnabled');
|
||||
|
||||
// Alert keys this module fires via shouldFire()/standard threshold path.
|
||||
// rapid_t2_t3 is intentionally excluded — it uses count-milestone firing below
|
||||
@@ -94,7 +95,7 @@ async function checkUserPatterns(client) {
|
||||
for (const [userId, count] of todayCounts) {
|
||||
if (count >= CONFIG.PATTERN_USER_TICKET_THRESHOLD) {
|
||||
const key = `user_tickets:${userId}:today`;
|
||||
if (shouldFire('user_tickets', key, 'today')) {
|
||||
if (isEnabled('user_tickets') && shouldFire('user_tickets', key, 'today')) {
|
||||
postPattern(client, 'USER_PATTERNS_CHANNEL_ID', buildEmbed(
|
||||
'Repeat ticket user',
|
||||
`User \`${userId}\` created ${count} tickets today (threshold: ${CONFIG.PATTERN_USER_TICKET_THRESHOLD}).`,
|
||||
@@ -114,7 +115,7 @@ async function checkUserPatterns(client) {
|
||||
]);
|
||||
for (const r of reopens) {
|
||||
const key = `user_reopen:${r._id}:week`;
|
||||
if (shouldFire('user_reopen', key, 'week')) {
|
||||
if (isEnabled('user_reopen') && shouldFire('user_reopen', key, 'week')) {
|
||||
postPattern(client, 'USER_PATTERNS_CHANNEL_ID', buildEmbed(
|
||||
'High reopen rate',
|
||||
`${r._id} reopened tickets ${r.count}x this week`,
|
||||
@@ -133,7 +134,7 @@ async function checkUserPatterns(client) {
|
||||
]);
|
||||
for (const c of crossGame) {
|
||||
const key = `user_crossgame:${c._id}:week`;
|
||||
if (shouldFire('user_crossgame', key, 'week')) {
|
||||
if (isEnabled('user_crossgame') && shouldFire('user_crossgame', key, 'week')) {
|
||||
postPattern(client, 'USER_PATTERNS_CHANNEL_ID', buildEmbed(
|
||||
'Cross-game user',
|
||||
`${c._id} has tickets across ${c.games.length} games: ${c.games.filter(Boolean).join(', ')}`,
|
||||
@@ -150,7 +151,7 @@ async function checkGamePatterns(client) {
|
||||
for (const [game, count] of todayCounts) {
|
||||
if (count >= CONFIG.PATTERN_GAME_TICKET_THRESHOLD) {
|
||||
const key = `game_surge:${game}:today`;
|
||||
if (shouldFire('game_surge', key, 'today')) {
|
||||
if (isEnabled('game_surge') && shouldFire('game_surge', key, 'today')) {
|
||||
postPattern(client, 'GAME_PATTERNS_CHANNEL_ID', buildEmbed(
|
||||
'Game ticket surge',
|
||||
`**${game}** has ${count} tickets today (threshold: ${CONFIG.PATTERN_GAME_TICKET_THRESHOLD}).`,
|
||||
@@ -171,7 +172,7 @@ async function checkGamePatterns(client) {
|
||||
for (const b of backlog) {
|
||||
const gameName = b._id || 'Unknown';
|
||||
const key = `game_backlog:${gameName}:today`;
|
||||
if (shouldFire('game_backlog', key, 'today')) {
|
||||
if (isEnabled('game_backlog') && shouldFire('game_backlog', key, 'today')) {
|
||||
postPattern(client, 'GAME_PATTERNS_CHANNEL_ID', buildEmbed(
|
||||
'Game backlog alert',
|
||||
`**${gameName}** has ${b.count} unclaimed tickets older than ${CONFIG.PATTERN_UNCLAIMED_HOURS}h.`,
|
||||
@@ -198,7 +199,7 @@ async function checkGamePatterns(client) {
|
||||
const lw = lastWeekMap.get(tw._id);
|
||||
if (lw && tw.avg > lw * 1.2) {
|
||||
const key = `game_resolution:${tw._id}:week`;
|
||||
if (shouldFire('game_resolution', key, 'week')) {
|
||||
if (isEnabled('game_resolution') && shouldFire('game_resolution', key, 'week')) {
|
||||
const twHrs = (tw.avg / 3600000).toFixed(1);
|
||||
const lwHrs = (lw / 3600000).toFixed(1);
|
||||
postPattern(client, 'GAME_PATTERNS_CHANNEL_ID', buildEmbed(
|
||||
@@ -223,7 +224,7 @@ async function checkGamePatterns(client) {
|
||||
for (const [game, count] of todayCounts) {
|
||||
if (count >= 3 && !recentGames.has(game)) {
|
||||
const key = `game_spike:${game}:today`;
|
||||
if (shouldFire('game_spike', key, 'today')) {
|
||||
if (isEnabled('game_spike') && shouldFire('game_spike', key, 'today')) {
|
||||
postPattern(client, 'GAME_PATTERNS_CHANNEL_ID', buildEmbed(
|
||||
'Possible outage',
|
||||
`**${game}**: ${count} tickets today after 0 in the last 3 days.`,
|
||||
@@ -244,7 +245,7 @@ async function checkTagPatterns(client) {
|
||||
}
|
||||
if (topTag && topCount >= 5) {
|
||||
const key = `tag_top:${topTag}:today`;
|
||||
if (shouldFire('tag_top', key, 'today')) {
|
||||
if (isEnabled('tag_top') && shouldFire('tag_top', key, 'today')) {
|
||||
postPattern(client, 'TAG_PATTERNS_CHANNEL_ID', buildEmbed(
|
||||
'Top issue tag today',
|
||||
`**${topTag}** used ${topCount} times today.`,
|
||||
@@ -263,7 +264,7 @@ async function checkTagPatterns(client) {
|
||||
]);
|
||||
for (const te of tagEscalations) {
|
||||
const key = `tag_escalation:${te._id}:week`;
|
||||
if (shouldFire('tag_escalation', key, 'week')) {
|
||||
if (isEnabled('tag_escalation') && shouldFire('tag_escalation', key, 'week')) {
|
||||
postPattern(client, 'TAG_PATTERNS_CHANNEL_ID', buildEmbed(
|
||||
'Tag frequently leads to escalation',
|
||||
`**${te._id}**: ${te.count} escalated tickets this week.`,
|
||||
@@ -277,7 +278,7 @@ async function checkTagPatterns(client) {
|
||||
const untaggedCount = get('untagged_closes', 'total', 'today');
|
||||
if (untaggedCount >= 5) {
|
||||
const key = 'untagged_closes:today';
|
||||
if (shouldFire('untagged_closes', key, 'today')) {
|
||||
if (isEnabled('untagged_closes') && shouldFire('untagged_closes', key, 'today')) {
|
||||
postPattern(client, 'TAG_PATTERNS_CHANNEL_ID', buildEmbed(
|
||||
'High untagged close rate',
|
||||
`${untaggedCount} tickets closed today without a tag.`,
|
||||
@@ -297,7 +298,7 @@ async function checkTagPatterns(client) {
|
||||
}
|
||||
if (total >= 5 && maxGame && maxCount / total > 0.8) {
|
||||
const key = `tag_game_corr:${tag}:${maxGame}:week`;
|
||||
if (shouldFire('tag_game_corr', key, 'week')) {
|
||||
if (isEnabled('tag_game_corr') && shouldFire('tag_game_corr', key, 'week')) {
|
||||
postPattern(client, 'COMBINED_PATTERNS_CHANNEL_ID', buildEmbed(
|
||||
'Auto-tagging opportunity',
|
||||
`**${tag}** is ${Math.round(maxCount / total * 100)}% from **${maxGame}** (${maxCount}/${total} this week).`,
|
||||
@@ -314,7 +315,7 @@ async function checkEscalationPatterns(client) {
|
||||
for (const [user, count] of userEscalations) {
|
||||
if (count >= CONFIG.PATTERN_ESCALATION_THRESHOLD) {
|
||||
const key = `user_esc:${user}:week`;
|
||||
if (shouldFire('user_esc', key, 'week')) {
|
||||
if (isEnabled('user_esc') && shouldFire('user_esc', key, 'week')) {
|
||||
postPattern(client, 'ESCALATION_PATTERNS_CHANNEL_ID', buildEmbed(
|
||||
'Frequent escalation user',
|
||||
`\`${user}\` has ${count} escalated tickets this week (threshold: ${CONFIG.PATTERN_ESCALATION_THRESHOLD}).`,
|
||||
@@ -337,7 +338,7 @@ async function checkEscalationPatterns(client) {
|
||||
const gameTotal = await Ticket.countDocuments({ createdAt: { $gte: thisWeekStart }, game: tw._id });
|
||||
if (gameTotal > 0 && tw.count / gameTotal > 0.5) {
|
||||
const key = `game_esc_rate:${tw._id}:week`;
|
||||
if (shouldFire('game_esc_rate', key, 'week')) {
|
||||
if (isEnabled('game_esc_rate') && shouldFire('game_esc_rate', key, 'week')) {
|
||||
postPattern(client, 'ESCALATION_PATTERNS_CHANNEL_ID', buildEmbed(
|
||||
'High escalation rate for game',
|
||||
`**${tw._id}**: ${tw.count}/${gameTotal} tickets escalated (${Math.round(tw.count / gameTotal * 100)}%) this week.`,
|
||||
@@ -349,6 +350,7 @@ async function checkEscalationPatterns(client) {
|
||||
} catch (_) {}
|
||||
|
||||
// Rapid tier 2→3
|
||||
if (!isEnabled('rapid_t2_t3')) return;
|
||||
try {
|
||||
const since = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
|
||||
const rapid = await Ticket.find({
|
||||
@@ -391,7 +393,7 @@ async function checkStaffPatterns(client) {
|
||||
for (const [staffId, claims] of todayClaims) {
|
||||
if (claims >= 3 && get('staff_closes', staffId, 'today') === 0) {
|
||||
const key = `staff_no_close:${staffId}:today`;
|
||||
if (shouldFire('staff_no_close', key, 'today')) {
|
||||
if (isEnabled('staff_no_close') && shouldFire('staff_no_close', key, 'today')) {
|
||||
postPattern(client, 'STAFF_PATTERNS_CHANNEL_ID', buildEmbed(
|
||||
'Claims without closes',
|
||||
`Staff \`${staffId}\` claimed ${claims} tickets today but closed 0.`,
|
||||
@@ -410,7 +412,7 @@ async function checkStaffPatterns(client) {
|
||||
]);
|
||||
for (const o of overloaded) {
|
||||
const key = `staff_overloaded:${o._id}:today`;
|
||||
if (shouldFire('staff_overloaded', key, 'today')) {
|
||||
if (isEnabled('staff_overloaded') && shouldFire('staff_overloaded', key, 'today')) {
|
||||
postPattern(client, 'STAFF_PATTERNS_CHANNEL_ID', buildEmbed(
|
||||
'Staff overloaded',
|
||||
`Staff \`${o._id}\` has ${o.count} open claimed tickets.`,
|
||||
@@ -425,7 +427,7 @@ async function checkStaffPatterns(client) {
|
||||
for (const [staffId, count] of stalePings) {
|
||||
if (count >= CONFIG.PATTERN_STAFF_STALE_PING_THRESHOLD) {
|
||||
const key = `staff_stale:${staffId}:today`;
|
||||
if (shouldFire('staff_stale', key, 'today')) {
|
||||
if (isEnabled('staff_stale') && shouldFire('staff_stale', key, 'today')) {
|
||||
postPattern(client, 'STAFF_PATTERNS_CHANNEL_ID', buildEmbed(
|
||||
'Staff stale ping threshold',
|
||||
`Staff \`${staffId}\` received ${count} stale pings today.`,
|
||||
@@ -441,7 +443,7 @@ async function checkStaffPatterns(client) {
|
||||
const claims = get('staff_claims', staffId, 'today');
|
||||
if (claims > 0 && transfers >= claims) {
|
||||
const key = `staff_transfer_rate:${staffId}:today`;
|
||||
if (shouldFire('staff_transfer_rate', key, 'today')) {
|
||||
if (isEnabled('staff_transfer_rate') && shouldFire('staff_transfer_rate', key, 'today')) {
|
||||
postPattern(client, 'STAFF_PATTERNS_CHANNEL_ID', buildEmbed(
|
||||
'High transfer rate',
|
||||
`Staff \`${staffId}\` transferred ${transfers}/${claims} claimed tickets today.`,
|
||||
@@ -456,7 +458,7 @@ async function checkStaffPatterns(client) {
|
||||
for (const [staffId, count] of weekEscalations) {
|
||||
if (count >= CONFIG.PATTERN_ESCALATION_THRESHOLD) {
|
||||
const key = `staff_esc:${staffId}:week`;
|
||||
if (shouldFire('staff_esc', key, 'week')) {
|
||||
if (isEnabled('staff_esc') && shouldFire('staff_esc', key, 'week')) {
|
||||
postPattern(client, 'STAFF_PATTERNS_CHANNEL_ID', buildEmbed(
|
||||
'Staff frequent escalator',
|
||||
`Staff \`${staffId}\` escalated ${count} tickets this week.`,
|
||||
@@ -475,7 +477,7 @@ async function checkCombinedPatterns(client) {
|
||||
for (const [game, count] of gameEsc) {
|
||||
if (count >= 3) {
|
||||
const key = `staff_game_esc:${staffId}:${game}:week`;
|
||||
if (shouldFire('staff_game_esc', key, 'week')) {
|
||||
if (isEnabled('staff_game_esc') && shouldFire('staff_game_esc', key, 'week')) {
|
||||
postPattern(client, 'COMBINED_PATTERNS_CHANNEL_ID', buildEmbed(
|
||||
'Staff may need training for this game',
|
||||
`Staff \`${staffId}\` escalated ${count} **${game}** tickets this week.`,
|
||||
@@ -494,7 +496,7 @@ async function checkCombinedPatterns(client) {
|
||||
const tagGameCount = get(`tag_game:${tag}`, game, 'week');
|
||||
if (tagGameCount >= 5) {
|
||||
const key = `game_tag_spike:${game}:${tag}:today`;
|
||||
if (shouldFire('game_tag_spike', key, 'today')) {
|
||||
if (isEnabled('game_tag_spike') && shouldFire('game_tag_spike', key, 'today')) {
|
||||
postPattern(client, 'COMBINED_PATTERNS_CHANNEL_ID', buildEmbed(
|
||||
'Specific feature of specific game spiking',
|
||||
`**${game}** + **${tag}**: ${tagGameCount} tickets this week.`,
|
||||
@@ -531,7 +533,7 @@ async function checkCombinedPatterns(client) {
|
||||
const daytimeRate = daytime / daytimeTotal;
|
||||
if (overnightRate > daytimeRate * 2 && overnight >= 3) {
|
||||
const key = 'overnight_gap:week';
|
||||
if (shouldFire('overnight_gap', key, 'week')) {
|
||||
if (isEnabled('overnight_gap') && shouldFire('overnight_gap', key, 'week')) {
|
||||
postPattern(client, 'COMBINED_PATTERNS_CHANNEL_ID', buildEmbed(
|
||||
'Overnight coverage gap',
|
||||
`Overnight escalation rate: ${Math.round(overnightRate * 100)}% vs daytime ${Math.round(daytimeRate * 100)}%.`,
|
||||
@@ -559,7 +561,7 @@ async function checkCombinedPatterns(client) {
|
||||
for (const s of staffGameStats) {
|
||||
if (s.escalated / s.total >= 0.9) {
|
||||
const key = `staff_always_esc:${s._id.staff}:${s._id.game}:month`;
|
||||
if (shouldFire('staff_always_esc', key, 'month')) {
|
||||
if (isEnabled('staff_always_esc') && shouldFire('staff_always_esc', key, 'month')) {
|
||||
postPattern(client, 'COMBINED_PATTERNS_CHANNEL_ID', buildEmbed(
|
||||
'Staff always escalates this game',
|
||||
`Staff \`${s._id.staff}\` escalated ${s.escalated}/${s.total} **${s._id.game}** tickets this month.`,
|
||||
|
||||
Reference in New Issue
Block a user