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: `
-
- {{ data.ip || '—' }}
+
+
+
+
+
+
+ {{ data.ip || '—' }}
+
+
`,
- 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: `
-
-
-
-
-
-
Status{{ radacctData.online ? 'Online' : 'Offline' }}
-
-
-
Kundennummer{{ radacctData.customerNumber || '—' }}
Kundenname{{ radacctData.customerName || '—' }}
Info{{ radacctData.info || '—' }}
WLAN Password{{ radacctData.wlanPassword || '—' }}
Bandbreite{{ radacctData.actualBandwidth || '—' }}
-
-
-
-
-
-
-
-
-
-
Gesamt {{ transferYear }}:
{{ window.RadiusUtils.formatBytes(transferYearlyData.yearlySummary.grandTotalBytes) }}
-
-
Monat gesamt
{{ window.RadiusUtils.formatBytes(transferMonthlyData?.summary?.grandTotalBytes || 0) }}
Download
{{ window.RadiusUtils.formatBytes(transferMonthlyData?.summary?.totalDownloadBytes || 0) }}
Upload
{{ window.RadiusUtils.formatBytes(transferMonthlyData?.summary?.totalUploadBytes || 0) }}
Dauer
{{ window.RadiusUtils.formatDuration(transferMonthlyData?.summary?.totalDurationSeconds || 0) }}
-
Keine Daten in diesem Monat verfügbar
-
-
-
Keine detaillierten Daten für diesen Monat.
-
| Startzeit | Dauer | IP-Adresse | Download | Upload | Gesamt |
|---|
| | | | | |
| {{ d.startTime }} | {{ window.RadiusUtils.formatDuration(d.durationSeconds) }} | {{ d.ipAddress }} | {{ window.RadiusUtils.formatBytes(d.downloadBytes) }} | {{ window.RadiusUtils.formatBytes(d.uploadBytes) }} | {{ window.RadiusUtils.formatBytes(d.totalBytes) }} |
-
-
-
Daten konnten nicht geladen werden.
-
-
-
-
-
-
- `,
- 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | Kundennummer |
+ Username |
+ Info |
+ Status |
+ Aktionen |
+
+
+
+
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ {{ item.customerNumber }}
+ |
+ {{ item.username }}
+
+ |
+ {{ item.info || '—' }} |
+
+
+ |
+
+
+
+ |
+
+
+
+
+
+
Suche läuft...{{ radiusUsers.length }} Treffer gefunden
+
+
+
+
Status
+
+
{{ radacctData.online ? 'Online' : 'Offline' }}
+
+
+
+
+
+
IP
+
+
{{ radacctData.ip }}—
+
+
+
+
+
+
+
+
+ Kundennummer{{ radacctData.customerNumber || '—' }}
+ Kundenname
+
{{ radacctData.customerName || '—' }}
+
+ Info
+
{{ radacctData.info || '—' }}
+
+ WLAN Password{{ radacctData.wlanPassword || '—' }}
+ Bandbreite{{ radacctData.actualBandwidth || '—' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Gesamt {{ transferYear }}
+ :
+
+ {{ window.RadiusUtils.formatBytes(transferYearlyData.yearlySummary.grandTotalBytes) }}
+
+
+
+
+
+
+
+
+
Monat gesamt
+
{{ window.RadiusUtils.formatBytes(transferMonthlyData?.summary?.grandTotalBytes || 0) }}
+
+
+
+
+
+
+
Download
+
{{ window.RadiusUtils.formatBytes(transferMonthlyData?.summary?.totalDownloadBytes || 0) }}
+
+
+
+
+
+
+
Upload
+
{{ window.RadiusUtils.formatBytes(transferMonthlyData?.summary?.totalUploadBytes || 0) }}
+
+
+
+
+
+
+
Dauer
+
{{ window.RadiusUtils.formatDuration(transferMonthlyData?.summary?.totalDurationSeconds || 0) }}
+
+
+
+
+
+
+
Keine Daten in diesem Monat verfügbar
+
+
+
+
+
+
Keine detaillierten Daten für diesen Monat.
+
+
+
+
+ | Startzeit |
+ Dauer |
+ IP-Adresse |
+ Download |
+ Upload |
+ Gesamt |
+
+
+
+
+
+ |
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+
+
+ | {{ d.startTime }} |
+ {{ window.RadiusUtils.formatDuration(d.durationSeconds) }} |
+ {{ d.ipAddress }} |
+
+ {{ window.RadiusUtils.formatBytes(d.downloadBytes) }}
+ |
+
+ {{ window.RadiusUtils.formatBytes(d.uploadBytes) }}
+ |
+
+ {{ window.RadiusUtils.formatBytes(d.totalBytes) }} |
+
+
+
+
+
+
+
+
Daten konnten nicht geladen werden.
+
+
+
+
+
+
+
+
+ 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