"use client"; import { useState, useEffect, useCallback, useRef } from "react"; import type { ReactElement } from "react"; import { MosaicSpinner } from "@/components/ui/MosaicSpinner"; import { fetchActivityLogs, ActivityAction, EntityType, type ActivityLog, type ActivityLogFilters, } from "@/lib/api/activity"; import { useWorkspaceId } from "@/lib/hooks"; // ─── Constants ──────────────────────────────────────────────────────── type ActionFilter = "all" | ActivityAction; type EntityFilter = "all" | EntityType; type DateRange = "24h" | "7d" | "30d" | "all"; const ACTION_OPTIONS: { value: ActionFilter; label: string }[] = [ { value: "all", label: "All actions" }, { value: ActivityAction.CREATED, label: "Created" }, { value: ActivityAction.UPDATED, label: "Updated" }, { value: ActivityAction.DELETED, label: "Deleted" }, { value: ActivityAction.COMPLETED, label: "Completed" }, { value: ActivityAction.ASSIGNED, label: "Assigned" }, ]; const ENTITY_OPTIONS: { value: EntityFilter; label: string }[] = [ { value: "all", label: "All entities" }, { value: EntityType.TASK, label: "Tasks" }, { value: EntityType.EVENT, label: "Events" }, { value: EntityType.PROJECT, label: "Projects" }, { value: EntityType.WORKSPACE, label: "Workspaces" }, { value: EntityType.USER, label: "Users" }, { value: EntityType.DOMAIN, label: "Domains" }, { value: EntityType.IDEA, label: "Ideas" }, ]; const DATE_RANGES: { value: DateRange; label: string }[] = [ { value: "24h", label: "Last 24h" }, { value: "7d", label: "7d" }, { value: "30d", label: "30d" }, { value: "all", label: "All" }, ]; const POLL_INTERVAL_MS = 5_000; // ─── Helpers ────────────────────────────────────────────────────────── const ACTION_COLORS: Record = { [ActivityAction.CREATED]: "var(--ms-teal-400)", [ActivityAction.UPDATED]: "var(--ms-blue-400)", [ActivityAction.DELETED]: "var(--danger)", [ActivityAction.COMPLETED]: "var(--ms-emerald-400)", [ActivityAction.ASSIGNED]: "var(--ms-amber-400)", }; function getActionColor(action: string): string { return ACTION_COLORS[action] ?? "var(--muted)"; } const ENTITY_LABELS: Record = { [EntityType.TASK]: "Task", [EntityType.EVENT]: "Event", [EntityType.PROJECT]: "Project", [EntityType.WORKSPACE]: "Workspace", [EntityType.USER]: "User", [EntityType.DOMAIN]: "Domain", [EntityType.IDEA]: "Idea", }; function getEntityTypeLabel(entityType: string): string { return ENTITY_LABELS[entityType] ?? entityType; } function formatRelativeTime(dateStr: string): string { const date = new Date(dateStr); const now = Date.now(); const diffMs = now - date.getTime(); const diffSec = Math.floor(diffMs / 1_000); const diffMin = Math.floor(diffSec / 60); const diffHr = Math.floor(diffMin / 60); const diffDay = Math.floor(diffHr / 24); if (diffSec < 60) return "just now"; if (diffMin < 60) return `${String(diffMin)}m ago`; if (diffHr < 24) return `${String(diffHr)}h ago`; if (diffDay < 30) return `${String(diffDay)}d ago`; return date.toLocaleDateString(); } function isWithinDateRange(dateStr: string, range: DateRange): boolean { if (range === "all") return true; const date = new Date(dateStr); const now = Date.now(); const hours = range === "24h" ? 24 : range === "7d" ? 168 : 720; return now - date.getTime() < hours * 60 * 60 * 1_000; } // ─── Action Badge ───────────────────────────────────────────────────── function ActionBadge({ action }: { action: string }): ReactElement { const color = getActionColor(action); return ( {action.toLowerCase()} ); } // ─── Main Page Component ────────────────────────────────────────────── export default function LogsPage(): ReactElement { const workspaceId = useWorkspaceId(); // Data state const [activities, setActivities] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); // Filters const [actionFilter, setActionFilter] = useState("all"); const [entityFilter, setEntityFilter] = useState("all"); const [dateRange, setDateRange] = useState("7d"); const [searchQuery, setSearchQuery] = useState(""); // Auto-refresh const [autoRefresh, setAutoRefresh] = useState(true); const intervalRef = useRef | null>(null); // ─── Data Loading ───────────────────────────────────────────────── const loadActivities = useCallback(async (): Promise => { try { const filters: ActivityLogFilters = {}; if (workspaceId) { filters.workspaceId = workspaceId; } if (actionFilter !== "all") { filters.action = actionFilter; } if (entityFilter !== "all") { filters.entityType = entityFilter; } const response: Awaited> = await fetchActivityLogs(filters); setActivities(response); setError(null); } catch (err: unknown) { console.error("[Logs] Failed to fetch activity logs:", err); setError( err instanceof Error ? err.message : "We had trouble loading activity logs. Please try again when you're ready." ); } }, [workspaceId, actionFilter, entityFilter]); // Initial load useEffect(() => { let cancelled = false; setIsLoading(true); loadActivities() .then(() => { if (!cancelled) { setIsLoading(false); } }) .catch(() => { if (!cancelled) { setIsLoading(false); } }); return (): void => { cancelled = true; }; }, [loadActivities]); // Auto-refresh polling useEffect(() => { if (autoRefresh) { intervalRef.current = setInterval(() => { void loadActivities(); }, POLL_INTERVAL_MS); } else if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } return (): void => { if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } }; }, [autoRefresh, loadActivities]); // ─── Filtering ──────────────────────────────────────────────────── const filteredActivities = activities.filter((activity) => { // Date range filter if (!isWithinDateRange(activity.createdAt, dateRange)) return false; // Search filter if (searchQuery.trim()) { const q = searchQuery.toLowerCase(); const matchesEntity = getEntityTypeLabel(activity.entityType).toLowerCase().includes(q); const matchesId = activity.entityId.toLowerCase().includes(q); const matchesUser = activity.user?.name?.toLowerCase().includes(q); const matchesEmail = activity.user?.email.toLowerCase().includes(q); if (!matchesEntity && !matchesId && !matchesUser && !matchesEmail) return false; } return true; }); // ─── Manual Refresh ─────────────────────────────────────────────── const handleManualRefresh = (): void => { setIsLoading(true); void loadActivities().finally(() => { setIsLoading(false); }); }; const handleRetry = (): void => { setError(null); handleManualRefresh(); }; // ─── Render ─────────────────────────────────────────────────────── return (
{/* Pulse animation for auto-refresh */} {/* ─── Header ─────────────────────────────────────────────── */}

