Add Docker WebUI MVP

This commit is contained in:
gpt-engineer-app[bot]
2025-10-20 09:58:18 +00:00
parent bef26c56cf
commit 2972a83b2e
8 changed files with 788 additions and 73 deletions

View File

@@ -3,12 +3,12 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>docker-watch-desk</title> <title>Docker WebUI - Container Monitoring & Control</title>
<meta name="description" content="Lovable Generated Project" /> <meta name="description" content="Lightweight self-hosted monitoring and control panel for Docker environments with live metrics, log streaming, and Discord alerts" />
<meta name="author" content="Lovable" /> <meta name="author" content="Docker WebUI" />
<meta property="og:title" content="docker-watch-desk" /> <meta property="og:title" content="Docker WebUI - Container Monitoring & Control" />
<meta property="og:description" content="Lovable Generated Project" /> <meta property="og:description" content="Lightweight self-hosted monitoring and control panel for Docker environments" />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:image" content="https://lovable.dev/opengraph-image-p98pqg.png" /> <meta property="og:image" content="https://lovable.dev/opengraph-image-p98pqg.png" />

View File

@@ -3,7 +3,10 @@ import { Toaster as Sonner } from "@/components/ui/sonner";
import { TooltipProvider } from "@/components/ui/tooltip"; import { TooltipProvider } from "@/components/ui/tooltip";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { BrowserRouter, Routes, Route } from "react-router-dom"; 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"; import NotFound from "./pages/NotFound";
const queryClient = new QueryClient(); const queryClient = new QueryClient();
@@ -15,7 +18,10 @@ const App = () => (
<Sonner /> <Sonner />
<BrowserRouter> <BrowserRouter>
<Routes> <Routes>
<Route path="/" element={<Index />} /> <Route path="/" element={<Login />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/container/:id" element={<ContainerDetails />} />
<Route path="/settings" element={<Settings />} />
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
<Route path="*" element={<NotFound />} /> <Route path="*" element={<NotFound />} />
</Routes> </Routes>

View File

@@ -8,89 +8,77 @@ All colors MUST be HSL.
@layer base { @layer base {
:root { :root {
--background: 0 0% 100%; --background: 210 20% 98%;
--foreground: 222.2 84% 4.9%; --foreground: 215 25% 15%;
--card: 0 0% 100%; --card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%; --card-foreground: 215 25% 15%;
--popover: 0 0% 100%; --popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%; --popover-foreground: 215 25% 15%;
--primary: 222.2 47.4% 11.2%; --primary: 210 100% 50%;
--primary-foreground: 210 40% 98%; --primary-foreground: 0 0% 100%;
--secondary: 210 40% 96.1%; --secondary: 210 15% 92%;
--secondary-foreground: 222.2 47.4% 11.2%; --secondary-foreground: 215 25% 15%;
--muted: 210 40% 96.1%; --muted: 210 15% 95%;
--muted-foreground: 215.4 16.3% 46.9%; --muted-foreground: 215 15% 45%;
--accent: 210 40% 96.1%; --accent: 188 95% 50%;
--accent-foreground: 222.2 47.4% 11.2%; --accent-foreground: 0 0% 100%;
--destructive: 0 84.2% 60.2%; --destructive: 0 85% 60%;
--destructive-foreground: 210 40% 98%; --destructive-foreground: 0 0% 100%;
--border: 214.3 31.8% 91.4%; --success: 142 76% 45%;
--input: 214.3 31.8% 91.4%; --success-foreground: 0 0% 100%;
--ring: 222.2 84% 4.9%;
--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%; --radius: 0.75rem;
--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%;
} }
.dark { .dark {
--background: 222.2 84% 4.9%; --background: 222 47% 11%;
--foreground: 210 40% 98%; --foreground: 210 40% 98%;
--card: 222.2 84% 4.9%; --card: 217 33% 17%;
--card-foreground: 210 40% 98%; --card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%; --popover: 217 33% 17%;
--popover-foreground: 210 40% 98%; --popover-foreground: 210 40% 98%;
--primary: 210 40% 98%; --primary: 210 100% 60%;
--primary-foreground: 222.2 47.4% 11.2%; --primary-foreground: 0 0% 100%;
--secondary: 217.2 32.6% 17.5%; --secondary: 217 33% 22%;
--secondary-foreground: 210 40% 98%; --secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%; --muted: 217 33% 22%;
--muted-foreground: 215 20.2% 65.1%; --muted-foreground: 215 20% 65%;
--accent: 217.2 32.6% 17.5%; --accent: 188 95% 50%;
--accent-foreground: 210 40% 98%; --accent-foreground: 0 0% 100%;
--destructive: 0 62.8% 30.6%; --destructive: 0 85% 60%;
--destructive-foreground: 210 40% 98%; --destructive-foreground: 0 0% 100%;
--border: 217.2 32.6% 17.5%; --success: 142 76% 45%;
--input: 217.2 32.6% 17.5%; --success-foreground: 0 0% 100%;
--ring: 212.7 26.8% 83.9%;
--sidebar-background: 240 5.9% 10%; --warning: 38 92% 50%;
--sidebar-foreground: 240 4.8% 95.9%; --warning-foreground: 0 0% 100%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%; --border: 217 33% 25%;
--sidebar-accent: 240 3.7% 15.9%; --input: 217 33% 25%;
--sidebar-accent-foreground: 240 4.8% 95.9%; --ring: 210 100% 60%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
} }
} }
@@ -100,6 +88,11 @@ All colors MUST be HSL.
} }
body { 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;
} }
} }

