feat: Inertia profile settings page, Studio edit redesign, EGS, Nova UI components\n\n- Redesign /dashboard/profile as Inertia React page (Settings/ProfileEdit)\n with SettingsLayout sidebar, Nova UI components (TextInput, Textarea,\n Toggle, Select, RadioGroup, Modal, Button), avatar drag-and-drop,\n password change, and account deletion sections\n- Redesign Studio artwork edit page with two-column layout, Nova components,\n integrated TagPicker, and version history modal\n- Add shared MarkdownEditor component\n- Add Early-Stage Growth System (EGS): SpotlightEngine, FeedBlender,\n GridFiller, AdaptiveTimeWindow, ActivityLayer, admin panel\n- Fix upload category/tag persistence (V1+V2 paths)\n- Fix tag source enum, category tree display, binding resolution\n- Add settings.jsx Vite entry, settings.blade.php wrapper\n- Update ProfileController with JSON response support for API calls\n- Various route fixes (profile.edit, toolbar settings link)"

This commit is contained in:
2026-03-03 20:57:43 +01:00
parent dc51d65440
commit b9c2d8597d
114 changed files with 8760 additions and 693 deletions

View File

@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace App\Services\EarlyGrowth;
use App\Models\Artwork;
use Illuminate\Support\Facades\Cache;
/**
* AdaptiveTimeWindow
*
* Dynamically widens the look-back window used by trending / rising feeds
* when recent upload volume is below configured thresholds.
*
* This only affects RANKING QUERIES it never modifies artwork timestamps,
* canonical URLs, or any stored data.
*
* Behaviour:
* uploads/day narrow_threshold normal window (7 d)
* uploads/day wide_threshold medium window (30 d)
* uploads/day < wide_threshold wide window (90 d)
*
* All thresholds and window sizes are configurable in config/early_growth.php.
*/
final class AdaptiveTimeWindow
{
/**
* Return the number of look-back days to use for trending / rising queries.
*
* @param int $defaultDays Returned as-is when EGS is disabled.
*/
public function getTrendingWindowDays(int $defaultDays = 30): int
{
if (! EarlyGrowth::adaptiveWindowEnabled()) {
return $defaultDays;
}
$uploadsPerDay = $this->getUploadsPerDay();
$narrowThreshold = (int) config('early_growth.thresholds.uploads_per_day_narrow', 10);
$wideThreshold = (int) config('early_growth.thresholds.uploads_per_day_wide', 3);
$narrowDays = (int) config('early_growth.thresholds.window_narrow_days', 7);
$mediumDays = (int) config('early_growth.thresholds.window_medium_days', 30);
$wideDays = (int) config('early_growth.thresholds.window_wide_days', 90);
if ($uploadsPerDay >= $narrowThreshold) {
return $narrowDays; // Healthy activity → normal 7-day window
}
if ($uploadsPerDay >= $wideThreshold) {
return $mediumDays; // Moderate activity → expand to 30 days
}
return $wideDays; // Low activity → expand to 90 days
}
/**
* Rolling 7-day average of approved public uploads per day.
* Cached for `early_growth.cache_ttl.time_window` seconds.
*/
public function getUploadsPerDay(): float
{
$ttl = (int) config('early_growth.cache_ttl.time_window', 600);
return Cache::remember('egs.uploads_per_day', $ttl, function (): float {
$count = Artwork::query()
->where('is_public', true)
->where('is_approved', true)
->whereNull('deleted_at')
->whereNotNull('published_at')
->where('published_at', '>=', now()->subDays(7))
->count();
return round($count / 7, 2);
});
}
}