Initial Commit
This commit is contained in:
19
.env.example
Normal file
19
.env.example
Normal file
@@ -0,0 +1,19 @@
|
||||
# Docker Dashboard Configuration
|
||||
# Copy this file to .env and customize the values
|
||||
|
||||
# Server Configuration
|
||||
PORT=5000
|
||||
HOST=0.0.0.0
|
||||
DEBUG=True
|
||||
|
||||
# Portainer Integration (Optional)
|
||||
PORTAINER_URL=https://port.liveaodh.com
|
||||
PORTAINER_USERNAME=aodhan
|
||||
PORTAINER_PASSWORD=8yFdXkx274aSRX
|
||||
|
||||
# Docker Configuration
|
||||
DOCKER_HOST=unix:///var/run/docker.sock
|
||||
|
||||
# Application Settings
|
||||
REFRESH_INTERVAL=30
|
||||
MAX_CONTAINERS=50
|
||||
121
.gitignore
vendored
Normal file
121
.gitignore
vendored
Normal file
@@ -0,0 +1,121 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# Virtual Environment
|
||||
venv/
|
||||
env/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# Database
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# Environment Variables
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# Runtime files
|
||||
*.pid
|
||||
*.sock
|
||||
|
||||
# Docker
|
||||
.dockerignore
|
||||
|
||||
# Node.js (if any)
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# celery beat schedule file
|
||||
celerybeat-schedule
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# Project specific
|
||||
dashboard.db
|
||||
dashboard.pid
|
||||
stop.sh
|
||||
logs/
|
||||
234
README.md
Normal file
234
README.md
Normal file
@@ -0,0 +1,234 @@
|
||||
# Docker Dashboard
|
||||
|
||||
A modern, responsive web dashboard for managing and monitoring Docker containers with SQLite database integration and manual customization capabilities.
|
||||
|
||||
## Features
|
||||
|
||||
- **Dynamic Container Tiles**: Automatically discovers and displays all Docker containers
|
||||
- **SQLite Database**: Persistent storage for custom tile configurations
|
||||
- **Manual Customization**: Edit display names, icons, colors, descriptions, and URLs
|
||||
- **Real-time Monitoring**: Live status updates and system metrics
|
||||
- **Responsive Design**: Works on desktop, tablet, and mobile devices
|
||||
- **Portainer Integration**: Optional enhanced functionality with Portainer API
|
||||
- **Container Management**: Start, stop, and restart containers directly from the dashboard
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Prerequisites
|
||||
- Docker installed and running
|
||||
- Python 3.7+
|
||||
- Docker socket access (typically requires sudo)
|
||||
|
||||
### Installation
|
||||
|
||||
1. **Clone or download the project**
|
||||
2. **Install dependencies**:
|
||||
```bash
|
||||
pip3 install -r requirements.txt
|
||||
```
|
||||
|
||||
3. **Configure environment**:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Edit .env with your specific settings
|
||||
```
|
||||
|
||||
4. **Run the setup script** (recommended):
|
||||
```bash
|
||||
chmod +x setup.sh
|
||||
./setup.sh
|
||||
```
|
||||
|
||||
5. **Or run manually**:
|
||||
```bash
|
||||
sudo python3 api.py
|
||||
```
|
||||
|
||||
6. **Access the dashboard**:
|
||||
Open your browser to `http://localhost:5000`
|
||||
|
||||
### Manual Setup
|
||||
|
||||
If you prefer to set up manually:
|
||||
|
||||
1. **Install Python dependencies**:
|
||||
```bash
|
||||
pip3 install flask flask-cors docker requests python-dotenv
|
||||
```
|
||||
|
||||
2. **Initialize database**:
|
||||
```bash
|
||||
python3 -c "import database; db = database.DatabaseManager(); db.init_db()"
|
||||
```
|
||||
|
||||
3. **Start the application**:
|
||||
```bash
|
||||
sudo python3 api.py
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `PORT` | Server port | 5000 |
|
||||
| `HOST` | Server host | 0.0.0.0 |
|
||||
| `DEBUG` | Debug mode | False |
|
||||
| `REFRESH_INTERVAL` | Auto-refresh interval (seconds) | 30 |
|
||||
| `PORTAINER_URL` | Portainer URL (optional) | - |
|
||||
| `PORTAINER_USERNAME` | Portainer username | - |
|
||||
| `PORTAINER_PASSWORD` | Portainer password | - |
|
||||
|
||||
### Portainer Integration (Optional)
|
||||
|
||||
To use Portainer for enhanced container management:
|
||||
|
||||
1. Set up Portainer on your Docker host
|
||||
2. Configure the environment variables:
|
||||
```bash
|
||||
PORTAINER_URL=https://your-portainer-instance.com
|
||||
PORTAINER_USERNAME=your-username
|
||||
PORTAINER_PASSWORD=your-password
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Dashboard Overview
|
||||
|
||||
- **Server Information**: CPU, memory, and disk usage
|
||||
- **Container Tiles**: Visual representation of all Docker containers
|
||||
- **Live Status**: Real-time container status (running/stopped)
|
||||
- **Uptime Display**: Container uptime information
|
||||
- **Quick Actions**: Open, edit, or manage containers
|
||||
|
||||
### Customizing Tiles
|
||||
|
||||
1. **Click the "Edit" button** on any container tile
|
||||
2. **Modify the fields**:
|
||||
- **Display Name**: Custom name shown on the tile
|
||||
- **Icon**: Font Awesome icon class (e.g., `fas fa-cube`)
|
||||
- **Color**: Tailwind CSS color (e.g., `blue`, `green`, `red`)
|
||||
- **Description**: Additional description text
|
||||
- **URL**: Custom URL for the application
|
||||
3. **Save changes** - updates are immediately reflected
|
||||
|
||||
### Syncing Containers
|
||||
|
||||
- **Automatic**: Containers sync every 30 seconds
|
||||
- **Manual**: Click "Sync Apps" button to force refresh
|
||||
- **New containers**: Automatically appear as new tiles
|
||||
- **Removed containers**: Tiles are removed from display
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### System Information
|
||||
- `GET /api/system` - System metrics and uptime
|
||||
|
||||
### Containers
|
||||
- `GET /api/containers` - List all Docker containers
|
||||
- `GET /api/health/<container_name>` - Check container health
|
||||
|
||||
### Applications (Database)
|
||||
- `GET /api/applications` - Get all applications from database
|
||||
- `GET /api/applications/<container_id>` - Get specific application
|
||||
- `PUT /api/applications/<container_id>` - Update application data
|
||||
- `DELETE /api/applications/<container_id>` - Delete application
|
||||
- `POST /api/applications/sync` - Sync with current containers
|
||||
|
||||
### Portainer (Optional)
|
||||
- `GET /api/portainer/containers` - Get containers via Portainer
|
||||
- `POST /api/portainer/container/<action>/<container_id>` - Manage containers
|
||||
|
||||
## Database Schema
|
||||
|
||||
The SQLite database (`dashboard.db`) contains the following table:
|
||||
|
||||
```sql
|
||||
CREATE TABLE applications (
|
||||
container_id TEXT PRIMARY KEY,
|
||||
container_name TEXT NOT NULL,
|
||||
display_name TEXT,
|
||||
image TEXT,
|
||||
status TEXT,
|
||||
ports TEXT,
|
||||
uptime TEXT,
|
||||
icon TEXT DEFAULT 'fas fa-cube',
|
||||
color TEXT DEFAULT 'gray',
|
||||
description TEXT,
|
||||
url TEXT,
|
||||
custom_data TEXT
|
||||
);
|
||||
```
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
├── api.py # Main Flask API server
|
||||
├── database.py # SQLite database manager
|
||||
├── template.html # Frontend dashboard
|
||||
├── portainer_integration.py # Portainer API integration
|
||||
├── setup.sh # Setup and launch script
|
||||
├── stop.sh # Stop script (generated)
|
||||
├── .env # Environment variables (create from .env.example)
|
||||
├── .env.example # Environment template
|
||||
├── requirements.txt # Python dependencies
|
||||
├── .gitignore # Git ignore rules
|
||||
├── dashboard.db # SQLite database (created automatically)
|
||||
└── logs/
|
||||
└── dashboard.log # Application logs
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Docker Permission Issues
|
||||
If you see "Permission denied" errors:
|
||||
```bash
|
||||
# Add your user to docker group
|
||||
sudo usermod -aG docker $USER
|
||||
# Log out and back in
|
||||
```
|
||||
|
||||
### Port Issues
|
||||
If port 5000 is already in use:
|
||||
```bash
|
||||
# Edit .env file and change PORT
|
||||
PORT=5001
|
||||
```
|
||||
|
||||
### Database Issues
|
||||
To reset the database:
|
||||
```bash
|
||||
rm dashboard.db
|
||||
python3 -c "import database; db = database.DatabaseManager(); db.init_db()"
|
||||
```
|
||||
|
||||
### View Logs
|
||||
```bash
|
||||
tail -f logs/dashboard.log
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Adding New Features
|
||||
1. **Backend**: Modify `api.py` for new endpoints
|
||||
2. **Database**: Update `database.py` for schema changes
|
||||
3. **Frontend**: Edit `template.html` for UI changes
|
||||
|
||||
### Contributing
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes
|
||||
4. Test thoroughly
|
||||
5. Submit a pull request
|
||||
|
||||
## License
|
||||
|
||||
This project is open source and available under the [MIT License](LICENSE).
|
||||
|
||||
## Support
|
||||
|
||||
For issues or questions:
|
||||
1. Check the troubleshooting section above
|
||||
2. Review the logs in `logs/dashboard.log`
|
||||
3. Open an issue on the project repository
|
||||
392
api.py
Normal file
392
api.py
Normal file
@@ -0,0 +1,392 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Docker Dashboard API
|
||||
Fetches container data from Docker instances and provides endpoints for the frontend
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import time
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from flask import Flask, jsonify, render_template, request, send_from_directory
|
||||
from flask_cors import CORS
|
||||
import docker
|
||||
import psutil
|
||||
import requests
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
from portainer_integration import PortainerClient
|
||||
from database import db
|
||||
|
||||
# Load environment variables
|
||||
load_dotenv()
|
||||
|
||||
app = Flask(__name__)
|
||||
CORS(app)
|
||||
|
||||
class DockerService:
|
||||
def __init__(self):
|
||||
try:
|
||||
self.client = docker.from_env()
|
||||
print("Docker client initialized successfully")
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to initialize Docker client: {e}")
|
||||
self.client = None
|
||||
|
||||
def get_containers(self):
|
||||
"""Get all containers with their status"""
|
||||
if not self.client:
|
||||
return []
|
||||
|
||||
containers = []
|
||||
try:
|
||||
for container in self.client.containers.list(all=True):
|
||||
container_data = {
|
||||
'id': container.id[:12],
|
||||
'name': container.name,
|
||||
'status': container.status,
|
||||
'image': container.image.tags[0] if container.image.tags else container.image.short_id,
|
||||
'ports': self._get_container_ports(container),
|
||||
'created': container.attrs['Created'],
|
||||
'uptime': self._calculate_uptime(container),
|
||||
'labels': container.labels,
|
||||
'environment': container.attrs['Config'].get('Env', []),
|
||||
'mounts': self._get_mounts(container),
|
||||
'networks': self._get_networks(container)
|
||||
}
|
||||
containers.append(container_data)
|
||||
except Exception as e:
|
||||
logging.error(f"Error fetching containers: {e}")
|
||||
return []
|
||||
|
||||
return containers
|
||||
|
||||
def _get_container_ports(self, container):
|
||||
"""Extract port mappings from container"""
|
||||
ports = []
|
||||
try:
|
||||
port_bindings = container.attrs['NetworkSettings']['Ports']
|
||||
for container_port, host_configs in port_bindings.items():
|
||||
if host_configs:
|
||||
for host_config in host_configs:
|
||||
ports.append({
|
||||
'container_port': container_port,
|
||||
'host_ip': host_config.get('HostIp', '0.0.0.0'),
|
||||
'host_port': host_config.get('HostPort')
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
return ports
|
||||
|
||||
def _calculate_uptime(self, container):
|
||||
"""Calculate container uptime"""
|
||||
if container.status != 'running':
|
||||
return "Down"
|
||||
|
||||
try:
|
||||
started_at = container.attrs['State']['StartedAt']
|
||||
started_time = datetime.fromisoformat(started_at.replace('Z', '+00:00'))
|
||||
uptime = datetime.now(started_time.tzinfo) - started_time
|
||||
|
||||
days = uptime.days
|
||||
hours, remainder = divmod(uptime.seconds, 3600)
|
||||
minutes, seconds = divmod(remainder, 60)
|
||||
|
||||
if days > 0:
|
||||
return f"{days}d {hours}h"
|
||||
elif hours > 0:
|
||||
return f"{hours}h {minutes}m"
|
||||
else:
|
||||
return f"{minutes}m"
|
||||
except Exception:
|
||||
return "Unknown"
|
||||
|
||||
def _get_mounts(self, container):
|
||||
"""Get container volume mounts"""
|
||||
mounts = []
|
||||
try:
|
||||
for mount in container.attrs['Mounts']:
|
||||
mounts.append({
|
||||
'type': mount.get('Type'),
|
||||
'source': mount.get('Source'),
|
||||
'destination': mount.get('Destination'),
|
||||
'mode': mount.get('Mode')
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
return mounts
|
||||
|
||||
def _get_networks(self, container):
|
||||
"""Get container network information"""
|
||||
networks = []
|
||||
try:
|
||||
networks_config = container.attrs['NetworkSettings']['Networks']
|
||||
for network_name, network_config in networks_config.items():
|
||||
networks.append({
|
||||
'name': network_name,
|
||||
'ip_address': network_config.get('IPAddress'),
|
||||
'gateway': network_config.get('Gateway'),
|
||||
'mac_address': network_config.get('MacAddress')
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
return networks
|
||||
|
||||
def get_system_info(self):
|
||||
"""Get system information"""
|
||||
try:
|
||||
memory = psutil.virtual_memory()
|
||||
disk = psutil.disk_usage('/')
|
||||
|
||||
return {
|
||||
'cpu_percent': psutil.cpu_percent(),
|
||||
'memory': {
|
||||
'total': memory.total,
|
||||
'available': memory.available,
|
||||
'percent': memory.percent,
|
||||
'used': memory.used
|
||||
},
|
||||
'disk': {
|
||||
'total': disk.total,
|
||||
'used': disk.used,
|
||||
'free': disk.free,
|
||||
'percent': (disk.used / disk.total) * 100
|
||||
},
|
||||
'uptime': self._get_system_uptime()
|
||||
}
|
||||
except Exception as e:
|
||||
logging.error(f"Error getting system info: {e}")
|
||||
return {}
|
||||
|
||||
def _get_system_uptime(self):
|
||||
"""Get system uptime"""
|
||||
try:
|
||||
with open('/proc/uptime', 'r') as f:
|
||||
uptime_seconds = float(f.readline().split()[0])
|
||||
|
||||
days = int(uptime_seconds // 86400)
|
||||
hours = int((uptime_seconds % 86400) // 3600)
|
||||
minutes = int((uptime_seconds % 3600) // 60)
|
||||
|
||||
return f"{days}d {hours}h {minutes}m"
|
||||
except Exception:
|
||||
return "Unknown"
|
||||
|
||||
def check_service_health(self, url, port=None):
|
||||
"""Check if a service is responding"""
|
||||
try:
|
||||
if port:
|
||||
url = f"http://{url}:{port}"
|
||||
|
||||
response = requests.get(url, timeout=5)
|
||||
return response.status_code == 200
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
# Configuration from environment variables
|
||||
PORT = int(os.getenv('PORT', 5000))
|
||||
HOST = os.getenv('HOST', '0.0.0.0')
|
||||
DEBUG = os.getenv('DEBUG', 'False').lower() == 'true'
|
||||
REFRESH_INTERVAL = int(os.getenv('REFRESH_INTERVAL', 30))
|
||||
|
||||
# Initialize Docker service
|
||||
docker_service = DockerService()
|
||||
|
||||
# Initialize Portainer client
|
||||
portainer_client = None
|
||||
portainer_url = os.getenv('PORTAINER_URL', 'http://localhost:9000')
|
||||
portainer_username = os.getenv('PORTAINER_USERNAME')
|
||||
portainer_password = os.getenv('PORTAINER_PASSWORD')
|
||||
|
||||
if portainer_username and portainer_password:
|
||||
portainer_client = PortainerClient(portainer_url, portainer_username, portainer_password)
|
||||
if portainer_client.authenticate():
|
||||
logging.info("Portainer client authenticated successfully")
|
||||
else:
|
||||
logging.error("Portainer authentication failed - using standard Docker API")
|
||||
portainer_client = None
|
||||
|
||||
@app.route('/')
|
||||
def dashboard():
|
||||
"""Serve the dashboard"""
|
||||
return render_template('template.html')
|
||||
|
||||
@app.route('/api/containers')
|
||||
def get_containers():
|
||||
"""Get all containers"""
|
||||
containers = docker_service.get_containers()
|
||||
return jsonify(containers)
|
||||
|
||||
@app.route('/api/system')
|
||||
def get_system_info():
|
||||
"""Get system information"""
|
||||
system_info = docker_service.get_system_info()
|
||||
return jsonify(system_info)
|
||||
|
||||
@app.route('/api/health/<container_name>')
|
||||
def check_container_health(container_name):
|
||||
"""Check health of a specific container"""
|
||||
try:
|
||||
# Get application from database for custom health check URL
|
||||
app = db.get_application(container_name)
|
||||
if app and app.get('url') != '#':
|
||||
url = app['url']
|
||||
else:
|
||||
url = f'http://{container_name}.local'
|
||||
|
||||
try:
|
||||
response = requests.get(url, timeout=5)
|
||||
return jsonify({
|
||||
'healthy': response.status_code < 400,
|
||||
'status_code': response.status_code,
|
||||
'url': url
|
||||
})
|
||||
except requests.RequestException:
|
||||
return jsonify({
|
||||
'healthy': False,
|
||||
'error': 'Service not responding',
|
||||
'url': url
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'healthy': False,
|
||||
'error': str(e)
|
||||
}), 500
|
||||
|
||||
@app.route('/api/portainer/containers')
|
||||
def get_portainer_containers():
|
||||
"""Get containers from Portainer if available"""
|
||||
try:
|
||||
# This would need Portainer API configuration
|
||||
# For now, return standard Docker containers
|
||||
return get_containers()
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/portainer/containers')
|
||||
def get_portainer_containers_list():
|
||||
"""Get containers from Portainer"""
|
||||
if not portainer_client:
|
||||
return jsonify({'error': 'Portainer not configured'}), 503
|
||||
|
||||
try:
|
||||
containers = portainer_client.get_containers()
|
||||
return jsonify(containers)
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/portainer/endpoints')
|
||||
def get_portainer_endpoints():
|
||||
"""Get Portainer endpoints"""
|
||||
if not portainer_client:
|
||||
return jsonify({'error': 'Portainer not configured'}), 503
|
||||
|
||||
try:
|
||||
endpoints = portainer_client.get_endpoints()
|
||||
return jsonify(endpoints)
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/portainer/stacks')
|
||||
def get_portainer_stacks():
|
||||
"""Get Docker stacks from Portainer"""
|
||||
if not portainer_client:
|
||||
return jsonify({'error': 'Portainer not configured'}), 503
|
||||
|
||||
try:
|
||||
stacks = portainer_client.get_stacks()
|
||||
return jsonify(stacks)
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/portainer/container/<action>/<container_id>', methods=['POST'])
|
||||
def manage_portainer_container(action, container_id):
|
||||
"""Manage containers via Portainer"""
|
||||
if not portainer_client:
|
||||
return jsonify({'error': 'Portainer not configured'}), 503
|
||||
|
||||
if action not in ['start', 'stop', 'restart']:
|
||||
return jsonify({'error': 'Invalid action'}), 400
|
||||
|
||||
try:
|
||||
success = False
|
||||
if action == 'start':
|
||||
success = portainer_client.start_container(container_id)
|
||||
elif action == 'stop':
|
||||
success = portainer_client.stop_container(container_id)
|
||||
elif action == 'restart':
|
||||
success = portainer_client.restart_container(container_id)
|
||||
|
||||
return jsonify({'success': success})
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/applications')
|
||||
def get_applications():
|
||||
"""Get all applications from database"""
|
||||
try:
|
||||
applications = db.get_applications()
|
||||
|
||||
# Sync with current containers
|
||||
containers = docker_service.get_containers()
|
||||
container_map = {c['id']: c for c in containers}
|
||||
|
||||
# Update applications with live container data
|
||||
for app in applications:
|
||||
container = container_map.get(app['container_id'])
|
||||
if container:
|
||||
app.update({
|
||||
'status': container.get('status', 'unknown'),
|
||||
'uptime': container.get('uptime', 'unknown'),
|
||||
'image': container.get('image', ''),
|
||||
'ports': container.get('ports', [])
|
||||
})
|
||||
|
||||
return jsonify({'applications': applications})
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/applications/<container_id>', methods=['GET', 'PUT', 'DELETE'])
|
||||
def manage_application(container_id):
|
||||
"""Manage individual application"""
|
||||
try:
|
||||
if request.method == 'GET':
|
||||
app = db.get_application(container_id)
|
||||
if app:
|
||||
return jsonify(app)
|
||||
return jsonify({'error': 'Application not found'}), 404
|
||||
|
||||
elif request.method == 'PUT':
|
||||
data = request.json
|
||||
db.update_application(container_id, data)
|
||||
return jsonify({'success': True, 'message': 'Application updated'})
|
||||
|
||||
elif request.method == 'DELETE':
|
||||
db.delete_application(container_id)
|
||||
return jsonify({'success': True, 'message': 'Application deleted'})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/applications/sync', methods=['POST'])
|
||||
def sync_applications():
|
||||
"""Sync applications with current containers"""
|
||||
try:
|
||||
containers = docker_service.get_containers()
|
||||
|
||||
# Sync each container with database
|
||||
for container in containers:
|
||||
db.upsert_application({
|
||||
'id': container['id'],
|
||||
'name': container['name'],
|
||||
'description': container['image'],
|
||||
'url': f"http://localhost:{container['ports'][0]['host_port']}" if container['ports'] else '#'
|
||||
})
|
||||
|
||||
return jsonify({'success': True, 'message': 'Applications synced successfully'})
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host=HOST, port=PORT, debug=DEBUG)
|
||||
194
database.py
Normal file
194
database.py
Normal file
@@ -0,0 +1,194 @@
|
||||
import sqlite3
|
||||
import os
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
class DatabaseManager:
|
||||
def __init__(self, db_path='dashboard.db'):
|
||||
self.db_path = db_path
|
||||
self.init_database()
|
||||
|
||||
def init_database(self):
|
||||
"""Initialize the database with required tables"""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Create applications table for storing custom app data
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS applications (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
container_id TEXT UNIQUE,
|
||||
container_name TEXT NOT NULL,
|
||||
display_name TEXT,
|
||||
icon TEXT DEFAULT 'fas fa-cube',
|
||||
color TEXT DEFAULT 'gray',
|
||||
description TEXT,
|
||||
url TEXT,
|
||||
custom_data TEXT, -- JSON for additional custom fields
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
''')
|
||||
|
||||
# Create settings table for general configuration
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
''')
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def get_connection(self):
|
||||
"""Get database connection"""
|
||||
return sqlite3.connect(self.db_path)
|
||||
|
||||
def upsert_application(self, container_data):
|
||||
"""Insert or update application data"""
|
||||
conn = self.get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
container_id = container_data.get('id')
|
||||
container_name = container_data.get('name')
|
||||
display_name = container_data.get('display_name', container_name)
|
||||
icon = container_data.get('icon', 'fas fa-cube')
|
||||
color = container_data.get('color', 'gray')
|
||||
description = container_data.get('description', container_data.get('image', ''))
|
||||
url = container_data.get('url', '#')
|
||||
custom_data = json.dumps(container_data.get('custom_data', {}))
|
||||
|
||||
cursor.execute('''
|
||||
INSERT OR REPLACE INTO applications
|
||||
(container_id, container_name, display_name, icon, color, description, url, custom_data, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
||||
''', (container_id, container_name, display_name, icon, color, description, url, custom_data))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def get_applications(self):
|
||||
"""Get all applications"""
|
||||
conn = self.get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('''
|
||||
SELECT container_id, container_name, display_name, icon, color, description, url, custom_data
|
||||
FROM applications
|
||||
ORDER BY display_name
|
||||
''')
|
||||
|
||||
results = cursor.fetchall()
|
||||
conn.close()
|
||||
|
||||
applications = []
|
||||
for row in results:
|
||||
app = {
|
||||
'container_id': row[0],
|
||||
'container_name': row[1],
|
||||
'display_name': row[2],
|
||||
'icon': row[3],
|
||||
'color': row[4],
|
||||
'description': row[5],
|
||||
'url': row[6],
|
||||
'custom_data': json.loads(row[7]) if row[7] else {}
|
||||
}
|
||||
applications.append(app)
|
||||
|
||||
return applications
|
||||
|
||||
def get_application(self, container_id):
|
||||
"""Get specific application by container_id"""
|
||||
conn = self.get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('''
|
||||
SELECT container_id, container_name, display_name, icon, color, description, url, custom_data
|
||||
FROM applications
|
||||
WHERE container_id = ?
|
||||
''', (container_id,))
|
||||
|
||||
result = cursor.fetchone()
|
||||
conn.close()
|
||||
|
||||
if result:
|
||||
return {
|
||||
'container_id': result[0],
|
||||
'container_name': result[1],
|
||||
'display_name': result[2],
|
||||
'icon': result[3],
|
||||
'color': result[4],
|
||||
'description': result[5],
|
||||
'url': result[6],
|
||||
'custom_data': json.loads(result[7]) if result[7] else {}
|
||||
}
|
||||
|
||||
return None
|
||||
|
||||
def update_application(self, container_id, updates):
|
||||
"""Update application data"""
|
||||
conn = self.get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
set_clause = []
|
||||
values = []
|
||||
|
||||
for key, value in updates.items():
|
||||
if key == 'custom_data':
|
||||
value = json.dumps(value)
|
||||
set_clause.append(f"{key} = ?")
|
||||
values.append(value)
|
||||
|
||||
if set_clause:
|
||||
set_clause.append("updated_at = CURRENT_TIMESTAMP")
|
||||
values.append(container_id)
|
||||
|
||||
query = f"UPDATE applications SET {', '.join(set_clause)} WHERE container_id = ?"
|
||||
cursor.execute(query, values)
|
||||
conn.commit()
|
||||
|
||||
conn.close()
|
||||
|
||||
def delete_application(self, container_id):
|
||||
"""Delete application"""
|
||||
conn = self.get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('DELETE FROM applications WHERE container_id = ?', (container_id,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def sync_with_containers(self, containers):
|
||||
"""Sync applications with current containers"""
|
||||
conn = self.get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Get existing container IDs
|
||||
cursor.execute('SELECT container_id FROM applications')
|
||||
existing_ids = {row[0] for row in cursor.fetchall()}
|
||||
|
||||
# Insert new containers
|
||||
for container in containers:
|
||||
container_id = container.get('id')
|
||||
if container_id not in existing_ids:
|
||||
self.upsert_application({
|
||||
'id': container_id,
|
||||
'name': container.get('name', ''),
|
||||
'description': container.get('image', ''),
|
||||
'url': self._generate_url_from_ports(container.get('ports', []))
|
||||
})
|
||||
|
||||
conn.close()
|
||||
|
||||
def _generate_url_from_ports(self, ports):
|
||||
"""Generate URL from container ports"""
|
||||
if ports and len(ports) > 0:
|
||||
port = ports[0]
|
||||
return f"http://localhost:{port.get('host_port', port.get('private_port', 80))}"
|
||||
return '#'
|
||||
|
||||
# Global database instance
|
||||
db = DatabaseManager()
|
||||
255
portainer_integration.py
Normal file
255
portainer_integration.py
Normal file
@@ -0,0 +1,255 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Portainer Integration Module
|
||||
Provides enhanced Docker management through Portainer API
|
||||
"""
|
||||
|
||||
import requests
|
||||
import json
|
||||
from typing import Dict, List, Optional
|
||||
import os
|
||||
|
||||
class PortainerClient:
|
||||
def __init__(self, base_url: str, username: str = None, password: str = None):
|
||||
self.base_url = base_url.rstrip('/')
|
||||
self.username = username or os.getenv('PORTAINER_USERNAME')
|
||||
self.password = password or os.getenv('PORTAINER_PASSWORD')
|
||||
self.token = None
|
||||
self.endpoint_id = None
|
||||
|
||||
def authenticate(self) -> bool:
|
||||
"""Authenticate with Portainer API"""
|
||||
try:
|
||||
auth_url = f"{self.base_url}/api/auth"
|
||||
payload = {
|
||||
"username": self.username,
|
||||
"password": self.password
|
||||
}
|
||||
|
||||
response = requests.post(auth_url, json=payload)
|
||||
if response.status_code == 200:
|
||||
self.token = response.json().get('jwt')
|
||||
return True
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"Portainer authentication failed: {e}")
|
||||
return False
|
||||
|
||||
def get_endpoints(self) -> List[Dict]:
|
||||
"""Get available endpoints"""
|
||||
if not self.token:
|
||||
return []
|
||||
|
||||
try:
|
||||
headers = {'Authorization': f'Bearer {self.token}'}
|
||||
response = requests.get(f"{self.base_url}/api/endpoints", headers=headers)
|
||||
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
return []
|
||||
except Exception as e:
|
||||
print(f"Error fetching endpoints: {e}")
|
||||
return []
|
||||
|
||||
def get_containers(self, endpoint_id: int = None) -> List[Dict]:
|
||||
"""Get containers from Portainer"""
|
||||
if not self.token:
|
||||
return []
|
||||
|
||||
try:
|
||||
if not endpoint_id:
|
||||
endpoints = self.get_endpoints()
|
||||
if endpoints:
|
||||
endpoint_id = endpoints[0]['Id']
|
||||
else:
|
||||
return []
|
||||
|
||||
headers = {'Authorization': f'Bearer {self.token}'}
|
||||
url = f"{self.base_url}/api/endpoints/{endpoint_id}/docker/containers/json?all=true"
|
||||
|
||||
response = requests.get(url, headers=headers)
|
||||
|
||||
if response.status_code == 200:
|
||||
containers = response.json()
|
||||
return self._format_portainer_containers(containers)
|
||||
return []
|
||||
except Exception as e:
|
||||
print(f"Error fetching containers from Portainer: {e}")
|
||||
return []
|
||||
|
||||
def _format_portainer_containers(self, containers: List[Dict]) -> List[Dict]:
|
||||
"""Format Portainer containers to match our standard format"""
|
||||
formatted = []
|
||||
|
||||
for container in containers:
|
||||
formatted_container = {
|
||||
'id': container.get('Id', '')[:12],
|
||||
'name': container.get('Names', [''])[0].lstrip('/'),
|
||||
'status': container.get('State', 'unknown'),
|
||||
'image': container.get('Image', ''),
|
||||
'ports': self._format_ports(container.get('Ports', [])),
|
||||
'created': container.get('Created', ''),
|
||||
'uptime': self._calculate_uptime(container),
|
||||
'labels': container.get('Labels', {}),
|
||||
'environment': [],
|
||||
'mounts': self._format_mounts(container.get('Mounts', [])),
|
||||
'networks': self._format_networks(container.get('NetworkSettings', {}).get('Networks', {}))
|
||||
}
|
||||
formatted.append(formatted_container)
|
||||
|
||||
return formatted
|
||||
|
||||
def _format_ports(self, ports: List[Dict]) -> List[Dict]:
|
||||
"""Format port mappings"""
|
||||
formatted_ports = []
|
||||
for port in ports:
|
||||
formatted_ports.append({
|
||||
'container_port': f"{port.get('PrivatePort', '')}/{port.get('Type', 'tcp')}",
|
||||
'host_ip': port.get('IP', '0.0.0.0'),
|
||||
'host_port': str(port.get('PublicPort', ''))
|
||||
})
|
||||
return formatted_ports
|
||||
|
||||
def _format_mounts(self, mounts: List[Dict]) -> List[Dict]:
|
||||
"""Format volume mounts"""
|
||||
formatted_mounts = []
|
||||
for mount in mounts:
|
||||
formatted_mounts.append({
|
||||
'type': mount.get('Type'),
|
||||
'source': mount.get('Source'),
|
||||
'destination': mount.get('Destination'),
|
||||
'mode': mount.get('Mode')
|
||||
})
|
||||
return formatted_mounts
|
||||
|
||||
def _format_networks(self, networks: Dict) -> List[Dict]:
|
||||
"""Format network information"""
|
||||
formatted_networks = []
|
||||
for network_name, network_config in networks.items():
|
||||
formatted_networks.append({
|
||||
'name': network_name,
|
||||
'ip_address': network_config.get('IPAddress'),
|
||||
'gateway': network_config.get('Gateway'),
|
||||
'mac_address': network_config.get('MacAddress')
|
||||
})
|
||||
return formatted_networks
|
||||
|
||||
def _calculate_uptime(self, container: Dict) -> str:
|
||||
"""Calculate uptime from container info"""
|
||||
status = container.get('State')
|
||||
if status != 'running':
|
||||
return 'Down'
|
||||
|
||||
# For now, return simplified uptime
|
||||
# In a real implementation, you'd parse the Created field
|
||||
return 'Running'
|
||||
|
||||
def get_stacks(self, endpoint_id: int = None) -> List[Dict]:
|
||||
"""Get Docker stacks from Portainer"""
|
||||
if not self.token:
|
||||
return []
|
||||
|
||||
try:
|
||||
if not endpoint_id:
|
||||
endpoints = self.get_endpoints()
|
||||
if endpoints:
|
||||
endpoint_id = endpoints[0]['Id']
|
||||
else:
|
||||
return []
|
||||
|
||||
headers = {'Authorization': f'Bearer {self.token}'}
|
||||
url = f"{self.base_url}/api/endpoints/{endpoint_id}/docker/stacks"
|
||||
|
||||
response = requests.get(url, headers=headers)
|
||||
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
return []
|
||||
except Exception as e:
|
||||
print(f"Error fetching stacks from Portainer: {e}")
|
||||
return []
|
||||
|
||||
def get_volumes(self, endpoint_id: int = None) -> List[Dict]:
|
||||
"""Get Docker volumes from Portainer"""
|
||||
if not self.token:
|
||||
return []
|
||||
|
||||
try:
|
||||
if not endpoint_id:
|
||||
endpoints = self.get_endpoints()
|
||||
if endpoints:
|
||||
endpoint_id = endpoints[0]['Id']
|
||||
else:
|
||||
return []
|
||||
|
||||
headers = {'Authorization': f'Bearer {self.token}'}
|
||||
url = f"{self.base_url}/api/endpoints/{endpoint_id}/docker/volumes"
|
||||
|
||||
response = requests.get(url, headers=headers)
|
||||
|
||||
if response.status_code == 200:
|
||||
volumes_data = response.json()
|
||||
return volumes_data.get('Volumes', [])
|
||||
return []
|
||||
except Exception as e:
|
||||
print(f"Error fetching volumes from Portainer: {e}")
|
||||
return []
|
||||
|
||||
def get_networks(self, endpoint_id: int = None) -> List[Dict]:
|
||||
"""Get Docker networks from Portainer"""
|
||||
if not self.token:
|
||||
return []
|
||||
|
||||
try:
|
||||
if not endpoint_id:
|
||||
endpoints = self.get_endpoints()
|
||||
if endpoints:
|
||||
endpoint_id = endpoints[0]['Id']
|
||||
else:
|
||||
return []
|
||||
|
||||
headers = {'Authorization': f'Bearer {self.token}'}
|
||||
url = f"{self.base_url}/api/endpoints/{endpoint_id}/docker/networks"
|
||||
|
||||
response = requests.get(url, headers=headers)
|
||||
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
return []
|
||||
except Exception as e:
|
||||
print(f"Error fetching networks from Portainer: {e}")
|
||||
return []
|
||||
|
||||
def start_container(self, container_id: str, endpoint_id: int = None) -> bool:
|
||||
"""Start a container"""
|
||||
return self._container_action(container_id, 'start', endpoint_id)
|
||||
|
||||
def stop_container(self, container_id: str, endpoint_id: int = None) -> bool:
|
||||
"""Stop a container"""
|
||||
return self._container_action(container_id, 'stop', endpoint_id)
|
||||
|
||||
def restart_container(self, container_id: str, endpoint_id: int = None) -> bool:
|
||||
"""Restart a container"""
|
||||
return self._container_action(container_id, 'restart', endpoint_id)
|
||||
|
||||
def _container_action(self, container_id: str, action: str, endpoint_id: int = None) -> bool:
|
||||
"""Perform container action"""
|
||||
if not self.token:
|
||||
return False
|
||||
|
||||
try:
|
||||
if not endpoint_id:
|
||||
endpoints = self.get_endpoints()
|
||||
if endpoints:
|
||||
endpoint_id = endpoints[0]['Id']
|
||||
else:
|
||||
return False
|
||||
|
||||
headers = {'Authorization': f'Bearer {self.token}'}
|
||||
url = f"{self.base_url}/api/endpoints/{endpoint_id}/docker/containers/{container_id}/{action}"
|
||||
|
||||
response = requests.post(url, headers=headers)
|
||||
return response.status_code == 204
|
||||
except Exception as e:
|
||||
print(f"Error performing {action} on container: {e}")
|
||||
return False
|
||||
6
requirements.txt
Normal file
6
requirements.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
flask==2.3.3
|
||||
flask-cors==4.0.0
|
||||
docker==6.1.3
|
||||
psutil==5.9.5
|
||||
requests==2.31.0
|
||||
python-dotenv==1.0.0
|
||||
255
setup.sh
Executable file
255
setup.sh
Executable file
@@ -0,0 +1,255 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Docker Dashboard Setup Script
|
||||
# This script sets up and launches the Docker Dashboard as a background process
|
||||
|
||||
set -e
|
||||
|
||||
echo "🚀 Docker Dashboard Setup Script"
|
||||
echo "================================"
|
||||
|
||||
# Get script directory
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Function to print colored output
|
||||
print_status() {
|
||||
echo -e "${GREEN}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# Check if running as root for Docker access
|
||||
check_permissions() {
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
print_warning "This script needs to run with sudo for Docker access"
|
||||
print_warning "Rerunning with sudo..."
|
||||
exec sudo "$0" "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if Docker is running
|
||||
check_docker() {
|
||||
if ! systemctl is-active --quiet docker; then
|
||||
print_warning "Docker service is not running. Starting Docker..."
|
||||
systemctl start docker
|
||||
fi
|
||||
|
||||
if ! docker info >/dev/null 2>&1; then
|
||||
print_error "Docker is not accessible. Please check Docker installation"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_status "Docker is running and accessible"
|
||||
}
|
||||
|
||||
# Install Python dependencies
|
||||
install_dependencies() {
|
||||
print_status "Installing Python dependencies..."
|
||||
|
||||
# Check if pip is available
|
||||
if ! command -v pip3 &> /dev/null; then
|
||||
print_error "pip3 is not installed. Please install pip3 first"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Install requirements
|
||||
if [[ -f "requirements.txt" ]]; then
|
||||
pip3 install -r requirements.txt
|
||||
else
|
||||
pip3 install flask flask-cors docker requests python-dotenv
|
||||
fi
|
||||
|
||||
print_status "Dependencies installed successfully"
|
||||
}
|
||||
|
||||
# Setup environment file
|
||||
setup_environment() {
|
||||
if [[ ! -f ".env" ]]; then
|
||||
print_status "Creating .env file from template..."
|
||||
cp .env.example .env
|
||||
print_status "Please edit .env file with your specific configuration"
|
||||
else
|
||||
print_status ".env file already exists"
|
||||
fi
|
||||
|
||||
# Source the environment file
|
||||
source .env
|
||||
|
||||
# Set defaults if not provided
|
||||
export PORT=${PORT:-5000}
|
||||
export HOST=${HOST:-0.0.0.0}
|
||||
export DEBUG=${DEBUG:-False}
|
||||
export REFRESH_INTERVAL=${REFRESH_INTERVAL:-30}
|
||||
|
||||
print_status "Environment configured"
|
||||
}
|
||||
|
||||
# Initialize database
|
||||
initialize_database() {
|
||||
print_status "Initializing database..."
|
||||
|
||||
# Create database if it doesn't exist
|
||||
if [[ ! -f "dashboard.db" ]]; then
|
||||
python3 -c "
|
||||
import database
|
||||
db = database.DatabaseManager()
|
||||
db.init_db()
|
||||
print('Database initialized successfully')
|
||||
"
|
||||
else
|
||||
print_status "Database already exists"
|
||||
fi
|
||||
}
|
||||
|
||||
# Kill existing processes
|
||||
kill_existing_processes() {
|
||||
print_status "Checking for existing processes..."
|
||||
|
||||
# Kill existing Python processes on the port
|
||||
PID=$(lsof -t -i :${PORT} 2>/dev/null || echo "")
|
||||
if [[ -n "$PID" ]]; then
|
||||
print_warning "Killing existing process on port ${PORT} (PID: $PID)"
|
||||
kill -TERM "$PID" 2>/dev/null || true
|
||||
sleep 2
|
||||
kill -KILL "$PID" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
# Launch application
|
||||
launch_application() {
|
||||
print_status "Launching Docker Dashboard..."
|
||||
|
||||
# Create logs directory
|
||||
mkdir -p logs
|
||||
|
||||
# Launch the application in background
|
||||
nohup python3 api.py > logs/dashboard.log 2>&1 &
|
||||
|
||||
# Get the process ID
|
||||
APP_PID=$!
|
||||
|
||||
# Wait a moment for the app to start
|
||||
sleep 3
|
||||
|
||||
# Check if the process is still running
|
||||
if kill -0 "$APP_PID" 2>/dev/null; then
|
||||
print_status "Application started successfully (PID: $APP_PID)"
|
||||
print_status "Dashboard available at: http://localhost:${PORT}"
|
||||
print_status "Logs available at: logs/dashboard.log"
|
||||
|
||||
# Save PID to file
|
||||
echo "$APP_PID" > dashboard.pid
|
||||
|
||||
print_status ""
|
||||
print_status "To stop the application, run:"
|
||||
print_status " sudo ./stop.sh"
|
||||
print_status ""
|
||||
print_status "To view logs:"
|
||||
print_status " tail -f logs/dashboard.log"
|
||||
else
|
||||
print_error "Failed to start application. Check logs/dashboard.log for details"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Create stop script
|
||||
create_stop_script() {
|
||||
cat > stop.sh << 'EOF'
|
||||
#!/bin/bash
|
||||
|
||||
# Stop Docker Dashboard Script
|
||||
|
||||
if [[ -f "dashboard.pid" ]]; then
|
||||
PID=$(cat dashboard.pid)
|
||||
if kill -0 "$PID" 2>/dev/null; then
|
||||
echo "Stopping Docker Dashboard (PID: $PID)..."
|
||||
kill -TERM "$PID"
|
||||
sleep 2
|
||||
|
||||
if kill -0 "$PID" 2>/dev/null; then
|
||||
echo "Force stopping..."
|
||||
kill -KILL "$PID"
|
||||
fi
|
||||
|
||||
rm dashboard.pid
|
||||
echo "Application stopped successfully"
|
||||
else
|
||||
echo "Application is not running"
|
||||
rm -f dashboard.pid
|
||||
fi
|
||||
else
|
||||
echo "PID file not found. Attempting to kill by port..."
|
||||
PID=$(lsof -t -i :5000 2>/dev/null || echo "")
|
||||
if [[ -n "$PID" ]]; then
|
||||
echo "Stopping process on port 5000 (PID: $PID)..."
|
||||
kill -TERM "$PID"
|
||||
sleep 2
|
||||
|
||||
if kill -0 "$PID" 2>/dev/null; then
|
||||
echo "Force stopping..."
|
||||
kill -KILL "$PID"
|
||||
fi
|
||||
|
||||
echo "Application stopped successfully"
|
||||
else
|
||||
echo "No application found running on port 5000"
|
||||
fi
|
||||
fi
|
||||
EOF
|
||||
|
||||
chmod +x stop.sh
|
||||
print_status "Stop script created: stop.sh"
|
||||
}
|
||||
|
||||
# Main execution
|
||||
main() {
|
||||
print_status "Starting Docker Dashboard setup..."
|
||||
|
||||
check_permissions
|
||||
check_docker
|
||||
install_dependencies
|
||||
setup_environment
|
||||
initialize_database
|
||||
kill_existing_processes
|
||||
create_stop_script
|
||||
launch_application
|
||||
|
||||
print_status ""
|
||||
print_status "🎉 Docker Dashboard setup complete!"
|
||||
print_status "Dashboard is running at: http://localhost:${PORT}"
|
||||
print_status ""
|
||||
print_status "Next steps:"
|
||||
print_status "1. Open your browser to http://localhost:${PORT}"
|
||||
print_status "2. Click 'Sync Apps' to load your Docker containers"
|
||||
print_status "3. Edit tiles by clicking the 'Edit' button on each tile"
|
||||
print_status "4. Use ./stop.sh to stop the application"
|
||||
}
|
||||
|
||||
# Handle command line arguments
|
||||
case "${1:-}" in
|
||||
"--help"|"-h")
|
||||
echo "Usage: $0 [--help]"
|
||||
echo ""
|
||||
echo "This script sets up and launches the Docker Dashboard"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --help, -h Show this help message"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
main
|
||||
;;
|
||||
esac
|
||||
379
template.html
Normal file
379
template.html
Normal file
@@ -0,0 +1,379 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Home Server Dashboard</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<style>
|
||||
.card-hover {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.card-hover:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
.status-indicator {
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
@keyframes pulse {
|
||||
0% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-900 min-h-screen text-gray-100">
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<!-- Header -->
|
||||
<header class="mb-8">
|
||||
<h1 class="text-4xl font-bold text-white mb-2">Home Server Dashboard</h1>
|
||||
<p class="text-gray-400">Centralized access to your self-hosted applications</p>
|
||||
<div class="flex items-center space-x-4 mt-4">
|
||||
<button onclick="syncApplications()" class="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-lg text-sm">
|
||||
<i class="fas fa-sync-alt mr-2"></i>Sync Apps
|
||||
</button>
|
||||
<button onclick="refreshData()" class="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-lg text-sm">
|
||||
<i class="fas fa-refresh mr-2"></i>Refresh
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<!-- Server Info Card -->
|
||||
<div class="bg-gray-800 rounded-xl shadow-md overflow-hidden col-span-1 border border-gray-700">
|
||||
<div class="bg-indigo-600 p-4">
|
||||
<h2 class="text-xl font-semibold text-white">Server Information</h2>
|
||||
</div>
|
||||
<div class="p-6 space-y-4">
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<span class="text-gray-300">CPU Usage</span>
|
||||
<span id="cpu-usage" class="text-white font-semibold">--</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-700 rounded-full h-2">
|
||||
<div id="cpu-bar" class="bg-indigo-500 h-2 rounded-full" style="width: 0%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<span class="text-gray-300">Memory Usage</span>
|
||||
<span id="memory-usage" class="text-white font-semibold">--</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-700 rounded-full h-2">
|
||||
<div id="memory-bar" class="bg-green-500 h-2 rounded-full" style="width: 0%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<span class="text-gray-300">Disk Usage</span>
|
||||
<span id="disk-usage" class="text-white font-semibold">--</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-700 rounded-full h-2">
|
||||
<div id="disk-bar" class="bg-yellow-500 h-2 rounded-full" style="width: 0%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-4 border-t border-gray-700">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-300">Uptime</span>
|
||||
<span id="server-uptime" class="text-white">--</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Applications Grid -->
|
||||
<div class="bg-gray-800 rounded-xl shadow-md overflow-hidden col-span-1 lg:col-span-2 border border-gray-700">
|
||||
<div class="bg-indigo-600 p-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="text-xl font-semibold text-white">Hosted Applications</h2>
|
||||
<div class="flex items-center">
|
||||
<div id="refresh-indicator" class="hidden">
|
||||
<i class="fas fa-spinner fa-spin text-white mr-2"></i>
|
||||
</div>
|
||||
<span id="app-count" class="text-sm text-gray-300">0 apps</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div id="container-grid" class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="text-center py-8">
|
||||
<i class="fas fa-spinner fa-spin text-2xl text-gray-400 mb-2"></i>
|
||||
<p class="text-gray-400">Loading applications...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Activity -->
|
||||
<div class="mt-8 bg-gray-800 rounded-xl shadow-md overflow-hidden border border-gray-700">
|
||||
<div class="bg-indigo-600 p-4">
|
||||
<h2 class="text-xl font-semibold text-white">Recent Activity</h2>
|
||||
</div>
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="flex items-start">
|
||||
<div class="flex-shrink-0 h-10 w-10 rounded-full bg-green-100 flex items-center justify-center mr-4">
|
||||
<i class="fas fa-check text-green-600"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-white">Dashboard initialized</p>
|
||||
<p class="text-xs text-gray-400">Just now - System ready</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="mt-8 text-center text-sm text-gray-400">
|
||||
<p>Home Server Dashboard v1.0.0 | Last updated: <span id="last-updated">Just now</span></p>
|
||||
<p class="mt-1">© 2023 Home Server | <a href="#" class="text-indigo-600 hover:text-indigo-400">Server Settings</a></p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// API endpoints
|
||||
const API_BASE = window.location.origin;
|
||||
const REFRESH_INTERVAL = 30; // seconds
|
||||
|
||||
// Show notification
|
||||
function showNotification(message, type = 'info') {
|
||||
const colors = {
|
||||
success: 'bg-green-500',
|
||||
error: 'bg-red-500',
|
||||
warning: 'bg-yellow-500',
|
||||
info: 'bg-blue-500'
|
||||
};
|
||||
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `fixed top-4 right-4 ${colors[type]} text-white px-6 py-3 rounded-lg shadow-lg z-50`;
|
||||
notification.textContent = message;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
notification.remove();
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Load system information
|
||||
async function loadSystemInfo() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/system`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.cpu_percent !== undefined) {
|
||||
const cpuPercent = Math.round(data.cpu_percent);
|
||||
document.getElementById('cpu-usage').textContent = `${cpuPercent}%`;
|
||||
document.getElementById('cpu-bar').style.width = `${cpuPercent}%`;
|
||||
}
|
||||
|
||||
if (data.memory_percent !== undefined) {
|
||||
const memoryPercent = Math.round(data.memory_percent);
|
||||
document.getElementById('memory-usage').textContent = `${memoryPercent}%`;
|
||||
document.getElementById('memory-bar').style.width = `${memoryPercent}%`;
|
||||
}
|
||||
|
||||
if (data.disk_percent !== undefined) {
|
||||
const diskPercent = Math.round(data.disk_percent);
|
||||
document.getElementById('disk-usage').textContent = `${diskPercent}%`;
|
||||
document.getElementById('disk-bar').style.width = `${diskPercent}%`;
|
||||
}
|
||||
|
||||
if (data.uptime) {
|
||||
document.getElementById('server-uptime').textContent = data.uptime;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading system info:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Create application card
|
||||
function createAppCard(app) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'bg-gray-700 rounded-lg p-4 border border-gray-600 card-hover';
|
||||
|
||||
const statusColor = app.status === 'running' ? 'text-green-400' : 'text-red-400';
|
||||
const statusIndicator = app.status === 'running' ? 'bg-green-500' : 'bg-red-500';
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="flex justify-between items-start mb-3">
|
||||
<div class="flex items-center">
|
||||
<div class="h-10 w-10 rounded-md bg-${app.color || 'gray'}-600 flex items-center justify-center mr-3">
|
||||
<i class="${app.icon || 'fas fa-cube'} text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-white">${app.display_name || app.container_name}</h3>
|
||||
<p class="text-xs text-gray-400">${app.description || app.image}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="w-2 h-2 rounded-full ${statusIndicator} ${app.status === 'running' ? 'status-indicator' : ''}"></div>
|
||||
<span class="text-xs ${statusColor}">${app.status}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-gray-400">Uptime:</span>
|
||||
<span class="text-white">${app.uptime || 'N/A'}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-2">
|
||||
${app.url ? `<a href="${app.url}" target="_blank" class="flex-1 bg-indigo-600 hover:bg-indigo-700 text-white text-sm py-1 px-3 rounded text-center">
|
||||
<i class="fas fa-external-link-alt mr-1"></i>Open
|
||||
</a>` : ''}
|
||||
|
||||
<button onclick="editApplication('${app.container_id}')" class="bg-gray-600 hover:bg-gray-500 text-white text-sm py-1 px-3 rounded">
|
||||
<i class="fas fa-edit mr-1"></i>Edit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
// Load applications from database
|
||||
async function loadApplications() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/applications`);
|
||||
const data = await response.json();
|
||||
|
||||
const containerGrid = document.getElementById('container-grid');
|
||||
containerGrid.innerHTML = '';
|
||||
|
||||
if (data.applications && data.applications.length > 0) {
|
||||
document.getElementById('app-count').textContent = `${data.applications.length} apps`;
|
||||
|
||||
data.applications.forEach(app => {
|
||||
const card = createAppCard(app);
|
||||
containerGrid.appendChild(card);
|
||||
});
|
||||
} else {
|
||||
containerGrid.innerHTML = '<div class="col-span-2 text-center py-8"><i class="fas fa-inbox text-4xl text-gray-400 mb-2"></i><p class="text-gray-400">No applications found</p><button onclick="syncApplications()" class="mt-4 bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded">Sync Applications</button></div>';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading applications:', error);
|
||||
showNotification('Error loading applications', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Edit application
|
||||
async function editApplication(containerId) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/applications/${containerId}`);
|
||||
const app = await response.json();
|
||||
|
||||
if (app.error) {
|
||||
showNotification('Application not found', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const newName = prompt('Enter display name:', app.display_name || app.container_name);
|
||||
const newIcon = prompt('Enter icon class (e.g., fas fa-cube):', app.icon || 'fas fa-cube');
|
||||
const newColor = prompt('Enter color (e.g., blue, green, red):', app.color || 'gray');
|
||||
const newDescription = prompt('Enter description:', app.description || '');
|
||||
const newUrl = prompt('Enter URL:', app.url || '');
|
||||
|
||||
if (newName && newIcon && newColor) {
|
||||
const updateResponse = await fetch(`${API_BASE}/api/applications/${containerId}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
display_name: newName,
|
||||
icon: newIcon,
|
||||
color: newColor,
|
||||
description: newDescription,
|
||||
url: newUrl
|
||||
})
|
||||
});
|
||||
|
||||
const result = await updateResponse.json();
|
||||
if (result.success) {
|
||||
showNotification('Application updated successfully', 'success');
|
||||
loadApplications();
|
||||
} else {
|
||||
showNotification('Failed to update application', 'error');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error editing application:', error);
|
||||
showNotification('Error editing application', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Sync applications with containers
|
||||
async function syncApplications() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/applications/sync`, {
|
||||
method: 'POST'
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showNotification('Applications synced successfully', 'success');
|
||||
loadApplications();
|
||||
} else {
|
||||
showNotification('Failed to sync applications', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error syncing applications:', error);
|
||||
showNotification('Error syncing applications', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh all data
|
||||
async function refreshData() {
|
||||
const indicator = document.getElementById('refresh-indicator');
|
||||
indicator.classList.remove('hidden');
|
||||
|
||||
await loadSystemInfo();
|
||||
await loadApplications();
|
||||
|
||||
indicator.classList.add('hidden');
|
||||
showNotification('Data refreshed', 'success');
|
||||
}
|
||||
|
||||
// Update timestamps
|
||||
function updateTimestamps() {
|
||||
const now = new Date();
|
||||
document.getElementById('last-updated').textContent = now.toLocaleString();
|
||||
}
|
||||
|
||||
// Initialize dashboard
|
||||
async function initDashboard() {
|
||||
await loadSystemInfo();
|
||||
await loadApplications();
|
||||
updateTimestamps();
|
||||
showNotification('Dashboard loaded successfully', 'success');
|
||||
}
|
||||
|
||||
// Start dashboard
|
||||
initDashboard();
|
||||
|
||||
// Auto refresh
|
||||
setInterval(() => {
|
||||
loadSystemInfo();
|
||||
loadApplications();
|
||||
updateTimestamps();
|
||||
}, REFRESH_INTERVAL * 1000);
|
||||
|
||||
// Handle connection events
|
||||
window.addEventListener('online', () => {
|
||||
showNotification('Connection restored', 'success');
|
||||
initDashboard();
|
||||
});
|
||||
|
||||
window.addEventListener('offline', () => {
|
||||
showNotification('Connection lost', 'error');
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user