From 73e2cfe43d692ee2b9823a4efd1f839d9ad5fdfc Mon Sep 17 00:00:00 2001 From: Copxer Date: Thu, 30 Apr 2026 19:37:21 -0700 Subject: [PATCH] fix(monitoring): wire project Monitoring tab + reconcile phase-5 placeholders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 5 just shipped its first spec (023 — website monitor MVP), but the codebase still referenced "Phase 5 — coming soon" in several places where the underlying functionality now exists. This non-spec fix plugs those gaps + adds a small dev-server self-probe deadlock warning on the Create form. - ProjectController::show payload extended with projectMonitors (websites scoped to the project, capped at 20). Projects/Show.vue Monitoring tab pendingPhase flipped from 'phase 5' → null and rendered with a real list, "Add monitor" + "Browse all" CTAs, and status badges. The MonitorRow shape mirrors the Index/Show pages. - Monitoring/Websites/Create.vue: small "Tip: monitor /up" hint under the URL field, plus a self-probe warning that fires when the entered URL hostname matches window.location.hostname (php artisan serve's default single-process worker deadlocks on a self-probe; the hint nudges the user to --workers=4 or a different URL). - Three command palette entries graduated from disabled → real run callbacks: go-issues-prs (Phase 2), go-deployments (Phase 4), go-monitoring (Phase 5 — this PR's primary scope). go-pipelines stays disabled but its stale "Phase 4" pin becomes a generic "Soon" since Phase 4 shipped Deployments and Pipelines hasn't been spec'd. - Welcome.vue: dropped the "— coming with phase 5" qualifier from the Website performance feature card. - Extracted websiteStatusTone to resources/js/lib/websiteStyles.ts — three consumers (Index, Show, Projects/Show Monitoring tab) now share one source of truth, mirroring the workflowRunStyles pattern from the project-tabs fix. Tests: extended test_show_renders_the_project with a projectMonitors prop assertion + new test_show_scopes_monitors_to_this_project proving sibling-project monitors don't leak. Self-review pass via superpowers:code-reviewer; addressed both substantive recommendations (extract websiteStyles since this is the 3rd consumer, reconcile go-pipelines stale label). Self-probe warning is non-blocking — the deadlock is recoverable (just times out), the warning is environment-specific (only --workers=1 defaults), and blocking would frustrate anyone running --workers=4 or Octane. Cross-tenant scoping note: projectMonitors uses where('project_id', \$project->id) without an additional owner check — matches the existing repository / activity / deployments scoping pattern on the project show page. Uniform fix when teams ship. --- app/Http/Controllers/ProjectController.php | 23 ++++ .../js/Pages/Monitoring/Websites/Create.vue | 49 ++++++- .../js/Pages/Monitoring/Websites/Index.vue | 13 +- .../js/Pages/Monitoring/Websites/Show.vue | 13 +- resources/js/Pages/Projects/Show.vue | 122 +++++++++++++++++- resources/js/Pages/Welcome.vue | 2 +- resources/js/lib/commands.ts | 12 +- resources/js/lib/websiteStyles.ts | 40 ++++++ .../Projects/ProjectControllerTest.php | 28 ++++ 9 files changed, 272 insertions(+), 30 deletions(-) create mode 100644 resources/js/lib/websiteStyles.ts diff --git a/app/Http/Controllers/ProjectController.php b/app/Http/Controllers/ProjectController.php index 3140af1..923ae2a 100644 --- a/app/Http/Controllers/ProjectController.php +++ b/app/Http/Controllers/ProjectController.php @@ -9,6 +9,7 @@ use App\Http\Requests\Projects\StoreProjectRequest; use App\Http\Requests\Projects\UpdateProjectRequest; use App\Models\Project; +use App\Models\Website; use App\Support\ProjectPalette; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; @@ -88,6 +89,27 @@ public function show( 10, ); + // Per-project Monitoring tab (spec 023) — website monitors + // belonging to this project. Cap at 20 inline — the cross-repo + // monitor list at `/monitoring/websites` is the wider view. + // Phase-1 single-tenant: any monitor under the project is + // visible to its owner; cross-tenant scoping arrives uniformly + // when teams ship. + $projectMonitors = Website::query() + ->where('project_id', $project->id) + ->orderBy('name') + ->limit(20) + ->get() + ->map(fn (Website $website) => [ + 'id' => $website->id, + 'name' => $website->name, + 'url' => $website->url, + 'method' => $website->method, + 'status' => $website->status?->value, + 'last_checked_at' => $website->last_checked_at?->diffForHumans(), + ]) + ->all(); + return Inertia::render('Projects/Show', [ 'project' => $this->transform($project), 'canUpdate' => $request->user()?->can('update', $project) ?? false, @@ -109,6 +131,7 @@ public function show( ])->all(), 'projectActivity' => $projectActivity, 'projectDeployments' => $projectDeployments, + 'projectMonitors' => $projectMonitors, ]); } diff --git a/resources/js/Pages/Monitoring/Websites/Create.vue b/resources/js/Pages/Monitoring/Websites/Create.vue index ae88f34..67bad9d 100644 --- a/resources/js/Pages/Monitoring/Websites/Create.vue +++ b/resources/js/Pages/Monitoring/Websites/Create.vue @@ -5,7 +5,8 @@ import PrimaryButton from '@/Components/PrimaryButton.vue'; import TextInput from '@/Components/TextInput.vue'; import AppLayout from '@/Layouts/AppLayout.vue'; import { Head, Link, useForm } from '@inertiajs/vue3'; -import { ChevronLeft } from 'lucide-vue-next'; +import { AlertTriangle, ChevronLeft } from 'lucide-vue-next'; +import { computed } from 'vue'; interface ProjectOption { id: number; @@ -45,6 +46,28 @@ const form = useForm({ const submit = () => { form.post(route('monitoring.websites.store')); }; + +/** + * Detect when the entered URL points back at the same Nexus instance. + * `php artisan serve`'s default single-process worker deadlocks when + * a sync request loops back to itself (probe → controller → probe). + * Surface a heads-up so the user knows to use a different URL or run + * the dev server with `--workers=4`. + * + * Match by hostname only — port, scheme, and path don't matter for + * the loop. SSR-safe: `window` may not exist during initial render. + */ +const selfProbeWarning = computed(() => { + if (typeof window === 'undefined' || !form.url) return false; + try { + const target = new URL(form.url); + return target.hostname === window.location.hostname; + } catch { + // `new URL()` throws on malformed input; the form's url + // validation will catch that path on submit. + return false; + } +});