feat: Add Docker Swarm terminal application

Docker Swarm management terminal for container orchestration.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-21 17:03:12 +00:00
parent 94f9cffe6f
commit fdd2a30b77
39 changed files with 9317 additions and 0 deletions

11
dockerterminal/.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,11 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: "devcontainers"
directory: "/"
schedule:
interval: "weekly"

View File

@@ -0,0 +1,80 @@
name: Build and Push to GHCR
on:
push:
branches:
- main
tags:
- 'v*'
pull_request:
branches:
- main
env:
REGISTRY: ghcr.io
IMAGE_NAME_BACKEND: ${{ github.repository }}-backend
IMAGE_NAME_FRONTEND: ${{ github.repository }}-frontend
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for backend
id: meta-backend
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_BACKEND }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=sha
- name: Build and push backend image
uses: docker/build-push-action@v5
with:
context: ./backend
file: ./backend/Dockerfile
push: true
tags: ${{ steps.meta-backend.outputs.tags }}
labels: ${{ steps.meta-backend.outputs.labels }}
- name: Extract metadata for frontend
id: meta-frontend
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_FRONTEND }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=sha
- name: Build and push frontend image
uses: docker/build-push-action@v5
with:
context: ./frontend
file: ./frontend/Dockerfile
push: true
tags: ${{ steps.meta-frontend.outputs.tags }}
labels: ${{ steps.meta-frontend.outputs.labels }}
build-args: |
NEXT_PUBLIC_API_URL=http://backend:5000

54
dockerterminal/.gitignore vendored Normal file
View File

@@ -0,0 +1,54 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*-dist
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.env
.env.local
**/agent-eval-report*
packages
pids
.file-manifest
.devcontainer/
.spark-workbench-id
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
venv/
env/
ENV/
*.egg-info/
.pytest_cache/
# Next.js
.next/
out/
build/
.vercel

21
dockerterminal/LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright GitHub, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

141
dockerterminal/MIGRATION.md Normal file
View File

@@ -0,0 +1,141 @@
# Project Restructure Summary
## Overview
Successfully migrated the Docker Swarm Container Terminal WebUI from a Vite + React + Radix UI stack to a modern Flask + Next.js + Material UI architecture.
## Changes Made
### 1. Project Organization
- ✅ Restructured project with separate `backend/` and `frontend/` directories
- ✅ Removed legacy Vite-based frontend from project root
### 2. Backend (Flask + Python)
Created a new Flask backend with the following features:
**Files Created:**
- `backend/app.py` - Main Flask application
- `backend/requirements.txt` - Python dependencies
- `backend/Dockerfile` - Container image definition
- `backend/.env.example` - Environment variable template
- `backend/README.md` - Backend documentation
**Key Features:**
- RESTful API for container management
- Direct Docker SDK integration
- Session-based authentication
- CORS enabled for frontend communication
- Health check endpoint
**API Endpoints:**
- `POST /api/auth/login` - User authentication
- `POST /api/auth/logout` - Session termination
- `GET /api/containers` - List all Docker containers
- `POST /api/containers/<id>/exec` - Execute commands in containers
- `GET /api/health` - Service health check
### 3. Frontend (Next.js + Material UI)
Created a modern Next.js application with:
**Files Created:**
- `frontend/app/page.tsx` - Login/home page
- `frontend/app/dashboard/page.tsx` - Main dashboard
- `frontend/app/layout.tsx` - Root layout with providers
- `frontend/components/LoginForm.tsx` - Authentication UI
- `frontend/components/ContainerCard.tsx` - Container display component
- `frontend/components/TerminalModal.tsx` - Interactive terminal
- `frontend/lib/api.ts` - API client with TypeScript types
- `frontend/lib/auth.tsx` - Authentication context/provider
- `frontend/lib/theme.tsx` - Material UI theme configuration
- `frontend/Dockerfile` - Container image definition
- `frontend/.env.local.example` - Environment variable template
**Key Features:**
- Next.js 14+ with App Router
- Material UI (MUI) components
- TypeScript for type safety
- Client-side authentication flow
- Real-time container status updates
- Interactive terminal for container commands
- Responsive design for mobile/desktop
### 4. Docker & GHCR Support
Created containerization and deployment infrastructure:
**Files Created:**
- `docker-compose.yml` - Multi-container orchestration
- `.github/workflows/docker-publish.yml` - Automated CI/CD
**Features:**
- Docker Compose for local development
- Automated image builds on push to main
- GitHub Container Registry (GHCR) integration
- Proper networking between services
- Volume mounting for Docker socket access
### 5. Documentation
Updated all documentation:
**Files Updated/Created:**
- `README.md` - Complete project documentation
- `backend/README.md` - Backend-specific guide
- `.gitignore` - Updated for Python and Next.js
**Documentation Includes:**
- Quick start guide
- Manual setup instructions
- API endpoint reference
- Configuration options
- Security considerations
- Deployment procedures
## Technology Stack Comparison
### Previous Stack
- **Frontend:** Vite + React + TypeScript
- **UI Library:** Radix UI + Tailwind CSS
- **Data:** Mock data (no backend)
- **Deployment:** Static site
### Current Stack
- **Backend:** Flask (Python) + Docker SDK
- **Frontend:** Next.js 14+ (React) + TypeScript
- **UI Library:** Material UI + Tailwind CSS
- **Deployment:** Dockerized with GHCR
## Benefits of New Architecture
1. **Separation of Concerns:** Backend and frontend are properly separated
2. **Real Docker Integration:** Actual Docker SDK integration vs. mock data
3. **Scalability:** Can independently scale frontend and backend
4. **Modern Stack:** Latest Next.js with App Router
5. **Production Ready:** Docker containers with CI/CD pipeline
6. **Security:** Proper authentication and API layer
7. **Maintainability:** Clear project structure and documentation
## Running the Application
### Using Docker Compose (Recommended)
```bash
docker-compose up -d
```
- Frontend: http://localhost:3000
- Backend: http://localhost:5000
### Manual Setup
1. Start backend: `cd backend && python app.py`
2. Start frontend: `cd frontend && npm run dev`
### Default Credentials
- Username: `admin`
- Password: `admin123`
## Next Steps / Future Improvements
1. Add WebSocket support for real-time terminal sessions
2. Implement JWT-based authentication
3. Add user management and role-based access control
4. Create comprehensive test suites
5. Add container logs viewing functionality
6. Implement container stats/metrics dashboard
7. Add Docker Swarm-specific features
8. Set up production-grade security measures

