Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .flow/tasks/fn-16.17.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"assignee": "bordumbb@gmail.com",
"claim_note": "",
"claimed_at": "2026-09-27T17:28:56.373909Z",
"created_at": "2026-09-27T17:26:27.656485Z",
"depends_on": [],
"epic": "fn-16",
"evidence": {
"commits": [],
"prs": [],
"tests": [
"pytest tests/integration/test_investigation_completed_at.py tests/integration/api/test_dashboard.py -m integration",
"pnpm test"
]
},
"id": "fn-16.17",
"priority": null,
"spec_path": ".flow/tasks/fn-16.17.md",
"status": "done",
"title": "Dashboard stats on the unified schema (completed_at + real numbers)",
"updated_at": "2026-09-27T17:58:48.568254Z"
}
22 changes: 22 additions & 0 deletions .flow/tasks/fn-16.17.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# fn-16.17 Dashboard stats on the unified schema (completed_at + real numbers)

## Description
TBD

## Acceptance
## Acceptance
- Migration 036 adds investigations.completed_at, set by a trigger the first time outcome is set (re-runnable, no BEGIN/COMMIT)
- get_dashboard_stats: active = status active, completed today = completed_at today; dashboard recent list reads alert JSONB (dataset_ids[0], metric display_name/anomaly_type, severity) and COALESCE(outcome status)
- frontend fetchDashboardStats maps the snake_case API and no longer falls back to mock numbers; the page shows no fake numbers on error
- Integration tests on the migrated schema; vitest for the frontend


## Done summary
Migration 036 adds investigations.completed_at, stamped by a trigger the first time outcome is set (cleared if outcome is cleared; re-runnable; backfills created_at for existing completions).
get_dashboard_stats: active = status active, completed today = completed_at >= CURRENT_DATE. list_investigations now returns summaries from the alert JSONB (dataset_ids[0], display_name/anomaly_type, severity, COALESCE outcome status), "unknown" for non-AnomalyAlert replays.
Frontend: fetchDashboardStats used /dashboard/stats (404: missing /api/v1) and fell back to mock numbers; it now uses the generated client and maps snake_case. New DashboardStatsCards shows "—" and an alert on error.
Tests: tests/integration/test_investigation_completed_at.py, tests/integration/api/test_dashboard.py, src/lib/api/dashboard.test.ts, src/features/dashboard/dashboard-stats.test.tsx (all RED first).
## Evidence
- Commits:
- Tests: pytest tests/integration/test_investigation_completed_at.py tests/integration/api/test_dashboard.py -m integration, pnpm test
- PRs:
21 changes: 21 additions & 0 deletions .flow/tasks/fn-16.18.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"assignee": "bordumbb@gmail.com",
"claim_note": "",
"claimed_at": "2026-09-27T17:58:48.835363Z",
"created_at": "2026-09-27T17:26:27.924541Z",
"depends_on": [],
"epic": "fn-16",
"evidence": {
"commits": [],
"prs": [],
"tests": [
"pytest tests/integration/core/test_permission_service.py -m integration"
]
},
"id": "fn-16.18",
"priority": null,
"spec_path": ".flow/tasks/fn-16.18.md",
"status": "done",
"title": "RBAC datasource grants match alert datasource_id",
"updated_at": "2026-09-27T18:04:18.537780Z"
}
18 changes: 18 additions & 0 deletions .flow/tasks/fn-16.18.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# fn-16.18 RBAC datasource grants match alert datasource_id

## Description
TBD

## Acceptance
## Acceptance
- PermissionService datasource grants (user and team) match investigations via alert->>datasource_id (4 places)
- Integration tests: can_access_investigation + get_accessible_investigation_ids


## Done summary
PermissionService matched datasource grants on the dropped investigations.data_source_id (4 places; the whole EXISTS/UNION query failed, so every access check raised). Now compares pg.data_source_id::text with i.alert->>datasource_id.
Tests: tests/integration/core/test_permission_service.py, user and team grants, can_access_investigation + get_accessible_investigation_ids (RED: column i.data_source_id does not exist).
## Evidence
- Commits:
- Tests: pytest tests/integration/core/test_permission_service.py -m integration
- PRs:
22 changes: 22 additions & 0 deletions .flow/tasks/fn-16.19.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"assignee": "bordumbb@gmail.com",
"claim_note": "",
"claimed_at": "2026-09-27T18:04:18.795162Z",
"created_at": "2026-09-27T17:26:28.178232Z",
"depends_on": [],
"epic": "fn-16",
"evidence": {
"commits": [],
"prs": [],
"tests": [
"pytest tests/integration/test_fix_feedback_export_context.py -m integration",
"pytest tests/unit/services/test_feedback.py"
]
},
"id": "fn-16.19",
"priority": null,
"spec_path": ".flow/tasks/fn-16.19.md",
"status": "done",
"title": "Fix feedback export: issue link from issue_investigation_runs",
"updated_at": "2026-09-27T18:11:55.210616Z"
}
19 changes: 19 additions & 0 deletions .flow/tasks/fn-16.19.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# fn-16.19 Fix feedback export: issue link from issue_investigation_runs

