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,149 @@
<?php
declare(strict_types=1);
namespace App\Services\EarlyGrowth;
use App\Models\Artwork;
use Illuminate\Support\Facades\Cache;
/**
* EarlyGrowth
*
* Central service for the Early-Stage Growth System.
* All other EGS modules consult this class for feature-flag status.
*
* Toggle via .env:
* NOVA_EARLY_GROWTH_ENABLED=true
* NOVA_EARLY_GROWTH_MODE=light # off | light | aggressive
*
* Set NOVA_EARLY_GROWTH_ENABLED=false to instantly revert to
* normal production behaviour across every integration point.
*/
final class EarlyGrowth
{
// ─── Feature-flag helpers ─────────────────────────────────────────────────
/**
* Is the entire Early Growth System active?
* Checks master enabled flag AND that mode is not 'off'.
*/
public static function enabled(): bool
{
if (! (bool) config('early_growth.enabled', false)) {
return false;
}
// Auto-disable check (optional)
if ((bool) config('early_growth.auto_disable.enabled', false) && self::shouldAutoDisable()) {
return false;
}
return self::mode() !== 'off';
}
/**
* Current operating mode: off | light | aggressive
*/
public static function mode(): string
{
$mode = (string) config('early_growth.mode', 'off');
return in_array($mode, ['off', 'light', 'aggressive'], true) ? $mode : 'off';
}
/** Is the AdaptiveTimeWindow module active? */
public static function adaptiveWindowEnabled(): bool
{
return self::enabled() && (bool) config('early_growth.adaptive_time_window', true);
}
/** Is the GridFiller module active? */
public static function gridFillerEnabled(): bool
{
return self::enabled() && (bool) config('early_growth.grid_filler', true);
}
/** Is the SpotlightEngine module active? */
public static function spotlightEnabled(): bool
{
return self::enabled() && (bool) config('early_growth.spotlight', true);
}
/** Is the optional ActivityLayer module active? */
public static function activityLayerEnabled(): bool
{
return self::enabled() && (bool) config('early_growth.activity_layer', false);
}
/**
* Blend ratios for the current mode.
* Returns proportions for fresh / curated / spotlight slices.
*/
public static function blendRatios(): array
{
$mode = self::mode();
return config("early_growth.blend_ratios.{$mode}", [
'fresh' => 1.0,
'curated' => 0.0,
'spotlight' => 0.0,
]);
}
// ─── Auto-disable logic ───────────────────────────────────────────────────
/**
* Check whether upload volume or active-user count has crossed the
* configured threshold for organic scale, and the system should self-disable.
* Result is cached for 10 minutes to avoid constant DB polling.
*/
private static function shouldAutoDisable(): bool
{
return (bool) Cache::remember('egs.auto_disable_check', 600, function (): bool {
$uploadsThreshold = (int) config('early_growth.auto_disable.uploads_per_day', 50);
$usersThreshold = (int) config('early_growth.auto_disable.active_users', 500);
// Average daily uploads over the last 7 days
$recentUploads = Artwork::query()
->where('is_public', true)
->where('is_approved', true)
->whereNull('deleted_at')
->where('published_at', '>=', now()->subDays(7))
->count();
$uploadsPerDay = $recentUploads / 7;
if ($uploadsPerDay >= $uploadsThreshold) {
return true;
}
// Active users: verified accounts who uploaded in last 30 days
$activeCreators = Artwork::query()
->where('is_public', true)
->where('is_approved', true)
->where('published_at', '>=', now()->subDays(30))
->distinct('user_id')
->count('user_id');
return $activeCreators >= $usersThreshold;
});
}
// ─── Status summary ──────────────────────────────────────────────────────
/**
* Return a summary array suitable for admin panels / logging.
*/
public static function status(): array
{
return [
'enabled' => self::enabled(),
'mode' => self::mode(),
'adaptive_window' => self::adaptiveWindowEnabled(),
'grid_filler' => self::gridFillerEnabled(),
'spotlight' => self::spotlightEnabled(),
'activity_layer' => self::activityLayerEnabled(),
];
}
}