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') ); }