diff --git a/src/components/AttestationHistory.tsx b/src/components/AttestationHistory.tsx
index 98220f0..bf469cf 100644
--- a/src/components/AttestationHistory.tsx
+++ b/src/components/AttestationHistory.tsx
@@ -1,23 +1,419 @@
-// Placeholder component for displaying attestation history
-// This will show all attestations for a commitment
+"use client";
+
+import React, { useCallback, useEffect, useMemo, useState } from "react";
+import {
+ CartesianGrid,
+ Line,
+ LineChart,
+ ResponsiveContainer,
+ Tooltip,
+ XAxis,
+ YAxis,
+} from "recharts";
+import { AlertTriangle, CheckCircle2, RefreshCw } from "lucide-react";
+import { Skeleton } from "@/components/Skeleton";
+import type { Attestation } from "@/lib/types/domain";
interface AttestationHistoryProps {
- commitmentId: string
+ commitmentId: string;
}
-export default function AttestationHistory({ commitmentId }: AttestationHistoryProps) {
- return (
-
- {/* TODO: Implement attestation history with:
- - List of all attestations
- - Timestamps
- - Attestation types
- - Compliance status
- - Health metrics over time
- - Charts/graphs
- */}
-
Attestation History component - Commitment ID: {commitmentId}
-
- )
+type LoadState = "idle" | "loading" | "loaded" | "error";
+
+interface AttestationHistoryItem {
+ id: string;
+ commitmentId: string;
+ kind: string;
+ observedAt: string;
+ attestor: string;
+ complianceScore?: number;
+ violation: boolean;
+ title: string;
+ description?: string;
+ txHash?: string;
+}
+
+interface AttestationApiResponse {
+ success?: boolean;
+ data?: {
+ attestations?: unknown[];
+ };
+ attestations?: unknown[];
+}
+
+const VIOLATION_THRESHOLD = 70;
+
+function isRecord(value: unknown): value is Record {
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
+}
+
+function readNestedNumber(
+ record: Record,
+ keys: string[],
+): number | undefined {
+ for (const key of keys) {
+ const value = record[key];
+ if (typeof value === "number" && Number.isFinite(value)) {
+ return Math.max(0, Math.min(100, Math.round(value)));
+ }
+ if (typeof value === "string" && value.trim() !== "") {
+ const parsed = Number(value);
+ if (Number.isFinite(parsed)) {
+ return Math.max(0, Math.min(100, Math.round(parsed)));
+ }
+ }
+ }
+
+ return undefined;
+}
+
+function readString(
+ record: Record,
+ keys: string[],
+): string | undefined {
+ for (const key of keys) {
+ const value = record[key];
+ if (typeof value === "string" && value.trim() !== "") {
+ return value.trim();
+ }
+ }
+
+ return undefined;
+}
+
+function formatKind(value: string): string {
+ return value
+ .replace(/[_-]/g, " ")
+ .replace(/\b\w/g, (char) => char.toUpperCase());
+}
+
+function formatTimestamp(value: string): string {
+ const date = new Date(value);
+ if (Number.isNaN(date.getTime())) return "Unknown time";
+
+ return new Intl.DateTimeFormat("en", {
+ month: "short",
+ day: "numeric",
+ hour: "2-digit",
+ minute: "2-digit",
+ }).format(date);
+}
+
+function formatTrendDate(value: string): string {
+ const date = new Date(value);
+ if (Number.isNaN(date.getTime())) return "Unknown";
+
+ return new Intl.DateTimeFormat("en", {
+ month: "short",
+ day: "numeric",
+ }).format(date);
+}
+
+function truncateAddress(value: string): string {
+ if (!value || value === "Unknown attestor") return value;
+ if (value.length <= 14) return value;
+
+ return `${value.slice(0, 6)}...${value.slice(-4)}`;
+}
+
+function normalizeAttestation(value: unknown): AttestationHistoryItem | null {
+ if (!isRecord(value)) return null;
+
+ const details = isRecord(value.details) ? value.details : {};
+ const data = isRecord(value.data) ? value.data : {};
+ const commitmentId = readString(value, ["commitmentId", "commitment_id"]);
+ const observedAt =
+ readString(value, ["observedAt", "timestamp", "recordedAt", "createdAt"]) ??
+ readString(details, ["timestamp", "observedAt"]) ??
+ new Date(0).toISOString();
+
+ if (!commitmentId) return null;
+
+ const kind =
+ readString(value, ["kind", "attestationType", "type"]) ??
+ readString(details, ["type", "attestationType"]) ??
+ "attestation";
+ const complianceScore =
+ readNestedNumber(value, ["complianceScore", "compliance_score"]) ??
+ readNestedNumber(details, ["complianceScore", "compliance_score"]) ??
+ readNestedNumber(data, ["complianceScore", "compliance_score"]);
+ const explicitViolation =
+ value.violation === true ||
+ details.violation === true ||
+ value.verdict === "fail" ||
+ value.severity === "violation" ||
+ details.severity === "violation";
+
+ return {
+ id:
+ readString(value, ["id", "attestationId", "attestation_id"]) ??
+ `${commitmentId}:${observedAt}:${kind}`,
+ commitmentId,
+ kind,
+ observedAt,
+ attestor:
+ readString(value, ["attestor", "attestorAddress", "verifiedBy"]) ??
+ readString(details, ["attestor", "attestorAddress", "verifiedBy"]) ??
+ "Unknown attestor",
+ complianceScore,
+ violation:
+ explicitViolation ||
+ (typeof complianceScore === "number" &&
+ complianceScore < VIOLATION_THRESHOLD),
+ title: readString(value, ["title"]) ?? `${formatKind(kind)} attestation`,
+ description:
+ readString(value, ["description"]) ??
+ readString(details, ["notes", "reason"]),
+ txHash: readString(value, ["txHash", "transactionHash"]),
+ };
}
+function extractAttestations(response: AttestationApiResponse): unknown[] {
+ if (Array.isArray(response.data?.attestations)) {
+ return response.data.attestations;
+ }
+
+ if (Array.isArray(response.attestations)) {
+ return response.attestations;
+ }
+
+ return [];
+}
+
+export default function AttestationHistory({
+ commitmentId,
+}: AttestationHistoryProps) {
+ const [items, setItems] = useState([]);
+ const [state, setState] = useState("idle");
+ const [error, setError] = useState(null);
+
+ const loadAttestations = useCallback(async () => {
+ setState("loading");
+ setError(null);
+
+ try {
+ const response = await fetch(
+ `/api/attestations?commitmentId=${encodeURIComponent(commitmentId)}`,
+ );
+
+ if (!response.ok) {
+ throw new Error("Unable to load attestation history.");
+ }
+
+ const payload = (await response.json()) as AttestationApiResponse;
+ const normalized = extractAttestations(payload)
+ .map((entry) => normalizeAttestation(entry as Attestation))
+ .filter((entry): entry is AttestationHistoryItem => Boolean(entry))
+ .filter((entry) => entry.commitmentId === commitmentId)
+ .sort(
+ (a, b) =>
+ new Date(a.observedAt).getTime() - new Date(b.observedAt).getTime(),
+ );
+
+ setItems(normalized);
+ setState("loaded");
+ } catch (err) {
+ setItems([]);
+ setError(
+ err instanceof Error
+ ? err.message
+ : "Unable to load attestation history.",
+ );
+ setState("error");
+ }
+ }, [commitmentId]);
+
+ useEffect(() => {
+ void loadAttestations();
+ }, [loadAttestations]);
+
+ const trendData = useMemo(
+ () =>
+ items
+ .filter((item) => typeof item.complianceScore === "number")
+ .map((item) => ({
+ date: formatTrendDate(item.observedAt),
+ complianceScore: item.complianceScore ?? 0,
+ })),
+ [items],
+ );
+
+ return (
+
+
+
+
+ Attestation History
+
+
+ Commitment {commitmentId}
+
+
+
+ Violation threshold: below {VIOLATION_THRESHOLD}
+
+
+
+ {state === "loading" && (
+
+
+ {[0, 1, 2].map((item) => (
+
+
+
+
+
+ ))}
+
+ )}
+
+ {state === "error" && (
+
+
+
+
+
{error}
+
+ The timeline is unavailable, but the commitment page can stay
+ usable.
+
+
+
+
+ Retry
+
+
+
+ )}
+
+ {state === "loaded" && items.length === 0 && (
+
+
+ No attestations recorded for this commitment yet.
+
+
+ New health checks and rule events will appear here once they are
+ recorded.
+
+
+ )}
+
+ {state === "loaded" && items.length > 0 && (
+
+
+
+
Compliance trend
+
+ Score movement across recorded attestations.
+
+
+ {trendData.length > 0 ? (
+
+
+
+
+
+
+
+
+
+ ) : (
+
+ No numeric compliance scores are available for charting.
+
+ )}
+
+
+
+ {items.map((item) => {
+ const scoreLabel =
+ typeof item.complianceScore === "number"
+ ? `${item.complianceScore}%`
+ : "No score";
+
+ return (
+
+
+
+
+ {formatTimestamp(item.observedAt)}
+
+
{item.title}
+
+ {formatKind(item.kind)} by{" "}
+
+ {truncateAddress(item.attestor)}
+
+
+ {item.description && (
+
+ {item.description}
+
+ )}
+
+
+
+
+ {scoreLabel}
+
+
+
+ {item.violation ? "Violation" : "Pass"}
+
+
+
+
+ );
+ })}
+
+
+ )}
+
+ );
+}
diff --git a/src/components/__tests__/AttestationHistory.test.tsx b/src/components/__tests__/AttestationHistory.test.tsx
new file mode 100644
index 0000000..79a964e
--- /dev/null
+++ b/src/components/__tests__/AttestationHistory.test.tsx
@@ -0,0 +1,236 @@
+/**
+ * @vitest-environment happy-dom
+ */
+
+import React from "react";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import { cleanup, fireEvent, render, screen } from "@testing-library/react";
+import "@testing-library/jest-dom";
+import AttestationHistory from "@/components/AttestationHistory";
+
+vi.mock("recharts", () => ({
+ ResponsiveContainer: ({ children }: { children: React.ReactNode }) =>
+ children,
+ LineChart: ({ children }: { children: React.ReactNode }) => children,
+ CartesianGrid: () => null,
+ XAxis: () => null,
+ YAxis: () => null,
+ Tooltip: () => null,
+ Line: () => null,
+}));
+
+function jsonResponse(payload: unknown, ok = true) {
+ return {
+ ok,
+ json: async () => payload,
+ } as Response;
+}
+
+function fetchMock() {
+ return global.fetch as unknown as ReturnType;
+}
+
+function mockMatchMedia() {
+ Object.defineProperty(window, "matchMedia", {
+ writable: true,
+ value: vi.fn().mockImplementation((query: string) => ({
+ matches: false,
+ media: query,
+ onchange: null,
+ addEventListener: vi.fn(),
+ removeEventListener: vi.fn(),
+ addListener: vi.fn(),
+ removeListener: vi.fn(),
+ dispatchEvent: vi.fn(),
+ })),
+ });
+}
+
+describe("AttestationHistory", () => {
+ beforeEach(() => {
+ mockMatchMedia();
+ vi.stubGlobal("fetch", vi.fn());
+ });
+
+ afterEach(() => {
+ cleanup();
+ vi.unstubAllGlobals();
+ vi.clearAllMocks();
+ });
+
+ it("shows the loading state while attestation records are requested", async () => {
+ let resolveFetch: (response: Response) => void = () => {};
+ fetchMock().mockReturnValueOnce(
+ new Promise((resolve) => {
+ resolveFetch = resolve;
+ }),
+ );
+
+ render( );
+
+ expect(
+ screen.getByLabelText("Loading attestation history"),
+ ).toBeInTheDocument();
+
+ resolveFetch(
+ jsonResponse({
+ success: true,
+ data: { attestations: [] },
+ }),
+ );
+
+ expect(
+ await screen.findByText(
+ "No attestations recorded for this commitment yet.",
+ ),
+ ).toBeInTheDocument();
+ });
+
+ it("filters records by commitment id, sorts them chronologically, and renders scores", async () => {
+ fetchMock().mockResolvedValueOnce(
+ jsonResponse({
+ success: true,
+ data: {
+ attestations: [
+ {
+ id: "other",
+ commitmentId: "CMT-999",
+ kind: "health_check",
+ observedAt: "2026-06-21T08:00:00Z",
+ details: { complianceScore: 88 },
+ },
+ {
+ id: "latest",
+ commitmentId: "CMT-123",
+ kind: "drawdown",
+ title: "Drawdown threshold check",
+ observedAt: "2026-06-21T10:00:00Z",
+ attestor: "GABCDEFGHIJKLMNOPQRSTUVWXYZ23456789",
+ details: {
+ complianceScore: 0,
+ notes: "Drawdown moved beyond the allowed band.",
+ },
+ },
+ {
+ id: "first",
+ commitmentId: "CMT-123",
+ kind: "health_check",
+ title: "Daily health check",
+ observedAt: "2026-06-21T09:00:00Z",
+ verifiedBy: "GATTESTOR0000000000000000000000000000000001",
+ details: { complianceScore: 100 },
+ },
+ ],
+ },
+ }),
+ );
+
+ render( );
+
+ expect(await screen.findByText("Daily health check")).toBeInTheDocument();
+ expect(screen.getByText("Drawdown threshold check")).toBeInTheDocument();
+ expect(screen.queryByText("CMT-999")).not.toBeInTheDocument();
+
+ const renderedTitles = screen
+ .getAllByRole("listitem")
+ .map((item) => item.textContent ?? "");
+ expect(renderedTitles[0]).toContain("Daily health check");
+ expect(renderedTitles[1]).toContain("Drawdown threshold check");
+
+ expect(screen.getByText("100%")).toBeInTheDocument();
+ expect(screen.getByText("0%")).toBeInTheDocument();
+ expect(screen.getByText("GATTES...0001")).toBeInTheDocument();
+ expect(screen.getByText("GABCDE...6789")).toBeInTheDocument();
+ expect(screen.getByText("Pass")).toBeInTheDocument();
+ expect(screen.getByText("Violation")).toBeInTheDocument();
+ expect(screen.getByTestId("attestation-trend-chart")).toBeInTheDocument();
+ });
+
+ it("renders an empty state when the API has no matching records", async () => {
+ fetchMock().mockResolvedValueOnce(
+ jsonResponse({
+ attestations: [
+ {
+ id: "other",
+ commitmentId: "CMT-OTHER",
+ observedAt: "2026-06-21T10:00:00Z",
+ },
+ ],
+ }),
+ );
+
+ render( );
+
+ expect(
+ await screen.findByText(
+ "No attestations recorded for this commitment yet.",
+ ),
+ ).toBeInTheDocument();
+ });
+
+ it("renders a retryable error state when the request fails", async () => {
+ fetchMock()
+ .mockRejectedValueOnce(new Error("Network unavailable"))
+ .mockResolvedValueOnce(
+ jsonResponse({
+ success: true,
+ data: {
+ attestations: [
+ {
+ id: "recovered",
+ commitmentId: "CMT-123",
+ kind: "health_check",
+ title: "Recovered attestation",
+ observedAt: "2026-06-21T09:00:00Z",
+ details: { complianceScore: 95 },
+ },
+ ],
+ },
+ }),
+ );
+
+ render( );
+
+ expect(await screen.findByRole("alert")).toHaveTextContent(
+ "Network unavailable",
+ );
+
+ fireEvent.click(screen.getByRole("button", { name: /retry/i }));
+
+ expect(
+ await screen.findByText("Recovered attestation"),
+ ).toBeInTheDocument();
+ });
+
+ it("handles records without numeric compliance scores", async () => {
+ fetchMock().mockResolvedValueOnce(
+ jsonResponse({
+ success: true,
+ data: {
+ attestations: [
+ {
+ id: "manual",
+ commitmentId: "CMT-123",
+ kind: "manual_review",
+ observedAt: "2026-06-21T09:00:00Z",
+ attestorAddress: "GMANUALREVIEW00000000000000000000000000001",
+ description: "Manual reviewer left a note.",
+ },
+ ],
+ },
+ }),
+ );
+
+ render( );
+
+ expect(
+ await screen.findByText("Manual Review attestation"),
+ ).toBeInTheDocument();
+ expect(screen.getByText("No score")).toBeInTheDocument();
+ expect(
+ screen.getByText(
+ "No numeric compliance scores are available for charting.",
+ ),
+ ).toBeInTheDocument();
+ });
+});