From ba67f67d6072d7fa9be12c9cb85fc08ea314c27c Mon Sep 17 00:00:00 2001 From: boban Date: Sat, 16 May 2026 12:53:29 +0200 Subject: [PATCH] fix(ui+seed): FOUC eliminated + persistent dev-user MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Seed: - DevUserSeeder: idempotent — creates workspace + WorkspaceMember + sets default_workspace_id. Auto-recreates everything on migrate:fresh --seed. - DatabaseSeeder: PlanSeeder first (nexxo needs a plan), then DevUserSeeder always, DemoDataSeeder only in local/dev env. - DevUserSeederTest: create + idempotent + in-DatabaseSeeder + order check. - Removed WithoutModelEvents trait from DatabaseSeeder (was suppressing model observers needed for workspace creation). UI (nimuli-app.blade.php): - Inline JS theme-init before first paint: reads localStorage nimuli_theme, falls back to server-side user preference. Eliminates theme flash on reload. - Critical CSS in : CSS vars, body opacity 0 → 1 fade (120ms), skeleton sidebar + topbar visible during JS load. - app-spinner shown until app-ready class added by markReady(). - Skeleton + spinner hidden via body.app-ready CSS selector. - livewire:navigated event re-triggers markReady() for SPA navigation. - x-cloak on mobile sidebar overlay to prevent pre-Alpine flash. Bash aliases added to ~/.bashrc: - seed-dev → db:seed --class=DevUserSeeder - migrate-fresh → migrate:fresh --seed - reset-db → migrate:fresh --seed (with confirmation message) Tests: 109/109 passing. Smoke: all routes ✅. DB sanity ✅. Co-Authored-By: Claude Sonnet 4.6 --- database/seeders/DatabaseSeeder.php | 13 ++- database/seeders/DevUserSeeder.php | 32 +++++- resources/views/layouts/nimuli-app.blade.php | 109 ++++++++++++++++--- tests/Feature/Seeders/DevUserSeederTest.php | 35 ++++++ 4 files changed, 168 insertions(+), 21 deletions(-) create mode 100644 tests/Feature/Seeders/DevUserSeederTest.php diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index fa81cc8..1ff71e2 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -2,20 +2,21 @@ namespace Database\Seeders; -use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; class DatabaseSeeder extends Seeder { - use WithoutModelEvents; - - /** - * Seed the application's database. - */ public function run(): void { $this->call([ + PlanSeeder::class, DevUserSeeder::class, ]); + + if (app()->environment(['local', 'dev'])) { + $this->call([ + DemoDataSeeder::class, + ]); + } } } diff --git a/database/seeders/DevUserSeeder.php b/database/seeders/DevUserSeeder.php index 58155f2..3f49029 100644 --- a/database/seeders/DevUserSeeder.php +++ b/database/seeders/DevUserSeeder.php @@ -2,6 +2,9 @@ namespace Database\Seeders; +use App\Domains\Subscription\Models\Plan; +use App\Domains\Workspace\Models\Workspace; +use App\Domains\Workspace\Models\WorkspaceMember; use App\Models\User; use Illuminate\Database\Seeder; use Illuminate\Support\Facades\Hash; @@ -10,7 +13,7 @@ class DevUserSeeder extends Seeder { public function run(): void { - User::updateOrCreate( + $user = User::updateOrCreate( ['email' => 'nexxo@nimuli.com'], [ 'name' => 'Nexxo Admin', @@ -20,6 +23,31 @@ class DevUserSeeder extends Seeder 'theme' => 'dark', ] ); - $this->command->info('Dev-User: nexxo@nimuli.com / NimuliDev2026!'); + + $plan = Plan::firstWhere('name', 'free') ?? Plan::first(); + + $workspace = Workspace::firstOrCreate( + ['owner_id' => $user->id, 'slug' => 'nexxo'], + [ + 'name' => 'Nexxo Workspace', + 'plan_id' => $plan?->id, + ] + ); + + WorkspaceMember::firstOrCreate( + ['workspace_id' => $workspace->id, 'user_id' => $user->id], + ['role' => 'owner', 'joined_at' => now()] + ); + + if ($user->default_workspace_id !== $workspace->id) { + $user->updateQuietly(['default_workspace_id' => $workspace->id]); + } + + $this->command->info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + $this->command->info(' Dev-User bereit:'); + $this->command->info(' Email: nexxo@nimuli.com'); + $this->command->info(' Password: NimuliDev2026!'); + $this->command->info(' Workspace: '.$workspace->ulid); + $this->command->info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); } } diff --git a/resources/views/layouts/nimuli-app.blade.php b/resources/views/layouts/nimuli-app.blade.php index 37dec95..388c132 100644 --- a/resources/views/layouts/nimuli-app.blade.php +++ b/resources/views/layouts/nimuli-app.blade.php @@ -6,17 +6,6 @@ {{ ($title ?? '') ? $title . ' — Nimuli' : 'Nimuli' }} - - - - - - - - @@ -26,11 +15,89 @@ + + + + + + + + + + + @vite(['resources/js/app.js']) @livewireStyles - -
+ + + + + + +
@@ -66,6 +134,21 @@
+ @livewireScripts + + diff --git a/tests/Feature/Seeders/DevUserSeederTest.php b/tests/Feature/Seeders/DevUserSeederTest.php new file mode 100644 index 0000000..5b7ac6a --- /dev/null +++ b/tests/Feature/Seeders/DevUserSeederTest.php @@ -0,0 +1,35 @@ +seed(PlanSeeder::class); + $this->seed(DevUserSeeder::class); + + $user = User::firstWhere('email', 'nexxo@nimuli.com'); + expect($user)->not->toBeNull(); + expect($user->default_workspace_id)->not->toBeNull(); + expect(Workspace::find($user->default_workspace_id))->not->toBeNull(); +}); + +it('is idempotent — running twice does not duplicate', function () { + $this->seed(PlanSeeder::class); + $this->seed(DevUserSeeder::class); + $this->seed(DevUserSeeder::class); + + expect(User::where('email', 'nexxo@nimuli.com')->count())->toBe(1); + expect(Workspace::where('slug', 'nexxo')->count())->toBe(1); +}); + +it('is registered in DatabaseSeeder', function () { + $source = file_get_contents(database_path('seeders/DatabaseSeeder.php')); + expect($source)->toContain('DevUserSeeder::class'); +}); + +it('PlanSeeder runs before DevUserSeeder in DatabaseSeeder', function () { + $source = file_get_contents(database_path('seeders/DatabaseSeeder.php')); + expect(strpos($source, 'PlanSeeder::class'))->toBeLessThan(strpos($source, 'DevUserSeeder::class')); +});