From b2d2b8b2f3cef1d8974f7bbe3c39f93785e39559 Mon Sep 17 00:00:00 2001 From: Bilal Teke Date: Wed, 15 Apr 2026 21:54:46 +0200 Subject: [PATCH] v3.1 --- app/components/FilterBar.tsx | 8 +-- app/components/ServiceCard.tsx | 54 ++++++++------ app/components/ServiceGrid.tsx | 2 +- app/components/ServiceSection.tsx | 84 ---------------------- app/components/dashboard-client.tsx | 105 ++++++++++++++++++++++++++++ app/components/index.ts | 2 +- app/layout.tsx | 1 - app/page.tsx | 15 +--- src/app/api/status/route.ts | 67 ++++++++++++++++++ src/data/services.ts | 73 +++++++------------ src/types/service.ts | 18 +++++ 11 files changed, 258 insertions(+), 171 deletions(-) delete mode 100644 app/components/ServiceSection.tsx create mode 100644 app/components/dashboard-client.tsx create mode 100644 src/app/api/status/route.ts create mode 100644 src/types/service.ts diff --git a/app/components/FilterBar.tsx b/app/components/FilterBar.tsx index ce37837..0ac90e1 100644 --- a/app/components/FilterBar.tsx +++ b/app/components/FilterBar.tsx @@ -1,13 +1,13 @@ 'use client'; -import { ServiceCategory, ServiceStatus } from '@/lib/data'; +import { ServiceStatus } from '@/src/types/service'; interface FilterBarProps { - categories: ServiceCategory[]; + categories: string[]; statuses: ServiceStatus[]; - selectedCategories: ServiceCategory[]; + selectedCategories: string[]; selectedStatuses: ServiceStatus[]; - onCategoryChange: (category: ServiceCategory) => void; + onCategoryChange: (category: string) => void; onStatusChange: (status: ServiceStatus) => void; onReset: () => void; } diff --git a/app/components/ServiceCard.tsx b/app/components/ServiceCard.tsx index d16ad49..899abc4 100644 --- a/app/components/ServiceCard.tsx +++ b/app/components/ServiceCard.tsx @@ -1,9 +1,10 @@ 'use client'; -import { Service } from '@/lib/data'; +import { Service, ServiceCheckResult } from '@/src/types/service'; interface ServiceCardProps { service: Service; + result?: ServiceCheckResult; } const STATUS_CONFIG = { @@ -25,14 +26,22 @@ const STATUS_CONFIG = { dotColor: 'bg-red-500', label: 'Offline', }, + unknown: { + bgColor: 'bg-slate-50 dark:bg-slate-950', + textColor: 'text-slate-700 dark:text-slate-300', + dotColor: 'bg-slate-500', + label: 'Unbekannt', + }, } as const; /** * ServiceCard - Zeigt einen einzelnen Service mit Status, Icon und Öffnen-Button - * @param service - Das Service-Objekt mit Name, Beschreibung, Status, etc. + * @param service - Das Service-Objekt mit Name, Beschreibung, etc. + * @param result - Optionale Status-Check-Ergebnisse */ -export function ServiceCard({ service }: ServiceCardProps) { - const config = STATUS_CONFIG[service.status]; +export function ServiceCard({ service, result }: ServiceCardProps) { + const status = result?.status || 'unknown'; + const config = STATUS_CONFIG[status]; return (
- {/* Header mit Icon, Name, Kategorie und Status */} + {/* Header mit Name, Kategorie und Status */}
-
- {service.icon && ( - - )} -
-

- {service.name} -

-

- {service.category} -

-
+
+

+ {service.name} +

+

+ {service.category} +

{/* Status Badge */} @@ -66,7 +68,7 @@ export function ServiceCard({ service }: ServiceCardProps) { className={`flex items-center gap-2 px-3 py-1 rounded-full whitespace-nowrap flex-shrink-0 ${config.bgColor}`} > {config.label} @@ -74,6 +76,18 @@ export function ServiceCard({ service }: ServiceCardProps) {
+ {/* Status Details */} + {result && ( +
+ {result.httpStatus && ( +
HTTP: {result.httpStatus}
+ )} + {result.responseTimeMs !== null && ( +
Zeit: {result.responseTimeMs}ms
+ )} +
+ )} + {/* Beschreibung */}

{service.description} diff --git a/app/components/ServiceGrid.tsx b/app/components/ServiceGrid.tsx index 4d2bace..83ca5ff 100644 --- a/app/components/ServiceGrid.tsx +++ b/app/components/ServiceGrid.tsx @@ -1,6 +1,6 @@ 'use client'; -import { Service } from '@/lib/data'; +import { Service } from '@/src/types/service'; import { ServiceCard } from './ServiceCard'; import { EmptyState } from './EmptyState'; diff --git a/app/components/ServiceSection.tsx b/app/components/ServiceSection.tsx deleted file mode 100644 index 38cfd52..0000000 --- a/app/components/ServiceSection.tsx +++ /dev/null @@ -1,84 +0,0 @@ -'use client'; - -import { useState, useMemo } from 'react'; -import { Service, ServiceCategory, ServiceStatus } from '@/lib/data'; -import { FilterBar } from './FilterBar'; -import { ServiceGrid } from './ServiceGrid'; - -interface ServiceSectionProps { - services: Service[]; -} - -/** - * ServiceSection - Verwaltet Filterlogik und rendert FilterBar + ServiceGrid - * @param services - Array aller verfügbaren Services - */ -export function ServiceSection({ services }: ServiceSectionProps) { - const [selectedCategories, setSelectedCategories] = useState< - ServiceCategory[] - >([]); - const [selectedStatuses, setSelectedStatuses] = useState([]); - - // Extrahiere eindeutige Kategorien und Status aus Services - const categories = useMemo( - () => Array.from(new Set(services.map((s) => s.category))).sort(), - [services] - ); - - const statuses = useMemo( - () => - (['online', 'warning', 'offline'] as ServiceStatus[]).filter((status) => - services.some((s) => s.status === status) - ), - [services] - ); - - // Filtere Services basierend auf ausgewählten Filtern - const filteredServices = useMemo(() => { - return services.filter((service) => { - const categoryMatch = - selectedCategories.length === 0 || - selectedCategories.includes(service.category); - - const statusMatch = - selectedStatuses.length === 0 || - selectedStatuses.includes(service.status); - - return categoryMatch && statusMatch; - }); - }, [services, selectedCategories, selectedStatuses]); - - const handleCategoryChange = (category: ServiceCategory) => { - setSelectedCategories((prev) => - prev.includes(category) - ? prev.filter((c) => c !== category) - : [...prev, category] - ); - }; - - const handleStatusChange = (status: ServiceStatus) => { - setSelectedStatuses((prev) => - prev.includes(status) ? prev.filter((s) => s !== status) : [...prev, status] - ); - }; - - const handleReset = () => { - setSelectedCategories([]); - setSelectedStatuses([]); - }; - - return ( - <> - - - - ); -} diff --git a/app/components/dashboard-client.tsx b/app/components/dashboard-client.tsx new file mode 100644 index 0000000..4f55318 --- /dev/null +++ b/app/components/dashboard-client.tsx @@ -0,0 +1,105 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { ServiceCheckResult } from '@/src/types/service'; +import { services } from '@/src/data/services'; +import { ServiceCard } from './ServiceCard'; + +export function DashboardClient() { + const [results, setResults] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchStatus = async () => { + try { + setError(null); + const response = await fetch('/api/status'); + if (!response.ok) { + throw new Error('Failed to fetch status'); + } + const data: ServiceCheckResult[] = await response.json(); + setResults(data); + } catch (err) { + setError(err instanceof Error ? err.message : 'Unknown error'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchStatus(); + + // Auto-refresh every 30 seconds + const interval = setInterval(fetchStatus, 30000); + + return () => clearInterval(interval); + }, []); + + const getResultForService = (serviceId: string): ServiceCheckResult | undefined => { + return results.find(result => result.id === serviceId); + }; + + return ( +

+ {/* Header */} +
+
+
+
+

+ Homelab Dashboard +

+
+ {loading ? ( + 'Lade Status...' + ) : ( + 'Automatische Aktualisierung: 30s' + )} +
+
+

+ Live-Status aller verwalteten Dienste +

+
+
+
+ + {/* Main Content */} +
+ {error && ( +
+
+ + + + + Fehler beim Laden der Status-Daten: {error} + +
+
+ )} + + {/* Services Grid */} +
+ {services.map((service) => ( + + ))} +
+
+
+ ); +} \ No newline at end of file diff --git a/app/components/index.ts b/app/components/index.ts index 04f96d3..750bf38 100644 --- a/app/components/index.ts +++ b/app/components/index.ts @@ -4,6 +4,6 @@ export { Header } from './Header'; export { ServiceCard } from './ServiceCard'; export { ServiceGrid } from './ServiceGrid'; -export { ServiceSection } from './ServiceSection'; export { FilterBar } from './FilterBar'; export { EmptyState } from './EmptyState'; +export { DashboardClient } from './dashboard-client'; diff --git a/app/layout.tsx b/app/layout.tsx index 87df3ca..64a0760 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,4 +1,3 @@ -import type { Metadata } from 'next'; import { Metadata as NextMetadata } from 'next'; export const metadata: NextMetadata = { diff --git a/app/page.tsx b/app/page.tsx index ebbf244..1395f87 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,21 +1,12 @@ import type { Metadata } from 'next'; -import { Header, ServiceSection } from './components'; -import { services } from '@/lib/data'; +import { DashboardClient } from './components/dashboard-client'; import './globals.css'; export const metadata: Metadata = { title: 'Homelab Dashboard', - description: 'Verwaltungs-Dashboard für dein Homelab', + description: 'Live-Monitoring-Dashboard für dein Homelab', }; export default function Home() { - return ( -
-
- -
- -
-
- ); + return ; } diff --git a/src/app/api/status/route.ts b/src/app/api/status/route.ts new file mode 100644 index 0000000..3ab5e5f --- /dev/null +++ b/src/app/api/status/route.ts @@ -0,0 +1,67 @@ +import { NextResponse } from 'next/server'; +import { services } from '@/src/data/services'; +import { ServiceCheckResult } from '@/src/types/service'; + +export const dynamic = 'force-dynamic'; + +async function checkService(service: typeof services[0]): Promise { + const startTime = Date.now(); + + try { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 second timeout + + const response = await fetch(service.monitorUrl, { + method: 'HEAD', // Use HEAD to minimize data transfer + cache: 'no-store', + signal: controller.signal, + }); + + clearTimeout(timeoutId); + const responseTime = Date.now() - startTime; + + let status: ServiceCheckResult['status']; + + if (response.ok) { + status = responseTime <= 2000 ? 'online' : 'warning'; + } else if (response.status >= 400 && response.status < 500) { + status = 'warning'; + } else { + status = 'offline'; + } + + return { + id: service.id, + status, + responseTimeMs: responseTime, + httpStatus: response.status, + checkedAt: new Date().toISOString(), + }; + } catch (error) { + const responseTime = Date.now() - startTime; + + return { + id: service.id, + status: 'offline', + responseTimeMs: responseTime, + httpStatus: null, + checkedAt: new Date().toISOString(), + }; + } +} + +export async function GET() { + try { + const results = await Promise.all( + services.map(service => checkService(service)) + ); + + return NextResponse.json(results); + } catch (error) { + console.error('Error checking services:', error); + return NextResponse.json( + { error: 'Failed to check services' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/src/data/services.ts b/src/data/services.ts index 9295d95..e486742 100644 --- a/src/data/services.ts +++ b/src/data/services.ts @@ -1,15 +1,4 @@ -export type ServiceCategory = 'Infrastruktur' | 'Smart Home' | 'Medien' | 'Dokumente'; -export type ServiceStatus = 'online' | 'warning' | 'offline'; - -export interface Service { - id: string; - name: string; - description: string; - category: ServiceCategory; - status: ServiceStatus; - url: string; - icon?: string; -} +import { Service } from '@/src/types/service'; export const services: Service[] = [ // Infrastruktur @@ -17,37 +6,33 @@ export const services: Service[] = [ id: 'unraid', name: 'Unraid', description: 'NAS und Server Management Platform', - category: 'Infrastruktur', - status: 'online', url: 'http://localhost', - icon: '🖥️', + monitorUrl: 'http://localhost', + category: 'Infrastruktur', }, { id: 'unifi', name: 'UniFi', description: 'Netzwerk-Management und WLAN-Controller', - category: 'Infrastruktur', - status: 'online', url: 'http://localhost:8443', - icon: '🌐', + monitorUrl: 'http://localhost:8443', + category: 'Infrastruktur', }, { id: 'checkmk', name: 'Checkmk', description: 'Monitoring und Alerting System', - category: 'Infrastruktur', - status: 'online', url: 'http://localhost/checkmk', - icon: '📊', + monitorUrl: 'http://localhost/checkmk', + category: 'Infrastruktur', }, { id: 'nginx-proxy', name: 'Nginx Proxy Manager', description: 'Reverse Proxy und SSL-Verwaltung', - category: 'Infrastruktur', - status: 'online', url: 'http://localhost:81', - icon: '⚙️', + monitorUrl: 'http://localhost:81', + category: 'Infrastruktur', }, // Smart Home @@ -55,10 +40,9 @@ export const services: Service[] = [ id: 'home-assistant', name: 'Home Assistant', description: 'Smart Home Automation und Integration', - category: 'Smart Home', - status: 'offline', url: 'http://localhost:8123', - icon: '🏠', + monitorUrl: 'http://localhost:8123', + category: 'Smart Home', }, // Medien @@ -66,46 +50,41 @@ export const services: Service[] = [ id: 'plex', name: 'Plex', description: 'Medienserver und Streaming-Dienst', - category: 'Medien', - status: 'online', url: 'http://localhost:32400', - icon: '🎬', + monitorUrl: 'http://localhost:32400', + category: 'Medien', }, { id: 'jellyfin', name: 'Jellyfin', description: 'Open-Source Medienserver', - category: 'Medien', - status: 'online', url: 'http://localhost:8096', - icon: '📺', + monitorUrl: 'http://localhost:8096', + category: 'Medien', }, { id: 'radarr', name: 'Radarr', description: 'Automatisches Filmmanagement', - category: 'Medien', - status: 'online', url: 'http://localhost:7878', - icon: '🎥', + monitorUrl: 'http://localhost:7878', + category: 'Medien', }, { id: 'sonarr', name: 'Sonarr', description: 'Automatisches Serienmanagement', - category: 'Medien', - status: 'online', url: 'http://localhost:8989', - icon: '📺', + monitorUrl: 'http://localhost:8989', + category: 'Medien', }, { id: 'prowlarr', name: 'Prowlarr', description: 'Indexer und Proxy Manager', - category: 'Medien', - status: 'warning', url: 'http://localhost:9696', - icon: '🔍', + monitorUrl: 'http://localhost:9696', + category: 'Medien', }, // Dokumente @@ -113,18 +92,16 @@ export const services: Service[] = [ id: 'seafile', name: 'Seafile', description: 'Dateifreigabe und Cloud Storage', - category: 'Dokumente', - status: 'online', url: 'http://localhost:8000', - icon: '☁️', + monitorUrl: 'http://localhost:8000', + category: 'Dokumente', }, { id: 'forgejo', name: 'Forgejo', description: 'Self-hosted Git und Code Repository', - category: 'Dokumente', - status: 'online', url: 'http://localhost:3000', - icon: '💾', + monitorUrl: 'http://localhost:3000', + category: 'Dokumente', }, ]; diff --git a/src/types/service.ts b/src/types/service.ts new file mode 100644 index 0000000..f04e2d1 --- /dev/null +++ b/src/types/service.ts @@ -0,0 +1,18 @@ +export type ServiceStatus = 'online' | 'warning' | 'offline' | 'unknown'; + +export interface Service { + id: string; + name: string; + description: string; + url: string; + monitorUrl: string; + category: string; +} + +export interface ServiceCheckResult { + id: string; + status: ServiceStatus; + responseTimeMs: number | null; + httpStatus: number | null; + checkedAt: string; +} \ No newline at end of file