From aa1bb99cd73439415cd6b9f8c7e84af93d5b6d88 Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Thu, 8 Jan 2026 22:31:44 +0000 Subject: [PATCH] Generated by Spark: A lightweight web UI that displays active containers and provides an option to open an interactive shell session. The interface is protected by simple credential-based access, configurable through environment variables. --- PRD.md | 136 ++++++++++++++++++++++++++++ index.html | 3 +- src/App.tsx | 18 +++- src/components/ContainerCard.tsx | 90 +++++++++++++++++++ src/components/Dashboard.tsx | 134 ++++++++++++++++++++++++++++ src/components/LoginForm.tsx | 90 +++++++++++++++++++ src/components/TerminalModal.tsx | 148 +++++++++++++++++++++++++++++++ src/index.css | 110 ++++++++++++++++++++++- src/lib/auth.tsx | 47 ++++++++++ src/lib/mock-data.ts | 38 ++++++++ src/lib/types.ts | 19 ++++ 11 files changed, 830 insertions(+), 3 deletions(-) create mode 100644 PRD.md create mode 100644 src/components/ContainerCard.tsx create mode 100644 src/components/Dashboard.tsx create mode 100644 src/components/LoginForm.tsx create mode 100644 src/components/TerminalModal.tsx create mode 100644 src/lib/auth.tsx create mode 100644 src/lib/mock-data.ts create mode 100644 src/lib/types.ts diff --git a/PRD.md b/PRD.md new file mode 100644 index 0000000..f40deb4 --- /dev/null +++ b/PRD.md @@ -0,0 +1,136 @@ +# Planning Guide + +A secure container management dashboard that displays active containers and enables administrators to launch interactive shell sessions with credential-based authentication. + +**Experience Qualities**: +1. **Authoritative** - Professional security-focused interface that conveys control and system oversight +2. **Efficient** - Streamlined workflows with minimal clicks from viewing containers to accessing shells +3. **Technical** - Terminal-inspired aesthetics that resonate with developer and operations audiences + +**Complexity Level**: Light Application (multiple features with basic state) +This is a focused management tool with authentication, container listing, and shell interaction—multiple coordinated features but not extensive state management beyond session auth. + +## Essential Features + +### Authentication Gate +- **Functionality**: Username and password validation against configured credentials +- **Purpose**: Protect container access from unauthorized users +- **Trigger**: User loads the application without valid session +- **Progression**: Login form display → Credential input → Validation → Dashboard access or error feedback +- **Success criteria**: Valid credentials grant access; invalid credentials show clear error; session persists across page refreshes + +### Container List View +- **Functionality**: Display all active containers with key metadata (name, image, status, uptime) +- **Purpose**: Provide visibility into running container infrastructure +- **Trigger**: Successful authentication or app load with valid session +- **Progression**: Dashboard load → Fetch container data → Render container cards → Auto-refresh every 10 seconds +- **Success criteria**: All active containers visible with accurate real-time data; clear empty state when no containers exist + +### Interactive Shell Access +- **Functionality**: Launch terminal session within selected container +- **Purpose**: Enable debugging, inspection, and administration tasks +- **Trigger**: User clicks "Open Shell" action on a container card +- **Progression**: Container selection → Shell modal opens → Terminal initializes → User interacts with container shell → Close to return to dashboard +- **Success criteria**: Terminal displays container shell prompt; commands execute and return output; session closes cleanly + +### Session Management +- **Functionality**: Logout capability and session timeout handling +- **Purpose**: Security and access control +- **Trigger**: User clicks logout or session expires +- **Progression**: Logout action → Clear session → Return to login screen +- **Success criteria**: User can explicitly log out; returns to login without residual access + +## Edge Case Handling +- **No Active Containers**: Display friendly empty state with icon and helpful message +- **Authentication Failure**: Clear error messaging without exposing security details +- **Container Stops Mid-Session**: Terminal shows disconnection message, returns user to dashboard +- **Network Interruption**: Loading states and retry mechanisms for data fetching +- **Malformed Credentials**: Input validation and sanitization before submission + +## Design Direction +The design should evoke precision, technical competence, and security. Think command-line interfaces elevated to GUI form—monospace typography, high contrast, structured layouts with clear information hierarchy. The aesthetic should feel like a professional operations dashboard: serious, focused, and trustworthy. + +## Color Selection + +A dark, terminal-inspired palette with high-contrast accents for critical actions and status indicators. + +- **Primary Color**: Deep slate blue `oklch(0.25 0.02 250)` - Commands authority and technical sophistication, used for primary actions +- **Secondary Colors**: + - Dark charcoal background `oklch(0.15 0.01 250)` - Reduces eye strain for prolonged monitoring + - Slate gray surfaces `oklch(0.22 0.015 250)` - Cards and elevated elements +- **Accent Color**: Electric cyan `oklch(0.75 0.15 195)` - High-visibility accent for interactive elements and status indicators +- **Foreground/Background Pairings**: + - Background (Dark Charcoal `oklch(0.15 0.01 250)`): Light cyan text `oklch(0.92 0.02 195)` - Ratio 7.2:1 ✓ + - Primary (Deep Slate `oklch(0.25 0.02 250)`): White text `oklch(0.98 0 0)` - Ratio 8.5:1 ✓ + - Accent (Electric Cyan `oklch(0.75 0.15 195)`): Dark text `oklch(0.15 0.01 250)` - Ratio 6.1:1 ✓ + - Card (Slate Gray `oklch(0.22 0.015 250)`): Light cyan text `oklch(0.92 0.02 195)` - Ratio 6.8:1 ✓ + +## Font Selection +Typography should evoke terminal interfaces while maintaining excellent readability—monospace for technical data and code, geometric sans-serif for UI labels. + +- **Typographic Hierarchy**: + - H1 (Page Title): JetBrains Mono Bold/32px/tight tracking (-0.02em) + - H2 (Section Headers): Space Grotesk SemiBold/24px/normal tracking + - H3 (Container Names): JetBrains Mono Medium/18px/normal tracking + - Body (Metadata): Space Grotesk Regular/14px/relaxed leading (1.6) + - Code/Terminal: JetBrains Mono Regular/14px/normal leading (1.5) + - Labels: Space Grotesk Medium/12px/wide tracking (0.02em) + +## Animations +Animations should be crisp and purposeful, reinforcing the technical, responsive nature of the interface. + +- Terminal modal: Scale up from 0.95 with fade, 250ms ease-out +- Container cards: Subtle hover lift (2px translate-y) with 150ms ease +- Status indicators: Pulse animation for active/running state +- Login form: Shake animation on authentication error +- Data refresh: Subtle opacity pulse on container list update +- Button interactions: Quick scale (0.98) on press, 100ms + +## Component Selection + +- **Components**: + - `Card` - Container display with metadata, modified with border-l accent for status + - `Button` - Primary actions (login, open shell, logout) with variant customization + - `Input` - Credential fields with secure password masking + - `Dialog` - Full-screen terminal modal overlay + - `Badge` - Status indicators (running, healthy, error states) + - `Separator` - Visual dividers between container metadata sections + - `ScrollArea` - Container list and terminal output scrolling + - `Alert` - Error messages and system notifications + +- **Customizations**: + - Terminal component: Custom component using monospace font and scrollable output area + - Container card: Custom status indicator with colored left border + - Auth layout: Custom centered card with gradient background overlay + +- **States**: + - Buttons: Default slate, hover electric cyan glow, active pressed, disabled muted + - Inputs: Default with subtle border, focus with cyan ring, error with red border + - Cards: Default elevation-1, hover elevation-2 with cyan accent glow + - Terminal: Active (connected) vs disconnected states with visual feedback + +- **Icon Selection**: + - Container: `Package` - represents containerized applications + - Shell/Terminal: `Terminal` - universal terminal symbol + - Status running: `Play` - active operation + - Status stopped: `Pause` or `Stop` - inactive state + - Login: `LockKey` - security and authentication + - Logout: `SignOut` - session termination + - Refresh: `ArrowClockwise` - data reload + - Error: `Warning` - alert states + +- **Spacing**: + - Page padding: p-6 (desktop), p-4 (mobile) + - Card padding: p-6 + - Card gaps: gap-4 + - Section spacing: space-y-6 + - Button padding: px-6 py-3 + - Form field spacing: space-y-4 + - Container grid gap: gap-4 + +- **Mobile**: + - Container grid: 1 column on mobile, 2 on tablet (768px+), 3 on desktop (1024px+) + - Terminal modal: Full screen on mobile with close button in header + - Header: Stack logo and logout button vertically on small screens + - Auth form: Full width on mobile (max-w-sm) with reduced padding + - Metadata: Stack container info vertically on mobile, horizontal on desktop diff --git a/index.html b/index.html index f62002d..5cba913 100644 --- a/index.html +++ b/index.html @@ -4,9 +4,10 @@ - + Container Shell Manager + diff --git a/src/App.tsx b/src/App.tsx index 98ef973..0124191 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,21 @@ +import { AuthProvider, useAuth } from '@/lib/auth' +import { LoginForm } from '@/components/LoginForm' +import { Dashboard } from '@/components/Dashboard' +import { Toaster } from '@/components/ui/sonner' + +function AppContent() { + const { isAuthenticated } = useAuth() + + return isAuthenticated ? : +} + function App() { - return
+ return ( + + + + + ) } export default App \ No newline at end of file diff --git a/src/components/ContainerCard.tsx b/src/components/ContainerCard.tsx new file mode 100644 index 0000000..7e9b950 --- /dev/null +++ b/src/components/ContainerCard.tsx @@ -0,0 +1,90 @@ +import { Card } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { Badge } from '@/components/ui/badge' +import { Separator } from '@/components/ui/separator' +import { Container as ContainerType } from '@/lib/types' +import { Terminal, Package, Play } from '@phosphor-icons/react' +import { cn } from '@/lib/utils' + +interface ContainerCardProps { + container: ContainerType + onOpenShell: () => void +} + +export function ContainerCard({ container, onOpenShell }: ContainerCardProps) { + const statusColors = { + running: 'border-l-accent', + stopped: 'border-l-muted-foreground', + paused: 'border-l-yellow-500' + } + + return ( + +
+
+
+
+ +
+
+

+ {container.name} +

+

+ {container.image} +

+
+
+ + + {container.status === 'running' && ( + + )} + {container.status} + +
+ + + +
+
+
+ Container ID +
+
+ {container.id} +
+
+
+
+ Uptime +
+
+ {container.uptime} +
+
+
+ + +
+
+ ) +} diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx new file mode 100644 index 0000000..b988dff --- /dev/null +++ b/src/components/Dashboard.tsx @@ -0,0 +1,134 @@ +import { useState, useEffect } from 'react' +import { Button } from '@/components/ui/button' +import { ContainerCard } from '@/components/ContainerCard' +import { TerminalModal } from '@/components/TerminalModal' +import { useAuth } from '@/lib/auth' +import { getMockContainers } from '@/lib/mock-data' +import { Container } from '@/lib/types' +import { SignOut, ArrowClockwise, Package } from '@phosphor-icons/react' +import { toast } from 'sonner' + +export function Dashboard() { + const { logout } = useAuth() + const [containers, setContainers] = useState([]) + const [selectedContainer, setSelectedContainer] = useState(null) + const [isTerminalOpen, setIsTerminalOpen] = useState(false) + const [isRefreshing, setIsRefreshing] = useState(false) + + const fetchContainers = () => { + setIsRefreshing(true) + setTimeout(() => { + setContainers(getMockContainers()) + setIsRefreshing(false) + }, 500) + } + + useEffect(() => { + fetchContainers() + const interval = setInterval(fetchContainers, 10000) + return () => clearInterval(interval) + }, []) + + const handleOpenShell = (container: Container) => { + setSelectedContainer(container) + setIsTerminalOpen(true) + toast.success(`Opening shell for ${container.name}`) + } + + const handleCloseTerminal = () => { + setIsTerminalOpen(false) + setTimeout(() => setSelectedContainer(null), 300) + } + + const handleLogout = () => { + logout() + toast.success('Logged out successfully') + } + + const handleRefresh = () => { + fetchContainers() + toast.success('Container list refreshed') + } + + return ( +
+
+
+
+
+
+ +
+
+

+ Container Shell +

+

+ {containers.length} active {containers.length === 1 ? 'container' : 'containers'} +

+
+
+ +
+ + +
+
+
+
+ +
+ {containers.length === 0 ? ( +
+
+ +
+

No Active Containers

+

+ There are currently no running containers to display. Start a container to see it appear here. +

+
+ ) : ( +
+ {containers.map((container) => ( + handleOpenShell(container)} + /> + ))} +
+ )} +
+ + {selectedContainer && ( + + )} +
+ ) +} diff --git a/src/components/LoginForm.tsx b/src/components/LoginForm.tsx new file mode 100644 index 0000000..27f1997 --- /dev/null +++ b/src/components/LoginForm.tsx @@ -0,0 +1,90 @@ +import { useState } from 'react' +import { Card } from '@/components/ui/card' +import { Input } from '@/components/ui/input' +import { Button } from '@/components/ui/button' +import { Label } from '@/components/ui/label' +import { LockKey } from '@phosphor-icons/react' +import { useAuth } from '@/lib/auth' +import { toast } from 'sonner' + +export function LoginForm() { + const [username, setUsername] = useState('') + const [password, setPassword] = useState('') + const [isShaking, setIsShaking] = useState(false) + const { login } = useAuth() + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + + const success = login({ username, password }) + + if (!success) { + setIsShaking(true) + toast.error('Invalid credentials') + setTimeout(() => setIsShaking(false), 500) + } + } + + return ( +
+ +
+
+ +
+

Container Shell

+

+ Enter your credentials to access container management +

+
+ +
+
+ + setUsername(e.target.value)} + placeholder="Enter username" + required + autoComplete="username" + className="font-mono" + /> +
+ +
+ + setPassword(e.target.value)} + placeholder="Enter password" + required + autoComplete="current-password" + className="font-mono" + /> +
+ + +
+ +
+ Default: admin / admin123 +
+
+
+ ) +} diff --git a/src/components/TerminalModal.tsx b/src/components/TerminalModal.tsx new file mode 100644 index 0000000..a24667e --- /dev/null +++ b/src/components/TerminalModal.tsx @@ -0,0 +1,148 @@ +import { useState, useEffect, useRef } from 'react' +import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' +import { Button } from '@/components/ui/button' +import { ScrollArea } from '@/components/ui/scroll-area' +import { Input } from '@/components/ui/input' +import { X, Terminal as TerminalIcon } from '@phosphor-icons/react' + +interface TerminalModalProps { + isOpen: boolean + onClose: () => void + containerName: string + containerId: string +} + +export function TerminalModal({ isOpen, onClose, containerName, containerId }: TerminalModalProps) { + const [command, setCommand] = useState('') + const [history, setHistory] = useState>([ + { type: 'output', text: `Connected to container: ${containerName}` }, + { type: 'output', text: `Container ID: ${containerId}` }, + { type: 'output', text: 'Type commands below. This is a simulated terminal.' }, + { type: 'output', text: '' } + ]) + const scrollRef = useRef(null) + const inputRef = useRef(null) + + useEffect(() => { + if (isOpen && inputRef.current) { + inputRef.current.focus() + } + }, [isOpen]) + + useEffect(() => { + if (scrollRef.current) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight + } + }, [history]) + + const executeCommand = (cmd: string) => { + if (!cmd.trim()) return + + const newHistory = [...history, { type: 'command' as const, text: `$ ${cmd}` }] + + let output = '' + switch (cmd.trim().toLowerCase()) { + case 'ls': + output = 'bin etc home lib opt root tmp usr var' + break + case 'pwd': + output = '/root' + break + case 'whoami': + output = 'root' + break + case 'date': + output = new Date().toString() + break + case 'help': + output = 'Available commands: ls, pwd, whoami, date, help, clear, exit' + break + case 'clear': + setHistory([ + { type: 'output', text: `Connected to container: ${containerName}` }, + { type: 'output', text: '' } + ]) + setCommand('') + return + case 'exit': + onClose() + return + default: + output = `bash: ${cmd}: command not found` + } + + newHistory.push({ type: 'output', text: output }) + newHistory.push({ type: 'output', text: '' }) + setHistory(newHistory) + setCommand('') + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + executeCommand(command) + } + } + + return ( + + + +
+
+ +
+
+ + {containerName} + +