## Description
TBD

## Acceptance
## Acceptance
- FixFeedbackService export context reads the issue link from issue_investigation_runs (not investigations.issue_id); no "None" issue ids
- Real-schema test of the context lookup (fix_feedback/fix_executions have no migrations; flagged)


## Done summary
FixFeedbackService export read issue_id from investigations (dropped column). The per-record lookup is now investigation_context(): created_at from investigations plus the latest issue_investigation_runs issue, omitted when none (previously would have been "None").
Note: fix_feedback/fix_executions have no migrations and neither FixFeedbackService nor FixExecutionService is constructed anywhere, so the export itself cannot run; flagged.
Tests: tests/integration/test_fix_feedback_export_context.py (2 cases); unit test_feedback.py still passes.
## Evidence
- Commits:
- Tests: pytest tests/integration/test_fix_feedback_export_context.py -m integration, pytest tests/unit/services/test_feedback.py
- PRs:
21 changes: 21 additions & 0 deletions .flow/tasks/fn-16.20.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"assignee": "bordumbb@gmail.com",
"claim_note": "",
"claimed_at": "2026-09-27T18:11:55.473365Z",
"created_at": "2026-09-27T17:26:28.429990Z",
"depends_on": [],
"epic": "fn-16",
"evidence": {
"commits": [],
"prs": [],
"tests": [
"pytest dataing-ee/tests/integration/api/test_runbook_from_issue.py -m integration"
]
},
"id": "fn-16.20",
"priority": null,
"spec_path": ".flow/tasks/fn-16.20.md",
"status": "done",
"title": "EE runbook generation from the unified schema",
"updated_at": "2026-09-27T19:32:58.954009Z"
}
20 changes: 20 additions & 0 deletions .flow/tasks/fn-16.20.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# fn-16.20 EE runbook generation from the unified schema

## Description
TBD

## Acceptance
## Acceptance
- RunbookGenerator reads resolution_note, issue_labels, and the latest linked investigation outcome via issue_investigation_runs
- POST /runbooks/from-issue records created_from_investigation_id from the linked run
- EE integration test on the migrated schema


## Done summary
RunbookGenerator read issues.resolution/metadata and investigations.issue_id/synthesis/metadata (all dropped). It now reads resolution_note, labels from issue_labels, and the outcome of the latest issue_investigation_runs investigation, and returns that investigation_id; POST /runbooks/from-issue uses it (its own investigations.issue_id lookup is gone).
_row_to_response decodes JSONB text (AppDatabase has no JSONB codec on main), without which every runbook-returning route failed pydantic list validation.
Tests: dataing-ee/tests/integration/api/test_runbook_from_issue.py (latest run wins; issue without investigation). RED: column inv.issue_id does not exist.
## Evidence
- Commits:
- Tests: pytest dataing-ee/tests/integration/api/test_runbook_from_issue.py -m integration
- PRs:
14 changes: 14 additions & 0 deletions .flow/tasks/fn-16.21.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"assignee": null,
"claim_note": "",
"claimed_at": null,
"created_at": "2026-09-27T17:26:28.678421Z",
"depends_on": [],
"epic": "fn-16",
"id": "fn-16.21",
"priority": null,
"spec_path": ".flow/tasks/fn-16.21.md",
"status": "todo",
"title": "Share the issue AnomalyAlert helper between CE and EE",
"updated_at": "2026-09-27T17:26:28.678692Z"
}
18 changes: 18 additions & 0 deletions .flow/tasks/fn-16.21.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# fn-16.21 Share the issue AnomalyAlert helper between CE and EE

## Description
TBD

