279 lines
11 KiB
HTML
279 lines
11 KiB
HTML
{% extends "base.html" %}
|
||
|
||
{% block title %}Devices – {{ app_name }}{% endblock %}
|
||
{% block page_title %}Devices{% endblock %}
|
||
|
||
{% block extra_css %}
|
||
<style>
|
||
.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; }
|
||
</style>
|
||
{% endblock %}
|
||
|
||
{% block content %}
|
||
|
||
<!-- Stats row -->
|
||
<div class="row g-3 mb-4">
|
||
<div class="col-6 col-md-2">
|
||
<div class="card text-center h-100">
|
||
<div class="card-body py-3">
|
||
<h4 class="mb-0 text-success">{{ devices|selectattr('status','equalto','active')|list|length }}</h4>
|
||
<small class="text-muted">Active</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-6 col-md-2">
|
||
<div class="card text-center h-100">
|
||
<div class="card-body py-3">
|
||
<h4 class="mb-0 text-danger">{{ devices|selectattr('status','equalto','inactive')|list|length }}</h4>
|
||
<small class="text-muted">Offline</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-6 col-md-2">
|
||
<div class="card text-center h-100">
|
||
<div class="card-body py-3">
|
||
<h4 class="mb-0 text-warning">{{ devices|selectattr('status','equalto','maintenance')|list|length }}</h4>
|
||
<small class="text-muted">Maintenance</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-6 col-md-2">
|
||
<div class="card text-center h-100">
|
||
<div class="card-body py-3">
|
||
<h4 class="mb-0 text-primary">{{ devices|length }}</h4>
|
||
<small class="text-muted">Total</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-6 col-md-2">
|
||
<div class="card text-center h-100">
|
||
<div class="card-body py-3">
|
||
<h4 class="mb-0 text-info">{{ devices|selectattr('mac_address')|list|length }}</h4>
|
||
<small class="text-muted">WMT Clients</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-6 col-md-2">
|
||
<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">
|
||
<h4 class="mb-0 {% if pending_count > 0 %}text-danger{% else %}text-secondary{% endif %}">{{ pending_count }}</h4>
|
||
<small class="text-muted">Pending Requests</small>
|
||
</div>
|
||
</a>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Toolbar -->
|
||
<div class="d-flex flex-wrap gap-2 mb-3 align-items-center">
|
||
<input type="text" id="deviceSearch" class="form-control" style="max-width:340px"
|
||
placeholder="Search hostname, IP, name, MAC…" oninput="filterTable()">
|
||
<button class="btn btn-primary ms-auto" data-bs-toggle="modal" data-bs-target="#addDeviceModal">
|
||
<i class="fas fa-plus me-1"></i>Add Device
|
||
</button>
|
||
<button class="btn btn-outline-secondary" onclick="location.reload()">
|
||
<i class="fas fa-sync-alt me-1"></i>Refresh
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Device table -->
|
||
<div class="card">
|
||
<div class="card-body p-0">
|
||
<div class="table-responsive">
|
||
<table class="table table-hover tbl-sm mb-0" id="deviceTable">
|
||
<thead class="table-light">
|
||
<tr>
|
||
<th>Device Name</th>
|
||
<th>Hostname</th>
|
||
<th>IP</th>
|
||
<th>MAC Address</th>
|
||
<th>Status</th>
|
||
<th>Logs</th>
|
||
<th>Last Seen</th>
|
||
<th>Config Sync</th>
|
||
<th class="text-end">Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for device in devices %}
|
||
<tr class="device-row"
|
||
data-search="{{ device.hostname|lower }} {{ device.device_ip }} {{ (device.nume_masa or '')|lower }} {{ (device.mac_address or '')|lower }}">
|
||
<td>
|
||
<strong>{{ device.nume_masa or '—' }}</strong>
|
||
</td>
|
||
<td>{{ device.hostname }}</td>
|
||
<td><code>{{ device.device_ip }}</code></td>
|
||
<td>
|
||
{% if device.mac_address %}
|
||
<code class="mac-badge">{{ device.mac_address }}</code>
|
||
{% else %}
|
||
<span class="text-muted">—</span>
|
||
{% endif %}
|
||
</td>
|
||
<td>
|
||
{% if device.status == 'active' %}
|
||
<span class="badge bg-success">Active</span>
|
||
{% elif device.status == 'maintenance' %}
|
||
<span class="badge bg-warning text-dark">Maintenance</span>
|
||
{% else %}
|
||
<span class="badge bg-danger">Offline</span>
|
||
{% endif %}
|
||
</td>
|
||
<td>{{ device_log_counts.get(device.id, 0) }}</td>
|
||
<td class="text-muted">
|
||
{% if device.last_seen %}
|
||
{{ device.last_seen.strftime('%Y-%m-%d %H:%M') }}
|
||
{% else %}—{% endif %}
|
||
</td>
|
||
<td>
|
||
{% if device.mac_address and device.config_updated_at %}
|
||
<span class="sync-ok" title="{{ device.config_updated_at.strftime('%Y-%m-%d %H:%M:%S') }}">
|
||
<i class="fas fa-check-circle"></i>
|
||
{{ device.config_updated_at.strftime('%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>
|
||
{% endif %}
|
||
</td>
|
||
<td class="text-end text-nowrap">
|
||
<a href="{{ url_for('main.device_detail', device_id=device.id) }}"
|
||
class="btn btn-sm btn-outline-primary py-0" title="View Details">
|
||
<i class="fas fa-eye"></i>
|
||
</a>
|
||
<a href="{{ url_for('main.logs', device_id=device.id) }}"
|
||
class="btn btn-sm btn-outline-info py-0" title="View Logs">
|
||
<i class="fas fa-list"></i>
|
||
</a>
|
||
<a href="{{ url_for('main.device_edit', device_id=device.id) }}"
|
||
class="btn btn-sm btn-outline-secondary py-0" title="Edit">
|
||
<i class="fas fa-edit"></i>
|
||
</a>
|
||
<form method="post" action="{{ url_for('main.device_delete', device_id=device.id) }}"
|
||
class="d-inline"
|
||
onsubmit="return confirm('Delete device {{ device.hostname }}? This also removes all its logs.')">
|
||
<button type="submit" class="btn btn-sm btn-outline-danger py-0" title="Delete">
|
||
<i class="fas fa-trash"></i>
|
||
</button>
|
||
</form>
|
||
</td>
|
||
</tr>
|
||
{% else %}
|
||
<tr>
|
||
<td colspan="9" class="text-center text-muted py-5">
|
||
<i class="fas fa-desktop fa-2x mb-2 d-block"></i>
|
||
No devices registered yet.
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Add Device Modal -->
|
||
<div class="modal fade" id="addDeviceModal" tabindex="-1">
|
||
<div class="modal-dialog modal-lg">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title"><i class="fas fa-plus-circle me-2"></i>Add Device</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||
</div>
|
||
<form id="addDeviceForm" onsubmit="submitAddDevice(event)">
|
||
<div class="modal-body">
|
||
<div class="row g-3">
|
||
<div class="col-md-6">
|
||
<label class="form-label">Hostname <span class="text-danger">*</span></label>
|
||
<input type="text" class="form-control" name="hostname" required placeholder="RPI-Masa-01">
|
||
</div>
|
||
<div class="col-md-6">
|
||
<label class="form-label">IP Address <span class="text-danger">*</span></label>
|
||
<input type="text" class="form-control" name="device_ip" required placeholder="192.168.1.100">
|
||
</div>
|
||
<div class="col-md-6">
|
||
<label class="form-label">Device Name / Masa <span class="text-danger">*</span></label>
|
||
<input type="text" class="form-control" name="nume_masa" required placeholder="Masa-01">
|
||
</div>
|
||
<div class="col-md-6">
|
||
<label class="form-label">MAC Address
|
||
<small class="text-muted">(WMT clients only)</small>
|
||
</label>
|
||
<input type="text" class="form-control" name="mac_address"
|
||
placeholder="b8:27:eb:aa:bb:cc"
|
||
pattern="^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$">
|
||
</div>
|
||
<div class="col-md-6">
|
||
<label class="form-label">Device Type</label>
|
||
<select class="form-select" name="device_type">
|
||
<option value="Raspberry Pi">Raspberry Pi</option>
|
||
<option value="PC">PC/Workstation</option>
|
||
<option value="Server">Server</option>
|
||
<option value="unknown" selected>Unknown</option>
|
||
</select>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<label class="form-label">Status</label>
|
||
<select class="form-select" name="status">
|
||
<option value="active" selected>Active</option>
|
||
<option value="inactive">Inactive</option>
|
||
<option value="maintenance">Maintenance</option>
|
||
</select>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<label class="form-label">Physical Location</label>
|
||
<input type="text" class="form-control" name="location" placeholder="Floor 2, Room 201">
|
||
</div>
|
||
<div class="col-md-6">
|
||
<label class="form-label">OS Version</label>
|
||
<input type="text" class="form-control" name="os_version" placeholder="Raspberry Pi OS 11">
|
||
</div>
|
||
<div class="col-12">
|
||
<label class="form-label">Description</label>
|
||
<textarea class="form-control" name="description" rows="2"></textarea>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||
<button type="submit" class="btn btn-primary"><i class="fas fa-save me-1"></i>Add Device</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{% endblock %}
|
||
|
||
{% block extra_js %}
|
||
<script>
|
||
function filterTable() {
|
||
const q = document.getElementById('deviceSearch').value.toLowerCase();
|
||
document.querySelectorAll('.device-row').forEach(row => {
|
||
row.style.display = row.dataset.search.includes(q) ? '' : 'none';
|
||
});
|
||
}
|
||
|
||
async function submitAddDevice(event) {
|
||
event.preventDefault();
|
||
const data = Object.fromEntries(new FormData(event.target).entries());
|
||
const btn = event.target.querySelector('[type=submit]');
|
||
btn.disabled = true;
|
||
try {
|
||
const resp = await fetch('/api/devices/add', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify(data)
|
||
});
|
||
const result = await resp.json();
|
||
if (resp.ok) { location.reload(); }
|
||
else { alert('Error: ' + (result.message || 'Unknown error')); }
|
||
} catch(e) { alert('Error: ' + e.message); }
|
||
finally { btn.disabled = false; }
|
||
}
|
||
</script>
|
||
{% endblock %}
|