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 <noreply@anthropic.com>
main
parent
5d90c375f7
commit
2d231fe6b8
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@ use LivewireUI\Modal\ModalComponent;
|
|||
class LinkVariants extends ModalComponent
|
||||
{
|
||||
public string $linkUlid = '';
|
||||
|
||||
public int $linkId = 0;
|
||||
|
||||
public string $linkTarget = '';
|
||||
|
||||
/** @var array<int, array{id: int|null, label: string, target_url: string, weight: int}> */
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ use Livewire\Component;
|
|||
class Anomalies extends Component
|
||||
{
|
||||
public string $report = '';
|
||||
|
||||
public bool $loading = false;
|
||||
|
||||
public function detect(): void
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ use Livewire\Component;
|
|||
class Insights extends Component
|
||||
{
|
||||
public string $insights = '';
|
||||
|
||||
public bool $loading = false;
|
||||
|
||||
public function generate(): void
|
||||
|
|
|
|||
|
|
@ -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<int, array<string, mixed>> */
|
||||
|
|
@ -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<Click> */
|
||||
private function baseQuery(): Builder
|
||||
{
|
||||
$workspace = require_workspace();
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<?php
|
||||
|
||||
use App\Listeners\ResetCurrentWorkspace;
|
||||
use Laravel\Octane\Contracts\OperationTerminated;
|
||||
use Laravel\Octane\Events\RequestHandled;
|
||||
use Laravel\Octane\Events\RequestReceived;
|
||||
|
|
@ -73,7 +74,7 @@ return [
|
|||
RequestReceived::class => [
|
||||
...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 => [
|
||||
|
|
|
|||
|
|
@ -1,22 +1,20 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (\DB::getDriverName() === 'mysql') {
|
||||
\DB::statement("ALTER TABLE qr_codes MODIFY COLUMN type ENUM('url','vcard','wifi','pdf','mp3','text','email','phone','sms','location') NOT NULL DEFAULT 'url'");
|
||||
if (DB::getDriverName() === 'mysql') {
|
||||
DB::statement("ALTER TABLE qr_codes MODIFY COLUMN type ENUM('url','vcard','wifi','pdf','mp3','text','email','phone','sms','location') NOT NULL DEFAULT 'url'");
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (\DB::getDriverName() === 'mysql') {
|
||||
\DB::statement("ALTER TABLE qr_codes MODIFY COLUMN type ENUM('url','vcard','wifi','pdf','mp3','text') NOT NULL DEFAULT 'url'");
|
||||
if (DB::getDriverName() === 'mysql') {
|
||||
DB::statement("ALTER TABLE qr_codes MODIFY COLUMN type ENUM('url','vcard','wifi','pdf','mp3','text') NOT NULL DEFAULT 'url'");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
"qrcode": "^1.5.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.60.0",
|
||||
"@tailwindcss/vite": "^4.3.0",
|
||||
"axios": "^1.11.0",
|
||||
"chart.js": "^4.5.1",
|
||||
|
|
@ -519,6 +520,22 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz",
|
||||
"integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.60.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
Loading…
Reference in New Issue