feat/reverb (#20)

Co-authored-by: jon brookes <marshyon@gmail.com>
Reviewed-on: https://codeberg.org/headshed/share-lt/pulls/20
This commit is contained in:
Jon Brookes 2026-02-14 17:49:01 +01:00
parent 74bc17d019
commit 21147af908
30 changed files with 1948 additions and 29 deletions

View file

@ -13,6 +13,7 @@ This application is a Laravel application and its main Laravel ecosystems packag
- laravel/fortify (FORTIFY) - v1
- laravel/framework (LARAVEL) - v12
- laravel/prompts (PROMPTS) - v0
- laravel/reverb (REVERB) - v1
- laravel/sanctum (SANCTUM) - v4
- livewire/flux (FLUXUI_FREE) - v2
- livewire/livewire (LIVEWIRE) - v3
@ -507,4 +508,4 @@ Fortify is a headless authentication backend that provides authentication routes
- Only implement what solves the immediate problem.
- Ask before adding optional infrastructure or configuration sections.
- If a system worked before without something, don't add it "just in case".
- Minimize configuration, complexity, and dependencies.
- Minimize configuration, complexity, and dependencies.

View file

@ -13,6 +13,7 @@ This application is a Laravel application and its main Laravel ecosystems packag
- laravel/fortify (FORTIFY) - v1
- laravel/framework (LARAVEL) - v12
- laravel/prompts (PROMPTS) - v0
- laravel/reverb (REVERB) - v1
- laravel/sanctum (SANCTUM) - v4
- livewire/flux (FLUXUI_FREE) - v2
- livewire/livewire (LIVEWIRE) - v3

View file

@ -0,0 +1,38 @@
<?php
namespace App\Console\Commands;
use App\Events\PreviewSiteBuilt;
use Illuminate\Console\Command;
class SendPreviewSiteBuiltNotification extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'test:preview-site-built {--message=Preview site is built}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Send a test notification that the preview site is built';
/**
* Execute the console command.
*/
public function handle(): void
{
$message = $this->option('message');
$this->info("Command :: Broadcasting preview site built notification: {$message}");
PreviewSiteBuilt::dispatch($message, 'success');
$this->info('Notification broadcasted successfully!');
$this->info('Check your Filament admin panel for the toast notification.');
}
}

View file

@ -0,0 +1,31 @@
<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class PreviewSiteBuilt implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(
public string $message = 'Preview site is built',
public string $type = 'success'
) {}
public function broadcastOn(): array
{
return [
new Channel('filament-notifications'),
];
}
public function broadcastAs(): string
{
return 'preview-site.built';
}
}

30
app/Events/TestEvent.php Normal file
View file

@ -0,0 +1,30 @@
<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class TestEvent implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(
public string $message = 'Test message from Laravel'
) {}
public function broadcastOn(): array
{
return [
new Channel('test-channel'),
];
}
public function broadcastAs(): string
{
return 'test.message';
}
}

View file

@ -0,0 +1,28 @@
<?php
namespace App\Http\Controllers;
use App\Events\PreviewSiteBuilt;
use App\Http\Requests\SendPreviewSiteBuiltNotificationRequest;
use Illuminate\Http\JsonResponse;
class NotificationController extends Controller
{
/**
* Send preview site built notification.
*/
public function sendPreviewSiteBuilt(SendPreviewSiteBuiltNotificationRequest $request): JsonResponse
{
$message = $request->validated()['message'];
PreviewSiteBuilt::dispatch($message, 'success');
return response()->json([
'success' => true,
'message' => 'Preview site built notification sent successfully',
'data' => [
'notification_message' => $message,
],
]);
}
}

View file

@ -0,0 +1,28 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class SendPreviewSiteBuiltNotificationRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return $this->user() && $this->user()->email === config('app.admin_email');
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'message' => 'required|string|max:255',
];
}
}

View file