136
dockerterminal/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

197
dockerterminal/README.md Normal file
View File

@@ -0,0 +1,197 @@
# Docker Swarm Container Terminal WebUI
A modern, secure web interface for managing Docker containers with interactive terminal access.
## Features
- 🔐 **Secure Authentication** - Username/password authentication to protect container access
- 📦 **Container Management** - View all active containers with real-time status updates
- 💻 **Interactive Terminal** - Execute commands directly in running containers
- 🎨 **Modern UI** - Built with Material UI and Next.js for a polished experience
- 🐳 **Docker Native** - Direct integration with Docker API
- 📱 **Responsive Design** - Works seamlessly on desktop and mobile devices
## Tech Stack
### Frontend
- **Next.js 14+** - React framework with App Router
- **Material UI (MUI)** - Modern component library
- **TypeScript** - Type-safe development
- **Tailwind CSS** - Utility-first styling
### Backend
- **Flask** - Python web framework
- **Docker SDK** - Python Docker integration
- **Flask-CORS** - Cross-origin resource sharing
### Deployment
- **Docker** - Containerized deployment
- **Docker Compose** - Multi-container orchestration
- **GHCR** - GitHub Container Registry for images
## Quick Start
### Using Docker Compose (Recommended)
1. Clone the repository:
```bash
git clone https://github.com/johndoe6345789/docker-swarm-termina.git
cd docker-swarm-termina
```
2. Start the application:
```bash
docker-compose up -d
```
3. Access the application:
- Frontend: http://localhost:3000
- Backend API: http://localhost:5000
4. Login with default credentials:
- Username: `admin`
- Password: `admin123`
### Manual Setup
#### Backend Setup
1. Navigate to the backend directory:
```bash
cd backend
```
2. Install Python dependencies:
```bash
pip install -r requirements.txt
```
3. Create `.env` file (optional):
```bash
cp .env.example .env
# Edit .env to set custom credentials
```
4. Run the Flask application:
```bash
python app.py
```
The backend will be available at http://localhost:5000
#### Frontend Setup
1. Navigate to the frontend directory:
```bash
cd frontend
```
2. Install Node.js dependencies:
```bash
npm install
```
3. Create `.env.local` file:
```bash
cp .env.local.example .env.local
# Edit if backend is running on a different URL
```
4. Run the development server:
```bash
npm run dev
```
The frontend will be available at http://localhost:3000
## Configuration
### Backend Environment Variables
- `ADMIN_USERNAME` - Admin username (default: `admin`)
- `ADMIN_PASSWORD` - Admin password (default: `admin123`)
- `FLASK_ENV` - Flask environment (default: `development`)
### Frontend Environment Variables
- `NEXT_PUBLIC_API_URL` - Backend API URL (default: `http://localhost:5000`)
## API Endpoints
### Authentication
- `POST /api/auth/login` - Authenticate user
- `POST /api/auth/logout` - Logout user
### Containers
- `GET /api/containers` - List all containers
- `POST /api/containers/<id>/exec` - Execute command in container
### Health
- `GET /api/health` - Health check endpoint
## Docker Images
Images are automatically built and pushed to GitHub Container Registry on every push to main:
- Backend: `ghcr.io/johndoe6345789/docker-swarm-termina-backend`
- Frontend: `ghcr.io/johndoe6345789/docker-swarm-termina-frontend`
## Security Considerations
⚠️ **Important Security Notes:**
1. **Change Default Credentials** - Always change the default admin credentials in production
2. **Docker Socket Access** - The backend requires access to Docker socket (`/var/run/docker.sock`)
3. **Network Security** - Use proper firewall rules and network isolation
4. **HTTPS** - Use HTTPS in production (reverse proxy recommended)
5. **Session Management** - Current implementation uses simple token-based auth; consider implementing JWT or OAuth for production
## Development
### Project Structure
```
.
├── backend/ # Flask backend
│ ├── app.py # Main Flask application
│ ├── requirements.txt # Python dependencies
│ └── Dockerfile # Backend Docker image
├── frontend/ # Next.js frontend
│ ├── app/ # Next.js app directory
│ ├── components/ # React components
│ ├── lib/ # Utility functions and API client
│ └── Dockerfile # Frontend Docker image
├── docker-compose.yml # Docker Compose configuration
└── .github/
└── workflows/
└── docker-publish.yml # GHCR publishing workflow
```
### Building for Production
#### Build Docker Images
```bash
docker-compose build
```
#### Push to GHCR
Images are automatically pushed when code is merged to main. To manually push:
```bash
docker tag docker-swarm-termina-backend ghcr.io/johndoe6345789/docker-swarm-termina-backend:latest
docker tag docker-swarm-termina-frontend ghcr.io/johndoe6345789/docker-swarm-termina-frontend:latest
docker push ghcr.io/johndoe6345789/docker-swarm-termina-backend:latest
docker push ghcr.io/johndoe6345789/docker-swarm-termina-frontend:latest
```
## License
MIT License - see [LICENSE](LICENSE) file for details
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
## Support
For issues, questions, or contributions, please open an issue on GitHub.

