From 774432b7201b4a23b8464c59ab90d1a8f678dd1e Mon Sep 17 00:00:00 2001 From: BLACK Date: Mon, 8 Dec 2025 22:47:42 +0100 Subject: [PATCH] feat: Implement initial Caddy panel UI with new pages, components, and styling, along with updated dependencies. --- README.md | 45 +++-- package-lock.json | 99 ++++++++- package.json | 9 +- src/app/api/page.tsx | 165 +++++++++++++++ src/app/certs/page.tsx | 58 ++++++ src/app/domains/page.tsx | 154 ++++++++++++++ src/app/globals.css | 78 ++++--- src/app/layout.tsx | 24 +-- src/app/logs/page.tsx | 146 ++++++++++++++ src/app/notifications/page.tsx | 79 ++++++++ src/app/page.module.css | 190 ++++++------------ src/app/page.tsx | 127 ++++++------ src/app/plugins/page.tsx | 65 ++++++ src/app/proxy/page.tsx | 170 ++++++++++++++++ src/app/settings/page.tsx | 87 ++++++++ .../Api/ApiEndpointModal.module.css | 123 ++++++++++++ src/components/Api/ApiEndpointModal.tsx | 142 +++++++++++++ src/components/Api/ApiTestModal.tsx | 111 ++++++++++ src/components/Common/DataTable.module.css | 64 ++++++ src/components/Common/DataTable.tsx | 54 +++++ src/components/Common/ToggleSwitch.module.css | 55 +++++ src/components/Common/ToggleSwitch.tsx | 25 +++ .../Dashboard/LogPreview.module.css | 81 ++++++++ src/components/Dashboard/LogPreview.tsx | 41 ++++ .../Dashboard/StatusCard.module.css | 85 ++++++++ src/components/Dashboard/StatusCard.tsx | 41 ++++ .../Dashboard/SystemStatsWidget.module.css | 70 +++++++ .../Dashboard/SystemStatsWidget.tsx | 43 ++++ src/components/Domains/DomainModal.module.css | 104 ++++++++++ src/components/Domains/DomainModal.tsx | 101 ++++++++++ src/components/Layout/AppLayout.module.css | 21 ++ src/components/Layout/AppLayout.tsx | 15 ++ src/components/Layout/Sidebar.module.css | 100 +++++++++ src/components/Layout/Sidebar.tsx | 81 ++++++++ .../Proxy/DirectiveBlock.module.css | 88 ++++++++ src/components/Proxy/DirectiveBlock.tsx | 85 ++++++++ src/components/Proxy/ProxyEditor.module.css | 172 ++++++++++++++++ src/components/Proxy/ProxyEditor.tsx | 176 ++++++++++++++++ 38 files changed, 3124 insertions(+), 250 deletions(-) create mode 100644 src/app/api/page.tsx create mode 100644 src/app/certs/page.tsx create mode 100644 src/app/domains/page.tsx create mode 100644 src/app/logs/page.tsx create mode 100644 src/app/notifications/page.tsx create mode 100644 src/app/plugins/page.tsx create mode 100644 src/app/proxy/page.tsx create mode 100644 src/app/settings/page.tsx create mode 100644 src/components/Api/ApiEndpointModal.module.css create mode 100644 src/components/Api/ApiEndpointModal.tsx create mode 100644 src/components/Api/ApiTestModal.tsx create mode 100644 src/components/Common/DataTable.module.css create mode 100644 src/components/Common/DataTable.tsx create mode 100644 src/components/Common/ToggleSwitch.module.css create mode 100644 src/components/Common/ToggleSwitch.tsx create mode 100644 src/components/Dashboard/LogPreview.module.css create mode 100644 src/components/Dashboard/LogPreview.tsx create mode 100644 src/components/Dashboard/StatusCard.module.css create mode 100644 src/components/Dashboard/StatusCard.tsx create mode 100644 src/components/Dashboard/SystemStatsWidget.module.css create mode 100644 src/components/Dashboard/SystemStatsWidget.tsx create mode 100644 src/components/Domains/DomainModal.module.css create mode 100644 src/components/Domains/DomainModal.tsx create mode 100644 src/components/Layout/AppLayout.module.css create mode 100644 src/components/Layout/AppLayout.tsx create mode 100644 src/components/Layout/Sidebar.module.css create mode 100644 src/components/Layout/Sidebar.tsx create mode 100644 src/components/Proxy/DirectiveBlock.module.css create mode 100644 src/components/Proxy/DirectiveBlock.tsx create mode 100644 src/components/Proxy/ProxyEditor.module.css create mode 100644 src/components/Proxy/ProxyEditor.tsx diff --git a/README.md b/README.md index e215bc4..9b92d7d 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,37 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). +# CaddyPanel -## Getting Started +Ein modernes Web-Interface für den Caddy Web Server. -First, run the development server: +Dieses Projekt basiert auf [Next.js](https://nextjs.org) und bietet eine intuitive Oberfläche zur Verwaltung von Caddy. + +## Erste Schritte + +Starten Sie den Entwicklungsserver: ```bash npm run dev -# or +# oder yarn dev -# or +# oder pnpm dev -# or +# oder bun dev ``` -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +Öffnen Sie [http://localhost:3000](http://localhost:3000) in Ihrem Browser. -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. +## Funktionen -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. +- **Dashboard**: Überblick über Systemstatus und Aktivitäten. +- **Proxy Regeln**: Visueller Drag-and-Drop Editor für Reverse-Proxy-Routen. +- **Domain Verwaltung**: Einfaches Hinzufügen und Konfigurieren von Domains inkl. SSL. +- **Log Viewer**: Detaillierte Log-Analyse mit Filtern für Logger, Inhalt und Zeit. +- **API Explorer**: Integriertes Tool zum Testen und Verwalten von API-Endpoints. +- **Zertifikate**: Übersicht über aktive SSL-Zertifikate. -## Learn More +## Technologien -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. +- Next.js 14 +- React +- TypeScript +- CSS Modules diff --git a/package-lock.json b/package-lock.json index 24e31bb..80c66f9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,16 @@ "name": "caddy-panel", "version": "0.1.0", "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", + "@types/uuid": "^10.0.0", + "clsx": "^2.1.1", + "lucide-react": "^0.556.0", "next": "16.0.7", "react": "19.2.0", - "react-dom": "19.2.0" + "react-dom": "19.2.0", + "uuid": "^13.0.0" }, "devDependencies": { "@types/node": "^20", @@ -261,6 +268,59 @@ "node": ">=6.9.0" } }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "license": "MIT", + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", + "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/@emnapi/core": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz", @@ -1289,6 +1349,12 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.48.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.1.tgz", @@ -2292,6 +2358,15 @@ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -4264,6 +4339,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "0.556.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.556.0.tgz", + "integrity": "sha512-iOb8dRk7kLaYBZhR2VlV1CeJGxChBgUthpSP8wom9jfj79qovgG6qcSdiy6vkoREKPnbUYzJsCn4o4PtG3Iy+A==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -5746,6 +5830,19 @@ "punycode": "^2.1.0" } }, + "node_modules/uuid": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 6ec7953..cff726b 100644 --- a/package.json +++ b/package.json @@ -9,9 +9,16 @@ "lint": "eslint" }, "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", + "@types/uuid": "^10.0.0", + "clsx": "^2.1.1", + "lucide-react": "^0.556.0", "next": "16.0.7", "react": "19.2.0", - "react-dom": "19.2.0" + "react-dom": "19.2.0", + "uuid": "^13.0.0" }, "devDependencies": { "@types/node": "^20", diff --git a/src/app/api/page.tsx b/src/app/api/page.tsx new file mode 100644 index 0000000..bc1cf39 --- /dev/null +++ b/src/app/api/page.tsx @@ -0,0 +1,165 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import { DataTable } from "@/components/Common/DataTable"; +import { Plus, Play, Edit2, Trash2, Globe, FileText } from "lucide-react"; +import { ApiEndpointModal, ApiEndpoint } from "@/components/Api/ApiEndpointModal"; +import { ApiTestModal } from "@/components/Api/ApiTestModal"; + +const MOCK_ENDPOINTS: ApiEndpoint[] = [ + { id: "1", method: "GET", path: "/api/users", description: "List all users", headers: "{}", body: "" }, + { id: "2", method: "POST", path: "/api/users", description: "Create a new user", headers: "{}", body: '{"name": "New User"}' }, + { id: "3", method: "GET", path: "/api/health", description: "System health check", headers: "{}", body: "" }, +]; + +export default function ApiPage() { + const [endpoints, setEndpoints] = useState(MOCK_ENDPOINTS); + const [isEditModalOpen, setIsEditModalOpen] = useState(false); + const [isTestModalOpen, setIsTestModalOpen] = useState(false); + const [currentEndpoint, setCurrentEndpoint] = useState(null); + + const handleAdd = () => { + setCurrentEndpoint(null); + setIsEditModalOpen(true); + }; + + const handleEdit = (endpoint: ApiEndpoint) => { + setCurrentEndpoint(endpoint); + setIsEditModalOpen(true); + }; + + const handleDelete = (id: string) => { + setEndpoints(endpoints.filter(e => e.id !== id)); + }; + + const handleRun = (endpoint: ApiEndpoint) => { + setCurrentEndpoint(endpoint); + setIsTestModalOpen(true); + }; + + const handleSave = (data: ApiEndpoint) => { + if (currentEndpoint && !isTestModalOpen) { // Edit mode + setEndpoints(endpoints.map(e => e.id === currentEndpoint.id ? data : e)); + } else { + setEndpoints([...endpoints, data]); + } + }; + + return ( +
+
+
+