@ -71,5 +71,155 @@ class AdminPanelProvider extends PanelProvider
PanelsRenderHook::BODY_END,
fn(): string => \Illuminate\Support\Facades\Blade::render('@vite("resources/js/app.js")'),
);
FilamentView::registerRenderHook(
PanelsRenderHook::BODY_END,
function (): string {
return '
<script>
document.addEventListener("DOMContentLoaded", function() {
if (window.Echo) {
<<<<<<< HEAD
=======
>>>>>>> 4143902e603c7e3e25de5e5f49c1116dd5a5743b
console.log("Setting up Filament Reverb notifications...");
console.log("Available globals:", {
FilamentNotification: typeof FilamentNotification,
Livewire: typeof window.Livewire,
filament: typeof window.filament
});
<<<<<<< HEAD
=======
>>>>>>> 4143902e603c7e3e25de5e5f49c1116dd5a5743b
// Listen for preview site built events
window.Echo.channel("filament-notifications")
.listen("preview-site.built", function(event) {
console.log("🎉 Received preview site built event:", event);
// Use Filament v4 notification system
if (typeof FilamentNotification !== "undefined") {
new FilamentNotification()
.title(event.message)
.success()
.duration(5000)
.send();
console.log("✅ Sent via FilamentNotification v4");
} else {
console.warn("FilamentNotification not available");
// Fallback notification
const notification = document.createElement("div");
notification.style.cssText = `
position: fixed !important;
top: 20px !important;
right: 20px !important;
z-index: 999999 !important;
background: #10b981 !important;
color: white !important;
padding: 16px 20px !important;
border-radius: 8px !important;
box-shadow: 0 10px 25px rgba(0,0,0,0.3) !important;
font-family: system-ui, -apple-system, sans-serif !important;
font-size: 14px !important;
font-weight: 500 !important;
max-width: 350px !important;
display: block !important;
opacity: 1 !important;
`;
notification.innerHTML = `
<div style="display: flex; align-items: center; justify-content: space-between;">
<div style="margin-right: 15px;">
${event.message}
</div>
<button onclick="this.parentElement.parentElement.remove()"
style="background: none; border: none; color: white; font-size: 18px; cursor: pointer; padding: 0;">
×
</button>
</div>
`;
document.body.appendChild(notification);
console.log("✅ Created fallback notification");
// Auto-remove after 5 seconds
setTimeout(() => {
if (notification.parentNode) {
notification.remove();
}
}, 5000);
}
})
.listen(".preview-site.built", function(event) {
console.log("🔄 Also listening with dot prefix (the working one):", event);
<<<<<<< HEAD
console.log("message:", event.message);
=======
>>>>>>> 4143902e603c7e3e25de5e5f49c1116dd5a5743b
// Use Filament v4 notification system
if (typeof FilamentNotification !== "undefined") {
new FilamentNotification()
.title(event.message)
.success()
.duration(5000)
.send();
console.log("✅ Sent via FilamentNotification v4 (from dot prefix)");
} else {
console.warn("FilamentNotification not available, creating fallback");
// Fallback notification
const notification = document.createElement("div");
notification.style.cssText = `
position: fixed !important;
top: 20px !important;
right: 20px !important;
z-index: 999999 !important;
background: #3b82f6 !important;
color: white !important;
padding: 16px 20px !important;
border-radius: 8px !important;
box-shadow: 0 10px 25px rgba(0,0,0,0.3) !important;
font-family: system-ui, -apple-system, sans-serif !important;
font-size: 14px !important;
font-weight: 500 !important;
max-width: 350px !important;
display: block !important;
opacity: 1 !important;
`;
notification.innerHTML = `
<div style="display: flex; align-items: center; justify-content: space-between;">
<div style="margin-right: 15px;">
🔄 ${event.message}
</div>
<button onclick="this.parentElement.parentElement.remove()"
style="background: none; border: none; color: white; font-size: 18px; cursor: pointer; padding: 0;">
×
</button>
</div>
`;
document.body.appendChild(notification);
console.log("✅ Created dot prefix fallback notification");
// Auto-remove after 5 seconds
setTimeout(() => {
if (notification.parentNode) {
notification.remove();
}
}, 5000);
}
});
console.log("✅ Event listeners set up for filament-notifications channel");
} else {
console.error("❌ Echo is not available for Filament notifications");
}
});
</script>';
}
);
}
}

View file

@ -9,6 +9,7 @@ return Application::configure(basePath: dirname(__DIR__))
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
channels: __DIR__.'/../routes/channels.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware): void {

