feat(ui): bump thumbnail sizes 1.5x, make Settings fluid, add subscription_count stat

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-22 23:18:22 -04:00
parent ec44c653fe
commit 6f68bf5fa7
6 changed files with 21 additions and 14 deletions
+5 -1
View File
@@ -6,7 +6,7 @@ from quart import Blueprint, jsonify, request
from sqlalchemy import func, select from sqlalchemy import func, select
from ..extensions import get_session from ..extensions import get_session
from ..models import AppSetting, ImageRecord, ImportBatch, ImportSettings, ImportTask, Tag from ..models import AppSetting, Artist, ImageRecord, ImportBatch, ImportSettings, ImportTask, Tag
settings_bp = Blueprint("settings", __name__, url_prefix="/api") settings_bp = Blueprint("settings", __name__, url_prefix="/api")
@@ -118,6 +118,9 @@ async def system_stats():
storage_bytes = ( storage_bytes = (
(await session.execute(select(func.coalesce(func.sum(ImageRecord.size_bytes), 0)))).scalar_one() (await session.execute(select(func.coalesce(func.sum(ImageRecord.size_bytes), 0)))).scalar_one()
) )
subscription_count = (await session.execute(
select(func.count(Artist.id)).where(Artist.is_subscription.is_(True))
)).scalar_one()
# Task counts grouped by status # Task counts grouped by status
status_rows = ( status_rows = (
@@ -163,6 +166,7 @@ async def system_stats():
"total_images": total_images, "total_images": total_images,
"total_tags": total_tags, "total_tags": total_tags,
"storage_bytes": storage_bytes, "storage_bytes": storage_bytes,
"subscription_count": int(subscription_count),
"tasks": { "tasks": {
"pending": status_counts.get("pending", 0), "pending": status_counts.get("pending", 0),
"queued": status_counts.get("queued", 0), "queued": status_counts.get("queued", 0),
@@ -86,7 +86,7 @@ function imagesForGroup(group) {
} }
.fc-gallery-grid__items { .fc-gallery-grid__items {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 8px; gap: 8px;
} }
.fc-gallery-grid__sentinel { .fc-gallery-grid__sentinel {
@@ -101,7 +101,7 @@ function imagesForGroup(group) {
} }
.fc-gallery-grid__skeleton { .fc-gallery-grid__skeleton {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 8px; gap: 8px;
} }
.fc-gallery-grid__skeleton-item { .fc-gallery-grid__skeleton-item {
@@ -124,7 +124,7 @@ function imagesForGroup(group) {
@media (max-width: 600px) { @media (max-width: 600px) {
.fc-gallery-grid__items, .fc-gallery-grid__items,
.fc-gallery-grid__skeleton { .fc-gallery-grid__skeleton {
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 4px; gap: 4px;
} }
} }
@@ -1,6 +1,6 @@
<template> <template>
<v-row dense> <v-row dense>
<v-col v-for="card in cards" :key="card.label" cols="12" sm="6" md="4" lg="2"> <v-col v-for="card in cards" :key="card.label" cols="12" sm="6" md="4" lg="3" xl="2">
<v-card class="fc-stat"> <v-card class="fc-stat">
<v-card-text> <v-card-text>
<div class="fc-stat__label text-caption">{{ card.label }}</div> <div class="fc-stat__label text-caption">{{ card.label }}</div>
@@ -33,15 +33,17 @@ const cards = computed(() => {
{ label: 'Total images', value: '—' }, { label: 'Total images', value: '—' },
{ label: 'Total tags', value: '—' }, { label: 'Total tags', value: '—' },
{ label: 'Storage used', value: '—' }, { label: 'Storage used', value: '—' },
{ label: 'Subscriptions', value: '—' },
{ label: 'Pending tasks', value: '—' }, { label: 'Pending tasks', value: '—' },
{ label: 'Failed tasks', value: '—' } { label: 'Failed tasks', value: '—' }
] ]
return [ return [
{ label: 'Total images', value: s.total_images.toLocaleString() }, { label: 'Total images', value: s.total_images.toLocaleString() },
{ label: 'Total tags', value: s.total_tags.toLocaleString() }, { label: 'Total tags', value: s.total_tags.toLocaleString() },
{ label: 'Storage used', value: formatBytes(s.storage_bytes) }, { label: 'Storage used', value: formatBytes(s.storage_bytes) },
{ label: 'Pending', value: (s.tasks.pending + s.tasks.queued).toLocaleString() }, { label: 'Subscriptions', value: (s.subscription_count ?? 0).toLocaleString() },
{ label: 'Failed', value: s.tasks.failed.toLocaleString() } { label: 'Pending', value: (s.tasks.pending + s.tasks.queued).toLocaleString() },
{ label: 'Failed', value: s.tasks.failed.toLocaleString() }
] ]
}) })
</script> </script>
+3 -2
View File
@@ -29,10 +29,11 @@ export function distributeIntoColumns(items, columnCount) {
} }
// Reactive column count from container width. Breakpoints mirror the // Reactive column count from container width. Breakpoints mirror the
// gallery grid intent: ~260px target column width, min 1 column. // gallery grid intent: ~390px target column width, min 1 column. Bumped
// from 260px on 2026-05-23 per dogfood UX feedback (thumbs were too small).
export function columnCountForWidth(width) { export function columnCountForWidth(width) {
if (!width || width < 0) return 1 if (!width || width < 0) return 1
return Math.max(1, Math.floor(width / 260)) return Math.max(1, Math.floor(width / 390))
} }
export function usePolyMasonry(containerRef) { export function usePolyMasonry(containerRef) {
+1 -1
View File
@@ -1,5 +1,5 @@
<template> <template>
<v-container class="py-6"> <v-container fluid class="py-6">
<v-tabs v-model="tab" color="accent" class="mb-4"> <v-tabs v-model="tab" color="accent" class="mb-4">
<v-tab value="overview">Overview</v-tab> <v-tab value="overview">Overview</v-tab>
<v-tab value="import">Import</v-tab> <v-tab value="import">Import</v-tab>
+1 -1
View File
@@ -76,7 +76,7 @@ async def test_system_stats_shape(client):
resp = await client.get("/api/system/stats") resp = await client.get("/api/system/stats")
assert resp.status_code == 200 assert resp.status_code == 200
body = await resp.get_json() body = await resp.get_json()
for key in ("total_images", "total_tags", "storage_bytes", "tasks", "active_batch"): for key in ("total_images", "total_tags", "storage_bytes", "subscription_count", "tasks", "active_batch"):
assert key in body assert key in body
for status in ("pending", "queued", "processing", "complete", "skipped", "failed"): for status in ("pending", "queued", "processing", "complete", "skipped", "failed"):
assert status in body["tasks"] assert status in body["tasks"]