From 185f015cb7bd2ba61599e55806806795e397ff94 Mon Sep 17 00:00:00 2001 From: mhdzumair Date: Thu, 20 Feb 2025 23:27:01 +0530 Subject: [PATCH] feat: add week selector for weekly top uploaders metrics --- metrics/routes.py | 163 ++++---- resources/css/metrics.css | 93 ++++- resources/html/metrics.html | 7 +- resources/js/metrics.js | 763 +++++++++++++++++------------------- 4 files changed, 541 insertions(+), 485 deletions(-) diff --git a/metrics/routes.py b/metrics/routes.py index 79b8518..939f6ac 100644 --- a/metrics/routes.py +++ b/metrics/routes.py @@ -186,92 +186,111 @@ async def get_torrents_by_uploaders(response: Response): return uploader_stats -@metrics_router.get("/torrents/uploaders/weekly", tags=["metrics"]) -async def get_weekly_top_uploaders(response: Response): +@metrics_router.get("/torrents/uploaders/weekly/{week_date}", tags=["metrics"]) +async def get_weekly_top_uploaders(week_date: str, response: Response): response.headers.update(const.NO_CACHE_HEADERS) - # Calculate the start of the current week (Monday) - today = datetime.now(tz=timezone.utc) - start_of_week = today - timedelta(days=today.weekday()) - start_of_week = start_of_week.replace(hour=0, minute=0, second=0, microsecond=0) + try: + # Parse the input date + selected_date = datetime.strptime(week_date, "%Y-%m-%d") + selected_date = selected_date.replace(tzinfo=timezone.utc) - # Get raw aggregation results - raw_results = await TorrentStreams.aggregate( - [ - { - "$match": { - "source": "Contribution Stream", - "$or": [ - {"created_at": {"$gte": start_of_week}}, - {"uploaded_at": {"$gte": start_of_week}}, - ], - "is_blocked": {"$ne": True}, - } - }, - { - "$group": { - "_id": "$uploader", - "count": {"$sum": 1}, - "latest_upload": {"$max": "$created_at"}, - } - }, - ] - ).to_list() + # Calculate the start of the week (Monday) for the selected date + start_of_week = selected_date - timedelta(days=selected_date.weekday()) + start_of_week = start_of_week.replace(hour=0, minute=0, second=0, microsecond=0) - # Process and clean the results - processed_results = [] - anonymous_total = 0 - anonymous_latest = None + # Calculate end of week + end_of_week = start_of_week + timedelta(days=7) - for stat in raw_results: - uploader_name = stat["_id"] - count = stat["count"] - latest_upload = stat["latest_upload"] + # Get raw aggregation results + raw_results = await TorrentStreams.aggregate( + [ + { + "$match": { + "source": "Contribution Stream", + "$or": [ + { + "uploaded_at": { + "$gte": start_of_week, + "$lt": end_of_week, + } + }, + ], + "is_blocked": {"$ne": True}, + } + }, + { + "$group": { + "_id": "$uploader", + "count": {"$sum": 1}, + "latest_upload": {"$max": "$created_at"}, + } + }, + ] + ).to_list() - # Skip invalid entries - if count <= 0: - continue + # Process and clean the results (keeping existing logic) + processed_results = [] + anonymous_total = 0 + anonymous_latest = None - # Combine all anonymous-like entries - if ( - uploader_name is None - or uploader_name.strip() == "" - or uploader_name.lower() == "anonymous" - ): - anonymous_total += count - if anonymous_latest is None or ( - latest_upload and latest_upload > anonymous_latest + for stat in raw_results: + uploader_name = stat["_id"] + count = stat["count"] + latest_upload = stat["latest_upload"] + + if count <= 0: + continue + + if ( + uploader_name is None + or uploader_name.strip() == "" + or uploader_name.lower() == "anonymous" ): - anonymous_latest = latest_upload - else: + anonymous_total += count + if anonymous_latest is None or ( + latest_upload and latest_upload > anonymous_latest + ): + anonymous_latest = latest_upload + else: + processed_results.append( + { + "name": uploader_name.strip(), + "count": count, + "latest_upload": ( + latest_upload.strftime("%Y-%m-%d") + if latest_upload + else None + ), + } + ) + + if anonymous_total > 0: processed_results.append( { - "name": uploader_name.strip(), - "count": count, + "name": "Anonymous", + "count": anonymous_total, "latest_upload": ( - latest_upload.strftime("%Y-%m-%d") if latest_upload else None + anonymous_latest.strftime("%Y-%m-%d") + if anonymous_latest + else None ), } ) - # Add anonymous entry if we have any anonymous uploads - if anonymous_total > 0: - processed_results.append( - { - "name": "Anonymous", - "count": anonymous_total, - "latest_upload": ( - anonymous_latest.strftime("%Y-%m-%d") if anonymous_latest else None - ), - } - ) + final_results = sorted( + processed_results, key=lambda x: x["count"], reverse=True + )[:20] - # Sort by count and limit to top 20 - final_results = sorted(processed_results, key=lambda x: x["count"], reverse=True)[ - :20 - ] + return { + "week_start": start_of_week.strftime("%Y-%m-%d"), + "week_end": end_of_week.strftime("%Y-%m-%d"), + "uploaders": final_results, + } - return { - "week_start": start_of_week.strftime("%Y-%m-%d"), - "uploaders": final_results, - } + except ValueError as e: + return { + "error": f"Invalid date format. Please use YYYY-MM-DD format. Details: {str(e)}" + } + except Exception as e: + return {"error": f"An error occurred: {str(e)}"} diff --git a/resources/css/metrics.css b/resources/css/metrics.css index 39ba3c9..00ffdd6 100644 --- a/resources/css/metrics.css +++ b/resources/css/metrics.css @@ -58,12 +58,86 @@ h4 { transform: translateY(-5px); } -.date-info { - background: rgba(129,87,208,0.6); +.week-selector { + display: flex; + align-items: center; + justify-content: center; + gap: 12px; + margin: 20px 0; + padding: 10px; + background: rgba(74, 71, 163, 0.2); + border-radius: 8px; +} + +.week-selector button { + background: #4a47a3; + color: white; + border: none; padding: 8px 16px; - border-radius: 4px; - text-align: center; + border-radius: 6px; font-size: 0.9rem; + cursor: pointer; + transition: all 0.3s ease; + min-width: 130px; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); +} + +.week-selector button:hover { + background: #5b58b4; + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); +} + +.week-selector button:active { + transform: translateY(0); +} + +.week-selector input[type="date"] { + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); + color: white; + padding: 7px 14px; + border-radius: 6px; + font-size: 0.9rem; + min-width: 150px; + cursor: pointer; + transition: all 0.3s ease; +} + +.week-selector input[type="date"]:hover { + background: rgba(255, 255, 255, 0.15); + border-color: rgba(255, 255, 255, 0.3); +} + +.week-selector input[type="date"]:focus { + outline: none; + border-color: #4a47a3; + background: rgba(255, 255, 255, 0.2); +} + +/* Style the calendar icon */ +.week-selector input[type="date"]::-webkit-calendar-picker-indicator { + filter: invert(1); + opacity: 0.7; + cursor: pointer; +} + +.week-selector input[type="date"]::-webkit-calendar-picker-indicator:hover { + opacity: 1; +} + +.date-info { + background: rgba(74, 71, 163, 0.4); + color: white; + padding: 10px 20px; + border-radius: 6px; + text-align: center; + font-size: 1rem; + margin: 15px 0; + border: 1px solid rgba(255, 255, 255, 0.1); } .chart-wrapper { @@ -229,6 +303,17 @@ h4 { padding: 10px; } + .week-selector { + flex-direction: column; + gap: 8px; + } + + .week-selector button, + .week-selector input[type="date"] { + width: 100%; + min-width: unset; + } + h3 { font-size: 1.2rem; } diff --git a/resources/html/metrics.html b/resources/html/metrics.html index 7f13841..3b4787f 100644 --- a/resources/html/metrics.html +++ b/resources/html/metrics.html @@ -64,7 +64,12 @@
-

