mirror of
https://github.com/Viren070/MediaFusion.git
synced 2025-12-01 23:21:11 +01:00
feat: add week selector for weekly top uploaders metrics
This commit is contained in:
+91
-72
@@ -186,92 +186,111 @@ async def get_torrents_by_uploaders(response: Response):
|
|||||||
return uploader_stats
|
return uploader_stats
|
||||||
|
|
||||||
|
|
||||||
@metrics_router.get("/torrents/uploaders/weekly", tags=["metrics"])
|
@metrics_router.get("/torrents/uploaders/weekly/{week_date}", tags=["metrics"])
|
||||||
async def get_weekly_top_uploaders(response: Response):
|
async def get_weekly_top_uploaders(week_date: str, response: Response):
|
||||||
response.headers.update(const.NO_CACHE_HEADERS)
|
response.headers.update(const.NO_CACHE_HEADERS)
|
||||||
|
|
||||||
# Calculate the start of the current week (Monday)
|
try:
|
||||||
today = datetime.now(tz=timezone.utc)
|
# Parse the input date
|
||||||
start_of_week = today - timedelta(days=today.weekday())
|
selected_date = datetime.strptime(week_date, "%Y-%m-%d")
|
||||||
start_of_week = start_of_week.replace(hour=0, minute=0, second=0, microsecond=0)
|
selected_date = selected_date.replace(tzinfo=timezone.utc)
|
||||||
|
|
||||||
# Get raw aggregation results
|
# Calculate the start of the week (Monday) for the selected date
|
||||||
raw_results = await TorrentStreams.aggregate(
|
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)
|
||||||
{
|
|
||||||
"$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()
|
|
||||||
|
|
||||||
# Process and clean the results
|
# Calculate end of week
|
||||||
processed_results = []
|
end_of_week = start_of_week + timedelta(days=7)
|
||||||
anonymous_total = 0
|
|
||||||
anonymous_latest = None
|
|
||||||
|
|
||||||
for stat in raw_results:
|
# Get raw aggregation results
|
||||||
uploader_name = stat["_id"]
|
raw_results = await TorrentStreams.aggregate(
|
||||||
count = stat["count"]
|
[
|
||||||
latest_upload = stat["latest_upload"]
|
{
|
||||||
|
"$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
|
# Process and clean the results (keeping existing logic)
|
||||||
if count <= 0:
|
processed_results = []
|
||||||
continue
|
anonymous_total = 0
|
||||||
|
anonymous_latest = None
|
||||||
|
|
||||||
# Combine all anonymous-like entries
|
for stat in raw_results:
|
||||||
if (
|
uploader_name = stat["_id"]
|
||||||
uploader_name is None
|
count = stat["count"]
|
||||||
or uploader_name.strip() == ""
|
latest_upload = stat["latest_upload"]
|
||||||
or uploader_name.lower() == "anonymous"
|
|
||||||
):
|
if count <= 0:
|
||||||
anonymous_total += count
|
continue
|
||||||
if anonymous_latest is None or (
|
|
||||||
latest_upload and latest_upload > anonymous_latest
|
if (
|
||||||
|
uploader_name is None
|
||||||
|
or uploader_name.strip() == ""
|
||||||
|
or uploader_name.lower() == "anonymous"
|
||||||
):
|
):
|
||||||
anonymous_latest = latest_upload
|
anonymous_total += count
|
||||||
else:
|
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(
|
processed_results.append(
|
||||||
{
|
{
|
||||||
"name": uploader_name.strip(),
|
"name": "Anonymous",
|
||||||
"count": count,
|
"count": anonymous_total,
|
||||||
"latest_upload": (
|
"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
|
final_results = sorted(
|
||||||
if anonymous_total > 0:
|
processed_results, key=lambda x: x["count"], reverse=True
|
||||||
processed_results.append(
|
)[:20]
|
||||||
{
|
|
||||||
"name": "Anonymous",
|
|
||||||
"count": anonymous_total,
|
|
||||||
"latest_upload": (
|
|
||||||
anonymous_latest.strftime("%Y-%m-%d") if anonymous_latest else None
|
|
||||||
),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Sort by count and limit to top 20
|
return {
|
||||||
final_results = sorted(processed_results, key=lambda x: x["count"], reverse=True)[
|
"week_start": start_of_week.strftime("%Y-%m-%d"),
|
||||||
:20
|
"week_end": end_of_week.strftime("%Y-%m-%d"),
|
||||||
]
|
"uploaders": final_results,
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
except ValueError as e:
|
||||||
"week_start": start_of_week.strftime("%Y-%m-%d"),
|
return {
|
||||||
"uploaders": final_results,
|
"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)}"}
|
||||||
|
|||||||
@@ -58,12 +58,86 @@ h4 {
|
|||||||
transform: translateY(-5px);
|
transform: translateY(-5px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.date-info {
|
.week-selector {
|
||||||
background: rgba(129,87,208,0.6);
|
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;
|
padding: 8px 16px;
|
||||||
border-radius: 4px;
|
border-radius: 6px;
|
||||||
text-align: center;
|
|
||||||
font-size: 0.9rem;
|
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 {
|
.chart-wrapper {
|
||||||
@@ -229,6 +303,17 @@ h4 {
|
|||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.week-selector {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.week-selector button,
|
||||||
|
.week-selector input[type="date"] {
|
||||||
|
width: 100%;
|
||||||
|
min-width: unset;
|
||||||
|
}
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,7 +64,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="chart-section">
|
<div class="chart-section">
|
||||||
<h4 class="text-left">Top 20 Contribution Streams Uploaders This Week</h4>
|
<h4 class="text-left">Top 20 Contribution Streams Uploaders</h4>
|
||||||
|
<div class="week-selector mb-3">
|
||||||
|
<button class="btn btn-outline-light btn-sm" id="prevWeek">Previous Week</button>
|
||||||
|
<input type="date" id="weekSelector" class="form-control mx-2">
|
||||||
|
<button class="btn btn-outline-light btn-sm" id="nextWeek">Next Week</button>
|
||||||
|
</div>
|
||||||
<div class="date-info mb-3" id="weekDateRange"></div>
|
<div class="date-info mb-3" id="weekDateRange"></div>
|
||||||
<div class="chart-wrapper">
|
<div class="chart-wrapper">
|
||||||
<canvas id="weeklyUploadersChart"></canvas>
|
<canvas id="weeklyUploadersChart"></canvas>
|
||||||
|
|||||||
+355
-408
@@ -1,84 +1,94 @@
|
|||||||
document.addEventListener('DOMContentLoaded', function () {
|
let weeklyUploadersChart = null;
|
||||||
const fetchData = async (url) => {
|
|
||||||
const response = await fetch(url);
|
|
||||||
return response.json();
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeLoadingElement = (spinnerId) => {
|
const formatDateToYYYYMMDD = (date) => {
|
||||||
const spinner = document.getElementById(spinnerId);
|
return date.toISOString().split('T')[0];
|
||||||
if (spinner) {
|
};
|
||||||
spinner.remove();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderSchedulerDetails = async () => {
|
const getStartOfWeek = (date) => {
|
||||||
const data = await fetchData('/metrics/scrapy-schedulers');
|
const d = new Date(date);
|
||||||
removeLoadingElement('schedulerChartsSkeleton');
|
const day = d.getDay();
|
||||||
const container = document.getElementById('schedulerChartsContainer');
|
const diff = d.getDate() - day + (day === 0 ? -6 : 1);
|
||||||
container.innerHTML = ''; // Clear loading skeleton
|
return new Date(d.setDate(diff));
|
||||||
|
};
|
||||||
|
|
||||||
const enabledWithStats = [];
|
const fetchData = async (url) => {
|
||||||
const enabledWithoutStats = [];
|
const response = await fetch(url);
|
||||||
const disabledWithStats = [];
|
return response.json();
|
||||||
const disabledWithoutStats = [];
|
};
|
||||||
|
|
||||||
data.forEach(scheduler => {
|
const removeLoadingElement = (spinnerId) => {
|
||||||
if (scheduler.is_scheduler_disabled) {
|
const spinner = document.getElementById(spinnerId);
|
||||||
if (scheduler.last_run_state) {
|
if (spinner) {
|
||||||
disabledWithStats.push(scheduler);
|
spinner.remove();
|
||||||
} else {
|
}
|
||||||
disabledWithoutStats.push(scheduler);
|
};
|
||||||
}
|
|
||||||
|
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 {
|
} else {
|
||||||
if (scheduler.last_run_state) {
|
disabledWithoutStats.push(scheduler);
|
||||||
enabledWithStats.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 {
|
} else {
|
||||||
enabledWithoutStats.push(scheduler);
|
cardClass += 'log-info';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
const createSchedulerSection = (schedulers, sectionTitle) => {
|
const lastRunState = scheduler.last_run_state ? `
|
||||||
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 ? `
|
|
||||||
<p>Items Scraped: ${scheduler.last_run_state.item_scraped_count}</p>
|
<p>Items Scraped: ${scheduler.last_run_state.item_scraped_count}</p>
|
||||||
<p>Items Dropped: ${scheduler.last_run_state.item_dropped_count}</p>
|
<p>Items Dropped: ${scheduler.last_run_state.item_dropped_count}</p>
|
||||||
<p class="text-info">Info: ${scheduler.last_run_state.log_count_info}</p>
|
<p class="text-info">Info: ${scheduler.last_run_state.log_count_info}</p>
|
||||||
@@ -86,7 +96,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
<p class="text-danger">Error: ${scheduler.last_run_state.log_count_error}</p>
|
<p class="text-danger">Error: ${scheduler.last_run_state.log_count_error}</p>
|
||||||
` : '';
|
` : '';
|
||||||
|
|
||||||
const cardContent = `
|
const cardContent = `
|
||||||
<div class="${cardClass}">
|
<div class="${cardClass}">
|
||||||
<h5>${scheduler.name}</h5>
|
<h5>${scheduler.name}</h5>
|
||||||
<p>Last Run: ${scheduler.time_since_last_run}</p>
|
<p>Last Run: ${scheduler.time_since_last_run}</p>
|
||||||
@@ -94,397 +104,335 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
${lastRunState}
|
${lastRunState}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
schedulerCard.innerHTML = cardContent;
|
schedulerCard.innerHTML = cardContent;
|
||||||
row.appendChild(schedulerCard);
|
row.appendChild(schedulerCard);
|
||||||
});
|
});
|
||||||
|
|
||||||
container.appendChild(section);
|
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');
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderMetadataCountsChart = async () => {
|
if (enabledWithStats.length > 0) {
|
||||||
const data = await fetchData('/metrics/metadata');
|
createSchedulerSection(enabledWithStats, 'Enabled Schedulers with Stats');
|
||||||
removeLoadingElement('metadataCountsSkeleton');
|
}
|
||||||
const ctx = document.getElementById('metadataCountsChart').getContext('2d');
|
if (enabledWithoutStats.length > 0) {
|
||||||
new Chart(ctx, {
|
createSchedulerSection(enabledWithoutStats, 'Enabled Schedulers without Stats');
|
||||||
type: 'pie',
|
}
|
||||||
data: {
|
if (disabledWithStats.length > 0) {
|
||||||
labels: ['Movies', 'Series', 'TV Channels'],
|
createSchedulerSection(disabledWithStats, 'Disabled Schedulers with Stats');
|
||||||
datasets: [{
|
}
|
||||||
label: 'Metadata Counts',
|
if (disabledWithoutStats.length > 0) {
|
||||||
data: [data.movies, data.series, data.tv_channels],
|
createSchedulerSection(disabledWithoutStats, 'Disabled Schedulers without Stats');
|
||||||
backgroundColor: [
|
}
|
||||||
'rgba(153, 102, 255, 0.6)',
|
};
|
||||||
'rgba(54, 162, 235, 0.6)',
|
|
||||||
'rgba(75, 192, 192, 0.6)'
|
const renderMetadataCountsChart = async () => {
|
||||||
],
|
const data = await fetchData('/metrics/metadata');
|
||||||
borderColor: [
|
removeLoadingElement('metadataCountsSkeleton');
|
||||||
'rgba(153, 102, 255, 1)',
|
const ctx = document.getElementById('metadataCountsChart').getContext('2d');
|
||||||
'rgba(54, 162, 235, 1)',
|
new Chart(ctx, {
|
||||||
'rgba(75, 192, 192, 1)'
|
type: 'pie', data: {
|
||||||
],
|
labels: ['Movies', 'Series', 'TV Channels'], datasets: [{
|
||||||
}]
|
label: 'Metadata Counts',
|
||||||
},
|
data: [data.movies, data.series, data.tv_channels],
|
||||||
options: {
|
backgroundColor: ['rgba(153, 102, 255, 0.6)', 'rgba(54, 162, 235, 0.6)', 'rgba(75, 192, 192, 0.6)'],
|
||||||
responsive: true,
|
borderColor: ['rgba(153, 102, 255, 1)', 'rgba(54, 162, 235, 1)', 'rgba(75, 192, 192, 1)'],
|
||||||
maintainAspectRatio: false,
|
}]
|
||||||
plugins: {
|
}, options: {
|
||||||
legend: {
|
responsive: true, maintainAspectRatio: false, plugins: {
|
||||||
display: false
|
legend: {
|
||||||
},
|
display: false
|
||||||
datalabels: {
|
}, datalabels: {
|
||||||
formatter: (value, context) => {
|
formatter: (value, context) => {
|
||||||
return context.chart.data.labels[context.dataIndex] + ': ' + value.toLocaleString();
|
return context.chart.data.labels[context.dataIndex] + ': ' + value.toLocaleString();
|
||||||
},
|
}, color: '#fff', anchor: 'center', align: 'center', offset: 0, borderRadius: 4, backgroundColor: (context) => {
|
||||||
color: '#fff',
|
return context.dataset.backgroundColor;
|
||||||
anchor: 'center',
|
}, font: {
|
||||||
align: 'center',
|
weight: 'bold'
|
||||||
offset: 0,
|
|
||||||
borderRadius: 4,
|
|
||||||
backgroundColor: (context) => {
|
|
||||||
return context.dataset.backgroundColor;
|
|
||||||
},
|
|
||||||
font: {
|
|
||||||
weight: 'bold'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
};
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const renderTotalTorrentsCount = async () => {
|
const renderTotalTorrentsCount = async () => {
|
||||||
const data = await fetchData('/metrics/torrents');
|
const data = await fetchData('/metrics/torrents');
|
||||||
removeLoadingElement('totalTorrentsSkeleton');
|
removeLoadingElement('totalTorrentsSkeleton');
|
||||||
const totalTorrentsValue = document.getElementById('totalTorrentsValue');
|
const totalTorrentsValue = document.getElementById('totalTorrentsValue');
|
||||||
totalTorrentsValue.textContent = `${data.total_torrents.toLocaleString()} (${data.total_torrents_readable})`;
|
totalTorrentsValue.textContent = `${data.total_torrents.toLocaleString()} (${data.total_torrents_readable})`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderTorrentSourcesChart = async () => {
|
const renderTorrentSourcesChart = async () => {
|
||||||
const data = await fetchData('/metrics/torrents/sources');
|
const data = await fetchData('/metrics/torrents/sources');
|
||||||
removeLoadingElement('torrentSourcesSkeleton');
|
removeLoadingElement('torrentSourcesSkeleton');
|
||||||
const ctx = document.getElementById('torrentSourcesChart').getContext('2d');
|
const ctx = document.getElementById('torrentSourcesChart').getContext('2d');
|
||||||
new Chart(ctx, {
|
new Chart(ctx, {
|
||||||
type: 'bar',
|
type: 'bar', data: {
|
||||||
data: {
|
datasets: data.map(source => {
|
||||||
datasets: data.map(source => {
|
return {
|
||||||
return {
|
label: source.name, data: [{x: source.name, y: source.count}],
|
||||||
label: source.name,
|
};
|
||||||
data: [{x: source.name, y: source.count}],
|
}),
|
||||||
};
|
}, options: {
|
||||||
}),
|
grouped: false, responsive: true, maintainAspectRatio: false, scales: {
|
||||||
},
|
y: {
|
||||||
options: {
|
beginAtZero: true, display: false
|
||||||
grouped: false,
|
}, x: {
|
||||||
responsive: true,
|
ticks: {
|
||||||
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,
|
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
formatter: (value, context) => {
|
}
|
||||||
return value.y.toLocaleString();
|
}
|
||||||
},
|
}, layout: {
|
||||||
font: {
|
padding: {
|
||||||
weight: 'bold'
|
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 renderDebridCacheMetrics = async () => {
|
||||||
const data = await fetchData('/metrics/torrents/uploaders');
|
const data = await fetchData('/metrics/debrid-cache');
|
||||||
removeLoadingElement('torrentUploadersSkeleton');
|
removeLoadingElement('debridChartSkeleton');
|
||||||
const ctx = document.getElementById('torrentUploadersChart').getContext('2d');
|
|
||||||
|
|
||||||
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, {
|
// Prepare data for the chart
|
||||||
type: 'bar',
|
const services = Object.keys(data.services);
|
||||||
data: {
|
const cachedTorrents = services.map(service => data.services[service].cached_torrents);
|
||||||
labels: sortedData.map(item => item.name),
|
|
||||||
datasets: [{
|
new Chart(ctx, {
|
||||||
data: sortedData.map(item => item.count),
|
type: 'bar', data: {
|
||||||
backgroundColor: 'rgba(75, 192, 192, 0.6)',
|
labels: services, datasets: [{
|
||||||
borderColor: 'rgba(75, 192, 192, 1)',
|
label: 'Cached Torrents', data: cachedTorrents, backgroundColor: 'rgba(54, 162, 235, 0.6)', borderColor: 'rgba(54, 162, 235, 1)', borderWidth: 1
|
||||||
borderWidth: 1
|
}]
|
||||||
}]
|
}, options: {
|
||||||
},
|
responsive: true, maintainAspectRatio: false, plugins: {
|
||||||
options: {
|
legend: {
|
||||||
indexAxis: 'y',
|
display: false
|
||||||
responsive: true,
|
}, tooltip: {
|
||||||
maintainAspectRatio: false,
|
callbacks: {
|
||||||
layout: {
|
label: function (context) {
|
||||||
padding: {
|
return `Cached Torrents: ${context.raw.toLocaleString()}`;
|
||||||
right: 50 // Add padding to prevent label cutoff
|
}
|
||||||
}
|
}
|
||||||
},
|
}, datalabels: {
|
||||||
plugins: {
|
color: '#fff', anchor: 'end', align: 'end', formatter: (value) => value.toLocaleString(), font: {
|
||||||
legend: {
|
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
|
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');
|
const renderWeeklyUploadersChart = async (selectedDate = null) => {
|
||||||
removeLoadingElement('weeklyUploadersSkeleton');
|
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
|
// Update date range display
|
||||||
const weekStart = new Date(data.week_start);
|
const weekStart = new Date(data.week_start);
|
||||||
const weekEnd = new Date(weekStart);
|
const weekEnd = new Date(data.week_end);
|
||||||
weekEnd.setDate(weekEnd.getDate() + 6);
|
|
||||||
|
|
||||||
const dateRangeElement = document.getElementById('weekDateRange');
|
const dateRangeElement = document.getElementById('weekDateRange');
|
||||||
dateRangeElement.innerHTML = `
|
dateRangeElement.innerHTML = `
|
||||||
<span class="text-white">
|
<span class="text-white">
|
||||||
${weekStart.toLocaleDateString()} - ${weekEnd.toLocaleDateString()}
|
${weekStart.toLocaleDateString()} - ${weekEnd.toLocaleDateString()}
|
||||||
</span>
|
</span>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// Update week selector value
|
||||||
|
document.getElementById('weekSelector').value = formattedDate;
|
||||||
|
|
||||||
const ctx = document.getElementById('weeklyUploadersChart').getContext('2d');
|
const ctx = document.getElementById('weeklyUploadersChart').getContext('2d');
|
||||||
|
|
||||||
new Chart(ctx, {
|
weeklyUploadersChart = new Chart(ctx, {
|
||||||
type: 'bar',
|
type: 'bar', data: {
|
||||||
data: {
|
labels: data.uploaders.map(uploader => uploader.name), datasets: [{
|
||||||
labels: data.uploaders.map(uploader => uploader.name),
|
data: data.uploaders.map(uploader => uploader.count), backgroundColor: 'rgba(147, 112, 219, 0.6)', borderColor: 'rgba(147, 112, 219, 1)', borderWidth: 1
|
||||||
datasets: [{
|
|
||||||
data: data.uploaders.map(uploader => uploader.count),
|
|
||||||
backgroundColor: 'rgba(147, 112, 219, 0.6)',
|
|
||||||
borderColor: 'rgba(147, 112, 219, 1)',
|
|
||||||
borderWidth: 1
|
|
||||||
}]
|
}]
|
||||||
},
|
}, options: {
|
||||||
options: {
|
indexAxis: 'y', responsive: true, maintainAspectRatio: false, layout: {
|
||||||
indexAxis: 'y',
|
|
||||||
responsive: true,
|
|
||||||
maintainAspectRatio: false,
|
|
||||||
layout: {
|
|
||||||
padding: {
|
padding: {
|
||||||
right: 50
|
right: 50
|
||||||
}
|
}
|
||||||
},
|
}, plugins: {
|
||||||
plugins: {
|
|
||||||
legend: {
|
legend: {
|
||||||
display: false
|
display: false
|
||||||
},
|
}, tooltip: {
|
||||||
tooltip: {
|
|
||||||
callbacks: {
|
callbacks: {
|
||||||
label: function (context) {
|
label: function (context) {
|
||||||
const uploader = data.uploaders[context.dataIndex];
|
const uploader = data.uploaders[context.dataIndex];
|
||||||
const lastUpload = new Date(uploader.latest_upload);
|
const lastUpload = new Date(uploader.latest_upload);
|
||||||
return [
|
return [`Torrents: ${context.raw}`, `Last Upload: ${lastUpload.toLocaleDateString()}`];
|
||||||
`Torrents: ${context.raw}`,
|
|
||||||
`Last Upload: ${lastUpload.toLocaleDateString()}`
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}, datalabels: {
|
||||||
datalabels: {
|
color: '#fff', anchor: 'end', align: 'right', formatter: (value) => value, font: {
|
||||||
color: '#fff',
|
weight: 'bold', size: 12
|
||||||
anchor: 'end',
|
}, padding: {
|
||||||
align: 'right',
|
|
||||||
formatter: (value) => value,
|
|
||||||
font: {
|
|
||||||
weight: 'bold',
|
|
||||||
size: 12
|
|
||||||
},
|
|
||||||
padding: {
|
|
||||||
right: 6
|
right: 6
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}, scales: {
|
||||||
scales: {
|
|
||||||
x: {
|
x: {
|
||||||
beginAtZero: true,
|
beginAtZero: true, grid: {
|
||||||
grid: {
|
|
||||||
color: 'rgba(255, 255, 255, 0.1)'
|
color: 'rgba(255, 255, 255, 0.1)'
|
||||||
},
|
}, ticks: {
|
||||||
ticks: {
|
color: '#fff', callback: function (value) {
|
||||||
color: '#fff',
|
|
||||||
callback: function (value) {
|
|
||||||
return value.toLocaleString();
|
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()}`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}, y: {
|
||||||
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: {
|
grid: {
|
||||||
display: false
|
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 = () => {
|
const initCharts = () => {
|
||||||
renderSchedulerDetails();
|
renderSchedulerDetails();
|
||||||
@@ -492,7 +440,6 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
renderTotalTorrentsCount();
|
renderTotalTorrentsCount();
|
||||||
renderTorrentSourcesChart();
|
renderTorrentSourcesChart();
|
||||||
renderTorrentUploadersChart();
|
renderTorrentUploadersChart();
|
||||||
renderWeeklyUploadersChart();
|
|
||||||
renderDebridCacheMetrics();
|
renderDebridCacheMetrics();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user