diff --git a/config-server/server.js b/config-server/server.js deleted file mode 100644 index 0085d42..0000000 --- a/config-server/server.js +++ /dev/null @@ -1,69 +0,0 @@ -const express = require('express') -const fs = require('fs/promises') -const path = require('path') -const yaml = require('js-yaml') -const cors = require('cors') -const bodyParser = require('body-parser') - -const app = express() -const PORT = process.env.CONFIG_PORT || 3012 - -// CORS configuration -app.use(cors({ - origin: ['https://start.kent.pw', 'https://auth.kent.pw'], - credentials: true, - methods: ['GET', 'POST', 'OPTIONS'], - allowedHeaders: [ - 'Content-Type', - 'Authorization', - 'Remote-User', - 'Remote-Name', - 'Remote-Email', - 'Remote-Groups', - 'X-Requested-With' - ], - exposedHeaders: ['Set-Cookie'] -})) - -// Options preflight -app.options('/api/config/update', cors()) - -// Raw body parser for YAML -app.use(bodyParser.text({ type: 'text/yaml' })) - -// Authentication middleware -const requireAuth = (req, res, next) => { - const user = req.headers['remote-user'] - if (!user) { - return res.status(401).json({ error: 'Unauthorized' }) - } - next() -} - -app.post('/api/config/update', requireAuth, async (req, res) => { - try { - // Get the raw YAML content from the request body - const yamlContent = req.body - - // Validate the YAML by trying to parse it - yaml.load(yamlContent) - - // Write to the config file - const configPath = path.join('/var/www/html/homepage/public', 'config.yaml') - await fs.writeFile(configPath, yamlContent, 'utf8') - - res.status(200).json({ message: 'Configuration updated successfully' }) - } catch (error) { - console.error('Error updating configuration:', error) - res.status(500).json({ error: 'Failed to update configuration: ' + error.message }) - } -}) - -// Health check endpoint -app.get('/health', (req, res) => { - res.status(200).json({ status: 'ok' }) -}) - -app.listen(PORT, () => { - console.log(`Config server listening on port ${PORT}`) -}) \ No newline at end of file diff --git a/public/config.yaml b/public/config.yaml index 1408396..16696f0 100644 --- a/public/config.yaml +++ b/public/config.yaml @@ -1,15 +1,64 @@ +sections: + - id: "system" + title: "System" + icon: "Server" + cardStyle: + background: "bg-blue-50 dark:bg-blue-950/30" + iconColor: "text-blue-600 dark:text-blue-400" + + - id: "media" + title: "Media" + icon: "Play" + cardStyle: + background: "bg-orange-50 dark:bg-orange-950/30" + iconColor: "text-orange-600 dark:text-orange-400" + + - id: "monitoring" + title: "Monitoring" + icon: "Activity" + cardStyle: + background: "bg-emerald-50 dark:bg-emerald-950/30" + iconColor: "text-emerald-600 dark:text-emerald-400" + + - id: "tools" + title: "Tools" + icon: "Settings" + cardStyle: + background: "bg-purple-50 dark:bg-purple-950/30" + iconColor: "text-purple-600 dark:text-purple-400" + + - id: "acot" + title: "ACOT" + icon: "LayoutDashboard" + cardStyle: + background: "bg-sky-50 dark:bg-sky-950/30" + iconColor: "text-sky-600 dark:text-sky-400" + + - id: "home" + title: "Home" + icon: "Home" + cardStyle: + background: "bg-indigo-50 dark:bg-indigo-950/30" + iconColor: "text-indigo-600 dark:text-indigo-400" + services: - name: "Portainer" description: "Container Management" url: "https://portainer.kent.pw" iconName: "portainer" category: "system" + cardStyle: + background: "bg-blue-50 dark:bg-blue-950/30" + iconColor: "text-blue-600 dark:text-blue-400" - name: "Gitea" description: "Git Server" url: "https://gitea.kent.pw" iconName: "gitea" category: "system" + cardStyle: + background: "bg-green-50 dark:bg-green-950/30" + iconColor: "text-green-600 dark:text-green-400" - name: "Cockpit" description: "Server Management" @@ -23,18 +72,27 @@ services: iconName: "synology" category: "system" monitorName: "Synology Diskstation" + cardStyle: + background: "bg-slate-50 dark:bg-slate-950/30" + iconColor: "text-slate-600 dark:text-slate-400" - name: "Plex" description: "Media Server" url: "https://plex.kent.pw" iconName: "plex" category: "media" + cardStyle: + background: "bg-orange-50 dark:bg-orange-950/30" + iconColor: "text-orange-600 dark:text-orange-400" - name: "Sonarr" description: "TV Show Management" url: "https://sonarr.kent.pw" iconName: "sonarr" category: "media" + cardStyle: + background: "bg-blue-50 dark:bg-blue-950/30" + iconColor: "text-blue-600 dark:text-blue-400" - name: "Jackett" description: "Torrent Indexer" @@ -47,6 +105,9 @@ services: url: "https://deluge.kent.pw" iconName: "deluge" category: "media" + cardStyle: + background: "bg-green-50 dark:bg-green-950/30" + iconColor: "text-green-600 dark:text-green-400" - name: "Uptime" description: "Service Monitoring" @@ -54,6 +115,9 @@ services: iconName: "uptime-kuma" category: "monitoring" monitorName: "Uptime Kuma" + cardStyle: + background: "bg-emerald-50 dark:bg-emerald-950/30" + iconColor: "text-emerald-600 dark:text-emerald-400" - name: "AdGuard" description: "Network Ad Blocking" @@ -61,6 +125,9 @@ services: iconName: "adguard-home" category: "system" monitorName: "Adguard Home" + cardStyle: + background: "bg-emerald-50 dark:bg-emerald-950/30" + iconColor: "text-emerald-600 dark:text-emerald-400" - name: "NocoDB" description: "Database Platform" @@ -81,6 +148,9 @@ services: url: "https://firefox.kent.pw" iconName: "firefox" category: "tools" + cardStyle: + background: "bg-orange-50 dark:bg-orange-950/30" + iconColor: "text-orange-600 dark:text-orange-400" - name: "Speedtest" description: "Network Speed Monitor" @@ -88,6 +158,9 @@ services: iconName: "speedtest-tracker" category: "monitoring" monitorName: "Speedtest Tracker" + cardStyle: + background: "bg-blue-50 dark:bg-blue-950/30" + iconColor: "text-blue-600 dark:text-blue-400" - name: "Netdata" description: "Monitoring Dashboard" @@ -95,6 +168,9 @@ services: iconName: "netdata" category: "monitoring" monitorName: "Netdata" + cardStyle: + background: "bg-purple-50 dark:bg-purple-950/30" + iconColor: "text-purple-600 dark:text-purple-400" - name: "Drive" description: "File Storage" @@ -108,6 +184,9 @@ services: url: "https://dashboard.kent.pw" iconName: "lucide-layout-dashboard" category: "acot" + cardStyle: + background: "bg-sky-50 dark:bg-sky-950/30" + iconColor: "text-sky-600 dark:text-sky-400" - name: "Inventory" description: "ACOT Inventory" @@ -120,9 +199,15 @@ services: url: "https://homebridge.kent.pw" iconName: "homebridge" category: "home" + cardStyle: + background: "bg-purple-50 dark:bg-purple-950/30" + iconColor: "text-purple-600 dark:text-purple-400" - name: "Scrypted" description: "Smart Home Integration" url: "https://scrypted.kent.pw" iconName: "scrypted" - category: "home" \ No newline at end of file + category: "home" + cardStyle: + background: "bg-indigo-50 dark:bg-indigo-950/30" + iconColor: "text-indigo-600 dark:text-indigo-400" \ No newline at end of file diff --git a/server/api/config.js b/server/api/config.js deleted file mode 100644 index f31a418..0000000 --- a/server/api/config.js +++ /dev/null @@ -1,27 +0,0 @@ -const express = require('express') -const fs = require('fs/promises') -const path = require('path') -const yaml = require('js-yaml') - -const router = express.Router() - -router.post('/api/config', async (req, res) => { - try { - // Get the raw YAML content from the request body - const yamlContent = req.body - - // Validate the YAML by trying to parse it - yaml.load(yamlContent) - - // Write to the config file - const configPath = path.join(process.cwd(), 'public', 'config.yaml') - await fs.writeFile(configPath, yamlContent, 'utf8') - - res.status(200).json({ message: 'Configuration updated successfully' }) - } catch (error) { - console.error('Error updating configuration:', error) - res.status(500).json({ error: 'Failed to update configuration' }) - } -}) - -module.exports = router \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 9a52fdc..1b539d6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,11 +7,10 @@ import { useTheme } from "@/components/theme-provider" import { useMetrics } from "./hooks/useMetrics" import { useConfig } from "./hooks/useConfig" import { ServiceStatus } from "./types/metrics" -import { ServiceCard } from "./types/services" +import { ServiceCard, Section } from "./types/services" import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" import { AlertCircle } from "lucide-react" -import { ConfigEditor } from './components/config-editor' const getIconUrl = (name: string | undefined, isDark: boolean = false): string => { if (!name) return '' @@ -55,18 +54,8 @@ function ServiceIcon({ service, isDark }: { service: ServiceCard, isDark: boolea ) } - // Default category icons - const categoryIcons: Record = { - system: 'Server', - media: 'Play', - monitoring: 'Activity', - tools: 'Settings', - acot: 'LayoutDashboard', - home: 'Home' - } - - // Cast to any to avoid TypeScript complexity with dynamic imports - const Icon = (LucideIcons as any)[categoryIcons[service.category] || 'Box'] + // Default category icons from section configuration + const Icon = (LucideIcons as any)['Box'] return } @@ -142,8 +131,8 @@ function StatusIndicator({ status, responseTime, certDaysRemaining }: { ) } -function ServiceSection({ title, services, metrics }: { - title: string, +function ServiceSection({ section, services, metrics }: { + section: Section, services: ServiceCard[], metrics: Record }) { @@ -153,7 +142,7 @@ function ServiceSection({ title, services, metrics }: { return (
-

