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.
This commit is contained in:
2026-01-08 22:31:44 +00:00
committed by GitHub
parent bb391d16aa
commit aa1bb99cd7
11 changed files with 830 additions and 3 deletions

136
PRD.md Normal file
View File

@@ -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

View File

@@ -4,9 +4,10 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title></title>
<title>Container Shell Manager</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&family=Space+Grotesk:wght@400;500;600&display=swap" rel="stylesheet">
<link href="/src/main.css" rel="stylesheet" />
</head>

View File

@@ -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 ? <Dashboard /> : <LoginForm />
}
function App() {
return <div></div>
return (
<AuthProvider>
<AppContent />
<Toaster position="top-right" />
</AuthProvider>
)
}
export default App

View File

@@ -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 (
<Card
className={cn(
"p-6 border-l-4 transition-all duration-150 hover:-translate-y-0.5 hover:shadow-lg hover:shadow-accent/10",
statusColors[container.status]
)}
>
<div className="space-y-4">
<div className="flex items-start justify-between gap-4">
<div className="flex items-start gap-3 min-w-0 flex-1">
<div className="w-10 h-10 bg-accent/10 rounded flex items-center justify-center flex-shrink-0 mt-0.5">
<Package className="text-accent" size={20} weight="duotone" />
</div>
<div className="min-w-0 flex-1">
<h3 className="font-mono font-medium text-lg truncate">
{container.name}
</h3>
<p className="text-sm text-muted-foreground truncate">
{container.image}
</p>
</div>
</div>
<Badge
variant={container.status === 'running' ? 'default' : 'secondary'}
className={cn(
"flex items-center gap-1.5 flex-shrink-0",
container.status === 'running' && "bg-accent/20 text-accent border-accent/30"
)}
>
{container.status === 'running' && (
<Play size={12} weight="fill" className="animate-pulse-glow" />
)}
{container.status}
</Badge>
</div>
<Separator />
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<div className="text-xs text-muted-foreground uppercase tracking-wider mb-1">
Container ID
</div>
<div className="font-mono text-foreground">
{container.id}
</div>
</div>
<div>
<div className="text-xs text-muted-foreground uppercase tracking-wider mb-1">
Uptime
</div>
<div className="font-mono text-foreground">
{container.uptime}
</div>
</div>
</div>
<Button
onClick={onOpenShell}
disabled={container.status !== 'running'}
className="w-full bg-primary hover:bg-accent hover:text-accent-foreground transition-colors font-medium"
>
<Terminal size={18} weight="bold" />
Open Shell
</Button>
</div>
</Card>
)
}

View File

@@ -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<Container[]>([])
const [selectedContainer, setSelectedContainer] = useState<Container | null>(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 (
<div className="min-h-screen bg-background">
<header className="border-b border-border bg-card/50 backdrop-blur-sm sticky top-0 z-10">
<div className="container mx-auto px-4 sm:px-6 py-4">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-accent/10 rounded-lg flex items-center justify-center">
<Package className="text-accent" size={24} weight="duotone" />
</div>
<div>
<h1 className="font-mono text-2xl font-bold tracking-tight">
Container Shell
</h1>
<p className="text-sm text-muted-foreground">
{containers.length} active {containers.length === 1 ? 'container' : 'containers'}
</p>
</div>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={handleRefresh}
disabled={isRefreshing}
className="font-medium"
>
<ArrowClockwise
size={16}
className={isRefreshing ? 'animate-spin' : ''}
/>
Refresh
</Button>
<Button
variant="outline"
size="sm"
onClick={handleLogout}
className="font-medium"
>
<SignOut size={16} />
Logout
</Button>
</div>
</div>
</div>
</header>
<main className="container mx-auto px-4 sm:px-6 py-6">
{containers.length === 0 ? (
<div className="flex flex-col items-center justify-center min-h-[400px] text-center">
<div className="w-20 h-20 bg-muted rounded-lg flex items-center justify-center mb-4">
<Package className="text-muted-foreground" size={40} weight="duotone" />
</div>
<h2 className="text-xl font-semibold mb-2">No Active Containers</h2>
<p className="text-muted-foreground max-w-md">
There are currently no running containers to display. Start a container to see it appear here.
</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{containers.map((container) => (
<ContainerCard
key={container.id}
container={container}
onOpenShell={() => handleOpenShell(container)}
/>
))}
</div>
)}
</main>
{selectedContainer && (
<TerminalModal
isOpen={isTerminalOpen}
onClose={handleCloseTerminal}
containerName={selectedContainer.name}
containerId={selectedContainer.id}
/>
)}
</div>
)
}

