added new device consolidation

This commit is contained in:
2025-08-19 19:19:08 +02:00
parent ba34e2ad56
commit 7dd0733d6d
6 changed files with 802 additions and 2 deletions
@@ -0,0 +1,346 @@
Vue.component('device-zabbix-consolidation', {
template: `
<tt-card>
<div v-if="loading" class="text-center p-5">
<div class="spinner-border" role="status"></div>
<p class="mt-2">Lade und vergleiche Daten von TheTool und Zabbix...</p>
</div>
<div v-else class="consolidation-container">
<template v-for="(section, key) in sections">
<div class="consolidation-section card" :key="key">
<div :class="[section.headerClass, {'collapsed': !section.isOpen}]" class="card-header" @click="toggleSection(key)">
<span>{{ section.title }} ({{ results[key] ? results[key].length : 0 }})</span>
<i class="fas fa-chevron-down"></i>
</div>
<div v-show="section.isOpen" class="card-body">
<ul class="list-group list-group-flush">
<li v-if="!results[key] || !results[key].length" class="list-group-item">Keine Abweichungen gefunden.</li>
<li v-if="key === 'inZabbixOnly'" v-for="host in results.inZabbixOnly" :key="host.hostid" class="list-group-item">
<div class="device-info">
<strong>{{ host.name }}</strong> ({{ host.host }})
<a :href="zabbixUrl + '/zabbix.php?action=latest.view&hostids%5B%5D=' + host.hostid" class="zabbix-link" target="_blank" title="In Zabbix anzeigen"><i class="fas fa-external-link-alt"></i></a>
</div>
<div class="actions"><tt-button sm text="Gerät anlegen" icon="fas fa-plus" additional-class="btn-primary" @click="createDeviceFromZabbix(host)"/></div>
</li>
<li v-if="key === 'inTheToolOnly'" v-for="item in results.inTheToolOnly" :key="item.id" class="list-group-item">
<div class="device-info">
<strong>{{ item.data.name }}</strong> ({{ item.data.ip }})
<a :href="window.TT_CONFIG.BASE_URL + '/Device/Detail?id=' + item.id" class="thetool-link" target="_blank" title="In TheTool anzeigen"><i class="fas fa-eye"></i></a>
</div>
<div class="actions">
<tt-button sm text="In Zabbix anlegen" icon="fas fa-server" additional-class="btn-success" @click="openCreateZabbixHostModal(item.id)" :loading="item.id === zabbixCreateModal.loadingId"/>
</div>
</li>
<li v-if="key === 'mismatchedNames'" v-for="item in results.mismatchedNames" :key="item.zabbix.hostid" class="list-group-item">
<div class="device-info">
<div><strong>IP:</strong> {{ item.zabbix.host }}</div>
<div>
<strong>TheTool Name:</strong> {{ item.theTool.data.name }}
<a :href="window.TT_CONFIG.BASE_URL + '/Device/Detail?id=' + item.theTool.id" class="thetool-link" target="_blank" title="In TheTool anzeigen"><i class="fas fa-eye"></i></a>
</div>
<div>
<strong>Zabbix Name:</strong> {{ item.zabbix.name }}
<a :href="zabbixUrl + '/zabbix.php?action=latest.view&hostids%5B%5D=' + item.zabbix.hostid" class="zabbix-link" target="_blank" title="In Zabbix anzeigen"><i class="fas fa-external-link-alt"></i></a>
</div>
</div>
</li>
<li v-if="key === 'noSnmp'" v-for="host in results.noSnmp" :key="host.hostid" class="list-group-item">
<div class="device-info">
<strong>{{ host.name }}</strong> ({{ host.host }})
<a :href="zabbixUrl + '/zabbix.php?action=latest.view&hostids%5B%5D=' + host.hostid" class="zabbix-link" target="_blank" title="In Zabbix anzeigen"><i class="fas fa-external-link-alt"></i></a>
</div>
</li>
<li v-if="key === 'mismatchedCoords'" v-for="item in results.mismatchedCoords" :key="item.zabbix.hostid" class="list-group-item">
<div class="device-info">
<div>
<strong>{{ item.theTool.data.name }}</strong>
<a :href="window.TT_CONFIG.BASE_URL + '/Device/Detail?id=' + item.theTool.id" class="thetool-link" target="_blank" title="In TheTool anzeigen"><i class="fas fa-eye"></i></a>
<a :href="zabbixUrl + '/zabbix.php?action=latest.view&hostids%5B%5D=' + item.zabbix.hostid" class="zabbix-link" target="_blank" title="In Zabbix anzeigen"><i class="fas fa-external-link-alt"></i></a>
</div>
<div><small><strong>TheTool:</strong> <span>Lat: {{ item.theTool.data.pop ? item.theTool.data.pop.gps_lat : item.theTool.data.gps_lat || 'N/A' }}, Lon: {{ item.theTool.data.pop ? item.theTool.data.pop.gps_long : item.theTool.data.gps_long || 'N/A' }}</span></small></div>
<div><small><strong>Zabbix:</strong> <span>Lat: {{ item.zabbix.inventory.location_lat || 'N/A' }}, Lon: {{ item.zabbix.inventory.location_lon || 'N/A' }}</span></small></div>
</div>
<div class="actions"><tt-button sm text="Koordinaten setzen" icon="fas fa-map-marker-alt" additional-class="btn-warning" @click="openMapModal(item.zabbix, item.theTool)"/></div>
</li>
<li v-if="key === 'noCoordsInZabbix'" v-for="item in results.noCoordsInZabbix" :key="item.zabbix.hostid" class="list-group-item">
<div class="device-info">
<strong>{{ item.zabbix.name }}</strong> ({{ item.zabbix.host }})
<span v-if="item.theTool" class="text-success ml-2">(Gerät in TheTool gefunden)</span>
<a v-if="item.theTool" :href="window.TT_CONFIG.BASE_URL + '/Device/Detail?id=' + item.theTool.id" class="thetool-link" target="_blank" title="In TheTool anzeigen"><i class="fas fa-eye"></i></a>
<a :href="zabbixUrl + '/zabbix.php?action=latest.view&hostids%5B%5D=' + item.zabbix.hostid" class="zabbix-link" target="_blank" title="In Zabbix anzeigen"><i class="fas fa-external-link-alt"></i></a>
<div v-if="item.theTool && item.theTool.data.pop_id && item.theTool.data.pop && item.theTool.data.pop.gps_lat" class="text-danger small mt-1">
Koordinaten vom POP-Standort geerbt. Bitte im POP-Modul bearbeiten.
</div>
</div>
<div class="actions" v-if="!(item.theTool && item.theTool.data.pop_id && item.theTool.data.pop && item.theTool.data.pop.gps_lat)">
<tt-button sm text="Koordinaten setzen" icon="fas fa-map-marker-alt" additional-class="btn-warning" @click="openMapModal(item.zabbix, item.theTool)"/>
</div>
</li>
</ul>
</div>
</div>
</template>
<tt-modal v-if="mapModal.show" id="map-modal" :show.sync="mapModal.show" :title="'Koordinaten für ' + mapModal.host.name" @submit="saveCoordinates" :save-loading="mapModal.loading" :delete="false">
<div id="leaflet-map" ref="leafletMap"></div>
<div class="coords-display">Lat: {{ mapModal.markerCoords ? mapModal.markerCoords.lat.toFixed(6) : 'N/A' }}, Lng: {{ mapModal.markerCoords ? mapModal.markerCoords.lng.toFixed(6) : 'N/A' }}</div>
<div v-if="mapModal.theToolDevice" style="padding: 1rem 1rem 0 1rem;">
<tt-checkbox label="Koordinaten in TheTool überschreiben" v-model="mapModal.updateTheToolCoords" :disabled="mapModal.deviceHasPop" :hint="mapModal.deviceHasPop ? 'Koordinaten werden vom POP-Standort geerbt.' : ''" sm/>
</div>
</tt-modal>
<tt-modal v-if="zabbixCreateModal.show" :show.sync="zabbixCreateModal.show" title="Gerät in Zabbix anlegen" @submit="submitCreateZabbixHost" :save-loading="zabbixCreateModal.loading">
<p>Bitte wählen Sie ein Template für das Gerät <strong>{{ zabbixCreateModal.deviceName }}</strong>.</p>
<tt-select label="Template" :options="zabbixCreateModal.templates" v-model="zabbixCreateModal.selectedTemplateId" sm row required/>
</tt-modal>
</div>
</tt-card>
`,
data() {
return {
loading: true,
window: window,
zabbixUrl: 'https://monitoring.xinon.at',
results: {},
sections: {
inZabbixOnly: { title: 'Geräte in Zabbix, aber nicht in TheTool', isOpen: false, headerClass: 'bg-warning' },
inTheToolOnly: { title: 'Geräte in TheTool, aber nicht in Zabbix', isOpen: false, headerClass: 'bg-info' },
mismatchedNames: { title: 'Abweichende Namen (IP-basiert)', isOpen: false, headerClass: 'bg-secondary text-white' },
noSnmp: { title: 'Geräte in Zabbix ohne SNMP Template', isOpen: false, headerClass: 'bg-light' },
mismatchedCoords: { title: 'Abweichende Koordinaten', isOpen: false, headerClass: 'bg-danger text-white' },
noCoordsInZabbix: { title: 'Geräte in Zabbix ohne Koordinaten', isOpen: false, headerClass: 'bg-purple text-white' }
},
mapModal: {
show: false,
loading: false,
host: null,
theToolDevice: null,
map: null,
marker: null,
markerCoords: null,
updateTheToolCoords: false,
deviceHasPop: false
},
zabbixCreateModal: {
show: false,
loading: false,
loadingId: null,
deviceId: null,
deviceName: '',
templates: [],
selectedTemplateId: null
}
};
},
async mounted() {
await this.refreshData();
},
methods: {
toggleSection(key) {
this.sections[key].isOpen = !this.sections[key].isOpen;
},
createDeviceFromZabbix(host) {
const params = new URLSearchParams();
params.append('name', host.name);
params.append('ip', host.host);
window.open(`${window.TT_CONFIG.BASE_URL}/Device/add?${params.toString()}`, '_blank');
},
async openCreateZabbixHostModal(deviceId) {
const device = this.results.inTheToolOnly.find(d => d.id === deviceId);
if (!device) return;
this.zabbixCreateModal.deviceId = deviceId;
this.zabbixCreateModal.deviceName = device.data.name;
this.zabbixCreateModal.loading = true;
this.zabbixCreateModal.show = true;
try {
const response = await axios.get(`${window.TT_CONFIG.API_URL}?do=getZabbixTemplates`);
this.zabbixCreateModal.templates = response.data;
} catch (e) {
window.notify('error', 'Fehler beim Laden der Zabbix Templates.');
this.zabbixCreateModal.show = false;
} finally {
this.zabbixCreateModal.loading = false;
}
},
async submitCreateZabbixHost() {
if (!this.zabbixCreateModal.selectedTemplateId) {
return window.notify('error', 'Bitte wählen Sie ein Template aus.');
}
this.zabbixCreateModal.loading = true;
this.zabbixCreateModal.loadingId = this.zabbixCreateModal.deviceId;
try {
const response = await axios.post(`${window.TT_CONFIG.API_URL}?do=createZabbixHost`, {
deviceId: this.zabbixCreateModal.deviceId,
templateId: this.zabbixCreateModal.selectedTemplateId
});
if (response.data.success) {
window.notify('success', response.data.message);
this.zabbixCreateModal.show = false;
await this.refreshData();
} else {
window.notify('error', response.data.message);
}
} catch (e) {
window.notify('error', 'Ein Fehler ist aufgetreten.');
} finally {
this.zabbixCreateModal.loading = false;
this.zabbixCreateModal.loadingId = null;
this.zabbixCreateModal.deviceId = null;
this.zabbixCreateModal.selectedTemplateId = null;
}
},
openMapModal(host, theToolDevice = null) {
this.mapModal.host = host;
this.mapModal.theToolDevice = theToolDevice;
this.mapModal.deviceHasPop = !!(theToolDevice && theToolDevice.data && theToolDevice.data.pop_id);
this.mapModal.updateTheToolCoords = false;
this.mapModal.show = true;
this.$nextTick(this.initMap);
},
async initMap() {
if (typeof L === 'undefined' || typeof window.GeoSearch === 'undefined') {
await this.loadScripts();
}
if (this.mapModal.map) {
this.mapModal.map.remove();
}
const ttDevice = this.mapModal.theToolDevice;
const zbxHost = this.mapModal.host;
let initialLat = 47.0707; // Default to Graz
let initialLon = 15.4395;
let zoom = 13;
const ttDeviceLat = ttDevice?.data?.pop?.gps_lat ?? ttDevice?.data?.gps_lat;
const ttDeviceLon = ttDevice?.data?.pop?.gps_long ?? ttDevice?.data?.gps_long;
if (ttDevice && parseFloat(ttDeviceLat) && parseFloat(ttDeviceLon)) {
initialLat = parseFloat(ttDeviceLat);
initialLon = parseFloat(ttDeviceLon);
zoom = 16;
} else if (zbxHost.inventory && parseFloat(zbxHost.inventory.location_lat) && parseFloat(zbxHost.inventory.location_lon)) {
initialLat = parseFloat(zbxHost.inventory.location_lat);
initialLon = parseFloat(zbxHost.inventory.location_lon);
zoom = 16;
}
this.mapModal.map = L.map(this.$refs.leafletMap).setView([initialLat, initialLon], zoom);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; OpenStreetMap contributors'
}).addTo(this.mapModal.map);
const searchControl = new window.GeoSearch.GeoSearchControl({
provider: new window.GeoSearch.OpenStreetMapProvider(),
style: 'bar',
showMarker: false,
autoClose: true,
});
this.mapModal.map.addControl(searchControl);
this.mapModal.marker = L.marker([initialLat, initialLon], { draggable: true }).addTo(this.mapModal.map);
this.mapModal.markerCoords = this.mapModal.marker.getLatLng();
this.mapModal.marker.on('dragend', (event) => this.mapModal.markerCoords = event.target.getLatLng());
this.mapModal.map.on('click', (e) => {
this.mapModal.marker.setLatLng(e.latlng);
this.mapModal.markerCoords = e.latlng;
});
this.mapModal.map.on('geosearch/showlocation', (result) => {
const latLng = L.latLng(result.location.y, result.location.x);
this.mapModal.marker.setLatLng(latLng);
this.mapModal.markerCoords = latLng;
});
setTimeout(() => this.mapModal.map.invalidateSize(), 100);
},
async saveCoordinates() {
this.mapModal.loading = true;
const coords = this.mapModal.marker.getLatLng();
let allSuccess = true;
try {
const zabbixResponse = await axios.post(`${window.TT_CONFIG.API_URL}?do=updateZabbixCoordinates`, {
hostId: this.mapModal.host.hostid,
lat: coords.lat,
lon: coords.lng
});
if (!zabbixResponse.data.success) {
allSuccess = false;
window.notify('error', 'Fehler beim Speichern der Zabbix-Koordinaten: ' + zabbixResponse.data.message);
}
} catch (error) {
allSuccess = false;
window.notify('error', 'Fehler beim Speichern der Zabbix-Koordinaten.');
}
if (this.mapModal.updateTheToolCoords && this.mapModal.theToolDevice && allSuccess) {
try {
const theToolResponse = await axios.post(`${window.TT_CONFIG.API_URL}?do=updateTheToolCoordinates`, {
deviceId: this.mapModal.theToolDevice.id,
lat: coords.lat,
lon: coords.lng
});
if (!theToolResponse.data.success) {
allSuccess = false;
window.notify('error', 'Fehler beim Speichern der TheTool-Koordinaten: ' + theToolResponse.data.message);
}
} catch (e) {
allSuccess = false;
window.notify('error', 'Fehler beim Speichern der TheTool-Koordinaten.');
}
}
if(allSuccess) {
window.notify('success', 'Koordinaten erfolgreich gespeichert.');
this.mapModal.show = false;
await this.refreshData();
}
this.mapModal.loading = false;
},
async refreshData() {
this.loading = true;
try {
const response = await axios.post(`${window.TT_CONFIG.API_URL}?do=getZabbixConsolidationData`);
this.results = response.data;
} catch (error) {
window.notify('error', 'Fehler beim Aktualisieren der Daten.');
} finally {
this.loading = false;
}
},
loadScripts() {
const scripts = [
{ type: 'link', url: 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css' },
{ type: 'link', url: 'https://unpkg.com/leaflet-geosearch@3.0.0/dist/geosearch.css' },
{ type: 'script', url: 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js' },
{ type: 'script', url: 'https://unpkg.com/leaflet-geosearch@latest/dist/bundle.min.js' },
];
const promises = scripts.map(s => new Promise((resolve, reject) => {
let el;
if (document.querySelector(`[href="${s.url}"], [src="${s.url}"]`)) {
resolve(); return;
}
if (s.type === 'script') {
el = document.createElement('script');
el.src = s.url; el.async = false; el.onload = resolve; el.onerror = reject;
} else {
el = document.createElement('link');
el.rel = 'stylesheet'; el.href = s.url; resolve();
}
if (el) document.head.appendChild(el); else reject();
}));
return Promise.all(promises);
},
}
});