From 2972a83b2e2dc2cc844eafe5a2b3666362c25594 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Mon, 20 Oct 2025 09:58:18 +0000 Subject: [PATCH] Add Docker WebUI MVP --- index.html | 10 +- src/App.tsx | 10 +- src/index.css | 105 ++++++------- src/pages/ContainerDetails.tsx | 276 +++++++++++++++++++++++++++++++++ src/pages/Dashboard.tsx | 184 ++++++++++++++++++++++ src/pages/Login.tsx | 80 ++++++++++ src/pages/Settings.tsx | 178 +++++++++++++++++++++ tailwind.config.ts | 18 +-- 8 files changed, 788 insertions(+), 73 deletions(-) create mode 100644 src/pages/ContainerDetails.tsx create mode 100644 src/pages/Dashboard.tsx create mode 100644 src/pages/Login.tsx create mode 100644 src/pages/Settings.tsx diff --git a/index.html b/index.html index 1647cb9..11593f9 100644 --- a/index.html +++ b/index.html @@ -3,12 +3,12 @@ - docker-watch-desk - - + Docker WebUI - Container Monitoring & Control + + - - + + diff --git a/src/App.tsx b/src/App.tsx index 18daf2e..6ffc4e0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,7 +3,10 @@ import { Toaster as Sonner } from "@/components/ui/sonner"; import { TooltipProvider } from "@/components/ui/tooltip"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { BrowserRouter, Routes, Route } from "react-router-dom"; -import Index from "./pages/Index"; +import Dashboard from "./pages/Dashboard"; +import Login from "./pages/Login"; +import ContainerDetails from "./pages/ContainerDetails"; +import Settings from "./pages/Settings"; import NotFound from "./pages/NotFound"; const queryClient = new QueryClient(); @@ -15,7 +18,10 @@ const App = () => ( - } /> + } /> + } /> + } /> + } /> {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} } /> diff --git a/src/index.css b/src/index.css index 4844bbd..e09a85a 100644 --- a/src/index.css +++ b/src/index.css @@ -8,89 +8,77 @@ All colors MUST be HSL. @layer base { :root { - --background: 0 0% 100%; - --foreground: 222.2 84% 4.9%; + --background: 210 20% 98%; + --foreground: 215 25% 15%; --card: 0 0% 100%; - --card-foreground: 222.2 84% 4.9%; + --card-foreground: 215 25% 15%; --popover: 0 0% 100%; - --popover-foreground: 222.2 84% 4.9%; + --popover-foreground: 215 25% 15%; - --primary: 222.2 47.4% 11.2%; - --primary-foreground: 210 40% 98%; + --primary: 210 100% 50%; + --primary-foreground: 0 0% 100%; - --secondary: 210 40% 96.1%; - --secondary-foreground: 222.2 47.4% 11.2%; + --secondary: 210 15% 92%; + --secondary-foreground: 215 25% 15%; - --muted: 210 40% 96.1%; - --muted-foreground: 215.4 16.3% 46.9%; + --muted: 210 15% 95%; + --muted-foreground: 215 15% 45%; - --accent: 210 40% 96.1%; - --accent-foreground: 222.2 47.4% 11.2%; + --accent: 188 95% 50%; + --accent-foreground: 0 0% 100%; - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 210 40% 98%; + --destructive: 0 85% 60%; + --destructive-foreground: 0 0% 100%; - --border: 214.3 31.8% 91.4%; - --input: 214.3 31.8% 91.4%; - --ring: 222.2 84% 4.9%; + --success: 142 76% 45%; + --success-foreground: 0 0% 100%; - --radius: 0.5rem; + --warning: 38 92% 50%; + --warning-foreground: 0 0% 100%; - --sidebar-background: 0 0% 98%; + --border: 215 20% 88%; + --input: 215 20% 88%; + --ring: 210 100% 50%; - --sidebar-foreground: 240 5.3% 26.1%; - - --sidebar-primary: 240 5.9% 10%; - - --sidebar-primary-foreground: 0 0% 98%; - - --sidebar-accent: 240 4.8% 95.9%; - - --sidebar-accent-foreground: 240 5.9% 10%; - - --sidebar-border: 220 13% 91%; - - --sidebar-ring: 217.2 91.2% 59.8%; + --radius: 0.75rem; } .dark { - --background: 222.2 84% 4.9%; + --background: 222 47% 11%; --foreground: 210 40% 98%; - --card: 222.2 84% 4.9%; + --card: 217 33% 17%; --card-foreground: 210 40% 98%; - --popover: 222.2 84% 4.9%; + --popover: 217 33% 17%; --popover-foreground: 210 40% 98%; - --primary: 210 40% 98%; - --primary-foreground: 222.2 47.4% 11.2%; + --primary: 210 100% 60%; + --primary-foreground: 0 0% 100%; - --secondary: 217.2 32.6% 17.5%; + --secondary: 217 33% 22%; --secondary-foreground: 210 40% 98%; - --muted: 217.2 32.6% 17.5%; - --muted-foreground: 215 20.2% 65.1%; + --muted: 217 33% 22%; + --muted-foreground: 215 20% 65%; - --accent: 217.2 32.6% 17.5%; - --accent-foreground: 210 40% 98%; + --accent: 188 95% 50%; + --accent-foreground: 0 0% 100%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 210 40% 98%; + --destructive: 0 85% 60%; + --destructive-foreground: 0 0% 100%; - --border: 217.2 32.6% 17.5%; - --input: 217.2 32.6% 17.5%; - --ring: 212.7 26.8% 83.9%; - --sidebar-background: 240 5.9% 10%; - --sidebar-foreground: 240 4.8% 95.9%; - --sidebar-primary: 224.3 76.3% 48%; - --sidebar-primary-foreground: 0 0% 100%; - --sidebar-accent: 240 3.7% 15.9%; - --sidebar-accent-foreground: 240 4.8% 95.9%; - --sidebar-border: 240 3.7% 15.9%; - --sidebar-ring: 217.2 91.2% 59.8%; + --success: 142 76% 45%; + --success-foreground: 0 0% 100%; + + --warning: 38 92% 50%; + --warning-foreground: 0 0% 100%; + + --border: 217 33% 25%; + --input: 217 33% 25%; + --ring: 210 100% 60%; } } @@ -100,6 +88,11 @@ All colors MUST be HSL. } body { - @apply bg-background text-foreground; + @apply bg-background text-foreground font-sans antialiased; + } + + code, pre, .font-mono { + font-family: 'JetBrains Mono', 'Fira Code', 'Courier New', monospace; } } + diff --git a/src/pages/ContainerDetails.tsx b/src/pages/ContainerDetails.tsx new file mode 100644 index 0000000..d6c1c07 --- /dev/null +++ b/src/pages/ContainerDetails.tsx @@ -0,0 +1,276 @@ +import { Card } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { ArrowLeft, Play, Square, RotateCw, Activity, Database, Network, HardDrive } from "lucide-react"; +import { useNavigate } from "react-router-dom"; +import { Switch } from "@/components/ui/switch"; +import { Input } from "@/components/ui/input"; + +const ContainerDetails = () => { + const navigate = useNavigate(); + + const logLines = [ + { time: "2025-10-20T10:22:31Z", level: "INFO", message: "Server started on port 8080" }, + { time: "2025-10-20T10:22:35Z", level: "INFO", message: "Connected to database" }, + { time: "2025-10-20T10:22:42Z", level: "WARN", message: "High memory usage detected: 87%" }, + { time: "2025-10-20T10:23:01Z", level: "INFO", message: "Request processed: GET /api/health" }, + { time: "2025-10-20T10:23:15Z", level: "ERROR", message: "Connection timeout to external service" }, + ]; + + return ( +
+ {/* Header */} +
+
+
+
+ +
+
+

redis-prod

+ + Running + +
+

abc123def456

+
+
+
+ + +
+
+
+
+ +
+ + + Overview + Metrics + Logs + Settings + + + + {/* Container Info */} + +

Container Information

+
+
+

Image

+

redis:7.2-alpine

+
+
+

Status

+

Running (3d 14h)

+
+
+

IP Address

+

172.18.0.5

+
+
+

Ports

+

6379:6379/tcp

+
+
+

Network

+

bridge

+
+
+

Restart Policy

+

unless-stopped

+
+
+
+ + {/* Mounts */} + +

Volume Mounts

+
+
+
+

/var/lib/docker/volumes/redis_data

+

→ /data

+
+ Read/Write +
+
+
+
+ + +
+ +
+
+ +

CPU

+
+ 2.5% +
+

Real-time usage

+
+ + +
+
+ +

Memory

+
+ 128 MB +
+

of 512 MB limit

+
+ + +
+
+ +

Network

+
+ 1.2 MB/s +
+

↓ 0.8 MB/s ↑ 0.4 MB/s

+
+ + +
+
+ +

Block I/O

+
+ 45 KB/s +
+

Read + Write

+
+
+ + +

Resource Usage Over Time

+
+ Live metrics chart (real-time WebSocket updates) +
+
+
+ + + +
+

Live Log Stream

+
+ + +
+
+
+ {logLines.map((log, i) => ( +
+ {log.time} + + [{log.level}] + + {log.message} +
+ ))} +
+

+ Logs are streamed in real-time and not stored. Filtering happens client-side. +

+
+
+ + + +

Alert Configuration

+
+
+
+

Enable Alerts

+

+ Send Discord notifications for this container +

+
+ +
+ +
+
+

Alert on Stop

+

+ Notify when container stops or crashes +

+
+ +
+ +
+ + +

+ Logs matching this pattern will trigger an alert +

+
+ +
+ + +

+ Minimum time between alerts to prevent spam +

+
+ +
+ + +

+ Leave empty to use global webhook +

+
+ + +
+
+
+
+
+
+ ); +}; + +export default ContainerDetails; diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx new file mode 100644 index 0000000..bb59108 --- /dev/null +++ b/src/pages/Dashboard.tsx @@ -0,0 +1,184 @@ +import { Card } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Activity, Server, HardDrive, Network, Play, Square, RotateCw, AlertTriangle } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; + +const Dashboard = () => { + const containers = [ + { id: "abc123", name: "redis-prod", status: "running", cpu: "2.5%", memory: "128 MB", uptime: "3d 14h" }, + { id: "def456", name: "postgres-main", status: "running", cpu: "5.2%", memory: "512 MB", uptime: "7d 2h" }, + { id: "ghi789", name: "nginx-proxy", status: "running", cpu: "0.8%", memory: "64 MB", uptime: "12d 8h" }, + { id: "jkl012", name: "api-backend", status: "stopped", cpu: "0%", memory: "0 MB", uptime: "-" }, + ]; + + return ( +
+ {/* Header */} +
+
+
+
+ +
+
+

Docker WebUI

+

docker-node-01

+
+
+ +
+
+ +
+ {/* System Metrics */} +
+ +
+
+ +

CPU

+
+ 24% +
+
+
+
+

4 cores @ 3.2 GHz

+ + + +
+
+ +

Memory

+
+ 8.2 GB +
+
+
+
+

51% of 16 GB

+ + + +
+
+ +

Disk

+
+ 124 GB +
+
+
+
+

62% of 200 GB

+ + + +
+
+ +

Network

+
+ 2.4 MB/s +
+
+ ↓ 1.8 MB/s + + ↑ 0.6 MB/s +
+
+
+ + {/* Container Stats */} +
+

Containers

+
+ +
+ 3 Running + + +
+ 1 Stopped + +
+
+ + {/* Container List */} + +
+ + + + + + + + + + + + + + {containers.map((container) => ( + + + + + + + + + + ))} + +
StatusNameIDCPUMemoryUptimeActions
+ + {container.status} + + {container.name}{container.id}{container.cpu}{container.memory}{container.uptime} +
+ {container.status === "running" ? ( + <> + + + + ) : ( + + )} +
+
+
+
+ + {/* Active Alerts */} + +
+ +

Recent Alerts

+
+
+
+
+

api-backend stopped

+

2 minutes ago

+
+ Container Stopped +
+
+
+
+
+ ); +}; + +export default Dashboard; diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx new file mode 100644 index 0000000..998779a --- /dev/null +++ b/src/pages/Login.tsx @@ -0,0 +1,80 @@ +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Card } from "@/components/ui/card"; +import { Server, Lock, User } from "lucide-react"; +import { useNavigate } from "react-router-dom"; + +const Login = () => { + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const navigate = useNavigate(); + + const handleLogin = (e: React.FormEvent) => { + e.preventDefault(); + // Mock login - in real app would authenticate + navigate("/dashboard"); + }; + + return ( +
+ +
+
+ +
+

Docker WebUI

+

+ Lightweight container monitoring and control +

+
+ +
+
+ +
+ + setUsername(e.target.value)} + className="pl-10 bg-secondary border-border" + /> +
+
+ +
+ +
+ + setPassword(e.target.value)} + className="pl-10 bg-secondary border-border" + /> +
+
+ + +
+ +

