FN-6298: fix agent heartbeat timestamp badges
Fix agent heartbeat card badges to use dedicated timestamp translation keys. - Use separate Last/Next heartbeat timestamp keys so existing label strings do not collide with interpolated badge text. - Add localized timestamp strings for all app locales. - Cover desktop and mobile agent cards against raw placeholder or missing timestamp regressions. Files changed: packages/dashboard/app/components/AgentsView.tsx | 4 ++-- .../app/components/__tests__/AgentsView.test.tsx | 25 +++++++++++++++++++--- .../__tests__/agents-view-mobile.test.tsx | 23 ++++++++++++++++++-- packages/i18n/locales/en/app.json | 2 ++ packages/i18n/locales/es/app.json | 2 ++ packages/i18n/locales/fr/app.json | 2 ++ packages/i18n/locales/ko/app.json | 2 ++ packages/i18n/locales/zh-CN/app.json | 2 ++ packages/i18n/locales/zh-TW/app.json | 2 ++ 9 files changed, 57 insertions(+), 7 deletions(-) Fusion-Task-Id: FN-6298 Fusion-Task-Lineage: 1561bcdc-83c9-4a81-aa7b-09d0c441ac1a
This commit is contained in:
@@ -1852,11 +1852,11 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
|
||||
return (
|
||||
<>
|
||||
<span className="agent-heartbeat-last text-secondary" title={lastAt.toLocaleString()}>
|
||||
{t("agents.lastHeartbeat", "Last: {{time}}", { time: lastAt.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" }) })}
|
||||
{t("agents.lastHeartbeatAt", "Last: {{time}}", { time: lastAt.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" }) })}
|
||||
</span>
|
||||
{isTicking && (
|
||||
<span className="agent-heartbeat-next text-secondary" title={nextAt.toLocaleString()}>
|
||||
{t("agents.nextHeartbeat", "Next: {{time}}", { time: nextAt.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" }) })}
|
||||
{t("agents.nextHeartbeatAt", "Next: {{time}}", { time: nextAt.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" }) })}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { render, screen, fireEvent, waitFor, within } from "@testing-library/react";
|
||||
import i18next from "i18next";
|
||||
import { loadAllAppCss } from "../../test/cssFixture";
|
||||
import { AgentsView } from "../AgentsView";
|
||||
import * as apiModule from "../../api";
|
||||
@@ -184,6 +185,10 @@ describe("AgentsView", () => {
|
||||
mockUpdateSettings.mockResolvedValue({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
i18next.removeResourceBundle("en", "app");
|
||||
});
|
||||
|
||||
const openControlsPanel = async () => {
|
||||
const trigger = await screen.findByRole("button", { name: "Controls" });
|
||||
fireEvent.click(trigger);
|
||||
@@ -817,7 +822,14 @@ describe("AgentsView", () => {
|
||||
expect(options).not.toContain("1m");
|
||||
});
|
||||
|
||||
it("renders Last/Next heartbeat timestamps without seconds", async () => {
|
||||
it("renders Last/Next heartbeat timestamps without seconds when old catalog keys collide", async () => {
|
||||
i18next.addResourceBundle(
|
||||
"en",
|
||||
"app",
|
||||
{ agents: { lastHeartbeat: "Last heartbeat", nextHeartbeat: "Next heartbeat in {{elapsed}}" } },
|
||||
true,
|
||||
true,
|
||||
);
|
||||
const lastHeartbeatAt = "2026-05-04T14:23:45.000Z";
|
||||
mockFetchAgents.mockResolvedValueOnce([
|
||||
{
|
||||
@@ -828,7 +840,7 @@ describe("AgentsView", () => {
|
||||
]);
|
||||
mockFetchAgentStats.mockResolvedValueOnce({ total: 1, byState: { active: 1 }, byRole: { triage: 1 } });
|
||||
|
||||
render(<AgentsView addToast={mockAddToast} />);
|
||||
const { container } = render(<AgentsView addToast={mockAddToast} />);
|
||||
|
||||
const lastAt = new Date(lastHeartbeatAt);
|
||||
const nextAt = new Date(lastAt.getTime() + 300000);
|
||||
@@ -840,6 +852,13 @@ describe("AgentsView", () => {
|
||||
expect(screen.getByText(expectedNext)).toBeTruthy();
|
||||
});
|
||||
|
||||
const lastBadge = container.querySelector(".agent-heartbeat-last");
|
||||
const nextBadge = container.querySelector(".agent-heartbeat-next");
|
||||
expect(lastBadge?.textContent).toMatch(/Last: .*\d/);
|
||||
expect(lastBadge?.textContent).not.toBe("Last heartbeat");
|
||||
expect(nextBadge?.textContent).toMatch(/Next: .*\d/);
|
||||
expect(nextBadge?.textContent).not.toContain("{{");
|
||||
expect(nextBadge?.textContent).not.toContain("{{elapsed}}");
|
||||
expect(screen.queryByText(/Last: .*:\d{2}:\d{2}/)).toBeNull();
|
||||
expect(screen.queryByText(/Next: .*:\d{2}:\d{2}/)).toBeNull();
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import i18next from "i18next";
|
||||
import { AgentsView } from "../AgentsView";
|
||||
import { loadAllAppCss } from "../../test/cssFixture";
|
||||
import type { Agent, AgentCapability, AgentState } from "../../api";
|
||||
@@ -149,6 +150,10 @@ describe("AgentsView mobile adaptations", () => {
|
||||
vi.mocked(fetchOrgTree).mockResolvedValue([]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
i18next.removeResourceBundle("en", "app");
|
||||
});
|
||||
|
||||
it("renders board view grid and board cards", async () => {
|
||||
const { container } = render(<AgentsView addToast={vi.fn()} />);
|
||||
await waitFor(() => expect(screen.getByText("Agents")).toBeTruthy());
|
||||
@@ -161,7 +166,14 @@ describe("AgentsView mobile adaptations", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("renders list view cards", async () => {
|
||||
it("renders list view cards with interpolated heartbeat badges", async () => {
|
||||
i18next.addResourceBundle(
|
||||
"en",
|
||||
"app",
|
||||
{ agents: { lastHeartbeat: "Last heartbeat", nextHeartbeat: "Next heartbeat in {{elapsed}}" } },
|
||||
true,
|
||||
true,
|
||||
);
|
||||
const { container } = render(<AgentsView addToast={vi.fn()} />);
|
||||
await waitFor(() => expect(screen.getByText("Agents")).toBeTruthy());
|
||||
|
||||
@@ -172,6 +184,13 @@ describe("AgentsView mobile adaptations", () => {
|
||||
expect(container.querySelectorAll(".agent-card").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
const lastBadge = container.querySelector(".agent-heartbeat-last");
|
||||
const nextBadge = container.querySelector(".agent-heartbeat-next");
|
||||
expect(lastBadge?.textContent).toMatch(/Last: .*\d/);
|
||||
expect(lastBadge?.textContent).not.toBe("Last heartbeat");
|
||||
expect(nextBadge?.textContent).toMatch(/Next: .*\d/);
|
||||
expect(nextBadge?.textContent).not.toContain("{{");
|
||||
|
||||
// Token-stats panel now lives in the controls popup; open it before
|
||||
// asserting on the panel content.
|
||||
fireEvent.click(screen.getByRole("button", { name: "Controls" }));
|
||||
|
||||
@@ -564,6 +564,7 @@
|
||||
"last24h": "Last 24h",
|
||||
"last7d": "Last 7 days",
|
||||
"lastHeartbeat": "Last heartbeat",
|
||||
"lastHeartbeatAt": "Last: {{time}}",
|
||||
"latestRunLabel": "Latest run",
|
||||
"layoutAuto": "Auto",
|
||||
"layoutAutoAria": "Automatic layout",
|
||||
@@ -656,6 +657,7 @@
|
||||
"next": "Next",
|
||||
"nextExpected": "Next expected",
|
||||
"nextHeartbeat": "Next heartbeat in {{elapsed}}",
|
||||
"nextHeartbeatAt": "Next: {{time}}",
|
||||
"noActiveAssignment": "No active assignment",
|
||||
"noActiveEligible": "No active agents eligible to pause",
|
||||
"noActivityYet": "No activity yet",
|
||||
|
||||
@@ -564,6 +564,7 @@
|
||||
"last24h": "Últimas 24h",
|
||||
"last7d": "Últimos 7 días",
|
||||
"lastHeartbeat": "Última: {{time}}",
|
||||
"lastHeartbeatAt": "Última: {{time}}",
|
||||
"latestRunLabel": "Última ejecución",
|
||||
"layoutAuto": "Auto",
|
||||
"layoutAutoAria": "Diseño automático",
|
||||
@@ -656,6 +657,7 @@
|
||||
"next": "Siguiente",
|
||||
"nextExpected": "Próximo esperado",
|
||||
"nextHeartbeat": "Próxima: {{time}}",
|
||||
"nextHeartbeatAt": "Próxima: {{time}}",
|
||||
"noActiveAssignment": "Sin asignación activa",
|
||||
"noActiveEligible": "Sin agentes activos elegibles para pausar",
|
||||
"noActivityYet": "Sin actividad aún",
|
||||
|
||||
@@ -564,6 +564,7 @@
|
||||
"last24h": "Dernières 24h",
|
||||
"last7d": "7 derniers jours",
|
||||
"lastHeartbeat": "Dernière : {{time}}",
|
||||
"lastHeartbeatAt": "Dernière : {{time}}",
|
||||
"latestRunLabel": "Dernière exécution",
|
||||
"layoutAuto": "Auto",
|
||||
"layoutAutoAria": "Disposition automatique",
|
||||
@@ -656,6 +657,7 @@
|
||||
"next": "Suivant",
|
||||
"nextExpected": "Prochain attendu",
|
||||
"nextHeartbeat": "Prochaine : {{time}}",
|
||||
"nextHeartbeatAt": "Prochaine : {{time}}",
|
||||
"noActiveAssignment": "Aucune assignation active",
|
||||
"noActiveEligible": "Aucun agent actif éligible à la pause",
|
||||
"noActivityYet": "Aucune activité pour l'instant",
|
||||
|
||||
@@ -558,6 +558,7 @@
|
||||
"last24h": "최근 24시간",
|
||||
"last7d": "최근 7일",
|
||||
"lastHeartbeat": "마지막 하트비트",
|
||||
"lastHeartbeatAt": "",
|
||||
"latestRunLabel": "최신 실행",
|
||||
"layoutAuto": "자동",
|
||||
"layoutAutoAria": "자동 레이아웃",
|
||||
@@ -649,6 +650,7 @@
|
||||
"next": "다음",
|
||||
"nextExpected": "다음 예정",
|
||||
"nextHeartbeat": "다음 하트비트까지 {{elapsed}}",
|
||||
"nextHeartbeatAt": "",
|
||||
"noActiveAssignment": "활성 할당 없음",
|
||||
"noActiveEligible": "일시정지 가능한 활성 에이전트 없음",
|
||||
"noActivityYet": "아직 활동 없음",
|
||||
|
||||
@@ -558,6 +558,7 @@
|
||||
"last24h": "最近 24 小时",
|
||||
"last7d": "最近 7 天",
|
||||
"lastHeartbeat": "上次:{{time}}",
|
||||
"lastHeartbeatAt": "上次:{{time}}",
|
||||
"latestRunLabel": "最新运行",
|
||||
"layoutAuto": "自动",
|
||||
"layoutAutoAria": "自动布局",
|
||||
@@ -649,6 +650,7 @@
|
||||
"next": "下一步",
|
||||
"nextExpected": "下次预期",
|
||||
"nextHeartbeat": "下次:{{time}}",
|
||||
"nextHeartbeatAt": "下次:{{time}}",
|
||||
"noActiveAssignment": "无活动任务",
|
||||
"noActiveEligible": "没有符合条件可暂停的活动代理",
|
||||
"noActivityYet": "暂无活动",
|
||||
|
||||
@@ -558,6 +558,7 @@
|
||||
"last24h": "最近 24 小時",
|
||||
"last7d": "最近 7 天",
|
||||
"lastHeartbeat": "上次:{{time}}",
|
||||
"lastHeartbeatAt": "上次:{{time}}",
|
||||
"latestRunLabel": "最新執行",
|
||||
"layoutAuto": "自動",
|
||||
"layoutAutoAria": "自動版面",
|
||||
@@ -649,6 +650,7 @@
|
||||
"next": "下一步",
|
||||
"nextExpected": "下次預期",
|
||||
"nextHeartbeat": "下次:{{time}}",
|
||||
"nextHeartbeatAt": "下次:{{time}}",
|
||||
"noActiveAssignment": "無活動任務",
|
||||
"noActiveEligible": "沒有符合條件可暫停的活動代理",
|
||||
"noActivityYet": "尚無活動",
|
||||
|
||||
Reference in New Issue
Block a user