This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
BreakPilot Dev 19855efacc
Some checks failed
Tests / Go Tests (push) Has been cancelled
Tests / Python Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Go Lint (push) Has been cancelled
Tests / Python Lint (push) Has been cancelled
Tests / Security Scan (push) Has been cancelled
Tests / All Checks Passed (push) Has been cancelled
Security Scanning / Secret Scanning (push) Has been cancelled
Security Scanning / Dependency Vulnerability Scan (push) Has been cancelled
Security Scanning / Go Security Scan (push) Has been cancelled
Security Scanning / Python Security Scan (push) Has been cancelled
Security Scanning / Node.js Security Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
CI/CD Pipeline / Go Tests (push) Has been cancelled
CI/CD Pipeline / Python Tests (push) Has been cancelled
CI/CD Pipeline / Website Tests (push) Has been cancelled
CI/CD Pipeline / Linting (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Docker Build & Push (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / CI Summary (push) Has been cancelled
ci/woodpecker/manual/build-ci-image Pipeline was successful
ci/woodpecker/manual/main Pipeline failed
feat: BreakPilot PWA - Full codebase (clean push without large binaries)
All services: admin-v2, studio-v2, website, ai-compliance-sdk,
consent-service, klausur-service, voice-service, and infrastructure.
Large PDFs and compiled binaries excluded via .gitignore.
2026-02-11 13:25:58 +01:00

483 lines
20 KiB
TypeScript
Raw Permalink 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>
)
}