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