Add live badge to issues with active runs on /issues/active
Server: the /companies/:companyId/live-runs endpoint now returns issueId extracted from contextSnapshot, so the UI can match runs to issues without N+1 queries. UI (Issues.tsx): fetches company live runs (with 5s polling), builds a set of issue IDs with active runs, and shows a pulsing "Live" badge in each matching issue row — matching the existing blue live indicator style from AgentDetail. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -906,6 +906,7 @@ export function agentRoutes(db: Db) {
|
|||||||
agentId: heartbeatRuns.agentId,
|
agentId: heartbeatRuns.agentId,
|
||||||
agentName: agentsTable.name,
|
agentName: agentsTable.name,
|
||||||
adapterType: agentsTable.adapterType,
|
adapterType: agentsTable.adapterType,
|
||||||
|
issueId: sql<string | null>`${heartbeatRuns.contextSnapshot} ->> 'issueId'`.as("issueId"),
|
||||||
})
|
})
|
||||||
.from(heartbeatRuns)
|
.from(heartbeatRuns)
|
||||||
.innerJoin(agentsTable, eq(heartbeatRuns.agentId, agentsTable.id))
|
.innerJoin(agentsTable, eq(heartbeatRuns.agentId, agentsTable.id))
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export interface LiveRunForIssue {
|
|||||||
agentId: string;
|
agentId: string;
|
||||||
agentName: string;
|
agentName: string;
|
||||||
adapterType: string;
|
adapterType: string;
|
||||||
|
issueId?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const heartbeatsApi = {
|
export const heartbeatsApi = {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
import { useNavigate, useLocation } from "react-router-dom";
|
import { useNavigate, useLocation } from "react-router-dom";
|
||||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { issuesApi } from "../api/issues";
|
import { issuesApi } from "../api/issues";
|
||||||
import { agentsApi } from "../api/agents";
|
import { agentsApi } from "../api/agents";
|
||||||
|
import { heartbeatsApi } from "../api/heartbeats";
|
||||||
import { useCompany } from "../context/CompanyContext";
|
import { useCompany } from "../context/CompanyContext";
|
||||||
import { useDialog } from "../context/DialogContext";
|
import { useDialog } from "../context/DialogContext";
|
||||||
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
||||||
@@ -69,6 +70,21 @@ export function Issues() {
|
|||||||
enabled: !!selectedCompanyId,
|
enabled: !!selectedCompanyId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { data: liveRuns } = useQuery({
|
||||||
|
queryKey: queryKeys.liveRuns(selectedCompanyId!),
|
||||||
|
queryFn: () => heartbeatsApi.liveRunsForCompany(selectedCompanyId!),
|
||||||
|
enabled: !!selectedCompanyId,
|
||||||
|
refetchInterval: 5000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const liveIssueIds = useMemo(() => {
|
||||||
|
const ids = new Set<string>();
|
||||||
|
for (const run of liveRuns ?? []) {
|
||||||
|
if (run.issueId) ids.add(run.issueId);
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}, [liveRuns]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setBreadcrumbs([{ label: "Issues" }]);
|
setBreadcrumbs([{ label: "Issues" }]);
|
||||||
}, [setBreadcrumbs]);
|
}, [setBreadcrumbs]);
|
||||||
@@ -169,6 +185,15 @@ export function Issues() {
|
|||||||
}
|
}
|
||||||
trailing={
|
trailing={
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
{liveIssueIds.has(issue.id) && (
|
||||||
|
<span className="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full bg-blue-500/10">
|
||||||
|
<span className="relative flex h-2 w-2">
|
||||||
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-blue-400 opacity-75" />
|
||||||
|
<span className="relative inline-flex rounded-full h-2 w-2 bg-blue-500" />
|
||||||
|
</span>
|
||||||
|
<span className="text-[11px] font-medium text-blue-400">Live</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{issue.assigneeAgentId && (() => {
|
{issue.assigneeAgentId && (() => {
|
||||||
const name = agentName(issue.assigneeAgentId);
|
const name = agentName(issue.assigneeAgentId);
|
||||||
return name
|
return name
|
||||||
|
|||||||
Reference in New Issue
Block a user