feat(dashboard): thème auto, config centralisée, dates UTC→TZ navigateur, tooltip Anubis
- ThemeContext: thème par défaut 'auto' (suit prefers-color-scheme du navigateur) - config.ts: fichier de configuration centrale (API_BASE_URL, DEFAULT_THEME, PAGE_SIZES, seuils, description du mécanisme d'identification Anubis) - dateUtils.ts: utilitaire partagé formatDate/formatDateShort/formatDateOnly/ formatTimeOnly/formatNumber — convertit les dates UTC ClickHouse dans le fuseau horaire et la locale du navigateur (plus de 'fr-FR' hardcodé) - tooltips.ts: ajout TIPS.anubis_identification — explique que les bots sont identifiés par UA (regex), IP/CIDR, ASN, pays via les règles Anubis - DetectionsList: colonne Anubis avec icône ⓘ affichant le tooltip explicatif - DataTable: Column.label étendu à React.ReactNode (pour JSX dans les headers) - 24 composants mis à jour: fr-FR remplacé par locale navigateur partout Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@ -2,6 +2,9 @@ import { useState } from 'react';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { useDetections } from '../hooks/useDetections';
|
||||
import DataTable, { Column } from './ui/DataTable';
|
||||
import { InfoTip } from './ui/Tooltip';
|
||||
import { TIPS } from './ui/tooltips';
|
||||
import { formatDate, formatDateOnly, formatTimeOnly } from '../utils/dateUtils';
|
||||
|
||||
type SortField = 'detected_at' | 'threat_level' | 'anomaly_score' | 'src_ip' | 'country_code' | 'asn_number' | 'host' | 'hits' | 'hit_velocity';
|
||||
type SortOrder = 'asc' | 'desc';
|
||||
@ -33,6 +36,9 @@ interface DetectionRow {
|
||||
unique_ja4s?: string[];
|
||||
unique_hosts?: string[];
|
||||
unique_client_headers?: string[];
|
||||
anubis_bot_name?: string;
|
||||
anubis_bot_action?: string;
|
||||
anubis_bot_category?: string;
|
||||
}
|
||||
|
||||
export function DetectionsList() {
|
||||
@ -65,6 +71,7 @@ export function DetectionsList() {
|
||||
{ key: 'client_headers', label: 'Client Headers', visible: false, sortable: false },
|
||||
{ key: 'model_name', label: 'Modèle', visible: true, sortable: true },
|
||||
{ key: 'anomaly_score', label: 'Score', visible: true, sortable: true },
|
||||
{ key: 'anubis', label: '🤖 Anubis', visible: true, sortable: false },
|
||||
{ key: 'hits', label: 'Hits', visible: true, sortable: true },
|
||||
{ key: 'hit_velocity', label: 'Velocity', visible: true, sortable: true },
|
||||
{ key: 'asn', label: 'ASN', visible: true, sortable: true },
|
||||
@ -236,6 +243,38 @@ export function DetectionsList() {
|
||||
sortable: true,
|
||||
render: (_, row) => <ModelBadge model={row.model_name} />,
|
||||
};
|
||||
case 'anubis':
|
||||
return {
|
||||
key: 'anubis_bot_name',
|
||||
label: (
|
||||
<span className="inline-flex items-center gap-1">
|
||||
🤖 Anubis
|
||||
<InfoTip content={TIPS.anubis_identification} />
|
||||
</span>
|
||||
),
|
||||
sortable: false,
|
||||
render: (_, row) => {
|
||||
const name = row.anubis_bot_name;
|
||||
const action = row.anubis_bot_action;
|
||||
const category = row.anubis_bot_category;
|
||||
if (!name) return <span className="text-text-disabled text-xs">—</span>;
|
||||
const actionColor =
|
||||
action === 'ALLOW' ? 'bg-green-500/15 text-green-400 border-green-500/30' :
|
||||
action === 'DENY' ? 'bg-red-500/15 text-red-400 border-red-500/30' :
|
||||
'bg-yellow-500/15 text-yellow-400 border-yellow-500/30';
|
||||
return (
|
||||
<div className="space-y-0.5">
|
||||
<div className={`inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded border ${actionColor}`}>
|
||||
<span className="font-medium">{name}</span>
|
||||
</div>
|
||||
<div className="flex gap-1 flex-wrap">
|
||||
{action && <span className="text-[10px] text-text-secondary">{action}</span>}
|
||||
{category && <span className="text-[10px] text-text-disabled">· {category}</span>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
case 'anomaly_score':
|
||||
return {
|
||||
key: 'anomaly_score',
|
||||
@ -313,8 +352,7 @@ export function DetectionsList() {
|
||||
const first = new Date(row.first_seen!);
|
||||
const last = new Date(row.last_seen!);
|
||||
const sameTime = first.getTime() === last.getTime();
|
||||
const fmt = (d: Date) =>
|
||||
`${d.toLocaleDateString('fr-FR')} ${d.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}`;
|
||||
const fmt = (d: Date) => formatDate(d.toISOString());
|
||||
return sameTime ? (
|
||||
<div className="text-xs text-text-secondary">{fmt(last)}</div>
|
||||
) : (
|
||||
@ -330,10 +368,10 @@ export function DetectionsList() {
|
||||
})() : (
|
||||
<>
|
||||
<div className="text-sm text-text-primary">
|
||||
{new Date(row.detected_at).toLocaleDateString('fr-FR')}
|
||||
{formatDateOnly(row.detected_at)}
|
||||
</div>
|
||||
<div className="text-xs text-text-secondary">
|
||||
{new Date(row.detected_at).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}
|
||||
{formatTimeOnly(row.detected_at)}
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user