import { useEffect, useMemo, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Clock3, ExternalLink, Settings } from "lucide-react"; import type { InstanceSchedulerHeartbeatAgent } from "@paperclipai/shared"; import { Link } from "@/lib/router"; import { heartbeatsApi } from "../api/heartbeats"; import { agentsApi } from "../api/agents"; import { useBreadcrumbs } from "../context/BreadcrumbContext"; import { EmptyState } from "../components/EmptyState"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { queryKeys } from "../lib/queryKeys"; import { formatDateTime, relativeTime } from "../lib/utils"; function asRecord(value: unknown): Record | null { if (typeof value !== "object" || value === null || Array.isArray(value)) return null; return value as Record; } function humanize(value: string) { return value.replaceAll("_", " "); } function buildAgentHref(agent: InstanceSchedulerHeartbeatAgent) { return `/${agent.companyIssuePrefix}/agents/${encodeURIComponent(agent.agentUrlKey)}`; } export function InstanceSettings() { const { setBreadcrumbs } = useBreadcrumbs(); const queryClient = useQueryClient(); const [actionError, setActionError] = useState(null); useEffect(() => { setBreadcrumbs([ { label: "Instance Settings" }, { label: "Heartbeats" }, ]); }, [setBreadcrumbs]); const heartbeatsQuery = useQuery({ queryKey: queryKeys.instance.schedulerHeartbeats, queryFn: () => heartbeatsApi.listInstanceSchedulerAgents(), refetchInterval: 15_000, }); const toggleMutation = useMutation({ mutationFn: async (agentRow: InstanceSchedulerHeartbeatAgent) => { const agent = await agentsApi.get(agentRow.id, agentRow.companyId); const runtimeConfig = asRecord(agent.runtimeConfig) ?? {}; const heartbeat = asRecord(runtimeConfig.heartbeat) ?? {}; return agentsApi.update( agentRow.id, { runtimeConfig: { ...runtimeConfig, heartbeat: { ...heartbeat, enabled: !agentRow.heartbeatEnabled, }, }, }, agentRow.companyId, ); }, onSuccess: async (_, agentRow) => { setActionError(null); await Promise.all([ queryClient.invalidateQueries({ queryKey: queryKeys.instance.schedulerHeartbeats }), queryClient.invalidateQueries({ queryKey: queryKeys.agents.list(agentRow.companyId) }), queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(agentRow.id) }), ]); }, onError: (error) => { setActionError(error instanceof Error ? error.message : "Failed to update heartbeat."); }, }); const agents = heartbeatsQuery.data ?? []; const activeCount = agents.filter((agent) => agent.schedulerActive).length; const disabledCount = agents.length - activeCount; const grouped = useMemo(() => { const map = new Map(); for (const agent of agents) { let group = map.get(agent.companyId); if (!group) { group = { companyName: agent.companyName, agents: [] }; map.set(agent.companyId, group); } group.agents.push(agent); } return [...map.values()]; }, [agents]); if (heartbeatsQuery.isLoading) { return
Loading scheduler heartbeats...
; } if (heartbeatsQuery.error) { return (
{heartbeatsQuery.error instanceof Error ? heartbeatsQuery.error.message : "Failed to load scheduler heartbeats."}
); } return (

Scheduler Heartbeats

Agents with a timer heartbeat enabled across all of your companies.

{activeCount} active {disabledCount} disabled {grouped.length} {grouped.length === 1 ? "company" : "companies"}
{actionError && (
{actionError}
)} {agents.length === 0 ? ( ) : (
{grouped.map((group) => (
{group.companyName}
{group.agents.map((agent) => { const saving = toggleMutation.isPending && toggleMutation.variables?.id === agent.id; return (
{agent.schedulerActive ? "On" : "Off"} {agent.agentName} {humanize(agent.title ?? agent.role)} {agent.intervalSec}s {agent.lastHeartbeatAt ? relativeTime(agent.lastHeartbeatAt) : "never"}
); })}
))}
)}
); }