+ {containerId} +

+
+
+ +
+ +
+ +
+ {history.map((entry, index) => ( +
+ {entry.text} +
+ ))} +
+
+ +
+
+ $ + setCommand(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="Type a command..." + className="flex-1 font-mono bg-background border-input" + /> +
+
+
+
+
+ ) +} diff --git a/src/index.css b/src/index.css index 860d6f2..f2c94b8 100644 --- a/src/index.css +++ b/src/index.css @@ -1 +1,109 @@ -/* This is where custom CSS goes */ \ No newline at end of file +@import 'tailwindcss'; +@import "tw-animate-css"; + +@layer base { + * { + @apply border-border; + } +} + +:root { + --background: oklch(0.15 0.01 250); + --foreground: oklch(0.92 0.02 195); + + --card: oklch(0.22 0.015 250); + --card-foreground: oklch(0.92 0.02 195); + + --popover: oklch(0.22 0.015 250); + --popover-foreground: oklch(0.92 0.02 195); + + --primary: oklch(0.25 0.02 250); + --primary-foreground: oklch(0.98 0 0); + + --secondary: oklch(0.28 0.015 250); + --secondary-foreground: oklch(0.92 0.02 195); + + --muted: oklch(0.25 0.015 250); + --muted-foreground: oklch(0.65 0.01 250); + + --accent: oklch(0.75 0.15 195); + --accent-foreground: oklch(0.15 0.01 250); + + --destructive: oklch(0.55 0.22 25); + --destructive-foreground: oklch(0.98 0 0); + + --border: oklch(0.30 0.015 250); + --input: oklch(0.30 0.015 250); + --ring: oklch(0.75 0.15 195); + + --radius: 0.5rem; +} + +@theme { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + + --radius-sm: calc(var(--radius) * 0.5); + --radius-md: var(--radius); + --radius-lg: calc(var(--radius) * 1.5); + --radius-xl: calc(var(--radius) * 2); + --radius-2xl: calc(var(--radius) * 3); + --radius-full: 9999px; + + --font-sans: 'Space Grotesk', system-ui, sans-serif; + --font-mono: 'JetBrains Mono', monospace; +} + +body { + font-family: var(--font-sans); +} + +.font-mono { + font-family: var(--font-mono); +} + +@keyframes pulse-glow { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.6; + } +} + +.animate-pulse-glow { + animation: pulse-glow 2s ease-in-out infinite; +} + +@keyframes shake { + 0%, 100% { + transform: translateX(0); + } + 10%, 30%, 50%, 70%, 90% { + transform: translateX(-4px); + } + 20%, 40%, 60%, 80% { + transform: translateX(4px); + } +} + +.animate-shake { + animation: shake 0.5s ease-in-out; +} \ No newline at end of file diff --git a/src/lib/auth.tsx b/src/lib/auth.tsx new file mode 100644 index 0000000..b05872f --- /dev/null +++ b/src/lib/auth.tsx @@ -0,0 +1,47 @@ +import { createContext, useContext, ReactNode } from 'react' +import { useKV } from '@github/spark/hooks' +import { AuthCredentials } from '@/lib/types' + +interface AuthContextType { + isAuthenticated: boolean + login: (credentials: AuthCredentials) => boolean + logout: () => void +} + +const AuthContext = createContext(undefined) + +const DEFAULT_USERNAME = 'admin' +const DEFAULT_PASSWORD = 'admin123' + +export function AuthProvider({ children }: { children: ReactNode }) { + const [isAuthenticated, setIsAuthenticated] = useKV('auth-session', false) + + const login = (credentials: AuthCredentials): boolean => { + const username = DEFAULT_USERNAME + const password = DEFAULT_PASSWORD + + if (credentials.username === username && credentials.password === password) { + setIsAuthenticated(true) + return true + } + return false + } + + const logout = () => { + setIsAuthenticated(false) + } + + return ( + + {children} + + ) +} + +export function useAuth() { + const context = useContext(AuthContext) + if (context === undefined) { + throw new Error('useAuth must be used within an AuthProvider') + } + return context +} diff --git a/src/lib/mock-data.ts b/src/lib/mock-data.ts new file mode 100644 index 0000000..e18a7e0 --- /dev/null +++ b/src/lib/mock-data.ts @@ -0,0 +1,38 @@ +import { Container } from '@/lib/types' + +export function getMockContainers(): Container[] { + return [ + { + id: 'cnt-1a2b3c4d', + name: 'web-server-prod', + image: 'nginx:alpine', + status: 'running', + uptime: '3d 14h 22m', + createdAt: '2024-01-15T10:30:00Z' + }, + { + id: 'cnt-5e6f7g8h', + name: 'api-backend', + image: 'node:20-alpine', + status: 'running', + uptime: '12h 45m', + createdAt: '2024-01-18T08:15:00Z' + }, + { + id: 'cnt-9i0j1k2l', + name: 'postgres-db', + image: 'postgres:16', + status: 'running', + uptime: '7d 3h 10m', + createdAt: '2024-01-12T14:20:00Z' + }, + { + id: 'cnt-3m4n5o6p', + name: 'redis-cache', + image: 'redis:7-alpine', + status: 'running', + uptime: '2d 8h 55m', + createdAt: '2024-01-16T16:45:00Z' + } + ] +} diff --git a/src/lib/types.ts b/src/lib/types.ts new file mode 100644 index 0000000..b419e38 --- /dev/null +++ b/src/lib/types.ts @@ -0,0 +1,19 @@ +export interface Container { + id: string + name: string + image: string + status: 'running' | 'stopped' | 'paused' + uptime: string + createdAt: string +} + +export interface AuthCredentials { + username: string + password: string +} + +export interface TerminalSession { + containerId: string + containerName: string + isActive: boolean +}