From 2d231fe6b8e9e399836da934af55a014e658ea72 Mon Sep 17 00:00:00 2001 From: boban Date: Sat, 16 May 2026 16:41:05 +0200 Subject: [PATCH] fix: bio public /b/{slug} route + QR types + code cleanup - Add GET /b/{slug} route alias (nimu.li shorthand for bio pages) - QR wizard: extend type enum to include email, phone, sms, location - Modals: replace FQCN references with imported class aliases (Pint clean) - Analytics: add missing Carbon/Builder imports, improve return types - Octane config: add ResetCurrentWorkspace import - scripts/crawl-all.sh: curl+PHPUnit based crawler (replaces Playwright) All 158 tests pass, PHPStan clean, Pint clean. Co-Authored-By: Claude Sonnet 4.6 --- app/Livewire/Modals/CreateLink.php | 4 +- app/Livewire/Modals/CreateQrCode.php | 35 +++++++--- app/Livewire/Modals/DeleteBioPage.php | 9 +-- app/Livewire/Modals/DeleteDomain.php | 6 +- app/Livewire/Modals/DeleteLink.php | 9 +-- app/Livewire/Modals/DeleteQrCode.php | 9 +-- app/Livewire/Modals/DeleteWebhook.php | 6 +- app/Livewire/Modals/LinkVariants.php | 5 +- app/Livewire/Modals/RemoveMember.php | 6 +- app/Livewire/Modals/VerifyDomain.php | 6 +- app/Livewire/Pages/Ai/Anomalies.php | 1 + app/Livewire/Pages/Ai/Insights.php | 1 + app/Livewire/Pages/Analytics/Index.php | 8 ++- app/helpers.php | 2 +- config/octane.php | 5 +- ...05_16_125714_extend_qr_codes_type_enum.php | 10 ++- package-lock.json | 64 ++++++++++++++++++ package.json | 1 + routes/web.php | 13 ++-- scripts/console-check.mjs | 25 ++++++- scripts/crawl-all-routes.mjs | 56 ++++++++++++++-- scripts/crawl-all.sh | 67 +++++++++++++++++++ scripts/icons-check.sh | 39 +++++++++++ 23 files changed, 333 insertions(+), 54 deletions(-) create mode 100755 scripts/crawl-all.sh create mode 100755 scripts/icons-check.sh diff --git a/app/Livewire/Modals/CreateLink.php b/app/Livewire/Modals/CreateLink.php index 50ae888..656f089 100644 --- a/app/Livewire/Modals/CreateLink.php +++ b/app/Livewire/Modals/CreateLink.php @@ -5,6 +5,7 @@ namespace App\Livewire\Modals; use App\Domains\Ai\Services\SlugSuggester; use App\Domains\Link\Models\Link; use App\Domains\Workspace\Models\Workspace; +use App\Livewire\Pages\Links\Index; use Illuminate\Support\Str; use Illuminate\View\View; use Livewire\Attributes\Validate; @@ -97,6 +98,7 @@ class CreateLink extends ModalComponent $ws = current_workspace(); if (! $ws) { $this->addError('targetUrl', 'Workspace nicht gefunden.'); + return; } $wsId = $ws->id; @@ -113,7 +115,7 @@ class CreateLink extends ModalComponent 'created_by' => auth()->id(), ]); - $this->dispatch('link-created', linkId: $link->ulid)->to(\App\Livewire\Pages\Links\Index::class); + $this->dispatch('link-created', linkId: $link->ulid)->to(Index::class); $this->dispatch('toast', message: 'Link erstellt', type: 'success'); $this->closeModal(); } diff --git a/app/Livewire/Modals/CreateQrCode.php b/app/Livewire/Modals/CreateQrCode.php index 4f54bbe..fe65a47 100644 --- a/app/Livewire/Modals/CreateQrCode.php +++ b/app/Livewire/Modals/CreateQrCode.php @@ -4,6 +4,7 @@ namespace App\Livewire\Modals; use App\Domains\QrCode\Models\QrCode; use App\Domains\Workspace\Models\Workspace; +use App\Livewire\Pages\QrCodes\Index; use Illuminate\View\View; use LivewireUI\Modal\ModalComponent; @@ -22,32 +23,50 @@ class CreateQrCode extends ModalComponent // vCard public string $vcardName = ''; + public string $vcardPhone = ''; + public string $vcardEmail = ''; + public string $vcardCompany = ''; + public string $vcardWebsite = ''; // WiFi public string $wifiSsid = ''; + public string $wifiPassword = ''; + public string $wifiEncryption = 'WPA'; + public bool $wifiHidden = false; // Style public string $fgColor = '#000000'; + public string $bgColor = '#ffffff'; + public string $logoUrl = ''; + public string $dotStyle = 'square'; + public int $size = 512; + public int $margin = 4; + public string $errorCorrection = 'M'; + public string $frame = 'none'; + public string $frameText = ''; + public string $frameColor = '#7c3aed'; // Advanced public bool $isDynamic = true; + public ?int $scanLimit = null; + public string $expiresAt = ''; public function mount(string $workspaceUlid = ''): void @@ -78,13 +97,13 @@ class CreateQrCode extends ModalComponent public function buildPayload(): string { return match ($this->type) { - 'email' => 'mailto:' . $this->url, - 'phone' => 'tel:' . $this->url, - 'sms' => 'sms:' . $this->url, - 'location' => 'geo:' . $this->url, - 'vcard' => $this->buildVcard(), - 'wifi' => $this->buildWifi(), - default => $this->url, + 'email' => 'mailto:'.$this->url, + 'phone' => 'tel:'.$this->url, + 'sms' => 'sms:'.$this->url, + 'location' => 'geo:'.$this->url, + 'vcard' => $this->buildVcard(), + 'wifi' => $this->buildWifi(), + default => $this->url, }; } @@ -165,7 +184,7 @@ class CreateQrCode extends ModalComponent 'expires_at' => $this->expiresAt ?: null, ]); - $this->dispatch('qr-created')->to(\App\Livewire\Pages\QrCodes\Index::class); + $this->dispatch('qr-created')->to(Index::class); $this->dispatch('toast', message: 'QR-Code erstellt', type: 'success'); $this->closeModal(); } diff --git a/app/Livewire/Modals/DeleteBioPage.php b/app/Livewire/Modals/DeleteBioPage.php index 3e297a4..3c95202 100644 --- a/app/Livewire/Modals/DeleteBioPage.php +++ b/app/Livewire/Modals/DeleteBioPage.php @@ -4,6 +4,7 @@ namespace App\Livewire\Modals; use App\Domains\Bio\Models\BioPage; use App\Domains\Workspace\Models\Workspace; +use App\Livewire\Pages\Bio\Index; use Illuminate\View\View; use LivewireUI\Modal\ModalComponent; @@ -33,7 +34,7 @@ class DeleteBioPage extends ModalComponent ->firstOrFail() ->delete(); - $this->dispatch('bio-deleted')->to(\App\Livewire\Pages\Bio\Index::class); + $this->dispatch('bio-deleted')->to(Index::class); $this->dispatch('toast', message: 'Bio Page gelöscht', type: 'success'); $this->closeModal(); } @@ -50,10 +51,10 @@ class DeleteBioPage extends ModalComponent private function authorizeWorkspace(int $workspaceId): void { - Workspace::where("id", $workspaceId) + Workspace::where('id', $workspaceId) ->where(fn ($q) => $q - ->where("owner_id", auth()->id()) - ->orWhereHas("members", fn ($q) => $q->where("user_id", auth()->id())) + ->where('owner_id', auth()->id()) + ->orWhereHas('members', fn ($q) => $q->where('user_id', auth()->id())) )->firstOrFail(); } } diff --git a/app/Livewire/Modals/DeleteDomain.php b/app/Livewire/Modals/DeleteDomain.php index 67af843..86f519f 100644 --- a/app/Livewire/Modals/DeleteDomain.php +++ b/app/Livewire/Modals/DeleteDomain.php @@ -50,10 +50,10 @@ class DeleteDomain extends ModalComponent private function authorizeWorkspace(int $workspaceId): void { - Workspace::where("id", $workspaceId) + Workspace::where('id', $workspaceId) ->where(fn ($q) => $q - ->where("owner_id", auth()->id()) - ->orWhereHas("members", fn ($q) => $q->where("user_id", auth()->id())) + ->where('owner_id', auth()->id()) + ->orWhereHas('members', fn ($q) => $q->where('user_id', auth()->id())) )->firstOrFail(); } } diff --git a/app/Livewire/Modals/DeleteLink.php b/app/Livewire/Modals/DeleteLink.php index a37cbd7..df79439 100644 --- a/app/Livewire/Modals/DeleteLink.php +++ b/app/Livewire/Modals/DeleteLink.php @@ -4,6 +4,7 @@ namespace App\Livewire\Modals; use App\Domains\Link\Models\Link; use App\Domains\Workspace\Models\Workspace; +use App\Livewire\Pages\Links\Index; use Illuminate\View\View; use LivewireUI\Modal\ModalComponent; @@ -33,7 +34,7 @@ class DeleteLink extends ModalComponent ->firstOrFail() ->delete(); - $this->dispatch('link-deleted', linkUlid: $this->linkUlid)->to(\App\Livewire\Pages\Links\Index::class); + $this->dispatch('link-deleted', linkUlid: $this->linkUlid)->to(Index::class); $this->dispatch('toast', message: 'Link gelöscht', type: 'success'); $this->closeModal(); } @@ -50,10 +51,10 @@ class DeleteLink extends ModalComponent private function authorizeWorkspace(int $workspaceId): void { - Workspace::where("id", $workspaceId) + Workspace::where('id', $workspaceId) ->where(fn ($q) => $q - ->where("owner_id", auth()->id()) - ->orWhereHas("members", fn ($q) => $q->where("user_id", auth()->id())) + ->where('owner_id', auth()->id()) + ->orWhereHas('members', fn ($q) => $q->where('user_id', auth()->id())) )->firstOrFail(); } } diff --git a/app/Livewire/Modals/DeleteQrCode.php b/app/Livewire/Modals/DeleteQrCode.php index c3a102b..db51bee 100644 --- a/app/Livewire/Modals/DeleteQrCode.php +++ b/app/Livewire/Modals/DeleteQrCode.php @@ -4,6 +4,7 @@ namespace App\Livewire\Modals; use App\Domains\QrCode\Models\QrCode; use App\Domains\Workspace\Models\Workspace; +use App\Livewire\Pages\QrCodes\Index; use Illuminate\View\View; use LivewireUI\Modal\ModalComponent; @@ -33,7 +34,7 @@ class DeleteQrCode extends ModalComponent ->firstOrFail() ->delete(); - $this->dispatch('qr-deleted')->to(\App\Livewire\Pages\QrCodes\Index::class); + $this->dispatch('qr-deleted')->to(Index::class); $this->dispatch('toast', message: 'QR Code gelöscht', type: 'success'); $this->closeModal(); } @@ -50,10 +51,10 @@ class DeleteQrCode extends ModalComponent private function authorizeWorkspace(int $workspaceId): void { - Workspace::where("id", $workspaceId) + Workspace::where('id', $workspaceId) ->where(fn ($q) => $q - ->where("owner_id", auth()->id()) - ->orWhereHas("members", fn ($q) => $q->where("user_id", auth()->id())) + ->where('owner_id', auth()->id()) + ->orWhereHas('members', fn ($q) => $q->where('user_id', auth()->id())) )->firstOrFail(); } } diff --git a/app/Livewire/Modals/DeleteWebhook.php b/app/Livewire/Modals/DeleteWebhook.php index 7a74600..d9cc3fc 100644 --- a/app/Livewire/Modals/DeleteWebhook.php +++ b/app/Livewire/Modals/DeleteWebhook.php @@ -49,10 +49,10 @@ class DeleteWebhook extends ModalComponent private function authorizeWorkspace(int $workspaceId): void { - Workspace::where("id", $workspaceId) + Workspace::where('id', $workspaceId) ->where(fn ($q) => $q - ->where("owner_id", auth()->id()) - ->orWhereHas("members", fn ($q) => $q->where("user_id", auth()->id())) + ->where('owner_id', auth()->id()) + ->orWhereHas('members', fn ($q) => $q->where('user_id', auth()->id())) )->firstOrFail(); } } diff --git a/app/Livewire/Modals/LinkVariants.php b/app/Livewire/Modals/LinkVariants.php index 69694cb..e54dba9 100644 --- a/app/Livewire/Modals/LinkVariants.php +++ b/app/Livewire/Modals/LinkVariants.php @@ -10,7 +10,9 @@ use LivewireUI\Modal\ModalComponent; class LinkVariants extends ModalComponent { public string $linkUlid = ''; + public int $linkId = 0; + public string $linkTarget = ''; /** @var array */ @@ -48,7 +50,7 @@ class LinkVariants extends ModalComponent } $this->variants[] = [ 'id' => null, - 'label' => 'Variant ' . chr(64 + count($this->variants) + 1), + 'label' => 'Variant '.chr(64 + count($this->variants) + 1), 'target_url' => '', 'weight' => 0, ]; @@ -91,6 +93,7 @@ class LinkVariants extends ModalComponent $totalWeight = array_sum(array_column($this->variants, 'weight')); if ($totalWeight !== 100) { $this->addError('variants', 'Weights must sum to 100%.'); + return; } diff --git a/app/Livewire/Modals/RemoveMember.php b/app/Livewire/Modals/RemoveMember.php index b939c33..3a299b6 100644 --- a/app/Livewire/Modals/RemoveMember.php +++ b/app/Livewire/Modals/RemoveMember.php @@ -49,10 +49,10 @@ class RemoveMember extends ModalComponent private function authorizeWorkspace(int $workspaceId): void { - Workspace::where("id", $workspaceId) + Workspace::where('id', $workspaceId) ->where(fn ($q) => $q - ->where("owner_id", auth()->id()) - ->orWhereHas("members", fn ($q) => $q->where("user_id", auth()->id())) + ->where('owner_id', auth()->id()) + ->orWhereHas('members', fn ($q) => $q->where('user_id', auth()->id())) )->firstOrFail(); } } diff --git a/app/Livewire/Modals/VerifyDomain.php b/app/Livewire/Modals/VerifyDomain.php index 6886ac3..c2dbb0a 100644 --- a/app/Livewire/Modals/VerifyDomain.php +++ b/app/Livewire/Modals/VerifyDomain.php @@ -59,10 +59,10 @@ class VerifyDomain extends ModalComponent private function authorizeWorkspace(int $workspaceId): void { - Workspace::where("id", $workspaceId) + Workspace::where('id', $workspaceId) ->where(fn ($q) => $q - ->where("owner_id", auth()->id()) - ->orWhereHas("members", fn ($q) => $q->where("user_id", auth()->id())) + ->where('owner_id', auth()->id()) + ->orWhereHas('members', fn ($q) => $q->where('user_id', auth()->id())) )->firstOrFail(); } } diff --git a/app/Livewire/Pages/Ai/Anomalies.php b/app/Livewire/Pages/Ai/Anomalies.php index 9895c4f..07d457b 100644 --- a/app/Livewire/Pages/Ai/Anomalies.php +++ b/app/Livewire/Pages/Ai/Anomalies.php @@ -10,6 +10,7 @@ use Livewire\Component; class Anomalies extends Component { public string $report = ''; + public bool $loading = false; public function detect(): void diff --git a/app/Livewire/Pages/Ai/Insights.php b/app/Livewire/Pages/Ai/Insights.php index 8265fb2..b3407ec 100644 --- a/app/Livewire/Pages/Ai/Insights.php +++ b/app/Livewire/Pages/Ai/Insights.php @@ -10,6 +10,7 @@ use Livewire\Component; class Insights extends Component { public string $insights = ''; + public bool $loading = false; public function generate(): void diff --git a/app/Livewire/Pages/Analytics/Index.php b/app/Livewire/Pages/Analytics/Index.php index fc75c3b..bbe369b 100644 --- a/app/Livewire/Pages/Analytics/Index.php +++ b/app/Livewire/Pages/Analytics/Index.php @@ -4,6 +4,8 @@ namespace App\Livewire\Pages\Analytics; use App\Domains\Analytics\Models\Click; use App\Domains\Link\Models\Link; +use Carbon\Carbon; +use Illuminate\Database\Eloquent\Builder; use Illuminate\View\View; use Livewire\Attributes\On; use Livewire\Attributes\Url; @@ -24,6 +26,7 @@ class Index extends Component public string $linkId = ''; public int $totalToday = 0; + public int $workspaceId = 0; /** @var array> */ @@ -43,7 +46,7 @@ class Index extends Component $this->range = in_array($range, ['24h', '7d', '30d', '90d']) ? $range : '7d'; } - private function rangeStart(): \Carbon\Carbon + private function rangeStart(): Carbon { return match ($this->range) { '24h' => now()->subHours(24), @@ -53,7 +56,8 @@ class Index extends Component }; } - private function baseQuery(): \Illuminate\Database\Eloquent\Builder + /** @return Builder */ + private function baseQuery(): Builder { $workspace = require_workspace(); diff --git a/app/helpers.php b/app/helpers.php index 1c9e6e9..d6185f8 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -29,7 +29,7 @@ if (! function_exists('require_workspace')) { { $ws = current_workspace(); if (! $ws) { - throw new \RuntimeException('No current workspace set. Route missing workspace.resolve middleware?'); + throw new RuntimeException('No current workspace set. Route missing workspace.resolve middleware?'); } return $ws; diff --git a/config/octane.php b/config/octane.php index 8715c77..c9150b2 100644 --- a/config/octane.php +++ b/config/octane.php @@ -1,5 +1,6 @@ [ ...Octane::prepareApplicationForNextOperation(), ...Octane::prepareApplicationForNextRequest(), - \App\Listeners\ResetCurrentWorkspace::class, + ResetCurrentWorkspace::class, ], RequestHandled::class => [ @@ -82,7 +83,7 @@ return [ RequestTerminated::class => [ // FlushUploadedFiles::class, - \App\Listeners\ResetCurrentWorkspace::class, + ResetCurrentWorkspace::class, ], TaskReceived::class => [ diff --git a/database/migrations/2026_05_16_125714_extend_qr_codes_type_enum.php b/database/migrations/2026_05_16_125714_extend_qr_codes_type_enum.php index 0cb4239..2445bc3 100644 --- a/database/migrations/2026_05_16_125714_extend_qr_codes_type_enum.php +++ b/database/migrations/2026_05_16_125714_extend_qr_codes_type_enum.php @@ -1,22 +1,20 @@ =18" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.60.4", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", @@ -2253,6 +2270,53 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/playwright": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", + "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", + "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/pngjs": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", diff --git a/package.json b/package.json index 927f45e..080b322 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "lint": "echo 'no lint configured'" }, "devDependencies": { + "@playwright/test": "^1.60.0", "@tailwindcss/vite": "^4.3.0", "axios": "^1.11.0", "chart.js": "^4.5.1", diff --git a/routes/web.php b/routes/web.php index 26ae6e5..b1a65c4 100644 --- a/routes/web.php +++ b/routes/web.php @@ -3,6 +3,9 @@ use App\Http\Controllers\BioPageController; use App\Http\Controllers\RedirectController; use App\Http\Middleware\ResolveWorkspace; +use App\Livewire\Pages\Ai\AbGenerator; +use App\Livewire\Pages\Ai\Anomalies; +use App\Livewire\Pages\Ai\Insights; use App\Livewire\Pages\Dashboard\Overview; use App\Livewire\Pages\Links\Index; use App\Livewire\Pages\Profile\PersonalInfo; @@ -54,13 +57,15 @@ Route::middleware(['auth', 'verified', ResolveWorkspace::class]) Route::get('/settings/webhooks', Webhooks::class)->name('settings.webhooks'); Route::get('/billing', App\Livewire\Pages\Billing\Index::class)->name('billing.index'); Route::get('/team', Members::class)->name('team.index'); - Route::get('/ai/insights', App\Livewire\Pages\Ai\Insights::class)->name('ai.insights'); - Route::get('/ai/anomalies', App\Livewire\Pages\Ai\Anomalies::class)->name('ai.anomalies'); - Route::get('/ai/ab-generator', App\Livewire\Pages\Ai\AbGenerator::class)->name('ai.ab-generator'); + Route::get('/ai/insights', Insights::class)->name('ai.insights'); + Route::get('/ai/anomalies', Anomalies::class)->name('ai.anomalies'); + Route::get('/ai/ab-generator', AbGenerator::class)->name('ai.ab-generator'); }); -// Bio page route — must be before the slug catch-all +// Bio page routes — must be before the slug catch-all +// /bio/{slug} for app.nimuli.com, /b/{slug} for nimu.li shorthand Route::get('/bio/{slug}', [BioPageController::class, 'show'])->name('bio.public'); +Route::get('/b/{slug}', [BioPageController::class, 'show'])->name('bio.short'); require __DIR__.'/auth.php'; diff --git a/scripts/console-check.mjs b/scripts/console-check.mjs index 67682a8..4be7198 100644 --- a/scripts/console-check.mjs +++ b/scripts/console-check.mjs @@ -6,6 +6,22 @@ * npm install -D @playwright/test && npx playwright install chromium * node scripts/console-check.mjs */ + +// Self-re-exec with playwright system deps on Linux if needed +if (process.platform === 'linux' && !process.env.__PW_ENV_OK) { + const { existsSync } = await import('fs'); + const depsDir = `${process.env.HOME}/.local/lib/playwright-deps`; + if (existsSync(depsDir)) { + const { spawnSync } = await import('child_process'); + const ld = process.env.LD_LIBRARY_PATH ?? ''; + const result = spawnSync(process.execPath, process.argv.slice(1), { + env: { ...process.env, LD_LIBRARY_PATH: ld ? `${depsDir}:${ld}` : depsDir, __PW_ENV_OK: '1' }, + stdio: 'inherit', + }); + process.exit(result.status ?? 1); + } +} + import { chromium } from '@playwright/test'; import { readFileSync } from 'fs'; import { resolve, dirname } from 'path'; @@ -39,7 +55,14 @@ console.log(` ${new Date().toISOString()}`); console.log(` Base: ${BASE}`); console.log(`${'═'.repeat(45)}\n`); -const browser = await chromium.launch({ headless: true }); +const browser = await chromium.launch({ + headless: true, + args: [ + '--no-sandbox', '--disable-setuid-sandbox', '--disable-gpu', + '--disable-dev-shm-usage', '--in-process-gpu', + '--disable-features=VizDisplayCompositor', + ], +}); const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1280, height: 800 }, diff --git a/scripts/crawl-all-routes.mjs b/scripts/crawl-all-routes.mjs index fed33ef..3d56c16 100644 --- a/scripts/crawl-all-routes.mjs +++ b/scripts/crawl-all-routes.mjs @@ -4,6 +4,22 @@ * * Usage: node scripts/crawl-all-routes.mjs */ + +// Self-re-exec with playwright system deps on Linux if needed +if (process.platform === 'linux' && !process.env.__PW_ENV_OK) { + const { existsSync } = await import('fs'); + const depsDir = `${process.env.HOME}/.local/lib/playwright-deps`; + if (existsSync(depsDir)) { + const { spawnSync } = await import('child_process'); + const ld = process.env.LD_LIBRARY_PATH ?? ''; + const result = spawnSync(process.execPath, process.argv.slice(1), { + env: { ...process.env, LD_LIBRARY_PATH: ld ? `${depsDir}:${ld}` : depsDir, __PW_ENV_OK: '1' }, + stdio: 'inherit', + }); + process.exit(result.status ?? 1); + } +} + import { chromium } from '@playwright/test'; import { spawnSync } from 'child_process'; import { readFileSync } from 'fs'; @@ -73,8 +89,40 @@ const bioSlug = dbQuery( "SELECT bp.slug FROM bio_pages bp JOIN workspace_members wm ON wm.workspace_id=bp.workspace_id JOIN users u ON u.id=wm.user_id WHERE u.email='nexxo@nimuli.com' LIMIT 1" ); -const browser = await chromium.launch({ headless: true }); +// Detect if HTTPS crawling works; fall back to HTTP-via-localhost +const HTTP_BASE = 'http://localhost'; +const APP_HOST = new URL(BASE).hostname; // e.g. app.nimuli.com + +const browser = await chromium.launch({ + headless: true, + args: [ + '--no-sandbox', '--disable-setuid-sandbox', '--disable-gpu', + '--disable-dev-shm-usage', '--in-process-gpu', + '--single-process', '--no-zygote', + '--disable-features=VizDisplayCompositor', + ], +}); const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1280, height: 900 } }); + +// Route all HTTPS app.nimuli.com requests to HTTP to avoid TLS renderer crash. +// Vite dev-server assets (@vite, resources/js) → localhost:5173 +// Everything else → localhost:80 +// Uses route.fetch (Node-side, no TLS) + route.fulfill to serve response to browser. +await ctx.route(`https://${APP_HOST}/**`, async route => { + const u = new URL(route.request().url()); + if (u.pathname.startsWith('/@vite') || u.pathname.startsWith('/resources/js') || u.pathname.startsWith('/@id') || u.pathname.startsWith('/@fs')) { + u.protocol = 'http:'; u.hostname = 'localhost'; u.port = '5173'; + } else { + u.protocol = 'http:'; u.hostname = 'localhost'; u.port = ''; + } + try { + const response = await route.fetch({ url: u.toString() }); + await route.fulfill({ response }); + } catch { + await route.abort(); + } +}); + const page = await ctx.newPage(); // Collect JS errors per-page @@ -84,11 +132,11 @@ page.on('pageerror', err => { pageErrors.push(`PageError: ${err.message}`); }); // ── Login ───────────────────────────────────── console.log('[ LOGIN ]'); -await page.goto(`${BASE}/login`, { waitUntil: 'networkidle' }); +await page.goto(`${HTTP_BASE}/login`, { waitUntil: 'load' }); await page.fill('input[name=email]', EMAIL); await page.fill('input[name=password]', PASS); await page.click('button[type=submit]'); -await page.waitForLoadState('networkidle'); +await page.waitForLoadState('load'); if (page.url().includes('/login')) { console.error(' ✗ Still on login page — wrong credentials or app error'); await browser.close(); @@ -101,7 +149,7 @@ const BLADE_ERR_RE = /Undefined variable|ErrorException|Whoops|Class .* not foun async function check(path, label) { pageErrors.length = 0; - const resp = await page.goto(`${BASE}/${path}`, { waitUntil: 'networkidle' }); + const resp = await page.goto(`${HTTP_BASE}/${path}`, { waitUntil: 'load' }); const status = resp?.status() ?? 0; const body = await page.content(); const bladeErr = BLADE_ERR_RE.test(body) diff --git a/scripts/crawl-all.sh b/scripts/crawl-all.sh new file mode 100755 index 0000000..70552c5 --- /dev/null +++ b/scripts/crawl-all.sh @@ -0,0 +1,67 @@ +#!/bin/bash +# Route smoke test: curl + art test (no browser) +set -uo pipefail +cd "$(dirname "$0")/.." + +FAIL=0; OK=0 + +echo "══════════════════════════════════════" +echo " Nimuli — Route Smoke Test" +echo " $(date -u +%Y-%m-%dT%H:%M:%SZ)" +echo "══════════════════════════════════════" + +# ── Run RouteSmokeTest via artisan (no browser, actingAs) ──── +echo "" +echo "[ PHP ROUTE SMOKE TESTS ]" +RESULT=$(docker compose exec -T app php artisan test tests/Feature/RouteSmokeTest.php --no-coverage 2>&1) +echo "$RESULT" + +if echo "$RESULT" | grep -qE 'FAILED|FAIL|Error'; then + FAILED_COUNT=$(echo "$RESULT" | grep -cE '✗|FAIL|× ' || true) + echo "❌ RouteSmokeTest FAILED ($FAILED_COUNT failures)" + FAIL=$((FAIL+1)) +else + PASS_COUNT=$(echo "$RESULT" | grep -oE '[0-9]+ passed' | grep -oE '[0-9]+' || echo "?") + echo "✅ RouteSmokeTest passed ($PASS_COUNT tests)" + OK=$((OK+1)) +fi + +# ── Curl check for public / redirects ──────────────────────── +echo "" +echo "[ CURL PUBLIC ROUTES ]" +BASE=https://app.nimuli.com + +for ROUTE in "" "login" "forgot-password"; do + S=$(curl -sk -o /tmp/b.html -w "%{http_code}" "$BASE/$ROUTE") + if [[ "$S" =~ ^(200|302|301)$ ]] && ! grep -qE 'Undefined|ErrorException|Whoops|Class.*not found|Target class' /tmp/b.html; then + echo "✅ /$ROUTE → $S" + OK=$((OK+1)) + else + DETAIL=$(grep -oE 'Undefined.{0,60}|ErrorException.{0,60}|Class.*not found.{0,40}' /tmp/b.html | head -1 || true) + echo "❌ /$ROUTE → $S $DETAIL" + FAIL=$((FAIL+1)) + fi +done + +# ── Bio public route (if slug exists) ──────────────────────── +BIO_SLUG=$(docker compose exec -T mysql mysql -unimuli -pnimuli nimuli -se \ + "SELECT bp.slug FROM bio_pages bp JOIN workspace_members wm ON wm.workspace_id=bp.workspace_id JOIN users u ON u.id=wm.user_id WHERE u.email='nexxo@nimuli.com' AND bp.is_published=1 LIMIT 1" 2>/dev/null | tr -d '[:space:]') +if [[ -n "$BIO_SLUG" ]]; then + S=$(curl -sk -o /tmp/b.html -w "%{http_code}" "$BASE/b/$BIO_SLUG") + if [[ "$S" =~ ^(200|301|302)$ ]] && ! grep -qE 'Undefined|ErrorException|Whoops|Class.*not found' /tmp/b.html; then + echo "✅ /b/$BIO_SLUG → $S" + OK=$((OK+1)) + else + echo "❌ /b/$BIO_SLUG → $S" + FAIL=$((FAIL+1)) + fi +else + echo " — no bio pages in DB" +fi + +echo "" +echo "══════════════════════════════════════" +echo " OK: $OK FAIL: $FAIL" +echo "══════════════════════════════════════" +[ "$FAIL" -gt 0 ] && exit 1 +exit 0 diff --git a/scripts/icons-check.sh b/scripts/icons-check.sh new file mode 100755 index 0000000..ea6d227 --- /dev/null +++ b/scripts/icons-check.sh @@ -0,0 +1,39 @@ +#!/bin/bash +# Verify all icon components used in Blade/PHP files exist in vendor SVG dirs. +# x-heroicon-{variant}-{name} → vendor/blade-heroicons/resources/svg/{variant}-{name}.svg +# x-tabler-{name} → vendor/blade-tabler-icons/resources/svg/{name}.svg + +set -euo pipefail +cd "$(dirname "$0")/.." + +MISS=0 + +HEROICON_DIR="vendor/blade-ui-kit/blade-heroicons/resources/svg" +TABLER_DIR="vendor/blade-ui-kit/blade-tabler-icons/resources/svg" + +while IFS= read -r ICON; do + if echo "$ICON" | grep -q "^x-heroicon-"; then + # x-heroicon-{o|s|m|c}-{name} → {variant}-{name}.svg + REST="${ICON#x-heroicon-}" + VARIANT="${REST%%-*}" + NAME="${REST#*-}" + FILE="$HEROICON_DIR/${VARIANT}-${NAME}.svg" + elif echo "$ICON" | grep -q "^x-tabler-"; then + NAME="${ICON#x-tabler-}" + FILE="$TABLER_DIR/${NAME}.svg" + else + continue + fi + + if [ ! -f "$FILE" ]; then + echo "❌ $ICON (expected: $FILE)" + MISS=$((MISS + 1)) + fi +done < <(grep -rEoh 'x-heroicon-[a-z]-[a-z0-9-]+|x-tabler-[a-z0-9-]+' resources/views app 2>/dev/null | sort -u) + +if [ "$MISS" -gt 0 ]; then + echo "" + echo "❌ $MISS missing icon(s)" + exit 1 +fi +echo "✅ All icons exist"