Top 20 Contribution Streams Uploaders This Week

+

Top 20 Contribution Streams Uploaders

+
+ + + +
diff --git a/resources/js/metrics.js b/resources/js/metrics.js index e4f222d..102c938 100644 --- a/resources/js/metrics.js +++ b/resources/js/metrics.js @@ -1,84 +1,94 @@ -document.addEventListener('DOMContentLoaded', function () { - const fetchData = async (url) => { - const response = await fetch(url); - return response.json(); - }; +let weeklyUploadersChart = null; - const removeLoadingElement = (spinnerId) => { - const spinner = document.getElementById(spinnerId); - if (spinner) { - spinner.remove(); - } - }; +const formatDateToYYYYMMDD = (date) => { + return date.toISOString().split('T')[0]; +}; - const renderSchedulerDetails = async () => { - const data = await fetchData('/metrics/scrapy-schedulers'); - removeLoadingElement('schedulerChartsSkeleton'); - const container = document.getElementById('schedulerChartsContainer'); - container.innerHTML = ''; // Clear loading skeleton +const getStartOfWeek = (date) => { + const d = new Date(date); + const day = d.getDay(); + const diff = d.getDate() - day + (day === 0 ? -6 : 1); + return new Date(d.setDate(diff)); +}; - const enabledWithStats = []; - const enabledWithoutStats = []; - const disabledWithStats = []; - const disabledWithoutStats = []; +const fetchData = async (url) => { + const response = await fetch(url); + return response.json(); +}; - data.forEach(scheduler => { - if (scheduler.is_scheduler_disabled) { - if (scheduler.last_run_state) { - disabledWithStats.push(scheduler); - } else { - disabledWithoutStats.push(scheduler); - } +const removeLoadingElement = (spinnerId) => { + const spinner = document.getElementById(spinnerId); + if (spinner) { + spinner.remove(); + } +}; + +const renderSchedulerDetails = async () => { + const data = await fetchData('/metrics/scrapy-schedulers'); + removeLoadingElement('schedulerChartsSkeleton'); + const container = document.getElementById('schedulerChartsContainer'); + container.innerHTML = ''; // Clear loading skeleton + + const enabledWithStats = []; + const enabledWithoutStats = []; + const disabledWithStats = []; + const disabledWithoutStats = []; + + data.forEach(scheduler => { + if (scheduler.is_scheduler_disabled) { + if (scheduler.last_run_state) { + disabledWithStats.push(scheduler); } else { - if (scheduler.last_run_state) { - enabledWithStats.push(scheduler); + disabledWithoutStats.push(scheduler); + } + } else { + if (scheduler.last_run_state) { + enabledWithStats.push(scheduler); + } else { + enabledWithoutStats.push(scheduler); + } + } + }); + + const createSchedulerSection = (schedulers, sectionTitle) => { + const section = document.createElement('div'); + section.className = 'scheduler-section'; + const sectionTitleElem = document.createElement('h5'); + sectionTitleElem.textContent = sectionTitle; + section.appendChild(sectionTitleElem); + + let row; + schedulers.forEach((scheduler, index) => { + if (index % 3 === 0) { + row = document.createElement('div'); + row.className = 'row'; + section.appendChild(row); + } + + const schedulerCard = document.createElement('div'); + schedulerCard.className = 'col-lg-4 col-md-6 col-sm-12 mb-4'; + + let cardClass = 'card-scheduler '; + if (scheduler.is_scheduler_disabled) { + cardClass += 'disabled'; + } else if (scheduler.last_run_state === null) { + cardClass += 'enabled'; + } else { + const logCounts = { + info: scheduler.last_run_state.log_count_info, warning: scheduler.last_run_state.log_count_warning, error: scheduler.last_run_state.log_count_error + }; + + const maxLogCount = Math.max(logCounts.info, logCounts.warning, logCounts.error); + if (maxLogCount === logCounts.error) { + cardClass += 'log-error'; + } else if (maxLogCount === logCounts.warning) { + cardClass += 'log-warning'; } else { - enabledWithoutStats.push(scheduler); + cardClass += 'log-info'; } } - }); - const createSchedulerSection = (schedulers, sectionTitle) => { - const section = document.createElement('div'); - section.className = 'scheduler-section'; - const sectionTitleElem = document.createElement('h5'); - sectionTitleElem.textContent = sectionTitle; - section.appendChild(sectionTitleElem); - - let row; - schedulers.forEach((scheduler, index) => { - if (index % 3 === 0) { - row = document.createElement('div'); - row.className = 'row'; - section.appendChild(row); - } - - const schedulerCard = document.createElement('div'); - schedulerCard.className = 'col-lg-4 col-md-6 col-sm-12 mb-4'; - - let cardClass = 'card-scheduler '; - if (scheduler.is_scheduler_disabled) { - cardClass += 'disabled'; - } else if (scheduler.last_run_state === null) { - cardClass += 'enabled'; - } else { - const logCounts = { - info: scheduler.last_run_state.log_count_info, - warning: scheduler.last_run_state.log_count_warning, - error: scheduler.last_run_state.log_count_error - }; - - const maxLogCount = Math.max(logCounts.info, logCounts.warning, logCounts.error); - if (maxLogCount === logCounts.error) { - cardClass += 'log-error'; - } else if (maxLogCount === logCounts.warning) { - cardClass += 'log-warning'; - } else { - cardClass += 'log-info'; - } - } - - const lastRunState = scheduler.last_run_state ? ` + const lastRunState = scheduler.last_run_state ? `

Items Scraped: ${scheduler.last_run_state.item_scraped_count}

Items Dropped: ${scheduler.last_run_state.item_dropped_count}

Info: ${scheduler.last_run_state.log_count_info}

@@ -86,7 +96,7 @@ document.addEventListener('DOMContentLoaded', function () {

Error: ${scheduler.last_run_state.log_count_error}

` : ''; - const cardContent = ` + const cardContent = `
${scheduler.name}

Last Run: ${scheduler.time_since_last_run}

@@ -94,397 +104,335 @@ document.addEventListener('DOMContentLoaded', function () { ${lastRunState}
`; - schedulerCard.innerHTML = cardContent; - row.appendChild(schedulerCard); - }); + schedulerCard.innerHTML = cardContent; + row.appendChild(schedulerCard); + }); - container.appendChild(section); - }; - - if (enabledWithStats.length > 0) { - createSchedulerSection(enabledWithStats, 'Enabled Schedulers with Stats'); - } - if (enabledWithoutStats.length > 0) { - createSchedulerSection(enabledWithoutStats, 'Enabled Schedulers without Stats'); - } - if (disabledWithStats.length > 0) { - createSchedulerSection(disabledWithStats, 'Disabled Schedulers with Stats'); - } - if (disabledWithoutStats.length > 0) { - createSchedulerSection(disabledWithoutStats, 'Disabled Schedulers without Stats'); - } + container.appendChild(section); }; - const renderMetadataCountsChart = async () => { - const data = await fetchData('/metrics/metadata'); - removeLoadingElement('metadataCountsSkeleton'); - const ctx = document.getElementById('metadataCountsChart').getContext('2d'); - new Chart(ctx, { - type: 'pie', - data: { - labels: ['Movies', 'Series', 'TV Channels'], - datasets: [{ - label: 'Metadata Counts', - data: [data.movies, data.series, data.tv_channels], - backgroundColor: [ - 'rgba(153, 102, 255, 0.6)', - 'rgba(54, 162, 235, 0.6)', - 'rgba(75, 192, 192, 0.6)' - ], - borderColor: [ - 'rgba(153, 102, 255, 1)', - 'rgba(54, 162, 235, 1)', - 'rgba(75, 192, 192, 1)' - ], - }] - }, - options: { - responsive: true, - maintainAspectRatio: false, - plugins: { - legend: { - display: false - }, - datalabels: { - formatter: (value, context) => { - return context.chart.data.labels[context.dataIndex] + ': ' + value.toLocaleString(); - }, - color: '#fff', - anchor: 'center', - align: 'center', - offset: 0, - borderRadius: 4, - backgroundColor: (context) => { - return context.dataset.backgroundColor; - }, - font: { - weight: 'bold' - } + if (enabledWithStats.length > 0) { + createSchedulerSection(enabledWithStats, 'Enabled Schedulers with Stats'); + } + if (enabledWithoutStats.length > 0) { + createSchedulerSection(enabledWithoutStats, 'Enabled Schedulers without Stats'); + } + if (disabledWithStats.length > 0) { + createSchedulerSection(disabledWithStats, 'Disabled Schedulers with Stats'); + } + if (disabledWithoutStats.length > 0) { + createSchedulerSection(disabledWithoutStats, 'Disabled Schedulers without Stats'); + } +}; + +const renderMetadataCountsChart = async () => { + const data = await fetchData('/metrics/metadata'); + removeLoadingElement('metadataCountsSkeleton'); + const ctx = document.getElementById('metadataCountsChart').getContext('2d'); + new Chart(ctx, { + type: 'pie', data: { + labels: ['Movies', 'Series', 'TV Channels'], datasets: [{ + label: 'Metadata Counts', + data: [data.movies, data.series, data.tv_channels], + backgroundColor: ['rgba(153, 102, 255, 0.6)', 'rgba(54, 162, 235, 0.6)', 'rgba(75, 192, 192, 0.6)'], + borderColor: ['rgba(153, 102, 255, 1)', 'rgba(54, 162, 235, 1)', 'rgba(75, 192, 192, 1)'], + }] + }, options: { + responsive: true, maintainAspectRatio: false, plugins: { + legend: { + display: false + }, datalabels: { + formatter: (value, context) => { + return context.chart.data.labels[context.dataIndex] + ': ' + value.toLocaleString(); + }, color: '#fff', anchor: 'center', align: 'center', offset: 0, borderRadius: 4, backgroundColor: (context) => { + return context.dataset.backgroundColor; + }, font: { + weight: 'bold' } } } - }); - }; + } + }); +}; - const renderTotalTorrentsCount = async () => { - const data = await fetchData('/metrics/torrents'); - removeLoadingElement('totalTorrentsSkeleton'); - const totalTorrentsValue = document.getElementById('totalTorrentsValue'); - totalTorrentsValue.textContent = `${data.total_torrents.toLocaleString()} (${data.total_torrents_readable})`; - }; +const renderTotalTorrentsCount = async () => { + const data = await fetchData('/metrics/torrents'); + removeLoadingElement('totalTorrentsSkeleton'); + const totalTorrentsValue = document.getElementById('totalTorrentsValue'); + totalTorrentsValue.textContent = `${data.total_torrents.toLocaleString()} (${data.total_torrents_readable})`; +}; - const renderTorrentSourcesChart = async () => { - const data = await fetchData('/metrics/torrents/sources'); - removeLoadingElement('torrentSourcesSkeleton'); - const ctx = document.getElementById('torrentSourcesChart').getContext('2d'); - new Chart(ctx, { - type: 'bar', - data: { - datasets: data.map(source => { - return { - label: source.name, - data: [{x: source.name, y: source.count}], - }; - }), - }, - options: { - grouped: false, - responsive: true, - maintainAspectRatio: false, - scales: { - y: { - beginAtZero: true, - display: false - }, - x: { - ticks: { - color: '#fff', - } - } - }, - layout: { - padding: { - top: 50 // Add padding to prevent label cutoff - } - }, - plugins: { - legend: { - display: false - }, - tooltip: { - callbacks: { - label: function (context) { - return `${context.raw.y.toLocaleString()} Torrents`; - } - } - }, - datalabels: { - align: 'end', - anchor: 'end', - rotation: 0, +const renderTorrentSourcesChart = async () => { + const data = await fetchData('/metrics/torrents/sources'); + removeLoadingElement('torrentSourcesSkeleton'); + const ctx = document.getElementById('torrentSourcesChart').getContext('2d'); + new Chart(ctx, { + type: 'bar', data: { + datasets: data.map(source => { + return { + label: source.name, data: [{x: source.name, y: source.count}], + }; + }), + }, options: { + grouped: false, responsive: true, maintainAspectRatio: false, scales: { + y: { + beginAtZero: true, display: false + }, x: { + ticks: { color: '#fff', - formatter: (value, context) => { - return value.y.toLocaleString(); - }, - font: { - weight: 'bold' - }, + } + } + }, layout: { + padding: { + top: 50 // Add padding to prevent label cutoff + } + }, plugins: { + legend: { + display: false + }, tooltip: { + callbacks: { + label: function (context) { + return `${context.raw.y.toLocaleString()} Torrents`; + } + } + }, datalabels: { + align: 'end', anchor: 'end', rotation: 0, color: '#fff', formatter: (value, context) => { + return value.y.toLocaleString(); + }, font: { + weight: 'bold' + }, + } + } + } + }); +}; + +const renderTorrentUploadersChart = async () => { + const data = await fetchData('/metrics/torrents/uploaders'); + removeLoadingElement('torrentUploadersSkeleton'); + const ctx = document.getElementById('torrentUploadersChart').getContext('2d'); + + const sortedData = data.sort((a, b) => b.count - a.count); + + new Chart(ctx, { + type: 'bar', data: { + labels: sortedData.map(item => item.name), datasets: [{ + data: sortedData.map(item => item.count), backgroundColor: 'rgba(75, 192, 192, 0.6)', borderColor: 'rgba(75, 192, 192, 1)', borderWidth: 1 + }] + }, options: { + indexAxis: 'y', responsive: true, maintainAspectRatio: false, layout: { + padding: { + right: 50 // Add padding to prevent label cutoff + } + }, plugins: { + legend: { + display: false + }, tooltip: { + callbacks: { + label: function (context) { + return `${context.raw.toLocaleString()} Torrents`; + } + } + }, datalabels: { + color: '#fff', anchor: 'end', align: 'end', formatter: (value) => value.toLocaleString(), font: { + weight: 'bold' + } + } + }, scales: { + x: { + beginAtZero: true, grid: { + color: 'rgba(255, 255, 255, 0.1)' + }, ticks: { + color: '#fff' + } + }, y: { + grid: { + display: false + }, ticks: { + color: '#fff' } } } - }); - }; + } + }); +}; - const renderTorrentUploadersChart = async () => { - const data = await fetchData('/metrics/torrents/uploaders'); - removeLoadingElement('torrentUploadersSkeleton'); - const ctx = document.getElementById('torrentUploadersChart').getContext('2d'); +const renderDebridCacheMetrics = async () => { + const data = await fetchData('/metrics/debrid-cache'); + removeLoadingElement('debridChartSkeleton'); - const sortedData = data.sort((a, b) => b.count - a.count); + // Create the bar chart for service comparison + const ctx = document.getElementById('debridCacheChart').getContext('2d'); - new Chart(ctx, { - type: 'bar', - data: { - labels: sortedData.map(item => item.name), - datasets: [{ - data: sortedData.map(item => item.count), - backgroundColor: 'rgba(75, 192, 192, 0.6)', - borderColor: 'rgba(75, 192, 192, 1)', - borderWidth: 1 - }] - }, - options: { - indexAxis: 'y', - responsive: true, - maintainAspectRatio: false, - layout: { - padding: { - right: 50 // Add padding to prevent label cutoff + // Prepare data for the chart + const services = Object.keys(data.services); + const cachedTorrents = services.map(service => data.services[service].cached_torrents); + + new Chart(ctx, { + type: 'bar', data: { + labels: services, datasets: [{ + label: 'Cached Torrents', data: cachedTorrents, backgroundColor: 'rgba(54, 162, 235, 0.6)', borderColor: 'rgba(54, 162, 235, 1)', borderWidth: 1 + }] + }, options: { + responsive: true, maintainAspectRatio: false, plugins: { + legend: { + display: false + }, tooltip: { + callbacks: { + label: function (context) { + return `Cached Torrents: ${context.raw.toLocaleString()}`; + } } - }, - plugins: { - legend: { + }, datalabels: { + color: '#fff', anchor: 'end', align: 'end', formatter: (value) => value.toLocaleString(), font: { + weight: 'bold' + } + } + }, scales: { + y: { + beginAtZero: true, title: { + display: true, text: 'Number of Cached Torrents', color: '#fff' + }, ticks: { + color: '#fff' + }, grid: { + color: 'rgba(255, 255, 255, 0.1)' + } + }, x: { + ticks: { + color: '#fff' + }, grid: { display: false - }, - tooltip: { - callbacks: { - label: function (context) { - return `${context.raw.toLocaleString()} Torrents`; - } - } - }, - datalabels: { - color: '#fff', - anchor: 'end', - align: 'end', - formatter: (value) => value.toLocaleString(), - font: { - weight: 'bold' - } - } - }, - scales: { - x: { - beginAtZero: true, - grid: { - color: 'rgba(255, 255, 255, 0.1)' - }, - ticks: { - color: '#fff' - } - }, - y: { - grid: { - display: false - }, - ticks: { - color: '#fff' - } } } } - }); - }; + } + }); +}; - const renderWeeklyUploadersChart = async () => { - const data = await fetchData('/metrics/torrents/uploaders/weekly'); - removeLoadingElement('weeklyUploadersSkeleton'); + +const renderWeeklyUploadersChart = async (selectedDate = null) => { + if (!selectedDate) { + selectedDate = new Date(); + } + + const startOfWeek = getStartOfWeek(selectedDate); + const formattedDate = formatDateToYYYYMMDD(startOfWeek); + + try { + // Show loading state + document.getElementById('weeklyUploadersSkeleton').style.display = 'block'; + if (weeklyUploadersChart) { + weeklyUploadersChart.destroy(); + } + + const data = await fetchData(`/metrics/torrents/uploaders/weekly/${formattedDate}`); + + if (data.error) { + console.error('Error fetching data:', data.error); + return; + } // Update date range display const weekStart = new Date(data.week_start); - const weekEnd = new Date(weekStart); - weekEnd.setDate(weekEnd.getDate() + 6); + const weekEnd = new Date(data.week_end); const dateRangeElement = document.getElementById('weekDateRange'); dateRangeElement.innerHTML = ` - - ${weekStart.toLocaleDateString()} - ${weekEnd.toLocaleDateString()} - - `; + + ${weekStart.toLocaleDateString()} - ${weekEnd.toLocaleDateString()} + + `; + + // Update week selector value + document.getElementById('weekSelector').value = formattedDate; const ctx = document.getElementById('weeklyUploadersChart').getContext('2d'); - new Chart(ctx, { - type: 'bar', - data: { - labels: data.uploaders.map(uploader => uploader.name), - datasets: [{ - data: data.uploaders.map(uploader => uploader.count), - backgroundColor: 'rgba(147, 112, 219, 0.6)', - borderColor: 'rgba(147, 112, 219, 1)', - borderWidth: 1 + weeklyUploadersChart = new Chart(ctx, { + type: 'bar', data: { + labels: data.uploaders.map(uploader => uploader.name), datasets: [{ + data: data.uploaders.map(uploader => uploader.count), backgroundColor: 'rgba(147, 112, 219, 0.6)', borderColor: 'rgba(147, 112, 219, 1)', borderWidth: 1 }] - }, - options: { - indexAxis: 'y', - responsive: true, - maintainAspectRatio: false, - layout: { + }, options: { + indexAxis: 'y', responsive: true, maintainAspectRatio: false, layout: { padding: { right: 50 } - }, - plugins: { + }, plugins: { legend: { display: false - }, - tooltip: { + }, tooltip: { callbacks: { label: function (context) { const uploader = data.uploaders[context.dataIndex]; const lastUpload = new Date(uploader.latest_upload); - return [ - `Torrents: ${context.raw}`, - `Last Upload: ${lastUpload.toLocaleDateString()}` - ]; + return [`Torrents: ${context.raw}`, `Last Upload: ${lastUpload.toLocaleDateString()}`]; } } - }, - datalabels: { - color: '#fff', - anchor: 'end', - align: 'right', - formatter: (value) => value, - font: { - weight: 'bold', - size: 12 - }, - padding: { + }, datalabels: { + color: '#fff', anchor: 'end', align: 'right', formatter: (value) => value, font: { + weight: 'bold', size: 12 + }, padding: { right: 6 } } - }, - scales: { + }, scales: { x: { - beginAtZero: true, - grid: { + beginAtZero: true, grid: { color: 'rgba(255, 255, 255, 0.1)' - }, - ticks: { - color: '#fff', - callback: function (value) { + }, ticks: { + color: '#fff', callback: function (value) { return value.toLocaleString(); - }, - padding: 10 // Add padding to axis ticks - }, - // Ensure the axis extends a bit beyond the max value - suggestedMax: function (context) { - const max = Math.max(...context.chart.data.datasets[0].data); - return max * 1.1; // Extend 10% beyond the max value - } - }, - y: { - grid: { - display: false, - borderWidth: 0 - }, - ticks: { - color: '#fff', - font: { - size: 12 - }, - padding: 10 - } - } - } - } - }); - }; - - const renderDebridCacheMetrics = async () => { - const data = await fetchData('/metrics/debrid-cache'); - removeLoadingElement('debridChartSkeleton'); - - // Create the bar chart for service comparison - const ctx = document.getElementById('debridCacheChart').getContext('2d'); - - // Prepare data for the chart - const services = Object.keys(data.services); - const cachedTorrents = services.map(service => data.services[service].cached_torrents); - - new Chart(ctx, { - type: 'bar', - data: { - labels: services, - datasets: [{ - label: 'Cached Torrents', - data: cachedTorrents, - backgroundColor: 'rgba(54, 162, 235, 0.6)', - borderColor: 'rgba(54, 162, 235, 1)', - borderWidth: 1 - }] - }, - options: { - responsive: true, - maintainAspectRatio: false, - plugins: { - legend: { - display: false - }, - tooltip: { - callbacks: { - label: function (context) { - return `Cached Torrents: ${context.raw.toLocaleString()}`; } } - }, - datalabels: { - color: '#fff', - anchor: 'end', - align: 'end', - formatter: (value) => value.toLocaleString(), - font: { - weight: 'bold' - } - } - }, - scales: { - y: { - beginAtZero: true, - title: { - display: true, - text: 'Number of Cached Torrents', - color: '#fff' - }, - ticks: { - color: '#fff' - }, - grid: { - color: 'rgba(255, 255, 255, 0.1)' - } - }, - x: { - ticks: { - color: '#fff' - }, + }, y: { grid: { display: false + }, ticks: { + color: '#fff' } } } } }); - }; + } finally { + // Hide loading state + document.getElementById('weeklyUploadersSkeleton').style.display = 'none'; + } +}; + +document.addEventListener('DOMContentLoaded', function () { + const weekSelector = document.getElementById('weekSelector'); + const prevWeekBtn = document.getElementById('prevWeek'); + const nextWeekBtn = document.getElementById('nextWeek'); + + // Set initial date to start of current week + const currentDate = new Date(); + const startOfWeek = getStartOfWeek(currentDate); + weekSelector.value = formatDateToYYYYMMDD(startOfWeek); + + // Event listeners for navigation + weekSelector.addEventListener('change', (e) => { + renderWeeklyUploadersChart(new Date(e.target.value)); + }); + + prevWeekBtn.addEventListener('click', () => { + const currentDate = new Date(weekSelector.value); + const prevWeek = new Date(currentDate.setDate(currentDate.getDate() - 7)); + renderWeeklyUploadersChart(prevWeek); + }); + + nextWeekBtn.addEventListener('click', () => { + const currentDate = new Date(weekSelector.value); + const nextWeek = new Date(currentDate.setDate(currentDate.getDate() + 7)); + renderWeeklyUploadersChart(nextWeek); + }); + + // Initial render + renderWeeklyUploadersChart(); +}); + +document.addEventListener('DOMContentLoaded', function () { const initCharts = () => { renderSchedulerDetails(); @@ -492,7 +440,6 @@ document.addEventListener('DOMContentLoaded', function () { renderTotalTorrentsCount(); renderTorrentSourcesChart(); renderTorrentUploadersChart(); - renderWeeklyUploadersChart(); renderDebridCacheMetrics(); };