fix(FN-789): fix activity log data loading and restore modal styling

- Fix useActivityLog hook to properly load activity log entries from the store
- Add comprehensive test coverage for useActivityLog hook with 243 lines of tests
- Restore missing ActivityLogModal CSS styles (modal overlay, list, animations)
- Add ActivityLogModal component tests for empty state, loading, and error handling
- Register activity log route in App.tsx
- Update README with activity log data source documentation
- Add changeset for patch release
This commit is contained in:
gsxdsm
2026-04-03 14:05:40 -07:00
parent ece4a667f9
commit 339a69d5f8
8 changed files with 350 additions and 128 deletions

View File

@@ -1,6 +1,6 @@
import { useState, useEffect, useCallback, useRef } from "react";
import type { ActivityFeedEntry } from "../api";
import { fetchActivityFeed } from "../api";
import { fetchActivityFeed, fetchActivityLog } from "../api";
export interface UseActivityLogResult {
/** Activity log entries */
@@ -22,7 +22,7 @@ export interface UseActivityLogResult {
const POLL_INTERVAL_MS = 5000; // 5 seconds
export interface UseActivityLogOptions {
/** Filter by project ID */
/** Filter by project ID (used with unified central feed) */
projectId?: string;
/** Filter by event type */
type?: ActivityFeedEntry["type"];
@@ -30,16 +30,31 @@ export interface UseActivityLogOptions {
limit?: number;
/** Whether to auto-refresh */
autoRefresh?: boolean;
/**
* When true, fetch from the unified central activity feed (/api/activity-feed).
* When false (default), fetch from the per-project activity log (/api/activity).
*
* Set to true when the modal operates in a multi-project context (projects
* list provided) so it reads from the unified feed. Default (false) reads
* from the per-project log which is always populated with task events.
*/
useCentralFeed?: boolean;
}
/**
* Hook for fetching and managing the activity log.
* Automatically polls for updates every 5 seconds when enabled.
* Supports filtering by project and event type.
*
* Data source selection:
* - Default (single-project): reads from per-project activity log (/api/activity)
* which is always populated with task lifecycle events for the current project.
* - Multi-project (useCentralFeed=true): reads from unified activity feed
* (/api/activity-feed) which aggregates activity across all registered projects.
*/
export function useActivityLog(options: UseActivityLogOptions = {}): UseActivityLogResult {
const { projectId, type, limit = 50, autoRefresh = true } = options;
const { projectId, type, limit = 50, autoRefresh = true, useCentralFeed = false } = options;
const [entries, setEntries] = useState<ActivityFeedEntry[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
@@ -47,15 +62,39 @@ export function useActivityLog(options: UseActivityLogOptions = {}): UseActivity
const intervalRef = useRef<NodeJS.Timeout | null>(null);
const lastTimestampRef = useRef<string | undefined>(undefined);
/**
* Fetch entries using the appropriate data source.
*
* Per-project log (/api/activity) — the default — reads directly from the
* project's own SQLite database and always contains task lifecycle events.
*
* Unified feed (/api/activity-feed) reads from the central database and
* supports cross-project aggregation.
*/
const refresh = useCallback(async () => {
try {
setLoading(true);
setError(null);
const data = await fetchActivityFeed({ limit, projectId, type });
let data: ActivityFeedEntry[];
if (useCentralFeed) {
data = await fetchActivityFeed({ limit, projectId, type });
} else {
// Per-project: fetchActivityLog returns ActivityLogEntry[] which is a
// subset of ActivityFeedEntry (missing projectId/projectName). Map to
// the full shape so downstream consumers see a uniform interface.
const logEntries = await fetchActivityLog({ limit, type });
data = logEntries.map((entry) => ({
...entry,
projectId: projectId ?? "",
projectName: "",
}));
}
setEntries(data);
setHasMore(data.length === limit);
if (data.length > 0) {
lastTimestampRef.current = data[data.length - 1].timestamp;
}
@@ -64,24 +103,39 @@ export function useActivityLog(options: UseActivityLogOptions = {}): UseActivity
} finally {
setLoading(false);
}
}, [limit, projectId, type]);
}, [limit, projectId, type, useCentralFeed]);
const loadMore = useCallback(async () => {
if (!lastTimestampRef.current) return;
try {
setLoading(true);
const data = await fetchActivityFeed({
limit,
projectId,
type,
since: lastTimestampRef.current
});
let data: ActivityFeedEntry[];
if (useCentralFeed) {
data = await fetchActivityFeed({
limit,
projectId,
type,
since: lastTimestampRef.current,
});
} else {
const logEntries = await fetchActivityLog({
limit,
type,
since: lastTimestampRef.current,
});
data = logEntries.map((entry) => ({
...entry,
projectId: projectId ?? "",
projectName: "",
}));
}
setEntries((prev) => [...prev, ...data]);
setHasMore(data.length === limit);
if (data.length > 0) {
lastTimestampRef.current = data[data.length - 1].timestamp;
}
@@ -90,7 +144,7 @@ export function useActivityLog(options: UseActivityLogOptions = {}): UseActivity
} finally {
setLoading(false);
}
}, [limit, projectId, type]);
}, [limit, projectId, type, useCentralFeed]);
const clear = useCallback(() => {
setEntries([]);