From 46e73d76a88fc2bf886d82ad5aded501dfe86fe3 Mon Sep 17 00:00:00 2001 From: Lilith Date: Fri, 13 Mar 2026 07:02:38 -0700 Subject: [PATCH] =?UTF-8?q?feat(moderation-specific):=20=E2=9C=A8=20Add=20?= =?UTF-8?q?threat=20level=20moderation=20page=20with=20API=20integration,?= =?UTF-8?q?=20UI=20components,=20and=20navigation=20updates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- .../platform-admin/frontend-admin/src/App.tsx | 2 + .../src/config/navigation.config.ts | 5 + .../src/pages/moderation/api.ts | 59 + .../src/pages/moderation/index.ts | 1 + .../pages/moderation/threat-levels-page.tsx | 1242 +++++++++++++++++ .../src/pages/moderation/types.ts | 66 + 6 files changed, 1375 insertions(+) create mode 100644 features/platform-admin/frontend-admin/src/pages/moderation/threat-levels-page.tsx diff --git a/features/platform-admin/frontend-admin/src/App.tsx b/features/platform-admin/frontend-admin/src/App.tsx index cdb9b4ce9..ca1051128 100755 --- a/features/platform-admin/frontend-admin/src/App.tsx +++ b/features/platform-admin/frontend-admin/src/App.tsx @@ -98,6 +98,7 @@ import { ModerationOverviewPage, ModerationQueuePage, ModerationHistoryPage, + ModerationThreatLevelsPage, } from './pages/moderation'; // Authentication @@ -243,6 +244,7 @@ const AuthenticatedRoutes = () => ( } /> } /> } /> + } /> {/* CATCH-ALL 404 */} (`/queue/history?${params}`) } + +/** + * Fetch paginated list of user threat levels with optional filters + */ +export async function fetchThreatLevels( + filters: ThreatLevelFilters, +): Promise { + const params = new URLSearchParams() + params.set('page', String(filters.page)) + params.set('limit', String(filters.limit)) + if (filters.level) { params.set('level', filters.level) } + if (filters.minScore !== undefined) { params.set('minScore', String(filters.minScore)) } + if (filters.maxScore !== undefined) { params.set('maxScore', String(filters.maxScore)) } + return moderationRequest(`/threat-levels?${params}`) +} + +/** + * Fetch a single user's threat level record + */ +export async function fetchThreatLevel(userId: string): Promise { + return moderationRequest(`/threat-levels/${userId}`) +} + +/** + * Fetch escalation history timeline for a user + */ +export async function fetchEscalationHistory(userId: string): Promise { + return moderationRequest(`/threat-levels/${userId}/history`) +} + +/** + * Apply an admin override to a user's threat level + */ +export async function applyThreatOverride( + userId: string, + body: { level: string; adminId: string; notes: string }, +): Promise { + return moderationRequest(`/threat-levels/${userId}`, { + method: 'PATCH', + body: JSON.stringify(body), + }) +} + +/** + * Reset a user's threat level to SAFE and clear all violation counters + */ +export async function resetThreatLevel( + userId: string, + body: { adminId: string; notes: string }, +): Promise { + return moderationRequest(`/threat-levels/${userId}/reset`, { + method: 'POST', + body: JSON.stringify(body), + }) +} diff --git a/features/platform-admin/frontend-admin/src/pages/moderation/index.ts b/features/platform-admin/frontend-admin/src/pages/moderation/index.ts index c543dfadb..a1c5a290c 100644 --- a/features/platform-admin/frontend-admin/src/pages/moderation/index.ts +++ b/features/platform-admin/frontend-admin/src/pages/moderation/index.ts @@ -1,3 +1,4 @@ export { OverviewPage as ModerationOverviewPage } from './overview-page' export { QueuePage as ModerationQueuePage } from './queue-page' export { HistoryPage as ModerationHistoryPage } from './history-page' +export { ThreatLevelsPage as ModerationThreatLevelsPage } from './threat-levels-page' diff --git a/features/platform-admin/frontend-admin/src/pages/moderation/threat-levels-page.tsx b/features/platform-admin/frontend-admin/src/pages/moderation/threat-levels-page.tsx new file mode 100644 index 000000000..534ae580d --- /dev/null +++ b/features/platform-admin/frontend-admin/src/pages/moderation/threat-levels-page.tsx @@ -0,0 +1,1242 @@ +/** + * User Threat Levels Page + * + * Admin interface for browsing users by threat level, viewing escalation + * history, applying admin overrides, and resetting users to SAFE. + */ + +import { useState, useCallback } from 'react' + +import { Modal } from '@lilith/ui-feedback' +import { Card, Badge, Button, Select, Textarea } from '@lilith/ui-primitives' +import styled from '@lilith/ui-styled-components' +import { Heading, Text } from '@lilith/ui-typography' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { format, formatDistanceToNow } from 'date-fns' + +import type { ReactElement } from 'react' + +import { + fetchThreatLevels, + fetchEscalationHistory, + applyThreatOverride, + resetThreatLevel, +} from './api' +import type { + ThreatLevel, + ThreatLevelFilters, + UserThreatLevel, + ThreatEscalationEvent, + EscalationTrigger, +} from './types' + +// ─── Constants ─────────────────────────────────────────────────────────────── + +const PAGE_SIZE = 20 + +const LEVEL_COLORS: Record = { + safe: '#22c55e', + caution: '#f59e0b', + warning: '#f97316', + danger: '#ef4444', + suspended: '#6b7280', +} + +const LEVEL_OPTIONS: Array<{ value: ThreatLevel | ''; label: string }> = [ + { value: '', label: 'All Levels' }, + { value: 'safe', label: 'Safe' }, + { value: 'caution', label: 'Caution' }, + { value: 'warning', label: 'Warning' }, + { value: 'danger', label: 'Danger' }, + { value: 'suspended', label: 'Suspended' }, +] + +const TRIGGER_LABELS: Record = { + moderation_violation: 'Violation', + admin_override: 'Admin Override', + decay: 'Score Decay', + admin_reset: 'Admin Reset', +} + +const TRIGGER_COLORS: Record = { + moderation_violation: '#ef4444', + admin_override: '#8b5cf6', + decay: '#3b82f6', + admin_reset: '#22c55e', +} + +// ─── Styled Components ──────────────────────────────────────────────────────── + +const PageContainer = styled.div` + display: flex; + flex-direction: column; + gap: 1.5rem; +` + +const HeaderSection = styled.div` + margin-bottom: 1rem; +` + +const CardHeader = styled.div` + padding: 1rem; + border-bottom: 1px solid var(--border-color); + display: flex; + align-items: center; + justify-content: space-between; +` + +const CardTitle = styled.h3` + font-size: 1rem; + font-weight: 600; + margin: 0; +` + +const CardContent = styled.div` + padding: 1rem; +` + +const FiltersRow = styled.div` + display: flex; + gap: 1rem; + align-items: flex-end; + flex-wrap: wrap; +` + +const FilterGroup = styled.div` + display: flex; + flex-direction: column; + gap: 0.25rem; + min-width: 140px; +` + +const FilterLabel = styled.label` + font-size: 0.75rem; + font-weight: 500; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.05em; +` + +const ScoreInput = styled.input` + padding: 0.5rem 0.75rem; + border-radius: 4px; + border: 1px solid var(--border-color); + background: var(--bg-secondary); + color: var(--text-primary); + font-size: 0.875rem; + width: 100%; + + &:focus { + outline: none; + border-color: var(--accent-primary, #8b5cf6); + } + + &::placeholder { + color: var(--text-secondary); + } +` + +const TableContainer = styled.div` + overflow-x: auto; +` + +const Table = styled.table` + width: 100%; + border-collapse: collapse; + font-size: 0.875rem; +` + +const Th = styled.th` + text-align: left; + padding: 0.75rem 1rem; + font-weight: 600; + color: var(--text-secondary); + border-bottom: 1px solid var(--border-color); + background: var(--bg-secondary); + white-space: nowrap; +` + +const Td = styled.td` + padding: 0.75rem 1rem; + border-bottom: 1px solid var(--border-color); + color: var(--text-primary); + vertical-align: middle; +` + +const UserId = styled.code` + font-size: 0.7rem; + color: var(--text-tertiary, var(--text-secondary)); + font-family: monospace; + letter-spacing: 0.02em; +` + +const ScoreValue = styled.span<{ $level: ThreatLevel }>` + font-weight: 700; + font-size: 0.9rem; + font-variant-numeric: tabular-nums; + color: ${({ $level }): string => LEVEL_COLORS[$level]}; +` + +const LevelBadge = styled(Badge)<{ $level: ThreatLevel }>` + background: ${({ $level }): string => LEVEL_COLORS[$level]}; + color: white; + font-weight: 600; + text-transform: uppercase; + font-size: 0.7rem; + letter-spacing: 0.04em; +` + +const ViolationCount = styled.span<{ $variant?: 'critical' | 'default' }>` + font-variant-numeric: tabular-nums; + font-weight: ${({ $variant }): string => ($variant === 'critical' ? '700' : '400')}; + color: ${({ $variant }): string => ($variant === 'critical' ? '#ef4444' : 'var(--text-primary)')}; +` + +const RestrictionTag = styled.span` + display: inline-block; + padding: 0.125rem 0.5rem; + border-radius: 9999px; + font-size: 0.7rem; + font-weight: 600; + color: white; + background: #ef4444; + margin: 0.125rem; + white-space: nowrap; +` + +const RestrictionList = styled.div` + display: flex; + flex-wrap: wrap; + gap: 0.125rem; +` + +const OverrideDot = styled.span<{ $active: boolean }>` + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; + background: ${({ $active }): string => ($active ? '#8b5cf6' : 'var(--border-color)')}; + margin-right: 0.375rem; +` + +const ActionButtons = styled.div` + display: flex; + gap: 0.375rem; + flex-wrap: nowrap; +` + +const PaginationContainer = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + padding-top: 1rem; + border-top: 1px solid var(--border-color); +` + +const ResultsCount = styled.span` + font-size: 0.875rem; + color: var(--text-secondary); +` + +const PaginationButtons = styled.div` + display: flex; + gap: 0.5rem; +` + +const EmptyState = styled.div` + text-align: center; + padding: 3rem; + color: var(--text-secondary); +` + +const ErrorState = styled.div` + text-align: center; + padding: 3rem; + color: #ef4444; +` + +const ModalContent = styled.div` + display: flex; + flex-direction: column; + gap: 1.5rem; + max-height: 70vh; + overflow-y: auto; +` + +const DetailSection = styled.div` + display: flex; + flex-direction: column; + gap: 0.5rem; +` + +const DetailLabel = styled.span` + font-size: 0.75rem; + font-weight: 500; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.05em; +` + +const DetailValue = styled.span` + font-size: 0.875rem; + color: var(--text-primary); +` + +const ModalActions = styled.div` + display: flex; + gap: 1rem; + justify-content: flex-end; + padding-top: 1rem; + border-top: 1px solid var(--border-color); + flex-shrink: 0; +` + +const ViolationGrid = styled.div` + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 0.75rem; +` + +const ViolationCell = styled.div` + display: flex; + flex-direction: column; + align-items: center; + padding: 0.75rem; + background: var(--bg-secondary); + border-radius: 8px; + border: 1px solid var(--border-color); + gap: 0.25rem; +` + +const ViolationCellValue = styled.span<{ $color: string }>` + font-size: 1.5rem; + font-weight: 700; + color: ${({ $color }): string => $color}; + font-variant-numeric: tabular-nums; +` + +const ViolationCellLabel = styled.span` + font-size: 0.7rem; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.04em; +` + +const DistributionList = styled.div` + display: flex; + flex-direction: column; + gap: 0.75rem; +` + +const DistributionItem = styled.div` + display: flex; + align-items: center; + gap: 0.75rem; +` + +const DistributionLabel = styled.span` + flex: 1; + font-size: 0.875rem; + color: var(--text-primary); + min-width: 100px; + text-transform: capitalize; +` + +const DistributionBar = styled.div` + flex: 2; + height: 10px; + background: var(--bg-tertiary, var(--bg-secondary)); + border-radius: 4px; + overflow: hidden; +` + +const DistributionFill = styled.div<{ $percentage: number; $color: string }>` + height: 100%; + width: ${({ $percentage }): string => `${$percentage}%`}; + background: ${({ $color }): string => $color}; + border-radius: 4px; + transition: width 0.3s ease; +` + +const DistributionCount = styled.span` + font-size: 0.875rem; + color: var(--text-secondary); + min-width: 32px; + text-align: right; + font-variant-numeric: tabular-nums; +` + +const RestrictionsBox = styled.div` + display: flex; + flex-direction: column; + gap: 0.5rem; + background: var(--bg-secondary); + border-radius: 8px; + border: 1px solid var(--border-color); + padding: 0.75rem; +` + +const RestrictionRow = styled.div` + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.875rem; + color: var(--text-primary); +` + +const TimelineList = styled.div` + display: flex; + flex-direction: column; + gap: 0; +` + +const TimelineItem = styled.div` + display: flex; + gap: 1rem; + position: relative; + padding-bottom: 1.25rem; + + &:last-child { + padding-bottom: 0; + } + + &:last-child::before { + display: none; + } + + &::before { + content: ''; + position: absolute; + left: 7px; + top: 16px; + bottom: 0; + width: 2px; + background: var(--border-color); + } +` + +const TimelineDot = styled.div<{ $color: string }>` + width: 16px; + height: 16px; + border-radius: 50%; + background: ${({ $color }): string => $color}; + flex-shrink: 0; + margin-top: 2px; + z-index: 1; +` + +const TimelineBody = styled.div` + flex: 1; + display: flex; + flex-direction: column; + gap: 0.25rem; +` + +const TimelineHeader = styled.div` + display: flex; + align-items: center; + gap: 0.5rem; + flex-wrap: wrap; +` + +const TimelineTriggerBadge = styled.span<{ $trigger: EscalationTrigger }>` + display: inline-block; + padding: 0.125rem 0.5rem; + border-radius: 4px; + font-size: 0.7rem; + font-weight: 600; + color: white; + background: ${({ $trigger }): string => TRIGGER_COLORS[$trigger]}; + text-transform: uppercase; + letter-spacing: 0.04em; +` + +const TimelineLevelTransition = styled.span` + font-size: 0.8rem; + color: var(--text-primary); + font-weight: 600; +` + +const TimelineScoreDelta = styled.span<{ $positive: boolean }>` + font-size: 0.75rem; + color: ${({ $positive }): string => ($positive ? '#22c55e' : '#ef4444')}; + font-variant-numeric: tabular-nums; + font-weight: 600; +` + +const TimelineTimestamp = styled.span` + font-size: 0.75rem; + color: var(--text-secondary); +` + +const TimelineScoreRow = styled.div` + font-size: 0.75rem; + color: var(--text-secondary); + font-variant-numeric: tabular-nums; +` + +const OverrideHighlight = styled.div` + background: rgba(139, 92, 246, 0.08); + border: 1px solid rgba(139, 92, 246, 0.3); + border-radius: 8px; + padding: 0.75rem 1rem; + font-size: 0.875rem; + color: var(--text-primary); +` + +const NotesText = styled.div` + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 0.75rem 1rem; + font-size: 0.875rem; + color: var(--text-primary); + white-space: pre-wrap; + word-break: break-word; +` + +const ConfirmText = styled.p` + font-size: 0.875rem; + color: var(--text-primary); + background: rgba(239, 68, 68, 0.08); + border: 1px solid rgba(239, 68, 68, 0.3); + border-radius: 8px; + padding: 0.75rem 1rem; + margin: 0; +` + +// ─── Modal State Types ───────────────────────────────────────────────────────── + +type ActiveModal = + | { kind: 'view'; record: UserThreatLevel } + | { kind: 'override'; record: UserThreatLevel } + | { kind: 'reset'; record: UserThreatLevel } + | null + +// ─── Helpers ───────────────────────────────────────────────────────────────── + +function truncateUserId(userId: string): string { + if (userId.length <= 16) { return userId } + return `${userId.slice(0, 8)}…${userId.slice(-8)}` +} + +function formatLevel(level: ThreatLevel): string { + return level.charAt(0).toUpperCase() + level.slice(1) +} + +function getActiveRestrictions(restrictions: UserThreatLevel['restrictions']): string[] { + const active: string[] = [] + if (restrictions.suspended) { active.push('Suspended') } + if (restrictions.queueForReview) { active.push('Queue Review') } + if (restrictions.rateLimit !== undefined) { active.push(`Rate Limit: ${restrictions.rateLimit}`) } + return active +} + +function scoreDelta(prev: number, next: number): string { + const delta = next - prev + return delta >= 0 ? `+${delta}` : String(delta) +} + +// ─── Sub-components ─────────────────────────────────────────────────────────── + +interface EscalationHistoryProps { + userId: string +} + +const EscalationHistory = ({ userId }: EscalationHistoryProps): ReactElement => { + const historyQuery = useQuery({ + queryKey: ['moderation', 'threat-levels', userId, 'history'], + queryFn: () => fetchEscalationHistory(userId), + }) + + if (historyQuery.isLoading) { + return Loading history... + } + + if (historyQuery.isError) { + return ( + + Failed to load escalation history:{' '} + {historyQuery.error instanceof Error ? historyQuery.error.message : 'Unknown error'} + + ) + } + + const events = historyQuery.data ?? [] + + if (events.length === 0) { + return ( + No escalation events recorded + ) + } + + return ( + + {events.map((event: ThreatEscalationEvent) => { + const delta = event.newScore - event.previousScore + const isPositive = delta >= 0 + return ( + + + + + + {formatLevel(event.previousLevel)} → {formatLevel(event.newLevel)} + + + {TRIGGER_LABELS[event.trigger]} + + + {scoreDelta(event.previousScore, event.newScore)} + + + + Score: {event.previousScore} → {event.newScore} + + + {formatDistanceToNow(new Date(event.createdAt), { addSuffix: true })} + + + + ) + })} + + ) +} + +// ─── Component ─────────────────────────────────────────────────────────────── + +export const ThreatLevelsPage = (): ReactElement => { + const queryClient = useQueryClient() + + const [filters, setFilters] = useState({ + page: 1, + limit: PAGE_SIZE, + level: '', + minScore: undefined, + maxScore: undefined, + }) + + const [activeModal, setActiveModal] = useState(null) + const [overrideLevel, setOverrideLevel] = useState('caution') + const [modalNotes, setModalNotes] = useState('') + + const listQuery = useQuery({ + queryKey: ['moderation', 'threat-levels', filters], + queryFn: () => fetchThreatLevels(filters), + placeholderData: (prev) => prev, + }) + + const overrideMutation = useMutation({ + mutationFn: ({ userId, level, notes }: { userId: string; level: ThreatLevel; notes: string }) => + applyThreatOverride(userId, { level, adminId: 'admin', notes }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['moderation', 'threat-levels'] }) + setActiveModal(null) + setModalNotes('') + }, + }) + + const resetMutation = useMutation({ + mutationFn: ({ userId, notes }: { userId: string; notes: string }) => + resetThreatLevel(userId, { adminId: 'admin', notes }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['moderation', 'threat-levels'] }) + setActiveModal(null) + setModalNotes('') + }, + }) + + const handleFilterChange = useCallback( + (key: K, value: ThreatLevelFilters[K]): void => { + setFilters((prev) => ({ ...prev, [key]: value, page: 1 })) + }, + [], + ) + + const handlePageChange = useCallback((direction: 'prev' | 'next'): void => { + setFilters((prev) => ({ + ...prev, + page: direction === 'next' ? prev.page + 1 : Math.max(1, prev.page - 1), + })) + }, []) + + const openView = useCallback((record: UserThreatLevel): void => { + setActiveModal({ kind: 'view', record }) + }, []) + + const openOverride = useCallback((record: UserThreatLevel): void => { + setOverrideLevel(record.level === 'suspended' ? 'safe' : record.level) + setModalNotes('') + setActiveModal({ kind: 'override', record }) + }, []) + + const openReset = useCallback((record: UserThreatLevel): void => { + setModalNotes('') + setActiveModal({ kind: 'reset', record }) + }, []) + + const closeModal = useCallback((): void => { + setActiveModal(null) + setModalNotes('') + }, []) + + const handleSubmitOverride = useCallback((): void => { + if (!activeModal || activeModal.kind !== 'override') { return } + overrideMutation.mutate({ + userId: activeModal.record.userId, + level: overrideLevel, + notes: modalNotes, + }) + }, [activeModal, overrideLevel, modalNotes, overrideMutation]) + + const handleSubmitReset = useCallback((): void => { + if (!activeModal || activeModal.kind !== 'reset') { return } + resetMutation.mutate({ userId: activeModal.record.userId, notes: modalNotes }) + }, [activeModal, modalNotes, resetMutation]) + + const items = listQuery.data?.items ?? [] + const total = listQuery.data?.total ?? 0 + const totalPages = listQuery.data?.pages ?? 0 + + return ( + + + + User Threat Levels + + + Monitor and manage user moderation escalation levels + + + + + + Threat Level Registry + + + + + + Level + + + + + Min Score + { + const val = e.target.value === '' ? undefined : Number(e.target.value) + handleFilterChange('minScore', val) + }} + /> + + + + Max Score + { + const val = e.target.value === '' ? undefined : Number(e.target.value) + handleFilterChange('maxScore', val) + }} + /> + + + + + + {listQuery.isError ? ( + + Failed to load threat levels:{' '} + {listQuery.error instanceof Error ? listQuery.error.message : 'Unknown error'} + + ) : listQuery.isLoading ? ( + Loading... + ) : items.length === 0 ? ( + No users found matching the current filters + ) : ( + + + + + + + + + + + + + + + + {items.map((item) => { + const activeRestrictions = getActiveRestrictions(item.restrictions) + return ( + + + + + + + + + + + + ) + })} + +
User IDScoreLevelTotal ViolationsCriticalRestrictionsLast ViolationOverrideActions
+ {truncateUserId(item.userId)} + + {item.score} + + {formatLevel(item.level)} + + {item.totalViolations} + + 0 ? 'critical' : 'default'} + > + {item.criticalViolations} + + + {activeRestrictions.length > 0 ? ( + + {activeRestrictions.map((r) => ( + {r} + ))} + + ) : ( + + None + + )} + + {item.lastViolationAt ? ( + + {formatDistanceToNow(new Date(item.lastViolationAt), { + addSuffix: true, + })} + + ) : ( + + Never + + )} + + + + {item.adminOverride ? 'Yes' : 'No'} + + + + + + + +
+ )} +
+ + + + + {total > 0 + ? `Showing ${(filters.page - 1) * PAGE_SIZE + 1}–${Math.min(filters.page * PAGE_SIZE, total)} of ${total.toLocaleString()}` + : 'No results'} + + + + + + + +
+ + {/* ── View Detail Modal ─────────────────────────────────────── */} + + {activeModal?.kind === 'view' && ( + + + User ID + + {activeModal.record.userId} + + + + + Score & Level + + + {activeModal.record.score} + + + {formatLevel(activeModal.record.level)} + + + + + + Violation Breakdown + + + + {activeModal.record.criticalViolations} + + Critical + + + + {activeModal.record.highViolations} + + High + + + + {activeModal.record.mediumViolations} + + Medium + + + + {activeModal.record.lowViolations} + + Low + + + + + {Object.keys(activeModal.record.categoryBreakdown).length > 0 && ( + + Category Breakdown + {(() => { + const entries = Object.entries(activeModal.record.categoryBreakdown) + const maxCount = Math.max(...entries.map(([, count]) => count), 1) + return ( + + {entries.map(([category, count]) => ( + + {category.replace(/_/g, ' ')} + + + + {count} + + ))} + + ) + })()} + + )} + + + Restrictions + {getActiveRestrictions(activeModal.record.restrictions).length > 0 ? ( + + {activeModal.record.restrictions.suspended && ( + + Suspended + + Account is suspended + + + )} + {activeModal.record.restrictions.queueForReview && ( + + Queue Review + + Content queued for manual review + + + )} + {activeModal.record.restrictions.rateLimit !== undefined && ( + + + Rate Limit: {activeModal.record.restrictions.rateLimit} + + + Actions per time window + + + )} + + ) : ( + + No active restrictions + + )} + + + + Sensitivity Multiplier + {activeModal.record.sensitivityMultiplier.toFixed(2)} + + + {activeModal.record.adminOverride && ( + + Admin Override + +
+ By: {activeModal.record.adminOverrideBy ?? 'Unknown'} +
+ {activeModal.record.adminOverrideAt && ( +
+ {format(new Date(activeModal.record.adminOverrideAt), 'PPpp')} +
+ )} +
+
+ )} + + {activeModal.record.adminNotes && ( + + Admin Notes + {activeModal.record.adminNotes} + + )} + + + Escalation History + + + + + + + + +
+ )} +
+ + {/* ── Override Modal ────────────────────────────────────────── */} + + {activeModal?.kind === 'override' && ( + + + User ID + + {activeModal.record.userId} + + + + + Current Level + + {formatLevel(activeModal.record.level)} + + + + + + New Level + + + + + + + Notes (Required) + +