View File

@@ -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 (
<div className="min-h-screen bg-background dark">
{/* Header */}
<header className="border-b border-border bg-card sticky top-0 z-10">
<div className="container mx-auto px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Button variant="outline" size="sm" onClick={() => navigate("/dashboard")}>
<ArrowLeft className="w-4 h-4 mr-2" />
Back
</Button>
<div>
<div className="flex items-center gap-3">
<h1 className="text-2xl font-bold text-foreground">redis-prod</h1>
<Badge variant="default" className="bg-success hover:bg-success">
Running
</Badge>
</div>
<p className="text-sm text-muted-foreground font-mono mt-1">abc123def456</p>
</div>
</div>
<div className="flex gap-2">
<Button variant="outline" size="sm">
<Square className="w-4 h-4 mr-2" />
Stop
</Button>
<Button variant="outline" size="sm">
<RotateCw className="w-4 h-4 mr-2" />
Restart
</Button>
</div>
</div>
</div>
</header>
<div className="container mx-auto px-6 py-8">
<Tabs defaultValue="overview" className="space-y-6">
<TabsList className="bg-card border border-border">
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="metrics">Metrics</TabsTrigger>
<TabsTrigger value="logs">Logs</TabsTrigger>
<TabsTrigger value="settings">Settings</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="space-y-6">
{/* Container Info */}
<Card className="p-6 bg-card border-border">
<h3 className="text-lg font-semibold text-foreground mb-4">Container Information</h3>
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm text-muted-foreground mb-1">Image</p>
<p className="font-mono text-foreground">redis:7.2-alpine</p>
</div>
<div>
<p className="text-sm text-muted-foreground mb-1">Status</p>
<p className="text-foreground">Running (3d 14h)</p>
</div>
<div>
<p className="text-sm text-muted-foreground mb-1">IP Address</p>
<p className="font-mono text-foreground">172.18.0.5</p>
</div>
<div>
<p className="text-sm text-muted-foreground mb-1">Ports</p>
<p className="font-mono text-foreground">6379:6379/tcp</p>
</div>
<div>
<p className="text-sm text-muted-foreground mb-1">Network</p>
<p className="font-mono text-foreground">bridge</p>
</div>
<div>
<p className="text-sm text-muted-foreground mb-1">Restart Policy</p>
<p className="text-foreground">unless-stopped</p>
</div>
</div>
</Card>
{/* Mounts */}
<Card className="p-6 bg-card border-border">
<h3 className="text-lg font-semibold text-foreground mb-4">Volume Mounts</h3>
<div className="space-y-3">
<div className="flex items-center justify-between p-3 rounded-lg bg-secondary/50">
<div>
<p className="font-mono text-sm text-foreground">/var/lib/docker/volumes/redis_data</p>
<p className="text-xs text-muted-foreground mt-1"> /data</p>
</div>
<Badge variant="outline">Read/Write</Badge>
</div>
</div>
</Card>
</TabsContent>
<TabsContent value="metrics" className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<Card className="p-6 bg-card border-border">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<Activity className="w-5 h-5 text-primary" />
<h3 className="font-semibold text-card-foreground">CPU</h3>
</div>
<span className="text-2xl font-bold text-primary">2.5%</span>
</div>
<p className="text-xs text-muted-foreground">Real-time usage</p>
</Card>
<Card className="p-6 bg-card border-border">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<Database className="w-5 h-5 text-accent" />
<h3 className="font-semibold text-card-foreground">Memory</h3>
</div>
<span className="text-2xl font-bold text-accent">128 MB</span>
</div>
<p className="text-xs text-muted-foreground">of 512 MB limit</p>
</Card>
<Card className="p-6 bg-card border-border">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<Network className="w-5 h-5 text-success" />
<h3 className="font-semibold text-card-foreground">Network</h3>
</div>
<span className="text-2xl font-bold text-success">1.2 MB/s</span>
</div>
<p className="text-xs text-muted-foreground"> 0.8 MB/s 0.4 MB/s</p>
</Card>
<Card className="p-6 bg-card border-border">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<HardDrive className="w-5 h-5 text-warning" />
<h3 className="font-semibold text-card-foreground">Block I/O</h3>
</div>
<span className="text-2xl font-bold text-warning">45 KB/s</span>
</div>
<p className="text-xs text-muted-foreground">Read + Write</p>
</Card>
</div>
<Card className="p-6 bg-card border-border">
<h3 className="text-lg font-semibold text-foreground mb-4">Resource Usage Over Time</h3>
<div className="h-64 flex items-center justify-center text-muted-foreground">
Live metrics chart (real-time WebSocket updates)
</div>
</Card>
</TabsContent>
<TabsContent value="logs" className="space-y-4">
<Card className="p-6 bg-card border-border">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-foreground">Live Log Stream</h3>
<div className="flex gap-2">
<Input
placeholder="Filter logs (regex)..."
className="w-64 bg-secondary border-border"
/>
<Button variant="outline" size="sm">
Pause
</Button>
</div>
</div>
<div className="bg-background rounded-lg p-4 font-mono text-sm space-y-1 max-h-[600px] overflow-y-auto">
{logLines.map((log, i) => (
<div key={i} className="flex gap-4 hover:bg-secondary/50 px-2 py-1 rounded">
<span className="text-muted-foreground">{log.time}</span>
<span
className={
log.level === "ERROR"
? "text-destructive font-bold"
: log.level === "WARN"
? "text-warning font-bold"
: "text-success"
}
>
[{log.level}]
</span>
<span className="text-foreground">{log.message}</span>
</div>
))}
</div>
<p className="text-xs text-muted-foreground mt-4">
Logs are streamed in real-time and not stored. Filtering happens client-side.
</p>
</Card>
</TabsContent>
<TabsContent value="settings" className="space-y-6">
<Card className="p-6 bg-card border-border">
<h3 className="text-lg font-semibold text-foreground mb-6">Alert Configuration</h3>
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<p className="font-medium text-foreground">Enable Alerts</p>
<p className="text-sm text-muted-foreground">
Send Discord notifications for this container
</p>
</div>
<Switch defaultChecked />
</div>
<div className="flex items-center justify-between">
<div>
<p className="font-medium text-foreground">Alert on Stop</p>
<p className="text-sm text-muted-foreground">
Notify when container stops or crashes
</p>
</div>
<Switch defaultChecked />
</div>
<div className="space-y-2">
<label className="font-medium text-foreground">Error Pattern (Regex)</label>
<Input
defaultValue="(?i)(error|err|exception|traceback|crit(ical)?)"
className="font-mono bg-secondary border-border"
/>
<p className="text-xs text-muted-foreground">
Logs matching this pattern will trigger an alert
</p>
</div>
<div className="space-y-2">
<label className="font-medium text-foreground">Debounce Interval (seconds)</label>
<Input
type="number"
defaultValue="30"
className="bg-secondary border-border"
/>
<p className="text-xs text-muted-foreground">
Minimum time between alerts to prevent spam
</p>
</div>
<div className="space-y-2">
<label className="font-medium text-foreground">Custom Webhook URL (Optional)</label>
<Input
placeholder="https://discord.com/api/webhooks/..."
className="font-mono bg-secondary border-border"
/>
<p className="text-xs text-muted-foreground">
Leave empty to use global webhook
</p>
</div>
<Button className="w-full bg-primary hover:bg-primary/90">
Save Settings
</Button>
</div>
</Card>
</TabsContent>
</Tabs>
</div>
</div>
);
};
export default ContainerDetails;