View file

@ -1,7 +1,7 @@
#!/usr/bin/env bash
LARAVEL_CONTAINER_NAME="quay.io/marshyon/share-lt"
CONTAINER_LABEL="v0.0.6"
CONTAINER_LABEL="v0.0.7"
CACHE="--no-cache"
# CACHE=""

View file

@ -0,0 +1,11 @@
#!/usr/bin/env bash
URL='http://127.0.0.1:8000'
curl -X POST $URL/api/notifications/preview-site-built \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"message": "Published to LIVE 💯🚀🎯 - site notification!"}'

View file

@ -30,7 +30,7 @@ if [ ! -d /var/www/public/storage ]; then
fi
php artisan config:clear
npm run build
# Start supervisord directly
exec "$@"

View file

@ -12,6 +12,7 @@
"filament/spatie-laravel-tags-plugin": "^4.0",
"laravel/fortify": "^1.30",
"laravel/framework": "^12.0",
"laravel/reverb": "^1.0",
"laravel/sanctum": "^4.0",
"laravel/tinker": "^2.10.1",
"league/flysystem-aws-s3-v3": "^3.0",

1004
composer.lock generated

File diff suppressed because it is too large Load diff

82
config/broadcasting.php Normal file
View file

@ -0,0 +1,82 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Broadcaster
|--------------------------------------------------------------------------
|
| This option controls the default broadcaster that will be used by the
| framework when an event needs to be broadcast. You may set this to
| any of the connections defined in the "connections" array below.
|
| Supported: "reverb", "pusher", "ably", "redis", "log", "null"
|
*/
'default' => env('BROADCAST_CONNECTION', 'null'),
/*
|--------------------------------------------------------------------------
| Broadcast Connections
|--------------------------------------------------------------------------
|
| Here you may define all of the broadcast connections that will be used
| to broadcast events to other systems or over WebSockets. Samples of
| each available type of connection are provided inside this array.
|
*/
'connections' => [
'reverb' => [
'driver' => 'reverb',
'key' => env('REVERB_APP_KEY'),
'secret' => env('REVERB_APP_SECRET'),
'app_id' => env('REVERB_APP_ID'),
'options' => [
'host' => env('REVERB_HOST'),
'port' => env('REVERB_PORT', 443),
'scheme' => env('REVERB_SCHEME', 'https'),
'useTLS' => env('REVERB_SCHEME', 'https') === 'https',
],
'client_options' => [
// Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html
],
],
'pusher' => [
'driver' => 'pusher',
'key' => env('PUSHER_APP_KEY'),
'secret' => env('PUSHER_APP_SECRET'),
'app_id' => env('PUSHER_APP_ID'),
'options' => [
'cluster' => env('PUSHER_APP_CLUSTER'),
'host' => env('PUSHER_HOST') ?: 'api-'.env('PUSHER_APP_CLUSTER', 'mt1').'.pusher.com',
'port' => env('PUSHER_PORT', 443),
'scheme' => env('PUSHER_SCHEME', 'https'),
'encrypted' => true,
'useTLS' => env('PUSHER_SCHEME', 'https') === 'https',
],
'client_options' => [
// Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html
],
],
'ably' => [
'driver' => 'ably',
'key' => env('ABLY_KEY'),
],
'log' => [
'driver' => 'log',
],
'null' => [
'driver' => 'null',
],
],
];

View file

