diff --git a/public/js/pages/Radius/RadiusUsers.js b/public/js/pages/Radius/RadiusUsers.js
index 7866dabdc..f2798be36 100644
--- a/public/js/pages/Radius/RadiusUsers.js
+++ b/public/js/pages/Radius/RadiusUsers.js
@@ -1,108 +1,138 @@
/* ===== RadiusUsers.js ===== */
Vue.component('radius-users', {
- template: `
-
-
-
-
-
-
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.
-
-
-
-
-
-
- `,
- 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; },
- 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] }); }
- }
+ 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();
+
+ 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] }); }
+ }
});