View File

@@ -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 (
<div className="min-h-screen flex items-center justify-center p-4 bg-gradient-to-br from-background via-background to-primary/20">
<Card
className={`w-full max-w-sm p-6 space-y-6 ${isShaking ? 'animate-shake' : ''}`}
>
<div className="flex flex-col items-center space-y-2">
<div className="w-16 h-16 bg-accent/10 rounded-lg flex items-center justify-center mb-2">
<LockKey className="text-accent" size={32} weight="duotone" />
</div>
<h1 className="font-mono text-2xl font-bold tracking-tight">Container Shell</h1>
<p className="text-sm text-muted-foreground text-center">
Enter your credentials to access container management
</p>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="username" className="text-xs uppercase tracking-wider">
Username
</Label>
<Input
id="username"
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="Enter username"
required
autoComplete="username"
className="font-mono"
/>
</div>
<div className="space-y-2">
<Label htmlFor="password" className="text-xs uppercase tracking-wider">
Password
</Label>
<Input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Enter password"
required
autoComplete="current-password"
className="font-mono"
/>
</div>
<Button
type="submit"
className="w-full bg-accent text-accent-foreground hover:bg-accent/90 font-medium"
>
Access Dashboard
</Button>
</form>
<div className="text-xs text-center text-muted-foreground border-t border-border pt-4">
Default: admin / admin123
</div>
</Card>
</div>
)
}

View File

@@ -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<Array<{ type: 'command' | 'output'; text: string }>>([
{ 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<HTMLDivElement>(null)
const inputRef = useRef<HTMLInputElement>(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<HTMLInputElement>) => {
if (e.key === 'Enter') {
executeCommand(command)
}
}
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-4xl h-[600px] p-0 gap-0 bg-card">
<DialogHeader className="px-6 py-4 border-b border-border flex flex-row items-center justify-between space-y-0">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-accent/10 rounded flex items-center justify-center">
<TerminalIcon className="text-accent" size={20} weight="duotone" />
</div>
<div>
<DialogTitle className="font-mono text-lg">
{containerName}
</DialogTitle>
<p className="text-xs text-muted-foreground font-mono mt-0.5">
{containerId}
</p>
</div>
</div>
<Button
variant="ghost"
size="icon"
onClick={onClose}
className="h-8 w-8"
>
<X size={18} />
</Button>
</DialogHeader>
<div className="flex-1 flex flex-col min-h-0">
<ScrollArea className="flex-1 p-6" ref={scrollRef}>
<div className="font-mono text-sm space-y-1">
{history.map((entry, index) => (
<div
key={index}
className={
entry.type === 'command'
? 'text-accent font-medium'
: 'text-foreground'
}
>
{entry.text}
</div>
))}
</div>
</ScrollArea>
<div className="px-6 py-4 border-t border-border bg-primary/5">
<div className="flex items-center gap-2">
<span className="text-accent font-mono font-medium">$</span>
<Input
ref={inputRef}
value={command}
onChange={(e) => setCommand(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Type a command..."
className="flex-1 font-mono bg-background border-input"
/>
</div>
</div>
</div>
</DialogContent>
</Dialog>
)
}

View File

@@ -1 +1,109 @@
/* This is where custom CSS goes */
@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;
}

47
src/lib/auth.tsx Normal file
View File

@@ -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<AuthContextType | undefined>(undefined)
const DEFAULT_USERNAME = 'admin'
const DEFAULT_PASSWORD = 'admin123'
export function AuthProvider({ children }: { children: ReactNode }) {
const [isAuthenticated, setIsAuthenticated] = useKV<boolean>('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 (
<AuthContext.Provider value={{ isAuthenticated: isAuthenticated ?? false, login, logout }}>
{children}
</AuthContext.Provider>
)
}
export function useAuth() {
const context = useContext(AuthContext)
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider')
}
return context
}

38
src/lib/mock-data.ts Normal file
View File

@@ -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'
}
]
}

19
src/lib/types.ts Normal file
View File

@@ -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
}