{title}

+

{section.title}

@@ -164,77 +153,8 @@ function ServiceSection({ title, services, metrics }: { certDaysRemaining: 0 } - const bgColor = (() => { - switch (service.name) { - case "Portainer": - return "bg-blue-50 dark:bg-blue-950/30" - case "Gitea": - return "bg-green-50 dark:bg-green-950/30" - case "Plex": - return "bg-orange-50 dark:bg-orange-950/30" - case "Sonarr": - return "bg-blue-50 dark:bg-blue-950/30" - case "AdGuard": - return "bg-emerald-50 dark:bg-emerald-950/30" - case "Homebridge": - return "bg-purple-50 dark:bg-purple-950/30" - case "Scrypted": - return "bg-indigo-50 dark:bg-indigo-950/30" - case "Dashboard": - case "Inventory": - return "bg-sky-50 dark:bg-sky-950/30" - case "DiskStation": - return "bg-slate-50 dark:bg-slate-950/30" - case "Deluge": - return "bg-green-50 dark:bg-green-950/30" - case "Firefox": - return "bg-orange-50 dark:bg-orange-950/30" - case "Uptime": - return "bg-emerald-50 dark:bg-emerald-950/30" - case "Speedtest": - return "bg-blue-50 dark:bg-blue-950/30" - case "Netdata": - return "bg-purple-50 dark:bg-purple-950/30" - default: - return "bg-gray-50 dark:bg-gray-950/30" - } - })() - - const iconColor = (() => { - switch (service.name) { - case "Portainer": - return "text-blue-600 dark:text-blue-400" - case "Gitea": - return "text-green-600 dark:text-green-400" - case "Plex": - return "text-orange-600 dark:text-orange-400" - case "Sonarr": - return "text-blue-600 dark:text-blue-400" - case "AdGuard": - return "text-emerald-600 dark:text-emerald-400" - case "Homebridge": - return "text-purple-600 dark:text-purple-400" - case "Scrypted": - return "text-indigo-600 dark:text-indigo-400" - case "Dashboard": - case "Inventory": - return "text-sky-600 dark:text-sky-400" - case "DiskStation": - return "text-slate-600 dark:text-slate-400" - case "Deluge": - return "text-green-600 dark:text-green-400" - case "Firefox": - return "text-orange-600 dark:text-orange-400" - case "Uptime": - return "text-emerald-600 dark:text-emerald-400" - case "Speedtest": - return "text-blue-600 dark:text-blue-400" - case "Netdata": - return "text-purple-600 dark:text-purple-400" - default: - return "text-gray-600 dark:text-gray-400" - } - })() + // Use service-specific style or fall back to section style + const style = service.cardStyle || section.cardStyle return ( - +
-
+
{service.name} @@ -272,24 +192,14 @@ function ServiceSection({ title, services, metrics }: { function App() { const { metrics } = useMetrics() - const { config, error, saveConfig } = useConfig() - const services = config.services - - const systemServices = services.filter(s => s.category === 'system') - const mediaServices = services.filter(s => s.category === 'media') - const monitoringServices = services.filter(s => s.category === 'monitoring') - const toolServices = services.filter(s => s.category === 'tools') - const acotServices = services.filter(s => s.category === 'acot') - const homeServices = services.filter(s => s.category === 'home') + const { config, error } = useConfig() + const { services, sections } = config return (

Kent.pw Homepage

-
- - -
+
{error && ( @@ -302,14 +212,21 @@ function App() { )} - {services.length > 0 ? ( + {sections?.length > 0 ? ( <> - - - - - - + {sections.map((section: Section) => { + const sectionServices = services.filter((s: { category: string }) => s.category === section.id) + if (sectionServices.length === 0) return null + + return ( + + ) + })} ) : !error && ( diff --git a/src/components/config-editor.tsx b/src/components/config-editor.tsx deleted file mode 100644 index 37b755d..0000000 --- a/src/components/config-editor.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { useEffect, useState } from 'react' -import Editor from '@monaco-editor/react' -import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet' -import { Button } from '@/components/ui/button' -import { Settings2 } from 'lucide-react' -import yaml from 'js-yaml' -import { ServiceCard } from '@/types/services' - -interface ConfigEditorProps { - currentConfig: { services: ServiceCard[] } - onSave: (newConfig: string) => Promise -} - -export function ConfigEditor({ currentConfig, onSave }: ConfigEditorProps) { - const [isOpen, setIsOpen] = useState(false) - const [editorContent, setEditorContent] = useState('') - const [error, setError] = useState(null) - - useEffect(() => { - try { - const yamlString = yaml.dump(currentConfig, { indent: 2 }) - setEditorContent(yamlString) - setError(null) - } catch (err) { - setError('Failed to convert configuration to YAML') - console.error('Error converting config to YAML:', err) - } - }, [currentConfig]) - - const handleSave = async () => { - try { - // Validate YAML - yaml.load(editorContent) - - // If validation passes, save - await onSave(editorContent) - setIsOpen(false) - setError(null) - } catch (err) { - setError(err instanceof Error ? err.message : 'Invalid YAML format') - } - } - - return ( - - - - - - - Edit Configuration - -
- setEditorContent(value || '')} - options={{ - minimap: { enabled: false }, - fontSize: 14, - lineNumbers: 'on', - scrollBeyondLastLine: false, - wordWrap: 'on', - wrappingIndent: 'indent', - }} - /> - {error && ( -
{error}
- )} -
- - -
-
-
-
- ) -} \ No newline at end of file diff --git a/src/hooks/useConfig.ts b/src/hooks/useConfig.ts index aef2b2a..451c1f6 100644 --- a/src/hooks/useConfig.ts +++ b/src/hooks/useConfig.ts @@ -1,57 +1,28 @@ import { useState, useEffect } from 'react' import yaml from 'js-yaml' -import { ServiceCard } from '../types/services' - -interface Config { - services: ServiceCard[] -} +import { Config } from '../types/services' export function useConfig() { - const [config, setConfig] = useState({ services: [] }) + const [config, setConfig] = useState({ services: [], sections: [] }) const [error, setError] = useState(null) - const loadConfig = async () => { - try { - const response = await fetch('/config.yaml') - if (!response.ok) { - throw new Error('Failed to load configuration') - } - const text = await response.text() - const parsed = yaml.load(text) as Config - setConfig(parsed) - setError(null) - } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to load configuration') - console.error('Error loading configuration:', err) - } - } - - const saveConfig = async (newConfig: string) => { - try { - const response = await fetch('/api/config/update', { - method: 'POST', - credentials: 'include', - headers: { - 'Content-Type': 'text/yaml', - }, - body: newConfig, - }) - - if (!response.ok) { - const errorData = await response.json().catch(() => ({ error: 'Unknown error' })) - throw new Error(errorData.error || 'Failed to save configuration') - } - - // Reload the config after saving - await loadConfig() - } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to save configuration') - console.error('Error saving configuration:', err) - throw err - } - } - useEffect(() => { + const loadConfig = async () => { + try { + const response = await fetch('/config.yaml') + if (!response.ok) { + throw new Error('Failed to load configuration') + } + const text = await response.text() + const parsed = yaml.load(text) as Config + setConfig(parsed) + setError(null) + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to load configuration') + console.error('Error loading configuration:', err) + } + } + // Load config immediately loadConfig() @@ -61,5 +32,5 @@ export function useConfig() { return () => clearInterval(interval) }, []) - return { config, error, saveConfig } + return { config, error } } \ No newline at end of file diff --git a/src/types/services.ts b/src/types/services.ts index f40de2a..1bcedfc 100644 --- a/src/types/services.ts +++ b/src/types/services.ts @@ -1,11 +1,31 @@ import { ReactNode } from 'react' +export type CategoryId = 'system' | 'media' | 'monitoring' | 'tools' | 'acot' | 'home' + +export interface CardStyle { + background: string + iconColor: string +} + +export interface Section { + id: CategoryId + title: string + icon: string + cardStyle: CardStyle +} + export interface ServiceCard { name: string description: string url: string icon?: ReactNode iconName?: string - category: 'system' | 'media' | 'monitoring' | 'tools' | 'acot' | 'home' + category: CategoryId monitorName?: string + cardStyle?: CardStyle +} + +export interface Config { + sections: Section[] + services: ServiceCard[] } \ No newline at end of file