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:
2025-08-14 15:53:00 +03:00
commit 42989aa8df
14 changed files with 2029 additions and 0 deletions

210
templates/dashboard.html Normal file
View 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">&copy; 2023 Device Logs Dashboard. All rights reserved.</p>
</footer>
</body>
</html>

View 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">&copy; 2023 Device Logs Dashboard. All rights reserved.</p>
</footer>
</body>
</html>

View 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">&nbsp;</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>

View 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">&copy; 2023 Device Logs Dashboard. All rights reserved.</p>
</footer>
</body>
</html>

230
templates/server_logs.html Normal file
View 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">&copy; 2025 Server Operations Dashboard. All rights reserved.</p>
</footer>
</body>
</html>

View 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">&copy; 2023 Unique Devices Dashboard. All rights reserved.</p>
</footer>
</body>
</html>