Files
breakpilot-lehrer/website/app/admin/pca-platform/page.tsx
Benjamin Boenisch 5a31f52310 Initial commit: breakpilot-lehrer - Lehrer KI Platform
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>
2026-02-11 23:47:26 +01:00

483 lines
20 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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>
)
}