Add per-staff metrics: StaffAction event log + /stats command
Event-sourced tracking of staff/ticket lifecycle actions, plus a /stats command. Foundation for a future tickets-website analytics dashboard. Data: - StaffAction model (event log) + Ticket.game / Ticket.closedAt - STATS_ADMIN_IDS config (who may view others' stats) Recording (fire-and-forget, idempotent on real state transitions): - claim, response (channel reply + /response send), escalate, de-escalate, transfer, close (4 sites), reopen — each denormalizes ticketType, tier, priority, game, requester (senderEmail / creatorId), guildId - close events carry closerType / resolverId (claimer credit) / wasClaimed; transfer carries fromId / toId; reopen stamps resolverId - conditional close transition helper (atomic open->closed + closedAt) shared by all four close paths Query + command: - pure period parser (presets + free-text) and stats shaper (per-metric keys) - command-aware autocomplete dispatch - /stats: period (autocomplete) + member (admin-gated) + source (all/email/ discord), ManageMessages + staff-role gated, ephemeral, tier-labeled embed 288+ unit tests; timing/busiest-times data is collected but displayed later.
This commit is contained in:
@@ -19,6 +19,10 @@ describe('ALLOWED_CONFIG_KEYS', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('includes STATS_ADMIN_IDS', () => {
|
||||
expect(ALLOWED_CONFIG_KEYS.has('STATS_ADMIN_IDS')).toBe(true);
|
||||
});
|
||||
|
||||
it('does not contain stale removed keys', () => {
|
||||
for (const k of ['BOSSCORD_API_KEY', 'SURGE_ENABLED']) {
|
||||
expect(ALLOWED_CONFIG_KEYS.has(k)).toBe(false);
|
||||
@@ -192,9 +196,8 @@ describe('discord_id validator', () => {
|
||||
|
||||
describe('discord_id_list validator', () => {
|
||||
// ADDITIONAL_STAFF_ROLES (a real comma-list) ends in _ROLES, not _IDS, so it
|
||||
// hits the string fallback. discord_id_list only fires for `*_IDS` keys, so
|
||||
// exercise it with a hypothetical name.
|
||||
const v = getValidator('STAFF_USER_IDS');
|
||||
// hits the string fallback. discord_id_list only fires for `*_IDS` keys.
|
||||
const v = getValidator('STATS_ADMIN_IDS');
|
||||
|
||||
it('infers type discord_id_list for *_IDS keys', () => {
|
||||
expect(v.type).toBe('discord_id_list');
|
||||
@@ -221,6 +224,40 @@ describe('discord_id_list validator', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('STATS_ADMIN_IDS parsing (config.js pattern)', () => {
|
||||
// Tests the .split(',').map(r=>r.trim()).filter(Boolean) idiom used for all
|
||||
// list env vars in config.js — exercised here as a pure expression.
|
||||
function parseIdList(v) {
|
||||
return (v || '').split(',').map(r => r.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
it('returns [] for empty string', () => {
|
||||
expect(parseIdList('')).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns [] for undefined', () => {
|
||||
expect(parseIdList(undefined)).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns a single-element array for one ID', () => {
|
||||
expect(parseIdList('321754640431710226')).toEqual(['321754640431710226']);
|
||||
});
|
||||
|
||||
it('returns multiple IDs for comma-separated input', () => {
|
||||
expect(parseIdList('321754640431710226,691678135527276614,224692549225283584'))
|
||||
.toEqual(['321754640431710226', '691678135527276614', '224692549225283584']);
|
||||
});
|
||||
|
||||
it('trims whitespace around each ID', () => {
|
||||
expect(parseIdList(' 321754640431710226 , 691678135527276614 '))
|
||||
.toEqual(['321754640431710226', '691678135527276614']);
|
||||
});
|
||||
|
||||
it('drops empty segments from trailing commas', () => {
|
||||
expect(parseIdList('321754640431710226,')).toEqual(['321754640431710226']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('string validator (fallback)', () => {
|
||||
const v = getValidator('TICKET_CATEGORY_NAME');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user