Ship production optimization M1-M12.5A: queues, metrics, HTTP observability, and vector search reliability.

Keep similar-ai from tripping the global circuit on a lone URL 502, clamp Qdrant search to 100, and add Server-Timing plus slow-request logging. Studio shared props, Academy S3 exists caching, heat chunking, and Redis/scheduler hygiene stay in this rollout.
This commit is contained in:
2026-08-25 07:58:47 +02:00
parent f52879edbb
commit 8a80aae21e
114 changed files with 9293 additions and 284 deletions
+33 -37
View File
@@ -6,6 +6,7 @@ namespace App\Services\Studio;
use App\Models\Artwork;
use App\Models\ArtworkStats;
use App\Services\Metrics\ArtworkHourlySnapshotWindow;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
@@ -16,10 +17,26 @@ final class StudioMetricsService
{
private const CACHE_TTL = 300; // 5 minutes
public function __construct(
private readonly ArtworkHourlySnapshotWindow $snapshotWindow,
) {
}
/**
* Get dashboard KPI metrics for a creator.
*
* @return array{total_artworks: int, views_30d: int, favourites_30d: int, shares_30d: int, followers: int}
* 30-day counters are latest-minus-baseline deltas of cumulative hourly snapshots, never SUM.
*
* @return array{
* total_artworks: int,
* views_30d: int,
* favourites_30d: int,
* shares_30d: int,
* downloads_30d: int,
* comments_30d: int,
* followers: int,
* snapshot_window: array<string, mixed>
* }
*/
public function getDashboardKpis(int $userId): array
{
@@ -30,35 +47,7 @@ final class StudioMetricsService
->whereNull('deleted_at')
->count();
// Aggregate stats from artwork_stats for this user's artworks
$statsAgg = DB::table('artwork_stats')
->join('artworks', 'artworks.id', '=', 'artwork_stats.artwork_id')
->where('artworks.user_id', $userId)
->whereNull('artworks.deleted_at')
->selectRaw('
COALESCE(SUM(artwork_stats.views), 0) as total_views,
COALESCE(SUM(artwork_stats.favorites), 0) as total_favourites,
COALESCE(SUM(artwork_stats.shares_count), 0) as total_shares
')
->first();
// Views in last 30 days from hourly snapshots if available, fallback to totals
$views30d = 0;
try {
if (\Illuminate\Support\Facades\Schema::hasTable('artwork_metric_snapshots_hourly')) {
$views30d = (int) DB::table('artwork_metric_snapshots_hourly')
->join('artworks', 'artworks.id', '=', 'artwork_metric_snapshots_hourly.artwork_id')
->where('artworks.user_id', $userId)
->where('artwork_metric_snapshots_hourly.bucket_hour', '>=', now()->subDays(30))
->sum('artwork_metric_snapshots_hourly.views_count');
}
} catch (\Throwable $e) {
// Table or column doesn't exist — fall back to totals
}
if ($views30d === 0) {
$views30d = (int) ($statsAgg->total_views ?? 0);
}
$window = $this->snapshotWindow->userWindow($userId, 30);
$followers = DB::table('user_followers')
->where('user_id', $userId)
@@ -66,10 +55,20 @@ final class StudioMetricsService
return [
'total_artworks' => $totalArtworks,
'views_30d' => $views30d,
'favourites_30d' => (int) ($statsAgg->total_favourites ?? 0),
'shares_30d' => (int) ($statsAgg->total_shares ?? 0),
'followers' => $followers,
'views_30d' => $window['views'],
'favourites_30d' => $window['favourites'],
'shares_30d' => $window['shares'],
'downloads_30d' => $window['downloads'],
'comments_30d' => $window['comments'],
'followers' => $followers,
'snapshot_window' => [
'requested_days' => $window['requested_days'],
'table_coverage_days' => $window['table_coverage_days'],
'user_coverage_days' => $window['user_coverage_days'],
'window_complete' => $window['window_complete'],
'oldest_bucket' => $window['oldest_bucket'],
'newest_bucket' => $window['newest_bucket'],
],
];
});
}
@@ -145,7 +144,6 @@ final class StudioMetricsService
$cacheKey = "studio.analytics_overview.{$userId}";
return Cache::remember($cacheKey, self::CACHE_TTL, function () use ($userId) {
// Totals
$totals = DB::table('artwork_stats')
->join('artworks', 'artworks.id', '=', 'artwork_stats.artwork_id')
->where('artworks.user_id', $userId)
@@ -161,7 +159,6 @@ final class StudioMetricsService
')
->first();
// Top 10 artworks by ranking score
$topArtworks = Artwork::where('user_id', $userId)
->whereNull('deleted_at')
->where('is_public', true)
@@ -188,7 +185,6 @@ final class StudioMetricsService
'heat_score' => (float) ($art->stats?->heat_score ?? 0),
]);
// Content type breakdown
$contentBreakdown = DB::table('artworks')
->join('artwork_category', 'artwork_category.artwork_id', '=', 'artworks.id')
->join('categories', 'categories.id', '=', 'artwork_category.category_id')