From 08d2a1f3cf90c5cd75efb18e2dd0900ef7339721 Mon Sep 17 00:00:00 2001 From: byteful Date: Fri, 10 Jul 2026 06:40:06 -0500 Subject: [PATCH] audit/analytics improvements --- .../src/components/settings/UsageSettings.tsx | 11 ++++- client/src/locales/de.json | 1 + client/src/locales/en.json | 1 + client/src/locales/es.json | 1 + client/src/locales/fr.json | 1 + client/src/locales/hi.json | 1 + client/src/locales/it.json | 1 + client/src/locales/ja.json | 1 + client/src/locales/nl.json | 1 + client/src/locales/pt.json | 1 + client/src/locales/ru.json | 1 + client/src/locales/zh.json | 1 + client/src/pages/audit.tsx | 41 ++++++++----------- client/src/utils/date-utils.ts | 12 ++++++ 14 files changed, 50 insertions(+), 25 deletions(-) diff --git a/client/src/components/settings/UsageSettings.tsx b/client/src/components/settings/UsageSettings.tsx index 2270d23..415802c 100644 --- a/client/src/components/settings/UsageSettings.tsx +++ b/client/src/components/settings/UsageSettings.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { HardDrive, Search, Trash2, Download, FolderOpen, Calendar, AlertCircle, Settings, CreditCard, Brain, Play } from 'lucide-react'; +import { HardDrive, Search, Trash2, Download, FolderOpen, Calendar, AlertCircle, Settings, CreditCard, Brain, Play, Info } from 'lucide-react'; import { getApiUrl, getCurrentDomain, apiFetch } from '@/lib/api'; import { Button } from '@modl-gg/shared-web/components/ui/button'; import { Input } from '@modl-gg/shared-web/components/ui/input'; @@ -889,6 +889,15 @@ const fetchStorageData = async () => {

+ {replayRetentionEnabled && ( +
+ +

+ {t('settings.usage.replayRetentionExemptionNote')} +

+
+ )} +
{replayRetentionEnabled diff --git a/client/src/locales/de.json b/client/src/locales/de.json index e46c5c1..382b828 100644 --- a/client/src/locales/de.json +++ b/client/src/locales/de.json @@ -796,6 +796,7 @@ "replayRetentionEnabledDesc": "Gespeicherte Replays nach der konfigurierten Aufbewahrungszeit automatisch löschen.", "replayRetentionDays": "Aufbewahrungszeitraum (Tage)", "replayRetentionDaysDesc": "Gespeicherte Replays, die älter als diese Anzahl von Tagen sind, können bereinigt werden.", + "replayRetentionExemptionNote": "An offene Tickets oder Berufungen angehängte Replays werden nie gelöscht, auch nicht nach Ablauf der Aufbewahrungszeit.", "replayRetentionDisabledDesc": "Automatische Replay-Bereinigung ist deaktiviert. Gespeicherte Replays bleiben erhalten, bis sie manuell gelöscht werden.", "replayRetentionActive": "Replays werden nach {{days}} Tagen gelöscht", "replayRetentionOff": "Automatische Bereinigung deaktiviert", diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 4d7e93c..c860f04 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -796,6 +796,7 @@ "replayRetentionEnabledDesc": "Automatically delete stored replays after the configured retention period.", "replayRetentionDays": "Retention period (days)", "replayRetentionDaysDesc": "Stored replays older than this many days are eligible for cleanup.", + "replayRetentionExemptionNote": "Replays attached to open tickets or appeals are never deleted, even after the retention period.", "replayRetentionDisabledDesc": "Automatic replay cleanup is disabled. Stored replays will be retained until manually deleted.", "replayRetentionActive": "Deleting replays after {{days}} days", "replayRetentionOff": "Automatic cleanup disabled", diff --git a/client/src/locales/es.json b/client/src/locales/es.json index b342d39..b011505 100644 --- a/client/src/locales/es.json +++ b/client/src/locales/es.json @@ -796,6 +796,7 @@ "replayRetentionEnabledDesc": "Eliminar automáticamente las repeticiones almacenadas después del periodo de retención configurado.", "replayRetentionDays": "Periodo de retención (días)", "replayRetentionDaysDesc": "Las repeticiones almacenadas con más de esta cantidad de días podrán limpiarse.", + "replayRetentionExemptionNote": "Las repeticiones adjuntas a tickets o apelaciones abiertos nunca se eliminan, incluso después del periodo de retención.", "replayRetentionDisabledDesc": "La limpieza automática de repeticiones está deshabilitada. Las repeticiones almacenadas se conservarán hasta que se eliminen manualmente.", "replayRetentionActive": "Eliminando repeticiones después de {{days}} días", "replayRetentionOff": "Limpieza automática deshabilitada", diff --git a/client/src/locales/fr.json b/client/src/locales/fr.json index 348edf8..2a51a92 100644 --- a/client/src/locales/fr.json +++ b/client/src/locales/fr.json @@ -796,6 +796,7 @@ "replayRetentionEnabledDesc": "Supprimer automatiquement les replays stockés après la période de rétention configurée.", "replayRetentionDays": "Période de rétention (jours)", "replayRetentionDaysDesc": "Les replays stockés plus anciens que ce nombre de jours peuvent être nettoyés.", + "replayRetentionExemptionNote": "Les replays associés à des tickets ou appels ouverts ne sont jamais supprimés, même après la période de rétention.", "replayRetentionDisabledDesc": "Le nettoyage automatique des replays est désactivé. Les replays stockés seront conservés jusqu'à leur suppression manuelle.", "replayRetentionActive": "Suppression des replays après {{days}} jours", "replayRetentionOff": "Nettoyage automatique désactivé", diff --git a/client/src/locales/hi.json b/client/src/locales/hi.json index fd1a269..68429bf 100644 --- a/client/src/locales/hi.json +++ b/client/src/locales/hi.json @@ -796,6 +796,7 @@ "replayRetentionEnabledDesc": "कॉन्फ़िगर की गई प्रतिधारण अवधि के बाद संग्रहीत रिप्ले को स्वचालित रूप से हटाएं।", "replayRetentionDays": "प्रतिधारण अवधि (दिन)", "replayRetentionDaysDesc": "इस संख्या से अधिक पुराने संग्रहीत रिप्ले सफाई के पात्र होंगे।", + "replayRetentionExemptionNote": "खुले टिकट या अपील से जुड़े रिप्ले कभी नहीं हटाए जाते, भले ही प्रतिधारण अवधि समाप्त हो गई हो।", "replayRetentionDisabledDesc": "स्वचालित रिप्ले सफाई अक्षम है। संग्रहीत रिप्ले तब तक रहेंगे जब तक उन्हें मैन्युअल रूप से हटाया नहीं जाता।", "replayRetentionActive": "{{days}} दिनों के बाद रिप्ले हटाया जा रहा है", "replayRetentionOff": "स्वचालित सफाई अक्षम", diff --git a/client/src/locales/it.json b/client/src/locales/it.json index 7587905..2559839 100644 --- a/client/src/locales/it.json +++ b/client/src/locales/it.json @@ -796,6 +796,7 @@ "replayRetentionEnabledDesc": "Elimina automaticamente i replay archiviati dopo il periodo di conservazione configurato.", "replayRetentionDays": "Periodo di conservazione (giorni)", "replayRetentionDaysDesc": "I replay archiviati più vecchi di questo numero di giorni sono soggetti a pulizia.", + "replayRetentionExemptionNote": "I replay allegati a ticket o appelli aperti non vengono mai eliminati, anche dopo il periodo di conservazione.", "replayRetentionDisabledDesc": "La pulizia automatica dei replay è disabilitata. I replay archiviati verranno conservati fino all'eliminazione manuale.", "replayRetentionActive": "Eliminazione dei replay dopo {{days}} giorni", "replayRetentionOff": "Pulizia automatica disabilitata", diff --git a/client/src/locales/ja.json b/client/src/locales/ja.json index ddfa385..5ceabb3 100644 --- a/client/src/locales/ja.json +++ b/client/src/locales/ja.json @@ -796,6 +796,7 @@ "replayRetentionEnabledDesc": "設定された保持期間を過ぎた保存済みリプレイを自動的に削除します。", "replayRetentionDays": "保持期間 (日)", "replayRetentionDaysDesc": "この日数より古い保存済みリプレイは削除の対象となります。", + "replayRetentionExemptionNote": "未解決のチケットや不服申し立てに添付されたリプレイは、保持期間を過ぎても削除されません。", "replayRetentionDisabledDesc": "自動リプレイ削除は無効になっています。保存されたリプレイは手動で削除されるまで保持されます。", "replayRetentionActive": "{{days}} 日後にリプレイを削除中", "replayRetentionOff": "自動削除は無効です", diff --git a/client/src/locales/nl.json b/client/src/locales/nl.json index b387745..c148f56 100644 --- a/client/src/locales/nl.json +++ b/client/src/locales/nl.json @@ -796,6 +796,7 @@ "replayRetentionEnabledDesc": "Verwijder opgeslagen replays automatisch na de ingestelde bewaartermijn.", "replayRetentionDays": "Bewaartermijn (dagen)", "replayRetentionDaysDesc": "Opgeslagen replays ouder dan dit aantal dagen komen in aanmerking voor opruiming.", + "replayRetentionExemptionNote": "Replays die aan open tickets of beroepen zijn gekoppeld, worden nooit verwijderd, zelfs niet na de bewaartermijn.", "replayRetentionDisabledDesc": "Automatische replay-opruiming is uitgeschakeld. Opgeslagen replays blijven bewaard totdat ze handmatig worden verwijderd.", "replayRetentionActive": "Replays worden na {{days}} dagen verwijderd", "replayRetentionOff": "Automatische opruiming uitgeschakeld", diff --git a/client/src/locales/pt.json b/client/src/locales/pt.json index 0c2bb7a..fb43fe4 100644 --- a/client/src/locales/pt.json +++ b/client/src/locales/pt.json @@ -796,6 +796,7 @@ "replayRetentionEnabledDesc": "Excluir automaticamente os replays armazenados após o período de retenção configurado.", "replayRetentionDays": "Período de retenção (dias)", "replayRetentionDaysDesc": "Replays armazenados mais antigos que este número de dias serão qualificados para limpeza.", + "replayRetentionExemptionNote": "Os replays anexados a tickets ou apelações abertos nunca são excluídos, mesmo após o período de retenção.", "replayRetentionDisabledDesc": "A limpeza automática de replays está desativada. Os replays armazenados serão retidos até serem excluídos manualmente.", "replayRetentionActive": "Excluindo replays após {{days}} dias", "replayRetentionOff": "Limpeza automática desativada", diff --git a/client/src/locales/ru.json b/client/src/locales/ru.json index abf24bc..5cb822e 100644 --- a/client/src/locales/ru.json +++ b/client/src/locales/ru.json @@ -800,6 +800,7 @@ "replayRetentionEnabledDesc": "Автоматически удалять сохраненные записи по истечении настроенного срока хранения.", "replayRetentionDays": "Срок хранения (дней)", "replayRetentionDaysDesc": "Сохраненные записи старше указанного количества дней будут удалены при очистке.", + "replayRetentionExemptionNote": "Записи, прикреплённые к открытым тикетам или апелляциям, никогда не удаляются, даже по истечении срока хранения.", "replayRetentionDisabledDesc": "Автоматическая очистка записей отключена. Сохраненные записи будут храниться до тех пор, пока не будут удалены вручную.", "replayRetentionActive": "Удаление записей через {{days}} дн.", "replayRetentionOff": "Автоматическая очистка отключена", diff --git a/client/src/locales/zh.json b/client/src/locales/zh.json index 436e424..b46f01c 100644 --- a/client/src/locales/zh.json +++ b/client/src/locales/zh.json @@ -796,6 +796,7 @@ "replayRetentionEnabledDesc": "在配置的保留期过后自动删除存储的回放。", "replayRetentionDays": "保留期(天)", "replayRetentionDaysDesc": "超过此天数的存储回放将符合清理条件。", + "replayRetentionExemptionNote": "附加到未解决工单或申诉的回放永远不会被删除,即使超过保留期。", "replayRetentionDisabledDesc": "已禁用自动回放清理。存储的回放将被保留,直到手动删除。", "replayRetentionActive": "将在 {{days}} 天后删除回放", "replayRetentionOff": "自动清理已禁用", diff --git a/client/src/pages/audit.tsx b/client/src/pages/audit.tsx index db8b4f3..1840d4c 100644 --- a/client/src/pages/audit.tsx +++ b/client/src/pages/audit.tsx @@ -32,7 +32,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, Dialog import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@modl-gg/shared-web/components/ui/alert-dialog'; import { Textarea } from '@modl-gg/shared-web/components/ui/textarea'; import { subDays } from 'date-fns'; -import { formatDateOnly } from '@/utils/date-utils'; +import { formatChartDateLabel, formatDateOnly } from '@/utils/date-utils'; import { useLogs } from '@/hooks/use-data'; import { useQuery } from '@tanstack/react-query'; import PageContainer from '@/components/layout/PageContainer'; @@ -294,14 +294,15 @@ interface CustomTooltipProps { label?: string | number; formatValue?: (value: number | string, name?: string) => string; formatName?: (name: string) => string; + formatLabel?: (label: string | number) => string; } // Custom themed tooltip component for charts -const CustomTooltip = ({ active, payload, label, formatValue, formatName }: CustomTooltipProps) => { +const CustomTooltip = ({ active, payload, label, formatValue, formatName, formatLabel }: CustomTooltipProps) => { if (active && payload && payload.length) { return (
- {label &&

{label}

} + {label &&

{formatLabel ? formatLabel(label) : label}

} {payload.map((entry, index) => (
- + - } /> + } /> @@ -1275,9 +1276,9 @@ const TicketAnalyticsSection = ({ analyticsPeriod }: { analyticsPeriod: string } - + - } /> + } /> { }); } - // Convert to array and sort by date - return Array.from(dateMap.values()).sort((a, b) => { - // Handle different date formats (e.g., "Jan 15" vs "2024-01-15") - const dateA = new Date(a.date); - const dateB = new Date(b.date); - if (!isNaN(dateA.getTime()) && !isNaN(dateB.getTime())) { - return dateA.getTime() - dateB.getTime(); - } - return a.date.localeCompare(b.date); - }); + return Array.from(dateMap.values()).sort((a, b) => a.date.localeCompare(b.date)); }, [ticketUpdatedAt, punishmentUpdatedAt, playerActivityUpdatedAt]); const maxLoginsByCountry = useMemo(() => { @@ -1827,7 +1819,7 @@ const AuditLog = () => { }, [playerActivityUpdatedAt]); // Memoize a single CustomTooltip element used across many charts. - const customTooltipEl = useMemo(() => , []); + const customTooltipEl = useMemo(() => , []); return ( @@ -1918,10 +1910,11 @@ const AuditLog = () => { - + { switch (name) { case 'tickets': return t('audit.legendNewTickets'); @@ -2032,9 +2025,9 @@ const AuditLog = () => { - + - name === "count" ? "Count" : name} />} /> + name === "count" ? "Count" : name} />} /> @@ -2188,9 +2181,9 @@ const AuditLog = () => { - + - name === "count" ? "New Players" : name} />} /> + name === "count" ? "New Players" : name} />} /> @@ -2199,7 +2192,7 @@ const AuditLog = () => { - + diff --git a/client/src/utils/date-utils.ts b/client/src/utils/date-utils.ts index 3b889a3..5829837 100644 --- a/client/src/utils/date-utils.ts +++ b/client/src/utils/date-utils.ts @@ -36,6 +36,18 @@ const formatDateParts = (date: Date, includeTime: boolean): string => { return `${datePart} ${hh}:${min}`; }; +const ISO_DATE_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/; + +export const formatChartDateLabel = (label: string | number): string => { + const text = String(label); + const match = ISO_DATE_PATTERN.exec(text); + if (!match) { + return text; + } + const date = new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3])); + return formatDateParts(date, false); +}; + export const formatDate = (dateString: string): string => { try { if (!dateString || dateString === 'Invalid Date') {