@ -16,18 +16,18 @@ return [
'broadcasting' => [
// 'echo' => [
// 'broadcaster' => 'pusher',
// 'key' => env('VITE_PUSHER_APP_KEY'),
// 'cluster' => env('VITE_PUSHER_APP_CLUSTER'),
// 'wsHost' => env('VITE_PUSHER_HOST'),
// 'wsPort' => env('VITE_PUSHER_PORT'),
// 'wssPort' => env('VITE_PUSHER_PORT'),
// 'authEndpoint' => '/broadcasting/auth',
// 'disableStats' => true,
// 'encrypted' => true,
// 'forceTLS' => true,
// ],
'echo' => [
'broadcaster' => 'reverb',
'key' => env('VITE_REVERB_APP_KEY'),
'cluster' => env('VITE_REVERB_APP_CLUSTER'),
'wsHost' => env('VITE_REVERB_HOST'),
'wsPort' => env('VITE_REVERB_PORT'),
'wssPort' => env('VITE_REVERB_PORT'),
'authEndpoint' => '/broadcasting/auth',
'disableStats' => true,
'encrypted' => env('VITE_REVERB_SCHEME', 'https') === 'https',
'forceTLS' => env('VITE_REVERB_SCHEME', 'https') === 'https',
],
],

95
config/reverb.php Normal file
View file

@ -0,0 +1,95 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Reverb Server
|--------------------------------------------------------------------------
|
| This option controls the default server used by Reverb to handle
| incoming messages as well as broadcasting message to all your
| connected clients. At this time only "reverb" is supported.
|
*/
'default' => env('REVERB_SERVER', 'reverb'),
/*
|--------------------------------------------------------------------------
| Reverb Servers
|--------------------------------------------------------------------------
|
| Here you may define details for each of the supported Reverb servers.
| Each server has its own configuration options that are defined in
| the array below. You should ensure all the options are present.
|
*/
'servers' => [
'reverb' => [
'host' => env('REVERB_SERVER_HOST', '0.0.0.0'),
'port' => env('REVERB_SERVER_PORT', 8080),
'path' => env('REVERB_SERVER_PATH', ''),
'hostname' => env('REVERB_HOST'),
'options' => [
'tls' => [],
],
'max_request_size' => env('REVERB_MAX_REQUEST_SIZE', 10_000),
'scaling' => [
'enabled' => env('REVERB_SCALING_ENABLED', false),
'channel' => env('REVERB_SCALING_CHANNEL', 'reverb'),
'server' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'port' => env('REDIS_PORT', '6379'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'database' => env('REDIS_DB', '0'),
'timeout' => env('REDIS_TIMEOUT', 60),
],
],
'pulse_ingest_interval' => env('REVERB_PULSE_INGEST_INTERVAL', 15),
'telescope_ingest_interval' => env('REVERB_TELESCOPE_INGEST_INTERVAL', 15),
],
],
/*
|--------------------------------------------------------------------------
| Reverb Applications
|--------------------------------------------------------------------------
|
| Here you may define how Reverb applications are managed. If you choose
| to use the "config" provider, you may define an array of apps which
| your server will support, including their connection credentials.
|
*/
'apps' => [
'provider' => 'config',
'apps' => [
[
'key' => env('REVERB_APP_KEY'),
'secret' => env('REVERB_APP_SECRET'),
'app_id' => env('REVERB_APP_ID'),
'options' => [
'host' => env('REVERB_HOST'),
'port' => env('REVERB_PORT', 443),
'scheme' => env('REVERB_SCHEME', 'https'),
'useTLS' => env('REVERB_SCHEME', 'https') === 'https',
],
'allowed_origins' => ['*'],
'ping_interval' => env('REVERB_APP_PING_INTERVAL', 60),
'activity_timeout' => env('REVERB_APP_ACTIVITY_TIMEOUT', 30),
'max_connections' => env('REVERB_APP_MAX_CONNECTIONS'),
'max_message_size' => env('REVERB_APP_MAX_MESSAGE_SIZE', 10_000),
],
],
],
];

View file

@ -33,3 +33,14 @@ stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
stopwaitsecs=3600
[program:reverb]
command=/usr/local/bin/php /var/www/artisan reverb:start
autostart=true
autorestart=true
startretries=3
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
stopwaitsecs=3600

View file

