+ {/* Header */}
+
+
+
+
Analytics
+ {isLoading && (
+
+ )}
+
+
+
+
+ {/* Summary stat cards */}
+
+
+ }
+ label={`Executions (${hours}h)`}
+ value={totalExecutions}
+ color="text-blue-600"
+ />
+
+
+ }
+ label={`Events (${hours}h)`}
+ value={totalEvents}
+ color="text-indigo-600"
+ />
+
+
+ }
+ label={`Enforcements (${hours}h)`}
+ value={totalEnforcements}
+ color="text-purple-600"
+ />
+
+
+
+ {/* Charts row 1: throughput + failure rate */}
+
+ {/* Execution throughput */}
+
+
+
+ Execution Throughput
+
+
+
+
+ {/* Failure rate */}
+
+
+
+ {/* Charts row 2: status breakdown + event volume */}
+
+ {/* Execution status breakdown */}
+
+
+
+ Execution Status Over Time
+
+
+
+
+ {/* Event volume */}
+
+
+
+ Event Volume
+
+
+
+
+
+ {/* Charts row 3: enforcements + worker health */}
+
+ {/* Enforcement volume */}
+
+
+
+ Enforcement Volume
+
+
+
+
+ {/* Worker status */}
+
+
+
+ Worker Status Transitions
+
+
+
+
+
+ );
+}
+
+// Re-export sub-components and types for standalone use
+export {
+ MiniBarChart,
+ StackedBarChart,
+ FailureRateCard,
+ StatCard,
+ TimeRangeSelector,
+};
+export type { TimeRangeHours };
diff --git a/web/src/components/common/EntityHistoryPanel.tsx b/web/src/components/common/EntityHistoryPanel.tsx
new file mode 100644
index 0000000..b89c95f
--- /dev/null
+++ b/web/src/components/common/EntityHistoryPanel.tsx
@@ -0,0 +1,463 @@
+import { useState } from "react";
+import { formatDistanceToNow } from "date-fns";
+import {
+ ChevronDown,
+ ChevronRight,
+ History,
+ Filter,
+ ChevronLeft,
+ ChevronsLeft,
+ ChevronsRight,
+} from "lucide-react";
+import {
+ useEntityHistory,
+ type HistoryEntityType,
+ type HistoryRecord,
+ type HistoryQueryParams,
+} from "@/hooks/useHistory";
+
+interface EntityHistoryPanelProps {
+ /** The type of entity whose history to display */
+ entityType: HistoryEntityType;
+ /** The entity's primary key */
+ entityId: number;
+ /** Optional title override (default: "Change History") */
+ title?: string;
+ /** Whether the panel starts collapsed (default: true) */
+ defaultCollapsed?: boolean;
+ /** Number of items per page (default: 10) */
+ pageSize?: number;
+}
+
+/**
+ * A reusable panel that displays the change history for an entity.
+ *
+ * Queries the TimescaleDB history hypertables via the API and renders
+ * a timeline of changes with expandable details showing old/new values.
+ */
+export default function EntityHistoryPanel({
+ entityType,
+ entityId,
+ title = "Change History",
+ defaultCollapsed = true,
+ pageSize = 10,
+}: EntityHistoryPanelProps) {
+ const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed);
+ const [page, setPage] = useState(1);
+ const [operationFilter, setOperationFilter] = useState