Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion client/src/components/settings/UsageSettings.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -889,6 +889,15 @@ const fetchStorageData = async () => {
</p>
</div>

{replayRetentionEnabled && (
<div className="flex items-start gap-2 rounded-lg border border-border bg-surface-1 p-3">
<Info className="h-4 w-4 shrink-0 text-muted-foreground mt-0.5" />
<p className="text-xs text-muted-foreground">
{t('settings.usage.replayRetentionExemptionNote')}
</p>
</div>
)}

<div className="flex items-center justify-between gap-3">
<Badge variant="outline">
{replayRetentionEnabled
Expand Down
1 change: 1 addition & 0 deletions client/src/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions client/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions client/src/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions client/src/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -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é",
Expand Down
1 change: 1 addition & 0 deletions client/src/locales/hi.json
Original file line number Diff line number Diff line change
Expand Up @@ -796,6 +796,7 @@
"replayRetentionEnabledDesc": "कॉन्फ़िगर की गई प्रतिधारण अवधि के बाद संग्रहीत रिप्ले को स्वचालित रूप से हटाएं।",
"replayRetentionDays": "प्रतिधारण अवधि (दिन)",
"replayRetentionDaysDesc": "इस संख्या से अधिक पुराने संग्रहीत रिप्ले सफाई के पात्र होंगे।",
"replayRetentionExemptionNote": "खुले टिकट या अपील से जुड़े रिप्ले कभी नहीं हटाए जाते, भले ही प्रतिधारण अवधि समाप्त हो गई हो।",
"replayRetentionDisabledDesc": "स्वचालित रिप्ले सफाई अक्षम है। संग्रहीत रिप्ले तब तक रहेंगे जब तक उन्हें मैन्युअल रूप से हटाया नहीं जाता।",
"replayRetentionActive": "{{days}} दिनों के बाद रिप्ले हटाया जा रहा है",
"replayRetentionOff": "स्वचालित सफाई अक्षम",
Expand Down
1 change: 1 addition & 0 deletions client/src/locales/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions client/src/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -796,6 +796,7 @@
"replayRetentionEnabledDesc": "設定された保持期間を過ぎた保存済みリプレイを自動的に削除します。",
"replayRetentionDays": "保持期間 (日)",
"replayRetentionDaysDesc": "この日数より古い保存済みリプレイは削除の対象となります。",
"replayRetentionExemptionNote": "未解決のチケットや不服申し立てに添付されたリプレイは、保持期間を過ぎても削除されません。",
"replayRetentionDisabledDesc": "自動リプレイ削除は無効になっています。保存されたリプレイは手動で削除されるまで保持されます。",
"replayRetentionActive": "{{days}} 日後にリプレイを削除中",
"replayRetentionOff": "自動削除は無効です",
Expand Down
1 change: 1 addition & 0 deletions client/src/locales/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions client/src/locales/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions client/src/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -800,6 +800,7 @@
"replayRetentionEnabledDesc": "Автоматически удалять сохраненные записи по истечении настроенного срока хранения.",
"replayRetentionDays": "Срок хранения (дней)",
"replayRetentionDaysDesc": "Сохраненные записи старше указанного количества дней будут удалены при очистке.",
"replayRetentionExemptionNote": "Записи, прикреплённые к открытым тикетам или апелляциям, никогда не удаляются, даже по истечении срока хранения.",
"replayRetentionDisabledDesc": "Автоматическая очистка записей отключена. Сохраненные записи будут храниться до тех пор, пока не будут удалены вручную.",
"replayRetentionActive": "Удаление записей через {{days}} дн.",
"replayRetentionOff": "Автоматическая очистка отключена",
Expand Down
1 change: 1 addition & 0 deletions client/src/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -796,6 +796,7 @@
"replayRetentionEnabledDesc": "在配置的保留期过后自动删除存储的回放。",
"replayRetentionDays": "保留期(天)",
"replayRetentionDaysDesc": "超过此天数的存储回放将符合清理条件。",
"replayRetentionExemptionNote": "附加到未解决工单或申诉的回放永远不会被删除,即使超过保留期。",
"replayRetentionDisabledDesc": "已禁用自动回放清理。存储的回放将被保留,直到手动删除。",
"replayRetentionActive": "将在 {{days}} 天后删除回放",
"replayRetentionOff": "自动清理已禁用",
Expand Down
41 changes: 17 additions & 24 deletions client/src/pages/audit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 (
<div className="bg-background border border-border rounded-lg p-3 shadow-lg z-50 pointer-events-none">
{label && <p className="text-sm font-medium mb-2">{label}</p>}
{label && <p className="text-sm font-medium mb-2">{formatLabel ? formatLabel(label) : label}</p>}
{payload.map((entry, index) => (
<div key={entry?.dataKey ?? entry?.name ?? index} className="flex items-center gap-2 text-sm">
<div
Expand Down Expand Up @@ -813,9 +814,9 @@ const StaffDetailModal = ({ staff, isOpen, onClose, initialPeriod = '30d' }: {
<ResponsiveContainer width="100%" height={300}>
<AreaChart data={staffActivityData}>
<CartesianGrid strokeDasharray="3 3" className="opacity-30" />
<XAxis dataKey="date" className="text-muted-foreground" fontSize={12} />
<XAxis dataKey="date" className="text-muted-foreground" fontSize={12} tickFormatter={formatChartDateLabel} />
<YAxis className="text-muted-foreground" fontSize={12} />
<Tooltip content={<CustomTooltip />} />
<Tooltip content={<CustomTooltip formatLabel={formatChartDateLabel} />} />
<Area type="monotone" dataKey="punishments" stackId="1" stroke="#ef4444" fill="#ef4444" dot={false} activeDot={false} />
<Area type="monotone" dataKey="tickets" stackId="1" stroke="#3b82f6" fill="#3b82f6" dot={false} activeDot={false} />
<Area type="monotone" dataKey="evidence" stackId="1" stroke="#10b981" fill="#10b981" dot={false} activeDot={false} />
Expand Down Expand Up @@ -1275,9 +1276,9 @@ const TicketAnalyticsSection = ({ analyticsPeriod }: { analyticsPeriod: string }
<ResponsiveContainer width="100%" height={300}>
<LineChart data={ticketAnalytics.dailyTickets}>
<CartesianGrid strokeDasharray="3 3" className="opacity-30" />
<XAxis dataKey="date" className="text-muted-foreground" fontSize={12} />
<XAxis dataKey="date" className="text-muted-foreground" fontSize={12} tickFormatter={formatChartDateLabel} />
<YAxis className="text-muted-foreground" fontSize={12} />
<Tooltip content={<CustomTooltip />} />
<Tooltip content={<CustomTooltip formatLabel={formatChartDateLabel} />} />
<Line
type="monotone"
dataKey="count"
Expand Down Expand Up @@ -1804,16 +1805,7 @@ const AuditLog = () => {
});
}

// 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));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Preserve chronological sorting
combinedMetricsData still accepts arbitrary string date labels from three analytics responses, and the removed code explicitly handled labels such as Jan 15. Sorting with localeCompare puts non-ISO month labels alphabetically, so the combined metrics chart renders points out of chronological order when those existing labels are returned.

Rule Used: This is a React frontend project on React 19 with ... (source)

Artifacts

Repro: executable combined metrics sort harness

  • Contains supporting evidence from the run (text/javascript; charset=utf-8).

Repro: failing execution log showing non-chronological sorted output

  • Keeps the command output available without making the summary code-heavy.

View artifacts

T-Rex Ran code and verified through T-Rex

Prompt To Fix With AI
This is a comment left during a code review.
Path: client/src/pages/audit.tsx
Line: 1808

Comment:
**Preserve chronological sorting**
`combinedMetricsData` still accepts arbitrary `string` date labels from three analytics responses, and the removed code explicitly handled labels such as `Jan 15`. Sorting with `localeCompare` puts non-ISO month labels alphabetically, so the combined metrics chart renders points out of chronological order when those existing labels are returned.

**Rule Used:** This is a React frontend project on React 19 with ... ([source](https://app.greptile.com/modl-gg/-/custom-context?memory=b7532101-0c9e-4ab6-b168-353a105ba593))

How can I resolve this? If you propose a fix, please make it concise.

}, [ticketUpdatedAt, punishmentUpdatedAt, playerActivityUpdatedAt]);

const maxLoginsByCountry = useMemo(() => {
Expand All @@ -1827,7 +1819,7 @@ const AuditLog = () => {
}, [playerActivityUpdatedAt]);

// Memoize a single CustomTooltip element used across many charts.
const customTooltipEl = useMemo(() => <CustomTooltip />, []);
const customTooltipEl = useMemo(() => <CustomTooltip formatLabel={formatChartDateLabel} />, []);

return (
<PageContainer>
Expand Down Expand Up @@ -1918,10 +1910,11 @@ const AuditLog = () => {
<ResponsiveContainer width="100%" height={300}>
<LineChart data={combinedMetricsData}>
<CartesianGrid strokeDasharray="3 3" className="opacity-30" />
<XAxis dataKey="date" className="text-muted-foreground" fontSize={12} />
<XAxis dataKey="date" className="text-muted-foreground" fontSize={12} tickFormatter={formatChartDateLabel} />
<YAxis className="text-muted-foreground" fontSize={12} />
<Tooltip
content={<CustomTooltip
formatLabel={formatChartDateLabel}
formatName={(name: string) => {
switch (name) {
case 'tickets': return t('audit.legendNewTickets');
Expand Down Expand Up @@ -2032,9 +2025,9 @@ const AuditLog = () => {
<ResponsiveContainer width="100%" height={250}>
<LineChart data={punishmentAnalytics.dailyPunishments}>
<CartesianGrid strokeDasharray="3 3" className="opacity-30" />
<XAxis dataKey="date" className="text-muted-foreground" fontSize={12} />
<XAxis dataKey="date" className="text-muted-foreground" fontSize={12} tickFormatter={formatChartDateLabel} />
<YAxis className="text-muted-foreground" fontSize={12} />
<Tooltip content={<CustomTooltip formatName={(name) => name === "count" ? "Count" : name} />} />
<Tooltip content={<CustomTooltip formatLabel={formatChartDateLabel} formatName={(name) => name === "count" ? "Count" : name} />} />
<Line type="monotone" dataKey="count" stroke="#ef4444" strokeWidth={2} dot={{r:0}} activeDot={false} />
</LineChart>
</ResponsiveContainer>
Expand Down Expand Up @@ -2188,9 +2181,9 @@ const AuditLog = () => {
<ResponsiveContainer width="100%" height={250}>
<AreaChart data={playerActivity.newPlayersTrend}>
<CartesianGrid strokeDasharray="3 3" className="opacity-30" />
<XAxis dataKey="date" className="text-muted-foreground" fontSize={12} />
<XAxis dataKey="date" className="text-muted-foreground" fontSize={12} tickFormatter={formatChartDateLabel} />
<YAxis className="text-muted-foreground" fontSize={12} />
<Tooltip content={<CustomTooltip formatName={(name) => name === "count" ? "New Players" : name} />} />
<Tooltip content={<CustomTooltip formatLabel={formatChartDateLabel} formatName={(name) => name === "count" ? "New Players" : name} />} />
<Area type="monotone" dataKey="count" stroke="#10b981" fill="#10b981" fillOpacity={0.3} dot={false} activeDot={false} name="New Players" />
</AreaChart>
</ResponsiveContainer>
Expand All @@ -2199,7 +2192,7 @@ const AuditLog = () => {
<ResponsiveContainer width="100%" height={250}>
<AreaChart data={playerActivity.loginTrend}>
<CartesianGrid strokeDasharray="3 3" className="opacity-30" />
<XAxis dataKey="date" className="text-muted-foreground" fontSize={12} />
<XAxis dataKey="date" className="text-muted-foreground" fontSize={12} tickFormatter={formatChartDateLabel} />
<YAxis className="text-muted-foreground" fontSize={12} />
<Tooltip content={customTooltipEl} />
<Area type="monotone" dataKey="logins" stroke="#10b981" fill="#10b981" fillOpacity={0.3} dot={false} activeDot={false} name="Logins" />
Expand Down
12 changes: 12 additions & 0 deletions client/src/utils/date-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
Loading