f1449285ba
- api/wmt.py: add GET /api/wmt/client/version and GET /api/wmt/client/download endpoints; rewrite submit_update_request with dedup logic - web/wmt.py: add releases, releases_upload, releases_delete, releases_build routes; build-from-folder excludes hidden/data/venv/pyc files - web/main.py: admin per-device delete route; clear-device-logs route; pass devices list to admin template - templates/wmt/releases.html: new release management page (current release info, upload form, build-from-folder card) - templates/admin.html: replace nuclear clear-devices with clear-logs + per-device delete table - templates/base.html: add Client Releases nav link in WMT sidebar section - templates/ansible/execute.html: add Update WMT Code playbook card - ansible/playbooks/update_wmt_code.yml: rsync WMT_project to clients excluding data/; backs up app.py; restarts wmt service - ansible_service.py: register update_wmt_code description - .gitignore: whitelist update_wmt_code.yml
315 lines
14 KiB
HTML
315 lines
14 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Admin — Server Monitoring{% endblock %}
|
|
{% block page_title %}Admin & Maintenance{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
.danger-card { border: 2px solid #dc3545; }
|
|
.danger-card .card-header { background-color: #dc3545; color: #fff; }
|
|
.warning-card { border: 2px solid #fd7e14; }
|
|
.warning-card .card-header { background-color: #fd7e14; color: #fff; }
|
|
.stat-box { background: #f8f9fa; border-radius: 8px; padding: 12px 20px; text-align: center; }
|
|
.stat-box .num { font-size: 2rem; font-weight: bold; line-height: 1; }
|
|
.stat-box .lbl { font-size: .78rem; color: #6c757d; margin-top: 2px; }
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid">
|
|
|
|
<!-- Current stats row -->
|
|
<div class="row g-3 mb-4">
|
|
<div class="col-12">
|
|
<div class="card">
|
|
<div class="card-header bg-secondary text-white">
|
|
<h5 class="mb-0"><i class="fas fa-database me-2"></i>Current Database & Inventory State</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row g-3">
|
|
<div class="col-6 col-md-2">
|
|
<div class="stat-box">
|
|
<div class="num text-primary" id="stat-devices">{{ stats.get('devices', '?') }}</div>
|
|
<div class="lbl">Devices</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-6 col-md-2">
|
|
<div class="stat-box">
|
|
<div class="num text-info" id="stat-logs">{{ stats.get('logs', '?') }}</div>
|
|
<div class="lbl">Log Entries</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-6 col-md-2">
|
|
<div class="stat-box">
|
|
<div class="num text-secondary" id="stat-templates">{{ stats.get('templates', '?') }}</div>
|
|
<div class="lbl">Msg Templates</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-6 col-md-2">
|
|
<div class="stat-box">
|
|
<div class="num text-danger" id="stat-wmt">{{ stats.get('wmt_requests', '?') }}</div>
|
|
<div class="lbl">WMT Requests</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-6 col-md-2">
|
|
<div class="stat-box">
|
|
<div class="num text-warning" id="stat-inv-hosts">{{ stats.get('inventory_hosts', '?') }}</div>
|
|
<div class="lbl">Inventory Hosts</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-6 col-md-2">
|
|
<div class="stat-box">
|
|
<div class="num text-success" id="stat-inv-groups">{{ stats.get('inventory_groups_yaml', '?') }}</div>
|
|
<div class="lbl">Inventory Groups</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-4">
|
|
|
|
<!-- Clear Log Entries -->
|
|
<div class="col-md-3">
|
|
<div class="card warning-card h-100">
|
|
<div class="card-header">
|
|
<h5 class="mb-0"><i class="fas fa-stream me-2"></i>Clear Log Entries</h5>
|
|
</div>
|
|
<div class="card-body d-flex flex-column">
|
|
<p class="text-muted flex-grow-1">
|
|
Deletes <strong>all log entries</strong> from the database.
|
|
Devices remain intact — they will start logging again automatically.
|
|
</p>
|
|
<div class="alert alert-warning py-2 mb-3">
|
|
<i class="fas fa-exclamation-triangle me-1"></i>
|
|
Currently <strong id="badge-logs">{{ stats.get('logs', '?') }}</strong> log entries.
|
|
</div>
|
|
<button class="btn btn-warning w-100"
|
|
onclick="runAction('clear-logs', 'Delete ALL log entries? This cannot be undone.')">
|
|
<i class="fas fa-trash me-2"></i>Clear All Logs
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Clear Device Logs -->
|
|
<div class="col-md-3">
|
|
<div class="card warning-card h-100">
|
|
<div class="card-header">
|
|
<h5 class="mb-0"><i class="fas fa-file-alt me-2"></i>Clear Device Logs</h5>
|
|
</div>
|
|
<div class="card-body d-flex flex-column">
|
|
<p class="text-muted flex-grow-1">
|
|
Deletes <strong>all log entries</strong> from the database.
|
|
Registered devices are <strong>not affected</strong> and will continue logging automatically.
|
|
</p>
|
|
<div class="alert alert-warning py-2 mb-3">
|
|
<i class="fas fa-exclamation-triangle me-1"></i>
|
|
Currently <strong id="badge-logs2">{{ stats.get('logs', '?') }}</strong> log entries.
|
|
</div>
|
|
<button class="btn btn-warning w-100"
|
|
onclick="runAction('clear-device-logs', 'Delete ALL device log entries? Devices stay registered. This cannot be undone.')">
|
|
<i class="fas fa-trash me-2"></i>Clear Device Logs
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Registered Device Registry -->
|
|
<div class="col-md-6">
|
|
<div class="card danger-card h-100">
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
<h5 class="mb-0"><i class="fas fa-server me-2"></i>Registered Device Registry</h5>
|
|
<span class="badge bg-light text-danger">{{ devices|length }} device(s)</span>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
{% if devices %}
|
|
<div class="table-responsive" style="max-height:320px;overflow-y:auto;">
|
|
<table class="table table-sm table-hover mb-0">
|
|
<thead class="table-light sticky-top">
|
|
<tr>
|
|
<th>Work Place</th>
|
|
<th>Hostname</th>
|
|
<th>MAC</th>
|
|
<th>IP</th>
|
|
<th class="text-end">Delete</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="device-registry-body">
|
|
{% for d in devices %}
|
|
<tr id="device-row-{{ d.id }}">
|
|
<td>{{ d.nume_masa or '—' }}</td>
|
|
<td>{{ d.hostname or '—' }}</td>
|
|
<td><code>{{ d.mac_address or '—' }}</code></td>
|
|
<td>{{ d.device_ip or '—' }}</td>
|
|
<td class="text-end">
|
|
<button class="btn btn-danger btn-sm"
|
|
onclick="deleteDevice({{ d.id }}, '{{ (d.nume_masa or d.hostname)|e }}')">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% else %}
|
|
<div class="p-4 text-center text-muted">
|
|
<i class="fas fa-check-circle fa-2x mb-2 text-success"></i><br>
|
|
No registered devices.
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Clear Ansible Inventory -->
|
|
<div class="col-md-3">
|
|
<div class="card danger-card h-100">
|
|
<div class="card-header">
|
|
<h5 class="mb-0"><i class="fas fa-sitemap me-2"></i>Clear Ansible Inventory</h5>
|
|
</div>
|
|
<div class="card-body d-flex flex-column">
|
|
<p class="text-muted flex-grow-1">
|
|
Resets the Ansible inventory file and clears all inventory groups.
|
|
The file is left fully empty — ready for new hosts and groups.
|
|
</p>
|
|
<div class="alert alert-danger py-2 mb-3">
|
|
<i class="fas fa-exclamation-triangle me-1"></i>
|
|
Currently <strong id="badge-inv-hosts">{{ stats.get('inventory_hosts', '?') }}</strong> hosts
|
|
in <strong id="badge-inv-groups">{{ stats.get('inventory_groups_yaml', '?') }}</strong> group(s).
|
|
</div>
|
|
<button class="btn btn-danger w-100"
|
|
onclick="runAction('clear-inventory', 'Reset Ansible inventory and delete all groups? This cannot be undone.')">
|
|
<i class="fas fa-trash me-2"></i>Clear Inventory
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Clear WMT Update Requests -->
|
|
<div class="col-md-3">
|
|
<div class="card warning-card h-100">
|
|
<div class="card-header">
|
|
<h5 class="mb-0"><i class="fas fa-clipboard-list me-2"></i>Clear WMT Requests</h5>
|
|
</div>
|
|
<div class="card-body d-flex flex-column">
|
|
<p class="text-muted flex-grow-1">
|
|
Deletes <strong>all WMT update requests</strong> (pending, accepted and rejected).
|
|
Devices are not affected and can submit new requests at any time.
|
|
</p>
|
|
<div class="alert alert-warning py-2 mb-3">
|
|
<i class="fas fa-exclamation-triangle me-1"></i>
|
|
Currently <strong id="badge-wmt">{{ stats.get('wmt_requests', '?') }}</strong> update request(s).
|
|
</div>
|
|
<button class="btn btn-warning w-100"
|
|
onclick="runAction('clear-wmt', 'Delete ALL WMT update requests? This cannot be undone.')">
|
|
<i class="fas fa-trash me-2"></i>Clear WMT Requests
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div><!-- /row -->
|
|
|
|
</div><!-- /container -->
|
|
|
|
<!-- Result toast -->
|
|
<div class="position-fixed bottom-0 end-0 p-3" style="z-index:9999">
|
|
<div id="resultToast" class="toast align-items-center text-white border-0" role="alert" aria-live="assertive">
|
|
<div class="d-flex">
|
|
<div class="toast-body" id="toastMsg">Done.</div>
|
|
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const ENDPOINTS = {
|
|
'clear-logs': '{{ url_for("main.admin_clear_logs") }}',
|
|
'clear-device-logs': '{{ url_for("main.admin_clear_device_logs") }}',
|
|
'clear-inventory': '{{ url_for("main.admin_clear_inventory") }}',
|
|
'clear-wmt': '{{ url_for("main.admin_clear_wmt") }}'
|
|
};
|
|
|
|
function runAction(action, confirmMsg) {
|
|
if (!confirm(confirmMsg)) return;
|
|
const btn = event.currentTarget;
|
|
btn.disabled = true;
|
|
const origLabel = btn.innerHTML;
|
|
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Working…';
|
|
|
|
fetch(ENDPOINTS[action], {
|
|
method: 'POST',
|
|
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
|
})
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showToast('success', buildMessage(action, data));
|
|
refreshStats();
|
|
} else {
|
|
showToast('danger', 'Error: ' + (data.error || 'Unknown'));
|
|
}
|
|
})
|
|
.catch(err => showToast('danger', 'Network error: ' + err))
|
|
.finally(() => { btn.disabled = false; btn.innerHTML = origLabel; });
|
|
}
|
|
|
|
function deleteDevice(deviceId, deviceName) {
|
|
if (!confirm(`Delete device "${deviceName}" and all its logs and WMT requests?\n\nThis cannot be undone.`)) return;
|
|
const btn = event.currentTarget;
|
|
btn.disabled = true;
|
|
const origLabel = btn.innerHTML;
|
|
btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span>';
|
|
|
|
fetch(`/admin/delete/device/${deviceId}`, {
|
|
method: 'POST',
|
|
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
|
})
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
// Remove row from table without page reload
|
|
const row = document.getElementById(`device-row-${deviceId}`);
|
|
if (row) row.remove();
|
|
showToast('success', `Device "${data.name}" deleted.`);
|
|
refreshStats();
|
|
} else {
|
|
showToast('danger', 'Error: ' + (data.error || 'Unknown'));
|
|
btn.disabled = false;
|
|
btn.innerHTML = origLabel;
|
|
}
|
|
})
|
|
.catch(err => {
|
|
showToast('danger', 'Network error: ' + err);
|
|
btn.disabled = false;
|
|
btn.innerHTML = origLabel;
|
|
});
|
|
}
|
|
|
|
function buildMessage(action, data) {
|
|
if (action === 'clear-logs' || action === 'clear-device-logs')
|
|
return `Deleted ${data.deleted} log entries.`;
|
|
if (action === 'clear-inventory')
|
|
return `Inventory reset. ${data.groups_deleted} group(s) removed.`;
|
|
if (action === 'clear-wmt')
|
|
return `Deleted ${data.deleted} WMT update request(s).`;
|
|
return 'Done.';
|
|
}
|
|
|
|
function showToast(type, msg) {
|
|
const toast = document.getElementById('resultToast');
|
|
toast.className = `toast align-items-center text-white bg-${type} border-0`;
|
|
document.getElementById('toastMsg').textContent = msg;
|
|
bootstrap.Toast.getOrCreateInstance(toast, {delay: 4000}).show();
|
|
}
|
|
|
|
function refreshStats() {
|
|
// Reload the page stats after a short delay to let DB settle
|
|
setTimeout(() => location.reload(), 800);
|
|
}
|
|
</script>
|
|
{% endblock %}
|