Pipeline/frontend/src/app/dashboard/page.tsx

1252 lines
39 KiB
TypeScript

"use client";
export const dynamic = "force-dynamic";
import { type KeyboardEvent, type MouseEvent, useMemo } from "react";
import { useRouter } from "next/navigation";
import { useQuery } from "@tanstack/react-query";
import { SignedIn, SignedOut, useAuth } from "@/auth/clerk";
import { Activity, Bot, LayoutGrid, Timer } from "lucide-react";
import { DashboardSidebar } from "@/components/organisms/DashboardSidebar";
import { DashboardShell } from "@/components/templates/DashboardShell";
import { SignedOutPanel } from "@/components/auth/SignedOutPanel";
import { ForgejoIssueMetricCards } from "@/components/git/ForgejoIssueMetricCards";
import { ForgejoHeatmap } from "@/components/git/ForgejoHeatmap";
import {
DashboardMetricCard,
DashboardInfoBlock,
DashboardEmptyState,
PendingApprovalsSection,
SessionsSection,
RecentActivitySection,
} from "@/components/dashboard";
import { ApiError } from "@/api/mutator";
import {
type dashboardMetricsApiV1MetricsDashboardGetResponse,
useDashboardMetricsApiV1MetricsDashboardGet,
} from "@/api/generated/metrics/metrics";
import {
gatewaysStatusApiV1GatewaysStatusGet,
getGatewayProviderUsageApiV1GatewaysGatewayIdProviderUsageGet,
getGatewayRuntimeUsageApiV1GatewaysGatewayIdRuntimeUsageGet,
} from "@/api/generated/gateways/gateways";
import type { GatewaysStatusResponse } from "@/api/generated/model/gatewaysStatusResponse";
import type {
CronStatusResponse,
ProviderUsageResponse,
RuntimeUsageResponse,
SystemHealthResponse,
} from "@/api/generated/model";
import {
getGatewayCronApiV1GatewaysGatewayIdCronGet,
getGatewayHealthApiV1GatewaysGatewayIdHealthGet,
} from "@/api/generated/gateways/gateways";
import {
type ProviderNativeUsageWindow,
RuntimeUsageSection,
aggregateRuntimeUsage,
buildPerGatewayUsage,
type AggregatedRuntimeUsage,
type PerGatewayUsage,
} from "@/components/dashboard/RuntimeUsageSection";
import { GatewayHealthPanel } from "@/components/dashboard/GatewayHealthPanel";
import { GatewayCronPanel } from "@/components/dashboard/GatewayCronPanel";
import {
type listAgentsApiV1AgentsGetResponse,
useListAgentsApiV1AgentsGet,
} from "@/api/generated/agents/agents";
import {
type listBoardsApiV1BoardsGetResponse,
useListBoardsApiV1BoardsGet,
} from "@/api/generated/boards/boards";
import {
type listActivityApiV1ActivityGetResponse,
useListActivityApiV1ActivityGet,
} from "@/api/generated/activity/activity";
import type { ActivityEventRead } from "@/api/generated/model";
import {
getForgejoHeatmap,
getForgejoMetrics,
getForgejoRepositories,
type ForgejoHeatmapDay,
type ForgejoIssueMetrics,
type ForgejoRepository,
} from "@/lib/api-forgejo";
import {
formatRelativeTimestamp,
formatTimestamp,
parseTimestamp,
} from "@/lib/formatters";
type SessionSummary = {
key: string;
title: string;
subtitle: string;
usage: string;
lastSeenAt: string | null;
isMain: boolean;
};
type SummaryRow = {
label: string;
value: string;
tone?: "default" | "success" | "warning" | "danger";
};
type GatewayTarget = {
gatewayId: string;
boardId: string;
boardName: string;
};
type GatewaySnapshot = GatewayTarget & {
connected: boolean;
gatewayUrl: string | null;
sessionsCount: number;
sessions: unknown[];
mainSession: unknown | null;
mainSessionError: string | null;
error: string | null;
requestError: string | null;
};
const DASH = "—";
const DASHBOARD_RANGE = "7d";
const DASHBOARD_RANGE_DAYS = 7;
const DASHBOARD_RANGE_LABEL = "7 days";
const numberFormatter = new Intl.NumberFormat("en-US");
const SESSION_ID_KEYS = ["key", "id", "session_key", "sessionKey", "sessionId"];
const toRecord = (value: unknown): Record<string, unknown> | null => {
if (!value || Array.isArray(value) || typeof value !== "object") return null;
return value as Record<string, unknown>;
};
const readString = (
record: Record<string, unknown> | null,
keys: string[],
): string | null => {
if (!record) return null;
for (const key of keys) {
const value = record[key];
if (typeof value === "string" && value.trim()) return value.trim();
}
return null;
};
const readNumber = (
record: Record<string, unknown> | null,
keys: string[],
): number | null => {
if (!record) return null;
for (const key of keys) {
const value = record[key];
if (typeof value === "number" && Number.isFinite(value)) return value;
if (typeof value === "string") {
const cleaned = value.replace(/[^0-9.-]/g, "");
const parsed = Number.parseFloat(cleaned);
if (Number.isFinite(parsed)) return parsed;
}
}
return null;
};
const readStringFromRecords = (
records: Array<Record<string, unknown> | null>,
keys: string[],
): string | null => {
for (const record of records) {
const value = readString(record, keys);
if (value) return value;
}
return null;
};
const readNumberFromRecords = (
records: Array<Record<string, unknown> | null>,
keys: string[],
): number | null => {
for (const record of records) {
const value = readNumber(record, keys);
if (value !== null) return value;
}
return null;
};
const normalizeEpochMs = (value: number): number => {
if (value >= 1_000_000_000_000) return value;
if (value >= 1_000_000_000) return value * 1000;
return value;
};
const readTimestamp = (
record: Record<string, unknown> | null,
keys: string[],
): string | null => {
if (!record) return null;
for (const key of keys) {
const value = record[key];
if (typeof value === "number" && Number.isFinite(value)) {
const date = new Date(normalizeEpochMs(value));
if (!Number.isNaN(date.getTime())) return date.toISOString();
}
if (typeof value === "string") {
const trimmed = value.trim();
if (!trimmed) continue;
const numeric = Number.parseFloat(trimmed);
if (Number.isFinite(numeric)) {
const date = new Date(normalizeEpochMs(numeric));
if (!Number.isNaN(date.getTime())) return date.toISOString();
}
const parsed = parseTimestamp(trimmed);
if (parsed) return parsed.toISOString();
}
}
return null;
};
const readTimestampFromRecords = (
records: Array<Record<string, unknown> | null>,
keys: string[],
): string | null => {
for (const record of records) {
const value = readTimestamp(record, keys);
if (value) return value;
}
return null;
};
const sessionIdentifiers = (
record: Record<string, unknown> | null,
): string[] => {
if (!record) return [];
const ids = SESSION_ID_KEYS.map((key) => readString(record, [key])).filter(
Boolean,
) as string[];
return [...new Set(ids)];
};
const sharesSessionIdentity = (left: string[], right: string[]): boolean =>
left.some((value) => right.includes(value));
const compactNumber = (value: number): string => {
if (!Number.isFinite(value)) return DASH;
if (Math.abs(value) >= 1_000_000) {
return `${(value / 1_000_000).toFixed(1)}m`;
}
if (Math.abs(value) >= 1_000) {
return `${(value / 1_000).toFixed(1)}k`;
}
return numberFormatter.format(value);
};
const formatCount = (value: number): string =>
Number.isFinite(value)
? numberFormatter.format(Math.max(0, Math.round(value)))
: "0";
const formatPercent = (value: number): string =>
Number.isFinite(value) ? `${value.toFixed(1)}%` : DASH;
const formatPerDay = (total: number, days: number): string => {
if (!Number.isFinite(total) || !Number.isFinite(days) || days <= 0)
return DASH;
return `${(total / days).toFixed(1)}/day`;
};
const formatForgejoDashboardError = (error: Error | null): string | null => {
if (!error) return null;
if (error.message === "Failed to fetch") {
return "Pipeline could not load Git Project data. Check that the frontend can reach the backend API.";
}
return error.message;
};
const toSessionSummaries = (
sessions: unknown[] | null | undefined,
mainSession: unknown,
): SessionSummary[] => {
const sessionRecords = (sessions ?? [])
.map(toRecord)
.filter(Boolean) as Array<Record<string, unknown>>;
const mainRecord = toRecord(mainSession);
const mainIdentifiers = sessionIdentifiers(mainRecord);
if (mainRecord && mainIdentifiers.length > 0) {
const exists = sessionRecords.some((entry) =>
sharesSessionIdentity(sessionIdentifiers(entry), mainIdentifiers),
);
if (!exists) sessionRecords.unshift(mainRecord);
}
const uniqueRecords: Record<string, unknown>[] = [];
const seenIdentifiers = new Set<string>();
for (const entry of sessionRecords) {
const identifiers = sessionIdentifiers(entry);
if (
identifiers.length > 0 &&
identifiers.some((value) => seenIdentifiers.has(value))
) {
continue;
}
uniqueRecords.push(entry);
identifiers.forEach((value) => seenIdentifiers.add(value));
}
return uniqueRecords.map((entry, index) => {
const usageRecord = toRecord(entry.usage);
const statsRecord = toRecord(entry.stats);
const metricsRecord = toRecord(entry.metrics);
const originRecord = toRecord(entry.origin);
const candidateRecords = [entry, usageRecord, statsRecord, metricsRecord];
const identifiers = sessionIdentifiers(entry);
const key =
readString(entry, [
"key",
"session_key",
"sessionKey",
"id",
"sessionId",
]) ?? `session-${index}`;
const label = readString(entry, ["label", "name", "title"]) ?? key;
const channel = readStringFromRecords(
[entry, originRecord],
["channel", "source", "kind", "chatType"],
);
const model = readString(entry, [
"model",
"model_name",
"provider",
"engine",
]);
const modelProvider = readString(entry, [
"modelProvider",
"model_provider",
"provider",
]);
const lastSeenAt = readTimestampFromRecords(candidateRecords, [
"updated_at",
"updatedAt",
"last_updated_at",
"lastUpdatedAt",
"last_seen_at",
"lastSeen",
"last_seen",
"last_active_at",
"lastActiveAt",
"lastActivityAt",
"activityAt",
"created_at",
"createdAt",
]);
const usedTokens = readNumberFromRecords(candidateRecords, [
"used",
"used_tokens",
"tokens",
"current",
"token_count",
"tokenCount",
"totalTokens",
"total_tokens",
"inputTokens",
"input_tokens",
]);
const maxTokens = readNumberFromRecords(candidateRecords, [
"max",
"limit",
"token_limit",
"capacity",
"max_tokens",
"maxTokens",
"context_window",
"contextWindow",
"contextTokens",
"context_tokens",
"maxContextTokens",
"max_context_tokens",
]);
const pctFromPayload = readNumberFromRecords(candidateRecords, [
"pct",
"percent",
"ratio_pct",
"ratioPct",
"token_pct",
"usage_pct",
"percentUsed",
"contextPercent",
]);
const usagePct = Number.isFinite(pctFromPayload ?? NaN)
? Math.max(0, Math.min(100, Math.round(pctFromPayload ?? 0)))
: usedTokens !== null && maxTokens !== null && maxTokens > 0
? Math.max(0, Math.min(100, Math.round((usedTokens / maxTokens) * 100)))
: 0;
const usage =
usedTokens !== null && maxTokens !== null
? `${compactNumber(usedTokens)}/${compactNumber(maxTokens)} (${usagePct}%)`
: usedTokens !== null
? `${compactNumber(usedTokens)} tokens`
: DASH;
const subtitleBits = [channel, model].filter(Boolean) as string[];
const subtitle =
subtitleBits.length > 0 ? subtitleBits.join(" · ") : "Session";
const modelWithProvider =
modelProvider && model && modelProvider !== model
? `${model} · ${modelProvider}`
: model;
const subtitleWithProvider = [channel, modelWithProvider]
.filter(Boolean)
.join(" · ");
return {
key,
title: label,
subtitle: subtitleWithProvider || subtitle,
usage,
lastSeenAt,
isMain:
mainIdentifiers.length > 0 &&
sharesSessionIdentity(identifiers, mainIdentifiers),
};
});
};
export default function DashboardPage() {
const router = useRouter();
const { isSignedIn } = useAuth();
const boardsQuery = useListBoardsApiV1BoardsGet<
listBoardsApiV1BoardsGetResponse,
ApiError
>(
{ limit: 200 },
{
query: {
enabled: Boolean(isSignedIn),
refetchInterval: 30_000,
refetchOnMount: "always",
},
},
);
const agentsQuery = useListAgentsApiV1AgentsGet<
listAgentsApiV1AgentsGetResponse,
ApiError
>(
{ limit: 200 },
{
query: {
enabled: Boolean(isSignedIn),
refetchInterval: 15_000,
refetchOnMount: "always",
},
},
);
const metricsQuery = useDashboardMetricsApiV1MetricsDashboardGet<
dashboardMetricsApiV1MetricsDashboardGetResponse,
ApiError
>(
{
range_key: DASHBOARD_RANGE,
},
{
query: {
enabled: Boolean(isSignedIn),
refetchInterval: 15_000,
refetchOnMount: "always",
retry: 3,
retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 5000),
},
},
);
const activityQuery = useListActivityApiV1ActivityGet<
listActivityApiV1ActivityGetResponse,
ApiError
>(
{ limit: 200 },
{
query: {
enabled: Boolean(isSignedIn),
refetchInterval: 15_000,
refetchOnMount: "always",
},
},
);
const forgejoRepositoriesQuery = useQuery<ForgejoRepository[], Error>({
queryKey: ["dashboard", "forgejo", "repositories"],
enabled: Boolean(isSignedIn),
refetchInterval: 60_000,
refetchOnMount: "always",
queryFn: () => getForgejoRepositories(),
});
const forgejoRepositories = useMemo(
() => forgejoRepositoriesQuery.data ?? [],
[forgejoRepositoriesQuery.data],
);
const forgejoOrganizationId = useMemo(
() =>
forgejoRepositories.find((repository) => repository.organization_id)
?.organization_id ?? null,
[forgejoRepositories],
);
const forgejoMetricsQuery = useQuery<ForgejoIssueMetrics | null, Error>({
queryKey: ["dashboard", "forgejo", "metrics", forgejoOrganizationId],
enabled: Boolean(
isSignedIn &&
!forgejoRepositoriesQuery.isLoading &&
!forgejoRepositoriesQuery.error,
),
refetchInterval: 60_000,
refetchOnMount: "always",
queryFn: () => {
if (!forgejoOrganizationId) return Promise.resolve(null);
return getForgejoMetrics({ organization_id: forgejoOrganizationId });
},
});
const forgejoHeatmapQuery = useQuery<{ days: ForgejoHeatmapDay[]; max_count: number } | null, Error>({
queryKey: ["dashboard", "forgejo", "heatmap", forgejoOrganizationId],
enabled: Boolean(
isSignedIn &&
forgejoOrganizationId &&
!forgejoRepositoriesQuery.isLoading &&
!forgejoRepositoriesQuery.error,
),
refetchInterval: 60_000,
refetchOnMount: "always",
queryFn: () => {
if (!forgejoOrganizationId) return Promise.resolve(null);
return getForgejoHeatmap({ organization_id: forgejoOrganizationId });
},
});
const boards = useMemo(
() =>
boardsQuery.data?.status === 200
? [...(boardsQuery.data.data.items ?? [])].sort((a, b) =>
a.name.localeCompare(b.name),
)
: [],
[boardsQuery.data],
);
const agents = useMemo(
() =>
agentsQuery.data?.status === 200
? [...(agentsQuery.data.data.items ?? [])].sort((a, b) =>
a.name.localeCompare(b.name),
)
: [],
[agentsQuery.data],
);
const metrics =
metricsQuery.data?.status === 200 ? metricsQuery.data.data : null;
const onlineAgents = useMemo(
() =>
agents.filter((agent) => (agent.status ?? "").toLowerCase() === "online")
.length,
[agents],
);
const gatewayTargets = useMemo<GatewayTarget[]>(() => {
const byGateway = new Map<string, GatewayTarget>();
for (const board of boards) {
const gatewayId = board.gateway_id;
if (!gatewayId) continue;
if (byGateway.has(gatewayId)) continue;
byGateway.set(gatewayId, {
gatewayId,
boardId: board.id,
boardName: board.name,
});
}
return [...byGateway.values()].sort((a, b) =>
a.boardName.localeCompare(b.boardName),
);
}, [boards]);
const hasConfiguredGateways = gatewayTargets.length > 0;
const gatewayStatusesQuery = useQuery<GatewaySnapshot[], ApiError>({
queryKey: [
"dashboard",
"gateway-statuses",
gatewayTargets.map((target) => `${target.gatewayId}:${target.boardId}`),
],
enabled: Boolean(isSignedIn && hasConfiguredGateways),
refetchInterval: 15_000,
refetchOnMount: "always",
queryFn: async ({ signal }) => {
return Promise.all(
gatewayTargets.map(async (target): Promise<GatewaySnapshot> => {
try {
const response = await gatewaysStatusApiV1GatewaysStatusGet(
{ board_id: target.boardId },
{ signal },
);
if (response.status !== 200) {
return {
...target,
connected: false,
gatewayUrl: null,
sessionsCount: 0,
sessions: [],
mainSession: null,
mainSessionError: null,
error: null,
requestError: `Gateway status request failed (${response.status})`,
};
}
const payload: GatewaysStatusResponse = response.data;
return {
...target,
connected: Boolean(payload.connected),
gatewayUrl: payload.gateway_url ?? null,
sessionsCount: Number(payload.sessions_count ?? 0),
sessions: Array.isArray(payload.sessions) ? payload.sessions : [],
mainSession: payload.main_session ?? null,
mainSessionError: payload.main_session_error ?? null,
error: payload.error ?? null,
requestError: null,
};
} catch (error) {
if (signal.aborted) throw error;
return {
...target,
connected: false,
gatewayUrl: null,
sessionsCount: 0,
sessions: [],
mainSession: null,
mainSessionError: null,
error: null,
requestError:
error instanceof Error
? error.message
: "Gateway status request failed.",
};
}
}),
);
},
});
const gatewaySnapshots = useMemo(
() => gatewayStatusesQuery.data ?? [],
[gatewayStatusesQuery.data],
);
// Runtime usage — query all gateways in parallel, aggregate + per-gateway
const runtimeUsageQuery = useQuery<
{ aggregate: AggregatedRuntimeUsage; perGateway: PerGatewayUsage[] },
ApiError
>({
queryKey: [
"dashboard",
"runtime-usage",
gatewayTargets.map((t) => t.gatewayId),
],
enabled: Boolean(isSignedIn && hasConfiguredGateways),
refetchInterval: 30_000,
refetchOnMount: "always",
queryFn: async () => {
const results = await Promise.allSettled(
gatewayTargets.map((target) =>
getGatewayRuntimeUsageApiV1GatewaysGatewayIdRuntimeUsageGet(
target.gatewayId,
).then((res) => {
if (res.status === 200) return res.data as RuntimeUsageResponse;
return null;
}),
),
);
const valid = results
.filter(
(r): r is PromiseFulfilledResult<RuntimeUsageResponse> =>
r.status === "fulfilled" && r.value !== null,
)
.map((r) => r.value);
const labels: Record<string, string> = {};
for (const t of gatewayTargets) {
labels[t.gatewayId] = t.boardName;
}
return {
aggregate: aggregateRuntimeUsage(valid),
perGateway: buildPerGatewayUsage(valid, labels),
};
},
});
const runtimeUsage = runtimeUsageQuery.data?.aggregate ?? null;
const perGatewayUsage = runtimeUsageQuery.data?.perGateway ?? [];
const providerUsageQuery = useQuery<ProviderNativeUsageWindow[], ApiError>({
queryKey: [
"dashboard",
"provider-usage",
gatewayTargets.map((t) => t.gatewayId),
],
enabled: Boolean(isSignedIn && hasConfiguredGateways),
refetchInterval: 30_000,
refetchOnMount: "always",
queryFn: async () => {
const settled = await Promise.allSettled(
gatewayTargets.map((target) =>
getGatewayProviderUsageApiV1GatewaysGatewayIdProviderUsageGet(
target.gatewayId,
).then((res) => ({ target, res })),
),
);
const windows: ProviderNativeUsageWindow[] = [];
for (const item of settled) {
if (item.status !== "fulfilled") continue;
const { target, res } = item.value;
if (res.status !== 200) continue;
const payload = res.data as ProviderUsageResponse;
if (!payload.scraper_enabled) continue;
for (const scrape of payload.results ?? []) {
const source = scrape.source ?? "provider_native";
const confidence = scrape.confidence ?? "medium";
if (Array.isArray(scrape.windows) && scrape.windows.length > 0) {
for (const window of scrape.windows) {
windows.push({
key: window.key,
label: window.label,
pctUsed: window.pct_used ?? null,
remainingMs: window.remaining_ms ?? null,
remainingLabel: window.remaining_label ?? null,
source: window.source ?? source,
confidence: window.confidence ?? confidence,
provider: scrape.provider,
gatewayLabel: target.boardName,
});
}
continue;
}
if (scrape.current_pct !== null || scrape.remaining_ms !== null) {
windows.push({
key: "current_session",
label: "Current session",
pctUsed: scrape.current_pct ?? null,
remainingMs: scrape.remaining_ms ?? null,
remainingLabel: scrape.remaining_label ?? null,
source,
confidence,
provider: scrape.provider,
gatewayLabel: target.boardName,
});
}
}
}
return windows;
},
});
const providerUsageWindows = providerUsageQuery.data ?? [];
// Gateway health — query the first gateway only for the compact dashboard panel
const primaryGatewayId = gatewayTargets[0]?.gatewayId ?? null;
const gatewayHealthQuery = useQuery<SystemHealthResponse | null, ApiError>({
queryKey: ["dashboard", "gateway-health", primaryGatewayId],
enabled: Boolean(isSignedIn && primaryGatewayId),
refetchInterval: 60_000,
refetchOnMount: "always",
queryFn: () => {
if (!primaryGatewayId) return Promise.resolve(null);
return getGatewayHealthApiV1GatewaysGatewayIdHealthGet(primaryGatewayId).then(
(r) => (r.status === 200 ? (r.data as SystemHealthResponse) : null),
);
},
});
const gatewayCronQuery = useQuery<CronStatusResponse | null, ApiError>({
queryKey: ["dashboard", "gateway-cron", primaryGatewayId],
enabled: Boolean(isSignedIn && primaryGatewayId),
refetchInterval: 60_000,
refetchOnMount: "always",
queryFn: () => {
if (!primaryGatewayId) return Promise.resolve(null);
return getGatewayCronApiV1GatewaysGatewayIdCronGet(primaryGatewayId).then(
(r) => (r.status === 200 ? (r.data as CronStatusResponse) : null),
);
},
});
// Build a session-id → TopSession lookup for enriching session summaries
const topSessionById = useMemo(() => {
const map = new Map<string, { costUsd: number; totalTokens: number; model: string | null }>();
for (const s of runtimeUsage?.topSessions ?? []) {
if (s.session_id) {
map.set(s.session_id, {
costUsd: s.cost_usd,
totalTokens: s.total_tokens,
model: s.model ?? null,
});
}
}
return map;
}, [runtimeUsage]);
const sessionSummaries = useMemo(
() =>
gatewaySnapshots.flatMap((snapshot) => {
if (snapshot.requestError) return [];
const sourceLabel = snapshot.gatewayUrl || snapshot.boardName;
return toSessionSummaries(snapshot.sessions, snapshot.mainSession).map(
(session) => {
const enrichment = topSessionById.get(session.key) ?? topSessionById.get(`${snapshot.gatewayId}:${session.key}`);
return {
...session,
key: `${snapshot.gatewayId}:${session.key}`,
subtitle: `${sourceLabel} · ${session.subtitle}`,
costUsd: enrichment?.costUsd ?? null,
totalTokens: enrichment?.totalTokens ?? null,
model: enrichment?.model ?? null,
};
},
);
}),
[gatewaySnapshots, topSessionById],
);
const activityEvents = useMemo(
() =>
activityQuery.data?.status === 200
? [...(activityQuery.data.data.items ?? [])]
: [],
[activityQuery.data],
);
const orderedActivityEvents = useMemo(
() =>
[...activityEvents].sort((a, b) => {
const left = parseTimestamp(a.created_at)?.getTime() ?? 0;
const right = parseTimestamp(b.created_at)?.getTime() ?? 0;
return right - left;
}),
[activityEvents],
);
const recentLogs = orderedActivityEvents.slice(0, 8);
const latestThroughputPoint =
metrics?.throughput.primary.points?.[
metrics.throughput.primary.points.length - 1
] ?? null;
const throughputTotal = (metrics?.throughput.primary.points ?? []).reduce(
(sum, point) => sum + Number(point.value ?? 0),
0,
);
const completionDaysCount = (metrics?.throughput.primary.points ?? []).reduce(
(sum, point) => sum + (Number(point.value ?? 0) > 0 ? 1 : 0),
0,
);
const inboxTasksMetric = metrics?.kpis.inbox_tasks ?? 0;
const inProgressTasksMetric = metrics?.kpis.in_progress_tasks ?? 0;
const reviewTasksMetric = metrics?.kpis.review_tasks ?? 0;
const doneTasksMetric = metrics?.kpis.done_tasks ?? 0;
const activeAgentsMetric = onlineAgents;
const tasksTotal =
inboxTasksMetric +
inProgressTasksMetric +
reviewTasksMetric +
doneTasksMetric;
const tasksInProgressMetric =
metrics?.kpis.tasks_in_progress ?? inProgressTasksMetric;
const errorRateMetric = Number(metrics?.kpis.error_rate_pct ?? 0);
const reviewBacklogRatio =
inProgressTasksMetric > 0
? reviewTasksMetric / inProgressTasksMetric
: null;
const gatewayConnectedCount = gatewaySnapshots.filter(
(snapshot) => !snapshot.requestError && snapshot.connected,
).length;
const gatewayDisconnectedCount = gatewaySnapshots.filter(
(snapshot) => !snapshot.requestError && !snapshot.connected,
).length;
const gatewayUnavailableCount = gatewaySnapshots.filter((snapshot) =>
Boolean(snapshot.requestError),
).length;
const gatewayHealthErrorCount = gatewaySnapshots.filter((snapshot) =>
Boolean(snapshot.error || snapshot.mainSessionError),
).length;
const countedSessions = gatewaySnapshots.reduce(
(sum, snapshot) => sum + Math.max(0, snapshot.sessionsCount),
0,
);
const activeSessions = Math.max(countedSessions, sessionSummaries.length);
const gatewayStatusLabel = !hasConfiguredGateways
? "Not configured"
: gatewayStatusesQuery.isLoading
? "Checking"
: gatewayConnectedCount === gatewayTargets.length
? "All connected"
: gatewayConnectedCount > 0
? "Partially connected"
: gatewayUnavailableCount === gatewayTargets.length
? "Unavailable"
: "Disconnected";
const gatewayBadgeTone: "online" | "offline" | "neutral" =
gatewayStatusLabel === "All connected"
? "online"
: gatewayStatusLabel === "Partially connected" ||
gatewayStatusLabel === "Disconnected" ||
gatewayStatusLabel === "Unavailable"
? "offline"
: "neutral";
const gatewayStatusTone: SummaryRow["tone"] =
gatewayStatusLabel === "All connected"
? "success"
: gatewayStatusLabel === "Checking" ||
gatewayStatusLabel === "Not configured"
? "default"
: gatewayStatusLabel === "Partially connected" ||
gatewayStatusLabel === "Disconnected"
? "warning"
: "danger";
const workloadRows: SummaryRow[] = [
{
label: "Total work items",
value: formatCount(tasksTotal),
},
{
label: "Inbox",
value: formatCount(inboxTasksMetric),
},
{
label: "In progress",
value: formatCount(inProgressTasksMetric),
tone: inProgressTasksMetric > 0 ? "warning" : "default",
},
{
label: "In review",
value: formatCount(reviewTasksMetric),
},
{
label: "Completed",
value: formatCount(doneTasksMetric),
tone: doneTasksMetric > 0 ? "success" : "default",
},
];
const throughputRows: SummaryRow[] = [
{
label: "Completed tasks",
value: formatCount(throughputTotal),
},
{
label: "Average throughput",
value: formatPerDay(throughputTotal, DASHBOARD_RANGE_DAYS),
},
{
label: "Error rate",
value: formatPercent(errorRateMetric),
tone: errorRateMetric > 0 ? "warning" : "success",
},
{
label: "Completion consistency",
value: `${formatCount(completionDaysCount)} active days`,
tone:
completionDaysCount >= Math.ceil(DASHBOARD_RANGE_DAYS * 0.75)
? "success"
: "default",
},
{
label: "Review backlog ratio",
value:
reviewBacklogRatio !== null
? `${reviewBacklogRatio.toFixed(2)}x`
: reviewTasksMetric > 0
? "∞"
: "0.00x",
tone:
reviewBacklogRatio !== null
? reviewBacklogRatio > 1
? "warning"
: "success"
: reviewTasksMetric > 0
? "warning"
: "success",
},
];
const gatewayRows: SummaryRow[] = [
{
label: "Gateway status",
value: gatewayStatusLabel,
tone: gatewayStatusTone,
},
{ label: "Configured gateways", value: formatCount(gatewayTargets.length) },
{
label: "Connected gateways",
value: formatCount(gatewayConnectedCount),
tone: gatewayConnectedCount > 0 ? "success" : "default",
},
{
label: "Unavailable gateways",
value: formatCount(gatewayUnavailableCount),
tone: gatewayUnavailableCount > 0 ? "danger" : "default",
},
{
label: "Gateways with issues",
value: formatCount(gatewayHealthErrorCount + gatewayDisconnectedCount),
tone:
gatewayHealthErrorCount + gatewayDisconnectedCount > 0
? "warning"
: "success",
},
];
const pendingApprovalItems = metrics?.pending_approvals.items ?? [];
const pendingApprovalsTotal = metrics?.pending_approvals.total ?? 0;
const hasPendingApprovals = pendingApprovalItems.length > 0;
const activityFeedHref = "/activity";
const forgejoIssueMetrics = forgejoMetricsQuery.data ?? null;
const forgejoIssueMetricsError =
formatForgejoDashboardError(forgejoRepositoriesQuery.error) ??
formatForgejoDashboardError(forgejoMetricsQuery.error) ??
null;
const forgejoIssueMetricsLoading =
forgejoRepositoriesQuery.isLoading || forgejoMetricsQuery.isLoading;
const shouldIgnoreRowNavigation = (target: EventTarget | null): boolean => {
if (!(target instanceof HTMLElement)) return false;
return Boolean(target.closest("a"));
};
const buildActivityEventHref = (event: ActivityEventRead): string => {
const routeName = event.route_name ?? null;
const routeParams = event.route_params ?? {};
if (routeName === "board.approvals") {
const boardId = routeParams.boardId;
if (boardId) {
return `/boards/${encodeURIComponent(boardId)}/approvals`;
}
}
if (routeName === "board") {
const boardId = routeParams.boardId;
if (boardId) {
const params = new URLSearchParams();
Object.entries(routeParams).forEach(([key, value]) => {
if (key !== "boardId") params.set(key, value);
});
const query = params.toString();
return query
? `/boards/${encodeURIComponent(boardId)}?${query}`
: `/boards/${encodeURIComponent(boardId)}`;
}
}
const params = new URLSearchParams(
Object.keys(routeParams).length > 0
? routeParams
: {
eventId: event.id,
eventType: event.event_type,
createdAt: event.created_at,
},
);
if (event.task_id && !params.has("taskId")) {
params.set("taskId", event.task_id);
}
return `${activityFeedHref}?${params.toString()}`;
};
const navigateToActivityFeed = (href: string) => {
router.push(href);
};
const handleLogRowClick = (
event: MouseEvent<HTMLDivElement>,
href: string,
) => {
if (shouldIgnoreRowNavigation(event.target)) return;
navigateToActivityFeed(href);
};
const handleLogRowKeyDown = (
event: KeyboardEvent<HTMLDivElement>,
href: string,
) => {
if (event.key !== "Enter" && event.key !== " ") return;
if (shouldIgnoreRowNavigation(event.target)) return;
event.preventDefault();
navigateToActivityFeed(href);
};
return (
<DashboardShell>
<SignedOut>
<SignedOutPanel
message="Sign in to access the dashboard."
forceRedirectUrl="/onboarding"
signUpForceRedirectUrl="/onboarding"
/>
</SignedOut>
<SignedIn>
<DashboardSidebar />
<main className="flex-1 overflow-y-auto bg-app">
<div className="p-4 md:p-8">
{metricsQuery.error ? (
<div className="mb-4 rounded-lg border border-[color:rgba(248,113,113,0.35)] bg-[color:rgba(248,113,113,0.08)] p-3 text-sm text-[color:var(--danger)]">
Load failed: {metricsQuery.error.message}
</div>
) : null}
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4">
<DashboardMetricCard
title="Online Agents"
value={formatCount(activeAgentsMetric)}
secondary={`${formatCount(agents.length)} total`}
icon={<Bot className="h-4 w-4" />}
tone="accent"
/>
<DashboardMetricCard
title="Tasks In Progress"
value={formatCount(tasksInProgressMetric)}
secondary={`${formatCount(tasksTotal)} total`}
icon={<LayoutGrid className="h-4 w-4" />}
tone="success"
/>
<DashboardMetricCard
title="Error Rate"
value={formatPercent(errorRateMetric)}
secondary={`${formatCount(Number(latestThroughputPoint?.value ?? 0))} completed (latest)`}
icon={<Activity className="h-4 w-4" />}
tone="warning"
/>
<DashboardMetricCard
title="Completion Speed"
value={formatPerDay(throughputTotal, DASHBOARD_RANGE_DAYS)}
secondary={`${formatCount(throughputTotal)} completed`}
infoText={`Based on ${DASHBOARD_RANGE_LABEL}`}
icon={<Timer className="h-4 w-4" />}
tone="success"
/>
</div>
<div className="mt-4">
<ForgejoIssueMetricCards
metrics={forgejoIssueMetrics}
repositories={forgejoRepositories}
isLoading={forgejoIssueMetricsLoading}
error={forgejoIssueMetricsError}
/>
</div>
<div className="mt-4">
<section className="rounded-xl border border-[color:var(--border)] bg-[color:var(--surface)] p-4 shadow-lush md:p-6">
<div className="mb-4">
<h3 className="text-lg font-semibold text-strong">
Git Activity
</h3>
<p className="mt-1 text-sm text-muted">
Issue contributions across all tracked repositories in the
last 12 months.
</p>
</div>
<ForgejoHeatmap
days={forgejoHeatmapQuery.data?.days ?? []}
maxCount={forgejoHeatmapQuery.data?.max_count ?? 0}
isLoading={forgejoHeatmapQuery.isLoading}
/>
</section>
</div>
<div className="mt-4 grid grid-cols-1 gap-4 xl:grid-cols-3">
<DashboardInfoBlock title="Workload" rows={workloadRows} />
<DashboardInfoBlock
title="Throughput"
infoText={`All throughput values are calculated for ${DASHBOARD_RANGE_LABEL}`}
rows={throughputRows}
/>
<DashboardInfoBlock
title="Gateway Health"
badge={{ text: gatewayStatusLabel, tone: gatewayBadgeTone }}
rows={gatewayRows}
/>
</div>
<PendingApprovalsSection
items={pendingApprovalItems}
total={pendingApprovalsTotal}
isLoading={!metrics && metricsQuery.isLoading}
hasError={!metrics && Boolean(metricsQuery.error)}
formatCount={formatCount}
formatRelative={formatRelativeTimestamp}
/>
{hasConfiguredGateways && (
<div className="mt-4">
<RuntimeUsageSection
usage={runtimeUsage}
providerUsageWindows={providerUsageWindows}
perGatewayUsage={perGatewayUsage}
isLoading={runtimeUsageQuery.isLoading || providerUsageQuery.isLoading}
hasGateways={hasConfiguredGateways}
/>
</div>
)}
<div className="mt-4 grid grid-cols-1 gap-4 md:grid-cols-2">
<SessionsSection
sessions={sessionSummaries}
activeSessions={activeSessions}
hasConfiguredGateways={hasConfiguredGateways}
isLoading={gatewayStatusesQuery.isLoading}
gatewayUnavailableCount={gatewayUnavailableCount}
gatewayTargetsCount={gatewayTargets.length}
formatCount={formatCount}
formatRelative={formatRelativeTimestamp}
dash={DASH}
/>
<RecentActivitySection
events={recentLogs}
feedHref={activityFeedHref}
onRowClick={handleLogRowClick}
onRowKeyDown={handleLogRowKeyDown}
buildHref={buildActivityEventHref}
formatRelative={formatRelativeTimestamp}
formatTimestamp={formatTimestamp}
/>
</div>
{hasConfiguredGateways && (
<div className="mt-4 grid grid-cols-1 gap-4 md:grid-cols-2">
<GatewayHealthPanel
health={gatewayHealthQuery.data ?? null}
isLoading={gatewayHealthQuery.isLoading}
hasGateways={hasConfiguredGateways}
/>
<GatewayCronPanel
cron={gatewayCronQuery.data ?? null}
isLoading={gatewayCronQuery.isLoading}
hasGateways={hasConfiguredGateways}
/>
</div>
)}
</div>
</main>
</SignedIn>
</DashboardShell>
);
}