updated to allow config of the new server
This commit is contained in:
@@ -217,10 +217,13 @@ def _register_context_processors(app):
|
||||
try:
|
||||
with get_db().get_session() as session:
|
||||
pending_wmt_count = session.query(WMTUpdateRequest).filter_by(status='pending').count()
|
||||
unconfigured_count = session.query(WMTUpdateRequest).filter_by(status='to_be_configured').count()
|
||||
except Exception:
|
||||
pending_wmt_count = 0
|
||||
unconfigured_count = 0
|
||||
return {
|
||||
'app_name': 'Enhanced Server Monitoring',
|
||||
'app_version': '2.0.0',
|
||||
'pending_wmt_count': pending_wmt_count,
|
||||
'unconfigured_count': unconfigured_count,
|
||||
}
|
||||
+64
-1
@@ -8,10 +8,12 @@ import hashlib
|
||||
from datetime import datetime
|
||||
from app.services.log_service import LogCompressionService
|
||||
from app.services.file_service import FileUploadService
|
||||
from app.models import Device, LogEntry, FileUpload
|
||||
from app.models import Device, LogEntry, FileUpload, WMTUpdateRequest
|
||||
from config.database_config import get_db
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Create blueprint
|
||||
logs_bp = Blueprint('logs', __name__, url_prefix='/api/logs')
|
||||
|
||||
@@ -68,6 +70,14 @@ def submit_log():
|
||||
'mac_address': data.get('mac_address') or (data.get('metadata') or {}).get('mac_address'),
|
||||
}
|
||||
|
||||
# ── Unconfigured device interception ──────────────────────────────────
|
||||
# A device with work_place=notconfig must NOT be added to the devices
|
||||
# table. Instead, create/refresh a "to_be_configured" update request so
|
||||
# it appears in the dedicated section on the Update Requests page.
|
||||
if data['nume_masa'].strip().lower() == 'notconfig':
|
||||
return _handle_notconfig_device(device_info, data.get('log_message', ''))
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
# Process log with compression
|
||||
result = log_service.process_log_message(
|
||||
device_info=device_info,
|
||||
@@ -204,6 +214,59 @@ def upload_log_file():
|
||||
'success': False
|
||||
}), 500
|
||||
|
||||
def _handle_notconfig_device(device_info: dict, log_message: str):
|
||||
"""
|
||||
A device reporting work_place='notconfig' is awaiting admin configuration.
|
||||
We must NOT create a Device record for it. Instead we create / refresh a
|
||||
WMTUpdateRequest with status='to_be_configured' so it surfaces under the
|
||||
dedicated tab on the Update Requests page.
|
||||
"""
|
||||
mac = (device_info.get('mac_address') or '').strip().lower()
|
||||
hostname = device_info.get('hostname', '')
|
||||
ip = device_info.get('device_ip', '')
|
||||
|
||||
try:
|
||||
with get_db().get_session() as session:
|
||||
existing = (
|
||||
session.query(WMTUpdateRequest)
|
||||
.filter_by(mac_address=mac, status='to_be_configured')
|
||||
.order_by(WMTUpdateRequest.submitted_at.desc())
|
||||
.first()
|
||||
) if mac else None
|
||||
|
||||
if existing:
|
||||
# Refresh the timestamp and any identity fields that may have changed
|
||||
existing.submitted_at = datetime.utcnow()
|
||||
existing.proposed_hostname = hostname or existing.proposed_hostname
|
||||
existing.proposed_device_ip = ip or existing.proposed_device_ip
|
||||
if log_message:
|
||||
existing.admin_notes = f"Last log: {log_message[:200]}"
|
||||
logger.debug(f'notconfig heartbeat refreshed for {mac or hostname}')
|
||||
else:
|
||||
req = WMTUpdateRequest(
|
||||
mac_address=mac or None,
|
||||
device_id=None,
|
||||
proposed_device_name=None,
|
||||
proposed_hostname=hostname or None,
|
||||
proposed_device_ip=ip or None,
|
||||
client_config_mtime=datetime.utcnow().isoformat(),
|
||||
submitted_at=datetime.utcnow(),
|
||||
status='to_be_configured',
|
||||
admin_notes=f"Last log: {log_message[:200]}" if log_message else None,
|
||||
)
|
||||
session.add(req)
|
||||
logger.info(f'New unconfigured device queued: {hostname} / {mac or "no-mac"}')
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'status': 'to_be_configured',
|
||||
'message': 'Device is not configured. Awaiting admin assignment.',
|
||||
}), 202
|
||||
except Exception as e:
|
||||
logger.error(f'Error handling notconfig device: {e}')
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@logs_bp.route('/query', methods=['GET'])
|
||||
def query_logs():
|
||||
"""
|
||||
|
||||
@@ -197,6 +197,38 @@ def submit_update_request():
|
||||
|
||||
# ── Outcome 3: unknown device ─────────────────────────────
|
||||
if not device:
|
||||
# Devices with work_place=notconfig are unconfigured and must not
|
||||
# be registered. Route them to their own "to_be_configured" bucket.
|
||||
if not proposed_name or proposed_name.lower() == 'notconfig':
|
||||
existing_tbc = (
|
||||
session.query(WMTUpdateRequest)
|
||||
.filter_by(mac_address=mac, status='to_be_configured')
|
||||
.order_by(WMTUpdateRequest.submitted_at.desc())
|
||||
.first()
|
||||
)
|
||||
if existing_tbc:
|
||||
existing_tbc.submitted_at = datetime.utcnow()
|
||||
existing_tbc.proposed_hostname = proposed_hostname or existing_tbc.proposed_hostname
|
||||
existing_tbc.proposed_device_ip = proposed_ip or existing_tbc.proposed_device_ip
|
||||
logger.debug(f'WMT notconfig heartbeat refreshed for {mac}')
|
||||
else:
|
||||
req = WMTUpdateRequest(
|
||||
mac_address=mac,
|
||||
device_id=None,
|
||||
proposed_device_name=None,
|
||||
proposed_hostname=proposed_hostname or None,
|
||||
proposed_device_ip=proposed_ip or None,
|
||||
client_config_mtime=data.get('client_config_mtime'),
|
||||
submitted_at=datetime.utcnow(),
|
||||
status='to_be_configured',
|
||||
)
|
||||
session.add(req)
|
||||
logger.info(f'WMT unconfigured device queued: {proposed_hostname} / {mac}')
|
||||
return jsonify({
|
||||
'status': 'to_be_configured',
|
||||
'message': 'Device has no work_place set. Awaiting admin assignment.',
|
||||
}), 202
|
||||
|
||||
# Check if a pending request with the same data already exists
|
||||
existing = (
|
||||
session.query(WMTUpdateRequest)
|
||||
|
||||
@@ -454,6 +454,8 @@ def device_edit(device_id):
|
||||
device.device_type = request.form.get('device_type', '').strip() or 'unknown'
|
||||
device.description = request.form.get('description', '').strip() or None
|
||||
device.os_version = request.form.get('os_version', '').strip() or None
|
||||
custom_url = request.form.get('custom_chrome_url', '').strip()
|
||||
device.custom_chrome_url = custom_url if custom_url else None
|
||||
device.config_updated_at = datetime.utcnow()
|
||||
device.info_reviewed_at = datetime.utcnow()
|
||||
flash('Device updated.', 'success')
|
||||
|
||||
+11
-34
@@ -37,31 +37,8 @@ def _get_or_create_global_config(session):
|
||||
|
||||
@wmt_web_bp.route('/')
|
||||
def index():
|
||||
"""WMT management dashboard."""
|
||||
try:
|
||||
with get_db().get_session() as session:
|
||||
global_cfg = _get_or_create_global_config(session)
|
||||
devices = session.query(Device).filter(Device.mac_address.isnot(None)).order_by(Device.nume_masa).all()
|
||||
pending_count = session.query(WMTUpdateRequest).filter_by(status='pending').count()
|
||||
recent_requests = (
|
||||
session.query(WMTUpdateRequest)
|
||||
.order_by(WMTUpdateRequest.submitted_at.desc())
|
||||
.limit(5)
|
||||
.all()
|
||||
)
|
||||
return render_template(
|
||||
'wmt/index.html',
|
||||
global_cfg=global_cfg,
|
||||
devices=devices,
|
||||
pending_count=pending_count,
|
||||
recent_requests=recent_requests,
|
||||
breadcrumbs=[{'url': url_for('wmt_web.index'), 'title': 'WMT Management'}],
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f'WMT dashboard error: {e}')
|
||||
flash(f'Error loading dashboard: {e}', 'error')
|
||||
return render_template('wmt/index.html', global_cfg=None, devices=[],
|
||||
pending_count=0, recent_requests=[])
|
||||
"""Retired: the WMT dashboard is merged into the unified Devices page."""
|
||||
return redirect(url_for('main.devices'))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -94,14 +71,14 @@ def settings():
|
||||
'wmt/settings.html',
|
||||
cfg=cfg,
|
||||
breadcrumbs=[
|
||||
{'url': url_for('wmt_web.index'), 'title': 'WMT Management'},
|
||||
{'url': url_for('wmt_web.settings'), 'title': 'Global Settings'},
|
||||
{'url': url_for('main.devices'), 'title': 'Devices'},
|
||||
{'url': url_for('wmt_web.settings'), 'title': 'WMT Settings'},
|
||||
],
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f'WMT settings error: {e}')
|
||||
flash(f'Error: {e}', 'error')
|
||||
return redirect(url_for('wmt_web.index'))
|
||||
return redirect(url_for('main.devices'))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -119,21 +96,23 @@ def update_requests():
|
||||
query = query.filter_by(status=status_filter)
|
||||
req_list = query.order_by(WMTUpdateRequest.submitted_at.desc()).all()
|
||||
pending_count = session.query(WMTUpdateRequest).filter_by(status='pending').count()
|
||||
unconfigured_count = session.query(WMTUpdateRequest).filter_by(status='to_be_configured').count()
|
||||
|
||||
return render_template(
|
||||
'wmt/requests.html',
|
||||
requests=req_list,
|
||||
status_filter=status_filter,
|
||||
pending_count=pending_count,
|
||||
unconfigured_count=unconfigured_count,
|
||||
breadcrumbs=[
|
||||
{'url': url_for('wmt_web.index'), 'title': 'WMT Management'},
|
||||
{'url': url_for('main.devices'), 'title': 'Devices'},
|
||||
{'url': url_for('wmt_web.update_requests'), 'title': 'Update Requests'},
|
||||
],
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f'WMT requests list error: {e}')
|
||||
flash(f'Error: {e}', 'error')
|
||||
return redirect(url_for('wmt_web.index'))
|
||||
return redirect(url_for('main.devices'))
|
||||
|
||||
|
||||
@wmt_web_bp.route('/requests/<int:req_id>/accept', methods=['POST'])
|
||||
@@ -256,8 +235,7 @@ def device_new():
|
||||
'wmt/device_form.html',
|
||||
device=None,
|
||||
breadcrumbs=[
|
||||
{'url': url_for('wmt_web.index'), 'title': 'WMT Management'},
|
||||
{'url': url_for('wmt_web.devices'), 'title': 'Devices'},
|
||||
{'url': url_for('main.devices'), 'title': 'Devices'},
|
||||
{'url': url_for('wmt_web.device_new'), 'title': 'New Device'},
|
||||
],
|
||||
)
|
||||
@@ -291,8 +269,7 @@ def device_edit(device_id):
|
||||
'wmt/device_form.html',
|
||||
device=device,
|
||||
breadcrumbs=[
|
||||
{'url': url_for('wmt_web.index'), 'title': 'WMT Management'},
|
||||
{'url': url_for('wmt_web.devices'), 'title': 'Devices'},
|
||||
{'url': url_for('main.devices'), 'title': 'Devices'},
|
||||
{'url': url_for('wmt_web.device_edit', device_id=device_id), 'title': 'Edit'},
|
||||
],
|
||||
)
|
||||
|
||||
+13
-31
@@ -867,33 +867,28 @@
|
||||
|
||||
<ul class="nav-menu">
|
||||
|
||||
<!-- ── Unified Devices (WMT + Live View health) ── -->
|
||||
<li class="nav-item" style="padding: 0 0px;">
|
||||
<a href="{{ url_for('main.devices') }}"
|
||||
class="nav-link {% if request.endpoint in ['main.devices','main.device_edit','main.device_detail','wmt_web.device_edit','wmt_web.device_new'] %}active{% endif %}">
|
||||
<i class="fas fa-desktop"></i>Devices
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<!-- ── WMT group ── -->
|
||||
<li class="nav-group" id="group-wmt">
|
||||
<div class="nav-group-header {% if request.endpoint and request.endpoint.startswith('wmt_web') %}open{% endif %}"
|
||||
onclick="toggleGroup('group-wmt')">
|
||||
<i class="fas fa-tablet-alt group-icon"></i>
|
||||
<span class="group-label">WMT</span>
|
||||
<!-- ── Monitoring group (Devices + WMT + Live View combined) ── -->
|
||||
{% set mon_endpoints = ['main.devices','main.device_detail','main.device_edit','main.logs','main.stats','main.templates'] %}
|
||||
{% set mon_active = request.endpoint in mon_endpoints or (request.endpoint and request.endpoint.startswith('wmt_web')) %}
|
||||
<li class="nav-group" id="group-monitoring">
|
||||
<div class="nav-group-header {% if mon_active %}open{% endif %}"
|
||||
onclick="toggleGroup('group-monitoring')">
|
||||
<i class="fas fa-desktop group-icon"></i>
|
||||
<span class="group-label">Monitoring</span>
|
||||
{% if pending_wmt_count > 0 %}<span class="group-badge">{{ pending_wmt_count }}</span>{% endif %}
|
||||
<i class="fas fa-chevron-right chevron"></i>
|
||||
</div>
|
||||
<ul class="nav-group-children {% if request.endpoint and request.endpoint.startswith('wmt_web') %}open{% endif %}" style="list-style:none;padding-left:10px;margin:0;">
|
||||
<ul class="nav-group-children {% if mon_active %}open{% endif %}" style="list-style:none;padding-left:10px;margin:0;">
|
||||
<li class="nav-item">
|
||||
<a href="{{ url_for('wmt_web.index') }}" class="nav-link {% if request.endpoint == 'wmt_web.index' %}active{% endif %}">
|
||||
<i class="fas fa-tachometer-alt"></i>Dashboard
|
||||
<a href="{{ url_for('main.devices') }}" class="nav-link {% if request.endpoint in ['main.devices','main.device_detail','main.device_edit','wmt_web.device_edit','wmt_web.device_new'] %}active{% endif %}">
|
||||
<i class="fas fa-network-wired"></i>Devices
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="{{ url_for('wmt_web.update_requests') }}" class="nav-link {% if request.endpoint == 'wmt_web.update_requests' %}active{% endif %}">
|
||||
<i class="fas fa-inbox"></i>Update Requests
|
||||
{% if pending_wmt_count > 0 %}<span class="badge bg-danger ms-auto">{{ pending_wmt_count }}</span>{% endif %}
|
||||
{% if unconfigured_count > 0 %}<span class="badge ms-1" style="background:#6f42c1;color:#fff">{{ unconfigured_count }}</span>{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
@@ -903,22 +898,9 @@
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="{{ url_for('wmt_web.settings') }}" class="nav-link {% if request.endpoint == 'wmt_web.settings' %}active{% endif %}">
|
||||
<i class="fas fa-cog"></i>Settings
|
||||
<i class="fas fa-cog"></i>WMT Settings
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<!-- ── Live View group ── -->
|
||||
{% set lv_active = request.endpoint in ['main.logs','main.templates','main.stats'] %}
|
||||
<li class="nav-group" id="group-liveview">
|
||||
<div class="nav-group-header {% if lv_active %}open{% endif %}"
|
||||
onclick="toggleGroup('group-liveview')">
|
||||
<i class="fas fa-heartbeat group-icon"></i>
|
||||
<span class="group-label">Live View</span>
|
||||
<i class="fas fa-chevron-right chevron"></i>
|
||||
</div>
|
||||
<ul class="nav-group-children {% if lv_active %}open{% endif %}" style="list-style:none;padding-left:10px;margin:0;">
|
||||
<li class="nav-item">
|
||||
<a href="{{ url_for('main.logs') }}" class="nav-link {% if request.endpoint == 'main.logs' %}active{% endif %}">
|
||||
<i class="fas fa-list-alt"></i>Logs
|
||||
|
||||
@@ -1,263 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Dashboard - Server Monitoring{% endblock %}
|
||||
|
||||
{% block page_title %}Dashboard{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.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(event) {
|
||||
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 <= 1) { // Only reset log exists
|
||||
alert('ℹ️ Database is already empty!\nNo user 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 || 'N/A'} to ${stats.latest_log || 'N/A'}\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 ? event.target : document.querySelector('button[onclick*="resetDatabase"]');
|
||||
const originalText = button ? button.innerHTML : '';
|
||||
if (button) {
|
||||
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);
|
||||
if (button) {
|
||||
button.innerHTML = originalText;
|
||||
button.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
alert('❌ Network Error:\n' + error.message);
|
||||
// Restore button if it was changed
|
||||
try {
|
||||
const button = event ? event.target : document.querySelector('button[onclick*="resetDatabase"]');
|
||||
if (button) {
|
||||
button.innerHTML = '<i class="fas fa-trash-alt"></i> Clear Database';
|
||||
</script>
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
// Countdown timer for refresh
|
||||
let countdown = 30; // 30 seconds
|
||||
function updateTimer() {
|
||||
const timerElement = document.getElementById('refresh-timer');
|
||||
if (timerElement) {
|
||||
timerElement.innerText = countdown;
|
||||
}
|
||||
countdown--;
|
||||
if (countdown < 0) {
|
||||
location.reload(); // Refresh the page
|
||||
}
|
||||
}
|
||||
setInterval(updateTimer, 1000); // Update every second
|
||||
|
||||
// Database reset functionality
|
||||
async function resetDatabase(event) {
|
||||
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 <= 1) { // Only reset log exists
|
||||
alert('ℹ️ Database is already empty!\nNo user 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 || 'N/A'} to ${stats.latest_log || 'N/A'}\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 ? event.target : document.querySelector('button[onclick*="resetDatabase"]');
|
||||
const originalText = button ? button.innerHTML : '';
|
||||
if (button) {
|
||||
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);
|
||||
if (button) {
|
||||
button.innerHTML = originalText;
|
||||
button.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
alert('❌ Network Error:\n' + error.message);
|
||||
// Restore button if it was changed
|
||||
try {
|
||||
const button = event ? event.target : document.querySelector('button[onclick*="resetDatabase"]');
|
||||
if (button) {
|
||||
button.innerHTML = '<i class="fas fa-trash-alt"></i> Clear Database';
|
||||
@@ -72,6 +72,27 @@
|
||||
pattern="^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$">
|
||||
<div class="form-text">Leave empty if this is not a WMT client device.</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label fw-semibold">Custom Production URL
|
||||
<small class="text-muted fw-normal">(device-specific Chrome launch override)</small>
|
||||
</label>
|
||||
<input type="url" name="custom_chrome_url" class="form-control"
|
||||
value="{{ device.custom_chrome_url or '' }}"
|
||||
placeholder="Leave blank to use the global default from WMT Settings">
|
||||
{% if device.custom_chrome_url %}
|
||||
<div class="form-text text-warning">
|
||||
<i class="fas fa-exclamation-triangle me-1"></i>
|
||||
This device opens a custom URL instead of the global default. Clear the field to revert to the
|
||||
<a href="{{ url_for('wmt_web.settings') }}" target="_blank">global production URL</a>.
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="form-text text-success">
|
||||
<i class="fas fa-check-circle me-1"></i>
|
||||
Using the global default production URL from
|
||||
<a href="{{ url_for('wmt_web.settings') }}" target="_blank">WMT Settings</a>.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if device.mac_address %}
|
||||
|
||||
@@ -1,507 +0,0 @@
|
||||
<!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 update && sudo apt upgrade -y">Update and Upgrade Device</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 update && sudo apt upgrade -y">Update and Upgrade Device</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>
|
||||
@@ -1,83 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}WMT Devices – {{ app_name }}{% endblock %}
|
||||
|
||||
{% block page_title %}WMT Devices{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="mb-3 d-flex justify-content-between align-items-center">
|
||||
<p class="text-muted mb-0">{{ devices | length }} device(s) registered.</p>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{{ url_for('wmt_web.devices_export_csv') }}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-file-csv me-1"></i> Export CSV
|
||||
</a>
|
||||
<a href="{{ url_for('wmt_web.device_new') }}" class="btn btn-success">
|
||||
<i class="fas fa-plus me-1"></i> New Device
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body p-0">
|
||||
{% if devices %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Work Place</th>
|
||||
<th>MAC Address</th>
|
||||
<th>Hostname</th>
|
||||
<th>IP Address</th>
|
||||
<th>Card Presence</th>
|
||||
<th>Last Seen</th>
|
||||
<th>Config Updated</th>
|
||||
<th class="text-end">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for d in devices %}
|
||||
<tr>
|
||||
<td><strong>{{ d.device_name or '—' }}</strong></td>
|
||||
<td><code>{{ d.mac_address }}</code></td>
|
||||
<td>{{ d.hostname or '—' }}</td>
|
||||
<td>{{ d.device_ip or '—' }}</td>
|
||||
<td>
|
||||
{% if d.card_presence == 'enable' %}
|
||||
<span class="badge bg-success">enable</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">disable</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-muted small">
|
||||
{{ d.last_seen | local_dt if d.last_seen else 'Never' }}
|
||||
</td>
|
||||
<td class="text-muted small">
|
||||
{{ d.config_updated_at | local_dt if d.config_updated_at else '—' }}
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<a href="{{ url_for('wmt_web.device_edit', device_id=d.id) }}"
|
||||
class="btn btn-sm btn-outline-primary">
|
||||
<i class="fas fa-edit"></i> Edit
|
||||
</a>
|
||||
<form method="post" action="{{ url_for('wmt_web.device_delete', device_id=d.id) }}"
|
||||
class="d-inline"
|
||||
onsubmit="return confirm('Delete device {{ d.mac_address }}?')">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center text-muted py-5">
|
||||
<i class="fas fa-desktop fa-3x mb-3 opacity-25"></i>
|
||||
<p>No devices registered yet. <a href="{{ url_for('wmt_web.device_new') }}">Add the first one</a>.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,183 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}WMT Management – {{ app_name }}{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.stat-card { border-left: 4px solid; }
|
||||
.stat-card.blue { border-color: #3498db; }
|
||||
.stat-card.green { border-color: #2ecc71; }
|
||||
.stat-card.orange{ border-color: #f39c12; }
|
||||
.stat-card.red { border-color: #e74c3c; }
|
||||
.badge-pending { background-color: #f39c12; }
|
||||
.badge-accepted { background-color: #2ecc71; }
|
||||
.badge-rejected { background-color: #e74c3c; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block page_title %}WMT Management{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<a href="{{ url_for('wmt_web.settings') }}" class="btn btn-primary me-2">
|
||||
<i class="fas fa-cog"></i> Global Settings
|
||||
</a>
|
||||
<a href="{{ url_for('main.devices') }}" class="btn btn-outline-primary me-2">
|
||||
<i class="fas fa-desktop"></i> Devices
|
||||
</a>
|
||||
<a href="{{ url_for('wmt_web.update_requests') }}" class="btn btn-outline-warning">
|
||||
<i class="fas fa-inbox"></i> Update Requests
|
||||
{% if pending_count > 0 %}
|
||||
<span class="badge bg-danger ms-1">{{ pending_count }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats row -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
<div class="card stat-card blue h-100">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<p class="text-muted mb-1 small">Registered Devices</p>
|
||||
<h4 class="mb-0">{{ devices | length }}</h4>
|
||||
</div>
|
||||
<i class="fas fa-desktop fa-2x text-primary opacity-50"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
<div class="card stat-card orange h-100">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<p class="text-muted mb-1 small">Pending Requests</p>
|
||||
<h4 class="mb-0">{{ pending_count }}</h4>
|
||||
</div>
|
||||
<i class="fas fa-clock fa-2x text-warning opacity-50"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
<div class="card stat-card green h-100">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<p class="text-muted mb-1 small">Config Last Updated</p>
|
||||
<h6 class="mb-0">
|
||||
{% if global_cfg and global_cfg.updated_at %}
|
||||
{{ global_cfg.updated_at | local_dt }}
|
||||
{% else %}
|
||||
Never
|
||||
{% endif %}
|
||||
</h6>
|
||||
</div>
|
||||
<i class="fas fa-sync fa-2x text-success opacity-50"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
<div class="card stat-card red h-100">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<p class="text-muted mb-1 small">Chrome URL</p>
|
||||
<small class="text-truncate d-block" style="max-width:160px">
|
||||
{{ global_cfg.chrome_url if global_cfg else '—' }}
|
||||
</small>
|
||||
</div>
|
||||
<i class="fas fa-globe fa-2x text-danger opacity-50"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<!-- Device list -->
|
||||
<div class="col-lg-7">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<strong><i class="fas fa-desktop me-2"></i>WMT Client Devices</strong>
|
||||
<a href="{{ url_for('main.devices') }}" class="btn btn-sm btn-success">
|
||||
<i class="fas fa-plus"></i> Add
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
{% if devices %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Work Place</th>
|
||||
<th>Client Name</th>
|
||||
<th>MAC</th>
|
||||
<th>IP</th>
|
||||
<th>Last Seen</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for d in devices %}
|
||||
<tr>
|
||||
<td><strong>{{ d.device_name or '—' }}</strong></td>
|
||||
<td>{{ d.hostname or '—' }}</td>
|
||||
<td><code>{{ d.mac_address }}</code></td>
|
||||
<td>{{ d.device_ip or '—' }}</td>
|
||||
<td class="text-muted small">
|
||||
{{ d.last_seen | local_dt if d.last_seen else 'Never' }}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ url_for('main.device_edit', device_id=d.id) }}"
|
||||
class="btn btn-xs btn-outline-primary btn-sm py-0">Edit</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-muted p-3 mb-0">No WMT client devices registered yet.
|
||||
<a href="{{ url_for('main.devices') }}">Manage devices</a>.
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent update requests -->
|
||||
<div class="col-lg-5">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<strong><i class="fas fa-inbox me-2"></i>Recent Requests</strong>
|
||||
<a href="{{ url_for('wmt_web.update_requests') }}" class="btn btn-sm btn-outline-secondary">
|
||||
View All
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
{% if recent_requests %}
|
||||
<ul class="list-group list-group-flush">
|
||||
{% for r in recent_requests %}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-start py-2">
|
||||
<div>
|
||||
<code class="small">{{ r.mac_address }}</code><br>
|
||||
<small class="text-muted">{{ r.submitted_at | local_dt }}</small>
|
||||
</div>
|
||||
<span class="badge badge-{{ r.status }} rounded-pill">{{ r.status }}</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p class="text-muted p-3 mb-0">No recent requests.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
+48
-22
@@ -4,9 +4,10 @@
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.badge-pending { background-color: #f39c12; color: #fff; }
|
||||
.badge-accepted { background-color: #2ecc71; color: #fff; }
|
||||
.badge-rejected { background-color: #e74c3c; color: #fff; }
|
||||
.badge-pending { background-color: #f39c12; color: #fff; }
|
||||
.badge-accepted { background-color: #2ecc71; color: #fff; }
|
||||
.badge-rejected { background-color: #e74c3c; color: #fff; }
|
||||
.badge-to_be_configured{ background-color: #6f42c1; color: #fff; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
@@ -15,11 +16,21 @@
|
||||
{% if pending_count > 0 %}
|
||||
<span class="badge bg-danger ms-2">{{ pending_count }} pending</span>
|
||||
{% endif %}
|
||||
{% if unconfigured_count > 0 %}
|
||||
<span class="badge bg-purple ms-1" style="background:#6f42c1">{{ unconfigured_count }} to configure</span>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Filter tabs -->
|
||||
<ul class="nav nav-tabs mb-4">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if status_filter == 'to_be_configured' %}active{% endif %}"
|
||||
href="{{ url_for('wmt_web.update_requests', status='to_be_configured') }}">
|
||||
<i class="fas fa-tools me-1"></i>To Be Configured
|
||||
{% if unconfigured_count > 0 %}<span class="badge ms-1" style="background:#6f42c1;color:#fff">{{ unconfigured_count }}</span>{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if status_filter == 'pending' %}active{% endif %}"
|
||||
href="{{ url_for('wmt_web.update_requests', status='pending') }}">
|
||||
@@ -41,6 +52,17 @@
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{% if status_filter == 'to_be_configured' and requests %}
|
||||
<div class="alert alert-info d-flex align-items-start gap-2 mb-3">
|
||||
<i class="fas fa-info-circle mt-1"></i>
|
||||
<div>
|
||||
These devices are online but have <strong>no work_place assigned</strong> yet.
|
||||
They are <em>not</em> registered in the devices table. Accept a request to create
|
||||
the device record and push a configuration to the client.
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if requests %}
|
||||
<div class="card">
|
||||
<div class="card-body p-0">
|
||||
@@ -53,47 +75,51 @@
|
||||
<th>Proposed Work Place</th>
|
||||
<th>Proposed Hostname</th>
|
||||
<th>Proposed IP</th>
|
||||
<th>Submitted</th>
|
||||
<th>Client Config Time</th>
|
||||
<th>Last Seen</th>
|
||||
<th>Status</th>
|
||||
{% if status_filter == 'pending' or status_filter == 'all' %}
|
||||
{% if status_filter in ('pending', 'to_be_configured', 'all') %}
|
||||
<th class="text-end">Actions</th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for r in requests %}
|
||||
<tr>
|
||||
<tr {% if r.status == 'to_be_configured' %}class="table-warning"{% endif %}>
|
||||
<td class="text-muted small">{{ r.id }}</td>
|
||||
<td><code>{{ r.mac_address }}</code></td>
|
||||
<td>{{ r.proposed_device_name or '—' }}</td>
|
||||
<td><code>{{ r.mac_address or '—' }}</code></td>
|
||||
<td>
|
||||
{% if r.status == 'to_be_configured' %}
|
||||
<span class="text-muted fst-italic">not assigned</span>
|
||||
{% else %}
|
||||
{{ r.proposed_device_name or '—' }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ r.proposed_hostname or '—' }}</td>
|
||||
<td>{{ r.proposed_device_ip or '—' }}</td>
|
||||
<td class="text-muted small">{{ r.submitted_at | local_dt }}</td>
|
||||
<td class="text-muted small">{{ r.client_config_mtime or '—' }}</td>
|
||||
<td>
|
||||
<span class="badge badge-{{ r.status }} rounded-pill">{{ r.status }}</span>
|
||||
<span class="badge badge-{{ r.status }} rounded-pill">{{ r.status.replace('_', ' ') }}</span>
|
||||
{% if r.admin_reviewed_at %}
|
||||
<br><small class="text-muted">{{ r.admin_reviewed_at | local_dt }}</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% if status_filter == 'pending' or status_filter == 'all' %}
|
||||
{% if status_filter in ('pending', 'to_be_configured', 'all') %}
|
||||
<td class="text-end">
|
||||
{% if r.status == 'pending' %}
|
||||
<!-- Accept -->
|
||||
{% if r.status in ('pending', 'to_be_configured') %}
|
||||
<!-- Accept / Assign -->
|
||||
<form method="post" action="{{ url_for('wmt_web.accept_request', req_id=r.id) }}"
|
||||
class="d-inline"
|
||||
onsubmit="return confirm('Accept this request and update the device record?')">
|
||||
onsubmit="return confirm('Accept this request and create/update the device record?')">
|
||||
<button type="submit" class="btn btn-sm btn-success">
|
||||
<i class="fas fa-check"></i> Accept
|
||||
<i class="fas fa-check"></i> {% if r.status == 'to_be_configured' %}Assign{% else %}Accept{% endif %}
|
||||
</button>
|
||||
</form>
|
||||
<!-- Reject -->
|
||||
<!-- Reject / Dismiss -->
|
||||
<form method="post" action="{{ url_for('wmt_web.reject_request', req_id=r.id) }}"
|
||||
class="d-inline ms-1"
|
||||
onsubmit="return confirm('Reject this request?')">
|
||||
onsubmit="return confirm('Dismiss this device?')">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger">
|
||||
<i class="fas fa-times"></i> Reject
|
||||
<i class="fas fa-times"></i> Dismiss
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
@@ -104,8 +130,8 @@
|
||||
</tr>
|
||||
{% if r.admin_notes %}
|
||||
<tr class="table-light">
|
||||
<td colspan="9" class="small text-muted ps-4">
|
||||
<i class="fas fa-comment me-1"></i> Admin note: {{ r.admin_notes }}
|
||||
<td colspan="8" class="small text-muted ps-4">
|
||||
<i class="fas fa-comment me-1"></i>{{ r.admin_notes }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
@@ -118,7 +144,7 @@
|
||||
{% else %}
|
||||
<div class="text-center text-muted py-5">
|
||||
<i class="fas fa-inbox fa-3x mb-3 opacity-25"></i>
|
||||
<p>No {{ status_filter }} requests found.</p>
|
||||
<p>No {{ status_filter.replace('_', ' ') }} requests found.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save me-1"></i> Save Settings
|
||||
</button>
|
||||
<a href="{{ url_for('wmt_web.index') }}" class="btn btn-outline-secondary">Cancel</a>
|
||||
<a href="{{ url_for('main.devices') }}" class="btn btn-outline-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user