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
+
+
+
+
+
+
+ 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 (
+
+ )
+}
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
+}