View File

@@ -0,0 +1,31 @@
Thanks for helping make GitHub safe for everyone.
# Security
GitHub takes the security of our software products and services seriously, including all of the open source code repositories managed through our GitHub organizations, such as [GitHub](https://github.com/GitHub).
Even though [open source repositories are outside of the scope of our bug bounty program](https://bounty.github.com/index.html#scope) and therefore not eligible for bounty rewards, we will ensure that your finding gets passed along to the appropriate maintainers for remediation.
## Reporting Security Issues
If you believe you have found a security vulnerability in any GitHub-owned repository, please report it to us through coordinated disclosure.
**Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.**
Instead, please send an email to opensource-security[@]github.com.
Please include as much of the information listed below as you can to help us better understand and resolve the issue:
* The type of issue (e.g., buffer overflow, SQL injection, or cross-site scripting)
* Full paths of source file(s) related to the manifestation of the issue
* The location of the affected source code (tag/branch/commit or direct URL)
* Any special configuration required to reproduce the issue
* Step-by-step instructions to reproduce the issue
* Proof-of-concept or exploit code (if possible)
* Impact of the issue, including how an attacker might exploit the issue
This information will help us triage your report more quickly.
## Policy
See [GitHub's Safe Harbor Policy](https://docs.github.com/en/site-policy/security-policies/github-bug-bounty-program-legal-safe-harbor#1-safe-harbor-terms)

View File

@@ -0,0 +1,5 @@
# Flask Backend Environment Variables
FLASK_APP=app.py
FLASK_ENV=development
ADMIN_USERNAME=admin
ADMIN_PASSWORD=admin123

View File

@@ -0,0 +1,16 @@
FROM python:3.11-slim
WORKDIR /app
# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application
COPY . .
# Expose port
EXPOSE 5000
# Run the application
CMD ["python", "app.py"]

View File

@@ -0,0 +1,59 @@
# Backend - Flask API
Python Flask backend for Docker container management.
## Features
- RESTful API for container management
- Docker SDK integration
- Session-based authentication
- CORS enabled for frontend access
## Setup
1. Install dependencies:
```bash
pip install -r requirements.txt
```
2. Configure environment (optional):
```bash
cp .env.example .env
# Edit .env with your settings
```
3. Run the server:
```bash
python app.py
```
The server will start on http://localhost:5000
## API Endpoints
### Authentication
- `POST /api/auth/login` - Login with username/password
- `POST /api/auth/logout` - Logout current session
### Containers
- `GET /api/containers` - List all containers (requires auth)
- `POST /api/containers/<id>/exec` - Execute command in container (requires auth)
### Health
- `GET /api/health` - Health check
## Docker
Build the Docker image:
```bash
docker build -t docker-swarm-backend .
```
Run the container:
```bash
docker run -p 5000:5000 -v /var/run/docker.sock:/var/run/docker.sock docker-swarm-backend
```
## Security
⚠️ This backend requires access to the Docker socket. Ensure proper security measures are in place in production environments.

View File

@@ -0,0 +1,145 @@
from flask import Flask, jsonify, request
from flask_cors import CORS
import docker
import os
from datetime import datetime, timedelta
app = Flask(__name__)
CORS(app)
# Simple in-memory session storage (in production, use proper session management)
sessions = {}
# Default credentials (should be environment variables in production)
ADMIN_USERNAME = os.getenv('ADMIN_USERNAME', 'admin')
ADMIN_PASSWORD = os.getenv('ADMIN_PASSWORD', 'admin123')
def get_docker_client():
"""Get Docker client"""
try:
return docker.from_env()
except Exception as e:
print(f"Error connecting to Docker: {e}")
return None
def format_uptime(created_at):
"""Format container uptime"""
created = datetime.fromisoformat(created_at.replace('Z', '+00:00'))
now = datetime.now(created.tzinfo)
delta = now - created
days = delta.days
hours = delta.seconds // 3600
minutes = (delta.seconds % 3600) // 60
if days > 0:
return f"{days}d {hours}h"
elif hours > 0:
return f"{hours}h {minutes}m"
else:
return f"{minutes}m"
@app.route('/api/auth/login', methods=['POST'])
def login():
"""Authenticate user"""
data = request.get_json()
username = data.get('username')
password = data.get('password')
if username == ADMIN_USERNAME and password == ADMIN_PASSWORD:
# Create a simple session token (in production, use JWT or proper session management)
session_token = f"session_{username}_{datetime.now().timestamp()}"
sessions[session_token] = {
'username': username,
'created_at': datetime.now()
}
return jsonify({
'success': True,
'token': session_token,
'username': username
})
return jsonify({
'success': False,
'message': 'Invalid credentials'
}), 401
@app.route('/api/auth/logout', methods=['POST'])
def logout():
"""Logout user"""
auth_header = request.headers.get('Authorization')
if auth_header and auth_header.startswith('Bearer '):
token = auth_header.split(' ')[1]
if token in sessions:
del sessions[token]
return jsonify({'success': True})
@app.route('/api/containers', methods=['GET'])
def get_containers():
"""Get list of all containers"""
auth_header = request.headers.get('Authorization')
if not auth_header or not auth_header.startswith('Bearer '):
return jsonify({'error': 'Unauthorized'}), 401
token = auth_header.split(' ')[1]
if token not in sessions:
return jsonify({'error': 'Invalid session'}), 401
client = get_docker_client()
if not client:
return jsonify({'error': 'Cannot connect to Docker'}), 500
try:
containers = client.containers.list(all=True)
container_list = []
for container in containers:
container_list.append({
'id': container.short_id,
'name': container.name,
'image': container.image.tags[0] if container.image.tags else 'unknown',
'status': container.status,
'uptime': format_uptime(container.attrs['Created']) if container.status == 'running' else 'N/A'
})
return jsonify({'containers': container_list})
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/containers/<container_id>/exec', methods=['POST'])
def exec_container(container_id):
"""Execute command in container"""
auth_header = request.headers.get('Authorization')
if not auth_header or not auth_header.startswith('Bearer '):
return jsonify({'error': 'Unauthorized'}), 401
token = auth_header.split(' ')[1]
if token not in sessions:
return jsonify({'error': 'Invalid session'}), 401
data = request.get_json()
command = data.get('command', '/bin/sh')
client = get_docker_client()
if not client:
return jsonify({'error': 'Cannot connect to Docker'}), 500
try:
container = client.containers.get(container_id)
exec_instance = container.exec_run(command, stdout=True, stderr=True, stdin=True, tty=True)
return jsonify({
'output': exec_instance.output.decode('utf-8') if exec_instance.output else '',
'exit_code': exec_instance.exit_code
})
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/health', methods=['GET'])
def health():
"""Health check endpoint"""
return jsonify({'status': 'healthy'})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True)

View File

@@ -0,0 +1,4 @@
Flask==3.0.0
Flask-CORS==6.0.0
python-dotenv==1.0.0
docker==7.0.0

View File

@@ -0,0 +1,29 @@
version: '3.8'
services:
backend:
build:
context: ./backend
dockerfile: Dockerfile
ports:
- "5000:5000"
environment:
- ADMIN_USERNAME=admin
- ADMIN_PASSWORD=admin123
volumes:
- /var/run/docker.sock:/var/run/docker.sock
restart: unless-stopped
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
args:
- NEXT_PUBLIC_API_URL=http://localhost:5000
ports:
- "3000:3000"
environment:
- NEXT_PUBLIC_API_URL=http://backend:5000
depends_on:
- backend
restart: unless-stopped

41
dockerterminal/frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

View File

@@ -0,0 +1,49 @@
FROM node:20-alpine AS base
# Install dependencies only when needed
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package*.json ./
RUN npm ci
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Set environment variables for build
ARG NEXT_PUBLIC_API_URL
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
RUN npm run build
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next
# Automatically leverage output traces to reduce image size
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]

View File

@@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.

View File

@@ -0,0 +1,222 @@
'use client';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import {
Box,
Container,
Typography,
Button,
Grid,
AppBar,
Toolbar,
IconButton,
CircularProgress,
} from '@mui/material';
import { Logout, Refresh, Inventory2 } from '@mui/icons-material';
import { useAuth } from '@/lib/auth';
import { apiClient, Container as ContainerType } from '@/lib/api';
import ContainerCard from '@/components/ContainerCard';
import TerminalModal from '@/components/TerminalModal';
export default function Dashboard() {
const { isAuthenticated, loading: authLoading, logout } = useAuth();
const router = useRouter();
const [containers, setContainers] = useState<ContainerType[]>([]);
const [selectedContainer, setSelectedContainer] = useState<ContainerType | null>(null);
const [isTerminalOpen, setIsTerminalOpen] = useState(false);
const [isRefreshing, setIsRefreshing] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState('');
useEffect(() => {
if (!authLoading && !isAuthenticated) {
router.push('/');
}
}, [isAuthenticated, authLoading, router]);
const fetchContainers = async () => {
setIsRefreshing(true);
setError('');
try {
const data = await apiClient.getContainers();
setContainers(data);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to fetch containers');
if (err instanceof Error && err.message === 'Session expired') {
router.push('/');
}
} finally {
setIsRefreshing(false);
setIsLoading(false);
}
};
useEffect(() => {
if (isAuthenticated) {
fetchContainers();
const interval = setInterval(fetchContainers, 10000);
return () => clearInterval(interval);
}
}, [isAuthenticated]);
const handleOpenShell = (container: ContainerType) => {
setSelectedContainer(container);
setIsTerminalOpen(true);
};
const handleCloseTerminal = () => {
setIsTerminalOpen(false);
setTimeout(() => setSelectedContainer(null), 300);
};
const handleLogout = async () => {
await logout();
router.push('/');
};
const handleRefresh = () => {
fetchContainers();
};
if (authLoading || isLoading) {
return (
<Box
sx={{
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<CircularProgress />
</Box>
);
}
return (
<Box sx={{ minHeight: '100vh', backgroundColor: 'background.default' }}>
<AppBar
position="sticky"
sx={{
backgroundColor: 'rgba(45, 55, 72, 0.5)',
backdropFilter: 'blur(8px)',
borderBottom: 1,
borderColor: 'divider',
}}
>
<Toolbar>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, flexGrow: 1 }}>
<Box
sx={{
width: 40,
height: 40,
background: 'rgba(56, 178, 172, 0.1)',
borderRadius: 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Inventory2 sx={{ color: 'secondary.main' }} />
</Box>
<Box>
<Typography
variant="h1"
sx={{ fontFamily: '"JetBrains Mono", monospace', fontSize: '1.5rem' }}
>
Container Shell
</Typography>
<Typography variant="caption" color="text.secondary">
{containers.length} active {containers.length === 1 ? 'container' : 'containers'}
</Typography>
</Box>
</Box>
<Box sx={{ display: 'flex', gap: 1 }}>
<Button
variant="outlined"
size="small"
onClick={handleRefresh}
disabled={isRefreshing}
startIcon={isRefreshing ? <CircularProgress size={16} /> : <Refresh />}
>
Refresh
</Button>
<Button
variant="outlined"
size="small"
onClick={handleLogout}
startIcon={<Logout />}
>
Logout
</Button>
</Box>
</Toolbar>
</AppBar>
<Container maxWidth="xl" sx={{ py: 4 }}>
{error && (
<Box sx={{ mb: 2, p: 2, bgcolor: 'error.dark', borderRadius: 1 }}>
<Typography color="error.contrastText">{error}</Typography>
</Box>
)}
{containers.length === 0 && !isLoading ? (
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
minHeight: 400,
textAlign: 'center',
}}
>
<Box
sx={{
width: 80,
height: 80,
backgroundColor: 'action.hover',
borderRadius: 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
mb: 2,
}}
>
<Inventory2 sx={{ fontSize: 40, color: 'text.secondary' }} />
</Box>
<Typography variant="h2" gutterBottom>
No Active Containers
</Typography>
<Typography color="text.secondary" sx={{ maxWidth: 500 }}>
There are currently no running containers to display. Start a container to see it
appear here.
</Typography>
</Box>
) : (
<Grid container spacing={3}>
{containers.map((container) => (
<Grid size={{ xs: 12, sm: 6, lg: 4 }} key={container.id}>
<ContainerCard
container={container}
onOpenShell={() => handleOpenShell(container)}
/>
</Grid>
))}
</Grid>
)}
</Container>
{selectedContainer && (
<TerminalModal
open={isTerminalOpen}
onClose={handleCloseTerminal}
containerName={selectedContainer.name}
containerId={selectedContainer.id}
/>
)}
</Box>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -0,0 +1,26 @@
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}

View File

@@ -0,0 +1,35 @@
import type { Metadata } from "next";
import "./globals.css";
import { ThemeProvider } from "@/lib/theme";
import { AuthProvider } from "@/lib/auth";
export const metadata: Metadata = {
title: "Container Shell - Docker Swarm Terminal",
description: "Docker container management terminal web UI",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<head>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
<link
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
</head>
<body>
<ThemeProvider>
<AuthProvider>
{children}
</AuthProvider>
</ThemeProvider>
</body>
</html>
);
}

View File

@@ -0,0 +1,23 @@
'use client';
import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/lib/auth';
import LoginForm from '@/components/LoginForm';
export default function Home() {
const { isAuthenticated, loading } = useAuth();
const router = useRouter();
useEffect(() => {
if (!loading && isAuthenticated) {
router.push('/dashboard');
}
}, [isAuthenticated, loading, router]);
if (loading) {
return null;
}
return <LoginForm />;
}

View File

@@ -0,0 +1,162 @@
'use client';
import React from 'react';
import {
Card,
CardContent,
Typography,
Button,
Box,
Chip,
Divider,
} from '@mui/material';
import { Terminal, PlayArrow, Inventory2 } from '@mui/icons-material';
import { Container } from '@/lib/api';
interface ContainerCardProps {
container: Container;
onOpenShell: () => void;
}
export default function ContainerCard({ container, onOpenShell }: ContainerCardProps) {
const statusColors = {
running: 'success',
stopped: 'default',
paused: 'warning',
} as const;
const borderColors = {
running: '#38b2ac',
stopped: '#718096',
paused: '#ecc94b',
};
return (
<Card
sx={{
borderLeft: 4,
borderColor: borderColors[container.status as keyof typeof borderColors] || borderColors.stopped,
}}
>
<CardContent>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 2 }}>
<Box sx={{ display: 'flex', gap: 1.5, alignItems: 'flex-start', flex: 1 }}>
<Box
sx={{
width: 40,
height: 40,
background: 'rgba(56, 178, 172, 0.1)',
borderRadius: 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
}}
>
<Inventory2 sx={{ color: 'secondary.main', fontSize: 20 }} />
</Box>
<Box sx={{ minWidth: 0, flex: 1 }}>
<Typography
variant="h3"
component="h3"
sx={{
fontFamily: '"JetBrains Mono", monospace',
fontWeight: 500,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{container.name}
</Typography>
<Typography
variant="body2"
color="text.secondary"
sx={{
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{container.image}
</Typography>
</Box>
</Box>
<Chip
label={container.status}
color={statusColors[container.status as keyof typeof statusColors] || 'default'}
size="small"
icon={container.status === 'running' ? <PlayArrow sx={{ fontSize: 12 }} /> : undefined}
sx={{
fontFamily: '"JetBrains Mono", monospace',
textTransform: 'capitalize',
}}
/>
</Box>
<Divider sx={{ my: 2 }} />
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 2, mb: 3 }}>
<Box>
<Typography
variant="caption"
color="text.secondary"
sx={{
textTransform: 'uppercase',
letterSpacing: '0.05em',
display: 'block',
mb: 0.5,
}}
>
Container ID
</Typography>
<Typography
variant="body2"
sx={{ fontFamily: '"JetBrains Mono", monospace' }}
>
{container.id}
</Typography>
</Box>
<Box>
<Typography
variant="caption"
color="text.secondary"
sx={{
textTransform: 'uppercase',
letterSpacing: '0.05em',
display: 'block',
mb: 0.5,
}}
>
Uptime
</Typography>
<Typography
variant="body2"
sx={{ fontFamily: '"JetBrains Mono", monospace' }}
>
{container.uptime}
</Typography>
</Box>
</Box>
<Button
fullWidth
variant="contained"
color="primary"
onClick={onOpenShell}
disabled={container.status !== 'running'}
startIcon={<Terminal />}
sx={{
fontWeight: 500,
'&:hover': {
backgroundColor: 'secondary.main',
},
}}
>
Open Shell
</Button>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,146 @@
'use client';
import React, { useState } from 'react';
import {
Card,
CardContent,
TextField,
Button,
Typography,
Box,
Alert,
} from '@mui/material';
import { LockOpen } from '@mui/icons-material';
import { useAuth } from '@/lib/auth';
import { useRouter } from 'next/navigation';
export default function LoginForm() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [isShaking, setIsShaking] = useState(false);
const { login } = useAuth();
const router = useRouter();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
const success = await login(username, password);
if (success) {
router.push('/dashboard');
} else {
setError('Invalid credentials');
setIsShaking(true);
setTimeout(() => setIsShaking(false), 500);
}
};
return (
<Box
sx={{
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'linear-gradient(135deg, #1a202c 0%, #2d3748 100%)',
padding: 2,
}}
>
<Card
sx={{
width: '100%',
maxWidth: 400,
animation: isShaking ? 'shake 0.5s' : 'none',
'@keyframes shake': {
'0%, 100%': { transform: 'translateX(0)' },
'25%': { transform: 'translateX(-10px)' },
'75%': { transform: 'translateX(10px)' },
},
}}
>
<CardContent sx={{ p: 4 }}>
<Box sx={{ textAlign: 'center', mb: 4 }}>
<Box
sx={{
width: 64,
height: 64,
margin: '0 auto 16px',
background: 'rgba(56, 178, 172, 0.1)',
borderRadius: '8px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<LockOpen sx={{ fontSize: 32, color: 'secondary.main' }} />
</Box>
<Typography variant="h1" component="h1" gutterBottom>
Container Shell
</Typography>
<Typography variant="body2" color="text.secondary">
Enter your credentials to access container management
</Typography>
</Box>
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}
<form onSubmit={handleSubmit}>
<TextField
fullWidth
label="Username"
value={username}
onChange={(e) => setUsername(e.target.value)}
margin="normal"
required
autoComplete="username"
sx={{ mb: 2 }}
/>
<TextField
fullWidth
label="Password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
margin="normal"
required
autoComplete="current-password"
sx={{ mb: 3 }}
/>
<Button
type="submit"
fullWidth
variant="contained"
color="secondary"
size="large"
sx={{ mb: 2 }}
>
Access Dashboard
</Button>
<Typography
variant="caption"
color="text.secondary"
sx={{
display: 'block',
textAlign: 'center',
pt: 2,
borderTop: 1,
borderColor: 'divider',
}}
>
Default: admin / admin123
</Typography>
</form>
</CardContent>
</Card>
</Box>
);
}

View File

@@ -0,0 +1,167 @@
'use client';
import React, { useState } from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
TextField,
Box,
Typography,
IconButton,
Paper,
} from '@mui/material';
import { Close, Send } from '@mui/icons-material';
import { apiClient } from '@/lib/api';
interface TerminalModalProps {
open: boolean;
onClose: () => void;
containerName: string;
containerId: string;
}
export default function TerminalModal({
open,
onClose,
containerName,
containerId,
}: TerminalModalProps) {
const [command, setCommand] = useState('');
const [output, setOutput] = useState<string[]>([]);
const [isExecuting, setIsExecuting] = useState(false);
const handleExecute = async () => {
if (!command.trim()) return;
setIsExecuting(true);
setOutput((prev) => [...prev, `$ ${command}`]);
try {
const result = await apiClient.executeCommand(containerId, command);
setOutput((prev) => [...prev, result.output || '(no output)']);
} catch (error) {
setOutput((prev) => [...prev, `Error: ${error instanceof Error ? error.message : 'Unknown error'}`]);
} finally {
setIsExecuting(false);
setCommand('');
}
};
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleExecute();
}
};
const handleClose = () => {
setOutput([]);
setCommand('');
onClose();
};
return (
<Dialog
open={open}
onClose={handleClose}
maxWidth="md"
fullWidth
PaperProps={{
sx: {
minHeight: '500px',
maxHeight: '80vh',
},
}}
>
<DialogTitle
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
pb: 2,
}}
>
<Typography variant="h2" component="div">
Terminal - {containerName}
</Typography>
<IconButton onClick={handleClose} size="small">
<Close />
</IconButton>
</DialogTitle>
<DialogContent dividers>
<Paper
elevation={0}
sx={{
backgroundColor: '#0d1117',
color: '#c9d1d9',
fontFamily: '"JetBrains Mono", monospace',
fontSize: '0.875rem',
padding: 2,
minHeight: '300px',
maxHeight: '400px',
overflowY: 'auto',
mb: 2,
'&::-webkit-scrollbar': {
width: '8px',
},
'&::-webkit-scrollbar-track': {
background: '#161b22',
},
'&::-webkit-scrollbar-thumb': {
background: '#30363d',
borderRadius: '4px',
},
}}
>
{output.length === 0 ? (
<Typography color="text.secondary" sx={{ fontFamily: 'inherit' }}>
Connected to {containerName}. Enter a command to start...
</Typography>
) : (
<Box component="pre" sx={{ margin: 0, whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
{output.join('\n')}
</Box>
)}
</Paper>
<Box sx={{ display: 'flex', gap: 1 }}>
<TextField
fullWidth
value={command}
onChange={(e) => setCommand(e.target.value)}
onKeyPress={handleKeyPress}
placeholder="Enter command (e.g., ls, pwd, echo 'hello')"
disabled={isExecuting}
variant="outlined"
size="small"
sx={{
fontFamily: '"JetBrains Mono", monospace',
'& input': {
fontFamily: '"JetBrains Mono", monospace',
},
}}
/>
<Button
variant="contained"
color="secondary"
onClick={handleExecute}
disabled={isExecuting || !command.trim()}
startIcon={<Send />}
>
Execute
</Button>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} variant="outlined">
Close
</Button>
</DialogActions>
</Dialog>
);
}

