diff --git a/public/js/pages/Radius/Radius.js b/public/js/pages/Radius/Radius.js index 95372024a..13891b3be 100644 --- a/public/js/pages/Radius/Radius.js +++ b/public/js/pages/Radius/Radius.js @@ -147,18 +147,128 @@ Vue.component('radius-processing-indicator', { /* ---------- Online state chip (fetches radacct when visible) ---------- */ Vue.component('radius-online-state', { - props: { username: String }, data: () => ({ data: null, observed: false, ob: null }), + props: { username: String }, + data: () => ({ + data: null, + observed: false, + ob: null, + isHovering: false, + ctrlPressed: false, + tooltipText: 'IP-Adresse kopieren' + }), template: `
- - + +
`, - mounted() { this.ob = new IntersectionObserver((en) => { if (en[0].isIntersecting && !this.observed) { this.observed = true; this.fetchState(); } }, { threshold: 0.1 }); if (this.$refs.root) this.ob.observe(this.$refs.root); }, - beforeDestroy() { this.ob?.disconnect(); }, + watch: { + data(newData) { + // Update tooltip text when data is loaded + if (newData && newData.ip) { + this.tooltipText = 'IP-Adresse kopieren'; + } else { + this.tooltipText = null; + } + } + }, + mounted() { + this.ob = new IntersectionObserver((en) => { if (en[0].isIntersecting && !this.observed) { this.observed = true; this.fetchState(); } }, { threshold: 0.1 }); + if (this.$refs.root) this.ob.observe(this.$refs.root); + // Listen for Ctrl/Meta key presses globally + document.addEventListener('keydown', this.handleKey); + document.addEventListener('keyup', this.handleKey); + }, + beforeDestroy() { + this.ob?.disconnect(); + // Clean up global listeners + document.removeEventListener('keydown', this.handleKey); + document.removeEventListener('keyup', this.handleKey); + }, methods: { - async fetchState() { try { const r = await fetch(`${window.TT_CONFIG['BASE_PATH']}/Radius/proxyUnsecureHTTPRequestToRadius?action2=fetchRadacct&username=${encodeURIComponent(this.username)}`); this.data = r.ok ? await r.json() : { online: false, ip: null }; } catch { this.data = { online: false, ip: null }; } }, - async copyIp(event) { if (!this.data?.ip) return; const c = event.currentTarget; if (!c || c.classList.contains('is-copied')) return; await window.RadiusUtils.copyToClipboard(this.data.ip); c.classList.add('is-copied'); const o = c.dataset.tooltip; if (o) c.dataset.tooltip = 'Kopiert!'; setTimeout(() => { c.classList.remove('is-copied'); if (o) c.dataset.tooltip = o; }, 1500); } + async fetchState() { + try { + const r = await fetch(`${window.TT_CONFIG['BASE_PATH']}/Radius/proxyUnsecureHTTPRequestToRadius?action2=fetchRadacct&username=${encodeURIComponent(this.username)}`); + this.data = r.ok ? await r.json() : { online: false, ip: null }; + } catch { + this.data = { online: false, ip: null }; + } + }, + async copyIp(event) { + if (!this.data?.ip) return; + const c = event.currentTarget; + if (!c || c.classList.contains('is-copied')) return; + await window.RadiusUtils.copyToClipboard(this.data.ip); + c.classList.add('is-copied'); + + // Temporarily change tooltip to "Kopiert!" + const originalTooltip = this.tooltipText; + this.tooltipText = 'Kopiert!'; + + setTimeout(() => { + c.classList.remove('is-copied'); + // Restore original tooltip + this.tooltipText = originalTooltip; + // Re-run updateTooltip in case Ctrl is still pressed + this.updateTooltip(); + }, 1500); + }, + // --- New methods for Ctrl+Click --- + handleKey(event) { + const newCtrlPressed = event.ctrlKey || event.metaKey; + if (newCtrlPressed !== this.ctrlPressed) { + this.ctrlPressed = newCtrlPressed; + // If hovering, update tooltip live + if (this.isHovering) { + this.updateTooltip(); + } + } + }, + onIpMouseOver(event) { + this.isHovering = true; + this.ctrlPressed = event.ctrlKey || event.metaKey; + this.updateTooltip(); + }, + onIpMouseOut() { + this.isHovering = false; + this.ctrlPressed = false; // Reset on mouse out + this.updateTooltip(); + }, + updateTooltip() { + if (!this.data?.ip) { + this.tooltipText = null; + } else if (this.isHovering && this.ctrlPressed) { + this.tooltipText = 'Scan starten & verbinden'; + } else { + this.tooltipText = 'IP-Adresse kopieren'; + } + }, + onClickIp(event) { + if (!this.data?.ip) return; + + if (event.ctrlKey || event.metaKey) { + // Ctrl+Click or Meta+Click + event.preventDefault(); + this.$emit('scan-ip', { ip: this.data.ip }); + } else { + // Normal click + this.copyIp(event); + } + } + // --- End new methods --- } }); @@ -247,4 +357,4 @@ Vue.component('radius', { methods: { switchView(v){ this.view=v; if(!this._initFlags||this._initFlags[v])return; let r=''; if(v==='free')r='freeView';else if(v==='unused')r='unusedView'; if(r){this.$nextTick(()=>{const c=this.$refs[r];if(c&&typeof c.initIfNeeded==='function'){c.initIfNeeded();this._initFlags[v]=true;}});}} } -}); \ No newline at end of file +}); diff --git a/public/js/pages/Radius/RadiusUsers.js b/public/js/pages/Radius/RadiusUsers.js index f2798be36..eeb4c7908 100644 --- a/public/js/pages/Radius/RadiusUsers.js +++ b/public/js/pages/Radius/RadiusUsers.js @@ -1,138 +1,826 @@ -/* ===== RadiusUsers.js ===== */ Vue.component('radius-users', { -    template: ` -     
-       
-         
-         
z.B. nat* für lazy Suche
-         
Prefixe: '=' exakt, '*' Verlauf (lazy), '*=' Verlauf (exakt)
-         
-         
-           
-            -            -         
-       
-       
-          -            -            -            -            -          -         
Suche läuft...{{ radiusUsers.length }} Treffer gefunden
-       
-        -         
-           
Status
{{ radacctData.online ? 'Online' : 'Offline' }}
-                        -                        -            -            -         
-       
-        -          -        -        -         
-           

Bitte geben Sie eine gültige E-Mail-Adresse ein.

-           
-         
-       
-     
-    `, -    data: () => ({ window: window, billAddrDisplay: '', billAddrCustnum: '', username: '', ip: '', info: '', searchMode: 'autocomplete', radiusUsers: [], checkOnlineState: false, isLoading: false, showRadacctModal: false, radacctData: null, searchCount: 0, hasSearched: false, visibleCount: 50, observer: null, showTransferModal: false, transferInitialLoading: false, transferMonthlyLoading: false, transferModalUsername: '', transferYear: new Date().getFullYear(), transferMonth: new Date().getMonth() + 1, transferYearlyData: null, transferMonthlyData: null, transferChartInstance: null, showYearDropdown: false, isSendingEmail: false, showEmailModal: false, recipientEmail: '' }), -    computed: { -        hasFilters() { return this.billAddrDisplay || this.username || this.ip || this.info; }, -        visibleUsers() { return this.radiusUsers.slice(0, this.visibleCount); }, -        availableYears() { const c = new Date().getFullYear(), s = 2021; if (s > c) return [c]; return Array.from({length: c - s + 1}, (_, i) => c - i); }, -        allMonths() { return Array.from({ length: 12 }, (_, i) => ({ month: i + 1, name: new Date(2000, i, 1).toLocaleString('de-DE', { month: 'short' }) })); }, -        isValidEmail() { if (!this.recipientEmail) return false; return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.recipientEmail); } -    }, -    mounted() { -        const urlParams = new URLSearchParams(window.location.search); -        const infoParam = urlParams.get('info'); -        if (infoParam) { -            this.info = infoParam; -            this.loadRadiusUsers(); -        } -        this.observer = new IntersectionObserver(([e]) => { if (e && e.isIntersecting) this.loadMore(); }, { root: this.$refs.tableWrap, threshold: 0.1 }); -        if (this.$refs.sentinel) this.observer.observe(this.$refs.sentinel); -    }, -    beforeDestroy() { if (this.observer) this.observer.disconnect(); if (this.transferChartInstance) this.transferChartInstance.destroy(); }, -    updated() { if (this.observer && this.$refs.sentinel) { this.observer.disconnect(); this.observer.observe(this.$refs.sentinel); } }, -    methods: { -        onAddrSelect({ custnum, display }) { this.billAddrCustnum = custnum || ''; this.billAddrDisplay = display || ''; }, -        onModeChange(newMode) { this.searchMode = newMode; }, -        async loadRadiusUsers() { this.isLoading = true; this.radiusUsers = []; this.hasSearched = true; this.visibleCount = 50; try { const p = new URLSearchParams({ username: this.username || '', info: this.info || '', ip: this.ip || '' }); if (this.searchMode === 'text') p.set('estmk_nr', this.billAddrDisplay || ''); else p.set('custnum', this.billAddrCustnum || ''); const r = await fetch(`${window.TT_CONFIG.BASE_PATH}/Radius/proxyUnsecureHTTPRequestToRadius?${p.toString()}`); if (r.ok) { const u = await r.json(); if (Array.isArray(u) && u.length < 6) this.checkOnlineState = true; this.radiusUsers = Array.isArray(u) ? u : []; } } catch (e) { console.error(e); } this.isLoading = false; this.searchCount++; }, -        async fetchRadacctData(username) { this.showRadacctModal = true; this.radacctData = null; try { const r = await fetch(`${window.TT_CONFIG.BASE_PATH}/Radius/proxyUnsecureHTTPRequestToRadius?action2=fetchRadacct&username=${encodeURIComponent(username)}`); if (r.ok) this.radacctData = await r.json(); } catch (e) { console.error(e); this.radacctData = {}; } }, -        async copy(text, event) { if (!event || !event.currentTarget) return; const btn = event.currentTarget; if (btn.classList.contains('is-copied')) return; await window.RadiusUtils.copyToClipboard(text); btn.classList.add('is-copied'); btn.disabled = true; setTimeout(() => { btn.classList.remove('is-copied'); btn.disabled = false; }, 1500); }, -        clearFilters() { this.billAddrDisplay = ''; this.billAddrCustnum = ''; this.username = ''; this.ip = ''; this.info = ''; this.radiusUsers = []; this.hasSearched = false; this.searchCount++; this.visibleCount = 50; }, -        loadMore() { if (this.visibleCount < this.radiusUsers.length) this.visibleCount += 50; }, -        // ===== NEW METHOD ===== -        async scanIp(ip) { -            if (!ip) return; -            if (window.notify) { -                window.notify('info', `Starte Scan für ${ip}...`); -            } -            try { -                const response = await fetch(`http://localhost:8094/scan?ip=${ip}`); -                if (!response.ok) { -                    throw new Error(`HTTP-Fehler! Status: ${response.status}`); -                } -                const data = await response.json(); + template: ` +
+
+
+ +
+
+
z.B. nat* für lazy Suche
+
+
+
Prefixe: '=' exakt, '*' Verlauf (lazy), '*=' Verlauf (exakt) +
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+ + + + + + +
Suche läuft...{{ radiusUsers.length }} Treffer gefunden
+
+ +
+
Status +
+
{{ radacctData.online ? 'Online' : 'Offline' }} +
+
+
+
+
+
+   +
IP +
+
{{ radacctData.ip }} + +
+
+
+
+
+
+   +
Username + +
+ + +
+
+ + + + +
+
+
+

+ Bitte geben Sie eine gültige E-Mail-Adresse ein.

+
+ + +
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+ `, + data: () => ({ + window: window, + billAddrDisplay: '', + billAddrCustnum: '', + username: '', + ip: '', + info: '', + searchMode: 'autocomplete', + radiusUsers: [], + checkOnlineState: false, + isLoading: false, + showRadacctModal: false, + radacctData: null, + searchCount: 0, + hasSearched: false, + visibleCount: 50, + observer: null, + showTransferModal: false, + transferInitialLoading: false, + transferMonthlyLoading: false, + transferModalUsername: '', + transferYear: new Date().getFullYear(), + transferMonth: new Date().getMonth() + 1, + transferYearlyData: null, + transferMonthlyData: null, + transferChartInstance: null, + showYearDropdown: false, + isSendingEmail: false, + showEmailModal: false, + recipientEmail: '', + showExtensionIdModal: false, + extensionId: 'jglijfiddilckddlmbnlojmmlahboffh' + }), + computed: { + hasFilters() { + return this.billAddrDisplay || this.username || this.ip || this.info; + }, + visibleUsers() { + return this.radiusUsers.slice(0, this.visibleCount); + }, + availableYears() { + const c = new Date().getFullYear(), s = 2021; + if (s > c) return [c]; + return Array.from({length: c - s + 1}, (_, i) => c - i); + }, + allMonths() { + return Array.from({length: 12}, (_, i) => ({ + month: i + 1, + name: new Date(2000, i, 1).toLocaleString('de-DE', {month: 'short'}) + })); + }, + isValidEmail() { + if (!this.recipientEmail) return false; + return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.recipientEmail); + } + }, + mounted() { + const urlParams = new URLSearchParams(window.location.search); + const infoParam = urlParams.get('info'); + if (infoParam) { + this.info = infoParam; + this.loadRadiusUsers(); + } + this.observer = new IntersectionObserver(([e]) => { + if (e && e.isIntersecting) this.loadMore(); + }, {root: this.$refs.tableWrap, threshold: 0.1}); + if (this.$refs.sentinel) this.observer.observe(this.$refs.sentinel); -                if (data.status === 'success' && data.url) { -                    window.open(data.url, '_blank'); -                } else if (data.status === 'not_found') { -                    if (window.notify) { -                        window.notify('warning', `Kein Gerät für ${ip} gefunden.`); -                    } -                } else { -                    throw new Error('Ungültige oder unerwartete Antwort vom Scan-Server.'); -                } -            } catch (error) { -                console.error('IP-Scan fehlgeschlagen:', error); -                if (window.notify) { -                    window.notify('error', `Scan für ${ip} fehlgeschlagen.`); -                } -            } -        }, -        // ===== END NEW METHOD ===== -        async openTransferModal(username) { this.showTransferModal = true; this.transferModalUsername = username; this.transferYear = new Date().getFullYear(); this.transferMonth = new Date().getMonth() + 1; await this.fetchTransferYearData(); }, -        closeTransferModal() { this.showTransferModal = false; this.transferModalUsername = ''; this.transferYearlyData = null; this.transferMonthlyData = null; this.showYearDropdown = false; this.showEmailModal = false; this.recipientEmail = ''; this.isSendingEmail = false; if (this.transferChartInstance) { this.transferChartInstance.destroy(); this.transferChartInstance = null; } }, -        async fetchTransferYearData() { this.transferInitialLoading = true; this.transferYearlyData = null; try { const r = await fetch(`${window.TT_CONFIG.BASE_PATH}/Radius/proxyUnsecureHTTPRequestToRadius?action2=transferStatistic&username=${this.transferModalUsername}&year=${this.transferYear}&month=0`); if (r.ok) { const d = await r.json(); if(d && d.monthlySummary) { this.transferYearlyData = d; const last = [...d.monthlySummary].reverse().find(m => m.grandTotalBytes > 0); this.transferMonth = last ? last.month : new Date().getMonth() + 1; await this.fetchTransferMonthData(); }} else this.transferYearlyData = null; } catch (e) { console.error(e); this.transferYearlyData = null; } this.transferInitialLoading = false; }, -        async fetchTransferMonthData() { this.transferMonthlyLoading = true; this.transferMonthlyData = null; if (this.transferChartInstance) this.transferChartInstance.destroy(); try { const r = await fetch(`${window.TT_CONFIG.BASE_PATH}/Radius/proxyUnsecureHTTPRequestToRadius?action2=transferStatistic&username=${this.transferModalUsername}&year=${this.transferYear}&month=${this.transferMonth}`); this.transferMonthlyData = r.ok ? await r.json() : null; } catch (e) { console.error(e); this.transferMonthlyData = null; } this.transferMonthlyLoading = false; this.$nextTick(() => { if(this.showTransferModal) this.renderTransferChart(); }); }, -        prepareEmailModal() { if (this.transferInitialLoading || !this.transferYearlyData || !this.transferYearlyData.yearlySummary || this.transferYearlyData.yearlySummary.grandTotalBytes === 0) return; this.recipientEmail = ''; this.showEmailModal = true; }, -        async sendTransferEmail() { if (!this.transferMonthlyData || !this.transferChartInstance || !this.isValidEmail) return; this.isSendingEmail = true; try { const chartImageBase64 = this.transferChartInstance.toBase64Image(); const payload = { username: this.transferModalUsername, year: this.transferYear, month: this.transferMonth, monthlySummary: this.transferMonthlyData.summary, monthlyDetails: this.transferMonthlyData.details, chartImage: chartImageBase64, recipient: this.recipientEmail }; const res = await fetch(`${window.TT_CONFIG.BASE_PATH}/Radius/sendCustomerEmail`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (res.ok) { if (window.notify) window.notify('success', 'E-Mail wurde erfolgreich versendet.'); this.showEmailModal = false; } else { throw new Error('Server responded with an error.'); } } catch (e) { console.error("Failed to send transfer email:", e); if (window.notify) window.notify('error', 'Fehler beim Senden der E-Mail.'); } finally { this.isSendingEmail = false; } }, -        isMonthDisabled(month) { if (this.transferInitialLoading || this.transferMonthlyLoading) return true; if (!this.transferYearlyData?.monthlySummary) return true; const m = this.transferYearlyData.monthlySummary.find(m => m.month === month); return !m || m.grandTotalBytes === 0; }, -        selectYear(year) { this.showYearDropdown = false; if (this.transferYear !== year) this.changeTransferYear(year); }, -        async changeTransferYear(year) { this.transferYear = year; await this.fetchTransferYearData(); }, -        async changeTransferMonth(month) { this.transferMonth = month; await this.fetchTransferMonthData(); }, -        processChartData(details) { if (!details || !details.length) return { labels: [], datasets: [] }; const daily = details.reduce((a, s) => { const d = s.startTime.split(' ')[0]; if (!a[d]) a[d] = { downloadBytes: 0, uploadBytes: 0 }; a[d].downloadBytes += Number(s.downloadBytes) || 0; a[d].uploadBytes += Number(s.uploadBytes) || 0; return a; }, {}); const dates = Object.keys(daily).sort((a, b) => new Date(a) - new Date(b)); return { labels: dates, datasets: [ { label: 'Download', data: dates.map(d => daily[d].downloadBytes), borderColor: 'rgba(15, 157, 88, 0.8)', backgroundColor: 'rgba(15, 157, 88, 0.1)', fill: true, tension: 0.3, pointRadius: 2, borderWidth: 1.5 }, { label: 'Upload', data: dates.map(d => daily[d].uploadBytes), borderColor: 'rgba(0, 83, 132, 0.8)', backgroundColor: 'rgba(0, 83, 132, 0.1)', fill: true, tension: 0.3, pointRadius: 2, borderWidth: 1.5 } ] }; }, -        renderTransferChart() { if (this.transferChartInstance) this.transferChartInstance.destroy(); if (!this.$refs.transferChartCanvas || !this.transferMonthlyData?.details?.length || !window.Chart) return; const d = this.processChartData(this.transferMonthlyData.details); if (!d.labels.length) return; const chartBackgroundColorPlugin = { id: 'customCanvasBackgroundColor', beforeDraw: (chart) => { const { ctx } = chart; ctx.save(); ctx.globalCompositeOperation = 'destination-over'; ctx.fillStyle = '#ffffff'; ctx.fillRect(0, 0, chart.width, chart.height); ctx.restore(); } }; this.transferChartInstance = new Chart(this.$refs.transferChartCanvas.getContext('2d'), { type: 'line', data: d, options: { responsive: true, maintainAspectRatio: false, scales: { x: { type: 'time', time: { unit: 'day', tooltipFormat: 'DD.MM.YYYY', displayFormats: { day: 'DD.MM' } }, grid: { display: false }, ticks: { maxRotation: 0, autoSkip: true, maxTicksLimit: 15 } }, y: { beginAtZero: true, ticks: { callback: (v) => window.RadiusUtils.formatBytes(v, 0) }, grid: { color: 'rgba(0,0,0,0.05)' } } }, plugins: { tooltip: { callbacks: { label: (c) => `${c.dataset.label || ''}: ${window.RadiusUtils.formatBytes(c.parsed.y)}` } }, legend: { position: 'bottom', labels: { usePointStyle: true, boxWidth: 8, padding: 20 } } }, interaction: { mode: 'index', intersect: false } }, plugins: [chartBackgroundColorPlugin] }); } -    } -}); + const savedExtensionId = localStorage.getItem('radiusExtensionId'); + if (savedExtensionId) { + this.extensionId = savedExtensionId; + } + window.addEventListener('keydown', this.handleKeydown); + }, + beforeDestroy() { + if (this.observer) this.observer.disconnect(); + if (this.transferChartInstance) this.transferChartInstance.destroy(); + window.removeEventListener('keydown', this.handleKeydown); + }, + updated() { + if (this.observer && this.$refs.sentinel) { + this.observer.disconnect(); + this.observer.observe(this.$refs.sentinel); + } + }, + methods: { + handleKeydown(e) { + console.log(e); + if (e.code === 'KeyE' && e.ctrlKey && e.altKey) { + e.preventDefault(); + this.openExtensionIdModal(); + } + }, + openExtensionIdModal() { + this.showExtensionIdModal = true; + }, + saveExtensionId() { + localStorage.setItem('radiusExtensionId', this.extensionId); + this.showExtensionIdModal = false; + window.notify('success', 'Extension ID gespeichert.'); + }, + onAddrSelect({custnum, display}) { + this.billAddrCustnum = custnum || ''; + this.billAddrDisplay = display || ''; + }, + onModeChange(newMode) { + this.searchMode = newMode; + }, + async loadRadiusUsers() { + this.isLoading = true; + this.radiusUsers = []; + this.hasSearched = true; + this.visibleCount = 50; + try { + const p = new URLSearchParams({ + username: this.username || '', + info: this.info || '', + ip: this.ip || '' + }); + if (this.searchMode === 'text') p.set('estmk_nr', this.billAddrDisplay || ''); else p.set('custnum', this.billAddrCustnum || ''); + const r = await fetch(`${window.TT_CONFIG.BASE_PATH}/Radius/proxyUnsecureHTTPRequestToRadius?${p.toString()}`); + if (r.ok) { + const u = await r.json(); + if (Array.isArray(u) && u.length < 6) this.checkOnlineState = true; + this.radiusUsers = Array.isArray(u) ? u : []; + } + } catch (e) { + console.error(e); + } + this.isLoading = false; + + const urlParams = new URLSearchParams(window.location.search); + const connectParam = urlParams.get('connect'); + if (connectParam && connectParam.toLowerCase() === 'true' && this.radiusUsers.length === 1) { + const r = await fetch(`${window.TT_CONFIG.BASE_PATH}/Radius/proxyUnsecureHTTPRequestToRadius?action2=fetchRadacct&username=${encodeURIComponent(this.radiusUsers[0].username)}`); + if (r.ok) { + const radacct = await r.json(); + if (radacct && radacct.ip) { + this.onScanIp({ip: radacct.ip}, this.radiusUsers[0]); + } + } + } + + this.searchCount++; + }, + async fetchRadacctData(username) { + this.showRadacctModal = true; + this.radacctData = null; + try { + const r = await fetch(`${window.TT_CONFIG.BASE_PATH}/Radius/proxyUnsecureHTTPRequestToRadius?action2=fetchRadacct&username=${encodeURIComponent(username)}`); + if (r.ok) this.radacctData = await r.json(); + } catch (e) { + console.error(e); + this.radacctData = {}; + } + }, + async copy(text, event) { + if (!event || !event.currentTarget) return; + const btn = event.currentTarget; + if (btn.classList.contains('is-copied')) return; + await window.RadiusUtils.copyToClipboard(text); + btn.classList.add('is-copied'); + btn.disabled = true; + setTimeout(() => { + btn.classList.remove('is-copied'); + btn.disabled = false; + }, 1500); + }, + clearFilters() { + this.billAddrDisplay = ''; + this.billAddrCustnum = ''; + this.username = ''; + this.ip = ''; + this.info = ''; + this.radiusUsers = []; + this.hasSearched = false; + this.searchCount++; + this.visibleCount = 50; + }, + loadMore() { + if (this.visibleCount < this.radiusUsers.length) this.visibleCount += 50; + }, + async onScanIp(payload, item) { + const {ip} = payload; + const info = item.info; + if (!ip) return; + + window.notify('info', `Starte Scan für ${ip}...`); + + try { + const response = await fetch(`http://localhost:8094/scan?ip=${ip}`); + + if (!response.ok) { + window.notify('error', `Scan-Server-Fehler: ${response.status}`); + return; + } + + const data = await response.json(); + + if (data.status === 'success' && data.url) { + const extensionId = this.extensionId; + const message = { + type: "START_RADIUS_SCAN", + payload: { + ip: ip, + url: data.url, + info: info + } + }; + + if (window.chrome && chrome.runtime && chrome.runtime.sendMessage) { + try { + chrome.runtime.sendMessage(extensionId, message, (response) => { + if (chrome.runtime.lastError) { + console.warn("Senden an Erweiterung fehlgeschlagen:", chrome.runtime.lastError.message); + window.notify('warning', 'Scan-Daten konnten nicht an die Erweiterung gesendet werden.'); + } else { + console.log("Erweiterung hat geantwortet:", response); + } + }); + } catch (e) { + console.error("Fehler beim Senden an die Erweiterung:", e); + window.notify('error', 'Fehler beim Senden an die Erweiterung.'); + } + } else { + console.warn("Chrome Extension Messaging API nicht verfügbar."); + window.notify('warning', 'Chrome Messaging API nicht gefunden.'); + } + + } else if (data.status === 'not_found') { + window.notify('warning', `Kein Gerät für ${ip} gefunden.`); + } else { + window.notify('error', 'Ungültige Antwort vom Scan-Server.'); + } + } catch (error) { + console.error('IP-Scan fehlgeschlagen:', error); + window.notify('error', `Scan für ${ip} fehlgeschlagen.`); + } + }, + async openTransferModal(username) { + this.showTransferModal = true; + this.transferModalUsername = username; + this.transferYear = new Date().getFullYear(); + this.transferMonth = new Date().getMonth() + 1; + await this.fetchTransferYearData(); + }, + closeTransferModal() { + this.showTransferModal = false; + this.transferModalUsername = ''; + this.transferYearlyData = null; + this.transferMonthlyData = null; + this.showYearDropdown = false; + this.showEmailModal = false; + this.recipientEmail = ''; + this.isSendingEmail = false; + if (this.transferChartInstance) { + this.transferChartInstance.destroy(); + this.transferChartInstance = null; + } + }, + async fetchTransferYearData() { + this.transferInitialLoading = true; + this.transferYearlyData = null; + try { + const r = await fetch(`${window.TT_CONFIG.BASE_PATH}/Radius/proxyUnsecureHTTPRequestToRadius?action2=transferStatistic&username=${this.transferModalUsername}&year=${this.transferYear}&month=0`); + if (r.ok) { + const d = await r.json(); + if (d && d.monthlySummary) { + this.transferYearlyData = d; + const last = [...d.monthlySummary].reverse().find(m => m.grandTotalBytes > 0); + this.transferMonth = last ? last.month : new Date().getMonth() + 1; + await this.fetchTransferMonthData(); + } + } else this.transferYearlyData = null; + } catch (e) { + console.error(e); + this.transferYearlyData = null; + } + this.transferInitialLoading = false; + }, + async fetchTransferMonthData() { + this.transferMonthlyLoading = true; + this.transferMonthlyData = null; + if (this.transferChartInstance) this.transferChartInstance.destroy(); + try { + const r = await fetch(`${window.TT_CONFIG.BASE_PATH}/Radius/proxyUnsecureHTTPRequestToRadius?action2=transferStatistic&username=${this.transferModalUsername}&year=${this.transferYear}&month=${this.transferMonth}`); + this.transferMonthlyData = r.ok ? await r.json() : null; + } catch (e) { + console.error(e); + this.transferMonthlyData = null; + } + this.transferMonthlyLoading = false; + this.$nextTick(() => { + if (this.showTransferModal) this.renderTransferChart(); + }); + }, + prepareEmailModal() { + if (this.transferInitialLoading || !this.transferYearlyData || !this.transferYearlyData.yearlySummary || this.transferYearlyData.yearlySummary.grandTotalBytes === 0) return; + this.recipientEmail = ''; + this.showEmailModal = true; + }, + async sendTransferEmail() { + if (!this.transferMonthlyData || !this.transferChartInstance || !this.isValidEmail) return; + this.isSendingEmail = true; + try { + const chartImageBase64 = this.transferChartInstance.toBase64Image(); + const payload = { + username: this.transferModalUsername, + year: this.transferYear, + month: this.transferMonth, + monthlySummary: this.transferMonthlyData.summary, + monthlyDetails: this.transferMonthlyData.details, + chartImage: chartImageBase64, + recipient: this.recipientEmail + }; + const res = await fetch(`${window.TT_CONFIG.BASE_PATH}/Radius/sendCustomerEmail`, { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(payload) + }); + if (res.ok) { + window.notify('success', 'E-Mail wurde erfolgreich versendet.'); + this.showEmailModal = false; + } else { + throw new Error('Server responded with an error.'); + } + } catch (e) { + console.error("Failed to send transfer email:", e); + window.notify('error', 'Fehler beim Senden der E-Mail.'); + } finally { + this.isSendingEmail = false; + } + }, + isMonthDisabled(month) { + if (this.transferInitialLoading || this.transferMonthlyLoading) return true; + if (!this.transferYearlyData?.monthlySummary) return true; + const m = this.transferYearlyData.monthlySummary.find(m => m.month === month); + return !m || m.grandTotalBytes === 0; + }, + selectYear(year) { + this.showYearDropdown = false; + if (this.transferYear !== year) this.changeTransferYear(year); + }, + async changeTransferYear(year) { + this.transferYear = year; + await this.fetchTransferYearData(); + }, + async changeTransferMonth(month) { + this.transferMonth = month; + await this.fetchTransferMonthData(); + }, + processChartData(details) { + if (!details || !details.length) return {labels: [], datasets: []}; + const daily = details.reduce((a, s) => { + const d = s.startTime.split(' ')[0]; + if (!a[d]) a[d] = {downloadBytes: 0, uploadBytes: 0}; + a[d].downloadBytes += Number(s.downloadBytes) || 0; + a[d].uploadBytes += Number(s.uploadBytes) || 0; + return a; + }, {}); + const dates = Object.keys(daily).sort((a, b) => new Date(a) - new Date(b)); + return { + labels: dates, + datasets: [{ + label: 'Download', + data: dates.map(d => daily[d].downloadBytes), + borderColor: 'rgba(15, 157, 88, 0.8)', + backgroundColor: 'rgba(15, 157, 88, 0.1)', + fill: true, + tension: 0.3, + pointRadius: 2, + borderWidth: 1.5 + }, { + label: 'Upload', + data: dates.map(d => daily[d].uploadBytes), + borderColor: 'rgba(0, 83, 132, 0.8)', + backgroundColor: 'rgba(0, 83, 132, 0.1)', + fill: true, + tension: 0.3, + pointRadius: 2, + borderWidth: 1.5 + }] + }; + }, + renderTransferChart() { + if (this.transferChartInstance) this.transferChartInstance.destroy(); + if (!this.$refs.transferChartCanvas || !this.transferMonthlyData?.details?.length || !window.Chart) return; + const d = this.processChartData(this.transferMonthlyData.details); + if (!d.labels.length) return; + const chartBackgroundColorPlugin = { + id: 'customCanvasBackgroundColor', beforeDraw: (chart) => { + const {ctx} = chart; + ctx.save(); + ctx.globalCompositeOperation = 'destination-over'; + ctx.fillStyle = '#ffffff'; + ctx.fillRect(0, 0, chart.width, chart.height); + ctx.restore(); + } + }; + this.transferChartInstance = new Chart(this.$refs.transferChartCanvas.getContext('2d'), { + type: 'line', + data: d, + options: { + responsive: true, + maintainAspectRatio: false, + scales: { + x: { + type: 'time', + time: {unit: 'day', tooltipFormat: 'DD.MM.YYYY', displayFormats: {day: 'DD.MM'}}, + grid: {display: false}, + ticks: {maxRotation: 0, autoSkip: true, maxTicksLimit: 15} + }, + y: { + beginAtZero: true, + ticks: {callback: (v) => window.RadiusUtils.formatBytes(v, 0)}, + grid: {color: 'rgba(0,0,0,0.05)'} + } + }, + plugins: { + tooltip: {callbacks: {label: (c) => `${c.dataset.label || ''}: ${window.RadiusUtils.formatBytes(c.parsed.y)}`}}, + legend: {position: 'bottom', labels: {usePointStyle: true, boxWidth: 8, padding: 20}} + }, + interaction: {mode: 'index', intersect: false} + }, + plugins: [chartBackgroundColorPlugin] + }); + } + } +}); \ No newline at end of file