294 lines
11 KiB
HTML
294 lines
11 KiB
HTML
|
|
{% extends "shared/frontend/base.html" %}
|
||
|
|
|
||
|
|
{% block title %}ESET Import - Hardware - BMC Hub{% endblock %}
|
||
|
|
|
||
|
|
{% block extra_css %}
|
||
|
|
<style>
|
||
|
|
.page-header {
|
||
|
|
margin-bottom: 2rem;
|
||
|
|
display: flex;
|
||
|
|
justify-content: space-between;
|
||
|
|
align-items: center;
|
||
|
|
gap: 1rem;
|
||
|
|
flex-wrap: wrap;
|
||
|
|
}
|
||
|
|
|
||
|
|
.section-card {
|
||
|
|
background: var(--bg-card);
|
||
|
|
border-radius: 12px;
|
||
|
|
border: 1px solid rgba(0,0,0,0.1);
|
||
|
|
padding: 1.5rem;
|
||
|
|
margin-bottom: 2rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.table thead th {
|
||
|
|
font-size: 0.85rem;
|
||
|
|
text-transform: uppercase;
|
||
|
|
letter-spacing: 0.02em;
|
||
|
|
color: var(--text-secondary);
|
||
|
|
}
|
||
|
|
|
||
|
|
.device-uuid {
|
||
|
|
max-width: 240px;
|
||
|
|
word-break: break-all;
|
||
|
|
}
|
||
|
|
|
||
|
|
.status-pill {
|
||
|
|
display: inline-flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 0.35rem;
|
||
|
|
font-size: 0.85rem;
|
||
|
|
padding: 0.35rem 0.6rem;
|
||
|
|
border-radius: 999px;
|
||
|
|
background: var(--accent-light);
|
||
|
|
color: var(--accent);
|
||
|
|
}
|
||
|
|
|
||
|
|
.contact-results {
|
||
|
|
max-height: 220px;
|
||
|
|
overflow: auto;
|
||
|
|
border: 1px solid rgba(0,0,0,0.1);
|
||
|
|
border-radius: 8px;
|
||
|
|
background: var(--bg-body);
|
||
|
|
}
|
||
|
|
|
||
|
|
.contact-result {
|
||
|
|
padding: 0.6rem 0.8rem;
|
||
|
|
cursor: pointer;
|
||
|
|
display: flex;
|
||
|
|
justify-content: space-between;
|
||
|
|
gap: 1rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.contact-result:hover {
|
||
|
|
background: var(--accent-light);
|
||
|
|
}
|
||
|
|
|
||
|
|
.contact-muted {
|
||
|
|
color: var(--text-secondary);
|
||
|
|
font-size: 0.85rem;
|
||
|
|
}
|
||
|
|
</style>
|
||
|
|
{% endblock %}
|
||
|
|
|
||
|
|
{% block content %}
|
||
|
|
<div class="container-fluid" style="margin-top: 2rem; max-width: 1400px;">
|
||
|
|
<div class="page-header">
|
||
|
|
<h1>⬇️ ESET Import</h1>
|
||
|
|
<div class="d-flex gap-2">
|
||
|
|
<a href="/hardware/eset" class="btn btn-outline-secondary">ESET Oversigt</a>
|
||
|
|
<a href="/hardware" class="btn btn-outline-secondary">Tilbage til hardware</a>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="section-card">
|
||
|
|
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-3">
|
||
|
|
<div class="status-pill" id="deviceStatus">Ingen data indlaest</div>
|
||
|
|
<button class="btn btn-primary" onclick="loadDevices()">Hent devices</button>
|
||
|
|
</div>
|
||
|
|
<div class="table-responsive">
|
||
|
|
<table class="table table-hover align-middle">
|
||
|
|
<thead>
|
||
|
|
<tr>
|
||
|
|
<th>Navn</th>
|
||
|
|
<th>Serial</th>
|
||
|
|
<th>Gruppe</th>
|
||
|
|
<th>Device UUID</th>
|
||
|
|
<th>Handling</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody id="devicesTable">
|
||
|
|
<tr>
|
||
|
|
<td colspan="5" class="text-center text-muted">Klik "Hent devices" for at hente ESET-listen.</td>
|
||
|
|
</tr>
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Import Modal -->
|
||
|
|
<div class="modal fade" id="esetImportModal" tabindex="-1" aria-hidden="true">
|
||
|
|
<div class="modal-dialog modal-lg modal-dialog-centered">
|
||
|
|
<div class="modal-content">
|
||
|
|
<div class="modal-header">
|
||
|
|
<h5 class="modal-title">Import fra ESET</h5>
|
||
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||
|
|
</div>
|
||
|
|
<div class="modal-body">
|
||
|
|
<div class="mb-3">
|
||
|
|
<label class="form-label">Device UUID</label>
|
||
|
|
<input type="text" class="form-control" id="importDeviceUuid" readonly>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="mb-3">
|
||
|
|
<label class="form-label">Find kontakt</label>
|
||
|
|
<div class="input-group mb-2">
|
||
|
|
<input type="text" class="form-control" id="contactSearch" placeholder="Navn eller email">
|
||
|
|
<button class="btn btn-outline-secondary" type="button" onclick="searchContacts()">Sog</button>
|
||
|
|
</div>
|
||
|
|
<div id="contactResults" class="contact-results"></div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="mb-3">
|
||
|
|
<label class="form-label">Valgt kontakt</label>
|
||
|
|
<input type="text" class="form-control" id="selectedContact" placeholder="Ingen valgt" readonly>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div id="importStatus" class="contact-muted"></div>
|
||
|
|
</div>
|
||
|
|
<div class="modal-footer">
|
||
|
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Annuller</button>
|
||
|
|
<button type="button" class="btn btn-primary" onclick="importDevice()">Importer</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
{% endblock %}
|
||
|
|
|
||
|
|
{% block extra_js %}
|
||
|
|
<script>
|
||
|
|
const devicesTable = document.getElementById('devicesTable');
|
||
|
|
const deviceStatus = document.getElementById('deviceStatus');
|
||
|
|
const importModal = new bootstrap.Modal(document.getElementById('esetImportModal'));
|
||
|
|
let selectedContactId = null;
|
||
|
|
|
||
|
|
function parseDevices(payload) {
|
||
|
|
if (Array.isArray(payload)) return payload;
|
||
|
|
if (!payload || typeof payload !== 'object') return [];
|
||
|
|
return payload.devices || payload.items || payload.results || payload.data || [];
|
||
|
|
}
|
||
|
|
|
||
|
|
function getField(device, keys) {
|
||
|
|
for (const key of keys) {
|
||
|
|
if (device[key]) return device[key];
|
||
|
|
}
|
||
|
|
return '';
|
||
|
|
}
|
||
|
|
|
||
|
|
function renderDevices(devices) {
|
||
|
|
if (!devices.length) {
|
||
|
|
devicesTable.innerHTML = '<tr><td colspan="5" class="text-center text-muted">Ingen devices fundet.</td></tr>';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
devicesTable.innerHTML = devices.map(device => {
|
||
|
|
const uuid = getField(device, ['deviceUuid', 'uuid', 'id']);
|
||
|
|
const name = getField(device, ['displayName', 'deviceName', 'name']);
|
||
|
|
const serial = getField(device, ['serialNumber', 'serial', 'serial_number']);
|
||
|
|
const group = getField(device, ['parentGroup', 'groupPath', 'group', 'path']);
|
||
|
|
|
||
|
|
return `
|
||
|
|
<tr>
|
||
|
|
<td>${name || '-'}</td>
|
||
|
|
<td>${serial || '-'}</td>
|
||
|
|
<td>${group || '-'}</td>
|
||
|
|
<td class="device-uuid">${uuid || '-'}</td>
|
||
|
|
<td>
|
||
|
|
<button class="btn btn-sm btn-outline-primary" onclick="openImportModal('${uuid || ''}')">Importer</button>
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
`;
|
||
|
|
}).join('');
|
||
|
|
}
|
||
|
|
|
||
|
|
async function loadDevices() {
|
||
|
|
deviceStatus.textContent = 'Henter...';
|
||
|
|
try {
|
||
|
|
const response = await fetch('/api/v1/hardware/eset/devices');
|
||
|
|
if (!response.ok) {
|
||
|
|
const err = await response.text();
|
||
|
|
throw new Error(err || 'Request failed');
|
||
|
|
}
|
||
|
|
const data = await response.json();
|
||
|
|
const devices = parseDevices(data);
|
||
|
|
deviceStatus.textContent = `${devices.length} devices hentet`;
|
||
|
|
renderDevices(devices);
|
||
|
|
} catch (err) {
|
||
|
|
deviceStatus.textContent = 'Fejl ved hentning';
|
||
|
|
devicesTable.innerHTML = `<tr><td colspan="5" class="text-center text-danger">${err.message}</td></tr>`;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function openImportModal(uuid) {
|
||
|
|
document.getElementById('importDeviceUuid').value = uuid;
|
||
|
|
document.getElementById('contactSearch').value = '';
|
||
|
|
document.getElementById('contactResults').innerHTML = '';
|
||
|
|
document.getElementById('selectedContact').value = '';
|
||
|
|
document.getElementById('importStatus').textContent = '';
|
||
|
|
selectedContactId = null;
|
||
|
|
importModal.show();
|
||
|
|
}
|
||
|
|
|
||
|
|
async function searchContacts() {
|
||
|
|
const query = document.getElementById('contactSearch').value.trim();
|
||
|
|
const results = document.getElementById('contactResults');
|
||
|
|
if (!query) {
|
||
|
|
results.innerHTML = '<div class="p-2 text-muted">Indtast soegning.</div>';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
results.innerHTML = '<div class="p-2 text-muted">Soeger...</div>';
|
||
|
|
try {
|
||
|
|
const response = await fetch(`/api/v1/contacts?search=${encodeURIComponent(query)}&limit=20`);
|
||
|
|
if (!response.ok) {
|
||
|
|
const err = await response.text();
|
||
|
|
throw new Error(err || 'Request failed');
|
||
|
|
}
|
||
|
|
const data = await response.json();
|
||
|
|
const contacts = data.contacts || [];
|
||
|
|
if (!contacts.length) {
|
||
|
|
results.innerHTML = '<div class="p-2 text-muted">Ingen kontakter fundet.</div>';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
results.innerHTML = contacts.map(c => {
|
||
|
|
const name = `${c.first_name || ''} ${c.last_name || ''}`.trim();
|
||
|
|
const company = (c.company_names || []).join(', ');
|
||
|
|
return `
|
||
|
|
<div class="contact-result" onclick="selectContact(${c.id}, '${name.replace(/'/g, "\\'")}', '${company.replace(/'/g, "\\'")}')">
|
||
|
|
<div>
|
||
|
|
<div>${name || 'Ukendt'}</div>
|
||
|
|
<div class="contact-muted">${c.email || ''}</div>
|
||
|
|
</div>
|
||
|
|
<div class="contact-muted">${company || '-'}</div>
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
}).join('');
|
||
|
|
} catch (err) {
|
||
|
|
results.innerHTML = `<div class="p-2 text-danger">${err.message}</div>`;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function selectContact(id, name, company) {
|
||
|
|
selectedContactId = id;
|
||
|
|
const label = company ? `${name} (${company})` : name;
|
||
|
|
document.getElementById('selectedContact').value = label;
|
||
|
|
document.getElementById('contactResults').innerHTML = '';
|
||
|
|
}
|
||
|
|
|
||
|
|
async function importDevice() {
|
||
|
|
const uuid = document.getElementById('importDeviceUuid').value.trim();
|
||
|
|
const statusEl = document.getElementById('importStatus');
|
||
|
|
statusEl.textContent = 'Importer...';
|
||
|
|
try {
|
||
|
|
const payload = { device_uuid: uuid };
|
||
|
|
if (selectedContactId) {
|
||
|
|
payload.contact_id = selectedContactId;
|
||
|
|
}
|
||
|
|
const response = await fetch('/api/v1/hardware/eset/import', {
|
||
|
|
method: 'POST',
|
||
|
|
headers: { 'Content-Type': 'application/json' },
|
||
|
|
body: JSON.stringify(payload)
|
||
|
|
});
|
||
|
|
if (!response.ok) {
|
||
|
|
const err = await response.text();
|
||
|
|
throw new Error(err || 'Request failed');
|
||
|
|
}
|
||
|
|
const data = await response.json();
|
||
|
|
statusEl.textContent = `Importeret hardware #${data.id}`;
|
||
|
|
} catch (err) {
|
||
|
|
statusEl.textContent = `Fejl: ${err.message}`;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
</script>
|
||
|
|
{% endblock %}
|