feat(stats): add percentage and stale counters to completeness cards

- Show percentage of total assets on missing metadata/thumbnail/preview/phash cards
- Add stale counts for thumbnails and previews (generated at wrong resolution vs current settings)
- Add h-100 to all completeness cards for consistent row heights
This commit is contained in:
2026-07-08 19:45:09 +02:00
parent 94bba45f45
commit 54cf5eff2b
3 changed files with 36 additions and 9 deletions

View File

@@ -88,10 +88,18 @@ public class StatsDto {
/// </summary>
public int AssetsMissingThumbnail { get; set; }
/// <summary>
/// Gets or sets the number of assets whose thumbnail was generated at a different size than the current setting.
/// </summary>
public int AssetsMissingThumbnailStale { get; set; }
/// <summary>
/// Gets or sets the number of assets missing a generated preview.
/// </summary>
public int AssetsMissingPreviews { get; set; }
/// <summary>
/// Gets or sets the number of assets whose preview was generated at a different size than the current setting.
/// </summary>
public int AssetsMissingPreviewsStale { get; set; }
/// <summary>
/// Gets or sets the number of assets missing a perceptual hash.
/// </summary>
public int AssetsMissingPhash { get; set; }

View File

@@ -1,4 +1,5 @@
using Butter.Dtos.Stats;
using Butter.Settings;
using Butter.Types;
using Lactose.Context;
using System.Collections;
@@ -6,7 +7,7 @@ using System.Collections;
namespace Lactose.Repositories;
/// <inheritdoc />
public class StatsRepository(LactoseDbContext context) : IStatsRepository {
public class StatsRepository(LactoseDbContext context, ISettingsRepository settingsRepo) : IStatsRepository {
/// <inheritdoc />
public StatsDto GetStats() {
var dto = new StatsDto();
@@ -56,12 +57,27 @@ public class StatsRepository(LactoseDbContext context) : IStatsRepository {
dto.AssetsMissingMetadata = context.Assets.Count(a =>
a.Type == EAssetType.Image && a.ResolutionWidth == 0 && a.DeletedAt == null);
var thumbnailSizeSetting = settingsRepo.Get(Settings.ThumbnailSize.AsString());
var previewSizeSetting = settingsRepo.Get(Settings.PreviewSize.AsString());
_ = int.TryParse(thumbnailSizeSetting?.Value, out var expectedThumbnailSize);
_ = int.TryParse(previewSizeSetting?.Value, out var expectedPreviewSize);
dto.AssetsMissingThumbnail = context.Assets.Count(a =>
(a.ThumbnailPath == null || a.ThumbnailPath == "") && a.DeletedAt == null);
dto.AssetsMissingThumbnailStale = expectedThumbnailSize > 0
? context.Assets.Count(a =>
a.ThumbnailPath != null && a.ThumbnailPath != "" && a.ThumbnailSize != expectedThumbnailSize && a.DeletedAt == null)
: 0;
dto.AssetsMissingPreviews = context.Assets.Count(a =>
(a.PreviewPath == null || a.PreviewPath == "") && a.DeletedAt == null);
dto.AssetsMissingPreviewsStale = expectedPreviewSize > 0
? context.Assets.Count(a =>
a.PreviewPath != null && a.PreviewPath != "" && a.PreviewSize != expectedPreviewSize && a.DeletedAt == null)
: 0;
var emptyHash = new BitArray(64);
dto.AssetsMissingPhash = context.Assets.Count(a =>
a.Hash == emptyHash && a.DeletedAt == null);

View File

@@ -38,14 +38,14 @@
@* ---- Data completeness indicators ---- *@
<div class="row row-cols-2 row-cols-md-4 row-cols-lg-4 g-3 mb-4">
<div class="col"><div class="card border-warning"><div class="card-body text-center py-3"><h5>@_stats.AssetsMissingMetadata.ToString("N0")</h5><small class="text-warning"><i class="bi bi-exclamation-triangle"></i> Missing Metadata</small></div></div></div>
<div class="col"><div class="card border-warning"><div class="card-body text-center py-3"><h5>@_stats.AssetsMissingThumbnail.ToString("N0")</h5><small class="text-warning"><i class="bi bi-exclamation-triangle"></i> Missing Thumbnail</small></div></div></div>
<div class="col"><div class="card border-warning"><div class="card-body text-center py-3"><h5>@_stats.AssetsMissingPreviews.ToString("N0")</h5><small class="text-warning"><i class="bi bi-exclamation-triangle"></i> Missing Previews</small></div></div></div>
<div class="col"><div class="card border-warning"><div class="card-body text-center py-3"><h5>@_stats.AssetsMissingPhash.ToString("N0")</h5><small class="text-warning"><i class="bi bi-exclamation-triangle"></i> Missing pHash</small></div></div></div>
<div class="col"><div class="card border-info"><div class="card-body text-center py-3"><h5>@_stats.AssetsWithNoPerson.ToString("N0")</h5><small class="text-info"><i class="bi bi-info-circle"></i> No Person</small></div></div></div>
<div class="col"><div class="card border-info"><div class="card-body text-center py-3"><h5>@_stats.AssetsWithNoAlbum.ToString("N0")</h5><small class="text-info"><i class="bi bi-info-circle"></i> No Album</small></div></div></div>
<div class="col"><div class="card border-danger"><div class="card-body text-center py-3"><h5>@_stats.AlbumsMissingCover.ToString("N0")</h5><small class="text-danger"><i class="bi bi-image"></i> Albums Missing Cover</small></div></div></div>
<div class="col"><div class="card border-secondary"><div class="card-body text-center py-3"><h5>@_stats.CosplayersMissingCover.ToString("N0")</h5><small class="text-muted"><i class="bi bi-person-badge"></i> Cosplayers Missing Cover</small></div></div></div>
<div class="col"><div class="card border-warning h-100"><div class="card-body text-center py-3"><h5>@_stats.AssetsMissingMetadata.ToString("N0")</h5><small class="text-muted">@PercentOfTotal(_stats.AssetsMissingMetadata)</small><br/><small class="text-warning"><i class="bi bi-exclamation-triangle"></i> Missing Metadata</small></div></div></div>
<div class="col"><div class="card border-warning h-100"><div class="card-body text-center py-3"><h5>@_stats.AssetsMissingThumbnail.ToString("N0")</h5><small class="text-muted">@PercentOfTotal(_stats.AssetsMissingThumbnail)</small><br/><small class="text-warning"><i class="bi bi-exclamation-triangle"></i> Missing Thumbnail</small><br/><small class="text-danger">@_stats.AssetsMissingThumbnailStale.ToString("N0") stale</small></div></div></div>
<div class="col"><div class="card border-warning h-100"><div class="card-body text-center py-3"><h5>@_stats.AssetsMissingPreviews.ToString("N0")</h5><small class="text-muted">@PercentOfTotal(_stats.AssetsMissingPreviews)</small><br/><small class="text-warning"><i class="bi bi-exclamation-triangle"></i> Missing Previews</small><br/><small class="text-danger">@_stats.AssetsMissingPreviewsStale.ToString("N0") stale</small></div></div></div>
<div class="col"><div class="card border-warning h-100"><div class="card-body text-center py-3"><h5>@_stats.AssetsMissingPhash.ToString("N0")</h5><small class="text-muted">@PercentOfTotal(_stats.AssetsMissingPhash)</small><br/><small class="text-warning"><i class="bi bi-exclamation-triangle"></i> Missing pHash</small></div></div></div>
<div class="col"><div class="card border-info h-100"><div class="card-body text-center py-3"><h5>@_stats.AssetsWithNoPerson.ToString("N0")</h5><small class="text-info"><i class="bi bi-info-circle"></i> No Person</small></div></div></div>
<div class="col"><div class="card border-info h-100"><div class="card-body text-center py-3"><h5>@_stats.AssetsWithNoAlbum.ToString("N0")</h5><small class="text-info"><i class="bi bi-info-circle"></i> No Album</small></div></div></div>
<div class="col"><div class="card border-danger h-100"><div class="card-body text-center py-3"><h5>@_stats.AlbumsMissingCover.ToString("N0")</h5><small class="text-danger"><i class="bi bi-image"></i> Albums Missing Cover</small></div></div></div>
<div class="col"><div class="card border-secondary h-100"><div class="card-body text-center py-3"><h5>@_stats.CosplayersMissingCover.ToString("N0")</h5><small class="text-muted"><i class="bi bi-person-badge"></i> Cosplayers Missing Cover</small></div></div></div>
</div>
@* ---- Duplicate / Orphan / Visibility ---- *@
@@ -195,6 +195,9 @@
StateHasChanged();
}
private string PercentOfTotal(int count) =>
_stats?.TotalAssets > 0 ? $"{(double)count / _stats.TotalAssets * 100:F1}%" : "0%";
private static string FormatBytes(long bytes) => bytes switch {
>= 1_073_741_824 => $"{bytes / 1_073_741_824.0:F2} GB",
>= 1_048_576 => $"{bytes / 1_048_576.0:F2} MB",