Files
whyrating-engine-legacy/web/components/JobDevTools/SessionPanel.tsx
Alejandro Gutiérrez 2637d982e0 Wave 4: JobDevTools UI components and crash report API
- Task #5: Create JobDevTools container component
  (tabs: All/Scraper/Browser/Network/System, level filters, count badges)
- Task #11: Add crash report API endpoints
  (GET /jobs/{id}/crash-report, POST /jobs/{id}/retry?apply_fix=true, GET /crashes/stats)
- Task #14: Create SessionPanel component
  (fingerprint display, bot detection indicators, collapsible sections)
- Task #15: Create MetricsDashboard with recharts
  (extraction rate, cumulative reviews, memory usage, scroll progress)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 12:37:56 +00:00

212 lines
8.5 KiB
TypeScript

'use client';
import { useState } from 'react';
import { ChevronDown, ChevronRight, User, Globe, Monitor, Cpu, Shield, Check, X, AlertTriangle } from 'lucide-react';
export interface SessionFingerprint {
user_agent: string;
platform: string;
language: string;
languages: string[];
timezone: string;
screen: { width: number; height: number; colorDepth: number };
viewport: { width: number; height: number };
webgl_vendor: string;
webgl_renderer: string;
canvas_fingerprint: string;
hardware_concurrency: number;
device_memory: number;
bot_detection_tests: {
webdriver_hidden: boolean;
chrome_runtime: boolean;
permissions_query: boolean;
};
captured_at: string;
}
interface SessionPanelProps {
fingerprint: SessionFingerprint;
}
function BotTestIndicator({ passed, label }: { passed: boolean | null | undefined; label: string }) {
if (passed === null || passed === undefined) {
return (
<div className="flex items-center gap-2 px-3 py-2 bg-yellow-900/30 border border-yellow-700/50 rounded-lg">
<AlertTriangle className="w-4 h-4 text-yellow-500" />
<span className="text-yellow-300 text-sm font-medium">{label}</span>
<span className="ml-auto text-yellow-500 text-xs font-mono">UNKNOWN</span>
</div>
);
}
if (passed) {
return (
<div className="flex items-center gap-2 px-3 py-2 bg-green-900/30 border border-green-700/50 rounded-lg">
<Check className="w-4 h-4 text-green-500" />
<span className="text-green-300 text-sm font-medium">{label}</span>
<span className="ml-auto text-green-500 text-xs font-mono">PASSED</span>
</div>
);
}
return (
<div className="flex items-center gap-2 px-3 py-2 bg-red-900/30 border border-red-700/50 rounded-lg">
<X className="w-4 h-4 text-red-500" />
<span className="text-red-300 text-sm font-medium">{label}</span>
<span className="ml-auto text-red-500 text-xs font-mono">FAILED</span>
</div>
);
}
function SectionHeader({ icon: Icon, title }: { icon: React.ElementType; title: string }) {
return (
<div className="flex items-center gap-2 mb-3 pb-2 border-b border-gray-700">
<Icon className="w-4 h-4 text-blue-400" />
<h4 className="text-sm font-semibold text-gray-300 uppercase tracking-wide">{title}</h4>
</div>
);
}
function DataRow({ label, value, mono = true }: { label: string; value: string | number; mono?: boolean }) {
return (
<div className="flex flex-col gap-0.5">
<span className="text-xs text-gray-500 uppercase tracking-wide">{label}</span>
<span className={`text-sm text-gray-200 ${mono ? 'font-mono' : ''} break-all`}>{value}</span>
</div>
);
}
export default function SessionPanel({ fingerprint }: SessionPanelProps) {
const [isExpanded, setIsExpanded] = useState(false);
// Calculate overall bot detection status
const tests = fingerprint.bot_detection_tests;
const testResults = [tests.webdriver_hidden, tests.chrome_runtime, tests.permissions_query];
const passedCount = testResults.filter(t => t === true).length;
const failedCount = testResults.filter(t => t === false).length;
const unknownCount = testResults.filter(t => t === null || t === undefined).length;
const overallStatus = failedCount > 0 ? 'warning' : unknownCount > 0 ? 'partial' : 'success';
const statusColors = {
success: 'bg-green-900/30 border-green-700/50 text-green-400',
partial: 'bg-yellow-900/30 border-yellow-700/50 text-yellow-400',
warning: 'bg-red-900/30 border-red-700/50 text-red-400',
};
return (
<div className="bg-gray-800 border border-gray-700 rounded-lg overflow-hidden">
{/* Collapsible Header */}
<button
onClick={() => setIsExpanded(!isExpanded)}
className="w-full flex items-center justify-between px-4 py-3 bg-gray-800 hover:bg-gray-750 transition-colors"
>
<div className="flex items-center gap-3">
{isExpanded ? (
<ChevronDown className="w-5 h-5 text-gray-400" />
) : (
<ChevronRight className="w-5 h-5 text-gray-400" />
)}
<Shield className="w-5 h-5 text-blue-400" />
<span className="text-sm font-semibold text-gray-200">What Google Saw</span>
</div>
<div className={`flex items-center gap-2 px-2.5 py-1 rounded-full text-xs font-semibold border ${statusColors[overallStatus]}`}>
{overallStatus === 'success' && <Check className="w-3 h-3" />}
{overallStatus === 'partial' && <AlertTriangle className="w-3 h-3" />}
{overallStatus === 'warning' && <X className="w-3 h-3" />}
<span>
{passedCount}/{testResults.length} Tests Passed
</span>
</div>
</button>
{/* Collapsible Content */}
{isExpanded && (
<div className="px-4 pb-4 border-t border-gray-700">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 pt-4">
{/* Identity Section */}
<div className="space-y-3">
<SectionHeader icon={User} title="Identity" />
<div className="space-y-3 bg-gray-900/50 rounded-lg p-3">
<DataRow label="User Agent" value={fingerprint.user_agent} />
<DataRow label="Platform" value={fingerprint.platform} />
<DataRow label="Primary Language" value={fingerprint.language} />
<DataRow label="Languages" value={fingerprint.languages.join(', ')} />
</div>
</div>
{/* Geolocation Section */}
<div className="space-y-3">
<SectionHeader icon={Globe} title="Geolocation" />
<div className="space-y-3 bg-gray-900/50 rounded-lg p-3">
<DataRow label="Timezone" value={fingerprint.timezone} />
<DataRow label="Captured At" value={fingerprint.captured_at} />
</div>
</div>
{/* Display Section */}
<div className="space-y-3">
<SectionHeader icon={Monitor} title="Display" />
<div className="space-y-3 bg-gray-900/50 rounded-lg p-3">
<DataRow
label="Screen Resolution"
value={`${fingerprint.screen.width} x ${fingerprint.screen.height}`}
/>
<DataRow
label="Viewport Size"
value={`${fingerprint.viewport.width} x ${fingerprint.viewport.height}`}
/>
<DataRow
label="Color Depth"
value={`${fingerprint.screen.colorDepth}-bit`}
/>
</div>
</div>
{/* Hardware Section */}
<div className="space-y-3">
<SectionHeader icon={Cpu} title="Hardware" />
<div className="space-y-3 bg-gray-900/50 rounded-lg p-3">
<DataRow label="WebGL Vendor" value={fingerprint.webgl_vendor} />
<DataRow label="WebGL Renderer" value={fingerprint.webgl_renderer} />
<DataRow label="CPU Cores" value={fingerprint.hardware_concurrency} />
<DataRow label="Device Memory" value={`${fingerprint.device_memory} GB`} />
<DataRow label="Canvas Fingerprint" value={fingerprint.canvas_fingerprint} />
</div>
</div>
</div>
{/* Bot Detection Section - Full Width */}
<div className="mt-6 space-y-3">
<SectionHeader icon={Shield} title="Bot Detection Tests" />
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<BotTestIndicator
passed={tests.webdriver_hidden}
label="WebDriver Hidden"
/>
<BotTestIndicator
passed={tests.chrome_runtime}
label="Chrome Runtime"
/>
<BotTestIndicator
passed={tests.permissions_query}
label="Permissions Query"
/>
</div>
<div className="mt-3 text-xs text-gray-500 bg-gray-900/30 rounded-lg p-3">
<p>
<span className="text-green-400 font-semibold">Green checkmark</span> = Test passed (bot detection evaded)
</p>
<p>
<span className="text-red-400 font-semibold">Red X</span> = Test failed (may have been detected as a bot)
</p>
<p>
<span className="text-yellow-400 font-semibold">Yellow warning</span> = Test result unknown
</p>
</div>
</div>
</div>
)}
</div>
);
}