View File

@@ -0,0 +1,18 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
]),
]);
export default eslintConfig;

View File

@@ -0,0 +1,117 @@
export const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5000';
export interface Container {
id: string;
name: string;
image: string;
status: string;
uptime: string;
}
export interface AuthResponse {
success: boolean;
token?: string;
username?: string;
message?: string;
}
export interface ContainersResponse {
containers: Container[];
}
class ApiClient {
private token: string | null = null;
setToken(token: string | null) {
this.token = token;
if (token) {
localStorage.setItem('auth_token', token);
} else {
localStorage.removeItem('auth_token');
}
}
getToken(): string | null {
if (!this.token && typeof window !== 'undefined') {
this.token = localStorage.getItem('auth_token');
}
return this.token;
}
async login(username: string, password: string): Promise<AuthResponse> {
const response = await fetch(`${API_BASE_URL}/api/auth/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ username, password }),
});
const data = await response.json();
if (data.success && data.token) {
this.setToken(data.token);
}
return data;
}
async logout(): Promise<void> {
const token = this.getToken();
if (token) {
await fetch(`${API_BASE_URL}/api/auth/logout`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
},
});
}
this.setToken(null);
}
async getContainers(): Promise<Container[]> {
const token = this.getToken();
if (!token) {
throw new Error('Not authenticated');
}
const response = await fetch(`${API_BASE_URL}/api/containers`, {
headers: {
'Authorization': `Bearer ${token}`,
},
});
if (!response.ok) {
if (response.status === 401) {
this.setToken(null);
throw new Error('Session expired');
}
throw new Error('Failed to fetch containers');
}
const data: ContainersResponse = await response.json();
return data.containers;
}
async executeCommand(containerId: string, command: string): Promise<any> {
const token = this.getToken();
if (!token) {
throw new Error('Not authenticated');
}
const response = await fetch(`${API_BASE_URL}/api/containers/${containerId}/exec`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ command }),
});
if (!response.ok) {
throw new Error('Failed to execute command');
}
return response.json();
}
}
export const apiClient = new ApiClient();

View File

@@ -0,0 +1,70 @@
'use client';
import React, { createContext, useContext, useState, useEffect } from 'react';
import { apiClient } from './api';
interface AuthContextType {
isAuthenticated: boolean;
username: string | null;
login: (username: string, password: string) => Promise<boolean>;
logout: () => Promise<void>;
loading: boolean;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [username, setUsername] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Check if user has a valid token
const token = apiClient.getToken();
if (token) {
setIsAuthenticated(true);
// In a real app, you'd validate the token with the backend
}
setLoading(false);
}, []);
const login = async (username: string, password: string): Promise<boolean> => {
try {
const response = await apiClient.login(username, password);
if (response.success) {
setIsAuthenticated(true);
setUsername(response.username || username);
return true;
}
return false;
} catch (error) {
console.error('Login error:', error);
return false;
}
};
const logout = async () => {
try {
await apiClient.logout();
} catch (error) {
console.error('Logout error:', error);
} finally {
setIsAuthenticated(false);
setUsername(null);
}
};
return (
<AuthContext.Provider value={{ isAuthenticated, username, login, logout, loading }}>
{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;
}

View File

@@ -0,0 +1,90 @@
'use client';
import { createTheme, ThemeProvider as MuiThemeProvider } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
const theme = createTheme({
palette: {
mode: 'dark',
primary: {
main: '#4a5568', // slate
light: '#718096',
dark: '#2d3748',
},
secondary: {
main: '#38b2ac', // cyan/teal
light: '#4fd1c5',
dark: '#319795',
},
background: {
default: '#1a202c',
paper: '#2d3748',
},
text: {
primary: '#f7fafc',
secondary: '#cbd5e0',
},
success: {
main: '#38b2ac',
},
},
typography: {
fontFamily: '"JetBrains Mono", "Courier New", monospace',
h1: {
fontWeight: 700,
fontSize: '2rem',
letterSpacing: '-0.02em',
},
h2: {
fontWeight: 600,
fontSize: '1.5rem',
},
h3: {
fontWeight: 500,
fontSize: '1.125rem',
},
body1: {
fontSize: '0.875rem',
lineHeight: 1.6,
},
button: {
fontWeight: 500,
textTransform: 'none',
},
},
components: {
MuiCard: {
styleOverrides: {
root: {
borderRadius: '8px',
transition: 'all 0.15s ease',
'&:hover': {
transform: 'translateY(-2px)',
boxShadow: '0 4px 20px rgba(56, 178, 172, 0.1)',
},
},
},
},
MuiButton: {
styleOverrides: {
root: {
borderRadius: '6px',
padding: '8px 16px',
transition: 'all 0.1s ease',
'&:active': {
transform: 'scale(0.98)',
},
},
},
},
},
});
export function ThemeProvider({ children }: { children: React.ReactNode }) {
return (
<MuiThemeProvider theme={theme}>
<CssBaseline />
{children}
</MuiThemeProvider>
);
}

View File

@@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: 'standalone',
};
export default nextConfig;

7103
dockerterminal/frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,30 @@
{
"name": "frontend",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@mui/icons-material": "^7.3.7",
"@mui/material": "^7.3.7",
"next": "16.1.1",
"react": "19.2.3",
"react-dom": "19.2.3"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.1.1",
"tailwindcss": "^4",
"typescript": "^5"
}
}

View File

@@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;

View File

@@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 391 B

View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 128 B

View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

After

Width:  |  Height:  |  Size: 385 B

View File

@@ -0,0 +1,34 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
"**/*.mts"
],
"exclude": ["node_modules"]
}