From e38bf07ef2474850352fc6e7c6ea837a998acc2d Mon Sep 17 00:00:00 2001 From: ske087 Date: Thu, 14 May 2026 17:02:23 +0300 Subject: [PATCH] feat: UI improvements and WMT sync workflow overhaul --- app/api/wmt.py | 138 +++-- app/models/__init__.py | 3 +- app/web/main.py | 14 +- scripts/update_database_schema.py | 17 + templates/ansible/playbooks.html | 20 + templates/base.html | 940 ++++++++++++++++++++++++------ templates/device_management.html | 103 +++- templates/logs.html | 14 + 8 files changed, 1004 insertions(+), 245 deletions(-) diff --git a/app/api/wmt.py b/app/api/wmt.py index a618ae1..339c75a 100644 --- a/app/api/wmt.py +++ b/app/api/wmt.py @@ -127,11 +127,25 @@ def get_device_config(mac_address): @wmt_api_bp.route('/config/update_request', methods=['POST']) def submit_update_request(): """ - WMT client sends current device info. Three outcomes: + WMT client sends its current running config so the server can validate it. + Three outcomes: - 1. Device not registered → create pending WMTUpdateRequest for admin approval. - 2. Device registered, info UNCHANGED → update last_seen only; no request created. - 3. Device registered, info CHANGED → create/refresh a pending WMTUpdateRequest. + 1. Device known AND config matches server record + → record config_synced_at = now, update last_seen + → respond {"status": "ok", "in_sync": true} + Client does nothing. + + 2. Device known BUT config differs from server record + → the server is authoritative; client must pull fresh config + → respond {"status": "sync_required", "in_sync": false} + Client should call GET /api/wmt/config/ to obtain correct values. + + 3. Device unknown (new client – not registered by MAC or hostname) + → create WMTUpdateRequest so admin can approve and assign settings + → respond {"status": "pending_approval"} + Client waits; it will get real config once admin approves. + + card_presence is always applied directly – no approval required. Expected JSON: { @@ -139,8 +153,8 @@ def submit_update_request(): "device_name": "Masa-01", "hostname": "rpi-masa01", "device_ip": "192.168.1.100", - "card_presence": "enable", // optional – applied directly, no approval - "client_config_mtime": "2026-04-22T09:30:00" // optional + "card_presence": "enable", + "client_config_mtime": "2026-04-22T09:30:00" } """ if not request.is_json: @@ -157,70 +171,82 @@ def submit_update_request(): card_presence = data.get('card_presence') def _eq(a, b): - """Compare two values treating None and '' as equivalent.""" return (a or '') == (b or '') try: with get_db().get_session() as session: device = session.query(Device).filter_by(mac_address=mac).first() - # Always update last_seen and card_presence (no approval needed) - if device: - device.last_seen = datetime.utcnow() - if card_presence in ('enable', 'disable'): - device.card_presence = card_presence - - # --- Determine whether the proposed info differs from the server record --- - if device: - info_changed = not ( - _eq(proposed_name, device.nume_masa) and - _eq(proposed_hostname, device.hostname) and - _eq(proposed_ip, device.device_ip) + # ── Outcome 3: unknown device ───────────────────────────── + if not device: + # Check if a pending request with the same data already exists + existing = ( + session.query(WMTUpdateRequest) + .filter_by(mac_address=mac, status='pending') + .order_by(WMTUpdateRequest.submitted_at.desc()) + .first() ) - else: - # Unknown device – always needs admin attention - info_changed = True + if existing and ( + _eq(existing.proposed_device_name, proposed_name) and + _eq(existing.proposed_hostname, proposed_hostname) and + _eq(existing.proposed_device_ip, proposed_ip) + ): + existing.submitted_at = datetime.utcnow() + logger.debug(f'WMT unknown device {mac} – refreshed existing pending request') + else: + req = WMTUpdateRequest( + mac_address=mac, + device_id=None, + proposed_device_name=proposed_name or 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='pending', + ) + session.add(req) + logger.info(f'WMT pending approval request created for unknown device {mac}') - if not info_changed: - # Heartbeat only – nothing for admin to review - logger.debug(f'WMT heartbeat from {mac} – info unchanged, last_seen updated') - return jsonify({'status': 'no_change', 'message': 'Device info matches server record'}), 200 + return jsonify({ + 'status': 'pending_approval', + 'message': 'Device not registered. Awaiting admin approval.', + }), 202 - # --- Info changed (or device unknown): avoid duplicate pending requests --- - existing = ( - session.query(WMTUpdateRequest) - .filter_by(mac_address=mac, status='pending') - .order_by(WMTUpdateRequest.submitted_at.desc()) - .first() + # Device is known from here on ───────────────────────────── + device.last_seen = datetime.utcnow() + if card_presence in ('enable', 'disable'): + device.card_presence = card_presence + + # ── Outcome 1: config matches server record ──────────────── + config_in_sync = ( + _eq(proposed_name, device.nume_masa) and + _eq(proposed_hostname, device.hostname) and + _eq(proposed_ip, device.device_ip) ) - if existing and ( - _eq(existing.proposed_device_name, proposed_name) and - _eq(existing.proposed_hostname, proposed_hostname) and - _eq(existing.proposed_device_ip, proposed_ip) - ): - # Identical pending request already exists – just refresh its timestamp - existing.submitted_at = datetime.utcnow() - logger.debug(f'WMT duplicate pending request from {mac} – timestamp refreshed') - return jsonify({'status': 'pending', 'message': 'Update request already pending admin review'}), 200 - # Create a new update request - req = WMTUpdateRequest( - mac_address=mac, - device_id=device.id if device else None, - proposed_device_name=proposed_name or 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='pending', + if config_in_sync: + device.config_synced_at = datetime.utcnow() + logger.debug(f'WMT config OK for {mac} – synced_at updated') + return jsonify({ + 'status': 'ok', + 'in_sync': True, + 'message': 'Config matches server record.', + }), 200 + + # ── Outcome 2: config differs – client must pull from server ─ + logger.info( + f'WMT config mismatch for {mac}: ' + f'client has name={proposed_name!r} host={proposed_hostname!r} ip={proposed_ip!r} ' + f'but server has name={device.nume_masa!r} host={device.hostname!r} ip={device.device_ip!r}' ) - session.add(req) + return jsonify({ + 'status': 'sync_required', + 'in_sync': False, + 'message': 'Config differs from server record. Pull updated config.', + }), 200 - reason = 'new device' if not device else 'info changed' - logger.info(f'WMT update request created for {mac} ({reason})') - return jsonify({'status': 'received', 'message': 'Update request queued for admin review'}), 201 except Exception as e: - logger.error(f'Error saving WMT update request from {mac}: {e}') + logger.error(f'Error processing WMT config check from {mac}: {e}') return jsonify({'error': str(e)}), 500 diff --git a/app/models/__init__.py b/app/models/__init__.py index d354fcb..47ac946 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -48,7 +48,8 @@ class Device(Base): # WMT (Workstation Management Terminal) integration fields mac_address = Column(String(17), unique=True, nullable=True, index=True) - config_updated_at = Column(DateTime) + config_updated_at = Column(DateTime) # set by admin when pushing new config + config_synced_at = Column(DateTime) # set by server when client confirms in-sync info_reviewed_at = Column(DateTime, default=lambda: datetime(1970, 1, 1)) card_presence = Column(String(10), default='enable') diff --git a/app/web/main.py b/app/web/main.py index 2c9aa55..cabaaf6 100644 --- a/app/web/main.py +++ b/app/web/main.py @@ -34,7 +34,7 @@ def devices(): with get_db().get_session() as session: devices = session.query(Device).order_by(Device.last_seen.desc()).all() - # Get log count per device + # Log count per device device_log_counts = {} for device in devices: log_count = session.query(LogEntry).filter_by(device_id=device.id).count() @@ -42,14 +42,22 @@ def devices(): pending_count = session.query(WMTUpdateRequest).filter_by(status='pending').count() + # Pending approval requests mapped by mac_address for per-device badge + pending_requests = session.query(WMTUpdateRequest).filter_by(status='pending').all() + pending_by_mac = {} + for req in pending_requests: + pending_by_mac[req.mac_address] = pending_by_mac.get(req.mac_address, 0) + 1 + return render_template('device_management.html', devices=devices, device_log_counts=device_log_counts, - pending_count=pending_count) + pending_count=pending_count, + pending_by_mac=pending_by_mac) except Exception as e: logging.error(f"Error loading devices: {e}") flash(f'Error loading devices: {e}', 'error') - return render_template('device_management.html', devices=[], device_log_counts={}, pending_count=0) + return render_template('device_management.html', devices=[], device_log_counts={}, + pending_count=0, pending_by_mac={}) @main_bp.route('/device/') def device_detail(device_id): diff --git a/scripts/update_database_schema.py b/scripts/update_database_schema.py index fa1700a..986ef1a 100755 --- a/scripts/update_database_schema.py +++ b/scripts/update_database_schema.py @@ -57,6 +57,22 @@ class DatabaseSchemaUpdater: except Exception as e: print(f" ❌ Error adding {column_name}: {e}") + def add_config_synced_at_column(self): + """Add config_synced_at column to devices table (WMT config sync tracking)""" + print("📊 Updating devices table schema (config_synced_at)...") + with self.engine.connect() as conn: + try: + result = conn.execute(text("PRAGMA table_info(devices)")) + existing = [row[1] for row in result.fetchall()] + if 'config_synced_at' not in existing: + conn.execute(text("ALTER TABLE devices ADD COLUMN config_synced_at DATETIME")) + conn.commit() + print(" ✅ Added column: config_synced_at") + else: + print(" ⏭️ Column config_synced_at already exists") + except Exception as e: + print(f" ❌ Error adding config_synced_at: {e}") + def create_tables(self): """Create all tables using SQLAlchemy metadata""" print("🏗️ Creating all database tables...") @@ -109,6 +125,7 @@ if __name__ == "__main__": # Update schema updater.update_playbook_executions_schema() + updater.add_config_synced_at_column() # Verify if updater.verify_schema(): diff --git a/templates/ansible/playbooks.html b/templates/ansible/playbooks.html index 88f0c7f..dab33f5 100644 --- a/templates/ansible/playbooks.html +++ b/templates/ansible/playbooks.html @@ -38,6 +38,26 @@ padding: 1rem; margin: -1rem -1rem 1rem -1rem; } + +/* Dark mode overrides */ +body.dark-mode .playbook-actions { + background: #2a2a2a; + border-bottom-color: #444444; +} + +body.dark-mode .playbook-item.selected { + border-color: #64b5f6; + background-color: #1a2a3a; +} + +body.dark-mode #welcomeMessage h4, +body.dark-mode #welcomeMessage p { + color: #888888 !important; +} + +body.dark-mode .code-editor-area { + border-color: #444444; +} {% endblock %} diff --git a/templates/base.html b/templates/base.html index e30d7a1..39d5032 100644 --- a/templates/base.html +++ b/templates/base.html @@ -4,6 +4,8 @@ {% block title %}Server Monitoring Dashboard{% endblock %} + + {% block extra_css %}{% endblock %} @@ -235,123 +804,130 @@ + @@ -399,55 +975,77 @@ {% block extra_js %}{% endblock %} diff --git a/templates/device_management.html b/templates/device_management.html index f166724..2c5f58b 100644 --- a/templates/device_management.html +++ b/templates/device_management.html @@ -1,14 +1,33 @@ {% extends "base.html" %} {% block title %}Devices – {{ app_name }}{% endblock %} -{% block page_title %}Devices{% endblock %} +{% block page_title %}Device Health{% endblock %} {% block extra_css %} {% endblock %} @@ -60,7 +79,7 @@

