diff --git a/apps/web/src/app/(authenticated)/page.test.tsx b/apps/web/src/app/(authenticated)/page.test.tsx index 98da381..27383bc 100644 --- a/apps/web/src/app/(authenticated)/page.test.tsx +++ b/apps/web/src/app/(authenticated)/page.test.tsx @@ -1,5 +1,5 @@ -import { describe, it, expect, vi } from "vitest"; -import { render, screen } from "@testing-library/react"; +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { render, screen, waitFor } from "@testing-library/react"; import DashboardPage from "./page"; // Mock Phase 3 dashboard widgets @@ -27,29 +27,64 @@ vi.mock("@/components/dashboard/TokenBudget", () => ({ TokenBudget: (): React.JSX.Element =>
Token Budget
, })); +// Mock hooks and API calls +vi.mock("@/lib/hooks", () => ({ + useWorkspaceId: (): string | null => "ws-test-123", +})); + +vi.mock("@/lib/api/dashboard", () => ({ + fetchDashboardSummary: vi.fn().mockResolvedValue({ + metrics: { + activeAgents: 5, + tasksCompleted: 42, + totalTasks: 100, + tasksInProgress: 10, + activeProjects: 3, + errorRate: 0.5, + }, + recentActivity: [], + activeJobs: [], + tokenBudget: [], + }), +})); + describe("DashboardPage", (): void => { - it("should render the DashboardMetrics widget", (): void => { - render(); - expect(screen.getByTestId("dashboard-metrics")).toBeInTheDocument(); + beforeEach((): void => { + vi.clearAllMocks(); }); - it("should render the OrchestratorSessions widget", (): void => { + it("should render the DashboardMetrics widget", async (): Promise => { render(); - expect(screen.getByTestId("orchestrator-sessions")).toBeInTheDocument(); + await waitFor((): void => { + expect(screen.getByTestId("dashboard-metrics")).toBeInTheDocument(); + }); }); - it("should render the QuickActions widget", (): void => { + it("should render the OrchestratorSessions widget", async (): Promise => { render(); - expect(screen.getByTestId("quick-actions")).toBeInTheDocument(); + await waitFor((): void => { + expect(screen.getByTestId("orchestrator-sessions")).toBeInTheDocument(); + }); }); - it("should render the ActivityFeed widget", (): void => { + it("should render the QuickActions widget", async (): Promise => { render(); - expect(screen.getByTestId("activity-feed")).toBeInTheDocument(); + await waitFor((): void => { + expect(screen.getByTestId("quick-actions")).toBeInTheDocument(); + }); }); - it("should render the TokenBudget widget", (): void => { + it("should render the ActivityFeed widget", async (): Promise => { render(); - expect(screen.getByTestId("token-budget")).toBeInTheDocument(); + await waitFor((): void => { + expect(screen.getByTestId("activity-feed")).toBeInTheDocument(); + }); + }); + + it("should render the TokenBudget widget", async (): Promise => { + render(); + await waitFor((): void => { + expect(screen.getByTestId("token-budget")).toBeInTheDocument(); + }); }); }); diff --git a/apps/web/src/app/(authenticated)/page.tsx b/apps/web/src/app/(authenticated)/page.tsx index bf09ec1..3a55688 100644 --- a/apps/web/src/app/(authenticated)/page.tsx +++ b/apps/web/src/app/(authenticated)/page.tsx @@ -1,24 +1,76 @@ "use client"; +import { useState, useEffect } from "react"; import type { ReactElement } from "react"; import { DashboardMetrics } from "@/components/dashboard/DashboardMetrics"; import { OrchestratorSessions } from "@/components/dashboard/OrchestratorSessions"; import { QuickActions } from "@/components/dashboard/QuickActions"; import { ActivityFeed } from "@/components/dashboard/ActivityFeed"; import { TokenBudget } from "@/components/dashboard/TokenBudget"; +import { fetchDashboardSummary } from "@/lib/api/dashboard"; +import type { DashboardSummaryResponse } from "@/lib/api/dashboard"; +import { useWorkspaceId } from "@/lib/hooks"; export default function DashboardPage(): ReactElement { + const workspaceId = useWorkspaceId(); + const [data, setData] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + let cancelled = false; + + async function loadSummary(): Promise { + try { + const summary = await fetchDashboardSummary(workspaceId ?? undefined); + if (!cancelled) { + setData(summary); + } + } catch (err: unknown) { + // Log but do not crash; widgets will render with empty states + console.error("[Dashboard] Failed to fetch summary:", err); + } finally { + if (!cancelled) { + setIsLoading(false); + } + } + } + + void loadSummary(); + + return (): void => { + cancelled = true; + }; + }, [workspaceId]); + + if (isLoading) { + return ( +
+ +
+
+ + +
+
+ + +
+
+
+ ); + } + return (
- +
- +
- - + +
diff --git a/apps/web/src/components/dashboard/ActivityFeed.tsx b/apps/web/src/components/dashboard/ActivityFeed.tsx index 3a623bf..a151a10 100644 --- a/apps/web/src/components/dashboard/ActivityFeed.tsx +++ b/apps/web/src/components/dashboard/ActivityFeed.tsx @@ -1,5 +1,6 @@ import type { ReactElement } from "react"; import { Card, SectionHeader, Badge } from "@mosaic/ui"; +import type { RecentActivity } from "@/lib/api/dashboard"; type BadgeVariantType = | "badge-amber" @@ -10,7 +11,7 @@ type BadgeVariantType = | "badge-purple" | "badge-pulse"; -interface ActivityItem { +interface ActivityDisplayItem { id: string; icon: string; iconBg: string; @@ -18,82 +19,91 @@ interface ActivityItem { highlight: string; rest: string; timestamp: string; - badge?: { - text: string; - variant: BadgeVariantType; + badge?: + | { + text: string; + variant: BadgeVariantType; + } + | undefined; +} + +export interface ActivityFeedProps { + items?: RecentActivity[] | undefined; +} + +/* ------------------------------------------------------------------ */ +/* Mapping helpers */ +/* ------------------------------------------------------------------ */ + +function getIconForAction(action: string): { icon: string; iconBg: string } { + const lower = action.toLowerCase(); + if (lower.includes("complet") || lower.includes("finish") || lower.includes("success")) { + return { icon: "\u2713", iconBg: "rgba(20,184,166,0.15)" }; + } + if (lower.includes("fail") || lower.includes("error")) { + return { icon: "\u2717", iconBg: "rgba(229,72,77,0.15)" }; + } + if (lower.includes("warn") || lower.includes("limit")) { + return { icon: "\u26A0", iconBg: "rgba(245,158,11,0.15)" }; + } + if (lower.includes("start") || lower.includes("creat")) { + return { icon: "\u2191", iconBg: "rgba(47,128,255,0.15)" }; + } + if (lower.includes("update") || lower.includes("modif")) { + return { icon: "\u21BB", iconBg: "rgba(139,92,246,0.15)" }; + } + return { icon: "\u2022", iconBg: "rgba(100,116,139,0.15)" }; +} + +function getBadgeForAction(action: string): ActivityDisplayItem["badge"] { + const lower = action.toLowerCase(); + if (lower.includes("fail") || lower.includes("error")) { + return { text: "error", variant: "badge-red" }; + } + if (lower.includes("warn") || lower.includes("limit")) { + return { text: "warn", variant: "badge-amber" }; + } + return undefined; +} + +function formatRelativeTime(isoDate: string): string { + const now = Date.now(); + const then = new Date(isoDate).getTime(); + const diffMs = now - then; + + if (Number.isNaN(diffMs) || diffMs < 0) return "just now"; + + const minutes = Math.floor(diffMs / 60_000); + if (minutes < 1) return "just now"; + if (minutes < 60) return `${String(minutes)}m ago`; + + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${String(hours)}h ago`; + + const days = Math.floor(hours / 24); + return `${String(days)}d ago`; +} + +function mapActivityToDisplay(activity: RecentActivity): ActivityDisplayItem { + const { icon, iconBg } = getIconForAction(activity.action); + return { + id: activity.id, + icon, + iconBg, + title: "", + highlight: activity.entityType, + rest: ` ${activity.action} (${activity.entityId})`, + timestamp: formatRelativeTime(activity.createdAt), + badge: getBadgeForAction(activity.action), }; } -const activityItems: ActivityItem[] = [ - { - id: "act-1", - icon: "✓", - iconBg: "rgba(20,184,166,0.15)", - title: "", - highlight: "planner-agent", - rest: " completed task analysis for infra-refactor", - timestamp: "2m ago", - }, - { - id: "act-2", - icon: "⚠", - iconBg: "rgba(245,158,11,0.15)", - title: "", - highlight: "executor-agent", - rest: " hit rate limit on Terraform API", - timestamp: "5m ago", - badge: { text: "warn", variant: "badge-amber" }, - }, - { - id: "act-3", - icon: "↑", - iconBg: "rgba(47,128,255,0.15)", - title: "", - highlight: "ORCH-002", - rest: " session started for api-v3-migration", - timestamp: "12m ago", - }, - { - id: "act-4", - icon: "✗", - iconBg: "rgba(229,72,77,0.15)", - title: "", - highlight: "migrator-agent", - rest: " failed to connect to staging database", - timestamp: "18m ago", - badge: { text: "error", variant: "badge-red" }, - }, - { - id: "act-5", - icon: "✓", - iconBg: "rgba(20,184,166,0.15)", - title: "", - highlight: "reviewer-agent", - rest: " approved PR #214 in infra-refactor", - timestamp: "34m ago", - }, - { - id: "act-6", - icon: "⟳", - iconBg: "rgba(139,92,246,0.15)", - title: "Token budget reset for ", - highlight: "gpt-4o", - rest: " model", - timestamp: "1h ago", - }, - { - id: "act-7", - icon: "★", - iconBg: "rgba(20,184,166,0.15)", - title: "Project ", - highlight: "data-pipeline", - rest: " marked as completed", - timestamp: "2h ago", - }, -]; +/* ------------------------------------------------------------------ */ +/* Components */ +/* ------------------------------------------------------------------ */ interface ActivityItemRowProps { - item: ActivityItem; + item: ActivityDisplayItem; } function ActivityItemRow({ item }: ActivityItemRowProps): ReactElement { @@ -155,14 +165,27 @@ function ActivityItemRow({ item }: ActivityItemRowProps): ReactElement { ); } -export function ActivityFeed(): ReactElement { +export function ActivityFeed({ items }: ActivityFeedProps): ReactElement { + const displayItems = items ? items.map(mapActivityToDisplay) : []; + return (
- {activityItems.map((item) => ( - - ))} + {displayItems.length > 0 ? ( + displayItems.map((item) => ) + ) : ( +
+ No recent activity +
+ )}
); diff --git a/apps/web/src/components/dashboard/DashboardMetrics.tsx b/apps/web/src/components/dashboard/DashboardMetrics.tsx index 16a5945..94e28ef 100644 --- a/apps/web/src/components/dashboard/DashboardMetrics.tsx +++ b/apps/web/src/components/dashboard/DashboardMetrics.tsx @@ -1,45 +1,69 @@ import type { ReactElement } from "react"; import { MetricsStrip, type MetricCell } from "@mosaic/ui"; +import type { DashboardMetrics as DashboardMetricsData } from "@/lib/api/dashboard"; -const cells: MetricCell[] = [ - { - label: "Active Agents", - value: "47", - color: "var(--ms-blue-400)", - trend: { direction: "up", text: "↑ +3 from yesterday" }, - }, - { - label: "Tasks Completed", - value: "1,284", - color: "var(--ms-teal-400)", - trend: { direction: "up", text: "↑ +128 today" }, - }, - { - label: "Avg Response Time", - value: "2.4s", - color: "var(--ms-purple-400)", - trend: { direction: "down", text: "↓ -0.3s improved" }, - }, - { - label: "Token Usage", - value: "3.2M", - color: "var(--ms-amber-400)", - trend: { direction: "neutral", text: "78% of budget" }, - }, - { - label: "Error Rate", - value: "0.4%", - color: "var(--ms-red-400)", - trend: { direction: "down", text: "↓ -0.1% improved" }, - }, - { - label: "Active Projects", - value: "8", - color: "var(--ms-cyan-500)", - trend: { direction: "neutral", text: "2 deploying" }, - }, +export interface DashboardMetricsProps { + metrics?: DashboardMetricsData | undefined; +} + +function formatNumber(n: number): string { + return n.toLocaleString(); +} + +function buildCells(metrics: DashboardMetricsData): MetricCell[] { + return [ + { + label: "Active Agents", + value: formatNumber(metrics.activeAgents), + color: "var(--ms-blue-400)", + trend: { direction: "neutral", text: "currently active" }, + }, + { + label: "Tasks Completed", + value: formatNumber(metrics.tasksCompleted), + color: "var(--ms-teal-400)", + trend: { direction: "neutral", text: `of ${formatNumber(metrics.totalTasks)} total` }, + }, + { + label: "Total Tasks", + value: formatNumber(metrics.totalTasks), + color: "var(--ms-purple-400)", + trend: { direction: "neutral", text: "across workspace" }, + }, + { + label: "In Progress", + value: formatNumber(metrics.tasksInProgress), + color: "var(--ms-amber-400)", + trend: { direction: "neutral", text: "tasks running" }, + }, + { + label: "Error Rate", + value: `${String(metrics.errorRate)}%`, + color: "var(--ms-red-400)", + trend: { + direction: metrics.errorRate > 1 ? "up" : "down", + text: metrics.errorRate > 1 ? "above threshold" : "within threshold", + }, + }, + { + label: "Active Projects", + value: formatNumber(metrics.activeProjects), + color: "var(--ms-cyan-500)", + trend: { direction: "neutral", text: "in workspace" }, + }, + ]; +} + +const EMPTY_CELLS: MetricCell[] = [ + { label: "Active Agents", value: "0", color: "var(--ms-blue-400)" }, + { label: "Tasks Completed", value: "0", color: "var(--ms-teal-400)" }, + { label: "Total Tasks", value: "0", color: "var(--ms-purple-400)" }, + { label: "In Progress", value: "0", color: "var(--ms-amber-400)" }, + { label: "Error Rate", value: "0%", color: "var(--ms-red-400)" }, + { label: "Active Projects", value: "0", color: "var(--ms-cyan-500)" }, ]; -export function DashboardMetrics(): ReactElement { +export function DashboardMetrics({ metrics }: DashboardMetricsProps): ReactElement { + const cells = metrics ? buildCells(metrics) : EMPTY_CELLS; return ; } diff --git a/apps/web/src/components/dashboard/OrchestratorSessions.tsx b/apps/web/src/components/dashboard/OrchestratorSessions.tsx index c9e9732..e654163 100644 --- a/apps/web/src/components/dashboard/OrchestratorSessions.tsx +++ b/apps/web/src/components/dashboard/OrchestratorSessions.tsx @@ -3,6 +3,22 @@ import { useState } from "react"; import type { ReactElement } from "react"; import { Card, SectionHeader, Badge, Dot } from "@mosaic/ui"; +import type { ActiveJob } from "@/lib/api/dashboard"; + +/* ------------------------------------------------------------------ */ +/* Internal display types */ +/* ------------------------------------------------------------------ */ + +type DotVariant = "teal" | "blue" | "amber" | "red" | "muted"; + +type BadgeVariant = + | "badge-teal" + | "badge-amber" + | "badge-red" + | "badge-blue" + | "badge-muted" + | "badge-purple" + | "badge-pulse"; interface AgentNode { id: string; @@ -10,7 +26,7 @@ interface AgentNode { avatarColor: string; name: string; task: string; - status: "teal" | "blue" | "amber" | "red" | "muted"; + status: DotVariant; } interface OrchestratorSession { @@ -18,73 +34,94 @@ interface OrchestratorSession { orchId: string; name: string; badge: string; - badgeVariant: - | "badge-teal" - | "badge-amber" - | "badge-red" - | "badge-blue" - | "badge-muted" - | "badge-purple" - | "badge-pulse"; + badgeVariant: BadgeVariant; duration: string; agents: AgentNode[]; } -const sessions: OrchestratorSession[] = [ - { - id: "s1", - orchId: "ORCH-001", - name: "infra-refactor", - badge: "running", - badgeVariant: "badge-teal", - duration: "2h 14m", - agents: [ - { - id: "a1", - initials: "PL", - avatarColor: "rgba(47,128,255,0.15)", - name: "planner-agent", - task: "Analyzing network topology", - status: "blue", - }, - { - id: "a2", - initials: "EX", - avatarColor: "rgba(20,184,166,0.15)", - name: "executor-agent", - task: "Applying Terraform modules", - status: "teal", - }, - { - id: "a3", - initials: "QA", - avatarColor: "rgba(245,158,11,0.15)", - name: "reviewer-agent", - task: "Waiting for executor output", - status: "amber", - }, - ], - }, - { - id: "s2", - orchId: "ORCH-002", - name: "api-v3-migration", - badge: "running", - badgeVariant: "badge-teal", - duration: "45m", - agents: [ - { - id: "a4", - initials: "MG", - avatarColor: "rgba(139,92,246,0.15)", - name: "migrator-agent", - task: "Rewriting endpoint handlers", - status: "blue", - }, - ], - }, +export interface OrchestratorSessionsProps { + jobs?: ActiveJob[] | undefined; +} + +/* ------------------------------------------------------------------ */ +/* Mapping helpers */ +/* ------------------------------------------------------------------ */ + +const STEP_COLORS: string[] = [ + "rgba(47,128,255,0.15)", + "rgba(20,184,166,0.15)", + "rgba(245,158,11,0.15)", + "rgba(139,92,246,0.15)", + "rgba(229,72,77,0.15)", ]; +function statusToDotVariant(status: string): DotVariant { + const lower = status.toLowerCase(); + if (lower === "running" || lower === "active" || lower === "completed") return "teal"; + if (lower === "pending" || lower === "queued") return "blue"; + if (lower === "waiting" || lower === "paused") return "amber"; + if (lower === "failed" || lower === "error") return "red"; + return "muted"; +} + +function statusToBadgeVariant(status: string): BadgeVariant { + const lower = status.toLowerCase(); + if (lower === "running" || lower === "active") return "badge-teal"; + if (lower === "pending" || lower === "queued") return "badge-blue"; + if (lower === "waiting" || lower === "paused") return "badge-amber"; + if (lower === "failed" || lower === "error") return "badge-red"; + if (lower === "completed") return "badge-purple"; + return "badge-muted"; +} + +function formatDuration(isoDate: string): string { + const now = Date.now(); + const start = new Date(isoDate).getTime(); + const diffMs = now - start; + + if (Number.isNaN(diffMs) || diffMs < 0) return "0m"; + + const totalMinutes = Math.floor(diffMs / 60_000); + if (totalMinutes < 60) return `${String(totalMinutes)}m`; + + const hours = Math.floor(totalMinutes / 60); + const minutes = totalMinutes % 60; + return `${String(hours)}h ${String(minutes)}m`; +} + +function initials(name: string): string { + return name + .split(/[\s\-_]+/) + .slice(0, 2) + .map((w) => w.charAt(0).toUpperCase()) + .join(""); +} + +function mapJobToSession(job: ActiveJob): OrchestratorSession { + const agents: AgentNode[] = job.steps.map((step, idx) => ({ + id: step.id, + initials: initials(step.name), + avatarColor: STEP_COLORS[idx % STEP_COLORS.length] ?? "rgba(100,116,139,0.15)", + name: step.name, + task: `Phase: ${step.phase}`, + status: statusToDotVariant(step.status), + })); + + return { + id: job.id, + orchId: job.id.length > 10 ? job.id.slice(0, 10).toUpperCase() : job.id.toUpperCase(), + name: job.type, + badge: job.status, + badgeVariant: statusToBadgeVariant(job.status), + duration: formatDuration(job.createdAt), + agents, + }; +} + +/* ------------------------------------------------------------------ */ +/* Sub-components */ +/* ------------------------------------------------------------------ */ + interface AgentNodeItemProps { agent: AgentNode; } @@ -182,7 +219,7 @@ function OrchCard({ session }: OrchCardProps): ReactElement { marginBottom: 10, }} > - + j.status.toLowerCase() === "running" || j.status.toLowerCase() === "active" + ).length + : 0; + return ( 3 active} + subtitle={ + sessions.length > 0 + ? `${String(activeCount)} of ${String(sessions.length)} jobs running` + : "No active sessions" + } + actions={ + sessions.length > 0 ? ( + {String(activeCount)} active + ) : undefined + } />
- {sessions.map((session) => ( - - ))} + {sessions.length > 0 ? ( + sessions.map((session) => ) + ) : ( +
+ No active sessions +
+ )}
); diff --git a/apps/web/src/components/dashboard/TokenBudget.tsx b/apps/web/src/components/dashboard/TokenBudget.tsx index 7aad59d..2e434cb 100644 --- a/apps/web/src/components/dashboard/TokenBudget.tsx +++ b/apps/web/src/components/dashboard/TokenBudget.tsx @@ -1,7 +1,24 @@ import type { ReactElement } from "react"; import { Card, SectionHeader, ProgressBar, type ProgressBarVariant } from "@mosaic/ui"; +import type { TokenBudgetEntry } from "@/lib/api/dashboard"; -interface ModelBudget { +export interface TokenBudgetProps { + budgets?: TokenBudgetEntry[] | undefined; +} + +/* ------------------------------------------------------------------ */ +/* Helpers */ +/* ------------------------------------------------------------------ */ + +const VARIANT_CYCLE: ProgressBarVariant[] = ["blue", "teal", "purple", "amber"]; + +function formatTokenCount(n: number): string { + if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`; + if (n >= 1_000) return `${(n / 1_000).toFixed(0)}K`; + return String(n); +} + +interface ModelBudgetDisplay { id: string; label: string; usage: string; @@ -9,39 +26,28 @@ interface ModelBudget { variant: ProgressBarVariant; } -const models: ModelBudget[] = [ - { - id: "sonnet", - label: "claude-3-5-sonnet", - usage: "2.1M / 3M", - value: 70, - variant: "blue", - }, - { - id: "haiku", - label: "claude-3-haiku", - usage: "890K / 5M", - value: 18, - variant: "teal", - }, - { - id: "gpt4o", - label: "gpt-4o", - usage: "320K / 1M", - value: 32, - variant: "purple", - }, - { - id: "llama", - label: "local/llama-3.3", - usage: "unlimited", - value: 55, - variant: "amber", - }, -]; +function mapBudgetToDisplay(entry: TokenBudgetEntry, index: number): ModelBudgetDisplay { + const percent = entry.limit > 0 ? Math.round((entry.used / entry.limit) * 100) : 0; + const usage = + entry.limit > 0 + ? `${formatTokenCount(entry.used)} / ${formatTokenCount(entry.limit)}` + : "unlimited"; + + return { + id: entry.model, + label: entry.model, + usage, + value: percent, + variant: VARIANT_CYCLE[index % VARIANT_CYCLE.length] ?? "blue", + }; +} + +/* ------------------------------------------------------------------ */ +/* Components */ +/* ------------------------------------------------------------------ */ interface ModelRowProps { - model: ModelBudget; + model: ModelBudgetDisplay; } function ModelRow({ model }: ModelRowProps): ReactElement { @@ -84,14 +90,27 @@ function ModelRow({ model }: ModelRowProps): ReactElement { ); } -export function TokenBudget(): ReactElement { +export function TokenBudget({ budgets }: TokenBudgetProps): ReactElement { + const displayModels = budgets ? budgets.map(mapBudgetToDisplay) : []; + return (
- {models.map((model) => ( - - ))} + {displayModels.length > 0 ? ( + displayModels.map((model) => ) + ) : ( +
+ No budget data +
+ )}
); diff --git a/apps/web/src/lib/api/dashboard.ts b/apps/web/src/lib/api/dashboard.ts new file mode 100644 index 0000000..ea2851e --- /dev/null +++ b/apps/web/src/lib/api/dashboard.ts @@ -0,0 +1,74 @@ +/** + * Dashboard API Client + * Handles dashboard summary data fetching + */ + +import { apiGet } from "./client"; + +/* ------------------------------------------------------------------ */ +/* Type definitions matching backend DashboardSummaryDto */ +/* ------------------------------------------------------------------ */ + +export interface DashboardMetrics { + activeAgents: number; + tasksCompleted: number; + totalTasks: number; + tasksInProgress: number; + activeProjects: number; + errorRate: number; +} + +export interface RecentActivity { + id: string; + action: string; + entityType: string; + entityId: string; + details: unknown; + userId: string; + createdAt: string; +} + +export interface ActiveJobStep { + id: string; + name: string; + status: string; + phase: string; +} + +export interface ActiveJob { + id: string; + type: string; + status: string; + progressPercent: number; + createdAt: string; + updatedAt: string; + steps: ActiveJobStep[]; +} + +export interface TokenBudgetEntry { + model: string; + used: number; + limit: number; +} + +export interface DashboardSummaryResponse { + metrics: DashboardMetrics; + recentActivity: RecentActivity[]; + activeJobs: ActiveJob[]; + tokenBudget: TokenBudgetEntry[]; +} + +/* ------------------------------------------------------------------ */ +/* API function */ +/* ------------------------------------------------------------------ */ + +/** + * Fetch dashboard summary data for the given workspace. + * + * @param workspaceId - Optional workspace ID sent via X-Workspace-Id header + */ +export async function fetchDashboardSummary( + workspaceId?: string +): Promise { + return apiGet("/api/dashboard/summary", workspaceId ?? undefined); +} diff --git a/apps/web/src/lib/api/index.ts b/apps/web/src/lib/api/index.ts index 5877de4..3d97eae 100644 --- a/apps/web/src/lib/api/index.ts +++ b/apps/web/src/lib/api/index.ts @@ -13,3 +13,4 @@ export * from "./domains"; export * from "./teams"; export * from "./personalities"; export * from "./telemetry"; +export * from "./dashboard";