184
src/pages/Dashboard.tsx Normal file
View File

@@ -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 (
<div className="min-h-screen bg-background dark">
{/* Header */}
<header className="border-b border-border bg-card">
<div className="container mx-auto px-6 py-4 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center">
<Server className="w-6 h-6 text-primary" />
</div>
<div>
<h1 className="text-xl font-bold text-foreground">Docker WebUI</h1>
<p className="text-sm text-muted-foreground">docker-node-01</p>
</div>
</div>
<Button variant="outline" size="sm" onClick={() => window.location.href = '/settings'}>
Settings
</Button>
</div>
</header>
<div className="container mx-auto px-6 py-8">
{/* System Metrics */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
<Card className="p-6 bg-card border-border">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<Activity className="w-5 h-5 text-primary" />
<h3 className="font-semibold text-card-foreground">CPU</h3>
</div>
<span className="text-2xl font-bold text-primary">24%</span>
</div>
<div className="h-2 bg-secondary rounded-full overflow-hidden">
<div className="h-full bg-primary rounded-full" style={{ width: "24%" }} />
</div>
<p className="text-xs text-muted-foreground mt-2">4 cores @ 3.2 GHz</p>
</Card>
<Card className="p-6 bg-card border-border">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<Server className="w-5 h-5 text-accent" />
<h3 className="font-semibold text-card-foreground">Memory</h3>
</div>
<span className="text-2xl font-bold text-accent">8.2 GB</span>
</div>
<div className="h-2 bg-secondary rounded-full overflow-hidden">
<div className="h-full bg-accent rounded-full" style={{ width: "51%" }} />
</div>
<p className="text-xs text-muted-foreground mt-2">51% of 16 GB</p>
</Card>
<Card className="p-6 bg-card border-border">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<HardDrive className="w-5 h-5 text-warning" />
<h3 className="font-semibold text-card-foreground">Disk</h3>
</div>
<span className="text-2xl font-bold text-warning">124 GB</span>
</div>
<div className="h-2 bg-secondary rounded-full overflow-hidden">
<div className="h-full bg-warning rounded-full" style={{ width: "62%" }} />
</div>
<p className="text-xs text-muted-foreground mt-2">62% of 200 GB</p>
</Card>
<Card className="p-6 bg-card border-border">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<Network className="w-5 h-5 text-success" />
<h3 className="font-semibold text-card-foreground">Network</h3>
</div>
<span className="text-2xl font-bold text-success">2.4 MB/s</span>
</div>
<div className="flex gap-2 text-xs text-muted-foreground">
<span> 1.8 MB/s</span>
<span></span>
<span> 0.6 MB/s</span>
</div>
</Card>
</div>
{/* Container Stats */}
<div className="flex items-center justify-between mb-4">
<h2 className="text-2xl font-bold text-foreground">Containers</h2>
<div className="flex gap-4 text-sm">
<span className="text-success flex items-center gap-1">
<div className="w-2 h-2 rounded-full bg-success" />
3 Running
</span>
<span className="text-destructive flex items-center gap-1">
<div className="w-2 h-2 rounded-full bg-destructive" />
1 Stopped
</span>
</div>
</div>
{/* Container List */}
<Card className="bg-card border-border">
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-border">
<th className="px-6 py-4 text-left text-sm font-semibold text-muted-foreground">Status</th>
<th className="px-6 py-4 text-left text-sm font-semibold text-muted-foreground">Name</th>
<th className="px-6 py-4 text-left text-sm font-semibold text-muted-foreground">ID</th>
<th className="px-6 py-4 text-left text-sm font-semibold text-muted-foreground">CPU</th>
<th className="px-6 py-4 text-left text-sm font-semibold text-muted-foreground">Memory</th>
<th className="px-6 py-4 text-left text-sm font-semibold text-muted-foreground">Uptime</th>
<th className="px-6 py-4 text-right text-sm font-semibold text-muted-foreground">Actions</th>
</tr>
</thead>
<tbody>
{containers.map((container) => (
<tr key={container.id} className="border-b border-border hover:bg-secondary/50 transition-colors">
<td className="px-6 py-4">
<Badge variant={container.status === "running" ? "default" : "destructive"} className={container.status === "running" ? "bg-success hover:bg-success" : ""}>
{container.status}
</Badge>
</td>
<td className="px-6 py-4 font-medium text-foreground">{container.name}</td>
<td className="px-6 py-4 font-mono text-sm text-muted-foreground">{container.id}</td>
<td className="px-6 py-4 text-sm text-foreground">{container.cpu}</td>
<td className="px-6 py-4 text-sm text-foreground">{container.memory}</td>
<td className="px-6 py-4 text-sm text-muted-foreground">{container.uptime}</td>
<td className="px-6 py-4">
<div className="flex items-center justify-end gap-2">
{container.status === "running" ? (
<>
<Button size="sm" variant="outline" className="h-8 w-8 p-0">
<Square className="w-4 h-4" />
</Button>
<Button size="sm" variant="outline" className="h-8 w-8 p-0">
<RotateCw className="w-4 h-4" />
</Button>
</>
) : (
<Button size="sm" variant="outline" className="h-8 w-8 p-0">
<Play className="w-4 h-4" />
</Button>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</Card>
{/* Active Alerts */}
<Card className="mt-8 p-6 bg-card border-border">
<div className="flex items-center gap-2 mb-4">
<AlertTriangle className="w-5 h-5 text-warning" />
<h3 className="text-lg font-semibold text-foreground">Recent Alerts</h3>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between p-3 rounded-lg bg-destructive/10 border border-destructive/20">
<div>
<p className="font-medium text-foreground">api-backend stopped</p>
<p className="text-sm text-muted-foreground">2 minutes ago</p>
</div>
<Badge variant="destructive">Container Stopped</Badge>
</div>
</div>
</Card>
</div>
</div>
);
};
export default Dashboard;

80
src/pages/Login.tsx Normal file
View File

@@ -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 (
<div className="min-h-screen bg-background dark flex items-center justify-center p-4">
<Card className="w-full max-w-md p-8 bg-card border-border">
<div className="flex flex-col items-center mb-8">
<div className="w-16 h-16 rounded-xl bg-primary/10 flex items-center justify-center mb-4">
<Server className="w-10 h-10 text-primary" />
</div>
<h1 className="text-3xl font-bold text-foreground mb-2">Docker WebUI</h1>
<p className="text-muted-foreground text-center">
Lightweight container monitoring and control
</p>
</div>
<form onSubmit={handleLogin} className="space-y-4">
<div className="space-y-2">
<label htmlFor="username" className="text-sm font-medium text-foreground">
Username
</label>
<div className="relative">
<User className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
id="username"
type="text"
placeholder="admin"
value={username}
onChange={(e) => setUsername(e.target.value)}
className="pl-10 bg-secondary border-border"
/>
</div>
</div>
<div className="space-y-2">
<label htmlFor="password" className="text-sm font-medium text-foreground">
Password
</label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
id="password"
type="password"
placeholder="••••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="pl-10 bg-secondary border-border"
/>
</div>
</div>
<Button type="submit" className="w-full bg-primary hover:bg-primary/90">
Sign In
</Button>
</form>
<p className="mt-6 text-xs text-center text-muted-foreground">
First time? You'll be prompted to change your password after login.
</p>
</Card>
</div>
);
};
export default Login;

178
src/pages/Settings.tsx Normal file
View File

@@ -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 (
<div className="min-h-screen bg-background dark">
{/* Header */}
<header className="border-b border-border bg-card sticky top-0 z-10">
<div className="container mx-auto px-6 py-4">
<div className="flex items-center gap-4">
<Button variant="outline" size="sm" onClick={() => navigate("/dashboard")}>
<ArrowLeft className="w-4 h-4 mr-2" />
Back
</Button>
<h1 className="text-2xl font-bold text-foreground">Settings</h1>
</div>
</div>
</header>
<div className="container mx-auto px-6 py-8 max-w-4xl space-y-6">
{/* Discord Webhooks */}
<Card className="p-6 bg-card border-border">
<div className="flex items-center gap-2 mb-6">
<Bell className="w-5 h-5 text-primary" />
<h3 className="text-lg font-semibold text-foreground">Discord Webhooks</h3>
</div>
<div className="space-y-4">
<div className="space-y-2">
<label className="font-medium text-foreground">Global Webhook URL</label>
<Input
placeholder="https://discord.com/api/webhooks/..."
className="font-mono bg-secondary border-border"
/>
<p className="text-xs text-muted-foreground">
Default webhook for all containers without a custom webhook
</p>
</div>
<Button variant="outline" className="w-full">
Test Webhook
</Button>
</div>
</Card>
{/* Metrics & Monitoring */}
<Card className="p-6 bg-card border-border">
<div className="flex items-center gap-2 mb-6">
<Clock className="w-5 h-5 text-accent" />
<h3 className="text-lg font-semibold text-foreground">Metrics & Monitoring</h3>
</div>
<div className="space-y-6">
<div className="space-y-2">
<label className="font-medium text-foreground">Scrape Interval (ms)</label>
<Input
type="number"
defaultValue="2000"
className="bg-secondary border-border"
/>
<p className="text-xs text-muted-foreground">
How often to collect metrics from Docker (lower = more resource usage)
</p>
</div>
<div className="space-y-2">
<label className="font-medium text-foreground">Retention Points</label>
<Input
type="number"
defaultValue="120"
className="bg-secondary border-border"
/>
<p className="text-xs text-muted-foreground">
Number of data points to keep in memory (for charts)
</p>
</div>
<div className="space-y-2">
<label className="font-medium text-foreground">Default Error Pattern</label>
<Input
defaultValue="(?i)(error|err|exception|traceback|crit(ical)?)"
className="font-mono bg-secondary border-border"
/>
<p className="text-xs text-muted-foreground">
Default regex pattern for detecting errors in logs
</p>
</div>
<div className="space-y-2">
<label className="font-medium text-foreground">Default Debounce (seconds)</label>
<Input
type="number"
defaultValue="30"
className="bg-secondary border-border"
/>
<p className="text-xs text-muted-foreground">
Default time between alerts to prevent spam
</p>
</div>
</div>
</Card>
{/* General Settings */}
<Card className="p-6 bg-card border-border">
<div className="flex items-center gap-2 mb-6">
<Globe className="w-5 h-5 text-success" />
<h3 className="text-lg font-semibold text-foreground">General</h3>
</div>
<div className="space-y-6">
<div className="space-y-2">
<label className="font-medium text-foreground">Timezone</label>
<Input
defaultValue="Europe/Copenhagen"
className="bg-secondary border-border"
/>
<p className="text-xs text-muted-foreground">
Used for timestamps in logs and alerts
</p>
</div>
<div className="flex items-center justify-between">
<div>
<p className="font-medium text-foreground">Dark Mode</p>
<p className="text-sm text-muted-foreground">
Use dark theme (recommended for monitoring dashboards)
</p>
</div>
<Switch defaultChecked />
</div>
</div>
</Card>
{/* Admin */}
<Card className="p-6 bg-card border-border">
<h3 className="text-lg font-semibold text-foreground mb-6">Admin</h3>
<div className="space-y-4">
<div className="space-y-2">
<label className="font-medium text-foreground">Change Password</label>
<Input
type="password"
placeholder="Current password"
className="bg-secondary border-border"
/>
<Input
type="password"
placeholder="New password"
className="bg-secondary border-border"
/>
<Input
type="password"
placeholder="Confirm new password"
className="bg-secondary border-border"
/>
</div>
<Button className="w-full bg-primary hover:bg-primary/90">
Update Password
</Button>
</div>
</Card>
{/* Save All */}
<div className="flex gap-4">
<Button variant="outline" className="flex-1" onClick={() => navigate("/dashboard")}>
Cancel
</Button>
<Button className="flex-1 bg-primary hover:bg-primary/90">
Save All Settings
</Button>
</div>
</div>
</div>
);
};
export default Settings;

View File

@@ -31,6 +31,14 @@ export default {
DEFAULT: "hsl(var(--destructive))", DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))", 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: { muted: {
DEFAULT: "hsl(var(--muted))", DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))", foreground: "hsl(var(--muted-foreground))",
@@ -47,16 +55,6 @@ export default {
DEFAULT: "hsl(var(--card))", DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))", 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: { borderRadius: {
lg: "var(--radius)", lg: "var(--radius)",