From debe3869e20b3fff1d07846ee707e03152975b38 Mon Sep 17 00:00:00 2001 From: xx7412421-cloud Date: Sun, 21 Jun 2026 19:51:41 +0800 Subject: [PATCH 1/2] feat: implement attestation history timeline --- src/components/AttestationHistory.tsx | 430 +++++++++++++++++++++++++- 1 file changed, 413 insertions(+), 17 deletions(-) diff --git a/src/components/AttestationHistory.tsx b/src/components/AttestationHistory.tsx index 98220f09..bf469cfe 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. +

+
+ +
+
+ )} + + {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 ( +
  1. +
    +
    +

    + {formatTimestamp(item.observedAt)} +

    +

    {item.title}

    +

    + {formatKind(item.kind)} by{" "} + + {truncateAddress(item.attestor)} + +

    + {item.description && ( +

    + {item.description} +

    + )} +
    + +
    + + {scoreLabel} + + + + {item.violation ? "Violation" : "Pass"} + +
    +
    +
  2. + ); + })} +
+
+ )} +
+ ); +} From cb243797ee0cda093ee40974ecd1ba0975182234 Mon Sep 17 00:00:00 2001 From: xx7412421-cloud Date: Sun, 21 Jun 2026 19:51:42 +0800 Subject: [PATCH 2/2] feat: implement attestation history timeline --- .../__tests__/AttestationHistory.test.tsx | 236 ++++++++++++++++++ 1 file changed, 236 insertions(+) create mode 100644 src/components/__tests__/AttestationHistory.test.tsx diff --git a/src/components/__tests__/AttestationHistory.test.tsx b/src/components/__tests__/AttestationHistory.test.tsx new file mode 100644 index 00000000..79a964e0 --- /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(); + }); +});