Add card_presence feature, device CRUD, CSV export, Update_Rest_WMT_client playbook, migrate_to_wmt dual-path idmasa
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -36,6 +36,7 @@ ansible/playbooks/*.yml
|
|||||||
!ansible/playbooks/distribute_ssh_keys.yml
|
!ansible/playbooks/distribute_ssh_keys.yml
|
||||||
!ansible/playbooks/restart_service.yml
|
!ansible/playbooks/restart_service.yml
|
||||||
!ansible/playbooks/migrate_to_wmt.yml
|
!ansible/playbooks/migrate_to_wmt.yml
|
||||||
|
!ansible/playbooks/Update_Rest_WMT_client.yml
|
||||||
|
|
||||||
# VS Code
|
# VS Code
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|||||||
453
ansible/playbooks/Update_Rest_WMT_client.yml
Normal file
453
ansible/playbooks/Update_Rest_WMT_client.yml
Normal file
@@ -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 }}
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# 1. Create /home/pi/Desktop/WMT on the target
|
# 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
|
# 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)
|
# 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
|
# 5. Update ~/.config/wayfire.ini [autostart] start_python to launch from WMT
|
||||||
# 6. Rename /home/pi/Desktop/Prezenta → /home/pi/Desktop/Prezenta_Old_Data
|
# 6. Rename /home/pi/Desktop/Prezenta → /home/pi/Desktop/Prezenta_Old_Data
|
||||||
@@ -49,10 +49,44 @@
|
|||||||
group: pi
|
group: pi
|
||||||
mode: '0755'
|
mode: '0755'
|
||||||
|
|
||||||
# ── 4. Read idmasa.txt from the Prezenta data folder ─────────────────
|
# ── 4. Read idmasa.txt – supports two Prezenta layouts ───────────────
|
||||||
- name: Read idmasa.txt from Prezenta
|
# 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:
|
slurp:
|
||||||
src: /home/pi/Desktop/Prezenta/data/idmasa.txt
|
src: "{{ idmasa_path }}"
|
||||||
register: idmasa_raw
|
register: idmasa_raw
|
||||||
|
|
||||||
- name: Decode idmasa value
|
- name: Decode idmasa value
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ def get_device_config(mac_address):
|
|||||||
'hostname': device.hostname if device else '',
|
'hostname': device.hostname if device else '',
|
||||||
'device_ip': device.device_ip if device else '',
|
'device_ip': device.device_ip if device else '',
|
||||||
'location': device.location 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)
|
# 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',
|
'info_reviewed_at': device.info_reviewed_at.isoformat() if (device and device.info_reviewed_at) else '1970-01-01T00:00:00',
|
||||||
# Sync metadata
|
# Sync metadata
|
||||||
@@ -162,6 +163,9 @@ def submit_update_request():
|
|||||||
# Update device last_seen
|
# Update device last_seen
|
||||||
if device:
|
if device:
|
||||||
device.last_seen = datetime.utcnow()
|
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}')
|
logger.info(f'WMT update request received from {mac}')
|
||||||
return jsonify({'status': 'received', 'message': 'Update request queued for admin review'}), 201
|
return jsonify({'status': 'received', 'message': 'Update request queued for admin review'}), 201
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ class Device(Base):
|
|||||||
mac_address = Column(String(17), unique=True, nullable=True, index=True)
|
mac_address = Column(String(17), unique=True, nullable=True, index=True)
|
||||||
config_updated_at = Column(DateTime)
|
config_updated_at = Column(DateTime)
|
||||||
info_reviewed_at = Column(DateTime, default=lambda: datetime(1970, 1, 1))
|
info_reviewed_at = Column(DateTime, default=lambda: datetime(1970, 1, 1))
|
||||||
|
card_presence = Column(String(10), default='enable')
|
||||||
|
|
||||||
# Relationships
|
# Relationships
|
||||||
logs = relationship("LogEntry", back_populates="device")
|
logs = relationship("LogEntry", back_populates="device")
|
||||||
|
|||||||
163
app/web/wmt.py
163
app/web/wmt.py
@@ -1,7 +1,9 @@
|
|||||||
"""
|
"""
|
||||||
WMT management web routes – global settings, device registry, update requests.
|
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 datetime import datetime
|
||||||
from app.models import WMTGlobalConfig, Device, WMTUpdateRequest
|
from app.models import WMTGlobalConfig, Device, WMTUpdateRequest
|
||||||
from config.database_config import get_db
|
from config.database_config import get_db
|
||||||
@@ -202,3 +204,162 @@ def reject_request(req_id):
|
|||||||
logger.error(f'WMT reject request error: {e}')
|
logger.error(f'WMT reject request error: {e}')
|
||||||
flash(f'Error rejecting request: {e}', 'error')
|
flash(f'Error rejecting request: {e}', 'error')
|
||||||
return redirect(url_for('wmt_web.update_requests'))
|
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/<int:device_id>/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/<int:device_id>/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'))
|
||||||
|
|||||||
@@ -263,6 +263,12 @@
|
|||||||
{% if pending_wmt_count > 0 %}<span class="badge bg-danger ms-1">{{ pending_wmt_count }}</span>{% endif %}
|
{% if pending_wmt_count > 0 %}<span class="badge bg-danger ms-1">{{ pending_wmt_count }}</span>{% endif %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="{{ url_for('wmt_web.devices') }}" class="nav-link {% if request.endpoint in ['wmt_web.devices','wmt_web.device_new','wmt_web.device_edit'] %}active{% endif %}">
|
||||||
|
<i class="fas fa-desktop"></i>
|
||||||
|
WMT Devices
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
<div class="nav-section">Monitoring</div>
|
<div class="nav-section">Monitoring</div>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
|
|||||||
@@ -62,6 +62,16 @@
|
|||||||
<textarea name="notes" class="form-control" rows="2">{{ device.notes or '' if device else '' }}</textarea>
|
<textarea name="notes" class="form-control" rows="2">{{ device.notes or '' if device else '' }}</textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label fw-semibold">Card Presence
|
||||||
|
<small class="text-muted fw-normal">(enable = RFID card reader present; disable = no card reader)</small>
|
||||||
|
</label>
|
||||||
|
<select name="card_presence" class="form-select">
|
||||||
|
<option value="enable" {% if not device or device.card_presence != 'disable' %}selected{% endif %}>enable</option>
|
||||||
|
<option value="disable" {% if device and device.card_presence == 'disable' %}selected{% endif %}>disable</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% if device %}
|
{% if device %}
|
||||||
<div class="alert alert-light border small mb-4">
|
<div class="alert alert-light border small mb-4">
|
||||||
<strong>Last seen:</strong>
|
<strong>Last seen:</strong>
|
||||||
|
|||||||
@@ -7,9 +7,14 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="mb-3 d-flex justify-content-between align-items-center">
|
<div class="mb-3 d-flex justify-content-between align-items-center">
|
||||||
<p class="text-muted mb-0">{{ devices | length }} device(s) registered.</p>
|
<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">
|
<a href="{{ url_for('wmt_web.device_new') }}" class="btn btn-success">
|
||||||
<i class="fas fa-plus me-1"></i> New Device
|
<i class="fas fa-plus me-1"></i> New Device
|
||||||
</a>
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
@@ -23,6 +28,7 @@
|
|||||||
<th>MAC Address</th>
|
<th>MAC Address</th>
|
||||||
<th>Hostname</th>
|
<th>Hostname</th>
|
||||||
<th>IP Address</th>
|
<th>IP Address</th>
|
||||||
|
<th>Card Presence</th>
|
||||||
<th>Last Seen</th>
|
<th>Last Seen</th>
|
||||||
<th>Config Updated</th>
|
<th>Config Updated</th>
|
||||||
<th class="text-end">Actions</th>
|
<th class="text-end">Actions</th>
|
||||||
@@ -35,6 +41,13 @@
|
|||||||
<td><code>{{ d.mac_address }}</code></td>
|
<td><code>{{ d.mac_address }}</code></td>
|
||||||
<td>{{ d.hostname or '—' }}</td>
|
<td>{{ d.hostname or '—' }}</td>
|
||||||
<td>{{ d.device_ip 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">
|
<td class="text-muted small">
|
||||||
{{ d.last_seen | local_dt if d.last_seen else 'Never' }}
|
{{ d.last_seen | local_dt if d.last_seen else 'Never' }}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
Reference in New Issue
Block a user