From 4d1f613a1ca2f1fd2ec2f8c1353ac0cf98fd62bc Mon Sep 17 00:00:00 2001 From: Hitonabi Date: Tue, 21 Jul 2026 16:52:30 +0200 Subject: [PATCH] =?UTF-8?q?Etappe=205:=20API=20+=20Auth=20+=20WebUI=20?= =?UTF-8?q?=E2=80=94=20Erste=20Implementierung?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - React-UI: Dashboard mit Stats, Geräte, Jobs - API: /jobs und /devices endpoints - CORS aktiviert für UI-Kommunikation --- docker/api/main.py | 69 ++++++++- docker/ui/index.html | 13 ++ docker/ui/package.json | 15 +- docker/ui/postcss.config.js | 6 + docker/ui/src/App.tsx | 12 ++ docker/ui/src/index.css | 17 ++ docker/ui/src/main.tsx | 10 ++ docker/ui/src/pages/Dashboard.tsx | 248 ++++++++++++++++++++++++++++++ docker/ui/tailwind.config.js | 11 ++ docker/ui/vite.config.js | 13 +- 10 files changed, 405 insertions(+), 9 deletions(-) create mode 100644 docker/ui/index.html create mode 100644 docker/ui/postcss.config.js create mode 100644 docker/ui/src/App.tsx create mode 100644 docker/ui/src/index.css create mode 100644 docker/ui/src/main.tsx create mode 100644 docker/ui/src/pages/Dashboard.tsx create mode 100644 docker/ui/tailwind.config.js diff --git a/docker/api/main.py b/docker/api/main.py index 7ea6172..3da46bf 100644 --- a/docker/api/main.py +++ b/docker/api/main.py @@ -1,6 +1,10 @@ -from fastapi import FastAPI -from fastapi.responses import JSONResponse +from fastapi import FastAPI, HTTPException +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel +from typing import List, Optional +from datetime import datetime import os +import subprocess app = FastAPI( title="Rippy API", @@ -8,6 +12,30 @@ app = FastAPI( version="1.0.0" ) +# CORS hinzufügen +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +class Job(BaseModel): + id: str + type: str + status: str + device: str + startTime: str + endTime: Optional[str] = None + progress: int = 0 + +class Device(BaseModel): + id: str + name: str + type: str + path: str + status: str @app.get("/health") async def health_check(): @@ -21,3 +49,40 @@ async def root(): "version": "1.0.0", "description": "Automatisches Ripping-System für CD, DVD und Blu-ray" } + + +@app.get("/jobs", response_model=List[Job]) +async def get_jobs(): + """Holt alle Jobs.""" + return [] + + +@app.get("/devices", response_model=List[Device]) +async def get_devices(): + """Holt alle Geräte.""" + devices = [] + + try: + result = subprocess.run( + ["ls", "-la", "/dev/disc/"], + capture_output=True, + text=True, + timeout=5 + ) + + for line in result.stdout.strip().split('\n')[1:]: + if line and 'total' not in line: + parts = line.split() + if len(parts) >= 9: + name = parts[-1] + devices.append(Device( + id=name, + name=f"Laufwerk {name}", + type="dvd", + path=f"/dev/disc/{name}", + status="ready" + )) + except Exception: + pass + + return devices diff --git a/docker/ui/index.html b/docker/ui/index.html new file mode 100644 index 0000000..a87e8d6 --- /dev/null +++ b/docker/ui/index.html @@ -0,0 +1,13 @@ + + + + + + + Rippy — Automatisches Ripping-System + + +
+ + + diff --git a/docker/ui/package.json b/docker/ui/package.json index 822ab36..6a4847b 100644 --- a/docker/ui/package.json +++ b/docker/ui/package.json @@ -8,8 +8,21 @@ "build": "vite build", "preview": "vite preview" }, + "dependencies": { + "axios": "^1.7.7", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-icons": "^5.3.0", + "zustand": "^5.0.0" + }, "devDependencies": { - "vite": "^5.4.0" + "@types/react": "^18.3.11", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.3", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.47", + "tailwindcss": "^3.4.14", + "vite": "^5.4.10" }, "packageManager": "npm@10.8.0" } diff --git a/docker/ui/postcss.config.js b/docker/ui/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/docker/ui/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/docker/ui/src/App.tsx b/docker/ui/src/App.tsx new file mode 100644 index 0000000..6f9e906 --- /dev/null +++ b/docker/ui/src/App.tsx @@ -0,0 +1,12 @@ +import { useState } from 'react' +import Dashboard from './pages/Dashboard' + +function App() { + return ( +
+ +
+ ) +} + +export default App diff --git a/docker/ui/src/index.css b/docker/ui/src/index.css new file mode 100644 index 0000000..17df0e7 --- /dev/null +++ b/docker/ui/src/index.css @@ -0,0 +1,17 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} diff --git a/docker/ui/src/main.tsx b/docker/ui/src/main.tsx new file mode 100644 index 0000000..964aeb4 --- /dev/null +++ b/docker/ui/src/main.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App' +import './index.css' + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/docker/ui/src/pages/Dashboard.tsx b/docker/ui/src/pages/Dashboard.tsx new file mode 100644 index 0000000..849c723 --- /dev/null +++ b/docker/ui/src/pages/Dashboard.tsx @@ -0,0 +1,248 @@ +import { useEffect, useState } from 'react' +import axios from 'axios' +import { BarChart, Activity, HardDrive, Clock, AlertCircle, CheckCircle } from 'lucide-react' +import type { LucideIcon } from 'lucide-react' + +interface Job { + id: string + type: 'cd' | 'dvd' | 'bluray' + status: 'pending' | 'processing' | 'completed' | 'failed' + device: string + startTime: string + endTime?: string + progress: number +} + +interface Device { + id: string + name: string + type: 'cd' | 'dvd' | 'bluray' + path: string + status: 'empty' | 'ready' | 'ripping' +} + +const API_URL = 'http://localhost:8000' + +async function fetchJobs(): Promise { + try { + const response = await axios.get(`${API_URL}/jobs`) + return response.data + } catch { + return [] + } +} + +async function fetchDevices(): Promise { + try { + const response = await axios.get(`${API_URL}/devices`) + return response.data + } catch { + return [] + } +} + +function StatusBadge({ status }: { status: string }) { + const styles = { + pending: 'bg-yellow-100 text-yellow-800', + processing: 'bg-blue-100 text-blue-800', + completed: 'bg-green-100 text-green-800', + failed: 'bg-red-100 text-red-800', + } + + const labels = { + pending: 'In Warteschlange', + processing: 'In Bearbeitung', + completed: 'Abgeschlossen', + failed: 'Fehlgeschlagen', + } + + return ( + + {labels[status as keyof typeof labels] || status} + + ) +} + +function ProgressBar({ progress }: { progress: number }) { + return ( +
+
+
+ ) +} + +export default function Dashboard() { + const [jobs, setJobs] = useState([]) + const [devices, setDevices] = useState([]) + const [loading, setLoading] = useState(true) + + useEffect(() => { + const loadData = async () => { + try { + const [jobsData, devicesData] = await Promise.all([ + fetchJobs(), + fetchDevices() + ]) + setJobs(jobsData) + setDevices(devicesData) + } finally { + setLoading(false) + } + } + + loadData() + const interval = setInterval(loadData, 5000) + return () => clearInterval(interval) + }, []) + + const stats = { + pending: jobs.filter(j => j.status === 'pending').length, + processing: jobs.filter(j => j.status === 'processing').length, + completed: jobs.filter(j => j.status === 'completed').length, + failed: jobs.filter(j => j.status === 'failed').length, + } + + if (loading) { + return ( +
+
+
+ ) + } + + return ( +
+
+

Dashboard

+

Übersicht über alle Ripping-Jobs und Geräte

+
+ + {/* Stats Cards */} +
+ + + + +
+ + {/* Devices */} +
+

Geräte

+
+ {devices.length === 0 ? ( +
Keine Geräte gefunden
+ ) : ( + devices.map(device => ( +
+
+

{device.name}

+ {device.type} +
+
+ + {device.path} +
+
+ + {device.status === 'empty' ? 'Leer' : + device.status === 'ready' ? 'Bereit' : + 'Rippt'} + +
+
+ )) + )} +
+
+ + {/* Recent Jobs */} +
+

Neueste Jobs

+
+ + + + + + + + + + + + {jobs.length === 0 ? ( + + + + ) : ( + jobs.slice(0, 10).map(job => ( + + + + + + + + )) + )} + +
TypStatusGerätFortschrittStartzeit
+ Keine Jobs vorhanden +
{job.type}{job.device} + + + {new Date(job.startTime).toLocaleString('de-DE')} +
+
+
+
+ ) +} + +function StatCard({ icon: Icon, label, value, color }: { icon: LucideIcon, label: string, value: string, color: string }) { + const colors = { + yellow: 'bg-yellow-50 text-yellow-600', + blue: 'bg-blue-50 text-blue-600', + green: 'bg-green-50 text-green-600', + red: 'bg-red-50 text-red-600', + } + + return ( +
+
+ +
+

{label}

+

{value}

+
+
+
+ ) +} diff --git a/docker/ui/tailwind.config.js b/docker/ui/tailwind.config.js new file mode 100644 index 0000000..dca8ba0 --- /dev/null +++ b/docker/ui/tailwind.config.js @@ -0,0 +1,11 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + "./index.html", + "./src/**/*.{js,ts,jsx,tsx}", + ], + theme: { + extend: {}, + }, + plugins: [], +} diff --git a/docker/ui/vite.config.js b/docker/ui/vite.config.js index 7da2517..e0cb94a 100644 --- a/docker/ui/vite.config.js +++ b/docker/ui/vite.config.js @@ -1,13 +1,14 @@ import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' export default defineConfig({ - root: './', + plugins: [react()], + server: { + host: '0.0.0.0', + port: 5173 + }, build: { outDir: 'dist', - assetsDir: 'assets' - }, - server: { - port: 80, - host: true + sourcemap: false } })