fix(admin-v2): Restore complete admin-v2 application

The admin-v2 application was incomplete in the repository. This commit
restores all missing components:

- Admin pages (76 pages): dashboard, ai, compliance, dsgvo, education,
  infrastructure, communication, development, onboarding, rbac
- SDK pages (45 pages): tom, dsfa, vvt, loeschfristen, einwilligungen,
  vendor-compliance, tom-generator, dsr, and more
- Developer portal (25 pages): API docs, SDK guides, frameworks
- All components, lib files, hooks, and types
- Updated package.json with all dependencies

The issue was caused by incomplete initial repository state - the full
admin-v2 codebase existed in backend/admin-v2 and docs-src/admin-v2
but was never fully synced to the main admin-v2 directory.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
BreakPilot Dev
2026-02-08 23:40:15 -08:00
parent f28244753f
commit 660295e218
385 changed files with 138126 additions and 3079 deletions

View File

@@ -0,0 +1,338 @@
import { NextRequest, NextResponse } from 'next/server'
/**
* Mac Mini System Monitoring API
*
* Provides system stats and Docker container management
* Requires Docker socket mounted at /var/run/docker.sock
*/
interface ContainerInfo {
id: string
name: string
image: string
status: string
state: string
created: string
ports: string[]
cpu_percent: number
memory_usage: string
memory_limit: string
memory_percent: number
network_rx: string
network_tx: string
}
interface SystemStats {
hostname: string
platform: string
arch: string
uptime: number
cpu: {
model: string
cores: number
usage_percent: number
}
memory: {
total: string
used: string
free: string
usage_percent: number
}
disk: {
total: string
used: string
free: string
usage_percent: number
}
timestamp: string
}
interface DockerStats {
containers: ContainerInfo[]
total_containers: number
running_containers: number
stopped_containers: number
}
// Helper to format bytes
function formatBytes(bytes: number): string {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
// Helper to format uptime
function formatUptime(seconds: number): string {
const days = Math.floor(seconds / 86400)
const hours = Math.floor((seconds % 86400) / 3600)
const minutes = Math.floor((seconds % 3600) / 60)
if (days > 0) return `${days}d ${hours}h ${minutes}m`
if (hours > 0) return `${hours}h ${minutes}m`
return `${minutes}m`
}
// Get Docker stats via socket
async function getDockerStats(): Promise<DockerStats> {
const DOCKER_SOCKET = process.env.DOCKER_HOST || 'unix:///var/run/docker.sock'
try {
// Fetch container list
const containersResponse = await fetch(`${DOCKER_SOCKET.replace('unix://', 'http://localhost')}/containers/json?all=true`, {
// @ts-expect-error - Node.js fetch supports unix sockets via socketPath
socketPath: '/var/run/docker.sock',
})
if (!containersResponse.ok) {
throw new Error('Failed to fetch containers')
}
const containers = await containersResponse.json()
// Get stats for running containers
const containerInfos: ContainerInfo[] = await Promise.all(
containers.map(async (container: Record<string, unknown>) => {
const names = container.Names as string[]
const name = names?.[0]?.replace(/^\//, '') || 'unknown'
const state = container.State as string
const status = container.Status as string
const image = container.Image as string
const created = container.Created as number
const ports = container.Ports as Array<{ PrivatePort: number; PublicPort?: number; Type: string }>
let cpu_percent = 0
let memory_usage = '0 B'
let memory_limit = '0 B'
let memory_percent = 0
let network_rx = '0 B'
let network_tx = '0 B'
// Get live stats for running containers
if (state === 'running') {
try {
const statsResponse = await fetch(
`http://localhost/containers/${container.Id}/stats?stream=false`,
{
// @ts-expect-error - Node.js fetch supports unix sockets
socketPath: '/var/run/docker.sock',
}
)
if (statsResponse.ok) {
const stats = await statsResponse.json()
// Calculate CPU usage
const cpuDelta = stats.cpu_stats.cpu_usage.total_usage -
(stats.precpu_stats?.cpu_usage?.total_usage || 0)
const systemDelta = stats.cpu_stats.system_cpu_usage -
(stats.precpu_stats?.system_cpu_usage || 0)
const cpuCount = stats.cpu_stats.online_cpus || 1
if (systemDelta > 0 && cpuDelta > 0) {
cpu_percent = (cpuDelta / systemDelta) * cpuCount * 100
}
// Memory usage
const memUsage = stats.memory_stats?.usage || 0
const memLimit = stats.memory_stats?.limit || 0
memory_usage = formatBytes(memUsage)
memory_limit = formatBytes(memLimit)
memory_percent = memLimit > 0 ? (memUsage / memLimit) * 100 : 0
// Network stats
const networks = stats.networks || {}
let rxBytes = 0
let txBytes = 0
Object.values(networks).forEach((net: unknown) => {
const network = net as { rx_bytes?: number; tx_bytes?: number }
rxBytes += network.rx_bytes || 0
txBytes += network.tx_bytes || 0
})
network_rx = formatBytes(rxBytes)
network_tx = formatBytes(txBytes)
}
} catch {
// Stats not available, use defaults
}
}
return {
id: (container.Id as string).substring(0, 12),
name,
image: (image as string).split(':')[0].split('/').pop() || image,
status,
state,
created: new Date(created * 1000).toISOString(),
ports: ports?.map(p =>
p.PublicPort ? `${p.PublicPort}:${p.PrivatePort}/${p.Type}` : `${p.PrivatePort}/${p.Type}`
) || [],
cpu_percent: Math.round(cpu_percent * 100) / 100,
memory_usage,
memory_limit,
memory_percent: Math.round(memory_percent * 100) / 100,
network_rx,
network_tx,
}
})
)
// Sort by name
containerInfos.sort((a, b) => a.name.localeCompare(b.name))
return {
containers: containerInfos,
total_containers: containerInfos.length,
running_containers: containerInfos.filter(c => c.state === 'running').length,
stopped_containers: containerInfos.filter(c => c.state !== 'running').length,
}
} catch (error) {
console.error('Docker stats error:', error)
// Return empty stats if Docker socket not available
return {
containers: [],
total_containers: 0,
running_containers: 0,
stopped_containers: 0,
}
}
}
// Get system stats
async function getSystemStats(): Promise<SystemStats> {
const os = await import('os')
const cpus = os.cpus()
const totalMem = os.totalmem()
const freeMem = os.freemem()
const usedMem = totalMem - freeMem
// Calculate CPU usage from cpus
let totalIdle = 0
let totalTick = 0
cpus.forEach(cpu => {
for (const type in cpu.times) {
totalTick += cpu.times[type as keyof typeof cpu.times]
}
totalIdle += cpu.times.idle
})
const cpuUsage = 100 - (totalIdle / totalTick * 100)
// Disk stats (root partition)
let diskTotal = 0
let diskUsed = 0
let diskFree = 0
try {
const { execSync } = await import('child_process')
const dfOutput = execSync('df -k / | tail -1').toString()
const parts = dfOutput.split(/\s+/)
diskTotal = parseInt(parts[1]) * 1024
diskUsed = parseInt(parts[2]) * 1024
diskFree = parseInt(parts[3]) * 1024
} catch {
// Disk stats not available
}
return {
hostname: os.hostname(),
platform: os.platform(),
arch: os.arch(),
uptime: os.uptime(),
cpu: {
model: cpus[0]?.model || 'Unknown',
cores: cpus.length,
usage_percent: Math.round(cpuUsage * 100) / 100,
},
memory: {
total: formatBytes(totalMem),
used: formatBytes(usedMem),
free: formatBytes(freeMem),
usage_percent: Math.round((usedMem / totalMem) * 100 * 100) / 100,
},
disk: {
total: formatBytes(diskTotal),
used: formatBytes(diskUsed),
free: formatBytes(diskFree),
usage_percent: diskTotal > 0 ? Math.round((diskUsed / diskTotal) * 100 * 100) / 100 : 0,
},
timestamp: new Date().toISOString(),
}
}
// Container action (start/stop/restart)
async function containerAction(containerId: string, action: 'start' | 'stop' | 'restart'): Promise<void> {
const response = await fetch(
`http://localhost/containers/${containerId}/${action}`,
{
method: 'POST',
// @ts-expect-error - Node.js fetch supports unix sockets
socketPath: '/var/run/docker.sock',
}
)
if (!response.ok && response.status !== 304) {
const error = await response.text()
throw new Error(`Failed to ${action} container: ${error}`)
}
}
// GET - Fetch system and Docker stats
export async function GET() {
try {
const [system, docker] = await Promise.all([
getSystemStats(),
getDockerStats(),
])
return NextResponse.json({
system,
docker,
timestamp: new Date().toISOString(),
})
} catch (error) {
console.error('Mac Mini stats error:', error)
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Unknown error' },
{ status: 500 }
)
}
}
// POST - Container actions (start/stop/restart)
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { container_id, action } = body
if (!container_id || !action) {
return NextResponse.json(
{ error: 'container_id and action required' },
{ status: 400 }
)
}
if (!['start', 'stop', 'restart'].includes(action)) {
return NextResponse.json(
{ error: 'Invalid action. Use: start, stop, restart' },
{ status: 400 }
)
}
await containerAction(container_id, action)
return NextResponse.json({
success: true,
message: `Container ${action} successful`,
container_id,
action,
})
} catch (error) {
console.error('Container action error:', error)
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Action failed' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,208 @@
import { NextRequest, NextResponse } from 'next/server'
// Woodpecker API configuration
const WOODPECKER_URL = process.env.WOODPECKER_URL || 'http://woodpecker-server:8000'
const WOODPECKER_TOKEN = process.env.WOODPECKER_TOKEN || ''
export interface PipelineStep {
name: string
state: 'pending' | 'running' | 'success' | 'failure' | 'skipped'
exit_code: number
error?: string
}
export interface Pipeline {
id: number
number: number
status: 'pending' | 'running' | 'success' | 'failure' | 'error'
event: string
branch: string
commit: string
message: string
author: string
created: number
started: number
finished: number
steps: PipelineStep[]
errors?: string[]
}
export interface WoodpeckerStatusResponse {
status: 'online' | 'offline'
pipelines: Pipeline[]
lastUpdate: string
error?: string
}
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams
const repoId = searchParams.get('repo') || '1'
const limit = parseInt(searchParams.get('limit') || '10')
try {
// Fetch pipelines from Woodpecker API
const response = await fetch(
`${WOODPECKER_URL}/api/repos/${repoId}/pipelines?per_page=${limit}`,
{
headers: {
'Authorization': `Bearer ${WOODPECKER_TOKEN}`,
'Content-Type': 'application/json',
},
cache: 'no-store',
}
)
if (!response.ok) {
return NextResponse.json({
status: 'offline',
pipelines: [],
lastUpdate: new Date().toISOString(),
error: `Woodpecker API nicht erreichbar (${response.status})`
} as WoodpeckerStatusResponse)
}
const rawPipelines = await response.json()
// Transform pipelines to our format
const pipelines: Pipeline[] = rawPipelines.map((p: any) => {
// Extract errors from workflows/steps
const errors: string[] = []
const steps: PipelineStep[] = []
if (p.workflows) {
for (const workflow of p.workflows) {
if (workflow.children) {
for (const child of workflow.children) {
steps.push({
name: child.name,
state: child.state,
exit_code: child.exit_code,
error: child.error
})
if (child.state === 'failure' && child.error) {
errors.push(`${child.name}: ${child.error}`)
}
}
}
}
}
return {
id: p.id,
number: p.number,
status: p.status,
event: p.event,
branch: p.branch,
commit: p.commit?.substring(0, 7) || '',
message: p.message || '',
author: p.author,
created: p.created,
started: p.started,
finished: p.finished,
steps,
errors: errors.length > 0 ? errors : undefined
}
})
return NextResponse.json({
status: 'online',
pipelines,
lastUpdate: new Date().toISOString()
} as WoodpeckerStatusResponse)
} catch (error) {
console.error('Woodpecker API error:', error)
return NextResponse.json({
status: 'offline',
pipelines: [],
lastUpdate: new Date().toISOString(),
error: 'Fehler beim Abrufen des Woodpecker Status'
} as WoodpeckerStatusResponse)
}
}
// Trigger a new pipeline
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { repoId = '1', branch = 'main' } = body
const response = await fetch(
`${WOODPECKER_URL}/api/repos/${repoId}/pipelines`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${WOODPECKER_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ branch }),
}
)
if (!response.ok) {
return NextResponse.json(
{ error: 'Pipeline konnte nicht gestartet werden' },
{ status: 500 }
)
}
const pipeline = await response.json()
return NextResponse.json({
success: true,
pipeline: {
id: pipeline.id,
number: pipeline.number,
status: pipeline.status
}
})
} catch (error) {
console.error('Pipeline trigger error:', error)
return NextResponse.json(
{ error: 'Fehler beim Starten der Pipeline' },
{ status: 500 }
)
}
}
// Get pipeline logs
export async function PUT(request: NextRequest) {
try {
const body = await request.json()
const { repoId = '1', pipelineNumber, stepId } = body
if (!pipelineNumber || !stepId) {
return NextResponse.json(
{ error: 'pipelineNumber und stepId erforderlich' },
{ status: 400 }
)
}
const response = await fetch(
`${WOODPECKER_URL}/api/repos/${repoId}/pipelines/${pipelineNumber}/logs/${stepId}`,
{
headers: {
'Authorization': `Bearer ${WOODPECKER_TOKEN}`,
'Content-Type': 'application/json',
},
}
)
if (!response.ok) {
return NextResponse.json(
{ error: 'Logs nicht verfuegbar' },
{ status: response.status }
)
}
const logs = await response.json()
return NextResponse.json({ logs })
} catch (error) {
console.error('Pipeline logs error:', error)
return NextResponse.json(
{ error: 'Fehler beim Abrufen der Logs' },
{ status: 500 }
)
}
}