@ -1,18 +1,18 @@
services:
nats:
image: nats:2.9.19-alpine
restart: unless-stopped
#nats:
# image: nats:2.9.19-alpine
# restart: unless-stopped
#ports:
# - 4222:4222
# - 8222:8222
volumes:
- ./nats/nats-server.conf:/nats-server.conf
- nats-data:/opt/storage
command: ["-c", "/nats-server.conf"]
# volumes:
# - ./nats/nats-server.conf:/nats-server.conf
# - nats-data:/opt/storage
# command: ["-c", "/nats-server.conf"]
networks:
- app-network
- nats
# networks:
# - app-network
# - nats
# nats-cli:
# image: natsio/nats-box
@ -25,7 +25,7 @@ services:
# - nats
app:
image: quay.io/marshyon/share-lt:v0.0.6
image: quay.io/marshyon/share-lt:v0.0.7
restart: unless-stopped
tty: false
working_dir: /var/www
@ -84,12 +84,26 @@ services:
- "AWS_DIRECTORY=${AWS_DIRECTORY}"
- "MEDIA_DISK=${MEDIA_DISK}"
- "VITE_APP_NAME=${APP_NAME}"
- "NATS_URL=${NATS_URL}"
- "NATS_USERNAME=${NATS_USERNAME}"
- "NATS_PASSWORD=${NATS_PASSWORD}"
- "NATS_STREAM=${NATS_STREAM}"
- "NATS_SUBJECT=${NATS_SUBJECT}"
- "REVERB_APP_ID=${REVERB_APP_ID}"
- "REVERB_APP_KEY=${REVERB_APP_KEY}"
- "REVERB_APP_SECRET=${REVERB_APP_SECRET}"
- "REVERB_HOST=${REVERB_HOST}"
- "REVERB_PORT=${REVERB_PORT}"
- "REVERB_SCHEME=${REVERB_SCHEME}"
- "REVERB_SERVER_HOST=${REVERB_SERVER_HOST}"
- "REVERB_SERVER_PORT=${REVERB_SERVER_PORT}"
- "VITE_REVERB_APP_KEY=${REVERB_APP_KEY}"
- "VITE_REVERB_HOST=${REVERB_HOST}"
- "VITE_REVERB_PORT=${REVERB_PORT}"
- "VITE_REVERB_SCHEME=${REVERB_SCHEME}"
- "BROADCAST_CONNECTION=${BROADCAST_CONNECTION}"
volumes:
storage-data:

View file

@ -9,6 +9,25 @@ server {
# access_log /var/log/nginx/access.log;
access_log /dev/stdout;
root /var/www/public;
location ~ ^/(app|apps) {
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
# This is the "Don't Crash" magic for Reverb
# It tells Reverb the original connection was HTTPS
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# Forward to Reverb's internal port
proxy_pass http://127.0.0.1:9001;
}
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;

163
package-lock.json generated
View file

@ -1,5 +1,5 @@
{
"name": "share-lt-recovery-zone",
"name": "share-lt",
"lockfileVersion": 3,
"requires": true,
"packages": {
@ -13,6 +13,10 @@
"tailwindcss": "^4.0.7",
"vite": "^7.0.4"
},
"devDependencies": {
"laravel-echo": "^2.3.0",
"pusher-js": "^8.4.0"
},
"optionalDependencies": {
"@rollup/rollup-linux-x64-gnu": "4.9.5",
"@tailwindcss/oxide-linux-x64-gnu": "^4.0.1",
@ -755,6 +759,14 @@
"win32"
]
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/@tailwindcss/node": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.11.tgz",
@ -1272,6 +1284,25 @@
"url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
}
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@ -1316,6 +1347,32 @@
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/engine.io-client": {
"version": "6.6.4",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.4.tgz",
"integrity": "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.4.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.18.3",
"xmlhttprequest-ssl": "~2.1.1"
}
},
"node_modules/engine.io-parser": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/enhanced-resolve": {
"version": "5.18.2",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz",
@ -1643,6 +1700,20 @@
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/laravel-echo": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/laravel-echo/-/laravel-echo-2.3.0.tgz",
"integrity": "sha512-wgHPnnBvfHmu2I58xJ4asZH37Nu6P0472ku6zuoGRLc3zEWwIbpovDLYTiOshDH1SM7rA6AjZTKuu+jYoM1tpQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=20"
},
"peerDependencies": {
"pusher-js": "*",
"socket.io-client": "*"
}
},
"node_modules/laravel-vite-plugin": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-2.0.0.tgz",
@ -1971,6 +2042,14 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
@ -2062,6 +2141,16 @@
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/pusher-js": {
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/pusher-js/-/pusher-js-8.4.0.tgz",
"integrity": "sha512-wp3HqIIUc1GRyu1XrP6m2dgyE9MoCsXVsWNlohj0rjSkLf+a0jLvEyVubdg58oMk7bhjBWnFClgp8jfAa6Ak4Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"tweetnacl": "^1.0.3"
}
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@ -2144,6 +2233,38 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/socket.io-client": {
"version": "4.8.3",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz",
"integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.4.1",
"engine.io-client": "~6.6.1",
"socket.io-parser": "~4.2.4"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-parser": {
"version": "4.2.5",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.5.tgz",
"integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.4.1"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@ -2257,6 +2378,13 @@
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/tweetnacl": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
"integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==",
"dev": true,
"license": "Unlicense"
},
"node_modules/update-browserslist-db": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
@ -2400,6 +2528,39 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/ws": {
"version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xmlhttprequest-ssl": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
"dev": true,
"peer": true,
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",

