From ee0571318f3500c310dac8a74312226793da4cde Mon Sep 17 00:00:00 2001 From: Copxer Date: Thu, 30 Apr 2026 23:37:29 -0700 Subject: [PATCH 1/3] =?UTF-8?q?chore(specs):=20draft=20spec=20025=20?= =?UTF-8?q?=E2=80=94=20Overview=20uptime=20KPI=20+=20Reverb=20live=20updat?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Last spec of phase 5. Replaces MOCK_KPIS['uptime'] with a real volume- weighted aggregate across all user's websites, broadcasts every persisted check via Reverb so the Show page reflects live data, and adds a response-time Sparkline of the last 50 checks. --- .../025-overview-and-realtime.md | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 specs/phase-5-monitoring/025-overview-and-realtime.md diff --git a/specs/phase-5-monitoring/025-overview-and-realtime.md b/specs/phase-5-monitoring/025-overview-and-realtime.md new file mode 100644 index 0000000..9517cd5 --- /dev/null +++ b/specs/phase-5-monitoring/025-overview-and-realtime.md @@ -0,0 +1,133 @@ +--- +spec: overview-and-realtime +phase: 5-monitoring +status: in-progress +owner: yoany +created: 2026-04-30 +updated: 2026-04-30 +issue: https://github.com/Copxer/nexus/issues/75 +branch: spec/025-overview-and-realtime +--- + +# 025 — Overview integration + Reverb live updates + perf charts + +## Goal +Close out phase 5 by surfacing website monitoring on Overview (replacing the long-standing `MOCK_KPIS['uptime']` placeholder with a real cross-website aggregate), wiring Reverb broadcasts on every persisted check so the Show page reflects live data, and rendering a response-time line chart on the Show page using the existing `Sparkline` component. After this spec, Phase 5 ships end-to-end: data layer (023) → automation (024) → user-facing dashboard surfaces (025). + +Roadmap reference: §8.8 Website Performance Monitoring (Overview Widget bullets), §19 Phase 5 acceptance ("Performance widget uses real data"). + +## Scope +**In scope:** + +- **`App\Domain\Monitoring\Queries\GetMonitoringUptimeKpiQuery`** — cross-website aggregate driving the Overview Uptime KPI card. + - `execute(): array` returns: + ``` + [ + 'overall' => float|null, // 24h volume-weighted uptime % across all checks + 'change' => float, // delta vs prior 24h; 0 if either window empty + 'sparkline' => array, // length 12, daily uptime % oldest-first + 'status' => 'success'|'warning'|'danger'|'muted', + ] + ``` + - **`overall`** — `successful_checks / total_checks * 100` over **all** websites in the last 24h, rounded to 2 decimals. Volume-weighted (a busy website with one failure matters more than a quiet website with one success). `null` when no checks landed in the window. + - **`change`** — overall − previous-24h overall, rounded to 2 decimals. `0.0` when either window is empty (no signal to compare). + - **`sparkline`** — daily uptime % for each of the last 12 days, oldest-first. Days with no checks at all default to `100.0` ("no news is good news") so a fresh account doesn't render as a 0-percent flatline. Document the limitation; future polish can switch to null + `Sparkline` gap rendering. + - **`status`** — `muted` when `overall` is null; `success` when ≥ 99.0; `warning` when ≥ 95.0; `danger` otherwise. + - **Phase-1 single-tenant scoping** — query handle takes no `User` arg; matches the rest of `GetOverviewDashboardQuery`'s slices. Multi-tenant scoping arrives uniformly when teams ship. + - Returned shape mirrors the existing `MOCK_KPIS['uptime']` exactly so `Overview.vue`'s `KpiCard` wiring needs no other changes. + +- **Wire it in.** `GetOverviewDashboardQuery::handle()` calls `app(GetMonitoringUptimeKpiQuery::class)->execute()` for the `'uptime'` slice. Drop the `'uptime'` entry from `MOCK_KPIS`. Update the class doc-comment so `dashboard.uptime` graduates from "still mock" to "real today". + +- **`App\Events\WebsiteCheckRecorded`** — `ShouldBroadcastNow` Reverb event on every persisted check. + - Constructor: `(int $checkId, int $websiteId, int $ownerUserId)` — pre-resolved ints (matches spec 021's `WorkflowRunUpserted` pattern: don't lazy-load relations during broadcast). + - `broadcastOn()` returns `[new PrivateChannel("users.{$ownerUserId}.monitoring")]`. Empty when `ownerUserId` is null (orphan website). + - `broadcastWith()` returns `{ check_id, website_id }` — light-weight pulse. Client uses it as a trigger, not as a source of truth. + - `broadcastAs()` returns `'WebsiteCheckRecorded'` — stable dotted name for Echo subscribers. + +- **Channel authorization** in `routes/channels.php`: + ```php + Broadcast::channel('users.{userId}.monitoring', function ($user, $userId) { + return (int) $user->id === (int) $userId; + }); + ``` + Mirrors specs 019 (activity) and 021 (deployments) exactly. + +- **`RecordWebsiteCheckAction` extension.** After the existing persistence + transition-event paths, dispatch `WebsiteCheckRecorded` with the pre-resolved owner id (via `$website->project->owner_user_id`, loaded eagerly to avoid the N+1 some broadcast pipelines hit). Skip the dispatch when the owner can't be resolved (orphan project). + +- **Show page realtime subscription.** + - `Pages/Monitoring/Websites/Show.vue` subscribes via `window.Echo.private("users.{auth.user.id}.monitoring")` on mount, listens for `.WebsiteCheckRecorded`, and on each pulse calls `router.reload({ only: ['summary', 'checks', 'website'] })` so the uptime stats, recent-checks list, and the parent website's `last_*` timestamps all refresh atomically. Filter client-side by the pulse's `website_id` matching the page's loaded website id — other websites' pulses don't trigger a reload on this page. + - Tracks `realtimeConnected: Ref` and renders an offline pill when the connection drops, mirroring `useActivityFeed.ts`'s pattern. + - Tear down the subscription in `onBeforeUnmount`. + +- **Response-time line chart on the Show page.** + - Render the last 50 checks' `response_time_ms` as a `Sparkline` (already a `KpiCard` building block; reuse standalone). Header strip in the existing Recent-checks card gains the chart inline above the list. + - Skip checks with null `response_time_ms` (Error rows where the probe didn't get a response time) — they'd render as gaps. Carry-forward the previous value when null is encountered to keep the line continuous. + - Compact `Sparkline` props: `accent="cyan"`, `height={48}`, no labels. + - When fewer than 2 data points exist, render a small "Not enough data yet" placeholder instead of the chart. + +- **Tests:** + - `GetMonitoringUptimeKpiQueryTest` — empty state returns null `overall` + muted; mixed checks compute volume-weighted percentage; 12-entry sparkline ordering; days with no checks default to 100; status thresholds at 99 / 95 boundaries. + - `WebsiteCheckRecordedTest` — implements `ShouldBroadcastNow`, `broadcastOn()` returns the right channel for the supplied owner id, payload + dotted name match. + - `RecordWebsiteCheckActionTest` — extend with `Event::fake([WebsiteCheckRecorded::class])` to assert the event dispatches on every check (incident, recovery, AND steady-state runs that don't emit transition events). + - `GetOverviewDashboardQueryTest` — extend the existing payload-shape test to include the new `uptime.overall` real-data assertion (no longer pinned to the mock 99.98). + +**Out of scope:** + +- Per-website status-page generator → roadmap §8.8 Future. +- SLA target configuration → polish. +- Per-region probes / Lighthouse / DNS / TLS / TTFB timings → §8.8 Future. +- Hourly aggregated response-time chart with 24h / 7d / 30d toggle (option B from the scope check) — phase-1 keeps the simple line of last-50-checks; revisit if real users find the raw points too noisy. +- Show page incident timeline (correlated transitions list) → polish. +- Channel auth integration test — same brittle `/broadcasting/auth` env-CSRF baseline that affected spec 021's drop; channel callback correctness is already covered by the event-level "broadcasts on the right channel" assertion. + +## Plan +1. **`GetMonitoringUptimeKpiQuery`** + tests — pure aggregate, no model dependencies beyond `WebsiteCheck`. +2. **Wire it into `GetOverviewDashboardQuery`** + drop `MOCK_KPIS['uptime']`. Update class docblock. +3. **`WebsiteCheckRecorded` event** + tests — pre-resolved owner pattern from spec 021. +4. **`routes/channels.php`** authorize the new channel. +5. **`RecordWebsiteCheckAction`** dispatches the event after persistence. Tests — `Event::fake` assertion on every kind of run. +6. **`Show.vue` subscribes via Echo** and partial-reloads on the right pulse. Add `realtimeConnected` offline pill. +7. **Response-time `Sparkline`** in the Recent-checks card header. Carry-forward null handling. +8. **Self-review pass via `superpowers:code-reviewer`**. +9. **Open the PR**. + +## Acceptance criteria +- [ ] `GetMonitoringUptimeKpiQuery` returns the documented shape; volume-weighted across all websites; null `overall` + muted status on empty windows. +- [ ] `GetOverviewDashboardQuery::handle()` exposes the real `dashboard.uptime` slice; `MOCK_KPIS['uptime']` removed; class docblock updated. +- [ ] `WebsiteCheckRecorded` implements `ShouldBroadcastNow`, broadcasts on `users.{ownerUserId}.monitoring` with payload `{ check_id, website_id }`. +- [ ] `routes/channels.php` rejects users other than the website's project owner. +- [ ] `RecordWebsiteCheckAction` dispatches `WebsiteCheckRecorded` on every persisted check (independent of transition emission). +- [ ] `Pages/Monitoring/Websites/Show.vue` subscribes via Echo on mount, partial-reloads on each pulse matching the page's website id, leaves on unmount. +- [ ] Show page renders a response-time `Sparkline` of the last 50 checks; null response times carry forward; <2 data points renders the placeholder. +- [ ] Pint + `php artisan test` (full suite) + `npm run build` clean. CI green. +- [ ] Self-review pass with `superpowers:code-reviewer`; material findings addressed before opening the PR. + +## Files touched +- `app/Domain/Monitoring/Queries/GetMonitoringUptimeKpiQuery.php` — new. +- `app/Domain/Dashboard/Queries/GetOverviewDashboardQuery.php` — call the new query; drop `MOCK_KPIS['uptime']`; update docblock. +- `app/Events/WebsiteCheckRecorded.php` — new (broadcast event). +- `routes/channels.php` — `users.{userId}.monitoring` per-user auth. +- `app/Domain/Monitoring/Actions/RecordWebsiteCheckAction.php` — dispatch the event after persistence. +- `resources/js/Pages/Monitoring/Websites/Show.vue` — Echo subscription + offline pill + response-time Sparkline. +- `tests/Feature/Monitoring/GetMonitoringUptimeKpiQueryTest.php` — new. +- `tests/Feature/Events/WebsiteCheckRecordedTest.php` — new. +- `tests/Feature/Monitoring/RecordWebsiteCheckActionTest.php` — extend with broadcast-event assertion. +- `tests/Feature/Dashboard/GetOverviewDashboardQueryTest.php` — extend `mock_kpis_remain_consistent_with_phase_0_values` to drop the uptime line + add a real-uptime test. + +## Work log +Dated notes as work progresses. + +### 2026-04-30 +- Spec drafted. +- Opened issue [#75](https://github.com/Copxer/nexus/issues/75) and branch `spec/025-overview-and-realtime` off `main`. + +## Decisions (locked 2026-04-30) +- **Volume-weighted uptime (option B).** `successful_checks / total_checks` across all websites — truest "system-wide uptime" measure. +- **Sparkline of last-50 response times (option A).** Reuses the existing component; matches the recent-checks data the user is already looking at. Hourly aggregation is a future polish. +- **Broadcast every persisted check (option A).** Mirrors spec 021's `WorkflowRunUpserted`; the right rail still gets transition events for free from spec 024. +- **Days with no checks → 100% on the sparkline.** A flat 100 line for a fresh account reads as "no failures observed" rather than "everything was down." Document the limitation. +- **Pre-resolved owner id in event constructor.** Avoids broadcast-time relation walking — matches spec 021's `WorkflowRunUpserted` decision. + +## Open questions / blockers +- **`null` response times in Sparkline.** `Sparkline.vue` accepts `number[]`. Carry-forward the previous value when a null is encountered; if the first point is null fall back to 0. Confirm during implementation that the rendered line stays readable. +- **Scaling read of `WebsiteCheck::query()->count()` over 24h.** At phase-1 row counts (≤100 monitors × 1440 minutes / interval) under 50k rows; cheap. Cache only if a slow-query log flags it. From a93bb86df9eaf50b07a312f74f9b0462c5ebf996 Mon Sep 17 00:00:00 2001 From: Copxer Date: Fri, 1 May 2026 00:34:07 -0700 Subject: [PATCH 2/3] feat(monitoring): Overview uptime KPI + Reverb live updates + perf chart (spec 025) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes phase 5. Replaces MOCK_KPIS['uptime'] with a real cross-website aggregate, broadcasts every persisted check via Reverb so the per- website Show page reflects live data, and renders a response-time Sparkline of the last 50 checks. - GetMonitoringUptimeKpiQuery: volume-weighted 24h uptime % across all websites (locked decision B — busy site with one failure dominates a quiet 100% site, the truest system-wide measure). Returns {overall, change, sparkline (12 days oldest-first), status}. - overall: float|null; null on empty 24h window. - change: 24h delta vs prior 24h; 0 when either window is empty. - sparkline: daily uptime %; days with no checks default to 100.0 ("no failures observed" reads better than "everything down" on a fresh account; documented limitation). - status: muted (null) | success (≥99) | warning (≥95) | danger. - GetOverviewDashboardQuery::handle() calls the new query for the uptime slice; MOCK_KPIS['uptime'] removed; docblock updated. - New WebsiteCheckRecorded ShouldBroadcastNow event: - Constructor takes pre-resolved (checkId, websiteId, ownerUserId) ints — mirrors spec 021's WorkflowRunUpserted pattern, avoids broadcast-time relation walking. - Broadcasts on users.{ownerUserId}.monitoring with light pulse {check_id, website_id}; client uses it as a trigger, not a source of truth. - Per-user channel + client-side filter trade-off documented; revisit at ~1k monitors or sub-30s intervals. - routes/channels.php authorizes users.{userId}.monitoring (per-user gate matching the activity / deployments channels). - RecordWebsiteCheckAction dispatches the event after every persisted check (steady-state runs included). Spec 024's transition activity events still fire separately on healthy↔failed swings. - Show.vue subscribes via Echo on mount, filters by website_id, partial-reloads website + summary + checks on matching pulses; realtimeConnected ref drives an offline pill. Sparkline of last 50 response_time_ms with leading-null skip + carry-forward fill; <2 data points renders the "not enough data" placeholder. Tests: 18 net new passing tests across 3 new files + 2 extended. Full suite 357 passed (was 339); 0 regressions. Self-review pass via superpowers:code-reviewer flagged 3 recommendations, all addressed: - usePage() hoisted to setup top-level (was inside onMounted — idiomatically wrong, Inertia could validate the call site). - responseTimeSeries skips leading-null Error checks so the line doesn't anchor at the 0ms floor (Sparkline's min/max normalizer was pulling the chart to the baseline). - broadcastOn() docblock documents the per-user-channel-vs-per- website-channel trade-off for future scaling. PHASE 5 COMPLETE (3/3 specs). The only remaining MOCK_KPIS slices are 'services' (phase 6) and 'alerts' (phase 7). --- .../Queries/GetOverviewDashboardQuery.php | 16 +- .../Actions/RecordWebsiteCheckAction.php | 18 ++ .../Queries/GetMonitoringUptimeKpiQuery.php | 156 +++++++++++ app/Events/WebsiteCheckRecorded.php | 96 +++++++ .../js/Pages/Monitoring/Websites/Show.vue | 144 ++++++++++- routes/channels.php | 9 + .../GetOverviewDashboardQueryTest.php | 18 +- .../Events/WebsiteCheckRecordedTest.php | 55 ++++ .../GetMonitoringUptimeKpiQueryTest.php | 243 ++++++++++++++++++ .../RecordWebsiteCheckActionTest.php | 39 +++ 10 files changed, 781 insertions(+), 13 deletions(-) create mode 100644 app/Domain/Monitoring/Queries/GetMonitoringUptimeKpiQuery.php create mode 100644 app/Events/WebsiteCheckRecorded.php create mode 100644 tests/Feature/Events/WebsiteCheckRecordedTest.php create mode 100644 tests/Feature/Monitoring/GetMonitoringUptimeKpiQueryTest.php diff --git a/app/Domain/Dashboard/Queries/GetOverviewDashboardQuery.php b/app/Domain/Dashboard/Queries/GetOverviewDashboardQuery.php index 2864121..1b7cf44 100644 --- a/app/Domain/Dashboard/Queries/GetOverviewDashboardQuery.php +++ b/app/Domain/Dashboard/Queries/GetOverviewDashboardQuery.php @@ -2,6 +2,7 @@ namespace App\Domain\Dashboard\Queries; +use App\Domain\Monitoring\Queries\GetMonitoringUptimeKpiQuery; use App\Models\ActivityEvent; use App\Models\Project; use App\Models\Repository; @@ -34,9 +35,13 @@ * `activity_events.occurred_at` over the last 90 days. Bucketing * happens in PHP so the query stays cross-DB without `DAYOFWEEK()` / * `HOUR()` polyfills. + * - dashboard.uptime.{overall,change,sparkline,status} — Spec 025; + * `GetMonitoringUptimeKpiQuery` aggregates `website_checks` + * volume-weighted across all of the user's monitors over 24h + * (vs prior 24h) plus a 12-day daily sparkline. * * Still mock (extracted to MOCK_* constants — clearly marked): - * - dashboard.{services,alerts,uptime} → MOCK_KPIS + * - dashboard.{services,alerts} → MOCK_KPIS * * The right-rail activity feed is no longer surfaced from this query — * the AppLayout consumes the shared `activity.recent` Inertia prop @@ -64,6 +69,7 @@ public function handle(): array 'projects' => $this->projects(), 'hosts' => $this->hosts(), 'deployments' => $this->deploymentsKpi(), + 'uptime' => app(GetMonitoringUptimeKpiQuery::class)->execute(), 'topRepositories' => $this->topRepositories(), ]), 'activityHeatmap' => $this->activityHeatmap(), @@ -364,7 +370,7 @@ private function dailyCounts(string $modelClass, int $days): array // that ships the real source. // ────────────────────────────────────────────────────────────────── - /** Phase 5/6 (Services/Hosts), phase 7 (Alerts), phase 8 (Uptime). */ + /** Phase 5/6 (Services/Hosts), phase 7 (Alerts). */ private const MOCK_KPIS = [ 'services' => [ 'running' => 47, @@ -378,11 +384,5 @@ private function dailyCounts(string $modelClass, int $days): array 'sparkline' => [1, 0, 1, 2, 1, 1, 2, 2, 3, 2, 3, 3], 'status' => 'danger', ], - 'uptime' => [ - 'overall' => 99.98, - 'change' => 0.01, - 'sparkline' => [99.92, 99.93, 99.95, 99.94, 99.96, 99.97, 99.96, 99.97, 99.98, 99.98, 99.97, 99.98], - 'status' => 'success', - ], ]; } diff --git a/app/Domain/Monitoring/Actions/RecordWebsiteCheckAction.php b/app/Domain/Monitoring/Actions/RecordWebsiteCheckAction.php index 3acfda5..e4586da 100644 --- a/app/Domain/Monitoring/Actions/RecordWebsiteCheckAction.php +++ b/app/Domain/Monitoring/Actions/RecordWebsiteCheckAction.php @@ -7,6 +7,7 @@ use App\Enums\ActivitySeverity; use App\Enums\WebsiteCheckStatus; use App\Enums\WebsiteStatus; +use App\Events\WebsiteCheckRecorded; use App\Models\Website; use App\Models\WebsiteCheck; use Illuminate\Support\Carbon; @@ -35,6 +36,12 @@ * `metadata.website_id → website → project → owner_user_id` (since * monitoring rows have `repository_id = null` and would otherwise * silently fail to broadcast). Realtime fan-out reaches the right rail. + * + * Spec 025: dispatches `WebsiteCheckRecorded` after every persisted + * check (steady-state runs included, not just transitions) so the + * per-website Show page reflects every probe in realtime — the + * transition path above only fires on healthy↔failed swings, which + * leaves "still up, response time changed" updates invisible. */ class RecordWebsiteCheckAction { @@ -73,6 +80,17 @@ public function execute(Website $website, WebsiteProbeResult $result): WebsiteCh $this->maybeEmitTransitionActivity($website, $previousStatus, $result, $checkedAt); + // Spec 025 — broadcast every persisted check so the Show page + // reflects steady-state runs too. Pre-resolve the owner id + // here so the event's broadcaster doesn't lazy-load + // website→project relations during fan-out. + $website->loadMissing('project:id,owner_user_id'); + WebsiteCheckRecorded::dispatch( + $check->id, + $website->id, + $website->project?->owner_user_id, + ); + return $check; } diff --git a/app/Domain/Monitoring/Queries/GetMonitoringUptimeKpiQuery.php b/app/Domain/Monitoring/Queries/GetMonitoringUptimeKpiQuery.php new file mode 100644 index 0000000..fd465bb --- /dev/null +++ b/app/Domain/Monitoring/Queries/GetMonitoringUptimeKpiQuery.php @@ -0,0 +1,156 @@ +, + * status: 'success'|'warning'|'danger'|'muted', + * } + */ + public function execute(): array + { + $now = now(); + $currentStart = $now->copy()->subDay(); + $previousStart = $now->copy()->subDays(2); + + $overall = $this->uptimeFor($currentStart, $now); + $previous = $this->uptimeFor($previousStart, $currentStart); + + $change = ($overall === null || $previous === null) + ? 0.0 + : round($overall - $previous, 2); + + return [ + 'overall' => $overall, + 'change' => $change, + 'sparkline' => $this->sparkline(), + 'status' => $this->statusFor($overall), + ]; + } + + /** + * Volume-weighted uptime % over the half-open window `[from, to)`. + * Null when the window is empty. + */ + private function uptimeFor(Carbon $from, Carbon $to): ?float + { + $total = WebsiteCheck::query() + ->where('checked_at', '>=', $from) + ->where('checked_at', '<', $to) + ->count(); + + if ($total === 0) { + return null; + } + + $successful = WebsiteCheck::query() + ->where('checked_at', '>=', $from) + ->where('checked_at', '<', $to) + ->whereIn('status', ['up', 'slow']) + ->count(); + + return round(($successful / $total) * 100, 2); + } + + /** + * 12-entry daily uptime sparkline. Days with no checks default to + * 100.0 so a fresh account doesn't render as a 0-percent flatline. + * + * One DB query (grouped by date) — beats 12 round-trips for a + * cheap read. + * + * @return array + */ + private function sparkline(): array + { + $start = now()->startOfDay()->subDays(self::SPARKLINE_DAYS - 1); + + /** @var Collection $rows */ + $rows = WebsiteCheck::query() + ->where('checked_at', '>=', $start) + ->selectRaw( + 'DATE(checked_at) as date, COUNT(*) as total,' + .'SUM(CASE WHEN status IN (?, ?) THEN 1 ELSE 0 END) as successful', + ['up', 'slow'], + ) + ->groupBy('date') + ->get() + ->keyBy(fn ($row) => (string) $row->date); + + $series = []; + for ($i = 0; $i < self::SPARKLINE_DAYS; $i++) { + $day = $start->copy()->addDays($i)->toDateString(); + $row = $rows->get($day); + + if ($row === null || (int) $row->total === 0) { + // Empty day → "no failures observed" interpretation. + $series[] = 100.0; + + continue; + } + + $series[] = round(((int) $row->successful / (int) $row->total) * 100, 2); + } + + return $series; + } + + /** + * Map overall rate → status tone. + * + * @return 'success'|'warning'|'danger'|'muted' + */ + private function statusFor(?float $overall): string + { + if ($overall === null) { + return 'muted'; + } + if ($overall >= 99.0) { + return 'success'; + } + if ($overall >= 95.0) { + return 'warning'; + } + + return 'danger'; + } +} diff --git a/app/Events/WebsiteCheckRecorded.php b/app/Events/WebsiteCheckRecorded.php new file mode 100644 index 0000000..f75e893 --- /dev/null +++ b/app/Events/WebsiteCheckRecorded.php @@ -0,0 +1,96 @@ + + */ + public function broadcastOn(): array + { + if ($this->ownerUserId === null) { + return []; + } + + return [ + new PrivateChannel("users.{$this->ownerUserId}.monitoring"), + ]; + } + + /** + * Light-weight pulse — the client uses this as a trigger, not as + * the source of truth. The Show page partial-reloads the website + * + summary + checks props after receiving any pulse for its own + * website id. + * + * @return array + */ + public function broadcastWith(): array + { + return [ + 'check_id' => $this->checkId, + 'website_id' => $this->websiteId, + ]; + } + + /** + * Stable event name for Echo subscribers: + * `Echo.private('users.{id}.monitoring').listen('.WebsiteCheckRecorded', ...)`. + */ + public function broadcastAs(): string + { + return 'WebsiteCheckRecorded'; + } +} diff --git a/resources/js/Pages/Monitoring/Websites/Show.vue b/resources/js/Pages/Monitoring/Websites/Show.vue index bc65bf7..67013bd 100644 --- a/resources/js/Pages/Monitoring/Websites/Show.vue +++ b/resources/js/Pages/Monitoring/Websites/Show.vue @@ -1,8 +1,10 @@