A first-class notification system across the multi-repo stack. Three event sources today, one inbox to triage them, three surfaces to act. Built so nothing blocks on the pending Laravel 11 upgrade or the Reverb deployment story.
Fired when the GTM Monitor poller detects a NEW_VERSION on a container it watches.
app/GtmMonitor/GtmMonitorPoller.phpWhen a QAJob reaches a successful terminal state. Payload carries pass / total / errors_found.
saas-qa-datalayer-apiDistinct from "audit found errors". Fires when the job itself crashed, timed out, or could not run.
saas-qa-datalayer-apiSendNotificationJob resolves recipients (mute-aware), bulk-inserts, marks read / unread / archived. Auto-archive at 30 days, hard-delete at 90.
Badge with unread count (cap "9+"). Drawer with Unread / All tabs, 20 most recent, deep-link on click.
Paginated inbox + filter bar + per-source widgets that deep-link into the GTM Monitor and QA Datalayer modules.
Per event type plus per-project overrides. Absence of row = enabled. Mute rows stored compactly.
Origin APIs dispatch a Laravel job whose class lives in core-api. Confirm this works through the shared queue, or fall back to a named-queue message that a core-api worker consumes.
grep -rn "dispatch(new " saas-qa-datalayer-api/app | head grep -rn "Bus::dispatch" saas-core-api | head
GET /notifications defaults client_id to the user's currently active client. Standardize where core-api reads that from (request header, session, query param) to match the rest of the app.
The plan assumes a version bump + composer/yarn update across all backends and the frontend. Confirm the exact mechanism (path repo, packagist, git tag) used today.
cat commons/composer.json grep -A2 commons saas-core-api/composer.json
The action_url field carries the URL the "View" button opens. Drafts: /projects/{id}/gtm-monitor?version=… and /projects/{id}/qa-datalayer/jobs/{id}. Confirm before hard-coding into the emitters.
v1 ships with polling because Joel hasn't figured out Reverb infra yet and the backends are still on Laravel 10 (Reverb wants 11). When both unblock, broadcasting drops in as an additive layer. Frontend switches polling off while the WebSocket is connected and falls back if it drops.
All four backends bumped together.
"laravel/framework": "^11.0"laravel/reverb installed centrally; BROADCAST_DRIVER + REVERB_* set in every emitter backend.
ShouldBroadcast on PrivateChannel("user.{id}.notifications"). Used by SendNotificationJob.
Single line in SendNotificationJob, already marked with a TODO. No other backend change.
Wire-up in startPolling(). Polling pauses while connected; resumes on disconnect.