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:
149
app/Services/EarlyGrowth/EarlyGrowth.php
Normal file
149
app/Services/EarlyGrowth/EarlyGrowth.php
Normal 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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user