fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
482
website/app/admin/pca-platform/page.tsx
Normal file
482
website/app/admin/pca-platform/page.tsx
Normal file
@@ -0,0 +1,482 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user