From d1f87b9e9195a374ebeaeb1b761e981a945c76d1 Mon Sep 17 00:00:00 2001 From: Copxer Date: Thu, 30 Apr 2026 19:51:14 -0700 Subject: [PATCH] =?UTF-8?q?fix(overview):=20wire=20shipped=20phases=20?= =?UTF-8?q?=E2=80=94=20drop=20stale=20Phase=204=20stub,=20real=20Issues=20?= =?UTF-8?q?&=20PRs=20widget?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Overview dashboard had four spots still pointing at "Phase X — coming soon" even though the underlying functionality has shipped. Reconcile in one sweep so the dashboard honestly reflects what's live. - visualizationStubs no longer lists "Deployment timeline / Phase 4" — Phase 4 (specs 020–022) shipped its real surface as the Deployments sidebar entry / /deployments page. The stub gridhad graduated; the placeholder only listed it because the cleanup was forgotten when 022 closed out. Phase 5's Website performance entry is reframed as "Phase 5 · spec 025" — the data layer (spec 023) shipped, the dedicated Overview widget lands with spec 025. - The Issues & Pull Requests widget on Overview was a hardcoded stubIssues fixture even though Phase 2's WorkItemsForUserQuery (spec 016) has been live for weeks. OverviewController now injects the query and passes a topWorkItems prop (capped at 4 most-recent open items, scoped to the user's repos). The Vue widget renders PR/Issue badges, real titles + numbers, repository chip, author, and "Updated Xm ago" timestamps. Each row links out to the GitHub issue/PR. Footer points at /work-items for the wider queue. - Activity Heatmap header label "7 days · 4-hour buckets · mock" was stale — the heatmap was wired to real activity_events aggregates in fix/activity-heatmap-real-data. Updated to "last 90 days" reflecting the actual aggregation window. - Services widget footer mistakenly attributed itself to "phase 5 — Website Monitoring." Services live with the Docker host agent (phase 6); fixed to match. Tests: extended SmokeTest::test_overview_carries_the_mock_dashboard_payload with a topWorkItems assertion, plus a new test_overview_topworkitems_pulls_from_user_repos_only proving sibling-user open work items don't leak into the widget. Self-review notes: kept GetOverviewDashboardQuery's no-arg handle() signature unchanged. WorkItemsForUserQuery requires a User, so the controller is the merge point. The four mock KPI slices (services, alerts, uptime) stay until their owning phases ship. Phase-1 single-tenant scoping for topWorkItems mirrors WorkItemsForUserQuery's existing behavior — uniform fix when teams ship. --- app/Http/Controllers/OverviewController.php | 46 +++++-- resources/js/Pages/Overview.vue | 130 +++++++++++--------- tests/Feature/SmokeTest.php | 44 +++++++ 3 files changed, 156 insertions(+), 64 deletions(-) diff --git a/app/Http/Controllers/OverviewController.php b/app/Http/Controllers/OverviewController.php index 2b96876..f446f7c 100644 --- a/app/Http/Controllers/OverviewController.php +++ b/app/Http/Controllers/OverviewController.php @@ -3,19 +3,51 @@ namespace App\Http\Controllers; use App\Domain\Dashboard\Queries\GetOverviewDashboardQuery; +use App\Domain\GitHub\Queries\WorkItemsForUserQuery; +use Illuminate\Http\Request; use Inertia\Inertia; use Inertia\Response; /** - * Overview dashboard. Delegates the read entirely to - * `GetOverviewDashboardQuery` (roadmap §10.2). The query is responsible - * for distinguishing real DB-backed slices from clearly-marked mock - * placeholders. + * Overview dashboard. Mostly delegates the read to + * `GetOverviewDashboardQuery` (roadmap §10.2) — that query handles the + * single-tenant phase-1 slices that don't need a user (projects, + * hosts, deployments, top repositories, activity heatmap). + * + * The Issues & PRs widget is the one user-scoped slice on this page + * — `WorkItemsForUserQuery` requires a `User` so we run it here and + * merge the result rather than threading the user into + * `GetOverviewDashboardQuery`'s no-arg signature. */ class OverviewController extends Controller { - public function __invoke(GetOverviewDashboardQuery $query): Response - { - return Inertia::render('Overview', $query->handle()); + /** + * Cap for the Issues & PRs widget — keeps the card visually + * consistent with the other 4-row stubs around it. + */ + private const TOP_WORK_ITEMS_LIMIT = 4; + + public function __invoke( + Request $request, + GetOverviewDashboardQuery $query, + WorkItemsForUserQuery $workItemsQuery, + ): Response { + $payload = $query->handle(); + + // Spec 016 shipped the work-items query; this surfaces the top + // N open items on the Overview's Issues & PRs widget so the + // card reflects real activity instead of fixture data. + $topWorkItems = array_slice( + $workItemsQuery->execute($request->user(), [ + 'kind' => 'all', + 'state' => 'open', + ]), + 0, + self::TOP_WORK_ITEMS_LIMIT, + ); + + return Inertia::render('Overview', array_merge($payload, [ + 'topWorkItems' => $topWorkItems, + ])); } } diff --git a/resources/js/Pages/Overview.vue b/resources/js/Pages/Overview.vue index f175eae..6ce8e63 100644 --- a/resources/js/Pages/Overview.vue +++ b/resources/js/Pages/Overview.vue @@ -22,9 +22,22 @@ import { ShieldCheck, } from 'lucide-vue-next'; +interface TopWorkItem { + id: string; + kind: 'issue' | 'pull_request' | string; + number: number; + title: string; + state: string | null; + author_login: string | null; + updated_at_github: string | null; + html_url: string | null; + repository: { id: number; full_name: string; name: string } | null; +} + defineProps<{ dashboard: DashboardPayload; activityHeatmap: ActivityHeatmapPayload; + topWorkItems: TopWorkItem[]; }>(); // ----- Hardcoded mock data for the populated stub widgets ----- @@ -32,40 +45,6 @@ defineProps<{ // mock data here exists only so the page looks intentional, not skeletal — // each stub footer points at the phase that will replace it. -const stubIssues = [ - { - title: 'Login flow rejects valid 2FA codes intermittently', - repo: 'nexus-web', - time: '12 min', - priority: 'critical' as const, - }, - { - title: 'Migrate analytics events to BigQuery sink', - repo: 'nexus-api', - time: '3 h', - priority: 'high' as const, - }, - { - title: 'Add dark-mode tokens to email templates', - repo: 'nexus-mail', - time: '1 d', - priority: 'medium' as const, - }, - { - title: 'Expose feature-flag SDK in the JS bundle', - repo: 'nexus-flags', - time: '2 d', - priority: 'low' as const, - }, -]; - -const priorityToneMap = { - critical: 'danger', - high: 'warning', - medium: 'info', - low: 'muted', -} as const; - const stubHosts = [ { name: 'prod-web-01', region: 'us-east', cpu: 0.42, mem: 0.61, status: 'success' as const }, { name: 'prod-api-02', region: 'us-east', cpu: 0.78, mem: 0.83, status: 'warning' as const }, @@ -96,12 +75,16 @@ const stubServices = [ }, ]; +// Stubs only list widgets whose owning phase hasn't shipped yet. +// `Deployment timeline` graduated when Phase 4 (specs 020–022) shipped +// — its real surface is the `Deployments` sidebar entry / `/deployments` +// page. `Website performance` stays here until spec 025 lands the +// dedicated Overview widget on top of the spec-023 monitor data. const visualizationStubs = [ { label: 'World map', icon: Globe, phase: 'Phase 8' }, { label: 'Resource utilization', icon: Activity, phase: 'Phase 6' }, - { label: 'Website performance', icon: LineChart, phase: 'Phase 5' }, + { label: 'Website performance', icon: LineChart, phase: 'Phase 5 · spec 025' }, { label: 'System metrics', icon: Gauge, phase: 'Phase 8' }, - { label: 'Deployment timeline', icon: Rocket, phase: 'Phase 4' }, ] as const; @@ -224,7 +207,10 @@ const visualizationStubs = [
- +
- @@ -474,7 +490,7 @@ const visualizationStubs = [ diff --git a/tests/Feature/SmokeTest.php b/tests/Feature/SmokeTest.php index 3fdb824..9e74a8e 100644 --- a/tests/Feature/SmokeTest.php +++ b/tests/Feature/SmokeTest.php @@ -2,6 +2,9 @@ namespace Tests\Feature; +use App\Models\GithubIssue; +use App\Models\Project; +use App\Models\Repository; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Inertia\Testing\AssertableInertia; @@ -46,6 +49,47 @@ public function test_overview_carries_the_mock_dashboard_payload(): void ->has('uptime.overall') ->has('topRepositories') ) + ->has('topWorkItems') + ); + } + + public function test_overview_topworkitems_pulls_from_user_repos_only(): void + { + $user = User::factory()->create([ + 'email_verified_at' => now(), + ]); + $project = Project::factory()->create(['owner_user_id' => $user->id]); + $repository = Repository::factory()->create([ + 'project_id' => $project->id, + 'full_name' => 'mine/repo', + ]); + GithubIssue::factory()->create([ + 'repository_id' => $repository->id, + 'number' => 7, + 'title' => 'Mine', + 'state' => 'open', + ]); + + // Sibling user's open issue must NOT leak into the widget. + $other = User::factory()->create(['email_verified_at' => now()]); + $otherProject = Project::factory()->create(['owner_user_id' => $other->id]); + $otherRepo = Repository::factory()->create([ + 'project_id' => $otherProject->id, + 'full_name' => 'other/repo', + ]); + GithubIssue::factory()->create([ + 'repository_id' => $otherRepo->id, + 'number' => 1, + 'title' => 'Sibling', + 'state' => 'open', + ]); + + $this->actingAs($user) + ->get('/overview') + ->assertInertia( + fn (AssertableInertia $page) => $page + ->has('topWorkItems', 1) + ->where('topWorkItems.0.title', 'Mine') ); }