+ First time? You'll be prompted to change your password after login. +

+
+
+ ); +}; + +export default Login; diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx new file mode 100644 index 0000000..66fa308 --- /dev/null +++ b/src/pages/Settings.tsx @@ -0,0 +1,178 @@ +import { Card } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { ArrowLeft, Bell, Clock, Globe } from "lucide-react"; +import { useNavigate } from "react-router-dom"; +import { Switch } from "@/components/ui/switch"; + +const Settings = () => { + const navigate = useNavigate(); + + return ( +
+ {/* Header */} +
+
+
+ +

Settings

+
+
+
+ +
+ {/* Discord Webhooks */} + +
+ +

Discord Webhooks

+
+
+
+ + +

+ Default webhook for all containers without a custom webhook +

+
+ +
+
+ + {/* Metrics & Monitoring */} + +
+ +

Metrics & Monitoring

+
+
+
+ + +

+ How often to collect metrics from Docker (lower = more resource usage) +

+
+ +
+ + +

+ Number of data points to keep in memory (for charts) +

+
+ +
+ + +

+ Default regex pattern for detecting errors in logs +

+
+ +
+ + +

+ Default time between alerts to prevent spam +

+
+
+
+ + {/* General Settings */} + +
+ +

General

+
+
+
+ + +

+ Used for timestamps in logs and alerts +

+
+ +
+
+

Dark Mode

+

+ Use dark theme (recommended for monitoring dashboards) +

+
+ +
+
+
+ + {/* Admin */} + +

Admin

+
+
+ + + + +
+ +
+
+ + {/* Save All */} +
+ + +
+
+
+ ); +}; + +export default Settings; diff --git a/tailwind.config.ts b/tailwind.config.ts index a1edb69..8f9ac32 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -31,6 +31,14 @@ export default { DEFAULT: "hsl(var(--destructive))", foreground: "hsl(var(--destructive-foreground))", }, + success: { + DEFAULT: "hsl(var(--success))", + foreground: "hsl(var(--success-foreground))", + }, + warning: { + DEFAULT: "hsl(var(--warning))", + foreground: "hsl(var(--warning-foreground))", + }, muted: { DEFAULT: "hsl(var(--muted))", foreground: "hsl(var(--muted-foreground))", @@ -47,16 +55,6 @@ export default { DEFAULT: "hsl(var(--card))", foreground: "hsl(var(--card-foreground))", }, - sidebar: { - DEFAULT: "hsl(var(--sidebar-background))", - foreground: "hsl(var(--sidebar-foreground))", - primary: "hsl(var(--sidebar-primary))", - "primary-foreground": "hsl(var(--sidebar-primary-foreground))", - accent: "hsl(var(--sidebar-accent))", - "accent-foreground": "hsl(var(--sidebar-accent-foreground))", - border: "hsl(var(--sidebar-border))", - ring: "hsl(var(--sidebar-ring))", - }, }, borderRadius: { lg: "var(--radius)",