API Explorer

+

Definieren und testen Sie Ihre eigenen API-Endpoints

+
+ +
+ + ( + + {item.method} + + ) + }, + { header: "Pfad", accessor: "path", className: "font-mono text-sm" }, + { header: "Beschreibung", accessor: "description" }, + { + header: "Aktionen", + accessor: (item) => ( +
+ + +
+ + + + + + +
+ + + +
+ ) + } + ]} + /> + + setIsEditModalOpen(false)} + onSave={handleSave} + initialData={currentEndpoint} + /> + + setIsTestModalOpen(false)} + endpoint={currentEndpoint} + /> +
+ ); +} diff --git a/src/app/certs/page.tsx b/src/app/certs/page.tsx new file mode 100644 index 0000000..19545b0 --- /dev/null +++ b/src/app/certs/page.tsx @@ -0,0 +1,58 @@ +import { DataTable, StatusBadge } from "@/components/Common/DataTable"; + +interface Certificate { + id: string; + domain: string; + issuer: string; + expiryDate: string; + status: "valid" | "expiring" | "expired"; +} + +const MOCK_CERTS: Certificate[] = [ + { id: "1", domain: "example.com", issuer: "Let's Encrypt", expiryDate: "2024-01-15", status: "valid" }, + { id: "2", domain: "api.test.org", issuer: "Let's Encrypt", expiryDate: "2024-01-20", status: "valid" }, + { id: "3", domain: "blog.mysite.net", issuer: "ZeroSSL", expiryDate: "2023-11-01", status: "expiring" }, + { id: "4", domain: "old.legacy.net", issuer: "Let's Encrypt", expiryDate: "2023-09-10", status: "expired" }, +]; + +export default function CertificatesPage() { + return ( +
+
+
+

Zertifikate

+

Verwaltete TLS Zertifikate

+
+
+ + ( + + {item.expiryDate} + + ) + }, + { + header: "Status", + accessor: (item) => ( + + ) + }, + ]} + /> +
+ ); +} diff --git a/src/app/domains/page.tsx b/src/app/domains/page.tsx new file mode 100644 index 0000000..7603f89 --- /dev/null +++ b/src/app/domains/page.tsx @@ -0,0 +1,154 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import { DataTable, StatusBadge } from "@/components/Common/DataTable"; +import { Plus, Edit2, Trash2, Globe, FileText, ShieldCheck } from "lucide-react"; +import { DomainModal, DomainData } from "@/components/Domains/DomainModal"; + +const MOCK_DOMAINS: DomainData[] = [ + { id: "1", domain: "example.com", port: "443", status: "active", ssl: true }, + { id: "2", domain: "api.test.org", port: "443", status: "active", ssl: true }, + { id: "3", domain: "dev.local", port: "80", status: "inactive", ssl: false }, + { id: "4", domain: "blog.mysite.net", port: "443", status: "error", ssl: true }, +]; + +export default function DomainsPage() { + const [domains, setDomains] = useState(MOCK_DOMAINS); + const [isModalOpen, setIsModalOpen] = useState(false); + const [editingDomain, setEditingDomain] = useState(null); + + const handleAdd = () => { + setEditingDomain(null); + setIsModalOpen(true); + }; + + const handleEdit = (domain: DomainData) => { + setEditingDomain(domain); + setIsModalOpen(true); + }; + + const handleDelete = (id: string) => { + // Confirm dialogs can be tricky in some environments, removing for now to ensure functionality + // In a real app, we would use a custom modal or toast + setDomains(domains.filter(d => d.id !== id)); + }; + + const handleSave = (data: DomainData) => { + if (editingDomain) { + setDomains(domains.map(d => d.id === editingDomain.id ? data : d)); + } else { + setDomains([...domains, data]); + } + setIsModalOpen(false); + }; + + return ( +
+
+
+

Domains

+

Verwalten Sie Ihre konfigurierten Domains und SSL-Zertifikate

+
+ +
+ + ( + + + {item.ssl ? 'Gesichert' : 'Unsicher'} + + ) + }, + { + header: "Status", + accessor: (item) => ( + + ) + }, + { + header: "Aktionen", + accessor: (item) => ( +
+
+ + + + + + + + + + + + +
+ + +
+ ) + } + ]} + /> + + setIsModalOpen(false)} + onSave={handleSave} + initialData={editingDomain} + /> +
+ ); +} diff --git a/src/app/globals.css b/src/app/globals.css index e3734be..e81ba07 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,27 +1,39 @@ :root { - --background: #ffffff; - --foreground: #171717; -} + /* Colors */ + --bg-app: #0f1014; + /* Very dark blue-ish grey */ + --bg-surface: #181b21; + --bg-surface-hover: #232730; + --bg-surface-active: #2d323b; -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - } -} + --color-primary: #1F88C0; + /* Caddy "Curious Blue" */ + --color-primary-dim: #16608a; -html, -body { - max-width: 100vw; - overflow-x: hidden; -} + /* Alias for components expecting --primary */ + --primary: var(--color-primary); -body { - color: var(--foreground); - background: var(--background); - font-family: Arial, Helvetica, sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; + --text-main: #ffffff; + --text-secondary: #cbd5e1; + --text-muted: #94a3b8; + + --status-success: #22c55e; + --status-warning: #eab308; + --status-error: #ef4444; + + /* Spacing */ + --spacing-xs: 4px; + --spacing-sm: 8px; + --spacing-md: 16px; + --spacing-lg: 24px; + --spacing-xl: 32px; + + /* Borders */ + --radius-sm: 4px; + --radius-md: 8px; + --radius-lg: 12px; + --border-color: #2d323b; + --border-light: #3e4552; } * { @@ -30,13 +42,29 @@ body { margin: 0; } +html, +body { + max-width: 100vw; + overflow-x: hidden; + height: 100%; +} + +body { + color: var(--text-main); + background: var(--bg-app); + font-family: var(--font-inter), sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + a { color: inherit; text-decoration: none; } -@media (prefers-color-scheme: dark) { - html { - color-scheme: dark; - } -} +button { + cursor: pointer; + border: none; + background: none; + font-family: inherit; +} \ No newline at end of file diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 42fc323..f24d9fe 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,20 +1,14 @@ import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; +import { Inter, JetBrains_Mono } from "next/font/google"; +import { AppLayout } from "@/components/Layout/AppLayout"; import "./globals.css"; -const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], -}); - -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", - subsets: ["latin"], -}); +const inter = Inter({ subsets: ["latin"], variable: "--font-inter" }); +const jetbrainsMono = JetBrains_Mono({ subsets: ["latin"], variable: "--font-mono" }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "CaddyPanel", + description: "Modern web interface for Caddy Reverse Proxy", }; export default function RootLayout({ @@ -24,8 +18,10 @@ export default function RootLayout({ }>) { return ( - - {children} + + + {children} + ); diff --git a/src/app/logs/page.tsx b/src/app/logs/page.tsx new file mode 100644 index 0000000..4f9d892 --- /dev/null +++ b/src/app/logs/page.tsx @@ -0,0 +1,146 @@ +"use client"; + +import { DataTable, StatusBadge } from "@/components/Common/DataTable"; +import { useState, useEffect } from "react"; +import { useSearchParams } from "next/navigation"; + +interface LogEntry { + id: string; + timestamp: string; + level: "info" | "warn" | "error"; + logger: string; + message: string; +} + +const MOCK_LOGS: LogEntry[] = [ + { id: "1", timestamp: "2023-10-27 10:42:01", level: "info", logger: "http.log.access.log0", message: "handled request" }, + { id: "2", timestamp: "2023-10-27 10:42:05", level: "info", logger: "http.log.access.log0", message: "handled request" }, + { id: "3", timestamp: "2023-10-27 10:45:12", level: "warn", logger: "http.handlers.reverse_proxy", message: "upstream unstructured" }, + { id: "4", timestamp: "2023-10-27 10:46:00", level: "error", logger: "http.handlers.reverse_proxy", message: "dial tcp 127.0.0.1:8080: connect: connection refused" }, + { id: "5", timestamp: "2023-10-27 10:46:01", level: "info", logger: "http.log.access.log0", message: "handled request" }, + { id: "6", timestamp: "2023-10-27 10:47:33", level: "info", logger: "admin.api", message: "config reload" }, + { id: "7", timestamp: "2023-10-27 10:48:15", level: "error", logger: "tls.obtain", message: "failed to obtain certificate: acme: error" }, +]; + +export default function LogsPage() { + const searchParams = useSearchParams(); + const initialFilter = searchParams.get('filter') || ''; + + // Filters + const [levelFilter, setLevelFilter] = useState("all"); + const [loggerFilter, setLoggerFilter] = useState(""); + const [contentFilter, setContentFilter] = useState(""); + const [timeStart, setTimeStart] = useState(""); + const [timeEnd, setTimeEnd] = useState(""); + + // Apply initial filter if present + useEffect(() => { + if (initialFilter) { + setContentFilter(initialFilter); + } + }, []); // Run only once + + const filteredLogs = MOCK_LOGS.filter(log => { + // Level Filter + if (levelFilter !== "all" && log.level !== levelFilter) return false; + + // Logger Filter (Global or Specific) + const logger = loggerFilter.trim(); + if (logger && !log.logger.toLowerCase().includes(logger.toLowerCase())) return false; + + // Content Filter + const content = contentFilter.trim(); + if (content && !log.message.toLowerCase().includes(content.toLowerCase()) && !log.logger.toLowerCase().includes(content.toLowerCase())) return false; + + // Time Filter + if (timeStart && log.timestamp < timeStart) return false; + if (timeEnd && log.timestamp > timeEnd) return false; + + return true; + }); + + return ( +
+
+

Logs

+

System- und Zugriffs-Log Viewer

+
+ +
+
+ + +
+ +
+ + setLoggerFilter(e.target.value)} + style={{ padding: '8px', borderRadius: '6px', border: '1px solid var(--border-color)', backgroundColor: 'var(--bg-main)', color: 'var(--text-main)' }} + /> +
+ +
+ + setContentFilter(e.target.value)} + style={{ padding: '8px', borderRadius: '6px', border: '1px solid var(--border-color)', backgroundColor: 'var(--bg-main)', color: 'var(--text-main)' }} + /> +
+ +
+ + setTimeStart(e.target.value)} + style={{ padding: '7px', borderRadius: '6px', border: '1px solid var(--border-color)', backgroundColor: 'var(--bg-main)', color: 'var(--text-main)', fontSize: '0.8rem' }} + /> +
+
+ + ( + + ) + }, + { header: "Logger", accessor: "logger", className: "font-mono text-sm color-[var(--primary)]" }, + { header: "Nachricht", accessor: "message" }, + ]} + /> +
+ ); +} diff --git a/src/app/notifications/page.tsx b/src/app/notifications/page.tsx new file mode 100644 index 0000000..24f96cf --- /dev/null +++ b/src/app/notifications/page.tsx @@ -0,0 +1,79 @@ +import { ArrowRight, Bell, CheckCircle, AlertTriangle, AlertCircle } from "lucide-react"; +import clsx from "clsx"; + +interface Notification { + id: string; + type: "info" | "warning" | "error" | "success"; + title: string; + message: string; + timestamp: string; + read: boolean; +} + +const MOCK_NOTIFICATIONS: Notification[] = [ + { id: "1", type: "warning", title: "Certificate Expiring Soon", message: "The certificate for blog.mysite.net will expire in 3 days.", timestamp: "2 hours ago", read: false }, + { id: "2", type: "error", title: "Proxy Connection Failed", message: "Failed to connect to upstream 192.168.1.50:8080. Connection refused.", timestamp: "5 hours ago", read: false }, + { id: "3", type: "success", title: "Caddy Updated", message: "Successfully updated Caddy to v2.7.5.", timestamp: "1 day ago", read: true }, + { id: "4", type: "info", title: "New Domain Added", message: "Domain example.com has been configured.", timestamp: "2 days ago", read: true }, +]; + +export default function NotificationsPage() { + return ( +
+
+
+

Benachrichtigungen

+

Systemwarnungen und Nachrichten

+
+ +
+ +
+ {MOCK_NOTIFICATIONS.map((notif) => ( +
+
+ {notif.type === 'error' ? : + notif.type === 'warning' ? : + notif.type === 'success' ? : + } +
+
+
+

{notif.title}

+ {notif.timestamp} +
+

{notif.message}

+
+
+ ))} +
+
+ ); +} diff --git a/src/app/page.module.css b/src/app/page.module.css index 59dea42..ea6892a 100644 --- a/src/app/page.module.css +++ b/src/app/page.module.css @@ -1,141 +1,75 @@ -.page { - --background: #fafafa; - --foreground: #fff; - - --text-primary: #000; - --text-secondary: #666; - - --button-primary-hover: #383838; - --button-secondary-hover: #f2f2f2; - --button-secondary-border: #ebebeb; - +.container { display: flex; - min-height: 100vh; - align-items: center; - justify-content: center; - font-family: var(--font-geist-sans); - background-color: var(--background); + flex-direction: column; + gap: var(--spacing-xl); } -.main { +.header { display: flex; - min-height: 100vh; - width: 100%; - max-width: 800px; flex-direction: column; - align-items: flex-start; + gap: var(--spacing-xs); +} + +.title { + font-size: 2rem; + font-weight: 700; + letter-spacing: -0.03em; +} + +.subtitle { + color: var(--text-muted); +} + +.grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: var(--spacing-lg); +} + +.sections { + display: grid; + grid-template-columns: 2fr 1fr; + gap: var(--spacing-lg); +} + +@media (max-width: 1024px) { + .sections { + grid-template-columns: 1fr; + } +} + +.placeholderWidget { + background-color: var(--bg-surface); + border: 1px solid var(--border-color); + border-radius: var(--radius-lg); + padding: var(--spacing-lg); + display: flex; + flex-direction: column; + gap: var(--spacing-md); +} + +.widgetHeader { + display: flex; justify-content: space-between; - background-color: var(--foreground); - padding: 120px 60px; -} - -.intro { - display: flex; - flex-direction: column; - align-items: flex-start; - text-align: left; - gap: 24px; -} - -.intro h1 { - max-width: 320px; - font-size: 40px; - font-weight: 600; - line-height: 48px; - letter-spacing: -2.4px; - text-wrap: balance; - color: var(--text-primary); -} - -.intro p { - max-width: 440px; - font-size: 18px; - line-height: 32px; - text-wrap: balance; - color: var(--text-secondary); -} - -.intro a { - font-weight: 500; - color: var(--text-primary); -} - -.ctas { - display: flex; - flex-direction: row; - width: 100%; - max-width: 440px; - gap: 16px; - font-size: 14px; -} - -.ctas a { - display: flex; - justify-content: center; align-items: center; - height: 40px; - padding: 0 16px; - border-radius: 128px; - border: 1px solid transparent; - transition: 0.2s; - cursor: pointer; - width: fit-content; - font-weight: 500; } -a.primary { - background: var(--text-primary); - color: var(--background); - gap: 8px; +.widgetTitle { + font-weight: 600; + font-size: 1.1rem; } -a.secondary { - border-color: var(--button-secondary-border); +.widgetIcon { + color: var(--text-muted); } -/* Enable hover only on non-touch devices */ -@media (hover: hover) and (pointer: fine) { - a.primary:hover { - background: var(--button-primary-hover); - border-color: transparent; - } - - a.secondary:hover { - background: var(--button-secondary-hover); - border-color: transparent; - } -} - -@media (max-width: 600px) { - .main { - padding: 48px 24px; - } - - .intro { - gap: 16px; - } - - .intro h1 { - font-size: 32px; - line-height: 40px; - letter-spacing: -1.92px; - } -} - -@media (prefers-color-scheme: dark) { - .logo { - filter: invert(); - } - - .page { - --background: #000; - --foreground: #000; - - --text-primary: #ededed; - --text-secondary: #999; - - --button-primary-hover: #ccc; - --button-secondary-hover: #1a1a1a; - --button-secondary-border: #1a1a1a; - } -} +.widgetContent { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + background: var(--bg-app); + border-radius: var(--radius-md); + color: var(--text-muted); + min-height: 200px; +} \ No newline at end of file diff --git a/src/app/page.tsx b/src/app/page.tsx index 7b947a2..b0ebc49 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,66 +1,75 @@ -import Image from "next/image"; +import { StatusCard } from "@/components/Dashboard/StatusCard"; +import { LogPreview } from "@/components/Dashboard/LogPreview"; +import { SystemStatsWidget } from "@/components/Dashboard/SystemStatsWidget"; +import { Activity, Server, Globe, ShieldCheck, ArrowUpRight } from "lucide-react"; import styles from "./page.module.css"; -export default function Home() { +const MOCK_LOGS = [ + { id: "1", timestamp: "2023-10-27 10:42:01", level: "info" as const, message: "http.log.access.log0 [INFO] handled request" }, + { id: "2", timestamp: "2023-10-27 10:42:05", level: "info" as const, message: "http.log.access.log0 [INFO] handled request" }, + { id: "3", timestamp: "2023-10-27 10:45:12", level: "warn" as const, message: "http.handlers.reverse_proxy [WARN] upstream unstructured" }, + { id: "4", timestamp: "2023-10-27 10:46:00", level: "error" as const, message: "http.handlers.reverse_proxy [ERROR] dial tcp 127.0.0.1:8080: connect: connection refused" }, + { id: "5", timestamp: "2023-10-27 10:46:01", level: "info" as const, message: "http.log.access.log0 [INFO] handled request" }, +]; + +export default function Dashboard() { return ( -
-
- Next.js logo +
+

Dashboard

+
Überblick über Ihren Caddy Server Status
+
+ +
+ -
-

To get started, edit the page.tsx file.

-

- Looking for a starting point or more instructions? Head over to{" "} - - Templates - {" "} - or the{" "} - - Learning - {" "} - center. -

-
- -
+ + + + +
+ +
+ + + +
); } diff --git a/src/app/plugins/page.tsx b/src/app/plugins/page.tsx new file mode 100644 index 0000000..997d808 --- /dev/null +++ b/src/app/plugins/page.tsx @@ -0,0 +1,65 @@ +import { DataTable, StatusBadge } from "@/components/Common/DataTable"; +import { Download, Trash2 } from "lucide-react"; + +interface Plugin { + id: string; + name: string; + version: string; + description: string; + author: string; + status: "installed" | "update_available"; +} + +const MOCK_PLUGINS: Plugin[] = [ + { id: "1", name: "caddy-dns-cloudflare", version: "v1.2.0", description: "Cloudflare DNS provider for Caddy", author: "caddy-dns", status: "installed" }, + { id: "2", name: "caddy-security", version: "v1.0.1", description: "Security plugin for Caddy", author: "greenpau", status: "update_available" }, + { id: "3", name: "caddy-ratelimit", version: "v0.9.5", description: "Rate limiting middleware", author: "mholt", status: "installed" }, +]; + +export default function PluginsPage() { + return ( +
+
+
+

Plugins

+

Installierte Caddy Module verwalten

+
+
+ + ( + + ) + }, + { + header: "Actions", + accessor: (item) => ( +
+ {item.status === 'update_available' && ( + + )} + +
+ ) + }, + ]} + /> +
+ ); +} diff --git a/src/app/proxy/page.tsx b/src/app/proxy/page.tsx new file mode 100644 index 0000000..49c191e --- /dev/null +++ b/src/app/proxy/page.tsx @@ -0,0 +1,170 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import { DataTable, StatusBadge } from "@/components/Common/DataTable"; +import { Plus, Edit2, Trash2, FileText, Globe, ShieldCheck } from "lucide-react"; +import { ProxyEditor } from "@/components/Proxy/ProxyEditor"; +import { DirectiveData } from "@/components/Proxy/DirectiveBlock"; + +interface ProxyRule { + id: string; + matcher: string; + upstream: string; + loadBalancing: string; + status: "active" | "inactive"; + directives: DirectiveData[]; +} + +const MOCK_PROXIES: ProxyRule[] = [ + { id: "1", matcher: "/api/*", upstream: "localhost:3000", loadBalancing: "Round Robin", status: "active", directives: [] }, + { id: "2", matcher: "/blog", upstream: "wordpress-container:80", loadBalancing: "Random", status: "active", directives: [] }, + { id: "3", matcher: "*.example.com", upstream: "192.168.1.50:8080", loadBalancing: "Least Conn", status: "inactive", directives: [] }, +]; + +import { ToggleSwitch } from "@/components/Common/ToggleSwitch"; + +export default function ProxyPage() { + const [proxies, setProxies] = useState(MOCK_PROXIES); + const [isEditing, setIsEditing] = useState(false); + const [editingId, setEditingId] = useState(null); + + const handleAdd = () => { + setEditingId(null); + setIsEditing(true); + }; + + const handleEdit = (id: string) => { + setEditingId(id); + setIsEditing(true); + }; + + const handleDelete = (id: string) => { + // Confirm dialog removed for better UX or replaced by custom modal + setProxies(proxies.filter(p => p.id !== id)); + }; + + const handleSave = (matcher: string, directives: DirectiveData[]) => { + if (editingId) { + setProxies(proxies.map(p => p.id === editingId ? { ...p, matcher, directives } : p)); + } else { + const newRule: ProxyRule = { + id: Math.random().toString(36).substr(2, 9), + matcher, + upstream: directives.find(d => d.type === 'reverse_proxy')?.args.upstream || "pending-config", + loadBalancing: directives.find(d => d.type === 'reverse_proxy')?.args.lb_policy || "Round Robin", + status: "active", + directives + }; + setProxies([...proxies, newRule]); + } + setIsEditing(false); + }; + + const currentRule = editingId ? proxies.find(p => p.id === editingId) : null; + + if (isEditing) { + return ( +
+
+

{editingId ? 'Proxy Regel bearbeiten' : 'Neue Proxy Regel'}

+

Konfigurieren Sie Direktiven per Drag & Drop

+
+ setIsEditing(false)} + /> +
+ ); + } + + return ( +
+
+
+

Proxy Regeln

+

Reverse Proxy Konfigurationen verwalten

+
+ +
+ + ( +
+ { + setProxies(proxies.map(p => p.id === item.id ? { ...p, status: checked ? 'active' : 'inactive' } : p)); + }} + /> + +
+ ) + }, + { + header: "Aktionen", + accessor: (item) => ( +
+
+ + + + + + + + + +
+ + +
+ ) + } + ]} + /> +
+ ); +} diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx new file mode 100644 index 0000000..8caaa14 --- /dev/null +++ b/src/app/settings/page.tsx @@ -0,0 +1,87 @@ +"use client"; + +import { Save } from "lucide-react"; + +export default function SettingsPage() { + return ( +
+
+

Globale Einstellungen

+

Allgemeine Caddy-Parameter konfigurieren

+
+ +
+
+

Admin Interface

+
+
+ + +
+
+
+ +
+ +
+

Sicherheit

+
+ + +
+
+ + +

E-Mail-Adresse für ACME-Registrierung und Wiederherstellung.

+
+
+ +
+ +
+
+
+ ); +} diff --git a/src/components/Api/ApiEndpointModal.module.css b/src/components/Api/ApiEndpointModal.module.css new file mode 100644 index 0000000..3354a42 --- /dev/null +++ b/src/components/Api/ApiEndpointModal.module.css @@ -0,0 +1,123 @@ +.overlay { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 50; + backdrop-filter: blur(4px); +} + +.modal { + background-color: var(--bg-surface); + border: 1px solid var(--border-color); + border-radius: var(--radius-lg); + width: 100%; + max-width: 600px; + padding: var(--spacing-lg); + box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); + display: flex; + flex-direction: column; + gap: var(--spacing-md); + max-height: 90vh; + overflow-y: auto; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.title { + font-size: 1.25rem; + font-weight: 600; + color: var(--text-main); +} + +.form { + display: flex; + flex-direction: column; + gap: var(--spacing-md); +} + +.inputGroup { + display: flex; + flex-direction: column; + gap: 6px; +} + +.label { + font-size: 0.875rem; + font-weight: 500; + color: var(--text-muted); +} + +.input, +.textarea, +.select { + padding: 10px; + border-radius: var(--radius-md); + border: 1px solid var(--border-color); + background-color: var(--bg-main); + color: var(--text-main); + font-size: 0.95rem; + font-family: inherit; +} + +.textarea { + min-height: 100px; + resize: vertical; +} + +.input:focus, +.textarea:focus, +.select:focus { + outline: none; + border-color: var(--primary); +} + +.row { + display: flex; + gap: var(--spacing-md); +} + +.actions { + display: flex; + justify-content: flex-end; + gap: var(--spacing-md); + margin-top: var(--spacing-sm); +} + +.btn { + padding: 8px 16px; + border-radius: 6px; + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + border: 1px solid transparent; + transition: all 0.2s; +} + +.btnSecondary { + background-color: transparent; + border-color: var(--border-color); + color: var(--text-main); +} + +.btnSecondary:hover { + background-color: var(--bg-surface-hover); +} + +.btnPrimary { + background-color: var(--primary); + color: white; +} + +.btnPrimary:hover { + opacity: 0.9; +} \ No newline at end of file diff --git a/src/components/Api/ApiEndpointModal.tsx b/src/components/Api/ApiEndpointModal.tsx new file mode 100644 index 0000000..a331742 --- /dev/null +++ b/src/components/Api/ApiEndpointModal.tsx @@ -0,0 +1,142 @@ +import React, { useState, useEffect } from "react"; +import styles from "./ApiEndpointModal.module.css"; +import { X } from "lucide-react"; + +export interface ApiEndpoint { + id: string; + method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH"; + path: string; + description: string; + headers?: string; // JSON string for simplicity in mock + body?: string; +} + +interface ApiEndpointModalProps { + isOpen: boolean; + onClose: () => void; + onSave: (data: ApiEndpoint) => void; + initialData?: ApiEndpoint | null; +} + +export function ApiEndpointModal({ isOpen, onClose, onSave, initialData }: ApiEndpointModalProps) { + const [method, setMethod] = useState("GET"); + const [path, setPath] = useState(""); + const [description, setDescription] = useState(""); + const [headers, setHeaders] = useState("{}"); + const [body, setBody] = useState(""); + + useEffect(() => { + if (initialData) { + setMethod(initialData.method); + setPath(initialData.path); + setDescription(initialData.description); + setHeaders(initialData.headers || "{}"); + setBody(initialData.body || ""); + } else { + setMethod("GET"); + setPath(""); + setDescription(""); + setHeaders("{}"); + setBody(""); + } + }, [initialData, isOpen]); + + if (!isOpen) return null; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + onSave({ + id: initialData?.id || Math.random().toString(36).substr(2, 9), + method, + path, + description, + headers, + body + }); + onClose(); + }; + + return ( +
+
+
+

{initialData ? "Endpoint bearbeiten" : "Neuen Endpoint definieren"}

+ +
+
+
+
+ + +
+
+ + setPath(e.target.value)} + placeholder="/api/v1/resource" + required + /> +
+
+ +
+ + setDescription(e.target.value)} + placeholder="Daten abrufen..." + /> +
+ +
+ +