From d328c7ad68ee62728f03fe67aaeb9d9551d5a2f6 Mon Sep 17 00:00:00 2001 From: Saravana Achu Mac Date: Sat, 4 Apr 2026 11:40:56 -0700 Subject: [PATCH] feat(admin-web): harden timeline review console --- .../src/__tests__/timeline-view.test.ts | 77 +++++++ .../admin-web/src/__tests__/timeline.test.ts | 23 ++ .../src/app/(dashboard)/timeline/page.tsx | 199 ++++++++++-------- .../admin-web/src/app/api/timeline/route.ts | 10 +- dashboards/admin-web/src/lib/timeline-view.ts | 85 ++++++++ .../ECOSYSTEM_IMPLEMENTATION_TRACKER.md | 1 + ...PHASE4_PERSONAL_TIMELINE_EXECUTION_PLAN.md | 6 +- 7 files changed, 312 insertions(+), 89 deletions(-) create mode 100644 dashboards/admin-web/src/__tests__/timeline-view.test.ts create mode 100644 dashboards/admin-web/src/lib/timeline-view.ts diff --git a/dashboards/admin-web/src/__tests__/timeline-view.test.ts b/dashboards/admin-web/src/__tests__/timeline-view.test.ts new file mode 100644 index 00000000..b6086149 --- /dev/null +++ b/dashboards/admin-web/src/__tests__/timeline-view.test.ts @@ -0,0 +1,77 @@ +import { describe, expect, it } from 'vitest'; +import { + buildTimelineGroups, + getProductLabel, + getTimelineStats, + type TimelineItem, +} from '@/lib/timeline-view'; + +const baseItems: TimelineItem[] = [ + { + itemId: 'timeline_1', + occurredAt: '2026-04-04T10:02:00.000Z', + eventName: 'artifact.created', + productId: 'notelett', + title: 'Note created', + summary: 'Latest note', + artifactRefs: ['art_note_1'], + relatedEventIds: ['evt_2'], + actorType: 'user', + visibility: 'private', + correlationId: 'corr_shared', + }, + { + itemId: 'timeline_2', + occurredAt: '2026-04-04T10:01:00.000Z', + eventName: 'capture.transcript.created', + productId: 'lysnrai', + title: 'Transcript captured', + summary: null, + artifactRefs: ['art_transcript_1'], + relatedEventIds: [], + actorType: 'user', + visibility: 'private', + correlationId: 'corr_shared', + }, + { + itemId: 'timeline_3', + occurredAt: '2026-04-04T09:59:00.000Z', + eventName: 'memory.entry.created', + productId: 'mindlyst', + title: 'Memory candidate proposed', + summary: null, + artifactRefs: [], + relatedEventIds: [], + actorType: 'agent', + visibility: 'private', + correlationId: null, + }, +]; + +describe('timeline view helpers', () => { + it('groups correlation-linked items together and sorts them newest-first', () => { + const groups = buildTimelineGroups(baseItems); + + expect(groups).toHaveLength(2); + expect(groups[0]).toMatchObject({ + correlationId: 'corr_shared', + latestOccurredAt: '2026-04-04T10:02:00.000Z', + productIds: ['notelett', 'lysnrai'], + }); + expect(groups[0]?.items.map(item => item.itemId)).toEqual(['timeline_1', 'timeline_2']); + }); + + it('computes timeline stats over grouped and ungrouped items', () => { + expect(getTimelineStats(baseItems)).toEqual({ + uniqueProducts: 3, + uniqueCorrelations: 1, + withArtifacts: 2, + groupedChains: 1, + }); + }); + + it('returns readable labels for known products', () => { + expect(getProductLabel('lysnrai')).toBe('LysnrAI'); + expect(getProductLabel('custom-tool')).toBe('custom-tool'); + }); +}); diff --git a/dashboards/admin-web/src/__tests__/timeline.test.ts b/dashboards/admin-web/src/__tests__/timeline.test.ts index d367db73..a4bb55a9 100644 --- a/dashboards/admin-web/src/__tests__/timeline.test.ts +++ b/dashboards/admin-web/src/__tests__/timeline.test.ts @@ -68,6 +68,29 @@ describe('GET /api/timeline', () => { ); }); + it('allows an explicit product override for internal timeline review', async () => { + mockGetCurrentUser.mockResolvedValue({ id: 'usr_admin', role: 'admin' }); + const fetchMock = vi.fn().mockResolvedValue({ + status: 200, + json: async () => ({ items: [], count: 0 }), + }); + vi.stubGlobal('fetch', fetchMock); + + const res = await callTimeline('?productId=flowmonk&limit=10'); + + expect(res.status).toBe(200); + expect(fetchMock).toHaveBeenCalledWith( + 'http://localhost:4003/api/timeline?limit=10', + expect.objectContaining({ + method: 'GET', + headers: expect.objectContaining({ + 'x-user-id': 'usr_admin', + 'x-product-id': 'flowmonk', + }), + }) + ); + }); + it('returns 502 when platform-service proxying fails', async () => { mockGetCurrentUser.mockResolvedValue({ id: 'usr_admin', role: 'admin' }); vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('boom'))); diff --git a/dashboards/admin-web/src/app/(dashboard)/timeline/page.tsx b/dashboards/admin-web/src/app/(dashboard)/timeline/page.tsx index 3e0ed529..4720fec2 100644 --- a/dashboards/admin-web/src/app/(dashboard)/timeline/page.tsx +++ b/dashboards/admin-web/src/app/(dashboard)/timeline/page.tsx @@ -1,7 +1,7 @@ 'use client'; import { useCallback, useEffect, useMemo, useState } from 'react'; -import { Activity, Filter, Link2, RefreshCw, Search } from 'lucide-react'; +import { Activity, Filter, GitBranch, Link2, RefreshCw, Search } from 'lucide-react'; import { createProxyFetch } from '@/lib/proxy-fetch'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; @@ -14,20 +14,13 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; - -type TimelineItem = { - itemId: string; - occurredAt: string; - eventName: string; - productId: string; - title: string; - summary?: string | null; - artifactRefs: string[]; - relatedEventIds: string[]; - actorType: 'user' | 'agent' | 'system' | 'device'; - visibility: 'private' | 'org' | 'shared' | 'local-only'; - correlationId?: string | null; -}; +import { + buildTimelineGroups, + getProductLabel, + getTimelineStats, + TIMELINE_PRODUCT_OPTIONS, + type TimelineItem, +} from '@/lib/timeline-view'; const apiFetch = createProxyFetch('/api/timeline'); @@ -60,6 +53,7 @@ export default function TimelinePage() { const [correlationId, setCorrelationId] = useState(''); const [eventName, setEventName] = useState('all'); const [artifactId, setArtifactId] = useState(''); + const [productId, setProductId] = useState('current'); const [limit, setLimit] = useState('50'); const loadData = useCallback(async () => { @@ -69,32 +63,21 @@ export default function TimelinePage() { if (correlationId.trim()) params.set('correlationId', correlationId.trim()); if (artifactId.trim()) params.set('artifactId', artifactId.trim()); if (eventName !== 'all') params.set('eventName', eventName); + if (productId !== 'current') params.set('productId', productId); params.set('limit', limit); const query = params.toString(); const data = await apiFetch(query ? `?${query}` : ''); setItems(Array.isArray(data?.items) ? data.items : []); setLoading(false); - }, [artifactId, correlationId, eventName, limit, userId]); + }, [artifactId, correlationId, eventName, limit, productId, userId]); useEffect(() => { void loadData(); }, [loadData]); - const stats = useMemo(() => { - const uniqueProducts = new Set(items.map(item => item.productId)).size; - const uniqueCorrelations = new Set( - items - .map(item => item.correlationId) - .filter((value): value is string => typeof value === 'string' && value.length > 0) - ).size; - const withArtifacts = items.filter(item => item.artifactRefs.length > 0).length; - return { - uniqueProducts, - uniqueCorrelations, - withArtifacts, - }; - }, [items]); + const stats = useMemo(() => getTimelineStats(items), [items]); + const groups = useMemo(() => buildTimelineGroups(items), [items]); return (
@@ -102,7 +85,8 @@ export default function TimelinePage() {

Ecosystem Timeline

- Unified cross-product activity stream from the canonical timeline service + Canonical timeline stream for the selected product scope, with correlation-chain + grouping