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'])
|
@wmt_api_bp.route('/config/update_request', methods=['POST'])
|
||||||
def submit_update_request():
|
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.
|
1. Device known AND config matches server record
|
||||||
2. Device registered, info UNCHANGED → update last_seen only; no request created.
|
→ record config_synced_at = now, update last_seen
|
||||||
3. Device registered, info CHANGED → create/refresh a pending WMTUpdateRequest.
|
→ 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:
|
Expected JSON:
|
||||||
{
|
{
|
||||||
@@ -139,8 +153,8 @@ def submit_update_request():
|
|||||||
"device_name": "Masa-01",
|
"device_name": "Masa-01",
|
||||||
"hostname": "rpi-masa01",
|
"hostname": "rpi-masa01",
|
||||||
"device_ip": "192.168.1.100",
|
"device_ip": "192.168.1.100",
|
||||||
"card_presence": "enable", // optional – applied directly, no approval
|
"card_presence": "enable",
|
||||||
"client_config_mtime": "2026-04-22T09:30:00" // optional
|
"client_config_mtime": "2026-04-22T09:30:00"
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
if not request.is_json:
|
if not request.is_json:
|
||||||
@@ -157,70 +171,82 @@ def submit_update_request():
|
|||||||
card_presence = data.get('card_presence')
|
card_presence = data.get('card_presence')
|
||||||
|
|
||||||
def _eq(a, b):
|
def _eq(a, b):
|
||||||
"""Compare two values treating None and '' as equivalent."""
|
|
||||||
return (a or '') == (b or '')
|
return (a or '') == (b or '')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with get_db().get_session() as session:
|
with get_db().get_session() as session:
|
||||||
device = session.query(Device).filter_by(mac_address=mac).first()
|
device = session.query(Device).filter_by(mac_address=mac).first()
|
||||||
|
|
||||||
# Always update last_seen and card_presence (no approval needed)
|
# ── Outcome 3: unknown device ─────────────────────────────
|
||||||
if device:
|
if not device:
|
||||||
device.last_seen = datetime.utcnow()
|
# Check if a pending request with the same data already exists
|
||||||
if card_presence in ('enable', 'disable'):
|
existing = (
|
||||||
device.card_presence = card_presence
|
session.query(WMTUpdateRequest)
|
||||||
|
.filter_by(mac_address=mac, status='pending')
|
||||||
# --- Determine whether the proposed info differs from the server record ---
|
.order_by(WMTUpdateRequest.submitted_at.desc())
|
||||||
if device:
|
.first()
|
||||||
info_changed = not (
|
|
||||||
_eq(proposed_name, device.nume_masa) and
|
|
||||||
_eq(proposed_hostname, device.hostname) and
|
|
||||||
_eq(proposed_ip, device.device_ip)
|
|
||||||
)
|
)
|
||||||
else:
|
if existing and (
|
||||||
# Unknown device – always needs admin attention
|
_eq(existing.proposed_device_name, proposed_name) and
|
||||||
info_changed = True
|
_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:
|
return jsonify({
|
||||||
# Heartbeat only – nothing for admin to review
|
'status': 'pending_approval',
|
||||||
logger.debug(f'WMT heartbeat from {mac} – info unchanged, last_seen updated')
|
'message': 'Device not registered. Awaiting admin approval.',
|
||||||
return jsonify({'status': 'no_change', 'message': 'Device info matches server record'}), 200
|
}), 202
|
||||||
|
|
||||||
# --- Info changed (or device unknown): avoid duplicate pending requests ---
|
# Device is known from here on ─────────────────────────────
|
||||||
existing = (
|
device.last_seen = datetime.utcnow()
|
||||||
session.query(WMTUpdateRequest)
|
if card_presence in ('enable', 'disable'):
|
||||||
.filter_by(mac_address=mac, status='pending')
|
device.card_presence = card_presence
|
||||||
.order_by(WMTUpdateRequest.submitted_at.desc())
|
|
||||||
.first()
|
# ── 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
|
if config_in_sync:
|
||||||
req = WMTUpdateRequest(
|
device.config_synced_at = datetime.utcnow()
|
||||||
mac_address=mac,
|
logger.debug(f'WMT config OK for {mac} – synced_at updated')
|
||||||
device_id=device.id if device else None,
|
return jsonify({
|
||||||
proposed_device_name=proposed_name or None,
|
'status': 'ok',
|
||||||
proposed_hostname=proposed_hostname or None,
|
'in_sync': True,
|
||||||
proposed_device_ip=proposed_ip or None,
|
'message': 'Config matches server record.',
|
||||||
client_config_mtime=data.get('client_config_mtime'),
|
}), 200
|
||||||
submitted_at=datetime.utcnow(),
|
|
||||||
status='pending',
|
# ── 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:
|
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
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,8 @@ class Device(Base):
|
|||||||
|
|
||||||
# WMT (Workstation Management Terminal) integration fields
|
# WMT (Workstation Management Terminal) integration fields
|
||||||
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) # 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))
|
info_reviewed_at = Column(DateTime, default=lambda: datetime(1970, 1, 1))
|
||||||
card_presence = Column(String(10), default='enable')
|
card_presence = Column(String(10), default='enable')
|
||||||
|
|
||||||
|
|||||||
+11
-3
@@ -34,7 +34,7 @@ def devices():
|
|||||||
with get_db().get_session() as session:
|
with get_db().get_session() as session:
|
||||||
devices = session.query(Device).order_by(Device.last_seen.desc()).all()
|
devices = session.query(Device).order_by(Device.last_seen.desc()).all()
|
||||||
|
|
||||||
# Get log count per device
|
# Log count per device
|
||||||
device_log_counts = {}
|
device_log_counts = {}
|
||||||
for device in devices:
|
for device in devices:
|
||||||
log_count = session.query(LogEntry).filter_by(device_id=device.id).count()
|
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_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',
|
return render_template('device_management.html',
|
||||||
devices=devices,
|
devices=devices,
|
||||||
device_log_counts=device_log_counts,
|
device_log_counts=device_log_counts,
|
||||||
pending_count=pending_count)
|
pending_count=pending_count,
|
||||||
|
pending_by_mac=pending_by_mac)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error loading devices: {e}")
|
logging.error(f"Error loading devices: {e}")
|
||||||
flash(f'Error loading devices: {e}', 'error')
|
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>')
|
@main_bp.route('/device/<int:device_id>')
|
||||||
def device_detail(device_id):
|
def device_detail(device_id):
|
||||||
|
|||||||
@@ -57,6 +57,22 @@ class DatabaseSchemaUpdater:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f" ❌ Error adding {column_name}: {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):
|
def create_tables(self):
|
||||||
"""Create all tables using SQLAlchemy metadata"""
|
"""Create all tables using SQLAlchemy metadata"""
|
||||||
print("🏗️ Creating all database tables...")
|
print("🏗️ Creating all database tables...")
|
||||||
@@ -109,6 +125,7 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
# Update schema
|
# Update schema
|
||||||
updater.update_playbook_executions_schema()
|
updater.update_playbook_executions_schema()
|
||||||
|
updater.add_config_synced_at_column()
|
||||||
|
|
||||||
# Verify
|
# Verify
|
||||||
if updater.verify_schema():
|
if updater.verify_schema():
|
||||||
|
|||||||
@@ -38,6 +38,26 @@
|
|||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
margin: -1rem -1rem 1rem -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;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
+769
-171
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,33 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}Devices – {{ app_name }}{% endblock %}
|
{% block title %}Devices – {{ app_name }}{% endblock %}
|
||||||
{% block page_title %}Devices{% endblock %}
|
{% block page_title %}Device Health{% endblock %}
|
||||||
|
|
||||||
{% block extra_css %}
|
{% block extra_css %}
|
||||||
<style>
|
<style>
|
||||||
.mac-badge { font-size: 0.75rem; letter-spacing: 0.03em; }
|
.mac-badge { font-size: 0.75rem; letter-spacing: 0.03em; }
|
||||||
.sync-ok { color: #2ecc71; }
|
|
||||||
.sync-old { color: #e74c3c; }
|
|
||||||
.tbl-sm td, .tbl-sm th { padding: 0.45rem 0.6rem; font-size: 0.88rem; vertical-align: middle; }
|
.tbl-sm td, .tbl-sm th { padding: 0.45rem 0.6rem; font-size: 0.88rem; vertical-align: middle; }
|
||||||
|
|
||||||
|
/* Sync status pills */
|
||||||
|
.sync-pill {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 3px 9px;
|
||||||
|
border-radius: 20px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.sync-ok { background: #d4edda; color: #155724; }
|
||||||
|
.sync-required { background: #fff3cd; color: #856404; }
|
||||||
|
.sync-pending { background: #cce5ff; color: #004085; }
|
||||||
|
.sync-never { background: #e2e3e5; color: #495057; }
|
||||||
|
|
||||||
|
body.dark-mode .sync-ok { background: #1a3a25; color: #6fcf97; }
|
||||||
|
body.dark-mode .sync-required { background: #3a2e00; color: #f0c040; }
|
||||||
|
body.dark-mode .sync-pending { background: #003060; color: #7ec8e3; }
|
||||||
|
body.dark-mode .sync-never { background: #2a2a2a; color: #999; }
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@@ -60,7 +79,7 @@
|
|||||||
<a href="{{ url_for('wmt_web.update_requests', status='pending') }}" class="card text-center h-100 text-decoration-none">
|
<a href="{{ url_for('wmt_web.update_requests', status='pending') }}" class="card text-center h-100 text-decoration-none">
|
||||||
<div class="card-body py-3">
|
<div class="card-body py-3">
|
||||||
<h4 class="mb-0 {% if pending_count > 0 %}text-danger{% else %}text-secondary{% endif %}">{{ pending_count }}</h4>
|
<h4 class="mb-0 {% if pending_count > 0 %}text-danger{% else %}text-secondary{% endif %}">{{ pending_count }}</h4>
|
||||||
<small class="text-muted">Pending Requests</small>
|
<small class="text-muted">Pending Approval</small>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -88,7 +107,7 @@
|
|||||||
<th>Work Place</th>
|
<th>Work Place</th>
|
||||||
<th>Hostname</th>
|
<th>Hostname</th>
|
||||||
<th>IP</th>
|
<th>IP</th>
|
||||||
<th>MAC Address</th>
|
<th>MAC / Type</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Logs</th>
|
<th>Logs</th>
|
||||||
<th>Last Seen</th>
|
<th>Last Seen</th>
|
||||||
@@ -98,20 +117,35 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for device in devices %}
|
{% 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) %}
|
||||||
<tr class="device-row"
|
<tr class="device-row"
|
||||||
data-search="{{ device.hostname|lower }} {{ device.device_ip }} {{ (device.nume_masa or '')|lower }} {{ (device.mac_address or '')|lower }}">
|
data-search="{{ device.hostname|lower }} {{ device.device_ip }} {{ (device.nume_masa or '')|lower }} {{ (device.mac_address or '')|lower }}">
|
||||||
|
|
||||||
|
<!-- Work Place -->
|
||||||
<td>
|
<td>
|
||||||
<strong>{{ device.nume_masa or '—' }}</strong>
|
<strong>{{ device.nume_masa or '—' }}</strong>
|
||||||
|
{% if is_wmt %}
|
||||||
|
<br><small class="text-muted"><i class="fas fa-tablet-alt me-1"></i>WMT</small>
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
<!-- Hostname -->
|
||||||
<td>{{ device.hostname }}</td>
|
<td>{{ device.hostname }}</td>
|
||||||
|
|
||||||
|
<!-- IP -->
|
||||||
<td><code>{{ device.device_ip }}</code></td>
|
<td><code>{{ device.device_ip }}</code></td>
|
||||||
|
|
||||||
|
<!-- MAC / Type -->
|
||||||
<td>
|
<td>
|
||||||
{% if device.mac_address %}
|
{% if is_wmt %}
|
||||||
<code class="mac-badge">{{ device.mac_address }}</code>
|
<code class="mac-badge">{{ device.mac_address }}</code>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-muted">—</span>
|
<span class="text-muted">—</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
<!-- Status -->
|
||||||
<td>
|
<td>
|
||||||
{% if device.status == 'active' %}
|
{% if device.status == 'active' %}
|
||||||
<span class="badge bg-success">Active</span>
|
<span class="badge bg-success">Active</span>
|
||||||
@@ -121,24 +155,55 @@
|
|||||||
<span class="badge bg-danger">Offline</span>
|
<span class="badge bg-danger">Offline</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
<!-- Logs -->
|
||||||
<td>{{ device_log_counts.get(device.id, 0) }}</td>
|
<td>{{ device_log_counts.get(device.id, 0) }}</td>
|
||||||
|
|
||||||
|
<!-- Last Seen -->
|
||||||
<td class="text-muted">
|
<td class="text-muted">
|
||||||
{% if device.last_seen %}
|
{% if device.last_seen %}
|
||||||
{{ device.last_seen | local_dt }}
|
{{ device.last_seen | local_dt }}
|
||||||
{% else %}—{% endif %}
|
{% else %}—{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
<!-- Config Sync status -->
|
||||||
<td>
|
<td>
|
||||||
{% if device.mac_address and device.config_updated_at %}
|
{% if not is_wmt %}
|
||||||
<span class="sync-ok" title="{{ device.config_updated_at | local_dt('%Y-%m-%d %H:%M:%S') }}">
|
|
||||||
<i class="fas fa-check-circle"></i>
|
|
||||||
{{ device.config_updated_at | local_dt('%m-%d %H:%M') }}
|
|
||||||
</span>
|
|
||||||
{% elif device.mac_address %}
|
|
||||||
<span class="sync-old"><i class="fas fa-clock"></i> Never</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="text-muted">—</span>
|
<span class="text-muted">—</span>
|
||||||
|
|
||||||
|
{% elif has_pending %}
|
||||||
|
{# Unknown device waiting for admin approval #}
|
||||||
|
<a href="{{ url_for('wmt_web.update_requests', status='pending') }}"
|
||||||
|
class="sync-pill sync-pending text-decoration-none"
|
||||||
|
title="Admin approval required for this device">
|
||||||
|
<i class="fas fa-user-clock"></i> Pending Approval
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{% elif device.config_synced_at %}
|
||||||
|
{# Device checked in and server confirmed configs match #}
|
||||||
|
<span class="sync-pill sync-ok"
|
||||||
|
title="Last confirmed in-sync: {{ device.config_synced_at | local_dt('%Y-%m-%d %H:%M:%S') }}">
|
||||||
|
<i class="fas fa-check-circle"></i>
|
||||||
|
OK · {{ device.config_synced_at | local_dt('%m-%d %H:%M') }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{% elif device.config_updated_at %}
|
||||||
|
{# Admin pushed config but client hasn't confirmed sync yet #}
|
||||||
|
<span class="sync-pill sync-required"
|
||||||
|
title="Config updated {{ device.config_updated_at | local_dt('%Y-%m-%d %H:%M:%S') }} – awaiting client check-in">
|
||||||
|
<i class="fas fa-arrow-circle-down"></i> Awaiting client
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
{# WMT device registered but never checked in #}
|
||||||
|
<span class="sync-pill sync-never"
|
||||||
|
title="Client has not checked in yet">
|
||||||
|
<i class="fas fa-question-circle"></i> Never synced
|
||||||
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
<!-- Actions -->
|
||||||
<td class="text-end text-nowrap">
|
<td class="text-end text-nowrap">
|
||||||
<a href="{{ url_for('main.device_detail', device_id=device.id) }}"
|
<a href="{{ url_for('main.device_detail', device_id=device.id) }}"
|
||||||
class="btn btn-sm btn-outline-primary py-0" title="View Details">
|
class="btn btn-sm btn-outline-primary py-0" title="View Details">
|
||||||
@@ -175,6 +240,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Sync legend -->
|
||||||
|
<div class="d-flex flex-wrap gap-3 mt-3 ms-1">
|
||||||
|
<small class="text-muted"><strong>Config Sync legend:</strong></small>
|
||||||
|
<small><span class="sync-pill sync-ok"><i class="fas fa-check-circle"></i> OK</span> — client confirmed configs match</small>
|
||||||
|
<small><span class="sync-pill sync-required"><i class="fas fa-arrow-circle-down"></i> Awaiting client</span> — server pushed new config, waiting for client check-in</small>
|
||||||
|
<small><span class="sync-pill sync-pending"><i class="fas fa-user-clock"></i> Pending Approval</span> — new/unknown device waiting for admin</small>
|
||||||
|
<small><span class="sync-pill sync-never"><i class="fas fa-question-circle"></i> Never synced</span> — registered but hasn't checked in</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Add Device Modal -->
|
<!-- Add Device Modal -->
|
||||||
<div class="modal fade" id="addDeviceModal" tabindex="-1">
|
<div class="modal fade" id="addDeviceModal" tabindex="-1">
|
||||||
<div class="modal-dialog modal-lg">
|
<div class="modal-dialog modal-lg">
|
||||||
@@ -276,3 +350,4 @@ async function submitAddDevice(event) {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
@@ -61,6 +61,20 @@
|
|||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Dark mode overrides for this page */
|
||||||
|
body.dark-mode .filter-container {
|
||||||
|
background-color: #2a2a2a;
|
||||||
|
border: 1px solid #444444;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .log-entry:hover {
|
||||||
|
background-color: #2e2e2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .log-meta {
|
||||||
|
color: #888888;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user