Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website, Klausur-Service, School-Service, Voice-Service, Geo-Service, BreakPilot Drive, Agent-Core Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
483 lines
20 KiB
TypeScript
483 lines
20 KiB
TypeScript
'use client'
|
||
|
||
/**
|
||
* PCA Platform Admin Page
|
||
*
|
||
* Manages bot detection, session monitoring, and payment settings
|
||
* for the Person-Corporate-Agent platform.
|
||
*/
|
||
|
||
import { useState, useEffect, useCallback } from 'react'
|
||
import AdminLayout from '@/components/admin/AdminLayout'
|
||
|
||
// Types
|
||
interface SessionMetrics {
|
||
session_id: string
|
||
score: number
|
||
dwell_ratio: number
|
||
scroll_depth: number
|
||
clicks: number
|
||
mouse_moves: number
|
||
action: string
|
||
last_update: string
|
||
}
|
||
|
||
interface ServiceStatus {
|
||
status: 'healthy' | 'unhealthy' | 'offline'
|
||
service: string
|
||
version: string
|
||
uptime?: string
|
||
}
|
||
|
||
interface PCAConfig {
|
||
thresholds: {
|
||
score_pass: number
|
||
score_challenge: number
|
||
}
|
||
weights: {
|
||
dwell_ratio: number
|
||
scroll_score: number
|
||
pointer_variance: number
|
||
click_rate: number
|
||
}
|
||
step_up: {
|
||
methods: string[]
|
||
primary: string
|
||
}
|
||
}
|
||
|
||
interface Stats {
|
||
activeSessions: number
|
||
humanScore: number
|
||
botScore: number
|
||
challengesIssued: number
|
||
paymentsReceived: number
|
||
totalRevenue: string
|
||
}
|
||
|
||
export default function PCAAdminPage() {
|
||
const [serviceStatus, setServiceStatus] = useState<ServiceStatus | null>(null)
|
||
const [config, setConfig] = useState<PCAConfig | null>(null)
|
||
const [sessions, setSessions] = useState<SessionMetrics[]>([])
|
||
const [stats, setStats] = useState<Stats>({
|
||
activeSessions: 0,
|
||
humanScore: 0,
|
||
botScore: 0,
|
||
challengesIssued: 0,
|
||
paymentsReceived: 0,
|
||
totalRevenue: '0.00 EUR'
|
||
})
|
||
const [loading, setLoading] = useState(true)
|
||
const [error, setError] = useState<string | null>(null)
|
||
const [activeTab, setActiveTab] = useState<'overview' | 'sessions' | 'config' | 'payments'>('overview')
|
||
|
||
// Fetch service status
|
||
const fetchStatus = useCallback(async () => {
|
||
try {
|
||
const response = await fetch('/api/admin/pca?action=status')
|
||
if (response.ok) {
|
||
const data = await response.json()
|
||
setServiceStatus(data)
|
||
setError(null)
|
||
} else {
|
||
setServiceStatus({ status: 'offline', service: 'pca-heuristic-service', version: 'unknown' })
|
||
}
|
||
} catch {
|
||
setServiceStatus({ status: 'offline', service: 'pca-heuristic-service', version: 'unknown' })
|
||
}
|
||
}, [])
|
||
|
||
// Fetch config
|
||
const fetchConfig = useCallback(async () => {
|
||
try {
|
||
const response = await fetch('/api/admin/pca?action=config')
|
||
if (response.ok) {
|
||
const data = await response.json()
|
||
setConfig(data)
|
||
}
|
||
} catch {
|
||
// Use defaults
|
||
setConfig({
|
||
thresholds: { score_pass: 0.7, score_challenge: 0.4 },
|
||
weights: { dwell_ratio: 0.30, scroll_score: 0.25, pointer_variance: 0.20, click_rate: 0.25 },
|
||
step_up: { methods: ['webauthn', 'pow'], primary: 'webauthn' }
|
||
})
|
||
}
|
||
}, [])
|
||
|
||
// Fetch sessions (mock for now)
|
||
const fetchSessions = useCallback(async () => {
|
||
// Mock data - in production would fetch from backend
|
||
setSessions([
|
||
{ session_id: 'pca_abc123', score: 0.85, dwell_ratio: 0.92, scroll_depth: 65, clicks: 12, mouse_moves: 234, action: 'allow', last_update: new Date().toISOString() },
|
||
{ session_id: 'pca_def456', score: 0.32, dwell_ratio: 0.15, scroll_depth: 5, clicks: 0, mouse_moves: 3, action: 'challenge', last_update: new Date().toISOString() },
|
||
{ session_id: 'pca_ghi789', score: 0.71, dwell_ratio: 0.78, scroll_depth: 45, clicks: 8, mouse_moves: 156, action: 'allow', last_update: new Date().toISOString() },
|
||
])
|
||
}, [])
|
||
|
||
useEffect(() => {
|
||
const init = async () => {
|
||
setLoading(true)
|
||
await Promise.all([fetchStatus(), fetchConfig(), fetchSessions()])
|
||
setLoading(false)
|
||
}
|
||
init()
|
||
|
||
// Refresh every 10 seconds
|
||
const interval = setInterval(() => {
|
||
fetchStatus()
|
||
fetchSessions()
|
||
}, 10000)
|
||
|
||
return () => clearInterval(interval)
|
||
}, [fetchStatus, fetchConfig, fetchSessions])
|
||
|
||
// Score color helper
|
||
const getScoreColor = (score: number) => {
|
||
if (score >= 0.7) return 'text-green-600 bg-green-100'
|
||
if (score >= 0.4) return 'text-yellow-600 bg-yellow-100'
|
||
return 'text-red-600 bg-red-100'
|
||
}
|
||
|
||
const getScoreLabel = (score: number) => {
|
||
if (score >= 0.7) return 'Human'
|
||
if (score >= 0.4) return 'Unsicher'
|
||
return 'Bot'
|
||
}
|
||
|
||
return (
|
||
<AdminLayout title="PCA Platform" description="Bot-Erkennung & Monetarisierung verwalten">
|
||
{/* Loading State */}
|
||
{loading && (
|
||
<div className="flex items-center justify-center py-12">
|
||
<svg className="w-8 h-8 animate-spin text-primary-600" fill="none" viewBox="0 0 24 24">
|
||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||
</svg>
|
||
<span className="ml-3 text-slate-600">Lade PCA Status...</span>
|
||
</div>
|
||
)}
|
||
|
||
{/* Error State */}
|
||
{error && !loading && (
|
||
<div className="mb-6 bg-red-50 border border-red-200 rounded-lg p-4">
|
||
<div className="flex items-center gap-3">
|
||
<span className="text-red-500 text-xl">⚠️</span>
|
||
<div>
|
||
<h4 className="font-medium text-red-800">{error}</h4>
|
||
<button onClick={fetchStatus} className="mt-2 text-sm text-red-700 underline hover:no-underline">
|
||
Erneut versuchen
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Service Status Banner */}
|
||
{!loading && serviceStatus && (
|
||
<div className={`mb-6 rounded-lg p-4 ${
|
||
serviceStatus.status === 'healthy' ? 'bg-green-50 border border-green-200' :
|
||
serviceStatus.status === 'offline' ? 'bg-red-50 border border-red-200' :
|
||
'bg-yellow-50 border border-yellow-200'
|
||
}`}>
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center gap-3">
|
||
<span className={`w-3 h-3 rounded-full ${
|
||
serviceStatus.status === 'healthy' ? 'bg-green-500' :
|
||
serviceStatus.status === 'offline' ? 'bg-red-500' : 'bg-yellow-500'
|
||
}`} />
|
||
<div>
|
||
<span className="font-medium">{serviceStatus.service}</span>
|
||
<span className="text-sm text-slate-500 ml-2">v{serviceStatus.version}</span>
|
||
</div>
|
||
</div>
|
||
<span className={`px-3 py-1 rounded-full text-sm font-medium ${
|
||
serviceStatus.status === 'healthy' ? 'bg-green-100 text-green-700' :
|
||
serviceStatus.status === 'offline' ? 'bg-red-100 text-red-700' : 'bg-yellow-100 text-yellow-700'
|
||
}`}>
|
||
{serviceStatus.status === 'healthy' ? 'Online' :
|
||
serviceStatus.status === 'offline' ? 'Offline' : 'Degraded'}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Tabs */}
|
||
{!loading && (
|
||
<div className="border-b border-slate-200 mb-6">
|
||
<nav className="flex gap-4">
|
||
{[
|
||
{ id: 'overview', label: 'Übersicht', icon: '📊' },
|
||
{ id: 'sessions', label: 'Sessions', icon: '👥' },
|
||
{ id: 'config', label: 'Konfiguration', icon: '⚙️' },
|
||
{ id: 'payments', label: 'Payments', icon: '💳' },
|
||
].map(tab => (
|
||
<button
|
||
key={tab.id}
|
||
onClick={() => setActiveTab(tab.id as typeof activeTab)}
|
||
className={`px-4 py-3 font-medium border-b-2 transition-colors ${
|
||
activeTab === tab.id
|
||
? 'border-primary-500 text-primary-600'
|
||
: 'border-transparent text-slate-500 hover:text-slate-700'
|
||
}`}
|
||
>
|
||
<span className="mr-2">{tab.icon}</span>
|
||
{tab.label}
|
||
</button>
|
||
))}
|
||
</nav>
|
||
</div>
|
||
)}
|
||
|
||
{/* Overview Tab */}
|
||
{!loading && activeTab === 'overview' && (
|
||
<div className="space-y-6">
|
||
{/* Stats Grid */}
|
||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||
<div className="bg-white rounded-lg border border-slate-200 p-4">
|
||
<div className="text-2xl font-bold text-slate-800">{stats.activeSessions}</div>
|
||
<div className="text-sm text-slate-500">Aktive Sessions</div>
|
||
</div>
|
||
<div className="bg-white rounded-lg border border-slate-200 p-4">
|
||
<div className="text-2xl font-bold text-green-600">{stats.humanScore}%</div>
|
||
<div className="text-sm text-slate-500">Human Score Avg</div>
|
||
</div>
|
||
<div className="bg-white rounded-lg border border-slate-200 p-4">
|
||
<div className="text-2xl font-bold text-yellow-600">{stats.challengesIssued}</div>
|
||
<div className="text-sm text-slate-500">Challenges ausgestellt</div>
|
||
</div>
|
||
<div className="bg-white rounded-lg border border-slate-200 p-4">
|
||
<div className="text-2xl font-bold text-primary-600">{stats.totalRevenue}</div>
|
||
<div className="text-sm text-slate-500">Umsatz (Bot-Zugriffe)</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Quick Actions */}
|
||
<div className="bg-white rounded-lg border border-slate-200 p-6">
|
||
<h3 className="text-lg font-semibold mb-4">Quick Actions</h3>
|
||
<div className="flex flex-wrap gap-3">
|
||
<button className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors">
|
||
SDK herunterladen
|
||
</button>
|
||
<button className="px-4 py-2 bg-slate-100 text-slate-700 rounded-lg hover:bg-slate-200 transition-colors">
|
||
Demo öffnen
|
||
</button>
|
||
<button className="px-4 py-2 bg-slate-100 text-slate-700 rounded-lg hover:bg-slate-200 transition-colors">
|
||
Dokumentation
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Heuristic Weights Visualization */}
|
||
{config && (
|
||
<div className="bg-white rounded-lg border border-slate-200 p-6">
|
||
<h3 className="text-lg font-semibold mb-4">Heuristik-Gewichtung</h3>
|
||
<div className="space-y-3">
|
||
{[
|
||
{ key: 'dwell_ratio', label: 'Verweildauer', value: config.weights.dwell_ratio },
|
||
{ key: 'scroll_score', label: 'Scroll-Verhalten', value: config.weights.scroll_score },
|
||
{ key: 'pointer_variance', label: 'Maus-Bewegungen', value: config.weights.pointer_variance },
|
||
{ key: 'click_rate', label: 'Klick-Muster', value: config.weights.click_rate },
|
||
].map(metric => (
|
||
<div key={metric.key} className="flex items-center gap-4">
|
||
<div className="w-32 text-sm text-slate-600">{metric.label}</div>
|
||
<div className="flex-1 bg-slate-100 rounded-full h-4">
|
||
<div
|
||
className="bg-primary-500 h-4 rounded-full transition-all"
|
||
style={{ width: `${metric.value * 100}%` }}
|
||
/>
|
||
</div>
|
||
<div className="w-12 text-right text-sm font-medium">{(metric.value * 100).toFixed(0)}%</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* Sessions Tab */}
|
||
{!loading && activeTab === 'sessions' && (
|
||
<div className="bg-white rounded-lg border border-slate-200 overflow-hidden">
|
||
<div className="p-4 border-b border-slate-200 flex justify-between items-center">
|
||
<h3 className="font-semibold">Aktive Sessions</h3>
|
||
<button onClick={fetchSessions} className="text-sm text-primary-600 hover:underline">
|
||
Aktualisieren
|
||
</button>
|
||
</div>
|
||
<div className="overflow-x-auto">
|
||
<table className="w-full">
|
||
<thead className="bg-slate-50">
|
||
<tr>
|
||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Session ID</th>
|
||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Score</th>
|
||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Verweildauer</th>
|
||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Scroll</th>
|
||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Klicks</th>
|
||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Aktion</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y divide-slate-200">
|
||
{sessions.map(session => (
|
||
<tr key={session.session_id} className="hover:bg-slate-50">
|
||
<td className="px-4 py-3 font-mono text-sm">{session.session_id}</td>
|
||
<td className="px-4 py-3">
|
||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getScoreColor(session.score)}`}>
|
||
{session.score.toFixed(2)} - {getScoreLabel(session.score)}
|
||
</span>
|
||
</td>
|
||
<td className="px-4 py-3 text-sm">{(session.dwell_ratio * 100).toFixed(0)}%</td>
|
||
<td className="px-4 py-3 text-sm">{session.scroll_depth}%</td>
|
||
<td className="px-4 py-3 text-sm">{session.clicks}</td>
|
||
<td className="px-4 py-3">
|
||
<span className={`px-2 py-1 rounded text-xs font-medium ${
|
||
session.action === 'allow' ? 'bg-green-100 text-green-700' : 'bg-yellow-100 text-yellow-700'
|
||
}`}>
|
||
{session.action === 'allow' ? 'Erlaubt' : 'Challenge'}
|
||
</span>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Config Tab */}
|
||
{!loading && activeTab === 'config' && config && (
|
||
<div className="space-y-6">
|
||
{/* Thresholds */}
|
||
<div className="bg-white rounded-lg border border-slate-200 p-6">
|
||
<h3 className="text-lg font-semibold mb-4">Score-Schwellenwerte</h3>
|
||
<div className="grid grid-cols-2 gap-6">
|
||
<div>
|
||
<label className="block text-sm font-medium text-slate-700 mb-2">
|
||
Pass Score (Human erkannt)
|
||
</label>
|
||
<input
|
||
type="number"
|
||
value={config.thresholds.score_pass}
|
||
step="0.1"
|
||
min="0"
|
||
max="1"
|
||
className="w-full px-3 py-2 border border-slate-300 rounded-lg"
|
||
readOnly
|
||
/>
|
||
<p className="mt-1 text-xs text-slate-500">Score ab dem ein Besucher als Mensch gilt</p>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium text-slate-700 mb-2">
|
||
Challenge Score
|
||
</label>
|
||
<input
|
||
type="number"
|
||
value={config.thresholds.score_challenge}
|
||
step="0.1"
|
||
min="0"
|
||
max="1"
|
||
className="w-full px-3 py-2 border border-slate-300 rounded-lg"
|
||
readOnly
|
||
/>
|
||
<p className="mt-1 text-xs text-slate-500">Score unter dem Step-Up erforderlich ist</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Step-Up Methods */}
|
||
<div className="bg-white rounded-lg border border-slate-200 p-6">
|
||
<h3 className="text-lg font-semibold mb-4">Step-Up Methoden</h3>
|
||
<div className="space-y-3">
|
||
{config.step_up.methods.map(method => (
|
||
<div key={method} className="flex items-center justify-between p-3 bg-slate-50 rounded-lg">
|
||
<div className="flex items-center gap-3">
|
||
<span className="text-xl">{method === 'webauthn' ? '🔐' : '⚡'}</span>
|
||
<div>
|
||
<div className="font-medium">{method === 'webauthn' ? 'WebAuthn' : 'Proof-of-Work'}</div>
|
||
<div className="text-sm text-slate-500">
|
||
{method === 'webauthn' ? 'Biometrische Authentifizierung' : 'Rechenintensive Challenge'}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{method === config.step_up.primary && (
|
||
<span className="px-2 py-1 bg-primary-100 text-primary-700 rounded text-xs font-medium">
|
||
Primär
|
||
</span>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* ai-access.json Preview */}
|
||
<div className="bg-white rounded-lg border border-slate-200 p-6">
|
||
<h3 className="text-lg font-semibold mb-4">ai-access.json</h3>
|
||
<pre className="bg-slate-900 text-slate-100 p-4 rounded-lg overflow-x-auto text-sm">
|
||
{`{
|
||
"thresholds": {
|
||
"score_pass": ${config.thresholds.score_pass},
|
||
"score_challenge": ${config.thresholds.score_challenge}
|
||
},
|
||
"weights": {
|
||
"dwell_ratio": ${config.weights.dwell_ratio},
|
||
"scroll_score": ${config.weights.scroll_score},
|
||
"pointer_variance": ${config.weights.pointer_variance},
|
||
"click_rate": ${config.weights.click_rate}
|
||
},
|
||
"step_up": {
|
||
"methods": ${JSON.stringify(config.step_up.methods)},
|
||
"primary": "${config.step_up.primary}"
|
||
},
|
||
"pca_roles": {
|
||
"Person": { "access": "allow", "price": null },
|
||
"Corporate": { "access": "allow", "price": null },
|
||
"Agent": { "access": "charge", "price": "0.001 EUR" }
|
||
}
|
||
}`}
|
||
</pre>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Payments Tab */}
|
||
{!loading && activeTab === 'payments' && (
|
||
<div className="space-y-6">
|
||
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
|
||
<div className="flex items-center gap-3">
|
||
<span className="text-yellow-500 text-xl">🚧</span>
|
||
<div>
|
||
<h4 className="font-medium text-yellow-800">Payment Gateway in Entwicklung</h4>
|
||
<p className="text-sm text-yellow-700 mt-1">
|
||
HTTP 402 Payment Required und Stablecoin-Integration werden in einer zukünftigen Version verfügbar sein.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="bg-white rounded-lg border border-slate-200 p-6">
|
||
<h3 className="text-lg font-semibold mb-4">Geplante Features</h3>
|
||
<ul className="space-y-3">
|
||
<li className="flex items-center gap-3">
|
||
<span className="w-6 h-6 rounded-full bg-slate-200 flex items-center justify-center text-xs">1</span>
|
||
<span>Micropayments per Request (Pay-per-Crawl)</span>
|
||
</li>
|
||
<li className="flex items-center gap-3">
|
||
<span className="w-6 h-6 rounded-full bg-slate-200 flex items-center justify-center text-xs">2</span>
|
||
<span>Stablecoin Support (USDC, EURC)</span>
|
||
</li>
|
||
<li className="flex items-center gap-3">
|
||
<span className="w-6 h-6 rounded-full bg-slate-200 flex items-center justify-center text-xs">3</span>
|
||
<span>Bitcoin Lightning Integration</span>
|
||
</li>
|
||
<li className="flex items-center gap-3">
|
||
<span className="w-6 h-6 rounded-full bg-slate-200 flex items-center justify-center text-xs">4</span>
|
||
<span>Agent Wallet Verwaltung</span>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</AdminLayout>
|
||
)
|
||
}
|