tuning job log views and deep scan archive processing.

This commit is contained in:
Bryan Van Deusen
2026-02-02 18:48:01 -05:00
parent 042a69f9c3
commit 09883960d4
9 changed files with 1078 additions and 103 deletions
+198 -9
View File
@@ -7,7 +7,7 @@ from sqlalchemy.orm import joinedload, aliased
from collections import OrderedDict
from datetime import datetime
from app.models import ImageRecord, Tag, ArchiveRecord, image_tags, ImportTask, ImportBatch
from app.models import ImageRecord, Tag, ArchiveRecord, image_tags, ImportTask, ImportBatch, AppSettings
from app import db
import os
import shutil
@@ -1225,6 +1225,91 @@ def import_queue_status():
return jsonify({'ok': False, 'error': str(e)}), 500
@main.route('/api/import/tasks')
def list_import_tasks():
"""
List import tasks with filtering and pagination.
Query params:
status: Filter by status (comma-separated, e.g., 'failed,skipped')
task_type: Filter by task type ('import_image', 'import_archive')
search: Search in source_path or error_message
limit: Max results (default 50, max 200)
offset: Skip first N results
sort: Sort field ('created_at', 'completed_at', default: 'created_at')
order: Sort order ('asc', 'desc', default: 'desc')
"""
try:
# Parse parameters
status_filter = request.args.get('status', '')
task_type = request.args.get('task_type', '')
search = request.args.get('search', '')
limit = min(int(request.args.get('limit', 50)), 200)
offset = int(request.args.get('offset', 0))
sort_field = request.args.get('sort', 'created_at')
sort_order = request.args.get('order', 'desc')
# Build query
query = ImportTask.query
# Filter by status
if status_filter:
statuses = [s.strip() for s in status_filter.split(',') if s.strip()]
if statuses:
query = query.filter(ImportTask.status.in_(statuses))
# Filter by task type
if task_type:
query = query.filter_by(task_type=task_type)
# Search in source_path or error_message
if search:
search_pattern = f'%{search}%'
query = query.filter(
db.or_(
ImportTask.source_path.ilike(search_pattern),
ImportTask.error_message.ilike(search_pattern)
)
)
# Get total count before pagination
total = query.count()
# Sort
sort_col = getattr(ImportTask, sort_field, ImportTask.created_at)
if sort_order == 'asc':
query = query.order_by(sort_col.asc())
else:
query = query.order_by(sort_col.desc())
# Paginate
tasks = query.offset(offset).limit(limit).all()
return jsonify({
'ok': True,
'total': total,
'offset': offset,
'limit': limit,
'tasks': [
{
'id': t.id,
'source_path': t.source_path,
'filename': os.path.basename(t.source_path) if t.source_path else None,
'task_type': t.task_type,
'status': t.status,
'error_message': t.error_message, # Full error message
'file_size': t.file_size,
'retry_count': t.retry_count,
'created_at': t.created_at.isoformat() if t.created_at else None,
'completed_at': t.completed_at.isoformat() if t.completed_at else None
}
for t in tasks
]
})
except Exception as e:
return jsonify({'ok': False, 'error': str(e)}), 500
@main.route('/api/import/task/<int:task_id>')
def get_import_task(task_id):
"""
@@ -1308,25 +1393,129 @@ def retry_failed_import_tasks():
@main.route('/api/import/clear-completed', methods=['POST'])
def clear_completed_tasks():
"""
Clear completed and skipped import tasks from the database.
Keeps failed tasks for review.
"""
try:
deleted = ImportTask.query.filter(
ImportTask.status.in_(['complete', 'skipped'])
).delete(synchronize_session=False)
Clear import tasks from the database with optional age filtering.
POST params:
statuses: Comma-separated statuses to clear (default: complete,skipped)
older_than_days: Only clear tasks older than N days (default: 0 = all)
"""
from datetime import timedelta
try:
# Parse parameters
statuses_param = request.form.get('statuses', 'complete,skipped')
older_than_days = int(request.form.get('older_than_days', 0))
# Validate statuses
allowed_statuses = {'complete', 'skipped', 'failed'}
statuses = [s.strip() for s in statuses_param.split(',') if s.strip() in allowed_statuses]
if not statuses:
statuses = ['complete', 'skipped']
# Build query
query = ImportTask.query.filter(ImportTask.status.in_(statuses))
# Apply age filter if specified
if older_than_days > 0:
cutoff = datetime.utcnow() - timedelta(days=older_than_days)
query = query.filter(ImportTask.completed_at < cutoff)
deleted = query.delete(synchronize_session=False)
db.session.commit()
return jsonify({
'ok': True,
'deleted': deleted
'deleted': deleted,
'statuses': statuses,
'older_than_days': older_than_days
})
except Exception as e:
db.session.rollback()
return jsonify({'ok': False, 'error': str(e)}), 500
@main.route('/api/import/task-stats')
def import_task_stats():
"""
Get detailed task statistics with age breakdown.
"""
from datetime import timedelta
try:
now = datetime.utcnow()
today = now - timedelta(days=1)
week_ago = now - timedelta(days=7)
stats = {}
for status in ['complete', 'skipped', 'failed']:
total = ImportTask.query.filter_by(status=status).count()
today_count = ImportTask.query.filter(
ImportTask.status == status,
ImportTask.completed_at >= today
).count()
week_count = ImportTask.query.filter(
ImportTask.status == status,
ImportTask.completed_at >= week_ago,
ImportTask.completed_at < today
).count()
older = total - today_count - week_count
stats[status] = {
'total': total,
'today': today_count,
'this_week': week_count,
'older': older
}
return jsonify({'ok': True, 'stats': stats})
except Exception as e:
return jsonify({'ok': False, 'error': str(e)}), 500
@main.route('/api/system/stats')
def system_stats():
"""
Get system-wide statistics for the dashboard.
Returns cached stats computed by the update_system_stats Celery task.
Stats are updated every 5 minutes by Celery Beat.
Query params:
refresh: If 'true', triggers an immediate stats update (async)
"""
import json
try:
# Check for refresh request
if request.args.get('refresh') == 'true':
from app.tasks.scan import update_system_stats
update_system_stats.delay()
# Get cached stats from AppSettings
setting = AppSettings.query.filter_by(key='system_stats_cache').first()
if setting and setting.value:
stats = json.loads(setting.value)
return jsonify({
'ok': True,
'cached': True,
**stats
})
# No cached stats - return empty with flag to indicate first run
return jsonify({
'ok': True,
'cached': False,
'images': {'total': 0, 'storage_bytes': 0, 'storage_human': '0 B'},
'tags': {'total': 0, 'by_kind': {}},
'import_folder': {'pending_files': 0, 'pending_archives': 0},
'message': 'Stats not yet computed. Will be available shortly.'
})
except Exception as e:
return jsonify({'ok': False, 'error': str(e)}), 500
@main.route('/api/thumbnails/regenerate', methods=['POST'])
def regenerate_thumbnails_celery():
"""