543 lines
25 KiB
HTML
543 lines
25 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 align-items-center gap-1">
|
|
<span id="lastSyncedLabel" class="text-muted me-1" style="font-size:.72rem;"></span>
|
|
<button class="btn btn-sm btn-success" onclick="syncDevices()" id="syncBtn"
|
|
title="Pull latest hostnames & IPs from the monitoring database">
|
|
<i class="fas fa-database me-1"></i>Sync IPs from DB
|
|
</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. Click <strong>Sync IPs from DB</strong> to import all active devices.</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 IPs from monitoring DB into inventory ── */
|
|
async function syncDevices() {
|
|
const btn = document.getElementById('syncBtn');
|
|
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) {
|
|
const now = new Date().toLocaleTimeString();
|
|
document.getElementById('lastSyncedLabel').textContent = `Last synced: ${now}`;
|
|
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-database me-1"></i>Sync IPs from DB';
|
|
}
|
|
}
|
|
|
|
/* ── 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 %}
|