diff --git a/api/web_routes.py b/api/web_routes.py
index faa86fc..41281e8 100644
--- a/api/web_routes.py
+++ b/api/web_routes.py
@@ -45,16 +45,24 @@ def map_source_to_description(source: str) -> str:
elif "omdb:" in source_lower:
return "OMDb Release"
+ # Sonarr sources
+ elif "sonarr:" in source_lower:
+ return "Sonarr API"
+
# Manual and other sources
elif "manual" in source_lower:
return "Manual Entry"
elif "digital_release" in source_lower:
return "Digital Release"
+ elif "nfo_file_existing" in source_lower:
+ return "NFO File (Legacy)"
elif "nfo:" in source_lower:
return "NFO File"
elif "webhook:" in source_lower:
return "Webhook/API"
-
+ elif "database" in source_lower:
+ return "Database"
+
# Fallback for unknown patterns
return source.title()
diff --git a/nfoguard-web/api/web_routes.py b/nfoguard-web/api/web_routes.py
index aa8bfd6..42d3b64 100644
--- a/nfoguard-web/api/web_routes.py
+++ b/nfoguard-web/api/web_routes.py
@@ -48,16 +48,24 @@ def map_source_to_description(source: str) -> str:
elif "omdb:" in source_lower:
return "OMDb Release"
+ # Sonarr sources
+ elif "sonarr:" in source_lower:
+ return "Sonarr API"
+
# Manual and other sources
elif "manual" in source_lower:
return "Manual Entry"
elif "digital_release" in source_lower:
return "Digital Release"
+ elif "nfo_file_existing" in source_lower:
+ return "NFO File (Legacy)"
elif "nfo:" in source_lower:
return "NFO File"
elif "webhook:" in source_lower:
return "Webhook/API"
-
+ elif "database" in source_lower:
+ return "Database"
+
# Fallback for unknown patterns
return source.title()
@@ -468,11 +476,18 @@ async def get_dashboard_stats(dependencies: dict):
cursor.execute("SELECT COUNT(*) FROM episodes WHERE source = 'no_valid_date_source'")
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("""
- SELECT COUNT(*) FROM processing_history
- WHERE processed_at > NOW() - INTERVAL '7 days'
+ SELECT COUNT(*) FROM (
+ 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())
diff --git a/nfoguard-web/static/index.html b/nfoguard-web/static/index.html
index 9e59a36..d67590e 100644
--- a/nfoguard-web/static/index.html
+++ b/nfoguard-web/static/index.html
@@ -139,16 +139,30 @@
-
+
- | Title |
- IMDb ID |
- Movie Released |
- Date Added to Library |
- Source |
- Date Type |
- Video File |
+
+ Title
+ |
+
+ IMDb ID
+ |
+
+ Movie Released
+ |
+
+ Date Added to Library
+ |
+
+ Source
+ |
+
+ Date Type
+ |
+
+ Video File
+ |
Actions |
@@ -196,14 +210,24 @@
-
+
- | Series Title |
- IMDb ID |
- Episodes |
- With Dates |
- With Video |
+
+ Series Title
+ |
+
+ IMDb ID
+ |
+
+ Episodes
+ |
+
+ With Dates
+ |
+
+ With Video
+ |
Actions |
diff --git a/nfoguard-web/static/js/app.js b/nfoguard-web/static/js/app.js
index 883d7ee..378b0b8 100644
--- a/nfoguard-web/static/js/app.js
+++ b/nfoguard-web/static/js/app.js
@@ -514,17 +514,27 @@ function showEpisodesModal(data) {
-
+
|
|
- Episode |
- Aired |
- Date Added |
- Source |
- Video |
+
+ Episode
+ |
+
+ Aired
+ |
+
+ Date Added
+ |
+
+ Source
+ |
+
+ Video
+ |
Actions |
@@ -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';
+ }
+ });
+}