View file

@ -19,5 +19,9 @@
"@rollup/rollup-linux-x64-gnu": "4.9.5",
"@tailwindcss/oxide-linux-x64-gnu": "^4.0.1",
"lightningcss-linux-x64-gnu": "^1.29.1"
},
"devDependencies": {
"laravel-echo": "^2.3.0",
"pusher-js": "^8.4.0"
}
}

View file

@ -1,4 +1,3 @@
document.addEventListener('livewire:init', () => {
Livewire.on('insert-editor-content', (data) => {
// console.log('Received insert-editor-content data:', data);
@ -44,3 +43,11 @@ document.addEventListener('livewire:init', () => {
});
});
/**
* Echo exposes an expressive API for subscribing to channels and listening
* for events that are broadcast by Laravel. Echo and event broadcasting
* allow your team to quickly build robust real-time web applications.
*/
import './echo';

28
resources/js/echo.js Normal file
View file

@ -0,0 +1,28 @@
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
window.Pusher = Pusher;
// Add error handling and logging
try {
console.log('Initializing Echo with:', {
key: import.meta.env.VITE_REVERB_APP_KEY,
wsHost: import.meta.env.VITE_REVERB_HOST,
wsPort: import.meta.env.VITE_REVERB_PORT,
scheme: import.meta.env.VITE_REVERB_SCHEME
});
window.Echo = new Echo({
broadcaster: 'reverb',
key: import.meta.env.VITE_REVERB_APP_KEY,
wsHost: import.meta.env.VITE_REVERB_HOST,
wsPort: import.meta.env.VITE_REVERB_PORT ?? 80,
wssPort: import.meta.env.VITE_REVERB_PORT ?? 443,
forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https',
enabledTransports: ['ws', 'wss'],
});
console.log('Echo initialized:', window.Echo);
} catch (error) {
console.error('Failed to initialize Echo:', error);
}

View file

@ -0,0 +1,50 @@
<!DOCTYPE html>
<html>
<head>
<title>Reverb Test</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body>
<h1>Reverb Connection Test</h1>
<div id="status">Waiting for Echo...</div>
<div id="messages"></div>
<script>
const status = document.getElementById('status');
const messages = document.getElementById('messages');
// Wait for Echo to be ready
function checkEcho() {
console.log('Checking Echo:', window.Echo);
if (window.Echo && window.Echo !== null) {
status.textContent = 'Echo ready, connecting...';
status.style.color = 'blue';
try {
window.Echo.channel('test-channel')
.listen('test.message', (e) => {
console.log('Received message:', e);
messages.innerHTML += `<p>Received: ${e.message}</p>`;
});
status.textContent = 'Connected to Reverb!';
status.style.color = 'green';
} catch (error) {
status.textContent = `Connection failed: ${error.message}`;
status.style.color = 'red';
console.error('Echo error:', error);
}
} else {
setTimeout(checkEcho, 100); // Check again in 100ms
}
}
// Start checking after DOM loads
document.addEventListener('DOMContentLoaded', checkEcho);
</script>
</body>
</html>

View file

@ -2,6 +2,7 @@
use App\Http\Controllers\CategoryController;
use App\Http\Controllers\EntryController;
use App\Http\Controllers\NotificationController;
use App\Http\Controllers\TextWidgetController;
use Illuminate\Support\Facades\Route;
@ -23,4 +24,6 @@ Route::middleware('auth:sanctum')->group(function () {
Route::get('/text-widgets/{textWidget}', [TextWidgetController::class, 'show']);
Route::put('/text-widgets/{textWidget}', [TextWidgetController::class, 'update']);
Route::delete('/text-widgets/{textWidget}', [TextWidgetController::class, 'destroy']);
Route::post('/notifications/preview-site-built', [NotificationController::class, 'sendPreviewSiteBuilt']);
});

7
routes/channels.php Normal file
View file

@ -0,0 +1,7 @@
<?php
use Illuminate\Support\Facades\Broadcast;
Broadcast::channel('App.Models.User.{id}', function ($user, $id) {
return (int) $user->id === (int) $id;
});

View file

@ -32,4 +32,20 @@ Route::middleware(['auth'])->group(function () {
),
)
->name('two-factor.show');
Route::get('/test-reverb', function () {
event(new App\Events\TestEvent('Hello from Laravel at ' . now()));
return 'Event fired! Check your browser console.';
})->name('test.reverb');
Route::get('/test-reverb-page', function () {
return view('test-reverb');
})->name('test.reverb.page');
Route::get('/test-preview-site-built', function () {
event(new App\Events\PreviewSiteBuilt('Preview site is built at ' . now(), 'success'));
return 'Preview site built notification sent! Check your Filament admin panel for the toast notification.';
})->name('test.preview.site.built');
});