{{ pending_count }}

- Pending Requests + Pending Approval
@@ -88,7 +107,7 @@ Work Place Hostname IP - MAC Address + MAC / Type Status Logs Last Seen @@ -98,20 +117,35 @@ {% for device in devices %} + {% set is_wmt = device.mac_address is not none and device.mac_address != '' %} + {% set has_pending = is_wmt and (pending_by_mac.get(device.mac_address, 0) > 0) %} + + {{ device.nume_masa or '—' }} + {% if is_wmt %} +
WMT + {% endif %} + + {{ device.hostname }} + + {{ device.device_ip }} + + - {% if device.mac_address %} + {% if is_wmt %} {{ device.mac_address }} {% else %} {% endif %} + + {% if device.status == 'active' %} Active @@ -121,24 +155,55 @@ Offline {% endif %} + + {{ device_log_counts.get(device.id, 0) }} + + {% if device.last_seen %} {{ device.last_seen | local_dt }} {% else %}—{% endif %} + + - {% if device.mac_address and device.config_updated_at %} - - - {{ device.config_updated_at | local_dt('%m-%d %H:%M') }} - - {% elif device.mac_address %} - Never - {% else %} + {% if not is_wmt %} + + {% elif has_pending %} + {# Unknown device waiting for admin approval #} + + Pending Approval + + + {% elif device.config_synced_at %} + {# Device checked in and server confirmed configs match #} + + + OK · {{ device.config_synced_at | local_dt('%m-%d %H:%M') }} + + + {% elif device.config_updated_at %} + {# Admin pushed config but client hasn't confirmed sync yet #} + + Awaiting client + + + {% else %} + {# WMT device registered but never checked in #} + + Never synced + {% endif %} + + @@ -175,6 +240,15 @@ + +
+ Config Sync legend: + OK — client confirmed configs match + Awaiting client — server pushed new config, waiting for client check-in + Pending Approval — new/unknown device waiting for admin + Never synced — registered but hasn't checked in +
+