bmc_hub/app/modules/hardware/templates/eset_import.html

637 lines
24 KiB
HTML
Raw Normal View History

{% 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;
}
.contact-result.active,
.contact-result:focus {
outline: none;
background: var(--accent-light);
}
.contact-chip {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.35rem 0.6rem;
border-radius: 999px;
background: var(--accent-light);
color: var(--accent);
font-size: 0.85rem;
margin-top: 0.5rem;
}
.devices-table {
display: block;
}
.devices-cards {
display: none;
}
.show-cards .devices-table {
display: none;
}
.show-cards .devices-cards {
display: block;
}
.device-card {
border: 1px solid rgba(0,0,0,0.1);
border-radius: 12px;
padding: 1rem;
background: var(--bg-body);
margin-bottom: 1rem;
}
.device-card-title {
font-weight: 600;
margin-bottom: 0.25rem;
}
.device-card-meta {
color: var(--text-secondary);
font-size: 0.85rem;
}
.inline-results {
max-height: 180px;
overflow: auto;
border: 1px solid rgba(0,0,0,0.1);
border-radius: 8px;
background: var(--bg-body);
margin-top: 0.35rem;
}
.inline-result {
padding: 0.5rem 0.7rem;
cursor: pointer;
display: flex;
justify-content: space-between;
gap: 0.75rem;
}
.inline-result:hover,
.inline-result.active {
background: var(--accent-light);
}
@media (max-width: 991px) {
.devices-table {
display: none;
}
.devices-cards {
display: block;
}
}
</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" id="devicesSection">
<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>
<div class="d-flex gap-2">
<button class="btn btn-outline-secondary" id="tabletToggle" onclick="toggleTabletView()">Tablet visning</button>
<button class="btn btn-primary" onclick="loadDevices()">Hent devices</button>
</div>
</div>
<div class="table-responsive devices-table">
<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 id="devicesCards" class="devices-cards"></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" autocomplete="off">
<button class="btn btn-outline-secondary" type="button" onclick="searchContacts()">Sog</button>
</div>
<div id="contactResults" class="contact-results"></div>
<div id="contactHint" class="contact-muted">Tip: Skriv 2+ tegn og tryk Enter for at vaelge den forste.</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 id="selectedContactChip" class="contact-chip d-none"></div>
</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 devicesCards = document.getElementById('devicesCards');
const devicesSection = document.getElementById('devicesSection');
const tabletToggle = document.getElementById('tabletToggle');
const importModal = new bootstrap.Modal(document.getElementById('esetImportModal'));
let selectedContactId = null;
let contactResults = [];
let contactSearchTimer = null;
const inlineSelections = {};
const inlineTimers = {};
let isTabletView = false;
function toggleTabletView() {
isTabletView = !isTabletView;
if (devicesSection) {
devicesSection.classList.toggle('show-cards', isTabletView);
}
if (tabletToggle) {
tabletToggle.textContent = isTabletView ? 'Tabel visning' : 'Tablet visning';
}
}
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 getNextPageToken(payload) {
if (!payload || typeof payload !== 'object') return null;
return payload.nextPageToken || payload.next_page_token || payload.nextPage || null;
}
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>';
if (devicesCards) {
devicesCards.innerHTML = '<div class="text-center text-muted">Ingen devices fundet.</div>';
}
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('');
if (devicesCards) {
devicesCards.innerHTML = devices.map((device, index) => {
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']);
const safeName = name || '-';
const safeSerial = serial || '-';
const safeGroup = group || '-';
const safeUuid = uuid || '';
return `
<div class="device-card" data-index="${index}" data-uuid="${safeUuid}">
<div class="device-card-title">${safeName}</div>
<div class="device-card-meta">Serial: ${safeSerial}</div>
<div class="device-card-meta">Gruppe: ${safeGroup}</div>
<div class="device-card-meta">UUID: ${safeUuid || '-'}</div>
<div class="mt-3">
<label class="form-label small">Vaelg kontakt (ejer)</label>
<input type="text" class="form-control form-control-sm inline-contact-search" data-index="${index}" placeholder="Sog kontakt..." autocomplete="off">
<div id="inlineResults-${index}" class="inline-results"></div>
<div id="inlineSelected-${index}" class="contact-muted">Ingen valgt</div>
<div id="inlineStatus-${index}" class="contact-muted"></div>
<div class="d-flex gap-2 mt-2">
<button class="btn btn-sm btn-primary" onclick="importDeviceInline(${index})">Importer</button>
<button class="btn btn-sm btn-outline-secondary" onclick="openImportModal('${safeUuid || ''}')">Detaljer</button>
</div>
</div>
</div>
`;
}).join('');
bindInlineSearch();
}
}
function bindInlineSearch() {
document.querySelectorAll('.inline-contact-search').forEach(input => {
input.addEventListener('input', (event) => {
const index = event.target.dataset.index;
const query = event.target.value.trim();
if (inlineTimers[index]) {
clearTimeout(inlineTimers[index]);
}
if (query.length < 2) {
clearInlineResults(index);
return;
}
inlineTimers[index] = setTimeout(() => {
searchInlineContacts(query, index);
}, 250);
});
});
}
function clearInlineResults(index) {
const results = document.getElementById(`inlineResults-${index}`);
if (results) results.innerHTML = '';
}
async function searchInlineContacts(query, index) {
const results = document.getElementById(`inlineResults-${index}`);
if (!results) return;
results.innerHTML = '<div class="p-2 text-muted">Soeger...</div>';
try {
const response = await fetch(`/api/v1/contacts?search=${encodeURIComponent(query)}&limit=10`);
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, idx) => {
const name = `${c.first_name || ''} ${c.last_name || ''}`.trim();
const company = (c.company_names || []).join(', ');
return `
<div class="inline-result" onclick="selectInlineContact(${index}, ${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 selectInlineContact(index, id, name, company) {
inlineSelections[index] = { id, label: company ? `${name} (${company})` : name };
const selectedEl = document.getElementById(`inlineSelected-${index}`);
if (selectedEl) selectedEl.textContent = inlineSelections[index].label || 'Ingen valgt';
clearInlineResults(index);
}
async function importDeviceInline(index) {
const card = document.querySelector(`.device-card[data-index="${index}"]`);
const statusEl = document.getElementById(`inlineStatus-${index}`);
if (!card || !statusEl) return;
const uuid = card.dataset.uuid || '';
if (!uuid) {
statusEl.textContent = 'Manglende UUID';
return;
}
statusEl.textContent = 'Importer...';
try {
const payload = { device_uuid: uuid };
if (inlineSelections[index]?.id) {
payload.contact_id = inlineSelections[index].id;
}
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}`;
}
}
async function loadDevices() {
deviceStatus.textContent = 'Henter...';
const allDevices = [];
let pageToken = null;
let pageIndex = 0;
const pageSize = 200;
const maxPages = 20;
try {
while (pageIndex < maxPages) {
const params = new URLSearchParams();
params.set('page_size', String(pageSize));
if (pageToken) {
params.set('page_token', pageToken);
}
deviceStatus.textContent = `Henter side ${pageIndex + 1}...`;
const response = await fetch(`/api/v1/hardware/eset/devices?${params.toString()}`);
if (!response.ok) {
const err = await response.text();
throw new Error(err || 'Request failed');
}
const data = await response.json();
const devices = parseDevices(data);
allDevices.push(...devices);
pageToken = getNextPageToken(data);
if (!pageToken || !devices.length) {
break;
}
pageIndex += 1;
}
if (pageIndex >= maxPages) {
deviceStatus.textContent = `${allDevices.length} devices hentet (afkortet)`;
} else {
deviceStatus.textContent = `${allDevices.length} devices hentet`;
}
renderDevices(allDevices);
} 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('selectedContactChip').classList.add('d-none');
document.getElementById('selectedContactChip').textContent = '';
document.getElementById('importStatus').textContent = '';
selectedContactId = null;
contactResults = [];
importModal.show();
setTimeout(() => {
const input = document.getElementById('contactSearch');
if (input) input.focus();
}, 150);
}
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 || [];
contactResults = 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" tabindex="0" 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('');
highlightFirstContact();
} 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;
const chip = document.getElementById('selectedContactChip');
chip.textContent = `Ejer: ${label}`;
chip.classList.remove('d-none');
document.getElementById('contactResults').innerHTML = '';
}
function highlightFirstContact() {
const first = document.querySelector('#contactResults .contact-result');
if (!first) return;
document.querySelectorAll('#contactResults .contact-result').forEach(el => el.classList.remove('active'));
first.classList.add('active');
}
function selectHighlightedContact() {
const active = document.querySelector('#contactResults .contact-result.active');
if (active) {
active.click();
} else if (contactResults.length > 0) {
const c = contactResults[0];
const name = `${c.first_name || ''} ${c.last_name || ''}`.trim();
const company = (c.company_names || []).join(', ');
selectContact(c.id, name, company);
}
}
function moveHighlight(step) {
const items = Array.from(document.querySelectorAll('#contactResults .contact-result'));
if (!items.length) return;
let index = items.findIndex(el => el.classList.contains('active'));
if (index === -1) index = 0;
index = (index + step + items.length) % items.length;
items.forEach(el => el.classList.remove('active'));
items[index].classList.add('active');
items[index].scrollIntoView({ block: 'nearest' });
}
document.getElementById('contactSearch').addEventListener('input', () => {
if (contactSearchTimer) {
clearTimeout(contactSearchTimer);
}
contactSearchTimer = setTimeout(() => {
const query = document.getElementById('contactSearch').value.trim();
if (query.length >= 2) {
searchContacts();
}
}, 250);
});
document.getElementById('contactSearch').addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
event.preventDefault();
selectHighlightedContact();
} else if (event.key === 'ArrowDown') {
event.preventDefault();
moveHighlight(1);
} else if (event.key === 'ArrowUp') {
event.preventDefault();
moveHighlight(-1);
}
});
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 %}