diff --git a/backend/app/api/settings.py b/backend/app/api/settings.py index 15c52e9..145890e 100644 --- a/backend/app/api/settings.py +++ b/backend/app/api/settings.py @@ -141,11 +141,23 @@ async def system_stats(): ).all() integrity_counts = {row[0]: row[1] for row in integrity_rows} - # Active batch (most recent running) + # Active batch = running batch that still has outstanding work. + # Plain "most recent running" picks a freshly-created scan that + # enqueued zero new files and hides the older batch that's + # actually being processed; the EXISTS clause filters those + # empty batches out. active_batch_row = ( await session.execute( select(ImportBatch) - .where(ImportBatch.status == "running") + .where( + ImportBatch.status == "running", + select(ImportTask.id) + .where( + ImportTask.batch_id == ImportBatch.id, + ImportTask.status.in_(["pending", "queued", "processing"]), + ) + .exists(), + ) .order_by(ImportBatch.started_at.desc()) .limit(1) ) diff --git a/backend/app/tasks/scan.py b/backend/app/tasks/scan.py index c4581f7..6aacde0 100644 --- a/backend/app/tasks/scan.py +++ b/backend/app/tasks/scan.py @@ -95,6 +95,15 @@ def scan_directory(self, triggered_by: str = "manual", files_seen += 1 batch.total_files = files_seen + # If the walk enqueued nothing (every file was already on a + # non-failed ImportTask from a prior scan), there's no + # import_media_file message that would ever flip this batch to + # 'complete' — finalize it now so the active-batch query in + # /api/system/stats doesn't get stuck reporting all-zero + # counters from an empty scan. + if files_seen == 0: + batch.status = "complete" + batch.finished_at = datetime.now(UTC) session.commit() # Now enqueue import_media_file for each pending task. diff --git a/frontend/src/components/TopNav.vue b/frontend/src/components/TopNav.vue index 4493050..e95bf1a 100644 --- a/frontend/src/components/TopNav.vue +++ b/frontend/src/components/TopNav.vue @@ -55,7 +55,13 @@ const health = computed(() => { position: sticky; top: 0; z-index: 1000; - display: flex; + /* Mirrored side columns (1fr / auto / 1fr) keep the link block dead- + centered no matter what the teleport-slot on the right is rendering + for the active view (Gallery: Select, Showcase: Shuffle, others: ∅). + With a plain flex layout the links shifted left as soon as actions + appeared. */ + display: grid; + grid-template-columns: 1fr auto 1fr; align-items: center; gap: 1rem; padding: 0.75rem 1rem; @@ -87,7 +93,6 @@ const health = computed(() => { } .fc-links { - flex: 1; display: flex; flex-wrap: wrap; justify-content: center; @@ -118,6 +123,7 @@ const health = computed(() => { align-items: center; gap: 8px; flex-shrink: 0; + justify-self: start; } .fc-health { display: flex; @@ -129,5 +135,6 @@ const health = computed(() => { align-items: center; gap: 0.5rem; flex-shrink: 0; + justify-self: end; } diff --git a/frontend/src/components/modal/ImageCanvas.vue b/frontend/src/components/modal/ImageCanvas.vue index 52cbb17..a4a4163 100644 --- a/frontend/src/components/modal/ImageCanvas.vue +++ b/frontend/src/components/modal/ImageCanvas.vue @@ -1,14 +1,16 @@ @@ -123,7 +125,11 @@ function isTextEntry(el) {