Activity Logs

Audit trail and activity history

{/* Auto-refresh toggle */} {/* Manual refresh */}
{/* ─── Filter Bar ─────────────────────────────────────────── */}
{/* Action filter */} {/* Entity filter */} {/* Date range tabs */}
{DATE_RANGES.map((range) => ( ))}
{/* Search input */} { setSearchQuery(e.target.value); }} style={{ padding: "8px 12px", borderRadius: 8, fontSize: "0.82rem", border: "1px solid var(--border)", background: "var(--surface)", color: "var(--text)", minWidth: 200, flex: "1 1 200px", maxWidth: 320, }} />
{/* ─── Content ────────────────────────────────────────────── */} {isLoading && activities.length === 0 ? (
) : error !== null ? (

{error}

) : filteredActivities.length === 0 ? (

No activity logs found

) : ( /* ─── Activity Table ──────────────────────────────────────── */
{["Action", "Entity", "User", "Details", "Time"].map((header) => ( ))} {filteredActivities.map((activity) => ( ))}
{header}
)}
); } // ─── Activity Row Component ─────────────────────────────────────────── function ActivityRow({ activity }: { activity: ActivityLog }): ReactElement { return (
{getEntityTypeLabel(activity.entityType)} {activity.entityId}
{activity.user ? (
{activity.user.name ?? activity.user.email} {activity.user.name && ( {activity.user.email} )}
) : ( )} {activity.details ? JSON.stringify(activity.details) : "—"} {formatRelativeTime(activity.createdAt)} ); }