Initial server monitoring system with port 80 support
Features: - Device management and monitoring dashboard - Remote command execution on devices via port 80 - Auto-update coordination for multiple devices - Database reset functionality with safety confirmations - Server logs filtering and dedicated logging interface - Device status monitoring and management - SQLite database for comprehensive logging - Web interface with Bootstrap styling - Comprehensive error handling and logging Key components: - server.py: Main Flask application with all routes - templates/: Complete web interface templates - data/database.db: SQLite database for device logs - UPDATE_SUMMARY.md: Development progress documentation
This commit is contained in:
93
UPDATE_SUMMARY.md
Normal file
93
UPDATE_SUMMARY.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# Device Management System Update - Version 2.4
|
||||
|
||||
## Overview
|
||||
This update adds remote command execution capabilities between the Server_Monitorizare and the prezenta devices, along with improved error handling for network connectivity issues.
|
||||
|
||||
## New Features
|
||||
|
||||
### 1. Robust Network Configuration (app.py)
|
||||
- **File-based fallback system**: Device hostname and IP are saved to `./data/device_info.txt`
|
||||
- **Automatic error recovery**: When socket.gaierror occurs, the system loads previously saved values
|
||||
- **No external dependencies**: Uses only built-in Python modules
|
||||
- **Graceful degradation**: App continues to run even with network issues
|
||||
|
||||
### 2. Remote Command Execution System
|
||||
|
||||
#### Server Side (server.py)
|
||||
New endpoints added:
|
||||
- `/device_management` - Web interface for managing devices
|
||||
- `/execute_command` - Execute command on single device
|
||||
- `/execute_command_bulk` - Execute command on multiple devices
|
||||
- `/device_status/<device_ip>` - Get device status information
|
||||
|
||||
#### Device Side (app.py)
|
||||
New Flask server running on port 5000:
|
||||
- `/execute_command` - Receive and execute system commands
|
||||
- `/status` - Provide device status information
|
||||
|
||||
### 3. Security Features
|
||||
- **Whitelisted commands only**: Prevents execution of arbitrary commands
|
||||
- **Command logging**: All command execution attempts are logged
|
||||
- **Timeout protection**: Commands have 5-minute timeout limit
|
||||
- **Error handling**: Comprehensive error reporting
|
||||
|
||||
### 4. Web Interface Improvements
|
||||
- **Device Management Dashboard**: Complete interface for managing all devices
|
||||
- **Real-time status checking**: Check if devices are online/offline
|
||||
- **Bulk operations**: Execute commands on multiple devices simultaneously
|
||||
- **Search functionality**: Filter devices by hostname or IP
|
||||
- **Command selection**: Dropdown menus for common system commands
|
||||
|
||||
## Allowed Commands
|
||||
For security, only these commands can be executed remotely:
|
||||
- `sudo apt update` - Update package lists
|
||||
- `sudo apt upgrade -y` - Upgrade packages
|
||||
- `sudo apt autoremove -y` - Remove unused packages
|
||||
- `sudo apt autoclean` - Clean package cache
|
||||
- `sudo reboot` - Reboot device
|
||||
- `sudo shutdown -h now` - Shutdown device
|
||||
- `df -h` - Check disk space
|
||||
- `free -m` - Check memory usage
|
||||
- `uptime` - Check system uptime
|
||||
- `systemctl status` - Check system services
|
||||
- `sudo systemctl restart networking` - Restart network services
|
||||
- `sudo systemctl restart ssh` - Restart SSH service
|
||||
|
||||
## File Structure Changes
|
||||
|
||||
### New Files:
|
||||
- `Server_Monitorizare/templates/device_management.html` - Device management interface
|
||||
- `prezenta/data/device_info.txt` - Device configuration cache (auto-created)
|
||||
|
||||
### Modified Files:
|
||||
- `prezenta/app.py` - Added command execution server and network error handling
|
||||
- `Server_Monitorizare/server.py` - Added command execution endpoints
|
||||
- `Server_Monitorizare/templates/dashboard.html` - Added device management link
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
### Starting the Services
|
||||
1. **Start the monitoring server** (port 80):
|
||||
```bash
|
||||
cd Server_Monitorizare
|
||||
sudo python3 server.py
|
||||
```
|
||||
|
||||
2. **Start the device app** (ports 5000 for commands + normal operation):
|
||||
```bash
|
||||
cd prezenta
|
||||
python3 app.py
|
||||
```
|
||||
|
||||
### Using the Web Interface
|
||||
1. Navigate to `http://server-ip:80/dashboard`
|
||||
2. Click "Device Management" to access the new interface
|
||||
3. Use the dropdown menus to select commands
|
||||
4. Execute on single devices or use bulk operations
|
||||
|
||||
## Benefits
|
||||
1. **Centralized device management** - Control all devices from one interface
|
||||
2. **Automated maintenance** - Schedule updates and maintenance tasks
|
||||
3. **Real-time monitoring** - Check device status and performance
|
||||
4. **Improved reliability** - Network error handling prevents app crashes
|
||||
5. **Security** - Controlled command execution with comprehensive logging
|
||||
93
Working_Files/Modul Logging /how_to_use_logging_module.txt
Normal file
93
Working_Files/Modul Logging /how_to_use_logging_module.txt
Normal file
@@ -0,0 +1,93 @@
|
||||
|
||||
---
|
||||
|
||||
### `logging_module.py`
|
||||
```python
|
||||
import logging
|
||||
import os
|
||||
import socket
|
||||
import requests
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# Configure logging
|
||||
LOG_FILE_PATH = './data/log.txt'
|
||||
logging.basicConfig(filename=LOG_FILE_PATH, level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
|
||||
# Function to delete old logs
|
||||
def delete_old_logs():
|
||||
log_dir = './data/'
|
||||
log_file = 'log.txt'
|
||||
log_path = os.path.join(log_dir, log_file)
|
||||
|
||||
if os.path.exists(log_path):
|
||||
file_mod_time = datetime.fromtimestamp(os.path.getmtime(log_path))
|
||||
if datetime.now() - file_mod_time > timedelta(days=10):
|
||||
os.remove(log_path)
|
||||
log_info_with_server(f"Deleted old log file: {log_file}")
|
||||
else:
|
||||
log_info_with_server(f"Log file is not older than 10 days: {log_file}")
|
||||
else:
|
||||
log_info_with_server(f"Log file does not exist: {log_file}")
|
||||
|
||||
# Function to read the name (idmasa) from the file
|
||||
def read_name_from_file():
|
||||
try:
|
||||
with open("./data/idmasa.txt", "r") as file:
|
||||
|
||||
### How to Use the Module in Another App
|
||||
|
||||
1. Save the script as `logging_module.py` in a directory accessible to your other Python applications.
|
||||
|
||||
2. Import the module and use its functions in your other app. For example:
|
||||
|
||||
```python
|
||||
# Import the logging module
|
||||
from logging_module import log_info_with_server, delete_old_logs
|
||||
|
||||
# Example usage
|
||||
log_info_with_server("This is a test log message.")
|
||||
delete_old_logs()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Explanation of the Script:
|
||||
1. **Encapsulation**:
|
||||
- The script encapsulates all logging-related functionality, including:
|
||||
- Deleting old logs ([`delete_old_logs`](command:_github.copilot.openSymbolFromReferences?%5B%22%22%2C%5B%7B%22uri%22%3A%7B%22scheme%22%3A%22file%22%2C%22authority%22%3A%22%22%2C%22path%22%3A%22%2Fhome%2Fpi%2FDesktop%2Fprezenta%2Fapp.py%22%2C%22query%22%3A%22%22%2C%22fragment%22%3A%22%22%7D%2C%22pos%22%3A%7B%22line%22%3A16%2C%22character%22%3A4%7D%7D%2C%7B%22uri%22%3A%7B%22scheme%22%3A%22file%22%2C%22authority%22%3A%22%22%2C%22path%22%3A%22%2Fhome%2Fpi%2FDesktop%2Fprezenta%2Fapp.py%22%2C%22query%22%3A%22%22%2C%22fragment%22%3A%22%22%7D%2C%22pos%22%3A%7B%22line%22%3A68%2C%22character%22%3A0%7D%7D%2C%7B%22uri%22%3A%7B%22scheme%22%3A%22file%22%2C%22authority%22%3A%22%22%2C%22path%22%3A%22%2Fhome%2Fpi%2FDesktop%2Fprezenta%2Fapp.py%22%2C%22query%22%3A%22%22%2C%22fragment%22%3A%22%22%7D%2C%22pos%22%3A%7B%22line%22%3A16%2C%22character%22%3A4%7D%7D%5D%2C%229495c72a-c9b2-498f-9787-1c43fb524282%22%5D "Go to definition")).
|
||||
- Reading the [`idmasa`](command:_github.copilot.openSymbolFromReferences?%5B%22%22%2C%5B%7B%22uri%22%3A%7B%22scheme%22%3A%22file%22%2C%22authority%22%3A%22%22%2C%22path%22%3A%22%2Fhome%2Fpi%2FDesktop%2Fprezenta%2Fapp.py%22%2C%22query%22%3A%22%22%2C%22fragment%22%3A%22%22%7D%2C%22pos%22%3A%7B%22line%22%3A31%2C%22character%22%3A29%7D%7D%2C%7B%22uri%22%3A%7B%22scheme%22%3A%22file%22%2C%22authority%22%3A%22%22%2C%22path%22%3A%22%2Fhome%2Fpi%2FDesktop%2Fprezenta%2Fapp.py%22%2C%22query%22%3A%22%22%2C%22fragment%22%3A%22%22%7D%2C%22pos%22%3A%7B%22line%22%3A31%2C%22character%22%3A29%7D%7D%5D%2C%229495c72a-c9b2-498f-9787-1c43fb524282%22%5D "Go to definition") value from a file ([`read_name_from_file`](command:_github.copilot.openSymbolFromReferences?%5B%22%22%2C%5B%7B%22uri%22%3A%7B%22scheme%22%3A%22file%22%2C%22authority%22%3A%22%22%2C%22path%22%3A%22%2Fhome%2Fpi%2FDesktop%2Fprezenta%2Fapp.py%22%2C%22query%22%3A%22%22%2C%22fragment%22%3A%22%22%7D%2C%22pos%22%3A%7B%22line%22%3A32%2C%22character%22%3A4%7D%7D%2C%7B%22uri%22%3A%7B%22scheme%22%3A%22file%22%2C%22authority%22%3A%22%22%2C%22path%22%3A%22%2Fhome%2Fpi%2FDesktop%2Fprezenta%2Fapp.py%22%2C%22query%22%3A%22%22%2C%22fragment%22%3A%22%22%7D%2C%22pos%22%3A%7B%22line%22%3A32%2C%22character%22%3A4%7D%7D%5D%2C%229495c72a-c9b2-498f-9787-1c43fb524282%22%5D "Go to definition")).
|
||||
- Sending logs to a remote server ([`send_log_to_server`](command:_github.copilot.openSymbolFromReferences?%5B%22%22%2C%5B%7B%22uri%22%3A%7B%22scheme%22%3A%22file%22%2C%22authority%22%3A%22%22%2C%22path%22%3A%22%2Fhome%2Fpi%2FDesktop%2Fprezenta%2Fapp.py%22%2C%22query%22%3A%22%22%2C%22fragment%22%3A%22%22%7D%2C%22pos%22%3A%7B%22line%22%3A42%2C%22character%22%3A4%7D%7D%2C%7B%22uri%22%3A%7B%22scheme%22%3A%22file%22%2C%22authority%22%3A%22%22%2C%22path%22%3A%22%2Fhome%2Fpi%2FDesktop%2Fprezenta%2Fapp.py%22%2C%22query%22%3A%22%22%2C%22fragment%22%3A%22%22%7D%2C%22pos%22%3A%7B%22line%22%3A42%2C%22character%22%3A4%7D%7D%5D%2C%229495c72a-c9b2-498f-9787-1c43fb524282%22%5D "Go to definition")).
|
||||
- Logging messages locally and remotely ([`log_info_with_server`](command:_github.copilot.openSymbolFromReferences?%5B%22%22%2C%5B%7B%22uri%22%3A%7B%22scheme%22%3A%22file%22%2C%22authority%22%3A%22%22%2C%22path%22%3A%22%2Fhome%2Fpi%2FDesktop%2Fprezenta%2Fapp.py%22%2C%22query%22%3A%22%22%2C%22fragment%22%3A%22%22%7D%2C%22pos%22%3A%7B%22line%22%3A60%2C%22character%22%3A4%7D%7D%2C%7B%22uri%22%3A%7B%22scheme%22%3A%22file%22%2C%22authority%22%3A%22%22%2C%22path%22%3A%22%2Fhome%2Fpi%2FDesktop%2Fprezenta%2Fapp.py%22%2C%22query%22%3A%22%22%2C%22fragment%22%3A%22%22%7D%2C%22pos%22%3A%7B%22line%22%3A25%2C%22character%22%3A12%7D%7D%5D%2C%229495c72a-c9b2-498f-9787-1c43fb524282%22%5D "Go to definition")).
|
||||
|
||||
2. **Reusability**:
|
||||
- The script is designed to be imported and reused in other Python applications.
|
||||
|
||||
3. **Configuration**:
|
||||
- The log file path (`LOG_FILE_PATH`) and server URL ([`server_url`](command:_github.copilot.openSymbolFromReferences?%5B%22%22%2C%5B%7B%22uri%22%3A%7B%22scheme%22%3A%22file%22%2C%22authority%22%3A%22%22%2C%22path%22%3A%22%2Fhome%2Fpi%2FDesktop%2Fprezenta%2Fapp.py%22%2C%22query%22%3A%22%22%2C%22fragment%22%3A%22%22%7D%2C%22pos%22%3A%7B%22line%22%3A52%2C%22character%22%3A8%7D%7D%5D%2C%229495c72a-c9b2-498f-9787-1c43fb524282%22%5D "Go to definition")) can be customized as needed.
|
||||
|
||||
4. **Error Handling**:
|
||||
- The script includes error handling for file operations and HTTP requests.
|
||||
|
||||
---
|
||||
|
||||
### Testing the Module
|
||||
1. Create a new Python file (e.g., `test_logging.py`) and import the module:
|
||||
```python
|
||||
from logging_module import log_info_with_server, delete_old_logs
|
||||
|
||||
# Test the logging functions
|
||||
log_info_with_server("Testing logging from another app.")
|
||||
delete_old_logs()
|
||||
```
|
||||
|
||||
2. Run the `test_logging.py` script:
|
||||
```bash
|
||||
python3 test_logging.py
|
||||
```
|
||||
|
||||
3. Verify that:
|
||||
- The log message is written to [`./data/log.txt`](command:_github.copilot.openRelativePath?%5B%7B%22scheme%22%3A%22file%22%2C%22authority%22%3A%22%22%2C%22path%22%3A%22%2Fhome%2Fpi%2FDesktop%2Fprezenta%2Fdata%2Flog.txt%22%2C%22query%22%3A%22%22%2C%22fragment%22%3A%22%22%7D%2C%229495c72a-c9b2-498f-9787-1c43fb524282%22%5D "/home/pi/Desktop/prezenta/data/log.txt").
|
||||
- The log message is sent to the remote server (if configured correctly).
|
||||
- Old logs are deleted if they are older than 10 days.
|
||||
|
||||
This approach ensures that the logging functionality is modular and reusable across multiple Python applications.
|
||||
60
Working_Files/Modul Logging /logging_module.py
Normal file
60
Working_Files/Modul Logging /logging_module.py
Normal file
@@ -0,0 +1,60 @@
|
||||
import logging
|
||||
import os
|
||||
import socket
|
||||
import requests
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# Configure logging
|
||||
LOG_FILE_PATH = './data/log.txt'
|
||||
logging.basicConfig(filename=LOG_FILE_PATH, level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
|
||||
# Function to delete old logs
|
||||
def delete_old_logs():
|
||||
log_dir = './data/'
|
||||
log_file = 'log.txt'
|
||||
log_path = os.path.join(log_dir, log_file)
|
||||
|
||||
if os.path.exists(log_path):
|
||||
file_mod_time = datetime.fromtimestamp(os.path.getmtime(log_path))
|
||||
if datetime.now() - file_mod_time > timedelta(days=10):
|
||||
os.remove(log_path)
|
||||
log_info_with_server(f"Deleted old log file: {log_file}")
|
||||
else:
|
||||
log_info_with_server(f"Log file is not older than 10 days: {log_file}")
|
||||
else:
|
||||
log_info_with_server(f"Log file does not exist: {log_file}")
|
||||
|
||||
# Function to read the name (idmasa) from the file
|
||||
def read_name_from_file():
|
||||
try:
|
||||
with open("./data/idmasa.txt", "r") as file:
|
||||
n_masa = file.readline().strip()
|
||||
return n_masa
|
||||
except FileNotFoundError:
|
||||
logging.error("File ./data/idmasa.txt not found.")
|
||||
return "unknown"
|
||||
|
||||
# Function to send logs to a remote server
|
||||
def send_log_to_server(log_message, n_masa):
|
||||
try:
|
||||
hostname = socket.gethostname()
|
||||
device_ip = socket.gethostbyname(hostname)
|
||||
log_data = {
|
||||
"hostname": str(hostname),
|
||||
"device_ip": str(device_ip),
|
||||
"nume_masa": str(n_masa),
|
||||
"log_message": str(log_message)
|
||||
}
|
||||
server_url = "http://rpi-ansible:80/logs" # Replace with your server's URL
|
||||
print(log_data) # Debugging: Print log_data to verify its contents
|
||||
response = requests.post(server_url, json=log_data, timeout=5)
|
||||
response.raise_for_status()
|
||||
logging.info("Log successfully sent to server: %s", log_message)
|
||||
except requests.exceptions.RequestException as e:
|
||||
logging.error("Failed to send log to server: %s", e)
|
||||
|
||||
# Wrapper for logging.info to also send logs to the server
|
||||
def log_info_with_server(message):
|
||||
n_masa = read_name_from_file() # Read name (idmasa) from the file
|
||||
logging.info(message)
|
||||
send_log_to_server(message, n_masa)
|
||||
22
Working_Files/drop.py
Normal file
22
Working_Files/drop.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# acest script sterge informatia din baza de date pentru a avea un nou inceput .
|
||||
|
||||
import sqlite3
|
||||
|
||||
# Define the database path
|
||||
DATABASE = './data/database.db'
|
||||
|
||||
def reset_database():
|
||||
# Connect to the database
|
||||
with sqlite3.connect(DATABASE) as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Drop the existing logs table if it exists
|
||||
cursor.execute('DELETE FROM logs;')
|
||||
|
||||
|
||||
conn.commit()
|
||||
|
||||
print("Database has been reset ")
|
||||
|
||||
if __name__ == '__main__':
|
||||
reset_database()
|
||||
19
Working_Files/initalizedb.py
Normal file
19
Working_Files/initalizedb.py
Normal file
@@ -0,0 +1,19 @@
|
||||
import sqlite3
|
||||
def initialize_database():
|
||||
DATABASE = "data/database.db"
|
||||
with sqlite3.connect(DATABASE) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
hostname TEXT NOT NULL,
|
||||
device_ip TEXT NOT NULL,
|
||||
nume_masa TEXT NOT NULL,
|
||||
timestamp TEXT NOT NULL,
|
||||
event_description TEXT NOT NULL
|
||||
)
|
||||
''')
|
||||
conn.commit()
|
||||
print("Database initialized")
|
||||
|
||||
initialize_database()
|
||||
BIN
__pycache__/server.cpython-311.pyc
Normal file
BIN
__pycache__/server.cpython-311.pyc
Normal file
Binary file not shown.
BIN
data/database.db
Executable file
BIN
data/database.db
Executable file
Binary file not shown.
462
server.py
Normal file
462
server.py
Normal file
@@ -0,0 +1,462 @@
|
||||
from flask import Flask, request, render_template, jsonify, redirect, url_for
|
||||
import sqlite3
|
||||
from datetime import datetime
|
||||
from urllib.parse import unquote
|
||||
import requests
|
||||
import threading
|
||||
|
||||
app = Flask(__name__)
|
||||
DATABASE = 'data/database.db' # Updated path for the database
|
||||
# Route to handle log submissions
|
||||
@app.route('/logs', methods=['POST'])
|
||||
@app.route('/log', methods=['POST'])
|
||||
def log_event():
|
||||
try:
|
||||
#print(f"Connecting to database at: {DATABASE}")
|
||||
|
||||
# Get the JSON payload
|
||||
data = request.json
|
||||
if not data:
|
||||
return {"error": "Invalid or missing JSON payload"}, 400
|
||||
|
||||
#print(f"Received request data: {data}")
|
||||
|
||||
# Extract fields from the JSON payload
|
||||
hostname = data.get('hostname')
|
||||
device_ip = data.get('device_ip')
|
||||
nume_masa = data.get('nume_masa')
|
||||
log_message = data.get('log_message')
|
||||
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
# Validate required fields
|
||||
if not hostname or not device_ip or not nume_masa or not log_message:
|
||||
print("Validation failed: Missing required fields")
|
||||
return {"error": "Missing required fields"}, 400
|
||||
|
||||
# Save the log to the database
|
||||
with sqlite3.connect(DATABASE) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
INSERT INTO logs (hostname, device_ip, nume_masa, timestamp, event_description)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
''', (hostname, device_ip, nume_masa, timestamp, log_message))
|
||||
conn.commit()
|
||||
print("Log saved successfully")
|
||||
|
||||
return {"message": "Log saved successfully"}, 201
|
||||
|
||||
except sqlite3.Error as e:
|
||||
print(f"Database error: {e}")
|
||||
return {"error": f"Database connection failed: {e}"}, 500
|
||||
|
||||
except Exception as e:
|
||||
print(f"Unexpected error: {e}")
|
||||
return {"error": "An unexpected error occurred"}, 500
|
||||
|
||||
# Route to display the dashboard (excluding server logs)
|
||||
@app.route('/dashboard', methods=['GET'])
|
||||
def dashboard():
|
||||
with sqlite3.connect(DATABASE) as conn:
|
||||
cursor = conn.cursor()
|
||||
# Fetch the last 60 logs excluding server logs, ordered by timestamp in descending order
|
||||
cursor.execute('''
|
||||
SELECT hostname, device_ip, nume_masa, timestamp, event_description
|
||||
FROM logs
|
||||
WHERE hostname != 'SERVER'
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT 60
|
||||
''')
|
||||
logs = cursor.fetchall()
|
||||
return render_template('dashboard.html', logs=logs)
|
||||
# Route to display logs for a specific device (excluding server logs)
|
||||
@app.route('/device_logs/<nume_masa>', methods=['GET'])
|
||||
def device_logs(nume_masa):
|
||||
nume_masa = unquote(nume_masa) # Decode URL-encoded value
|
||||
with sqlite3.connect(DATABASE) as conn:
|
||||
cursor = conn.cursor()
|
||||
# Order logs by timestamp in descending order, excluding server logs
|
||||
cursor.execute('''
|
||||
SELECT hostname, nume_masa, timestamp, event_description
|
||||
FROM logs
|
||||
WHERE nume_masa = ? AND hostname != 'SERVER'
|
||||
ORDER BY timestamp DESC
|
||||
''', (nume_masa,))
|
||||
logs = cursor.fetchall()
|
||||
return render_template('device_logs.html', logs=logs, nume_masa=nume_masa)
|
||||
|
||||
@app.route('/unique_devices', methods=['GET'])
|
||||
def unique_devices():
|
||||
with sqlite3.connect(DATABASE) as conn:
|
||||
cursor = conn.cursor()
|
||||
# Query to get unique devices with their most recent log (excluding server logs)
|
||||
cursor.execute('''
|
||||
SELECT hostname, device_ip, MAX(timestamp) AS last_log, event_description
|
||||
FROM logs
|
||||
WHERE hostname != 'SERVER'
|
||||
GROUP BY hostname, device_ip
|
||||
ORDER BY last_log DESC
|
||||
''')
|
||||
devices = cursor.fetchall()
|
||||
return render_template('unique_devices.html', devices=devices)
|
||||
|
||||
@app.route('/hostname_logs/<hostname>', methods=['GET'])
|
||||
def hostname_logs(hostname):
|
||||
with sqlite3.connect(DATABASE) as conn:
|
||||
cursor = conn.cursor()
|
||||
# Fetch logs for the specified hostname (excluding server logs)
|
||||
cursor.execute('''
|
||||
SELECT hostname, nume_masa, timestamp, event_description
|
||||
FROM logs
|
||||
WHERE hostname = ? AND hostname != 'SERVER'
|
||||
ORDER BY timestamp DESC
|
||||
''', (hostname,))
|
||||
logs = cursor.fetchall()
|
||||
return render_template('hostname_logs.html', logs=logs, hostname=hostname)
|
||||
|
||||
# Route to display server logs only
|
||||
@app.route('/server_logs', methods=['GET'])
|
||||
def server_logs():
|
||||
with sqlite3.connect(DATABASE) as conn:
|
||||
cursor = conn.cursor()
|
||||
# Fetch only server logs, ordered by timestamp in descending order
|
||||
cursor.execute('''
|
||||
SELECT hostname, device_ip, nume_masa, timestamp, event_description
|
||||
FROM logs
|
||||
WHERE hostname = 'SERVER'
|
||||
ORDER BY timestamp DESC
|
||||
''')
|
||||
logs = cursor.fetchall()
|
||||
return render_template('server_logs.html', logs=logs)
|
||||
|
||||
# Function to execute command on a remote device
|
||||
def execute_command_on_device(device_ip, command):
|
||||
"""
|
||||
Send command to a device for execution
|
||||
"""
|
||||
try:
|
||||
url = f"http://{device_ip}:80/execute_command"
|
||||
payload = {"command": command}
|
||||
|
||||
response = requests.post(url, json=payload, timeout=30)
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
return {"success": True, "result": result}
|
||||
else:
|
||||
error_data = response.json() if response.content else {"error": "Unknown error"}
|
||||
return {"success": False, "error": error_data}
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
return {"success": False, "error": f"Connection error: {str(e)}"}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": f"Unexpected error: {str(e)}"}
|
||||
|
||||
# Function to get device status
|
||||
def get_device_status(device_ip):
|
||||
"""
|
||||
Get status information from a device
|
||||
"""
|
||||
try:
|
||||
url = f"http://{device_ip}:80/status"
|
||||
response = requests.get(url, timeout=10)
|
||||
|
||||
if response.status_code == 200:
|
||||
return {"success": True, "status": response.json()}
|
||||
else:
|
||||
return {"success": False, "error": "Failed to get device status"}
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
return {"success": False, "error": f"Connection error: {str(e)}"}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": f"Unexpected error: {str(e)}"}
|
||||
|
||||
# Route to display device management page (excluding server)
|
||||
@app.route('/device_management', methods=['GET'])
|
||||
def device_management():
|
||||
with sqlite3.connect(DATABASE) as conn:
|
||||
cursor = conn.cursor()
|
||||
# Get unique devices excluding server
|
||||
cursor.execute('''
|
||||
SELECT hostname, device_ip, MAX(timestamp) AS last_log
|
||||
FROM logs
|
||||
WHERE hostname != 'SERVER'
|
||||
GROUP BY hostname, device_ip
|
||||
ORDER BY last_log DESC
|
||||
''')
|
||||
devices = cursor.fetchall()
|
||||
return render_template('device_management.html', devices=devices)
|
||||
|
||||
# Route to execute command on a specific device
|
||||
@app.route('/execute_command', methods=['POST'])
|
||||
def execute_command():
|
||||
try:
|
||||
data = request.json
|
||||
device_ip = data.get('device_ip')
|
||||
command = data.get('command')
|
||||
|
||||
if not device_ip or not command:
|
||||
return jsonify({"error": "device_ip and command are required"}), 400
|
||||
|
||||
# Execute command on device
|
||||
result = execute_command_on_device(device_ip, command)
|
||||
|
||||
# Log the command execution attempt
|
||||
with sqlite3.connect(DATABASE) as conn:
|
||||
cursor = conn.cursor()
|
||||
status = "SUCCESS" if result['success'] else "FAILED"
|
||||
log_message = f"Command '{command}' {status}"
|
||||
if not result['success']:
|
||||
log_message += f" - Error: {result['error']}"
|
||||
|
||||
cursor.execute('''
|
||||
INSERT INTO logs (hostname, device_ip, nume_masa, timestamp, event_description)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
''', ("SERVER", device_ip, "COMMAND", datetime.now().strftime('%Y-%m-%d %H:%M:%S'), log_message))
|
||||
conn.commit()
|
||||
|
||||
return jsonify(result), 200 if result['success'] else 400
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({"error": f"Server error: {str(e)}"}), 500
|
||||
|
||||
# Route to get device status
|
||||
@app.route('/device_status/<device_ip>', methods=['GET'])
|
||||
def device_status(device_ip):
|
||||
result = get_device_status(device_ip)
|
||||
return jsonify(result), 200 if result['success'] else 400
|
||||
|
||||
# Route to execute command on multiple devices
|
||||
@app.route('/execute_command_bulk', methods=['POST'])
|
||||
def execute_command_bulk():
|
||||
try:
|
||||
data = request.json
|
||||
device_ips = data.get('device_ips', [])
|
||||
command = data.get('command')
|
||||
|
||||
if not device_ips or not command:
|
||||
return jsonify({"error": "device_ips and command are required"}), 400
|
||||
|
||||
results = {}
|
||||
threads = []
|
||||
|
||||
def execute_on_device(ip):
|
||||
results[ip] = execute_command_on_device(ip, command)
|
||||
|
||||
# Execute commands in parallel
|
||||
for ip in device_ips:
|
||||
thread = threading.Thread(target=execute_on_device, args=(ip,))
|
||||
threads.append(thread)
|
||||
thread.start()
|
||||
|
||||
# Wait for all threads to complete
|
||||
for thread in threads:
|
||||
thread.join()
|
||||
|
||||
# Log bulk command execution
|
||||
with sqlite3.connect(DATABASE) as conn:
|
||||
cursor = conn.cursor()
|
||||
for ip, result in results.items():
|
||||
status = "SUCCESS" if result['success'] else "FAILED"
|
||||
log_message = f"Bulk command '{command}' {status}"
|
||||
if not result['success']:
|
||||
log_message += f" - Error: {result['error']}"
|
||||
|
||||
cursor.execute('''
|
||||
INSERT INTO logs (hostname, device_ip, nume_masa, timestamp, event_description)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
''', ("SERVER", ip, "BULK_COMMAND", datetime.now().strftime('%Y-%m-%d %H:%M:%S'), log_message))
|
||||
conn.commit()
|
||||
|
||||
return jsonify({"results": results}), 200
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({"error": f"Server error: {str(e)}"}), 500
|
||||
|
||||
@app.route('/auto_update_devices', methods=['POST'])
|
||||
def auto_update_devices():
|
||||
"""
|
||||
Trigger auto-update on selected devices
|
||||
"""
|
||||
try:
|
||||
data = request.json
|
||||
device_ips = data.get('device_ips', [])
|
||||
|
||||
if not device_ips:
|
||||
return jsonify({"error": "device_ips list is required"}), 400
|
||||
|
||||
results = []
|
||||
|
||||
for ip in device_ips:
|
||||
try:
|
||||
# Send auto-update command to device
|
||||
response = requests.post(
|
||||
f'http://{ip}:80/auto_update',
|
||||
json={},
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
result_data = response.json()
|
||||
results.append({
|
||||
"device_ip": ip,
|
||||
"success": True,
|
||||
"status": result_data.get('status'),
|
||||
"message": result_data.get('message'),
|
||||
"old_version": result_data.get('old_version'),
|
||||
"new_version": result_data.get('new_version')
|
||||
})
|
||||
|
||||
# Log successful update
|
||||
with sqlite3.connect(DATABASE) as conn:
|
||||
cursor = conn.cursor()
|
||||
log_message = f"Auto-update: {result_data.get('message', 'Update initiated')}"
|
||||
cursor.execute('''
|
||||
INSERT INTO logs (hostname, device_ip, nume_masa, timestamp, event_description)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
''', ("SERVER", ip, "AUTO_UPDATE", datetime.now().strftime('%Y-%m-%d %H:%M:%S'), log_message))
|
||||
conn.commit()
|
||||
|
||||
else:
|
||||
error_msg = f"HTTP {response.status_code}"
|
||||
try:
|
||||
error_data = response.json()
|
||||
error_msg = error_data.get('error', error_msg)
|
||||
except:
|
||||
pass
|
||||
|
||||
results.append({
|
||||
"device_ip": ip,
|
||||
"success": False,
|
||||
"error": error_msg
|
||||
})
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
results.append({
|
||||
"device_ip": ip,
|
||||
"success": False,
|
||||
"error": "Request timeout"
|
||||
})
|
||||
except requests.exceptions.ConnectionError:
|
||||
results.append({
|
||||
"device_ip": ip,
|
||||
"success": False,
|
||||
"error": "Connection failed - device may be offline"
|
||||
})
|
||||
except Exception as e:
|
||||
results.append({
|
||||
"device_ip": ip,
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
})
|
||||
|
||||
# Log failed update attempt
|
||||
if not results[-1]['success']:
|
||||
with sqlite3.connect(DATABASE) as conn:
|
||||
cursor = conn.cursor()
|
||||
log_message = f"Auto-update failed: {results[-1]['error']}"
|
||||
cursor.execute('''
|
||||
INSERT INTO logs (hostname, device_ip, nume_masa, timestamp, event_description)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
''', ("SERVER", ip, "AUTO_UPDATE", datetime.now().strftime('%Y-%m-%d %H:%M:%S'), log_message))
|
||||
conn.commit()
|
||||
|
||||
return jsonify({"results": results}), 200
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({"error": f"Server error: {str(e)}"}), 500
|
||||
|
||||
# Route to clear and reset the database
|
||||
@app.route('/reset_database', methods=['POST'])
|
||||
def reset_database():
|
||||
"""
|
||||
Clear all data from the database and reinitialize with fresh schema
|
||||
"""
|
||||
try:
|
||||
with sqlite3.connect(DATABASE) as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Get the count of logs before deletion for logging
|
||||
cursor.execute('SELECT COUNT(*) FROM logs')
|
||||
log_count = cursor.fetchone()[0]
|
||||
|
||||
# Drop the existing logs table
|
||||
cursor.execute('DROP TABLE IF EXISTS logs')
|
||||
|
||||
# Recreate the logs table with fresh schema
|
||||
cursor.execute('''
|
||||
CREATE TABLE logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
hostname TEXT NOT NULL,
|
||||
device_ip TEXT NOT NULL,
|
||||
nume_masa TEXT NOT NULL,
|
||||
timestamp TEXT NOT NULL,
|
||||
event_description TEXT NOT NULL
|
||||
)
|
||||
''')
|
||||
|
||||
# Insert a system log entry to mark the database reset
|
||||
reset_timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
cursor.execute('''
|
||||
INSERT INTO logs (hostname, device_ip, nume_masa, timestamp, event_description)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
''', ("SERVER", "127.0.0.1", "SYSTEM", reset_timestamp, f"Database cleared and reinitialized - {log_count} logs deleted"))
|
||||
|
||||
conn.commit()
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": "Database successfully cleared and reinitialized",
|
||||
"timestamp": reset_timestamp,
|
||||
"deleted_count": log_count
|
||||
}), 200
|
||||
|
||||
except sqlite3.Error as e:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": f"Database error: {str(e)}"
|
||||
}), 500
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": f"Unexpected error: {str(e)}"
|
||||
}), 500
|
||||
|
||||
# Route to get database statistics
|
||||
@app.route('/database_stats', methods=['GET'])
|
||||
def database_stats():
|
||||
"""
|
||||
Get database statistics including log count
|
||||
"""
|
||||
try:
|
||||
with sqlite3.connect(DATABASE) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT COUNT(*) FROM logs')
|
||||
total_logs = cursor.fetchone()[0]
|
||||
|
||||
cursor.execute('SELECT COUNT(DISTINCT hostname) FROM logs')
|
||||
unique_devices = cursor.fetchone()[0]
|
||||
|
||||
cursor.execute('SELECT MIN(timestamp), MAX(timestamp) FROM logs')
|
||||
date_range = cursor.fetchone()
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"total_logs": total_logs,
|
||||
"unique_devices": unique_devices,
|
||||
"earliest_log": date_range[0],
|
||||
"latest_log": date_range[1]
|
||||
}), 200
|
||||
|
||||
except sqlite3.Error as e:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": f"Database error: {str(e)}"
|
||||
}), 500
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": f"Unexpected error: {str(e)}"
|
||||
}), 500
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=80)
|
||||
210
templates/dashboard.html
Normal file
210
templates/dashboard.html
Normal file
@@ -0,0 +1,210 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Device Logs Dashboard</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||
<style>
|
||||
body {
|
||||
background-color: #f8f9fa;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #343a40;
|
||||
}
|
||||
.table-container {
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
padding: 20px;
|
||||
}
|
||||
.table {
|
||||
margin-bottom: 0;
|
||||
table-layout: fixed; /* Ensures consistent column widths */
|
||||
width: 100%; /* Makes the table take up the full container width */
|
||||
}
|
||||
.table th, .table td {
|
||||
text-align: center;
|
||||
word-wrap: break-word; /* Ensures long text wraps within the cell */
|
||||
}
|
||||
.table th:nth-child(1), .table td:nth-child(1) {
|
||||
width: 20%; /* Hostname column */
|
||||
}
|
||||
.table th:nth-child(2), .table td:nth-child(2) {
|
||||
width: 20%; /* Device IP column */
|
||||
}
|
||||
.table th:nth-child(3), .table td:nth-child(3) {
|
||||
width: 20%; /* Nume Masa column */
|
||||
}
|
||||
.table th:nth-child(4), .table td:nth-child(4) {
|
||||
width: 20%; /* Timestamp column */
|
||||
}
|
||||
.table th:nth-child(5), .table td:nth-child(5) {
|
||||
width: 20%; /* Event Description column */
|
||||
}
|
||||
.refresh-timer {
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
font-size: 1.2rem;
|
||||
color: #343a40;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
// Countdown timer for refresh
|
||||
let countdown = 30; // 30 seconds
|
||||
function updateTimer() {
|
||||
document.getElementById('refresh-timer').innerText = countdown;
|
||||
countdown--;
|
||||
if (countdown < 0) {
|
||||
location.reload(); // Refresh the page
|
||||
}
|
||||
}
|
||||
setInterval(updateTimer, 1000); // Update every second
|
||||
|
||||
// Database reset functionality
|
||||
async function resetDatabase() {
|
||||
try {
|
||||
// First, get database statistics
|
||||
const statsResponse = await fetch('/database_stats');
|
||||
const stats = await statsResponse.json();
|
||||
|
||||
if (!stats.success) {
|
||||
alert('❌ Error getting database statistics:\n' + stats.error);
|
||||
return;
|
||||
}
|
||||
|
||||
const totalLogs = stats.total_logs;
|
||||
const uniqueDevices = stats.unique_devices;
|
||||
|
||||
if (totalLogs === 0) {
|
||||
alert('ℹ️ Database is already empty!\nNo logs to delete.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show confirmation dialog with detailed statistics
|
||||
const confirmed = confirm(
|
||||
`⚠️ WARNING: Database Reset Operation ⚠️\n\n` +
|
||||
`This will permanently delete:\n` +
|
||||
`• ${totalLogs} log entries\n` +
|
||||
`• Data from ${uniqueDevices} unique devices\n` +
|
||||
`• Date range: ${stats.earliest_log} to ${stats.latest_log}\n\n` +
|
||||
`⚠️ ALL DEVICE HISTORY WILL BE LOST ⚠️\n\n` +
|
||||
`This action cannot be undone!\n\n` +
|
||||
`Are you absolutely sure you want to proceed?`
|
||||
);
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Second confirmation for safety
|
||||
const doubleConfirmed = confirm(
|
||||
`🚨 FINAL CONFIRMATION 🚨\n\n` +
|
||||
`You are about to permanently DELETE:\n` +
|
||||
`✗ ${totalLogs} log entries\n` +
|
||||
`✗ ${uniqueDevices} device histories\n\n` +
|
||||
`This is your LAST CHANCE to cancel!\n\n` +
|
||||
`Click OK to proceed with deletion.`
|
||||
);
|
||||
|
||||
if (!doubleConfirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading indicator
|
||||
const button = event.target;
|
||||
const originalText = button.innerHTML;
|
||||
button.innerHTML = '<span class="spinner-border spinner-border-sm" role="status"></span> Clearing Database...';
|
||||
button.disabled = true;
|
||||
|
||||
// Send reset request
|
||||
const resetResponse = await fetch('/reset_database', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const result = await resetResponse.json();
|
||||
|
||||
if (result.success) {
|
||||
alert(
|
||||
`✅ Database Reset Completed Successfully!\n\n` +
|
||||
`Operation Summary:\n` +
|
||||
`• ${result.deleted_count} log entries deleted\n` +
|
||||
`• Database schema reinitialized\n` +
|
||||
`• Reset timestamp: ${result.timestamp}\n\n` +
|
||||
`The dashboard will refresh to show the clean database.`
|
||||
);
|
||||
location.reload(); // Refresh to show empty database
|
||||
} else {
|
||||
alert('❌ Database Reset Failed:\n' + result.error);
|
||||
button.innerHTML = originalText;
|
||||
button.disabled = false;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
alert('❌ Network Error:\n' + error.message);
|
||||
// Restore button if it was changed
|
||||
try {
|
||||
const button = event.target;
|
||||
button.innerHTML = '<i class="fas fa-trash-alt"></i> Clear Database';
|
||||
button.disabled = false;
|
||||
} catch (e) {
|
||||
// Ignore if button restoration fails
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container mt-5">
|
||||
<h1 class="mb-4">Device Logs Dashboard</h1>
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div class="refresh-timer">
|
||||
Time until refresh: <span id="refresh-timer">30</span> seconds
|
||||
</div>
|
||||
<div>
|
||||
<a href="/unique_devices" class="btn btn-secondary">View Unique Devices</a>
|
||||
<a href="/device_management" class="btn btn-primary">Device Management</a>
|
||||
<a href="/server_logs" class="btn btn-info" title="View server operations and system logs">
|
||||
<i class="fas fa-server"></i> Server Logs
|
||||
</a>
|
||||
<button class="btn btn-danger" onclick="resetDatabase()" title="Clear all logs and reset database">
|
||||
<i class="fas fa-trash-alt"></i> Clear Database
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>Hostname</th>
|
||||
<th>Device IP</th>
|
||||
<th>Nume Masa</th>
|
||||
<th>Timestamp</th>
|
||||
<th>Event Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for log in logs %}
|
||||
<tr>
|
||||
<td><a href="hostname_logs/{{ log[0] }}">{{ log[0] }}</a></td>
|
||||
<td>{{ log[1] }}</td>
|
||||
<td><a href="/device_logs/{{ log[2] }}">{{ log[2] }}</a></td>
|
||||
<td>{{ log[3] }}</td>
|
||||
<td>{{ log[4] }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<footer>
|
||||
<p class="text-center mt-4">© 2023 Device Logs Dashboard. All rights reserved.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
89
templates/device_logs.html
Normal file
89
templates/device_logs.html
Normal file
@@ -0,0 +1,89 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Logs for Device: {{ nume_masa }}</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
|
||||
<style>
|
||||
body {
|
||||
background-color: #f8f9fa;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #343a40;
|
||||
}
|
||||
.table-container {
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
padding: 20px;
|
||||
}
|
||||
.table {
|
||||
margin-bottom: 0;
|
||||
table-layout: fixed; /* Ensures consistent column widths */
|
||||
width: 100%; /* Makes the table take up the full container width */
|
||||
}
|
||||
.table th, .table td {
|
||||
text-align: center;
|
||||
word-wrap: break-word; /* Ensures long text wraps within the cell */
|
||||
}
|
||||
.table th:nth-child(1), .table td:nth-child(1) {
|
||||
width: 20%; /* Device ID column */
|
||||
}
|
||||
.table th:nth-child(2), .table td:nth-child(2) {
|
||||
width: 20%; /* Nume Masa column */
|
||||
}
|
||||
.table th:nth-child(3), .table td:nth-child(3) {
|
||||
width: 30%; /* Timestamp column */
|
||||
}
|
||||
.table th:nth-child(4), .table td:nth-child(4) {
|
||||
width: 30%; /* Event Description column */
|
||||
}
|
||||
.back-button {
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container mt-5">
|
||||
<h1 class="mb-4">Logs for Device: {{ nume_masa }}</h1>
|
||||
<div class="back-button">
|
||||
<a href="/dashboard" class="btn btn-primary">Back to Dashboard</a>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>Device ID</th>
|
||||
<th>Nume Masa</th>
|
||||
<th>Timestamp</th>
|
||||
<th>Event Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% if logs %}
|
||||
{% for log in logs %}
|
||||
<tr>
|
||||
<td>{{ log[0] }}</td>
|
||||
<td>{{ log[1] }}</td>
|
||||
<td>{{ log[2] }}</td>
|
||||
<td>{{ log[3] }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="4">No logs found for this device.</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<footer>
|
||||
<p class="text-center mt-4">© 2023 Device Logs Dashboard. All rights reserved.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
505
templates/device_management.html
Normal file
505
templates/device_management.html
Normal file
@@ -0,0 +1,505 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Device Management</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||
<style>
|
||||
body {
|
||||
background-color: #f8f9fa;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #343a40;
|
||||
}
|
||||
.card {
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.device-card {
|
||||
border-left: 4px solid #007bff;
|
||||
}
|
||||
.status-online {
|
||||
color: #28a745;
|
||||
}
|
||||
.status-offline {
|
||||
color: #dc3545;
|
||||
}
|
||||
.command-buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
.back-button {
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
.search-container {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.search-input {
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.loading {
|
||||
display: none;
|
||||
}
|
||||
.result-container {
|
||||
margin-top: 15px;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
display: none;
|
||||
}
|
||||
.result-success {
|
||||
background-color: #d4edda;
|
||||
border: 1px solid #c3e6cb;
|
||||
color: #155724;
|
||||
}
|
||||
.result-error {
|
||||
background-color: #f8d7da;
|
||||
border: 1px solid #f5c6cb;
|
||||
color: #721c24;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container mt-5">
|
||||
<h1 class="mb-4">Device Management</h1>
|
||||
|
||||
<div class="back-button">
|
||||
<a href="/dashboard" class="btn btn-primary">Back to Dashboard</a>
|
||||
<a href="/unique_devices" class="btn btn-secondary">View Unique Devices</a>
|
||||
<a href="/server_logs" class="btn btn-info" title="View server operations and system logs">
|
||||
<i class="fas fa-server"></i> Server Logs
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Search Filter -->
|
||||
<div class="search-container">
|
||||
<div class="search-input">
|
||||
<input type="text" id="searchInput" class="form-control" placeholder="Search devices by hostname or IP...">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bulk Operations -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5>Bulk Operations</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<label for="bulkCommand" class="form-label">Select Command:</label>
|
||||
<select class="form-select" id="bulkCommand">
|
||||
<option value="">Select a command...</option>
|
||||
<option value="sudo apt update">Update Package Lists</option>
|
||||
<option value="sudo apt upgrade -y">Upgrade Packages</option>
|
||||
<option value="sudo apt autoremove -y">Remove Unused Packages</option>
|
||||
<option value="df -h">Check Disk Space</option>
|
||||
<option value="free -m">Check Memory Usage</option>
|
||||
<option value="uptime">Check Uptime</option>
|
||||
<option value="sudo systemctl restart networking">Restart Networking</option>
|
||||
<option value="sudo reboot">Reboot Device</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label"> </label>
|
||||
<div>
|
||||
<button class="btn btn-warning" onclick="executeOnAllDevices()">Execute on All Devices</button>
|
||||
<button class="btn btn-info" onclick="executeOnSelectedDevices()">Execute on Selected</button>
|
||||
<button class="btn btn-danger" onclick="autoUpdateAllDevices()" title="Auto-update all devices to latest app.py version">
|
||||
Auto Update All
|
||||
</button>
|
||||
<button class="btn btn-dark" onclick="autoUpdateSelectedDevices()" title="Auto-update selected devices">
|
||||
Auto Update Selected
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="result-container" id="bulkResult"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Device List -->
|
||||
<div id="deviceContainer">
|
||||
{% for device in devices %}
|
||||
<div class="card device-card" data-hostname="{{ device[0] }}" data-ip="{{ device[1] }}">
|
||||
<div class="card-header">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-6">
|
||||
<h6 class="mb-0">
|
||||
<input type="checkbox" class="device-checkbox me-2" value="{{ device[1] }}">
|
||||
<strong>{{ device[0] }}</strong> ({{ device[1] }})
|
||||
</h6>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<small class="text-muted">Last seen: {{ device[2] }}</small>
|
||||
</div>
|
||||
<div class="col-md-3 text-end">
|
||||
<span class="badge bg-secondary status" id="status-{{ device[1] }}">Checking...</span>
|
||||
<button class="btn btn-sm btn-outline-info" onclick="checkDeviceStatus('{{ device[1] }}')">
|
||||
Refresh Status
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<select class="form-select command-select" id="command-{{ device[1] }}">
|
||||
<option value="">Select a command...</option>
|
||||
<option value="sudo apt update">Update Package Lists</option>
|
||||
<option value="sudo apt upgrade -y">Upgrade Packages</option>
|
||||
<option value="sudo apt autoremove -y">Remove Unused Packages</option>
|
||||
<option value="df -h">Check Disk Space</option>
|
||||
<option value="free -m">Check Memory Usage</option>
|
||||
<option value="uptime">Check Uptime</option>
|
||||
<option value="sudo systemctl restart networking">Restart Networking</option>
|
||||
<option value="sudo reboot">Reboot Device</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<button class="btn btn-success" onclick="executeCommand('{{ device[1] }}')">
|
||||
Execute Command
|
||||
</button>
|
||||
<button class="btn btn-warning" onclick="autoUpdateDevice('{{ device[1] }}')" title="Auto-update app.py to latest version">
|
||||
Auto Update
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="result-container" id="result-{{ device[1] }}"></div>
|
||||
<div class="loading" id="loading-{{ device[1] }}">
|
||||
<div class="spinner-border spinner-border-sm" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
Executing command...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
// Search functionality
|
||||
document.getElementById('searchInput').addEventListener('keyup', function() {
|
||||
const filter = this.value.toLowerCase();
|
||||
const devices = document.querySelectorAll('.device-card');
|
||||
|
||||
devices.forEach(device => {
|
||||
const hostname = device.dataset.hostname.toLowerCase();
|
||||
const ip = device.dataset.ip.toLowerCase();
|
||||
|
||||
if (hostname.includes(filter) || ip.includes(filter)) {
|
||||
device.style.display = '';
|
||||
} else {
|
||||
device.style.display = 'none';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Check device status
|
||||
async function checkDeviceStatus(deviceIp) {
|
||||
const statusElement = document.getElementById(`status-${deviceIp}`);
|
||||
statusElement.textContent = 'Checking...';
|
||||
statusElement.className = 'badge bg-secondary';
|
||||
|
||||
try {
|
||||
const response = await fetch(`/device_status/${deviceIp}`);
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
statusElement.textContent = 'Online';
|
||||
statusElement.className = 'badge bg-success';
|
||||
} else {
|
||||
statusElement.textContent = 'Offline';
|
||||
statusElement.className = 'badge bg-danger';
|
||||
}
|
||||
} catch (error) {
|
||||
statusElement.textContent = 'Error';
|
||||
statusElement.className = 'badge bg-danger';
|
||||
}
|
||||
}
|
||||
|
||||
// Execute command on single device
|
||||
async function executeCommand(deviceIp) {
|
||||
const commandSelect = document.getElementById(`command-${deviceIp}`);
|
||||
const command = commandSelect.value;
|
||||
|
||||
if (!command) {
|
||||
alert('Please select a command first');
|
||||
return;
|
||||
}
|
||||
|
||||
const loadingElement = document.getElementById(`loading-${deviceIp}`);
|
||||
const resultElement = document.getElementById(`result-${deviceIp}`);
|
||||
|
||||
loadingElement.style.display = 'block';
|
||||
resultElement.style.display = 'none';
|
||||
|
||||
try {
|
||||
const response = await fetch('/execute_command', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
device_ip: deviceIp,
|
||||
command: command
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
loadingElement.style.display = 'none';
|
||||
resultElement.style.display = 'block';
|
||||
|
||||
if (result.success) {
|
||||
resultElement.className = 'result-container result-success';
|
||||
resultElement.innerHTML = `
|
||||
<strong>Success:</strong> ${result.result.message}<br>
|
||||
<small><strong>Output:</strong><br><pre>${result.result.output}</pre></small>
|
||||
`;
|
||||
} else {
|
||||
resultElement.className = 'result-container result-error';
|
||||
resultElement.innerHTML = `<strong>Error:</strong> ${result.error}`;
|
||||
}
|
||||
} catch (error) {
|
||||
loadingElement.style.display = 'none';
|
||||
resultElement.style.display = 'block';
|
||||
resultElement.className = 'result-container result-error';
|
||||
resultElement.innerHTML = `<strong>Network Error:</strong> ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Execute command on all devices
|
||||
async function executeOnAllDevices() {
|
||||
const command = document.getElementById('bulkCommand').value;
|
||||
if (!command) {
|
||||
alert('Please select a command first');
|
||||
return;
|
||||
}
|
||||
|
||||
const deviceIps = Array.from(document.querySelectorAll('.device-card')).map(card => card.dataset.ip);
|
||||
await executeBulkCommand(deviceIps, command);
|
||||
}
|
||||
|
||||
// Execute command on selected devices
|
||||
async function executeOnSelectedDevices() {
|
||||
const command = document.getElementById('bulkCommand').value;
|
||||
if (!command) {
|
||||
alert('Please select a command first');
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedIps = Array.from(document.querySelectorAll('.device-checkbox:checked')).map(cb => cb.value);
|
||||
if (selectedIps.length === 0) {
|
||||
alert('Please select at least one device');
|
||||
return;
|
||||
}
|
||||
|
||||
await executeBulkCommand(selectedIps, command);
|
||||
}
|
||||
|
||||
// Execute bulk command
|
||||
async function executeBulkCommand(deviceIps, command) {
|
||||
const resultElement = document.getElementById('bulkResult');
|
||||
|
||||
resultElement.style.display = 'block';
|
||||
resultElement.className = 'result-container';
|
||||
resultElement.innerHTML = '<div class="spinner-border spinner-border-sm" role="status"></div> Executing commands...';
|
||||
|
||||
try {
|
||||
const response = await fetch('/execute_command_bulk', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
device_ips: deviceIps,
|
||||
command: command
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
let html = '<h6>Bulk Execution Results:</h6>';
|
||||
let successCount = 0;
|
||||
|
||||
for (const [ip, deviceResult] of Object.entries(result.results)) {
|
||||
if (deviceResult.success) {
|
||||
successCount++;
|
||||
html += `<div class="alert alert-success alert-sm">✓ ${ip}: ${deviceResult.result.message}</div>`;
|
||||
} else {
|
||||
html += `<div class="alert alert-danger alert-sm">✗ ${ip}: ${deviceResult.error}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
html += `<div class="mt-2"><strong>Summary:</strong> ${successCount}/${deviceIps.length} devices succeeded</div>`;
|
||||
|
||||
resultElement.className = 'result-container result-success';
|
||||
resultElement.innerHTML = html;
|
||||
|
||||
} catch (error) {
|
||||
resultElement.className = 'result-container result-error';
|
||||
resultElement.innerHTML = `<strong>Network Error:</strong> ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-update functionality
|
||||
async function autoUpdateDevice(deviceIp) {
|
||||
const resultElement = document.getElementById(`result-${deviceIp}`);
|
||||
const loadingElement = document.getElementById(`loading-${deviceIp}`);
|
||||
|
||||
try {
|
||||
// Show loading
|
||||
loadingElement.style.display = 'block';
|
||||
resultElement.className = 'result-container';
|
||||
resultElement.innerHTML = '';
|
||||
|
||||
const response = await fetch('/auto_update_devices', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
device_ips: [deviceIp]
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
loadingElement.style.display = 'none';
|
||||
|
||||
if (result.results && result.results.length > 0) {
|
||||
const deviceResult = result.results[0];
|
||||
|
||||
if (deviceResult.success) {
|
||||
if (deviceResult.status === 'no_update_needed') {
|
||||
resultElement.className = 'result-container result-success';
|
||||
resultElement.innerHTML = `<strong>No Update Needed:</strong> Device is already running version ${deviceResult.new_version || 'latest'}`;
|
||||
} else {
|
||||
resultElement.className = 'result-container result-success';
|
||||
resultElement.innerHTML = `<strong>Update Success:</strong> ${deviceResult.message}<br>
|
||||
<small>Updated from v${deviceResult.old_version} to v${deviceResult.new_version}</small><br>
|
||||
<small class="text-warning">Device is restarting...</small>`;
|
||||
}
|
||||
} else {
|
||||
resultElement.className = 'result-container result-error';
|
||||
resultElement.innerHTML = `<strong>Update Failed:</strong> ${deviceResult.error}`;
|
||||
}
|
||||
} else {
|
||||
resultElement.className = 'result-container result-error';
|
||||
resultElement.innerHTML = '<strong>Error:</strong> No response from server';
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
loadingElement.style.display = 'none';
|
||||
resultElement.className = 'result-container result-error';
|
||||
resultElement.innerHTML = `<strong>Network Error:</strong> ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function autoUpdateAllDevices() {
|
||||
if (!confirm('Are you sure you want to auto-update ALL devices? This will restart all devices.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
await performBulkAutoUpdate('all');
|
||||
}
|
||||
|
||||
async function autoUpdateSelectedDevices() {
|
||||
const selectedDevices = Array.from(document.querySelectorAll('.device-checkbox:checked'))
|
||||
.map(cb => cb.value);
|
||||
|
||||
if (selectedDevices.length === 0) {
|
||||
alert('Please select at least one device');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm(`Are you sure you want to auto-update ${selectedDevices.length} selected device(s)? This will restart the selected devices.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await performBulkAutoUpdate('selected');
|
||||
}
|
||||
|
||||
async function performBulkAutoUpdate(mode) {
|
||||
const resultElement = document.getElementById('bulkResult');
|
||||
|
||||
try {
|
||||
// Determine which devices to update
|
||||
let deviceIps;
|
||||
if (mode === 'all') {
|
||||
deviceIps = Array.from(document.querySelectorAll('.device-card'))
|
||||
.map(card => card.dataset.ip);
|
||||
} else {
|
||||
deviceIps = Array.from(document.querySelectorAll('.device-checkbox:checked'))
|
||||
.map(cb => cb.value);
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
resultElement.className = 'result-container';
|
||||
resultElement.innerHTML = `<div class="alert alert-info">
|
||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||
Auto-updating ${deviceIps.length} device(s)... This may take several minutes.
|
||||
</div>`;
|
||||
|
||||
const response = await fetch('/auto_update_devices', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
device_ips: deviceIps
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
let html = '<h6>Auto-Update Results:</h6>';
|
||||
let successCount = 0;
|
||||
|
||||
for (const deviceResult of result.results) {
|
||||
if (deviceResult.success) {
|
||||
successCount++;
|
||||
if (deviceResult.status === 'no_update_needed') {
|
||||
html += `<div class="alert alert-info alert-sm">ℹ ${deviceResult.device_ip}: Already up to date</div>`;
|
||||
} else {
|
||||
html += `<div class="alert alert-success alert-sm">✓ ${deviceResult.device_ip}: ${deviceResult.message}</div>`;
|
||||
}
|
||||
} else {
|
||||
html += `<div class="alert alert-danger alert-sm">✗ ${deviceResult.device_ip}: ${deviceResult.error}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
html += `<div class="mt-2"><strong>Summary:</strong> ${successCount}/${deviceIps.length} devices updated successfully</div>`;
|
||||
if (successCount > 0) {
|
||||
html += `<div class="alert alert-warning mt-2"><small>Note: Updated devices are restarting and may be temporarily unavailable.</small></div>`;
|
||||
}
|
||||
|
||||
resultElement.className = 'result-container result-success';
|
||||
resultElement.innerHTML = html;
|
||||
|
||||
} catch (error) {
|
||||
resultElement.className = 'result-container result-error';
|
||||
resultElement.innerHTML = `<strong>Network Error:</strong> ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Check status of all devices on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const devices = document.querySelectorAll('.device-card');
|
||||
devices.forEach(device => {
|
||||
const ip = device.dataset.ip;
|
||||
checkDeviceStatus(ip);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
89
templates/hostname_logs.html
Normal file
89
templates/hostname_logs.html
Normal file
@@ -0,0 +1,89 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Logs for Hostname: {{ hostname }}</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
|
||||
<style>
|
||||
body {
|
||||
background-color: #f8f9fa;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #343a40;
|
||||
}
|
||||
.table-container {
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
padding: 20px;
|
||||
}
|
||||
.table {
|
||||
margin-bottom: 0;
|
||||
table-layout: fixed; /* Ensures consistent column widths */
|
||||
width: 100%; /* Makes the table take up the full container width */
|
||||
}
|
||||
.table th, .table td {
|
||||
text-align: center;
|
||||
word-wrap: break-word; /* Ensures long text wraps within the cell */
|
||||
}
|
||||
.table th:nth-child(1), .table td:nth-child(1) {
|
||||
width: 25%; /* Device ID column */
|
||||
}
|
||||
.table th:nth-child(2), .table td:nth-child(2) {
|
||||
width: 25%; /* Nume Masa column */
|
||||
}
|
||||
.table th:nth-child(3), .table td:nth-child(3) {
|
||||
width: 25%; /* Timestamp column */
|
||||
}
|
||||
.table th:nth-child(4), .table td:nth-child(4) {
|
||||
width: 25%; /* Event Description column */
|
||||
}
|
||||
.back-button {
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container mt-5">
|
||||
<h1 class="mb-4">Logs for Hostname: {{ hostname }}</h1>
|
||||
<div class="back-button">
|
||||
<a href="/dashboard" class="btn btn-primary">Back to Dashboard</a>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>Device ID</th>
|
||||
<th>Nume Masa</th>
|
||||
<th>Timestamp</th>
|
||||
<th>Event Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% if logs %}
|
||||
{% for log in logs %}
|
||||
<tr>
|
||||
<td>{{ log[0] }}</td>
|
||||
<td>{{ log[1] }}</td>
|
||||
<td>{{ log[2] }}</td>
|
||||
<td>{{ log[3] }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="4">No logs found for this hostname.</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<footer>
|
||||
<p class="text-center mt-4">© 2023 Device Logs Dashboard. All rights reserved.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
230
templates/server_logs.html
Normal file
230
templates/server_logs.html
Normal file
@@ -0,0 +1,230 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Server Logs - System Operations</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||
<style>
|
||||
body {
|
||||
background-color: #f8f9fa;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #343a40;
|
||||
}
|
||||
.table-container {
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
padding: 20px;
|
||||
}
|
||||
.table {
|
||||
margin-bottom: 0;
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
}
|
||||
.table th, .table td {
|
||||
text-align: center;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.table th:nth-child(1), .table td:nth-child(1) {
|
||||
width: 15%; /* Hostname column */
|
||||
}
|
||||
.table th:nth-child(2), .table td:nth-child(2) {
|
||||
width: 15%; /* Device IP column */
|
||||
}
|
||||
.table th:nth-child(3), .table td:nth-child(3) {
|
||||
width: 15%; /* Operation Type column */
|
||||
}
|
||||
.table th:nth-child(4), .table td:nth-child(4) {
|
||||
width: 20%; /* Timestamp column */
|
||||
}
|
||||
.table th:nth-child(5), .table td:nth-child(5) {
|
||||
width: 35%; /* Event Description column */
|
||||
}
|
||||
.refresh-timer {
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
font-size: 1.2rem;
|
||||
color: #343a40;
|
||||
}
|
||||
.server-log-header {
|
||||
background: linear-gradient(135deg, #dc3545, #6c757d);
|
||||
color: white;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.server-badge {
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
.operation-badge {
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.8em;
|
||||
font-weight: bold;
|
||||
}
|
||||
.operation-system { background-color: #6c757d; color: white; }
|
||||
.operation-auto_update { background-color: #ffc107; color: black; }
|
||||
.operation-command { background-color: #0d6efd; color: white; }
|
||||
.operation-reset { background-color: #dc3545; color: white; }
|
||||
.stats-row {
|
||||
background-color: #f8f9fa;
|
||||
border: 2px solid #dee2e6;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
// Countdown timer for refresh
|
||||
let countdown = 30;
|
||||
function updateTimer() {
|
||||
document.getElementById('refresh-timer').innerText = countdown;
|
||||
countdown--;
|
||||
if (countdown < 0) {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
setInterval(updateTimer, 1000);
|
||||
|
||||
// Function to get operation type from description
|
||||
function getOperationType(description) {
|
||||
if (description.toLowerCase().includes('auto-update') || description.toLowerCase().includes('auto_update')) {
|
||||
return 'auto_update';
|
||||
} else if (description.toLowerCase().includes('command')) {
|
||||
return 'command';
|
||||
} else if (description.toLowerCase().includes('reset') || description.toLowerCase().includes('clear')) {
|
||||
return 'reset';
|
||||
} else {
|
||||
return 'system';
|
||||
}
|
||||
}
|
||||
|
||||
// Apply operation badges after page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const rows = document.querySelectorAll('tbody tr:not(.stats-row)');
|
||||
rows.forEach(row => {
|
||||
const descCell = row.cells[4];
|
||||
const operationCell = row.cells[2];
|
||||
const description = descCell.textContent;
|
||||
const operationType = getOperationType(description);
|
||||
|
||||
operationCell.innerHTML = `<span class="operation-badge operation-${operationType}">${operationType.toUpperCase().replace('_', '-')}</span>`;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container mt-5">
|
||||
<div class="server-log-header text-center">
|
||||
<h1 class="mb-2">
|
||||
<i class="fas fa-server"></i> Server Operations Log
|
||||
</h1>
|
||||
<p class="mb-0">System operations, auto-updates, database resets, and server commands</p>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div class="refresh-timer">
|
||||
<i class="fas fa-clock"></i> Auto-refresh: <span id="refresh-timer">30</span> seconds
|
||||
</div>
|
||||
<div>
|
||||
<a href="/dashboard" class="btn btn-primary">
|
||||
<i class="fas fa-tachometer-alt"></i> Dashboard
|
||||
</a>
|
||||
<a href="/device_management" class="btn btn-success">
|
||||
<i class="fas fa-cogs"></i> Device Management
|
||||
</a>
|
||||
<a href="/unique_devices" class="btn btn-secondary">
|
||||
<i class="fas fa-list"></i> Unique Devices
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
{% if logs %}
|
||||
<!-- Statistics Row -->
|
||||
<div class="mb-3 p-3 stats-row rounded">
|
||||
<div class="row text-center">
|
||||
<div class="col-md-3">
|
||||
<h5 class="text-primary">{{ logs|length }}</h5>
|
||||
<small>Total Operations</small>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<h5 class="text-success">
|
||||
{% set system_ops = logs|selectattr('2', 'equalto', 'SYSTEM')|list|length %}
|
||||
{{ system_ops }}
|
||||
</h5>
|
||||
<small>System Operations</small>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<h5 class="text-warning">
|
||||
{% set auto_updates = 0 %}
|
||||
{% for log in logs %}
|
||||
{% if 'auto' in log[4]|lower or 'update' in log[4]|lower %}
|
||||
{% set auto_updates = auto_updates + 1 %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{{ auto_updates }}
|
||||
</h5>
|
||||
<small>Auto-Updates</small>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<h5 class="text-danger">
|
||||
{% set resets = 0 %}
|
||||
{% for log in logs %}
|
||||
{% if 'reset' in log[4]|lower or 'clear' in log[4]|lower %}
|
||||
{% set resets = resets + 1 %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{{ resets }}
|
||||
</h5>
|
||||
<small>Database Resets</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th><i class="fas fa-server"></i> Source</th>
|
||||
<th><i class="fas fa-network-wired"></i> IP Address</th>
|
||||
<th><i class="fas fa-cog"></i> Operation</th>
|
||||
<th><i class="fas fa-clock"></i> Timestamp</th>
|
||||
<th><i class="fas fa-info-circle"></i> Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for log in logs %}
|
||||
<tr>
|
||||
<td><span class="server-badge">{{ log[0] }}</span></td>
|
||||
<td>{{ log[1] }}</td>
|
||||
<td>{{ log[2] }}</td>
|
||||
<td>{{ log[3] }}</td>
|
||||
<td class="text-start">{{ log[4] }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="text-center py-5">
|
||||
<i class="fas fa-inbox fa-3x text-muted mb-3"></i>
|
||||
<h4 class="text-muted">No Server Operations Found</h4>
|
||||
<p class="text-muted">No server operations have been logged yet.</p>
|
||||
<a href="/dashboard" class="btn btn-primary">
|
||||
<i class="fas fa-arrow-left"></i> Back to Dashboard
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<p class="text-center mt-4">© 2025 Server Operations Dashboard. All rights reserved.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
157
templates/unique_devices.html
Normal file
157
templates/unique_devices.html
Normal file
@@ -0,0 +1,157 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Unique Devices</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
|
||||
<style>
|
||||
body {
|
||||
background-color: #f8f9fa;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #343a40;
|
||||
}
|
||||
.table-container {
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
padding: 20px;
|
||||
}
|
||||
.table {
|
||||
margin-bottom: 0;
|
||||
table-layout: fixed; /* Ensures consistent column widths */
|
||||
width: 100%; /* Makes the table take up the full container width */
|
||||
}
|
||||
.table th, .table td {
|
||||
text-align: center;
|
||||
word-wrap: break-word; /* Ensures long text wraps within the cell */
|
||||
}
|
||||
.table th:nth-child(1), .table td:nth-child(1) {
|
||||
width: 25%; /* Hostname column */
|
||||
}
|
||||
.table th:nth-child(2), .table td:nth-child(2) {
|
||||
width: 25%; /* Device IP column */
|
||||
}
|
||||
.table th:nth-child(3), .table td:nth-child(3) {
|
||||
width: 25%; /* Last Log column */
|
||||
}
|
||||
.table th:nth-child(4), .table td:nth-child(4) {
|
||||
width: 25%; /* Event Description column */
|
||||
}
|
||||
.back-button {
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
.search-container {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.search-input {
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.no-results {
|
||||
display: none;
|
||||
text-align: center;
|
||||
color: #6c757d;
|
||||
font-style: italic;
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container mt-5">
|
||||
<h1 class="mb-4">Unique Devices</h1>
|
||||
<div class="back-button">
|
||||
<a href="/dashboard" class="btn btn-primary">Back to Dashboard</a>
|
||||
</div>
|
||||
|
||||
<!-- Search Filter -->
|
||||
<div class="search-container">
|
||||
<div class="search-input">
|
||||
<input type="text" id="searchInput" class="form-control" placeholder="Search devices by hostname, IP, or event description...">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<table class="table table-striped table-bordered" id="devicesTable">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>Hostname</th>
|
||||
<th>Device IP</th>
|
||||
<th>Last Log</th>
|
||||
<th>Event Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for device in devices %}
|
||||
<tr>
|
||||
<!-- Make the Hostname column clickable -->
|
||||
<td>
|
||||
<a href="/hostname_logs/{{ device[0] }}">{{ device[0] }}</a>
|
||||
</td>
|
||||
<td>{{ device[1] }}</td> <!-- Device IP -->
|
||||
<td>{{ device[2] }}</td> <!-- Last Log -->
|
||||
<td>{{ device[3] }}</td> <!-- Event Description -->
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div id="noResults" class="no-results">
|
||||
No devices found matching your search criteria.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- JavaScript for filtering -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
const table = document.getElementById('devicesTable');
|
||||
const tableBody = table.getElementsByTagName('tbody')[0];
|
||||
const noResultsDiv = document.getElementById('noResults');
|
||||
|
||||
searchInput.addEventListener('keyup', function() {
|
||||
const filter = this.value.toLowerCase();
|
||||
const rows = tableBody.getElementsByTagName('tr');
|
||||
let visibleRowCount = 0;
|
||||
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const row = rows[i];
|
||||
const cells = row.getElementsByTagName('td');
|
||||
let found = false;
|
||||
|
||||
// Search through all cells in the row
|
||||
for (let j = 0; j < cells.length; j++) {
|
||||
const cellText = cells[j].textContent || cells[j].innerText;
|
||||
if (cellText.toLowerCase().indexOf(filter) > -1) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (found) {
|
||||
row.style.display = '';
|
||||
visibleRowCount++;
|
||||
} else {
|
||||
row.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Show/hide "no results" message
|
||||
if (visibleRowCount === 0 && filter !== '') {
|
||||
noResultsDiv.style.display = 'block';
|
||||
} else {
|
||||
noResultsDiv.style.display = 'none';
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<footer>
|
||||
<p class="text-center mt-4">© 2023 Unique Devices Dashboard. All rights reserved.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user