Vue.component('device-monitoring-modal', { //language=Vue template: `
Keine allgemeinen Monitoring-Daten gefunden.
{{ item.name.replace('ICMP: ', '') }} {{ formatGeneralValue(item) }} {{ item.units }}

{{ formatUptime(item.value) }}

Seit {{ moment(Date.now() - item.value * 1000).format('DD.MM.YYYY HH:mm') }}
Bitte eine oder mehrere Schnittstellen auswählen, um Graphen anzuzeigen.
{{ iface.name }}
Empfangen (Mbps) Min: {{ statistics[iface.name].rx.min }} Avg: {{ statistics[iface.name].rx.avg }} Median: {{ statistics[iface.name].rx.median }} Max: {{ statistics[iface.name].rx.max }} 95%: {{ statistics[iface.name].rx.p95 }}
Gesendet (Mbps) Min: {{ statistics[iface.name].tx.min }} Avg: {{ statistics[iface.name].tx.avg }} Median: {{ statistics[iface.name].tx.median }} Max: {{ statistics[iface.name].tx.max }} 95%: {{ statistics[iface.name].tx.p95 }}
Keine aktuellen Probleme für dieses Gerät gefunden.
{{ p.name }} {{ moment(p.clock * 1000).format('DD.MM.YYYY HH:mm:ss') }}
{{ p.opdata }}
`, props: ['hostId', 'deviceName'], data() { return { moment: window.moment, activeTab: 'overview', tabs: [ { id: 'overview', name: 'Übersicht', icon: 'fas fa-tachometer-alt' }, { id: 'interfaces', name: 'Schnittstellen', icon: 'fas fa-ethernet' }, { id: 'problems', name: 'Probleme', icon: 'fas fa-exclamation-triangle' }, ], loading: { overview: false, interfaces: false, problems: false, individualInterfaces: {} }, generalData: null, problemData: [], allInterfaces: [], selectedInterfaces: [], interfaceTimeRange: '24h', timeRanges: [ { text: '6H', value: '6h' }, { text: '24H', value: '24h' }, { text: '7T', value: '7d' }, { text: '30T', value: '30d' }, ], interfaceChartData: {}, chartInstances: {}, dataNormalizationMode: 'avg', downsampleThreshold: 500, }; }, computed: { interfaceOptions() { return this.allInterfaces.map(iface => ({ text: iface.name, value: iface.name })); }, selectedInterfacesData() { return this.allInterfaces.filter(iface => this.selectedInterfaces.includes(iface.name)); }, displayChartData() { const processedData = {}; for (const itemId in this.interfaceChartData) { const data = this.interfaceChartData[itemId]; if (data.length > this.downsampleThreshold) { processedData[itemId] = this.downsampleData(data, this.dataNormalizationMode); } else { processedData[itemId] = data; } } return processedData; }, statistics() { if (this.selectedInterfaces.length === 0 || Object.keys(this.interfaceChartData).length === 0) return {}; const stats = {}; this.selectedInterfacesData.forEach(iface => { const calculate = (data) => { if (!data || data.length === 0) return { min: 'N/A', max: 'N/A', avg: 'N/A', median: 'N/A', p95: 'N/A' }; const values = data.map(p => p.y); const sorted = [...values].sort((a, b) => a - b); const sum = values.reduce((acc, val) => acc + val, 0); const mid = Math.floor(sorted.length / 2); const median = sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid]; const p95 = this.calculateNormalized95thPercentile(data); return { min: this.formatStat(sorted[0]), max: this.formatStat(sorted[sorted.length - 1]), avg: this.formatStat(sum / values.length), median: this.formatStat(median), p95: this.formatStat(p95), }; }; stats[iface.name] = { rx: calculate(this.interfaceChartData[iface.rx?.itemid]), tx: calculate(this.interfaceChartData[iface.tx?.itemid]), }; }); return stats; } }, async mounted() { // We need chartjs-plugin-zoom for this to work. Assuming it's globally available. if (typeof Chart.register === 'function' && window.ChartZoom) { Chart.register(window.ChartZoom); } moment.locale('de'); this.fetchTabData(); }, beforeDestroy() { this.destroyAllCharts(); }, methods: { formatStat: val => typeof val === 'number' ? val.toFixed(2) : val, formatUptime: s => `${Math.floor(s/(3600*24))}t ${Math.floor(s%(3600*24)/3600)}h ${Math.floor(s%3600/60)}m`, formatGeneralValue: item => (item.units === 's') ? parseFloat(item.value).toFixed(3) : (item.units === '%') ? parseFloat(item.value).toFixed(2) : item.value, getSeverityClass: s => ['sev-info', 'sev-info', 'sev-warning', 'sev-average', 'sev-high', 'sev-disaster'][s] || 'sev-info', getSeverityIcon: s => ['fa-info-circle', 'fa-info-circle', 'fa-exclamation-circle', 'fa-exclamation-triangle', 'fa-radiation-alt', 'fa-biohazard'][s] || 'fa-info-circle', async fetchTabData() { const tab = this.activeTab; if (this.loading[tab]) return; this.loading[tab] = true; try { if (tab === 'overview' && !this.generalData) { const res = await axios.get(`${window.TT_CONFIG.BASE_URL}/DeviceMonitoring/generalData`, { params: { hostId: this.hostId } }); this.generalData = res.data; } else if (tab === 'interfaces' && this.allInterfaces.length === 0) { const res = await axios.get(`${window.TT_CONFIG.BASE_URL}/DeviceMonitoring/listInterfaces`, { params: { hostId: this.hostId } }); this.allInterfaces = res.data; } else if (tab === 'problems' && this.problemData.length === 0) { const res = await axios.get(`${window.TT_CONFIG.BASE_URL}/DeviceMonitoring/getProblems`, { params: { hostId: this.hostId } }); this.problemData = res.data; } } catch (e) { console.error(`Failed to load data for tab ${tab}`, e); window.notify('error', `Laden von ${tab}-Daten fehlgeschlagen.`); } finally { this.loading[tab] = false; } }, async fetchAndRenderInterface(iface) { const itemIds = [iface.rx?.itemid, iface.tx?.itemid].filter(Boolean); if (itemIds.length === 0) return; this.$set(this.loading.individualInterfaces, iface.name, true); try { const res = await axios.post(`${window.TT_CONFIG.BASE_URL}/DeviceMonitoring/interfaceData`, { itemIds, timeRange: this.interfaceTimeRange }); this.$set(this.interfaceChartData, iface.rx?.itemid, res.data[iface.rx?.itemid] || []); this.$set(this.interfaceChartData, iface.tx?.itemid, res.data[iface.tx?.itemid] || []); this.$nextTick(() => this.renderChart(iface)); } catch(e) { console.error(`Failed to fetch history for ${iface.name}`, e); } finally { this.$set(this.loading.individualInterfaces, iface.name, false); } }, async handleInterfaceSelectionChange(newSelection, oldSelection) { const added = newSelection.filter(name => !oldSelection.includes(name)); const removed = oldSelection.filter(name => !newSelection.includes(name)); removed.forEach(name => { const iface = this.allInterfaces.find(i => i.name === name); if (iface) { this.destroyChart(iface.name); delete this.interfaceChartData[iface.rx?.itemid]; delete this.interfaceChartData[iface.tx?.itemid]; } }); for (const name of added) { const iface = this.allInterfaces.find(i => i.name === name); if (iface) { await this.fetchAndRenderInterface(iface); } } }, async handleTimeOrNormalizationChange() { this.destroyAllCharts(); this.interfaceChartData = {}; this.loading.interfaces = true; const interfacesToFetch = this.selectedInterfacesData; for (const iface of interfacesToFetch) { await this.fetchAndRenderInterface(iface); } this.loading.interfaces = false; }, async renderChart(iface) { let tries = 0; while (!this.$refs['chartCanvas-' + iface.name]?.[0] && tries < 10) { console.log(typeof this.$refs['chartCanvas-' + iface.name]?.[0]); await Promise.all([ this.$nextTick(), new Promise(resolve => setTimeout(resolve, 100)) ]); } const canvas = this.$refs['chartCanvas-' + iface.name]?.[0]; console.log(canvas, this.$refs); if (!canvas) return; if (this.chartInstances[iface.name]) { this.chartInstances[iface.name].destroy(); } this.chartInstances[iface.name] = new Chart(canvas.getContext('2d'), { type: 'line', data: { datasets: [ { label: 'Empfangen', data: this.displayChartData[iface.rx?.itemid] || [], borderColor: '#4CAF50', borderWidth: 1.5, fill: true, backgroundColor: 'rgba(76, 175, 80, 0.2)', pointRadius: 0, tension: 0.1 }, { label: 'Gesendet', data: this.displayChartData[iface.tx?.itemid] || [], borderColor: '#2196F3', borderWidth: 1.5, fill: true, backgroundColor: 'rgba(33, 150, 243, 0.2)', pointRadius: 0, tension: 0.1 } ] }, options: { responsive: true, maintainAspectRatio: true, interaction: { mode: 'index', intersect: false, }, scales: { x: { type: 'time', time: { tooltipFormat: 'DD.MM.YYYY HH:mm:ss' }, adapters: { date: { locale: 'de' } } }, y: { beginAtZero: true, title: { display: true, text: 'Mbps' } } }, plugins: { legend: { display: true, position: 'bottom', labels: { boxWidth: 12, font: { size: 10 } } }, zoom: { pan: { enabled: true, mode: 'x' }, zoom: { wheel: { enabled: false }, pinch: { enabled: true }, mode: 'x', drag: { enabled: true } }, } } } }); }, downsampleData(data, mode) { const bucketSize = Math.ceil(data.length / this.downsampleThreshold); const downsampled = []; for (let i = 0; i < data.length; i += bucketSize) { const chunk = data.slice(i, i + bucketSize); if (chunk.length === 0) continue; const representativeX = chunk[Math.floor(chunk.length / 2)].x; let representativeY; if (mode === 'max') { representativeY = Math.max(...chunk.map(p => p.y)); } else { // avg const sum = chunk.reduce((acc, p) => acc + p.y, 0); representativeY = sum / chunk.length; } downsampled.push({ x: representativeX, y: representativeY }); } return downsampled; }, calculateNormalized95thPercentile(data) { if (!data || data.length < 3) return null; const averagedValues = []; for (let i = 0; i <= data.length - 3; i += 3) { const chunk = data.slice(i, i + 3); const sum = chunk.reduce((acc, p) => acc + p.y, 0); averagedValues.push(sum / 3); } if (averagedValues.length === 0) return null; const sorted = averagedValues.sort((a, b) => a - b); const index = Math.floor(sorted.length * 0.95); return sorted[index]; }, openLiveChartPopup(iface) { const url = `${window.TT_CONFIG.BASE_URL}/DeviceMonitoring/liveGraphPage?rx_id=${iface.rx?.itemid || ''}&tx_id=${iface.tx?.itemid || ''}&name=${encodeURIComponent(iface.name)}`; window.open(url, `livegraph_${iface.name.replace(/[^a-zA-Z0-9]/g, "_")}`, 'width=800,height=450,resizable=yes,scrollbars=yes'); }, destroyChart(name) { if (this.chartInstances[name]) { this.chartInstances[name].destroy(); delete this.chartInstances[name]; } }, destroyAllCharts() { Object.values(this.chartInstances).forEach(c => c.destroy()); this.chartInstances = {}; }, resetAllChartsZoom() { Object.values(this.chartInstances).forEach(chart => { chart.resetZoom(); }); } }, watch: { activeTab: 'fetchTabData', selectedInterfaces(newVal, oldVal) { this.handleInterfaceSelectionChange(newVal, oldVal); }, interfaceTimeRange: 'handleTimeOrNormalizationChange', dataNormalizationMode: 'handleTimeOrNormalizationChange', } });