View file

@ -0,0 +1,77 @@
<?php
use App\Events\PreviewSiteBuilt;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Event;
use function Pest\Laravel\actingAs;
use function Pest\Laravel\postJson;
uses(RefreshDatabase::class);
test('send preview site built notification requires authentication', function () {
postJson('/api/notifications/preview-site-built', [
'message' => 'Test notification',
])->assertUnauthorized();
});
test('send preview site built notification requires admin permissions', function () {
$user = User::factory()->create();
actingAs($user)
->postJson('/api/notifications/preview-site-built', [
'message' => 'Test notification',
])
->assertForbidden();
});
test('send preview site built notification validates message requirement', function () {
$adminEmail = config('app.admin_email');
$admin = User::factory()->create(['email' => $adminEmail]);
actingAs($admin)
->postJson('/api/notifications/preview-site-built', [])
->assertUnprocessable()
->assertJsonValidationErrors(['message']);
});
test('send preview site built notification validates message length', function () {
$adminEmail = config('app.admin_email');
$admin = User::factory()->create(['email' => $adminEmail]);
actingAs($admin)
->postJson('/api/notifications/preview-site-built', [
'message' => str_repeat('a', 256), // 256 characters, over the 255 limit
])
->assertUnprocessable()
->assertJsonValidationErrors(['message']);
});
test('send preview site built notification dispatches event successfully', function () {
Event::fake();
$adminEmail = config('app.admin_email');
$admin = User::factory()->create(['email' => $adminEmail]);
$message = 'Test preview site notification';
actingAs($admin)
->postJson('/api/notifications/preview-site-built', [
'message' => $message,
])
->assertSuccessful()
->assertJsonStructure([
'success',
'message',
'data' => [
'notification_message',
],
])
->assertJsonPath('success', true)
->assertJsonPath('data.notification_message', $message);
Event::assertDispatched(PreviewSiteBuilt::class, function ($event) use ($message) {
return $event->message === $message
&& $event->type === 'success';
});
});

View file

@ -0,0 +1,23 @@
<?php
use App\Events\PreviewSiteBuilt;
use Illuminate\Support\Facades\Event;
it('can broadcast preview site built event', function () {
Event::fake();
PreviewSiteBuilt::dispatch('Test preview site is built', 'success');
Event::assertDispatched(PreviewSiteBuilt::class, function ($event) {
return $event->message === 'Test preview site is built'
&& $event->type === 'success';
});
});
it('has correct broadcast channel and event name', function () {
$event = new PreviewSiteBuilt('Test message', 'success');
expect($event->broadcastOn())->toHaveCount(1)
->and($event->broadcastOn()[0]->name)->toBe('filament-notifications')
->and($event->broadcastAs())->toBe('preview-site.built');
});