feat: UI improvements and WMT sync workflow overhaul
This commit is contained in:
+82
-56
@@ -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/<mac> 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
|
||||
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
+11
-3
@@ -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/<int:device_id>')
|
||||
def device_detail(device_id):
|
||||
|
||||
Reference in New Issue
Block a user