Files
Server_Monitorizare_v2/templates/ansible/devices.html
2026-04-23 15:55:46 +03:00

539 lines
24 KiB
HTML

{% extends "base.html" %}
{% block title %}Ansible Inventory — Server Monitoring{% endblock %}
{% block page_title %}Ansible Inventory{% endblock %}
{% block content %}
<div class="container-fluid">
<div id="alertArea"></div>
<!-- ══ TOP ROW: Inventory list | Group management ══════════════ -->
<div class="row g-3 mb-4">
<!-- ── Panel 1: All hosts currently in inventory ──────────────── -->
<div class="col-lg-5">
<div class="card h-100">
<div class="card-header d-flex justify-content-between align-items-center py-2">
<span>
<i class="fas fa-network-wired text-primary me-2"></i>
<strong>Inventory Hosts</strong>
{% set ns = namespace(total=0) %}
{% for g in inventory.groups.values() %}{% set ns.total = ns.total + g.hosts|length %}{% endfor %}
<span class="badge bg-primary ms-2">{{ ns.total }}</span>
</span>
<div class="d-flex gap-1">
<button class="btn btn-sm btn-success" onclick="syncDevices()">
<i class="fas fa-sync-alt me-1"></i>Sync All
</button>
<button class="btn btn-sm btn-outline-secondary" onclick="toggleRaw()" title="Raw YAML">
<i class="fas fa-code"></i>
</button>
</div>
</div>
<!-- Raw YAML panel -->
<div id="rawPanel" class="d-none border-bottom">
<pre class="p-3 mb-0"
style="max-height:260px;overflow:auto;background:#1e1e1e;color:#d4d4d4;font-size:.78rem;">{{ inventory.raw_yaml | e }}</pre>
</div>
{% if inventory.groups %}
<div class="card-body p-0" style="max-height:480px;overflow-y:auto;">
{% for group_name, group in inventory.groups.items() %}
<!-- group label row -->
<div class="px-3 py-1 bg-light border-bottom d-flex align-items-center gap-2"
style="font-size:.75rem;">
<i class="fas fa-layer-group text-muted"></i>
<strong class="text-uppercase" style="letter-spacing:.04em;">{{ group_name }}</strong>
<span class="badge bg-light text-dark border">{{ group.hosts|length }}</span>
{% if group_name == 'monitoring_devices' %}
<span class="badge bg-info" style="font-size:.65rem;">default</span>
{% endif %}
</div>
<!-- host rows -->
{% for host in group.hosts %}
<div class="d-flex justify-content-between align-items-center px-3 py-2 border-bottom host-inv-row"
id="host-row-{{ group_name }}-{{ host.hostname }}"
data-hostname="{{ host.hostname }}"
data-group="{{ group_name }}">
<div>
<strong style="font-size:.88rem;">{{ host.hostname }}</strong>
<code class="ms-2 text-muted" style="font-size:.77rem;">{{ host.get('ansible_host','') }}</code>
</div>
<div class="d-flex align-items-center gap-2">
{% if host.get('ansible_connection') == 'local' %}
<span class="badge bg-secondary" style="font-size:.68rem;">local</span>
{% elif host.get('ansible_ssh_private_key_file') %}
<span class="badge bg-success" style="font-size:.68rem;">SSH key</span>
{% elif host.get('ansible_password') %}
<span class="badge bg-warning text-dark" style="font-size:.68rem;">password</span>
{% else %}
<span class="badge bg-light text-dark border" style="font-size:.68rem;"></span>
{% endif %}
<button class="btn btn-sm btn-outline-danger" style="padding:1px 6px;font-size:.72rem;"
onclick="removeHost('{{ group_name }}','{{ host.hostname }}')">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
{% else %}
<div class="px-3 py-2 text-muted border-bottom" style="font-size:.82rem;">
No hosts in this group.
<a href="#" onclick="openAddHostModal('{{ group_name }}'); return false;">Add one</a>
</div>
{% endfor %}
{% endfor %}
</div>
{% else %}
<div class="card-body text-center py-5 text-muted">
<i class="fas fa-box-open fa-3x mb-3"></i>
<p class="mb-0">Inventory is empty. Use <strong>Sync All</strong> or add from below.</p>
</div>
{% endif %}
</div>
</div>
<!-- ── Panel 2: Group management ──────────────────────────────── -->
<div class="col-lg-7">
<div class="card h-100">
<div class="card-header d-flex justify-content-between align-items-center py-2">
<span>
<i class="fas fa-layer-group text-success me-2"></i>
<strong>Groups</strong>
<span class="badge bg-secondary ms-2">{{ inventory.groups | length }}</span>
</span>
<button class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#createGroupModal">
<i class="fas fa-plus me-1"></i>New Group
</button>
</div>
{% if inventory.groups %}
<div class="card-body p-0">
<div class="accordion accordion-flush" id="groupAccordion">
{% for group_name, group in inventory.groups.items() %}
<div class="accordion-item" id="grp-panel-{{ group_name }}">
<h2 class="accordion-header">
<button class="accordion-button {% if not loop.first %}collapsed{% endif %} py-2"
type="button"
data-bs-toggle="collapse"
data-bs-target="#grp-body-{{ group_name }}">
<span class="me-2">
<i class="fas fa-layer-group me-2 text-primary"></i>
<strong>{{ group_name }}</strong>
</span>
<span class="badge bg-secondary me-2">{{ group.hosts|length }} host(s)</span>
{% if group_name == 'monitoring_devices' %}
<span class="badge bg-info" style="font-size:.68rem;">default</span>
{% endif %}
</button>
</h2>
<div id="grp-body-{{ group_name }}"
class="accordion-collapse collapse {% if loop.first %}show{% endif %}">
<div class="accordion-body p-2">
{% if group.hosts %}
<div class="table-responsive">
<table class="table table-sm table-hover mb-2">
<thead class="table-light">
<tr>
<th>Hostname</th>
<th>IP</th>
<th>Auth</th>
<th class="text-end">Remove</th>
</tr>
</thead>
<tbody>
{% for host in group.hosts %}
<tr id="grptbl-{{ group_name }}-{{ host.hostname }}">
<td><strong>{{ host.hostname }}</strong></td>
<td><code style="font-size:.8rem;">{{ host.get('ansible_host','—') }}</code></td>
<td>
{% if host.get('ansible_connection') == 'local' %}
<span class="badge bg-secondary" style="font-size:.68rem;">local</span>
{% elif host.get('ansible_ssh_private_key_file') %}
<span class="badge bg-success" style="font-size:.68rem;">key</span>
{% elif host.get('ansible_password') %}
<span class="badge bg-warning text-dark" style="font-size:.68rem;">pw</span>
{% else %}
<span class="badge bg-light text-dark border" style="font-size:.68rem;"></span>
{% endif %}
</td>
<td class="text-end">
<button class="btn btn-sm btn-outline-danger" style="padding:1px 6px;"
onclick="removeHost('{{ group_name }}','{{ host.hostname }}')">
<i class="fas fa-times"></i>
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-muted mb-2" style="font-size:.82rem;">No hosts in this group.</p>
{% endif %}
<div class="d-flex gap-2">
<button class="btn btn-sm btn-outline-primary"
onclick="openAddHostModal('{{ group_name }}')">
<i class="fas fa-plus me-1"></i>Add Host
</button>
{% if group_name != 'monitoring_devices' %}
<button class="btn btn-sm btn-outline-danger"
onclick="removeGroup('{{ group_name }}')">
<i class="fas fa-trash me-1"></i>Delete Group
</button>
{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
{% else %}
<div class="card-body text-center py-5 text-muted">
<i class="fas fa-folder-open fa-3x mb-3"></i>
<p class="mb-0">No groups yet. Click <strong>New Group</strong> to create one.</p>
</div>
{% endif %}
</div>
</div>
</div><!-- /top row -->
<!-- ══ BOTTOM: Available hosts from monitoring DB ════════════════ -->
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center py-2">
<span>
<i class="fas fa-desktop text-secondary me-2"></i>
<strong>Discovered Devices</strong>
<small class="text-muted ms-2">from monitoring database — not yet in inventory</small>
{% set avail = db_devices | selectattr('in_inventory','equalto',false) | list %}
<span class="badge bg-secondary ms-2">{{ avail | length }}</span>
</span>
</div>
{% set avail = db_devices | selectattr('in_inventory','equalto',false) | list %}
{% if avail %}
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-sm table-hover mb-0">
<thead class="table-light">
<tr>
<th>Hostname</th>
<th>IP</th>
<th>Type</th>
<th>Status</th>
<th class="text-end">Action</th>
</tr>
</thead>
<tbody>
{% for d in avail %}
<tr id="avail-row-{{ d.hostname }}">
<td><strong>{{ d.hostname }}</strong></td>
<td><code style="font-size:.8rem;">{{ d.device_ip }}</code></td>
<td>{{ d.device_type or '—' }}</td>
<td>
{% if d.status == 'active' %}
<span class="badge bg-success">Active</span>
{% elif d.status == 'inactive' %}
<span class="badge bg-warning text-dark">Inactive</span>
{% else %}
<span class="badge bg-secondary">{{ d.status }}</span>
{% endif %}
</td>
<td class="text-end">
<button class="btn btn-sm btn-outline-primary"
onclick="quickAdd('{{ d.hostname }}','{{ d.device_ip }}')">
<i class="fas fa-arrow-up me-1"></i>Add to Inventory
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% elif db_devices %}
<div class="card-body text-center py-4 text-muted">
<i class="fas fa-check-circle fa-2x mb-2 text-success"></i>
<p class="mb-0">All discovered devices are already in the inventory.</p>
</div>
{% else %}
<div class="card-body text-center py-4 text-muted">
<p class="mb-0">No devices discovered yet.</p>
</div>
{% endif %}
</div>
</div><!-- /container-fluid -->
<!-- ═══ Create Group Modal (with device picker) ══════════════════ -->
<div class="modal fade" id="createGroupModal" tabindex="-1" data-bs-backdrop="static">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="fas fa-layer-group me-2"></i>Create New Group</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Group Name <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="newGroupName"
placeholder="e.g. webservers, rpi_devices"
pattern="[a-zA-Z0-9_\-]+" title="Letters, numbers, underscores and hyphens only">
</div>
<label class="form-label">Select hosts to add to this group</label>
<div class="border rounded p-2" style="max-height:280px;overflow-y:auto;" id="groupDevicePicker">
{% set all_inv_hosts = [] %}
{% for g in inventory.groups.values() %}
{% for h in g.hosts %}{% if all_inv_hosts.append(h.hostname) %}{% endif %}{% endfor %}
{% endfor %}
{% if all_inv_hosts %}
<div class="mb-2">
<small class="text-muted fw-bold">Already in inventory</small>
{% for group_name, group in inventory.groups.items() %}
{% for host in group.hosts %}
<div class="form-check">
<input class="form-check-input" type="checkbox"
id="pick-inv-{{ host.hostname }}"
value="{{ host.hostname }}"
data-ip="{{ host.get('ansible_host','') }}"
name="groupHostPick">
<label class="form-check-label" for="pick-inv-{{ host.hostname }}">
{{ host.hostname }}
<code class="text-muted ms-1" style="font-size:.8rem;">{{ host.get('ansible_host','') }}</code>
<span class="badge bg-light text-dark border ms-1" style="font-size:.68rem;">{{ group_name }}</span>
</label>
</div>
{% endfor %}
{% endfor %}
</div>
{% endif %}
{% set avail2 = db_devices | selectattr('in_inventory','equalto',false) | list %}
{% if avail2 %}
<div>
<small class="text-muted fw-bold">Available (not yet in inventory)</small>
{% for d in avail2 %}
<div class="form-check">
<input class="form-check-input" type="checkbox"
id="pick-avail-{{ d.hostname }}"
value="{{ d.hostname }}"
data-ip="{{ d.device_ip }}"
name="groupHostPick">
<label class="form-check-label" for="pick-avail-{{ d.hostname }}">
{{ d.hostname }}
<code class="text-muted ms-1" style="font-size:.8rem;">{{ d.device_ip }}</code>
{% if d.status == 'active' %}<span class="badge bg-success ms-1" style="font-size:.65rem;">active</span>{% endif %}
</label>
</div>
{% endfor %}
</div>
{% endif %}
{% if not all_inv_hosts and not avail2 %}
<p class="text-muted mb-0 text-center py-3">No devices available.</p>
{% endif %}
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" onclick="createGroup()">Create Group</button>
</div>
</div>
</div>
</div>
<!-- ═══ Add Host to Group Modal ══════════════════════════════════ -->
<div class="modal fade" id="addHostModal" tabindex="-1" data-bs-backdrop="static">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-server me-2"></i>Add Host to:
<span id="addHostGroupLabel" class="text-primary ms-1"></span>
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="addHostGroup">
<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" id="addHostname" placeholder="e.g. rpi-desk-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" id="addHostIP" placeholder="e.g. 192.168.1.50">
</div>
<div class="col-md-4">
<label class="form-label">SSH User</label>
<input type="text" class="form-control" id="addHostUser" value="pi">
</div>
<div class="col-md-4">
<label class="form-label">SSH Port</label>
<input type="number" class="form-control" id="addHostPort" value="22" min="1" max="65535">
</div>
<div class="col-md-4">
<label class="form-label">Authentication</label>
<select class="form-select" id="addHostAuth" onchange="togglePwField(this.value)">
<option value="key">SSH Key (recommended)</option>
<option value="password">Password</option>
</select>
</div>
<div class="col-12 d-none" id="pwField">
<label class="form-label">Password</label>
<input type="password" class="form-control" id="addHostPassword" autocomplete="new-password">
<div class="form-text text-warning">
<i class="fas fa-exclamation-triangle me-1"></i>Stored in plain text in inventory file.
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" onclick="addHost()">Add Host</button>
</div>
</div>
</div>
</div>
<script>
const API = '/api/ansible';
function showAlert(msg, type='success') {
const d = document.createElement('div');
d.className = `alert alert-${type} alert-dismissible fade show`;
d.innerHTML = `${msg} <button type="button" class="btn-close" data-bs-dismiss="alert"></button>`;
document.getElementById('alertArea').appendChild(d);
setTimeout(() => { try { d.remove(); } catch {} }, 6000);
}
function toggleRaw() {
document.getElementById('rawPanel').classList.toggle('d-none');
}
function togglePwField(val) {
document.getElementById('pwField').classList.toggle('d-none', val !== 'password');
}
/* ── Sync all DB devices into monitoring_devices ── */
async function syncDevices() {
const btn = event.currentTarget;
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Syncing…';
try {
const r = await fetch(`${API}/inventory/sync`, {method:'POST'});
const d = await r.json();
if (d.success) {
showAlert(`<i class="fas fa-check-circle me-1"></i> ${d.message}`, 'success');
setTimeout(() => location.reload(), 1200);
} else { showAlert(`Sync failed: ${d.error}`, 'danger'); }
} catch { showAlert('Network error', 'danger'); }
finally {
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-sync-alt me-1"></i>Sync All';
}
}
/* ── Create group (with pre-selected hosts) ── */
async function createGroup() {
const name = document.getElementById('newGroupName').value.trim();
if (!name) { showAlert('Group name is required', 'warning'); return; }
// First create the group
let r = await fetch(`${API}/inventory/group/add`, {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({group_name: name})
});
let d = await r.json();
if (!d.success) { showAlert(d.error || 'Failed to create group', 'danger'); return; }
// Then add each checked host
const checks = document.querySelectorAll('input[name="groupHostPick"]:checked');
for (const cb of checks) {
const hostname = cb.value;
const ip = cb.dataset.ip;
await fetch(`${API}/inventory/host/add`, {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({group: name, hostname, ip, ssh_user:'pi', ssh_port:22, use_key:true})
});
}
bootstrap.Modal.getInstance(document.getElementById('createGroupModal')).hide();
showAlert(`<i class="fas fa-check-circle me-1"></i> Group "${name}" created with ${checks.length} host(s).`, 'success');
setTimeout(() => location.reload(), 1200);
}
/* ── Remove group ── */
async function removeGroup(groupName) {
if (!confirm(`Delete group "${groupName}" and remove all its hosts from the group?`)) return;
const r = await fetch(`${API}/inventory/group/remove`, {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({group_name: groupName})
});
const d = await r.json();
if (d.success) {
document.getElementById(`grp-panel-${groupName}`)?.remove();
showAlert(`<i class="fas fa-check-circle me-1"></i> ${d.message}`, 'success');
} else { showAlert(d.error || 'Failed', 'danger'); }
}
/* ── Open Add Host modal ── */
function openAddHostModal(groupName, hostname='', ip='') {
document.getElementById('addHostGroup').value = groupName;
document.getElementById('addHostGroupLabel').textContent = groupName;
document.getElementById('addHostname').value = hostname;
document.getElementById('addHostIP').value = ip;
document.getElementById('addHostUser').value = 'pi';
document.getElementById('addHostPort').value = 22;
document.getElementById('addHostAuth').value = 'key';
togglePwField('key');
new bootstrap.Modal(document.getElementById('addHostModal')).show();
}
/* ── Quick-add from discovered panel ── */
function quickAdd(hostname, ip) {
openAddHostModal('monitoring_devices', hostname, ip);
}
/* ── Add host ── */
async function addHost() {
const group = document.getElementById('addHostGroup').value;
const hostname = document.getElementById('addHostname').value.trim();
const ip = document.getElementById('addHostIP').value.trim();
const user = document.getElementById('addHostUser').value.trim() || 'pi';
const port = parseInt(document.getElementById('addHostPort').value) || 22;
const authType = document.getElementById('addHostAuth').value;
const password = document.getElementById('addHostPassword').value;
if (!hostname || !ip) { showAlert('Hostname and IP are required', 'warning'); return; }
const r = await fetch(`${API}/inventory/host/add`, {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({group, hostname, ip, ssh_user:user, ssh_port:port,
use_key: authType==='key',
password: authType==='password' ? password : null})
});
const d = await r.json();
if (d.success) {
bootstrap.Modal.getInstance(document.getElementById('addHostModal')).hide();
showAlert(`<i class="fas fa-check-circle me-1"></i> ${d.message}`, 'success');
setTimeout(() => location.reload(), 1000);
} else { showAlert(d.error || 'Failed to add host', 'danger'); }
}
/* ── Remove host ── */
async function removeHost(group, hostname) {
if (!confirm(`Remove "${hostname}" from group "${group}"?`)) return;
const r = await fetch(`${API}/inventory/host/remove`, {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({group, hostname})
});
const d = await r.json();
if (d.success) {
document.getElementById(`host-row-${group}-${hostname}`)?.remove();
document.getElementById(`grptbl-${group}-${hostname}`)?.remove();
showAlert(`<i class="fas fa-check-circle me-1"></i> ${d.message}`, 'success');
} else { showAlert(d.error || 'Failed', 'danger'); }
}
</script>
{% endblock %}