## Acceptance
## Acceptance
- One CE helper builds the issue AnomalyAlert for both the CE spawn route (#159) and the EE spawn_investigation action (#183)
- Deferred until busy-cartwright (fn-59) lands: the issues.py import lines are next to its uncommitted edits


## Done summary
TBD

## Evidence
- Commits:
- Tests:
- PRs:
69 changes: 12 additions & 57 deletions frontend/app/src/features/dashboard/dashboard-page.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import { useQuery } from "@tanstack/react-query";
import { Link } from "react-router-dom";
import { Search, Database, CheckCircle2, Plus } from "lucide-react";
import { Plus } from "lucide-react";

import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
import { Button } from "@/components/ui/Button";
import { Skeleton } from "@/components/ui/skeleton";
import { fetchDashboardStats } from "@/lib/api/dashboard";
import { DashboardStatsCards } from "./dashboard-stats";
import { RecentInvestigations } from "./recent-investigations";
import { PageHeader } from "@/components/shared/page-header";
import { useRole } from "@/lib/auth";

export function DashboardPage() {
const { data: stats, isLoading } = useQuery({
const {
data: stats,
isLoading,
isError,
} = useQuery({
queryKey: ["dashboard-stats"],
queryFn: fetchDashboardStats,
});
Expand All @@ -33,60 +37,11 @@ export function DashboardPage() {
}
/>

{/* Stats Grid */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Active Investigations
</CardTitle>
<Search className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
{isLoading ? (
<Skeleton className="h-8 w-16" />
) : (
<div className="text-2xl font-bold">
{stats?.activeInvestigations ?? 0}
</div>
)}
</CardContent>
</Card>

<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Completed Today
</CardTitle>
<CheckCircle2 className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
{isLoading ? (
<Skeleton className="h-8 w-16" />
) : (
<div className="text-2xl font-bold">
{stats?.completedToday ?? 0}
</div>
)}
</CardContent>
</Card>

<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Data Sources</CardTitle>
<Database className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
{isLoading ? (
<Skeleton className="h-8 w-16" />
) : (
<div className="text-2xl font-bold">
{stats?.dataSources ?? 0}
</div>
)}
</CardContent>
</Card>
</div>
<DashboardStatsCards
stats={stats}
isLoading={isLoading}
isError={isError}
/>

{/* Recent Investigations */}
<Card>
Expand Down
43 changes: 43 additions & 0 deletions frontend/app/src/features/dashboard/dashboard-stats.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";

import { DashboardStatsCards } from "./dashboard-stats";

describe("DashboardStatsCards", () => {
it("shows each stat", () => {
render(
<DashboardStatsCards
stats={{ activeInvestigations: 4, completedToday: 1, dataSources: 2 }}
isLoading={false}
isError={false}
/>,
);

expect(screen.getByLabelText("Active Investigations")).toHaveTextContent(
"4",
);
expect(screen.getByLabelText("Completed Today")).toHaveTextContent("1");
expect(screen.getByLabelText("Data Sources")).toHaveTextContent("2");
});

it("shows no numbers when the stats failed to load", () => {
render(
<DashboardStatsCards
stats={undefined}
isLoading={false}
isError={true}
/>,
);

for (const title of [
"Active Investigations",
"Completed Today",
"Data Sources",
]) {
expect(screen.getByLabelText(title)).toHaveTextContent("—");
}
expect(screen.getByRole("alert")).toHaveTextContent(
"Couldn't load dashboard stats.",
);
});
});
52 changes: 52 additions & 0 deletions frontend/app/src/features/dashboard/dashboard-stats.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { CheckCircle2, Database, Search } from "lucide-react";

import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
import { Skeleton } from "@/components/ui/skeleton";
import type { DashboardStats } from "@/lib/api/dashboard";

const STAT_CARDS = [
{ key: "activeInvestigations", title: "Active Investigations", Icon: Search },
{ key: "completedToday", title: "Completed Today", Icon: CheckCircle2 },
{ key: "dataSources", title: "Data Sources", Icon: Database },
] as const;

interface DashboardStatsCardsProps {
stats: DashboardStats | undefined;
isLoading: boolean;
isError: boolean;
}

export function DashboardStatsCards({
stats,
isLoading,
isError,
}: DashboardStatsCardsProps) {
return (
<div className="space-y-2">
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{STAT_CARDS.map(({ key, title, Icon }) => (
<Card key={key}>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">{title}</CardTitle>
<Icon className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
{isLoading ? (
<Skeleton className="h-8 w-16" />
) : (
<div className="text-2xl font-bold" aria-label={title}>
{stats ? stats[key] : "—"}
</div>
)}
</CardContent>
</Card>
))}
</div>
{isError && (
<p role="alert" className="text-sm text-destructive">
Couldn't load dashboard stats.
</p>
)}
</div>
);
}
Loading
Loading