0832ef777c
Ampel / ampel (push) Successful in 27s
Aus dem Weitergabe-Review. Kein Verhaltenswechsel fuer den Ursprungs-Host (alle Defaults = bisheriges Verhalten): Portabilitaet: - Laufwerk-Geraeteknoten via .env parametrisiert (OPTICAL_SR/OPTICAL_SG, Defaults sr0/sg1) - der #1-Blocker: auf Fremdhosts liegt das sg-Node woanders. - deploy.sh generisch (RIPPY_VM/RIPPY_REPO_URL/... aus der Umgebung) - keine festen Besitzer-Adressen (interne IP + DDNS) mehr im Repo. - POSTGRES_PASSWORD in compose durchverdrahtet (Default rippy) - war No-op. Aufraeumen (toter/irrefuehrender Code): - udev/ entfernt (fehlende Regeldatei, falscher Container-Name; durch ioctl- Polling ersetzt - reine Altlast). - docker/postgres/*, docker/redis/*, init.sql entfernt (nie eingebunden). Onboarding: - FirstRunWizard verdrahtet: App.tsx fragt GET /setup und zeigt den Einrichtungs-Assistenten beim ersten Start (war gebaut, aber nie gerendert). Doku: - README: Laufwerk-Abschnitt auf .env, ENV-Tabelle (OPTICAL_*/POSTGRES), rshared-Anleitung, neuer Abschnitt "Haertung fuer fremde/exponierte Netze". - config_validation.py: TMDB Pflicht->empfohlen, Zeichensalat-Tippfehler gefixt. - .env.example: TMDB-Wording, OPTICAL_*-Variablen. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
89 lines
4.1 KiB
TypeScript
89 lines
4.1 KiB
TypeScript
import { useEffect, useState } from 'react'
|
|
import { LayoutDashboard, BookOpen, FileText, Settings as SettingsIcon } from 'lucide-react'
|
|
import Dashboard from './pages/Dashboard'
|
|
import SettingsPage from './pages/Settings'
|
|
import LogsPage from './pages/Logs'
|
|
import AnleitungPage from './pages/Anleitung'
|
|
import FirstRunWizard from './components/FirstRunWizard'
|
|
import { api } from './lib/api'
|
|
|
|
type Page = 'dashboard' | 'anleitung' | 'logs' | 'settings'
|
|
|
|
export default function App() {
|
|
const [currentPage, setCurrentPage] = useState<Page>('dashboard')
|
|
// First-Run: solange der Einrichtungs-Assistent nicht abgeschlossen ist, zeigen wir
|
|
// ihn statt des Dashboards. null = /setup noch nicht geprüft (kein Aufblitzen).
|
|
const [setupDone, setSetupDone] = useState<boolean | null>(null)
|
|
|
|
useEffect(() => {
|
|
api.get('/setup')
|
|
.then((r) => setSetupDone(Boolean(r.data?.done)))
|
|
.catch(() => setSetupDone(true)) // API/Setup nicht erreichbar → nicht blockieren
|
|
}, [])
|
|
|
|
const navItems: { id: Page; label: string; icon: any }[] = [
|
|
{ id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard },
|
|
{ id: 'anleitung', label: 'Anleitung', icon: BookOpen },
|
|
{ id: 'logs', label: 'Logs', icon: FileText },
|
|
{ id: 'settings', label: 'Einstellungen', icon: SettingsIcon },
|
|
]
|
|
|
|
if (setupDone === null) return null // kurzer Moment, bis /setup geantwortet hat
|
|
if (!setupDone) return <FirstRunWizard onDone={() => setSetupDone(true)} />
|
|
|
|
return (
|
|
<div className="min-h-screen bg-[#080b11] text-slate-100 font-sans selection:bg-amber-500/30 selection:text-amber-300">
|
|
{/* Top Header Navigation Bar */}
|
|
<header className="sticky top-0 z-50 bg-[#0c101d]/95 backdrop-blur-2xl border-b border-amber-500/20 shadow-xl shadow-black/50">
|
|
<div className="max-w-[1400px] mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between gap-4">
|
|
|
|
{/* Logo */}
|
|
<div className="flex items-center gap-3 cursor-pointer" onClick={() => setCurrentPage('dashboard')}>
|
|
<span className="text-2xl font-black tracking-wider bg-gradient-to-r from-amber-400 via-amber-300 to-amber-500 bg-clip-text text-transparent drop-shadow-[0_0_12px_rgba(245,158,11,0.4)] font-mono">
|
|
RIPPY
|
|
</span>
|
|
<span className="px-2.5 py-0.5 rounded text-[10px] font-bold uppercase tracking-widest bg-amber-500/15 text-amber-400 border border-amber-500/30">
|
|
CINEMA OS
|
|
</span>
|
|
</div>
|
|
|
|
{/* Central Nav Tabs */}
|
|
<nav className="flex items-center gap-1.5 bg-[#121827] p-1.5 rounded-2xl border border-slate-800 shadow-inner">
|
|
{navItems.map((item) => {
|
|
const Icon = item.icon
|
|
const isActive = currentPage === item.id
|
|
return (
|
|
<button
|
|
key={item.id}
|
|
onClick={() => setCurrentPage(item.id)}
|
|
className={`flex items-center gap-2 px-4 py-2 rounded-xl text-xs font-semibold transition-all duration-200 ${
|
|
isActive
|
|
? 'bg-amber-500 text-slate-950 shadow-md shadow-amber-500/20 font-bold scale-[1.02]'
|
|
: 'text-slate-400 hover:text-slate-100 hover:bg-slate-800/60'
|
|
}`}
|
|
>
|
|
<Icon size={15} className={isActive ? 'text-slate-950' : 'text-slate-400'} />
|
|
<span>{item.label}</span>
|
|
</button>
|
|
)
|
|
})}
|
|
</nav>
|
|
|
|
<div className="flex items-center gap-2 text-xs font-mono text-emerald-400 bg-emerald-500/10 px-3 py-1.5 rounded-xl border border-emerald-500/20">
|
|
<span className="w-2 h-2 rounded-full bg-emerald-400 animate-pulse" />
|
|
<span>ONLINE</span>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
{/* Main Content Viewport */}
|
|
<main className="max-w-[1400px] mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
{currentPage === 'dashboard' && <Dashboard />}
|
|
{currentPage === 'anleitung' && <AnleitungPage />}
|
|
{currentPage === 'logs' && <LogsPage />}
|
|
{currentPage === 'settings' && <SettingsPage />}
|
|
</main>
|
|
</div>
|
|
)
|
|
}
|