mirror of
https://github.com/x1xhlol/system-prompts-and-models-of-ai-tools.git
synced 2026-01-30 13:54:18 -05:00
Add comprehensive Storage Device Performance Dashboard
This commit introduces a complete web-based dashboard for monitoring storage device performance metrics in real-time. Features: - Real-time monitoring with auto-refresh every 5 seconds - Comprehensive metrics collection (disk usage, I/O stats, IOPS, SMART data) - Interactive visualizations using Chart.js - Modern dark-themed responsive UI - Python Flask backend with REST API - System information and uptime tracking - Historical performance trend charts Tech Stack: - Backend: Python 3.8+, Flask, psutil, pySMART - Frontend: HTML5, CSS3, JavaScript ES6+, Chart.js - Cross-platform support with startup scripts for Linux/Windows The dashboard provides system administrators and monitoring enthusiasts with a powerful tool to track storage performance, identify bottlenecks, and monitor disk health. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
dd8601ce16
commit
563a800fad
47
Storage Dashboard/.gitignore
vendored
Normal file
47
Storage Dashboard/.gitignore
vendored
Normal file
@ -0,0 +1,47 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
pip-wheel-metadata/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# Virtual Environment
|
||||
venv/
|
||||
ENV/
|
||||
env/
|
||||
.venv
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
334
Storage Dashboard/README.md
Normal file
334
Storage Dashboard/README.md
Normal file
@ -0,0 +1,334 @@
|
||||
# Storage Device Performance Dashboard
|
||||
|
||||
A comprehensive, real-time web-based dashboard for monitoring storage device performance metrics. Track disk usage, I/O performance, IOPS, SMART health data, and more with beautiful visualizations.
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
## Features
|
||||
|
||||
### Real-time Monitoring
|
||||
- **Auto-refresh every 5 seconds** - Stay updated with the latest metrics
|
||||
- **Live performance charts** - Visualize trends over time
|
||||
- **Instant metrics** - See current performance at a glance
|
||||
|
||||
### Comprehensive Metrics
|
||||
|
||||
#### 1. System Information
|
||||
- Platform and OS details
|
||||
- System hostname
|
||||
- Uptime statistics
|
||||
- Boot time
|
||||
|
||||
#### 2. Disk Partitions & Usage
|
||||
- All mounted partitions
|
||||
- Total, used, and free space
|
||||
- Usage percentage with visual progress bars
|
||||
- File system type
|
||||
- Mount points
|
||||
|
||||
#### 3. I/O Performance
|
||||
- Read/Write speeds (MB/s)
|
||||
- IOPS (Input/Output Operations Per Second)
|
||||
- Total data read/written
|
||||
- Real-time speed calculations
|
||||
- Historical trend charts
|
||||
|
||||
#### 4. SMART Health Data
|
||||
- Disk health status
|
||||
- Temperature monitoring
|
||||
- Power-on hours
|
||||
- Power cycle count
|
||||
- Model and serial information
|
||||
- *Note: Requires root/admin privileges*
|
||||
|
||||
#### 5. Visual Analytics
|
||||
- Interactive bar charts for current performance
|
||||
- Line charts for historical trends
|
||||
- Color-coded health indicators
|
||||
- Responsive data tables
|
||||
|
||||
## Technology Stack
|
||||
|
||||
### Backend
|
||||
- **Python 3.8+** - Core language
|
||||
- **Flask 3.0.0** - Web framework
|
||||
- **psutil 5.9.6** - System metrics collection
|
||||
- **pySMART 1.3.0** - SMART data retrieval
|
||||
- **Flask-CORS 4.0.0** - Cross-origin resource sharing
|
||||
|
||||
### Frontend
|
||||
- **HTML5/CSS3** - Structure and styling
|
||||
- **JavaScript (ES6+)** - Interactive functionality
|
||||
- **Chart.js 4.4.0** - Data visualization
|
||||
- **Modern responsive design** - Works on all devices
|
||||
|
||||
## Installation
|
||||
|
||||
### Prerequisites
|
||||
- Python 3.8 or higher
|
||||
- pip (Python package manager)
|
||||
- Modern web browser (Chrome, Firefox, Safari, Edge)
|
||||
|
||||
### Step 1: Clone or Navigate to the Directory
|
||||
```bash
|
||||
cd "Storage Dashboard"
|
||||
```
|
||||
|
||||
### Step 2: Install Python Dependencies
|
||||
```bash
|
||||
cd backend
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### Step 3: Start the Backend Server
|
||||
```bash
|
||||
python app.py
|
||||
```
|
||||
|
||||
The server will start on `http://localhost:5000`
|
||||
|
||||
### Step 4: Access the Dashboard
|
||||
Open your web browser and navigate to:
|
||||
```
|
||||
http://localhost:5000
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Dashboard Interface
|
||||
|
||||
1. **System Information Panel**
|
||||
- View OS details and system uptime
|
||||
- Located at the top of the dashboard
|
||||
|
||||
2. **Disk Partitions Section**
|
||||
- Each partition displayed as a card
|
||||
- Color-coded progress bars:
|
||||
- Blue: < 75% usage (healthy)
|
||||
- Orange: 75-90% usage (warning)
|
||||
- Red: > 90% usage (critical)
|
||||
|
||||
3. **I/O Performance Charts**
|
||||
- Bar charts show current read/write speeds and IOPS
|
||||
- View all disks simultaneously
|
||||
|
||||
4. **Detailed Statistics Table**
|
||||
- Comprehensive I/O metrics in tabular format
|
||||
- Sortable and easy to read
|
||||
|
||||
5. **SMART Health Section**
|
||||
- Individual cards for each storage device
|
||||
- Health status badges (Good/Warning/Bad)
|
||||
- Detailed device information
|
||||
|
||||
6. **Historical Performance**
|
||||
- Line charts track performance over time
|
||||
- Shows trends for the last 20 data points
|
||||
- Auto-updates every 5 seconds
|
||||
|
||||
### Manual Refresh
|
||||
Click the "Refresh Now" button in the header to manually update all metrics.
|
||||
|
||||
## API Endpoints
|
||||
|
||||
The backend provides the following REST API endpoints:
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
|----------|--------|-------------|
|
||||
| `/api/metrics` | GET | Get all metrics in one call |
|
||||
| `/api/partitions` | GET | Get disk partition information |
|
||||
| `/api/io-stats` | GET | Get I/O statistics |
|
||||
| `/api/smart` | GET | Get SMART health data |
|
||||
| `/api/system-info` | GET | Get system information |
|
||||
|
||||
### Example API Response
|
||||
|
||||
```json
|
||||
{
|
||||
"timestamp": "2024-01-15T10:30:00",
|
||||
"system_info": {
|
||||
"platform": "Linux",
|
||||
"hostname": "server-01",
|
||||
"uptime_days": 15,
|
||||
"uptime_hours": 6,
|
||||
"uptime_minutes": 23
|
||||
},
|
||||
"partitions": [
|
||||
{
|
||||
"device": "/dev/sda1",
|
||||
"mountpoint": "/",
|
||||
"fstype": "ext4",
|
||||
"total_gb": 500.0,
|
||||
"used_gb": 250.0,
|
||||
"free_gb": 250.0,
|
||||
"percent": 50.0
|
||||
}
|
||||
],
|
||||
"io_stats": [
|
||||
{
|
||||
"disk": "sda",
|
||||
"read_speed_mbps": 45.2,
|
||||
"write_speed_mbps": 23.1,
|
||||
"iops_read": 1200,
|
||||
"iops_write": 800
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Change Refresh Interval
|
||||
Edit `frontend/dashboard.js`:
|
||||
```javascript
|
||||
const REFRESH_INTERVAL = 5000; // milliseconds (5 seconds)
|
||||
```
|
||||
|
||||
### Change Historical Data Length
|
||||
Edit `frontend/dashboard.js`:
|
||||
```javascript
|
||||
const HISTORY_LENGTH = 20; // number of data points
|
||||
```
|
||||
|
||||
### Change Server Port
|
||||
Edit `backend/app.py`:
|
||||
```python
|
||||
app.run(debug=True, host='0.0.0.0', port=5000)
|
||||
```
|
||||
|
||||
## SMART Data Collection
|
||||
|
||||
SMART data provides valuable insights into disk health but requires elevated privileges:
|
||||
|
||||
### Linux
|
||||
```bash
|
||||
sudo python app.py
|
||||
```
|
||||
|
||||
### Windows
|
||||
Run Command Prompt or PowerShell as Administrator:
|
||||
```bash
|
||||
python app.py
|
||||
```
|
||||
|
||||
### macOS
|
||||
```bash
|
||||
sudo python app.py
|
||||
```
|
||||
|
||||
If SMART data is unavailable, the dashboard will display a message indicating that root/admin privileges are required.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Backend Won't Start
|
||||
- Ensure Python 3.8+ is installed: `python --version`
|
||||
- Verify all dependencies are installed: `pip install -r requirements.txt`
|
||||
- Check if port 5000 is available
|
||||
|
||||
### No Data Displayed
|
||||
- Ensure the backend server is running
|
||||
- Check browser console for errors (F12)
|
||||
- Verify the API URL in `dashboard.js` matches your backend
|
||||
|
||||
### SMART Data Not Available
|
||||
- SMART data requires root/admin privileges
|
||||
- Run the backend with elevated permissions
|
||||
- Some virtual machines may not expose SMART data
|
||||
|
||||
### Charts Not Rendering
|
||||
- Ensure you have internet connectivity (Chart.js loads from CDN)
|
||||
- Check browser console for JavaScript errors
|
||||
- Try clearing browser cache
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- The dashboard runs on `0.0.0.0` (all interfaces) by default
|
||||
- For production use, configure proper firewall rules
|
||||
- Consider using HTTPS for secure connections
|
||||
- Implement authentication for sensitive environments
|
||||
|
||||
## Development
|
||||
|
||||
### Project Structure
|
||||
```
|
||||
Storage Dashboard/
|
||||
├── backend/
|
||||
│ ├── app.py # Flask application
|
||||
│ ├── metrics_collector.py # Metrics collection logic
|
||||
│ └── requirements.txt # Python dependencies
|
||||
└── frontend/
|
||||
├── index.html # Main dashboard page
|
||||
├── styles.css # Styling
|
||||
└── dashboard.js # Frontend logic and charts
|
||||
```
|
||||
|
||||
### Extending the Dashboard
|
||||
|
||||
#### Add New Metrics
|
||||
1. Add collection method in `metrics_collector.py`
|
||||
2. Create API endpoint in `app.py`
|
||||
3. Update frontend to display the new data
|
||||
|
||||
#### Customize Appearance
|
||||
- Modify `styles.css` to change colors and layout
|
||||
- Update CSS variables in `:root` for theme changes
|
||||
|
||||
## Performance
|
||||
|
||||
- **Low overhead** - Minimal impact on system performance
|
||||
- **Efficient polling** - Only collects data when needed
|
||||
- **Optimized charts** - Smooth animations and updates
|
||||
- **Responsive design** - Fast load times
|
||||
|
||||
## Browser Compatibility
|
||||
|
||||
- Chrome/Edge 90+
|
||||
- Firefox 88+
|
||||
- Safari 14+
|
||||
- Opera 76+
|
||||
|
||||
## License
|
||||
|
||||
This project is open source and available for personal and commercial use.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Feel free to:
|
||||
- Report bugs
|
||||
- Suggest new features
|
||||
- Submit pull requests
|
||||
- Improve documentation
|
||||
|
||||
## Support
|
||||
|
||||
For issues or questions:
|
||||
1. Check the Troubleshooting section
|
||||
2. Review the API documentation
|
||||
3. Examine browser console logs
|
||||
4. Check backend server logs
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Planned features:
|
||||
- [ ] Export metrics to CSV/JSON
|
||||
- [ ] Email/SMS alerts for critical conditions
|
||||
- [ ] Multi-server monitoring
|
||||
- [ ] Historical data persistence
|
||||
- [ ] Custom metric thresholds
|
||||
- [ ] Network storage support
|
||||
- [ ] Docker containerization
|
||||
|
||||
## Screenshots
|
||||
|
||||
The dashboard features:
|
||||
- Dark theme optimized for monitoring
|
||||
- Color-coded status indicators
|
||||
- Real-time updating charts
|
||||
- Responsive grid layouts
|
||||
- Professional UI design
|
||||
|
||||
---
|
||||
|
||||
**Built with ❤️ for system administrators and monitoring enthusiasts**
|
||||
70
Storage Dashboard/backend/app.py
Normal file
70
Storage Dashboard/backend/app.py
Normal file
@ -0,0 +1,70 @@
|
||||
from flask import Flask, jsonify, send_from_directory
|
||||
from flask_cors import CORS
|
||||
from metrics_collector import StorageMetricsCollector
|
||||
import os
|
||||
|
||||
app = Flask(__name__, static_folder='../frontend')
|
||||
CORS(app)
|
||||
|
||||
# Initialize metrics collector
|
||||
collector = StorageMetricsCollector()
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
"""Serve the main dashboard page"""
|
||||
return send_from_directory('../frontend', 'index.html')
|
||||
|
||||
@app.route('/api/metrics')
|
||||
def get_metrics():
|
||||
"""Get all storage metrics"""
|
||||
try:
|
||||
metrics = collector.get_all_metrics()
|
||||
return jsonify(metrics)
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/partitions')
|
||||
def get_partitions():
|
||||
"""Get disk partition information"""
|
||||
try:
|
||||
partitions = collector.get_disk_partitions()
|
||||
return jsonify(partitions)
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/io-stats')
|
||||
def get_io_stats():
|
||||
"""Get I/O statistics"""
|
||||
try:
|
||||
io_stats = collector.get_io_stats()
|
||||
return jsonify(io_stats)
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/smart')
|
||||
def get_smart():
|
||||
"""Get SMART data for disks"""
|
||||
try:
|
||||
smart_data = collector.get_smart_data()
|
||||
return jsonify(smart_data)
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/system-info')
|
||||
def get_system_info():
|
||||
"""Get system information"""
|
||||
try:
|
||||
system_info = collector.get_system_info()
|
||||
return jsonify(system_info)
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/<path:path>')
|
||||
def serve_static(path):
|
||||
"""Serve static files"""
|
||||
return send_from_directory('../frontend', path)
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("Starting Storage Dashboard Server...")
|
||||
print("Access the dashboard at: http://localhost:5000")
|
||||
app.run(debug=True, host='0.0.0.0', port=5000)
|
||||
149
Storage Dashboard/backend/metrics_collector.py
Normal file
149
Storage Dashboard/backend/metrics_collector.py
Normal file
@ -0,0 +1,149 @@
|
||||
import psutil
|
||||
import time
|
||||
import platform
|
||||
from datetime import datetime
|
||||
|
||||
class StorageMetricsCollector:
|
||||
"""Collects comprehensive storage device metrics"""
|
||||
|
||||
def __init__(self):
|
||||
self.previous_io = {}
|
||||
self.last_update = time.time()
|
||||
|
||||
def get_disk_partitions(self):
|
||||
"""Get all disk partitions with their details"""
|
||||
partitions = []
|
||||
for partition in psutil.disk_partitions(all=False):
|
||||
try:
|
||||
usage = psutil.disk_usage(partition.mountpoint)
|
||||
partitions.append({
|
||||
'device': partition.device,
|
||||
'mountpoint': partition.mountpoint,
|
||||
'fstype': partition.fstype,
|
||||
'total': usage.total,
|
||||
'used': usage.used,
|
||||
'free': usage.free,
|
||||
'percent': usage.percent,
|
||||
'total_gb': round(usage.total / (1024**3), 2),
|
||||
'used_gb': round(usage.used / (1024**3), 2),
|
||||
'free_gb': round(usage.free / (1024**3), 2)
|
||||
})
|
||||
except PermissionError:
|
||||
continue
|
||||
return partitions
|
||||
|
||||
def get_io_stats(self):
|
||||
"""Get I/O statistics for all disks"""
|
||||
current_time = time.time()
|
||||
time_delta = current_time - self.last_update
|
||||
|
||||
io_counters = psutil.disk_io_counters(perdisk=True)
|
||||
stats = []
|
||||
|
||||
for disk_name, counter in io_counters.items():
|
||||
disk_stats = {
|
||||
'disk': disk_name,
|
||||
'read_count': counter.read_count,
|
||||
'write_count': counter.write_count,
|
||||
'read_bytes': counter.read_bytes,
|
||||
'write_bytes': counter.write_bytes,
|
||||
'read_mb': round(counter.read_bytes / (1024**2), 2),
|
||||
'write_mb': round(counter.write_bytes / (1024**2), 2),
|
||||
'read_time': counter.read_time,
|
||||
'write_time': counter.write_time,
|
||||
}
|
||||
|
||||
# Calculate rates if we have previous data
|
||||
if disk_name in self.previous_io and time_delta > 0:
|
||||
prev = self.previous_io[disk_name]
|
||||
disk_stats['read_speed_mbps'] = round(
|
||||
(counter.read_bytes - prev['read_bytes']) / (1024**2) / time_delta, 2
|
||||
)
|
||||
disk_stats['write_speed_mbps'] = round(
|
||||
(counter.write_bytes - prev['write_bytes']) / (1024**2) / time_delta, 2
|
||||
)
|
||||
disk_stats['iops_read'] = round(
|
||||
(counter.read_count - prev['read_count']) / time_delta, 2
|
||||
)
|
||||
disk_stats['iops_write'] = round(
|
||||
(counter.write_count - prev['write_count']) / time_delta, 2
|
||||
)
|
||||
else:
|
||||
disk_stats['read_speed_mbps'] = 0
|
||||
disk_stats['write_speed_mbps'] = 0
|
||||
disk_stats['iops_read'] = 0
|
||||
disk_stats['iops_write'] = 0
|
||||
|
||||
self.previous_io[disk_name] = {
|
||||
'read_bytes': counter.read_bytes,
|
||||
'write_bytes': counter.write_bytes,
|
||||
'read_count': counter.read_count,
|
||||
'write_count': counter.write_count
|
||||
}
|
||||
|
||||
stats.append(disk_stats)
|
||||
|
||||
self.last_update = current_time
|
||||
return stats
|
||||
|
||||
def get_smart_data(self):
|
||||
"""Get SMART data for disks (requires root/admin privileges)"""
|
||||
smart_data = []
|
||||
try:
|
||||
from pySMART import DeviceList
|
||||
devlist = DeviceList()
|
||||
for device in devlist.devices:
|
||||
if device:
|
||||
smart_info = {
|
||||
'name': device.name,
|
||||
'model': device.model,
|
||||
'serial': device.serial,
|
||||
'capacity': device.capacity,
|
||||
'temperature': device.temperature,
|
||||
'health': device.assessment,
|
||||
'power_on_hours': None,
|
||||
'power_cycle_count': None
|
||||
}
|
||||
|
||||
# Extract specific SMART attributes
|
||||
if device.attributes:
|
||||
for attr in device.attributes:
|
||||
if attr and hasattr(attr, 'name'):
|
||||
if 'Power_On_Hours' in attr.name:
|
||||
smart_info['power_on_hours'] = attr.raw
|
||||
elif 'Power_Cycle_Count' in attr.name:
|
||||
smart_info['power_cycle_count'] = attr.raw
|
||||
|
||||
smart_data.append(smart_info)
|
||||
except Exception as e:
|
||||
# SMART data requires elevated privileges, return empty if not available
|
||||
return {'error': str(e), 'message': 'SMART data requires root/admin privileges'}
|
||||
|
||||
return smart_data
|
||||
|
||||
def get_system_info(self):
|
||||
"""Get general system information"""
|
||||
boot_time = datetime.fromtimestamp(psutil.boot_time())
|
||||
uptime = datetime.now() - boot_time
|
||||
|
||||
return {
|
||||
'platform': platform.system(),
|
||||
'platform_release': platform.release(),
|
||||
'platform_version': platform.version(),
|
||||
'architecture': platform.machine(),
|
||||
'hostname': platform.node(),
|
||||
'boot_time': boot_time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
'uptime_days': uptime.days,
|
||||
'uptime_hours': uptime.seconds // 3600,
|
||||
'uptime_minutes': (uptime.seconds % 3600) // 60
|
||||
}
|
||||
|
||||
def get_all_metrics(self):
|
||||
"""Get all storage metrics in one call"""
|
||||
return {
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'system_info': self.get_system_info(),
|
||||
'partitions': self.get_disk_partitions(),
|
||||
'io_stats': self.get_io_stats(),
|
||||
'smart_data': self.get_smart_data()
|
||||
}
|
||||
4
Storage Dashboard/backend/requirements.txt
Normal file
4
Storage Dashboard/backend/requirements.txt
Normal file
@ -0,0 +1,4 @@
|
||||
flask==3.0.0
|
||||
flask-cors==4.0.0
|
||||
psutil==5.9.6
|
||||
pySMART==1.3.0
|
||||
446
Storage Dashboard/frontend/dashboard.js
Normal file
446
Storage Dashboard/frontend/dashboard.js
Normal file
@ -0,0 +1,446 @@
|
||||
// Configuration
|
||||
const API_BASE_URL = 'http://localhost:5000/api';
|
||||
const REFRESH_INTERVAL = 5000; // 5 seconds
|
||||
const HISTORY_LENGTH = 20; // Keep last 20 data points
|
||||
|
||||
// Global state
|
||||
let charts = {};
|
||||
let historicalData = {
|
||||
labels: [],
|
||||
readSpeeds: {},
|
||||
writeSpeeds: {},
|
||||
readIOPS: {},
|
||||
writeIOPS: {}
|
||||
};
|
||||
|
||||
// Initialize the dashboard
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initializeCharts();
|
||||
fetchAllData();
|
||||
startAutoRefresh();
|
||||
|
||||
// Manual refresh button
|
||||
document.getElementById('refresh-btn').addEventListener('click', () => {
|
||||
fetchAllData();
|
||||
});
|
||||
});
|
||||
|
||||
// Initialize all charts
|
||||
function initializeCharts() {
|
||||
const chartConfig = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
labels: {
|
||||
color: '#e2e8f0'
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
color: '#94a3b8'
|
||||
},
|
||||
grid: {
|
||||
color: '#334155'
|
||||
}
|
||||
},
|
||||
x: {
|
||||
ticks: {
|
||||
color: '#94a3b8'
|
||||
},
|
||||
grid: {
|
||||
color: '#334155'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Speed Chart
|
||||
charts.speed = new Chart(document.getElementById('speed-chart'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [
|
||||
{
|
||||
label: 'Read Speed (MB/s)',
|
||||
data: [],
|
||||
backgroundColor: 'rgba(37, 99, 235, 0.8)',
|
||||
borderColor: 'rgba(37, 99, 235, 1)',
|
||||
borderWidth: 1
|
||||
},
|
||||
{
|
||||
label: 'Write Speed (MB/s)',
|
||||
data: [],
|
||||
backgroundColor: 'rgba(6, 182, 212, 0.8)',
|
||||
borderColor: 'rgba(6, 182, 212, 1)',
|
||||
borderWidth: 1
|
||||
}
|
||||
]
|
||||
},
|
||||
options: chartConfig
|
||||
});
|
||||
|
||||
// IOPS Chart
|
||||
charts.iops = new Chart(document.getElementById('iops-chart'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [
|
||||
{
|
||||
label: 'Read IOPS',
|
||||
data: [],
|
||||
backgroundColor: 'rgba(16, 185, 129, 0.8)',
|
||||
borderColor: 'rgba(16, 185, 129, 1)',
|
||||
borderWidth: 1
|
||||
},
|
||||
{
|
||||
label: 'Write IOPS',
|
||||
data: [],
|
||||
backgroundColor: 'rgba(245, 158, 11, 0.8)',
|
||||
borderColor: 'rgba(245, 158, 11, 1)',
|
||||
borderWidth: 1
|
||||
}
|
||||
]
|
||||
},
|
||||
options: chartConfig
|
||||
});
|
||||
|
||||
// Historical Speed Chart
|
||||
charts.historical = new Chart(document.getElementById('historical-chart'), {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: []
|
||||
},
|
||||
options: {
|
||||
...chartConfig,
|
||||
elements: {
|
||||
line: {
|
||||
tension: 0.4
|
||||
},
|
||||
point: {
|
||||
radius: 3
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Historical IOPS Chart
|
||||
charts.iopsTrend = new Chart(document.getElementById('iops-trend-chart'), {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: []
|
||||
},
|
||||
options: {
|
||||
...chartConfig,
|
||||
elements: {
|
||||
line: {
|
||||
tension: 0.4
|
||||
},
|
||||
point: {
|
||||
radius: 3
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch all data
|
||||
async function fetchAllData() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/metrics`);
|
||||
if (!response.ok) throw new Error('Failed to fetch metrics');
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
updateSystemInfo(data.system_info);
|
||||
updatePartitions(data.partitions);
|
||||
updateIOStats(data.io_stats);
|
||||
updateSMARTData(data.smart_data);
|
||||
updateHistoricalData(data.io_stats);
|
||||
updateLastUpdateTime();
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
showError('Failed to fetch data from server. Make sure the backend is running.');
|
||||
}
|
||||
}
|
||||
|
||||
// Update system information
|
||||
function updateSystemInfo(info) {
|
||||
if (!info) return;
|
||||
|
||||
document.getElementById('platform').textContent =
|
||||
`${info.platform} ${info.platform_release}`;
|
||||
document.getElementById('hostname').textContent = info.hostname;
|
||||
document.getElementById('uptime').textContent =
|
||||
`${info.uptime_days}d ${info.uptime_hours}h ${info.uptime_minutes}m`;
|
||||
document.getElementById('boot-time').textContent = info.boot_time;
|
||||
}
|
||||
|
||||
// Update disk partitions
|
||||
function updatePartitions(partitions) {
|
||||
if (!partitions || partitions.length === 0) {
|
||||
document.getElementById('partitions-container').innerHTML =
|
||||
'<div class="info-message">No partitions found</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const container = document.getElementById('partitions-container');
|
||||
container.innerHTML = '';
|
||||
|
||||
partitions.forEach(partition => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'partition-card';
|
||||
|
||||
let progressClass = '';
|
||||
if (partition.percent >= 90) progressClass = 'danger';
|
||||
else if (partition.percent >= 75) progressClass = 'warning';
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="partition-header">
|
||||
<div class="partition-name">${partition.device}</div>
|
||||
<div class="partition-type">${partition.fstype}</div>
|
||||
</div>
|
||||
<div class="partition-info">
|
||||
<div class="partition-info-row">
|
||||
<span class="label">Mount Point:</span>
|
||||
<span class="value">${partition.mountpoint}</span>
|
||||
</div>
|
||||
<div class="partition-info-row">
|
||||
<span class="label">Total:</span>
|
||||
<span class="value">${partition.total_gb} GB</span>
|
||||
</div>
|
||||
<div class="partition-info-row">
|
||||
<span class="label">Used:</span>
|
||||
<span class="value">${partition.used_gb} GB</span>
|
||||
</div>
|
||||
<div class="partition-info-row">
|
||||
<span class="label">Free:</span>
|
||||
<span class="value">${partition.free_gb} GB</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill ${progressClass}" style="width: ${partition.percent}%">
|
||||
${partition.percent.toFixed(1)}%
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
// Update I/O statistics
|
||||
function updateIOStats(ioStats) {
|
||||
if (!ioStats || ioStats.length === 0) {
|
||||
document.getElementById('io-stats-body').innerHTML =
|
||||
'<tr><td colspan="7" class="loading">No I/O data available</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Update table
|
||||
const tbody = document.getElementById('io-stats-body');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
ioStats.forEach(stat => {
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `
|
||||
<td><strong>${stat.disk}</strong></td>
|
||||
<td>${stat.read_speed_mbps.toFixed(2)} MB/s</td>
|
||||
<td>${stat.write_speed_mbps.toFixed(2)} MB/s</td>
|
||||
<td>${stat.iops_read.toFixed(0)}</td>
|
||||
<td>${stat.iops_write.toFixed(0)}</td>
|
||||
<td>${stat.read_mb.toFixed(2)} MB</td>
|
||||
<td>${stat.write_mb.toFixed(2)} MB</td>
|
||||
`;
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
|
||||
// Update bar charts
|
||||
const labels = ioStats.map(s => s.disk);
|
||||
const readSpeeds = ioStats.map(s => s.read_speed_mbps);
|
||||
const writeSpeeds = ioStats.map(s => s.write_speed_mbps);
|
||||
const readIOPS = ioStats.map(s => s.iops_read);
|
||||
const writeIOPS = ioStats.map(s => s.iops_write);
|
||||
|
||||
charts.speed.data.labels = labels;
|
||||
charts.speed.data.datasets[0].data = readSpeeds;
|
||||
charts.speed.data.datasets[1].data = writeSpeeds;
|
||||
charts.speed.update();
|
||||
|
||||
charts.iops.data.labels = labels;
|
||||
charts.iops.data.datasets[0].data = readIOPS;
|
||||
charts.iops.data.datasets[1].data = writeIOPS;
|
||||
charts.iops.update();
|
||||
}
|
||||
|
||||
// Update SMART data
|
||||
function updateSMARTData(smartData) {
|
||||
const container = document.getElementById('smart-container');
|
||||
|
||||
if (!smartData || (Array.isArray(smartData) && smartData.length === 0)) {
|
||||
container.innerHTML =
|
||||
'<div class="info-message">No SMART data available (requires root/admin privileges)</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
if (smartData.error) {
|
||||
container.innerHTML =
|
||||
`<div class="info-message">SMART data unavailable: ${smartData.message}</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = '';
|
||||
|
||||
smartData.forEach(device => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'smart-card';
|
||||
|
||||
let healthClass = 'good';
|
||||
let healthText = device.health || 'Unknown';
|
||||
if (healthText.toLowerCase().includes('fail')) healthClass = 'bad';
|
||||
else if (healthText.toLowerCase().includes('warn')) healthClass = 'warning';
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="smart-header">
|
||||
<div class="smart-model">${device.name}</div>
|
||||
<div class="health-badge ${healthClass}">${healthText}</div>
|
||||
</div>
|
||||
<div class="smart-details">
|
||||
<div class="smart-row">
|
||||
<span class="label">Model:</span>
|
||||
<span class="value">${device.model || 'N/A'}</span>
|
||||
</div>
|
||||
<div class="smart-row">
|
||||
<span class="label">Serial:</span>
|
||||
<span class="value">${device.serial || 'N/A'}</span>
|
||||
</div>
|
||||
<div class="smart-row">
|
||||
<span class="label">Capacity:</span>
|
||||
<span class="value">${device.capacity || 'N/A'}</span>
|
||||
</div>
|
||||
<div class="smart-row">
|
||||
<span class="label">Temperature:</span>
|
||||
<span class="value">${device.temperature ? device.temperature + '°C' : 'N/A'}</span>
|
||||
</div>
|
||||
<div class="smart-row">
|
||||
<span class="label">Power On Hours:</span>
|
||||
<span class="value">${device.power_on_hours || 'N/A'}</span>
|
||||
</div>
|
||||
<div class="smart-row">
|
||||
<span class="label">Power Cycles:</span>
|
||||
<span class="value">${device.power_cycle_count || 'N/A'}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
// Update historical data
|
||||
function updateHistoricalData(ioStats) {
|
||||
if (!ioStats || ioStats.length === 0) return;
|
||||
|
||||
const now = new Date().toLocaleTimeString();
|
||||
historicalData.labels.push(now);
|
||||
|
||||
ioStats.forEach(stat => {
|
||||
if (!historicalData.readSpeeds[stat.disk]) {
|
||||
historicalData.readSpeeds[stat.disk] = [];
|
||||
historicalData.writeSpeeds[stat.disk] = [];
|
||||
historicalData.readIOPS[stat.disk] = [];
|
||||
historicalData.writeIOPS[stat.disk] = [];
|
||||
}
|
||||
|
||||
historicalData.readSpeeds[stat.disk].push(stat.read_speed_mbps);
|
||||
historicalData.writeSpeeds[stat.disk].push(stat.write_speed_mbps);
|
||||
historicalData.readIOPS[stat.disk].push(stat.iops_read);
|
||||
historicalData.writeIOPS[stat.disk].push(stat.iops_write);
|
||||
});
|
||||
|
||||
// Keep only last N data points
|
||||
if (historicalData.labels.length > HISTORY_LENGTH) {
|
||||
historicalData.labels.shift();
|
||||
Object.keys(historicalData.readSpeeds).forEach(disk => {
|
||||
historicalData.readSpeeds[disk].shift();
|
||||
historicalData.writeSpeeds[disk].shift();
|
||||
historicalData.readIOPS[disk].shift();
|
||||
historicalData.writeIOPS[disk].shift();
|
||||
});
|
||||
}
|
||||
|
||||
updateHistoricalCharts();
|
||||
}
|
||||
|
||||
// Update historical charts
|
||||
function updateHistoricalCharts() {
|
||||
const colors = [
|
||||
'rgba(37, 99, 235, 1)',
|
||||
'rgba(6, 182, 212, 1)',
|
||||
'rgba(16, 185, 129, 1)',
|
||||
'rgba(245, 158, 11, 1)',
|
||||
'rgba(239, 68, 68, 1)',
|
||||
'rgba(168, 85, 247, 1)'
|
||||
];
|
||||
|
||||
// Speed trend chart
|
||||
charts.historical.data.labels = historicalData.labels;
|
||||
charts.historical.data.datasets = [];
|
||||
|
||||
let colorIndex = 0;
|
||||
Object.keys(historicalData.readSpeeds).forEach(disk => {
|
||||
charts.historical.data.datasets.push({
|
||||
label: `${disk} Read`,
|
||||
data: historicalData.readSpeeds[disk],
|
||||
borderColor: colors[colorIndex % colors.length],
|
||||
backgroundColor: colors[colorIndex % colors.length].replace('1)', '0.1)'),
|
||||
borderWidth: 2,
|
||||
fill: false
|
||||
});
|
||||
colorIndex++;
|
||||
});
|
||||
|
||||
charts.historical.update();
|
||||
|
||||
// IOPS trend chart
|
||||
charts.iopsTrend.data.labels = historicalData.labels;
|
||||
charts.iopsTrend.data.datasets = [];
|
||||
|
||||
colorIndex = 0;
|
||||
Object.keys(historicalData.readIOPS).forEach(disk => {
|
||||
charts.iopsTrend.data.datasets.push({
|
||||
label: `${disk} Read IOPS`,
|
||||
data: historicalData.readIOPS[disk],
|
||||
borderColor: colors[colorIndex % colors.length],
|
||||
backgroundColor: colors[colorIndex % colors.length].replace('1)', '0.1)'),
|
||||
borderWidth: 2,
|
||||
fill: false
|
||||
});
|
||||
colorIndex++;
|
||||
});
|
||||
|
||||
charts.iopsTrend.update();
|
||||
}
|
||||
|
||||
// Update last update time
|
||||
function updateLastUpdateTime() {
|
||||
const now = new Date();
|
||||
document.getElementById('last-update').textContent =
|
||||
`Last Update: ${now.toLocaleTimeString()}`;
|
||||
}
|
||||
|
||||
// Start auto-refresh
|
||||
function startAutoRefresh() {
|
||||
setInterval(fetchAllData, REFRESH_INTERVAL);
|
||||
}
|
||||
|
||||
// Show error message
|
||||
function showError(message) {
|
||||
console.error(message);
|
||||
// You could add a toast notification here
|
||||
}
|
||||
121
Storage Dashboard/frontend/index.html
Normal file
121
Storage Dashboard/frontend/index.html
Normal file
@ -0,0 +1,121 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Storage Device Performance Dashboard</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>Storage Device Performance Dashboard</h1>
|
||||
<div class="header-info">
|
||||
<span id="last-update">Last Update: Never</span>
|
||||
<button id="refresh-btn" class="btn">Refresh Now</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- System Information -->
|
||||
<section class="section">
|
||||
<h2>System Information</h2>
|
||||
<div class="system-info-grid" id="system-info">
|
||||
<div class="info-card">
|
||||
<span class="label">Platform:</span>
|
||||
<span class="value" id="platform">Loading...</span>
|
||||
</div>
|
||||
<div class="info-card">
|
||||
<span class="label">Hostname:</span>
|
||||
<span class="value" id="hostname">Loading...</span>
|
||||
</div>
|
||||
<div class="info-card">
|
||||
<span class="label">Uptime:</span>
|
||||
<span class="value" id="uptime">Loading...</span>
|
||||
</div>
|
||||
<div class="info-card">
|
||||
<span class="label">Boot Time:</span>
|
||||
<span class="value" id="boot-time">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Disk Partitions -->
|
||||
<section class="section">
|
||||
<h2>Disk Partitions & Usage</h2>
|
||||
<div id="partitions-container" class="partitions-grid">
|
||||
<div class="loading">Loading partition data...</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- I/O Performance Charts -->
|
||||
<section class="section">
|
||||
<h2>I/O Performance Metrics</h2>
|
||||
<div class="charts-grid">
|
||||
<div class="chart-container">
|
||||
<h3>Read/Write Speed (MB/s)</h3>
|
||||
<canvas id="speed-chart"></canvas>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<h3>IOPS (Operations/sec)</h3>
|
||||
<canvas id="iops-chart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- I/O Statistics Table -->
|
||||
<section class="section">
|
||||
<h2>Detailed I/O Statistics</h2>
|
||||
<div class="table-container">
|
||||
<table id="io-stats-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Disk</th>
|
||||
<th>Read Speed</th>
|
||||
<th>Write Speed</th>
|
||||
<th>Read IOPS</th>
|
||||
<th>Write IOPS</th>
|
||||
<th>Total Read</th>
|
||||
<th>Total Write</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="io-stats-body">
|
||||
<tr>
|
||||
<td colspan="7" class="loading">Loading I/O statistics...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- SMART Data -->
|
||||
<section class="section">
|
||||
<h2>SMART Health Data</h2>
|
||||
<div id="smart-container" class="smart-grid">
|
||||
<div class="info-message">Loading SMART data...</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Historical Data Charts -->
|
||||
<section class="section">
|
||||
<h2>Historical Performance</h2>
|
||||
<div class="charts-grid">
|
||||
<div class="chart-container">
|
||||
<h3>Read/Write Trends</h3>
|
||||
<canvas id="historical-chart"></canvas>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<h3>IOPS Trends</h3>
|
||||
<canvas id="iops-trend-chart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<p>Auto-refresh every 5 seconds | Data updates in real-time</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script src="dashboard.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
386
Storage Dashboard/frontend/styles.css
Normal file
386
Storage Dashboard/frontend/styles.css
Normal file
@ -0,0 +1,386 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
--primary-color: #2563eb;
|
||||
--secondary-color: #1e40af;
|
||||
--success-color: #10b981;
|
||||
--warning-color: #f59e0b;
|
||||
--danger-color: #ef4444;
|
||||
--bg-color: #0f172a;
|
||||
--card-bg: #1e293b;
|
||||
--text-color: #e2e8f0;
|
||||
--text-muted: #94a3b8;
|
||||
--border-color: #334155;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
header {
|
||||
background: var(--card-bg);
|
||||
padding: 25px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 30px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 28px;
|
||||
background: linear-gradient(135deg, var(--primary-color), #06b6d4);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.header-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
#last-update {
|
||||
color: var(--text-muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: var(--secondary-color);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.section {
|
||||
background: var(--card-bg);
|
||||
padding: 25px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 30px;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.section h2 {
|
||||
font-size: 22px;
|
||||
margin-bottom: 20px;
|
||||
color: var(--text-color);
|
||||
border-bottom: 2px solid var(--border-color);
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.section h3 {
|
||||
font-size: 16px;
|
||||
margin-bottom: 15px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.system-info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.info-card {
|
||||
background: var(--bg-color);
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.info-card .label {
|
||||
color: var(--text-muted);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.info-card .value {
|
||||
color: var(--text-color);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.partitions-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.partition-card {
|
||||
background: var(--bg-color);
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.partition-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.partition-name {
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.partition-type {
|
||||
background: var(--border-color);
|
||||
padding: 4px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.partition-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.partition-info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.partition-info-row .label {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.partition-info-row .value {
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 24px;
|
||||
background: var(--border-color);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--primary-color), #06b6d4);
|
||||
transition: width 0.5s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.progress-fill.warning {
|
||||
background: linear-gradient(90deg, var(--warning-color), #fbbf24);
|
||||
}
|
||||
|
||||
.progress-fill.danger {
|
||||
background: linear-gradient(90deg, var(--danger-color), #f87171);
|
||||
}
|
||||
|
||||
.charts-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
background: var(--bg-color);
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.chart-container canvas {
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
thead {
|
||||
background: var(--bg-color);
|
||||
}
|
||||
|
||||
th {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: var(--text-muted);
|
||||
border-bottom: 2px solid var(--border-color);
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 12px;
|
||||
font-size: 14px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
tbody tr:hover {
|
||||
background: var(--bg-color);
|
||||
}
|
||||
|
||||
.smart-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.smart-card {
|
||||
background: var(--bg-color);
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.smart-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.smart-model {
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.health-badge {
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.health-badge.good {
|
||||
background: var(--success-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.health-badge.warning {
|
||||
background: var(--warning-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.health-badge.bad {
|
||||
background: var(--danger-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.smart-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.smart-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.smart-row .label {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.smart-row .value {
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 30px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.info-message {
|
||||
padding: 20px;
|
||||
background: var(--bg-color);
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.error-message {
|
||||
padding: 15px;
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border: 1px solid var(--danger-color);
|
||||
border-radius: 8px;
|
||||
color: var(--danger-color);
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
footer {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: var(--text-muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
header {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header-info {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.charts-grid,
|
||||
.partitions-grid,
|
||||
.smart-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
45
Storage Dashboard/start.bat
Normal file
45
Storage Dashboard/start.bat
Normal file
@ -0,0 +1,45 @@
|
||||
@echo off
|
||||
REM Storage Dashboard Start Script for Windows
|
||||
|
||||
echo ================================
|
||||
echo Storage Dashboard Startup
|
||||
echo ================================
|
||||
echo.
|
||||
|
||||
REM Check if Python is installed
|
||||
python --version >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo Error: Python is not installed
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo Python version:
|
||||
python --version
|
||||
echo.
|
||||
|
||||
REM Navigate to backend directory
|
||||
cd backend
|
||||
|
||||
REM Check if requirements are installed
|
||||
echo Checking dependencies...
|
||||
python -c "import flask" >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo Installing dependencies...
|
||||
pip install -r requirements.txt
|
||||
) else (
|
||||
echo Dependencies already installed
|
||||
)
|
||||
|
||||
echo.
|
||||
echo ================================
|
||||
echo Starting Storage Dashboard...
|
||||
echo ================================
|
||||
echo.
|
||||
echo Access the dashboard at: http://localhost:5000
|
||||
echo.
|
||||
echo Press Ctrl+C to stop the server
|
||||
echo.
|
||||
|
||||
REM Start the Flask application
|
||||
python app.py
|
||||
42
Storage Dashboard/start.sh
Executable file
42
Storage Dashboard/start.sh
Executable file
@ -0,0 +1,42 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Storage Dashboard Start Script
|
||||
|
||||
echo "================================"
|
||||
echo "Storage Dashboard Startup"
|
||||
echo "================================"
|
||||
echo ""
|
||||
|
||||
# Check if Python is installed
|
||||
if ! command -v python3 &> /dev/null; then
|
||||
echo "Error: Python 3 is not installed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Python version: $(python3 --version)"
|
||||
echo ""
|
||||
|
||||
# Navigate to backend directory
|
||||
cd backend
|
||||
|
||||
# Check if requirements are installed
|
||||
echo "Checking dependencies..."
|
||||
if ! python3 -c "import flask" 2>/dev/null; then
|
||||
echo "Installing dependencies..."
|
||||
pip install -r requirements.txt
|
||||
else
|
||||
echo "Dependencies already installed"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "================================"
|
||||
echo "Starting Storage Dashboard..."
|
||||
echo "================================"
|
||||
echo ""
|
||||
echo "Access the dashboard at: http://localhost:5000"
|
||||
echo ""
|
||||
echo "Press Ctrl+C to stop the server"
|
||||
echo ""
|
||||
|
||||
# Start the Flask application
|
||||
python3 app.py
|
||||
Loading…
Reference in New Issue
Block a user