update to web interface
Local Docker Build (Dev) / build-dev (push) Successful in 5s

This commit is contained in:
2025-11-03 09:30:50 -05:00
parent d414da5bf3
commit c2c720cd8b
4 changed files with 181 additions and 26 deletions
+8
View File
@@ -45,15 +45,23 @@ def map_source_to_description(source: str) -> str:
elif "omdb:" in source_lower: elif "omdb:" in source_lower:
return "OMDb Release" return "OMDb Release"
# Sonarr sources
elif "sonarr:" in source_lower:
return "Sonarr API"
# Manual and other sources # Manual and other sources
elif "manual" in source_lower: elif "manual" in source_lower:
return "Manual Entry" return "Manual Entry"
elif "digital_release" in source_lower: elif "digital_release" in source_lower:
return "Digital Release" return "Digital Release"
elif "nfo_file_existing" in source_lower:
return "NFO File (Legacy)"
elif "nfo:" in source_lower: elif "nfo:" in source_lower:
return "NFO File" return "NFO File"
elif "webhook:" in source_lower: elif "webhook:" in source_lower:
return "Webhook/API" return "Webhook/API"
elif "database" in source_lower:
return "Database"
# Fallback for unknown patterns # Fallback for unknown patterns
return source.title() return source.title()
+18 -3
View File
@@ -48,15 +48,23 @@ def map_source_to_description(source: str) -> str:
elif "omdb:" in source_lower: elif "omdb:" in source_lower:
return "OMDb Release" return "OMDb Release"
# Sonarr sources
elif "sonarr:" in source_lower:
return "Sonarr API"
# Manual and other sources # Manual and other sources
elif "manual" in source_lower: elif "manual" in source_lower:
return "Manual Entry" return "Manual Entry"
elif "digital_release" in source_lower: elif "digital_release" in source_lower:
return "Digital Release" return "Digital Release"
elif "nfo_file_existing" in source_lower:
return "NFO File (Legacy)"
elif "nfo:" in source_lower: elif "nfo:" in source_lower:
return "NFO File" return "NFO File"
elif "webhook:" in source_lower: elif "webhook:" in source_lower:
return "Webhook/API" return "Webhook/API"
elif "database" in source_lower:
return "Database"
# Fallback for unknown patterns # Fallback for unknown patterns
return source.title() return source.title()
@@ -469,10 +477,17 @@ async def get_dashboard_stats(dependencies: dict):
cursor.execute("SELECT COUNT(*) FROM episodes WHERE source = 'no_valid_date_source'") cursor.execute("SELECT COUNT(*) FROM episodes WHERE source = 'no_valid_date_source'")
episodes_no_valid_source = db._get_first_value(cursor.fetchone()) episodes_no_valid_source = db._get_first_value(cursor.fetchone())
# Recent activity (last 7 days) # Recent activity (last 7 days) - count items processed, not history events
cursor.execute(""" cursor.execute("""
SELECT COUNT(*) FROM processing_history SELECT COUNT(*) FROM (
WHERE processed_at > NOW() - INTERVAL '7 days' SELECT imdb_id FROM movies
WHERE created_at > NOW() - INTERVAL '7 days'
OR updated_at > NOW() - INTERVAL '7 days'
UNION
SELECT DISTINCT imdb_id FROM episodes
WHERE created_at > NOW() - INTERVAL '7 days'
OR updated_at > NOW() - INTERVAL '7 days'
) AS recent_items
""") """)
recent_activity = db._get_first_value(cursor.fetchone()) recent_activity = db._get_first_value(cursor.fetchone())
+38 -14
View File
@@ -139,16 +139,30 @@
</div> </div>
<div class="table-container"> <div class="table-container">
<table class="data-table" id="movies-table"> <table class="data-table sortable-table" id="movies-table">
<thead> <thead>
<tr> <tr>
<th>Title</th> <th class="sortable" onclick="sortTable('movies-tbody', 0, 'text')" style="cursor: pointer;">
<th>IMDb ID</th> Title <i class="fas fa-sort"></i>
<th>Movie Released</th> </th>
<th>Date Added to Library</th> <th class="sortable" onclick="sortTable('movies-tbody', 1, 'text')" style="cursor: pointer;">
<th>Source</th> IMDb ID <i class="fas fa-sort"></i>
<th>Date Type</th> </th>
<th>Video File</th> <th class="sortable" onclick="sortTable('movies-tbody', 2, 'date')" style="cursor: pointer;">
Movie Released <i class="fas fa-sort"></i>
</th>
<th class="sortable" onclick="sortTable('movies-tbody', 3, 'date')" style="cursor: pointer;">
Date Added to Library <i class="fas fa-sort"></i>
</th>
<th class="sortable" onclick="sortTable('movies-tbody', 4, 'text')" style="cursor: pointer;">
Source <i class="fas fa-sort"></i>
</th>
<th class="sortable" onclick="sortTable('movies-tbody', 5, 'text')" style="cursor: pointer;">
Date Type <i class="fas fa-sort"></i>
</th>
<th class="sortable" onclick="sortTable('movies-tbody', 6, 'text')" style="cursor: pointer;">
Video File <i class="fas fa-sort"></i>
</th>
<th>Actions</th> <th>Actions</th>
</tr> </tr>
</thead> </thead>
@@ -196,14 +210,24 @@
</div> </div>
<div class="table-container"> <div class="table-container">
<table class="data-table" id="series-table"> <table class="data-table sortable-table" id="series-table">
<thead> <thead>
<tr> <tr>
<th>Series Title</th> <th class="sortable" onclick="sortTable('series-tbody', 0, 'text')" style="cursor: pointer;">
<th>IMDb ID</th> Series Title <i class="fas fa-sort"></i>
<th>Episodes</th> </th>
<th>With Dates</th> <th class="sortable" onclick="sortTable('series-tbody', 1, 'text')" style="cursor: pointer;">
<th>With Video</th> IMDb ID <i class="fas fa-sort"></i>
</th>
<th class="sortable" onclick="sortTable('series-tbody', 2, 'number')" style="cursor: pointer;">
Episodes <i class="fas fa-sort"></i>
</th>
<th class="sortable" onclick="sortTable('series-tbody', 3, 'number')" style="cursor: pointer;">
With Dates <i class="fas fa-sort"></i>
</th>
<th class="sortable" onclick="sortTable('series-tbody', 4, 'number')" style="cursor: pointer;">
With Video <i class="fas fa-sort"></i>
</th>
<th>Actions</th> <th>Actions</th>
</tr> </tr>
</thead> </thead>
+114 -6
View File
@@ -514,17 +514,27 @@ function showEpisodesModal(data) {
</button> </button>
</div> </div>
<div class="table-container"> <div class="table-container">
<table class="data-table"> <table class="data-table sortable-table">
<thead> <thead>
<tr> <tr>
<th width="40px"> <th width="40px">
<input type="checkbox" id="select-all-checkbox" onchange="toggleSelectAll()"> <input type="checkbox" id="select-all-checkbox" onchange="toggleSelectAll()">
</th> </th>
<th>Episode</th> <th class="sortable" onclick="sortTable('episodes-table-body', 1, 'text')" style="cursor: pointer;">
<th>Aired</th> Episode <i class="fas fa-sort"></i>
<th>Date Added</th> </th>
<th>Source</th> <th class="sortable" onclick="sortTable('episodes-table-body', 2, 'date')" style="cursor: pointer;">
<th>Video</th> Aired <i class="fas fa-sort"></i>
</th>
<th class="sortable" onclick="sortTable('episodes-table-body', 3, 'date')" style="cursor: pointer;">
Date Added <i class="fas fa-sort"></i>
</th>
<th class="sortable" onclick="sortTable('episodes-table-body', 4, 'text')" style="cursor: pointer;">
Source <i class="fas fa-sort"></i>
</th>
<th class="sortable" onclick="sortTable('episodes-table-body', 5, 'text')" style="cursor: pointer;">
Video <i class="fas fa-sort"></i>
</th>
<th>Actions</th> <th>Actions</th>
</tr> </tr>
</thead> </thead>
@@ -1952,3 +1962,101 @@ function updatePopulateProgress(status) {
} }
} }
} }
// ==================== Table Sorting Functions ====================
let sortDirections = {}; // Track sort direction for each table column
function sortTable(tableBodyId, columnIndex, dataType) {
const tbody = document.getElementById(tableBodyId);
if (!tbody) return;
const sortKey = `${tableBodyId}-${columnIndex}`;
// Toggle sort direction
if (!sortDirections[sortKey]) {
sortDirections[sortKey] = 'asc';
} else {
sortDirections[sortKey] = sortDirections[sortKey] === 'asc' ? 'desc' : 'asc';
}
const rows = Array.from(tbody.querySelectorAll('tr'));
rows.sort((a, b) => {
const aCell = a.cells[columnIndex];
const bCell = b.cells[columnIndex];
if (!aCell || !bCell) return 0;
let aValue = aCell.textContent.trim();
let bValue = bCell.textContent.trim();
// Handle MISSING values - always sort to bottom
if (aValue === 'MISSING') return 1;
if (bValue === 'MISSING') return -1;
// Handle different data types
if (dataType === 'date') {
// Parse dates for comparison
aValue = aValue === '-' ? '' : aValue;
bValue = bValue === '-' ? '' : bValue;
if (!aValue && !bValue) return 0;
if (!aValue) return 1;
if (!bValue) return -1;
const aDate = new Date(aValue);
const bDate = new Date(bValue);
if (sortDirections[sortKey] === 'asc') {
return aDate - bDate;
} else {
return bDate - aDate;
}
} else if (dataType === 'number') {
const aNum = parseFloat(aValue) || 0;
const bNum = parseFloat(bValue) || 0;
if (sortDirections[sortKey] === 'asc') {
return aNum - bNum;
} else {
return bNum - aNum;
}
} else {
// Text comparison
if (sortDirections[sortKey] === 'asc') {
return aValue.localeCompare(bValue);
} else {
return bValue.localeCompare(aValue);
}
}
});
// Re-append sorted rows
rows.forEach(row => tbody.appendChild(row));
// Update sort icons
updateSortIcons(tableBodyId, columnIndex, sortDirections[sortKey]);
}
function updateSortIcons(tableBodyId, activeColumn, direction) {
// Find the table and update icons
const tbody = document.getElementById(tableBodyId);
if (!tbody) return;
const table = tbody.closest('table');
if (!table) return;
const headers = table.querySelectorAll('th.sortable');
headers.forEach((header, index) => {
const icon = header.querySelector('i');
if (!icon) return;
// +1 to account for checkbox column
if (index + 1 === activeColumn) {
icon.className = direction === 'asc' ? 'fas fa-sort-up' : 'fas fa-sort-down';
} else {
icon.className = 'fas fa-sort';
}
});
}