From 10dd0a560ce57abf33ee0e1f3d6531cd3b640120 Mon Sep 17 00:00:00 2001 From: ske087 Date: Mon, 27 Apr 2026 14:11:57 +0300 Subject: [PATCH] Add card_presence feature, device CRUD, CSV export, Update_Rest_WMT_client playbook, migrate_to_wmt dual-path idmasa --- .gitignore | 1 + ansible/playbooks/Update_Rest_WMT_client.yml | 453 +++++++++++++++++++ ansible/playbooks/migrate_to_wmt.yml | 42 +- app/api/wmt.py | 4 + app/models/__init__.py | 1 + app/web/wmt.py | 163 ++++++- templates/base.html | 6 + templates/wmt/device_form.html | 10 + templates/wmt/devices.html | 19 +- 9 files changed, 691 insertions(+), 8 deletions(-) create mode 100644 ansible/playbooks/Update_Rest_WMT_client.yml diff --git a/.gitignore b/.gitignore index 11566cb..4d54272 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,7 @@ ansible/playbooks/*.yml !ansible/playbooks/distribute_ssh_keys.yml !ansible/playbooks/restart_service.yml !ansible/playbooks/migrate_to_wmt.yml +!ansible/playbooks/Update_Rest_WMT_client.yml # VS Code .vscode/ diff --git a/ansible/playbooks/Update_Rest_WMT_client.yml b/ansible/playbooks/Update_Rest_WMT_client.yml new file mode 100644 index 0000000..ff985cb --- /dev/null +++ b/ansible/playbooks/Update_Rest_WMT_client.yml @@ -0,0 +1,453 @@ +--- +# Update_Rest_WMT_client – Push standard WMT client files to target devices +# ────────────────────────────────────────────────────────────────────────── +# What this playbook does (in order): +# +# 1. Verify that WMT_project exists on the controller +# 2. Kill any running WMT app.py on the target +# 3. Remove previous WMT_old (if any) and rename WMT → WMT_old +# 4. Copy WMT_project → fresh WMT on the target +# 5. Fix execute permissions on scripts +# 6. Migrate config values: old config.txt → new config.txt +# Rules: +# • For every key that exists in BOTH old and new config → use old value +# • Key exists only in new config (new feature) → keep new default +# • Key exists only in old config (removed key) → skip / ignore +# File structure, comments, and new keys are always from +# the new template (WMT_project/data/config.txt). +# 7. Restore runtime data files (log.txt, tag.txt) from WMT_old +# 8. Delete WMT_old (migration complete, backup no longer needed) +# 9. Reboot the target host +# +# Run via: Ansible > Playbooks > "Update_Rest_WMT_client" or +# POST /api/ansible/execute { "playbook": "Update_Rest_WMT_client.yml" } +# ────────────────────────────────────────────────────────────────────────── + +- name: Update WMT client files on target devices + hosts: all + gather_facts: false + become: false + + tasks: + + # ── 1. Verify source exists on the controller ───────────────────────── + - name: Check WMT_project source folder exists on controller + delegate_to: localhost + stat: + path: /home/pi/Desktop/WMT_project + register: wmt_project_stat + + - name: Fail if WMT_project is missing on the controller + delegate_to: localhost + fail: + msg: > + /home/pi/Desktop/WMT_project not found on the Ansible controller. + Ensure the WMT_project folder is present before running this playbook. + when: not wmt_project_stat.stat.exists + + # ── 2. Kill any running WMT app.py ──────────────────────────────────── + - name: Stop running WMT app.py (if any) + shell: pkill -f "python3.*WMT/app.py" + ignore_errors: true + changed_when: false + + - name: Wait for process to terminate + wait_for: + timeout: 4 + + # ── 3a. Remove previous WMT_old backup (if one exists from a prior run) + - name: Check if WMT_old already exists + stat: + path: /home/pi/Desktop/WMT_old + register: wmt_old_stat + + - name: Remove previous WMT_old folder + file: + path: /home/pi/Desktop/WMT_old + state: absent + when: wmt_old_stat.stat.exists + + # ── 3b. Rename current WMT → WMT_old ───────────────────────────────── + - name: Check if WMT folder currently exists + stat: + path: /home/pi/Desktop/WMT + register: wmt_stat + + - name: Rename WMT to WMT_old + command: mv /home/pi/Desktop/WMT /home/pi/Desktop/WMT_old + when: wmt_stat.stat.exists + + # ── 4. Copy fresh WMT_project → WMT ────────────────────────────────── + - name: Copy WMT_project to target as /home/pi/Desktop/WMT + copy: + src: /home/pi/Desktop/WMT_project/ + dest: /home/pi/Desktop/WMT/ + owner: pi + group: pi + mode: preserve + force: true + + - name: Ensure WMT/data directory exists + file: + path: /home/pi/Desktop/WMT/data + state: directory + owner: pi + group: pi + mode: '0755' + + # ── 5. Fix execute permissions ──────────────────────────────────────── + - name: Ensure app.py is executable + file: + path: /home/pi/Desktop/WMT/app.py + owner: pi + group: pi + mode: '0755' + + - name: Ensure setup_port_capability.sh is executable + file: + path: /home/pi/Desktop/WMT/setup_port_capability.sh + state: file + owner: pi + group: pi + mode: '0755' + ignore_errors: true + + # ── 6. Migrate config values from old config.txt into new template ──── + # + # Strategy (line-preserving Python migration): + # • Read WMT_old/data/config.txt → source of device-specific values + # • Read WMT/data/config.txt → new template (correct keys & comments) + # • Walk the new template line by line: + # - Keep all comment lines and blank lines unchanged + # - For each key=value line: + # If old config has the SAME section + key → replace value with old value + # If old config does NOT have that key → keep new default value + # - Keys only in old config are silently ignored (removed features) + # • Write the result back to WMT/data/config.txt + # + - name: Check if old config.txt exists in WMT_old + stat: + path: /home/pi/Desktop/WMT_old/data/config.txt + register: old_cfg_stat + + - name: Migrate config values (old → new template) + shell: | + python3 << 'PYEOF' + import configparser, os, re, sys + + old_path = '/home/pi/Desktop/WMT_old/data/config.txt' + new_path = '/home/pi/Desktop/WMT/data/config.txt' + + if not os.path.exists(old_path): + print('No old config.txt found – keeping new defaults as-is') + sys.exit(0) + + if not os.path.exists(new_path): + print('New config.txt missing – nothing to migrate into') + sys.exit(1) + + # Parse old config to get existing values + old = configparser.ConfigParser() + old.read(old_path) + + # Walk new template line by line, replacing values where old config has them + current_section = None + output_lines = [] + migrated = [] + kept_new = [] + + with open(new_path, 'r') as f: + for line in f: + stripped = line.rstrip('\n') + + # Detect section header e.g. [device] + section_match = re.match(r'^\s*\[([^\]]+)\]\s*$', stripped) + if section_match: + current_section = section_match.group(1) + output_lines.append(line) + continue + + # Detect key = value line (skip comments and blank lines) + kv_match = re.match(r'^([^#;\s][^=]*?)\s*=\s*(.*)', stripped) + if kv_match and current_section: + key = kv_match.group(1).strip() + new_val = kv_match.group(2) + + if old.has_section(current_section) and old.has_option(current_section, key): + old_val = old.get(current_section, key) + if old_val != new_val: + output_lines.append(f'{key}={old_val}\n') + migrated.append(f' [{current_section}] {key} = {old_val!r} (was: {new_val!r})') + else: + output_lines.append(line) + else: + output_lines.append(line) + kept_new.append(f' [{current_section}] {key} = {new_val!r} (new key – keeping default)') + else: + output_lines.append(line) + + with open(new_path, 'w') as f: + f.writelines(output_lines) + + print('=== Config migration complete ===') + if migrated: + print(f'Migrated {len(migrated)} value(s) from old config:') + for m in migrated: + print(m) + if kept_new: + print(f'Kept {len(kept_new)} new default(s) (not in old config):') + for k in kept_new: + print(k) + PYEOF + register: cfg_migration_result + when: old_cfg_stat.stat.exists + changed_when: true + + - name: Show config migration output + debug: + msg: "{{ cfg_migration_result.stdout_lines }}" + when: old_cfg_stat.stat.exists + + - name: Note – no old config found, new defaults kept + debug: + msg: "WMT_old/data/config.txt not found – new WMT_project defaults will be used." + when: not old_cfg_stat.stat.exists + + # ── 7. Restore runtime data files from WMT_old ─────────────────────── + - name: Check if log.txt exists in WMT_old + stat: + path: /home/pi/Desktop/WMT_old/data/log.txt + register: old_log_stat + + - name: Restore log.txt from WMT_old + copy: + src: /home/pi/Desktop/WMT_old/data/log.txt + dest: /home/pi/Desktop/WMT/data/log.txt + owner: pi + group: pi + mode: '0644' + remote_src: true + when: old_log_stat.stat.exists + + - name: Check if tag.txt exists in WMT_old + stat: + path: /home/pi/Desktop/WMT_old/data/tag.txt + register: old_tag_stat + + - name: Restore tag.txt from WMT_old (pending card posts) + copy: + src: /home/pi/Desktop/WMT_old/data/tag.txt + dest: /home/pi/Desktop/WMT/data/tag.txt + owner: pi + group: pi + mode: '0644' + remote_src: true + when: old_tag_stat.stat.exists + + # ── 8. Remove WMT_old now that migration is complete ───────────────── + - name: Remove WMT_old folder (migration done, no longer needed) + file: + path: /home/pi/Desktop/WMT_old + state: absent + + # ── 9. Show final config and reboot ─────────────────────────────────── + - name: Show final config.txt on target + command: cat /home/pi/Desktop/WMT/data/config.txt + register: final_cfg + changed_when: false + + - name: Display final config.txt + debug: + msg: "{{ final_cfg.stdout_lines }}" + + - name: Show updated app.py version + command: head -1 /home/pi/Desktop/WMT/app.py + register: app_version + changed_when: false + + - name: Report update summary + debug: + msg: > + ✓ WMT updated on {{ inventory_hostname }} – {{ app_version.stdout }}. + Rebooting now to apply changes. + + - name: Reboot target to apply all changes + become: true + reboot: + msg: "Rebooting after WMT client update" + reboot_timeout: 180 + pre_reboot_delay: 3 + post_reboot_delay: 15 + + +- name: Update WMT client files on target devices + hosts: all + gather_facts: false + become: false + + tasks: + + # ── 1. Confirm WMT_project source exists on the controller ──────────── + - name: Check that WMT_project source folder exists on controller + delegate_to: localhost + stat: + path: /home/pi/Desktop/WMT_project + register: wmt_project_stat + + - name: Fail if WMT_project is missing on the controller + delegate_to: localhost + fail: + msg: > + /home/pi/Desktop/WMT_project was not found on the Ansible controller. + Make sure the WMT_project folder is present before running this playbook. + when: not wmt_project_stat.stat.exists + + # ── 2. Ensure WMT destination directory exists on target ───────────── + - name: Ensure /home/pi/Desktop/WMT directory exists + file: + path: /home/pi/Desktop/WMT + state: directory + owner: pi + group: pi + mode: '0755' + + # ── 3. Kill any running WMT app.py process ──────────────────────────── + - name: Stop running WMT app.py (if any) + shell: pkill -f "python3.*WMT/app.py" + ignore_errors: true + changed_when: false + + - name: Wait for process to terminate + wait_for: + timeout: 3 + + # ── 4. Copy WMT_project → WMT, preserving device-specific data files ─ + - name: Copy WMT_project files to target (excluding device-specific data) + copy: + src: /home/pi/Desktop/WMT_project/ + dest: /home/pi/Desktop/WMT/ + owner: pi + group: pi + mode: preserve + force: true + # Exclude device-specific files that must never be overwritten + # Note: Ansible's copy module does not support 'exclude' natively; + # we restore the originals in step 4b–4e below. + + # ── 4b-4e. Restore device-specific data files from the target backup ── + # Strategy: before overwriting we slurp the existing files, then + # write them back so the 'copy' above cannot clobber them. + + - name: Check if device config.txt exists (pre-copy) + stat: + path: /home/pi/Desktop/WMT/data/config.txt + register: cfg_stat + + - name: Slurp existing config.txt (if present) + slurp: + src: /home/pi/Desktop/WMT/data/config.txt + register: cfg_backup + when: cfg_stat.stat.exists + + - name: Restore config.txt after copy (device config must not be overwritten) + copy: + content: "{{ cfg_backup.content | b64decode }}" + dest: /home/pi/Desktop/WMT/data/config.txt + owner: pi + group: pi + mode: '0644' + when: cfg_stat.stat.exists + + # idmasa.txt is a legacy Prezenta file – not used by WMT, not restored + + - name: Check if log.txt exists + stat: + path: /home/pi/Desktop/WMT/data/log.txt + register: log_stat + + - name: Slurp existing log.txt (if present) + slurp: + src: /home/pi/Desktop/WMT/data/log.txt + register: log_backup + when: log_stat.stat.exists + + - name: Restore log.txt after copy + copy: + content: "{{ log_backup.content | b64decode }}" + dest: /home/pi/Desktop/WMT/data/log.txt + owner: pi + group: pi + mode: '0644' + when: log_stat.stat.exists + + - name: Check if tag.txt exists + stat: + path: /home/pi/Desktop/WMT/data/tag.txt + register: tag_stat + + - name: Slurp existing tag.txt (if present) + slurp: + src: /home/pi/Desktop/WMT/data/tag.txt + register: tag_backup + when: tag_stat.stat.exists + + - name: Restore tag.txt after copy + copy: + content: "{{ tag_backup.content | b64decode }}" + dest: /home/pi/Desktop/WMT/data/tag.txt + owner: pi + group: pi + mode: '0644' + when: tag_stat.stat.exists + + # ── 5. Fix execute permissions on scripts ───────────────────────────── + - name: Ensure app.py is executable + file: + path: /home/pi/Desktop/WMT/app.py + owner: pi + group: pi + mode: '0755' + + - name: Ensure setup_port_capability.sh is executable + file: + path: /home/pi/Desktop/WMT/setup_port_capability.sh + state: file + owner: pi + group: pi + mode: '0755' + ignore_errors: true + + # ── 6. Ensure WMT/data directory is intact ──────────────────────────── + - name: Ensure WMT/data directory exists + file: + path: /home/pi/Desktop/WMT/data + state: directory + owner: pi + group: pi + mode: '0755' + + # ── 7. Restart WMT app in a new lxterminal session ──────────────────── + - name: Launch WMT app.py in background lxterminal + shell: > + DISPLAY=:0 lxterminal -e + "bash -c 'cd /home/pi/Desktop/WMT; python3 app.py; exec bash'" & + environment: + DISPLAY: ":0" + ignore_errors: true + changed_when: true + + - name: Wait for WMT app to start + wait_for: + timeout: 5 + + # ── 8. Confirm update on target ─────────────────────────────────────── + - name: Show updated app.py version line + command: head -1 /home/pi/Desktop/WMT/app.py + register: app_version + changed_when: false + + - name: Report update result + debug: + msg: > + ✓ WMT client updated on {{ inventory_hostname }} ({{ ansible_host }}). + Version: {{ app_version.stdout }} diff --git a/ansible/playbooks/migrate_to_wmt.yml b/ansible/playbooks/migrate_to_wmt.yml index 6755532..c76336a 100644 --- a/ansible/playbooks/migrate_to_wmt.yml +++ b/ansible/playbooks/migrate_to_wmt.yml @@ -5,7 +5,7 @@ # # 1. Create /home/pi/Desktop/WMT on the target # 2. Copy /home/pi/Desktop/WMT_project from the CONTROLLER to /home/pi/Desktop/WMT on the target -# 3. Read /home/pi/Desktop/Prezenta/data/idmasa.txt from the target +# 3. Read idmasa.txt from the target (checks Prezenta/data/ first, falls back to Prezenta/Files/ for older app models) # 4. Write that value as work_place in WMT/data/config.txt (replaces notconfig) # 5. Update ~/.config/wayfire.ini [autostart] start_python to launch from WMT # 6. Rename /home/pi/Desktop/Prezenta → /home/pi/Desktop/Prezenta_Old_Data @@ -49,10 +49,44 @@ group: pi mode: '0755' - # ── 4. Read idmasa.txt from the Prezenta data folder ───────────────── - - name: Read idmasa.txt from Prezenta + # ── 4. Read idmasa.txt – supports two Prezenta layouts ─────────────── + # Modern layout : Prezenta/data/idmasa.txt + # Older layout : Prezenta/Files/idmasa.txt + - name: Check for idmasa.txt in modern path (Prezenta/data/) + stat: + path: /home/pi/Desktop/Prezenta/data/idmasa.txt + register: idmasa_data_stat + + - name: Check for idmasa.txt in older path (Prezenta/Files/) + stat: + path: /home/pi/Desktop/Prezenta/Files/idmasa.txt + register: idmasa_files_stat + + - name: Resolve idmasa.txt path (prefer data/, fall back to Files/) + set_fact: + idmasa_path: >- + {{ + '/home/pi/Desktop/Prezenta/data/idmasa.txt' + if idmasa_data_stat.stat.exists + else '/home/pi/Desktop/Prezenta/Files/idmasa.txt' + }} + + - name: Show which idmasa.txt will be used + debug: + msg: "Using idmasa.txt from: {{ idmasa_path }}" + + - name: Fail if idmasa.txt was not found in either location + fail: + msg: >- + idmasa.txt not found in Prezenta/data/ or Prezenta/Files/. + Cannot determine work_place – aborting migration. + when: + - not idmasa_data_stat.stat.exists + - not idmasa_files_stat.stat.exists + + - name: Read idmasa.txt from resolved path slurp: - src: /home/pi/Desktop/Prezenta/data/idmasa.txt + src: "{{ idmasa_path }}" register: idmasa_raw - name: Decode idmasa value diff --git a/app/api/wmt.py b/app/api/wmt.py index afe407e..d61039d 100644 --- a/app/api/wmt.py +++ b/app/api/wmt.py @@ -110,6 +110,7 @@ def get_device_config(mac_address): 'hostname': device.hostname if device else '', 'device_ip': device.device_ip if device else '', 'location': device.location if device else '', + 'card_presence': device.card_presence if device else 'enable', # Admin-review timestamp for device info (client stores in [device] section) 'info_reviewed_at': device.info_reviewed_at.isoformat() if (device and device.info_reviewed_at) else '1970-01-01T00:00:00', # Sync metadata @@ -162,6 +163,9 @@ def submit_update_request(): # Update device last_seen if device: device.last_seen = datetime.utcnow() + # card_presence is a device capability flag – update directly (no approval needed) + if data.get('card_presence') in ('enable', 'disable'): + device.card_presence = data['card_presence'] logger.info(f'WMT update request received from {mac}') return jsonify({'status': 'received', 'message': 'Update request queued for admin review'}), 201 diff --git a/app/models/__init__.py b/app/models/__init__.py index 1ba80f1..d354fcb 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -50,6 +50,7 @@ class Device(Base): mac_address = Column(String(17), unique=True, nullable=True, index=True) config_updated_at = Column(DateTime) info_reviewed_at = Column(DateTime, default=lambda: datetime(1970, 1, 1)) + card_presence = Column(String(10), default='enable') # Relationships logs = relationship("LogEntry", back_populates="device") diff --git a/app/web/wmt.py b/app/web/wmt.py index ec07302..aef6757 100644 --- a/app/web/wmt.py +++ b/app/web/wmt.py @@ -1,7 +1,9 @@ """ WMT management web routes – global settings, device registry, update requests. """ -from flask import Blueprint, render_template, request, redirect, url_for, flash +import csv +import io +from flask import Blueprint, render_template, request, redirect, url_for, flash, Response from datetime import datetime from app.models import WMTGlobalConfig, Device, WMTUpdateRequest from config.database_config import get_db @@ -202,3 +204,162 @@ def reject_request(req_id): logger.error(f'WMT reject request error: {e}') flash(f'Error rejecting request: {e}', 'error') return redirect(url_for('wmt_web.update_requests')) + + +# --------------------------------------------------------------------------- +# Device registry +# --------------------------------------------------------------------------- + +@wmt_web_bp.route('/devices') +def devices(): + """List all WMT-registered devices.""" + try: + with get_db().get_session() as session: + device_list = ( + session.query(Device) + .filter(Device.mac_address.isnot(None)) + .order_by(Device.nume_masa) + .all() + ) + return render_template( + 'wmt/devices.html', + devices=device_list, + breadcrumbs=[ + {'url': url_for('wmt_web.index'), 'title': 'WMT Management'}, + {'url': url_for('wmt_web.devices'), 'title': 'Devices'}, + ], + ) + except Exception as e: + logger.error(f'WMT devices list error: {e}') + flash(f'Error: {e}', 'error') + return redirect(url_for('wmt_web.index')) + + +@wmt_web_bp.route('/devices/new', methods=['GET', 'POST']) +def device_new(): + """Register a new WMT device manually.""" + if request.method == 'POST': + mac = (request.form.get('mac_address') or '').strip().lower() + if not mac: + flash('MAC address is required.', 'error') + return render_template('wmt/device_form.html', device=None) + try: + with get_db().get_session() as session: + existing = session.query(Device).filter_by(mac_address=mac).first() + if existing: + flash(f'Device with MAC {mac} already exists.', 'error') + return render_template('wmt/device_form.html', device=None) + device = Device( + mac_address=mac, + nume_masa=request.form.get('device_name', '').strip(), + hostname=request.form.get('hostname', '').strip(), + device_ip=request.form.get('device_ip', '').strip() or '127.0.0.1', + location=request.form.get('location', '').strip() or None, + card_presence=request.form.get('card_presence', 'enable'), + description=request.form.get('notes', '').strip() or None, + info_reviewed_at=datetime.utcnow(), + ) + session.add(device) + flash('Device registered.', 'success') + return redirect(url_for('wmt_web.devices')) + except Exception as e: + logger.error(f'WMT device_new error: {e}') + flash(f'Error: {e}', 'error') + return render_template('wmt/device_form.html', device=None) + + return render_template( + '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('wmt_web.device_new'), 'title': 'New Device'}, + ], + ) + + +@wmt_web_bp.route('/devices//edit', methods=['GET', 'POST']) +def device_edit(device_id): + """Edit an existing WMT device.""" + try: + with get_db().get_session() as session: + device = session.query(Device).filter_by(id=device_id).first() + if not device: + flash('Device not found.', 'error') + return redirect(url_for('wmt_web.devices')) + + if request.method == 'POST': + device.nume_masa = request.form.get('device_name', '').strip() + device.hostname = request.form.get('hostname', '').strip() + device.device_ip = request.form.get('device_ip', '').strip() or device.device_ip + device.location = request.form.get('location', '').strip() or None + device.card_presence = request.form.get('card_presence', 'enable') + device.description = request.form.get('notes', '').strip() or None + device.config_updated_at = datetime.utcnow() + device.info_reviewed_at = datetime.utcnow() + flash('Device updated.', 'success') + return redirect(url_for('wmt_web.devices')) + + return render_template( + '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('wmt_web.device_edit', device_id=device_id), 'title': 'Edit'}, + ], + ) + except Exception as e: + logger.error(f'WMT device_edit error: {e}') + flash(f'Error: {e}', 'error') + return redirect(url_for('wmt_web.devices')) + + +@wmt_web_bp.route('/devices/export.csv') +def devices_export_csv(): + """Export all WMT devices as a CSV file.""" + try: + with get_db().get_session() as session: + device_list = ( + session.query(Device) + .filter(Device.mac_address.isnot(None)) + .order_by(Device.nume_masa) + .all() + ) + output = io.StringIO() + writer = csv.writer(output) + writer.writerow(['hostname', 'mac_address', 'work_place', 'rfid_card_status']) + for d in device_list: + writer.writerow([ + d.hostname or '', + d.mac_address or '', + d.nume_masa or '', + d.card_presence or 'enable', + ]) + csv_data = output.getvalue() + return Response( + csv_data, + mimetype='text/csv', + headers={'Content-Disposition': 'attachment; filename=wmt_devices.csv'}, + ) + except Exception as e: + logger.error(f'WMT devices_export_csv error: {e}') + flash(f'Export error: {e}', 'error') + return redirect(url_for('wmt_web.devices')) + + +@wmt_web_bp.route('/devices//delete', methods=['POST']) +def device_delete(device_id): + """Delete a WMT device.""" + try: + with get_db().get_session() as session: + device = session.query(Device).filter_by(id=device_id).first() + if device: + session.delete(device) + flash('Device deleted.', 'success') + else: + flash('Device not found.', 'error') + except Exception as e: + logger.error(f'WMT device_delete error: {e}') + flash(f'Error: {e}', 'error') + return redirect(url_for('wmt_web.devices')) diff --git a/templates/base.html b/templates/base.html index 2f855e6..fc4675c 100644 --- a/templates/base.html +++ b/templates/base.html @@ -263,6 +263,12 @@ {% if pending_wmt_count > 0 %}{{ pending_wmt_count }}{% endif %} +