From a6233d017515346c765b22aeef06260a939fae20 Mon Sep 17 00:00:00 2001 From: Jamie Brunton Date: Sat, 5 Sep 2026 16:57:57 +0100 Subject: [PATCH 1/2] fix(admin): render standalone feature interests --- apps/admin/src/lib/featureInterests.test.ts | 16 +++++++++ apps/admin/src/lib/featureInterests.ts | 32 +++++++++++++++++ apps/admin/src/pages/DashboardPage.tsx | 38 +++++---------------- 3 files changed, 57 insertions(+), 29 deletions(-) create mode 100644 apps/admin/src/lib/featureInterests.test.ts create mode 100644 apps/admin/src/lib/featureInterests.ts diff --git a/apps/admin/src/lib/featureInterests.test.ts b/apps/admin/src/lib/featureInterests.test.ts new file mode 100644 index 0000000..9d4360e --- /dev/null +++ b/apps/admin/src/lib/featureInterests.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from 'vitest'; +import { formatFeatureName, toRecentFeatureInterest } from './featureInterests'; + +describe('toRecentFeatureInterest', () => { + it('preserves the standalone API feature field for dashboard rendering', () => { + const interest = toRecentFeatureInterest({ + id: 'interest-1', + user_id: 'user-1', + feature: 'better_chat', + created_at: '2026-09-05T12:00:00.000Z', + }); + + expect(interest.feature).toBe('better_chat'); + expect(formatFeatureName(interest.feature)).toBe('Better Chat'); + }); +}); diff --git a/apps/admin/src/lib/featureInterests.ts b/apps/admin/src/lib/featureInterests.ts new file mode 100644 index 0000000..8193fee --- /dev/null +++ b/apps/admin/src/lib/featureInterests.ts @@ -0,0 +1,32 @@ +export interface FeatureInterestRow { + id: string; + user_id: string; + feature: string; + created_at: string; +} + +export interface RecentFeatureInterest extends FeatureInterestRow { + profile?: { + id: string; + name: string | null; + email: string | null; + avatar_url: string | null; + }; +} + +export function toRecentFeatureInterest(row: FeatureInterestRow): RecentFeatureInterest { + return { + id: row.id, + user_id: row.user_id, + feature: row.feature, + created_at: row.created_at, + }; +} + +export function formatFeatureName(feature: string): string { + return feature + .split('_') + .filter(Boolean) + .map(part => part.charAt(0).toUpperCase() + part.slice(1)) + .join(' '); +} diff --git a/apps/admin/src/pages/DashboardPage.tsx b/apps/admin/src/pages/DashboardPage.tsx index 857757e..aecf042 100644 --- a/apps/admin/src/pages/DashboardPage.tsx +++ b/apps/admin/src/pages/DashboardPage.tsx @@ -10,36 +10,21 @@ import { Bar, BarChart, ResponsiveContainer, XAxis, YAxis, Tooltip } from 'recha import { format, subDays, startOfDay, endOfDay, formatDistanceToNow } from 'date-fns'; import { useRealtimeSubscription } from '@/hooks/useRealtimeSubscription'; import { RealtimeStatusIndicator } from '@/components/RealtimeStatusIndicator'; +import { + formatFeatureName, + toRecentFeatureInterest, + type FeatureInterestRow, + type RecentFeatureInterest, +} from '@/lib/featureInterests'; type FeatureInterestChartPoint = { date: string; count: number }; -type RecentFeatureInterest = { - id: string; - user_id: string; - feature_name: string; - created_at: string; - profile?: { - id: string; - name: string | null; - email: string | null; - avatar_url: string | null; - }; -}; - type FeatureInterestCountsRow = { feature_name: string; opt_in_count: number; opt_in_count_last_7_days: number; }; -function formatFeatureName(featureName: string) { - return featureName - .split('_') - .filter(Boolean) - .map(part => part.charAt(0).toUpperCase() + part.slice(1)) - .join(' '); -} - export function DashboardPage() { const { hasPermission } = useAuth(); const canViewUsers = hasPermission(PERMISSION_KEYS.VIEW_USERS); @@ -95,7 +80,7 @@ export function DashboardPage() { ? adminData.from('feature_interests').select('created_at, user_id').gte('created_at', startDate.toISOString()).lte('created_at', endDate.toISOString()) : Promise.resolve({ data: [] }), canViewUsers - ? adminData.from('feature_interests').select('id, user_id, feature_name, created_at').order('created_at', { ascending: false }).limit(10) + ? adminData.from('feature_interests').select('id, user_id, feature, created_at').order('created_at', { ascending: false }).limit(10) : Promise.resolve({ data: [] }), ]); @@ -158,12 +143,7 @@ export function DashboardPage() { })) ); - let recentActivity: RecentFeatureInterest[] = (recentInterests ?? []).map(item => ({ - id: item.id, - user_id: item.user_id, - feature_name: item.feature_name, - created_at: item.created_at, - })); + let recentActivity: RecentFeatureInterest[] = (recentInterests ?? []).map(toRecentFeatureInterest); if (canViewUsers && recentActivity.length > 0) { const userIds = Array.from(new Set(recentActivity.map(item => item.user_id))); @@ -579,7 +559,7 @@ export function DashboardPage() { if (item.profile?.name && item.profile.email) { subtitleParts.push(item.profile.email); } - subtitleParts.push(formatFeatureName(item.feature_name)); + subtitleParts.push(formatFeatureName(item.feature)); const fallbackChar = (item.profile?.name || item.profile?.email || 'U') .charAt(0) From 53a48ab6d55d139ad40817b36e894127a07b480a Mon Sep 17 00:00:00 2001 From: Jamie Brunton Date: Sat, 5 Sep 2026 17:02:07 +0100 Subject: [PATCH 2/2] fix(admin): use feature interest composite key --- apps/admin/src/lib/featureInterests.test.ts | 10 +++++++--- apps/admin/src/lib/featureInterests.ts | 6 ++++-- apps/admin/src/pages/DashboardPage.tsx | 5 +++-- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/apps/admin/src/lib/featureInterests.test.ts b/apps/admin/src/lib/featureInterests.test.ts index 9d4360e..f962c77 100644 --- a/apps/admin/src/lib/featureInterests.test.ts +++ b/apps/admin/src/lib/featureInterests.test.ts @@ -1,16 +1,20 @@ import { describe, expect, it } from 'vitest'; -import { formatFeatureName, toRecentFeatureInterest } from './featureInterests'; +import { featureInterestKey, formatFeatureName, toRecentFeatureInterest } from './featureInterests'; describe('toRecentFeatureInterest', () => { it('preserves the standalone API feature field for dashboard rendering', () => { const interest = toRecentFeatureInterest({ - id: 'interest-1', user_id: 'user-1', feature: 'better_chat', created_at: '2026-09-05T12:00:00.000Z', }); - expect(interest.feature).toBe('better_chat'); + expect(interest).toEqual({ + user_id: 'user-1', + feature: 'better_chat', + created_at: '2026-09-05T12:00:00.000Z', + }); expect(formatFeatureName(interest.feature)).toBe('Better Chat'); + expect(featureInterestKey(interest)).toBe('user-1:better_chat'); }); }); diff --git a/apps/admin/src/lib/featureInterests.ts b/apps/admin/src/lib/featureInterests.ts index 8193fee..4e73ecd 100644 --- a/apps/admin/src/lib/featureInterests.ts +++ b/apps/admin/src/lib/featureInterests.ts @@ -1,5 +1,4 @@ export interface FeatureInterestRow { - id: string; user_id: string; feature: string; created_at: string; @@ -16,13 +15,16 @@ export interface RecentFeatureInterest extends FeatureInterestRow { export function toRecentFeatureInterest(row: FeatureInterestRow): RecentFeatureInterest { return { - id: row.id, user_id: row.user_id, feature: row.feature, created_at: row.created_at, }; } +export function featureInterestKey(row: Pick): string { + return `${row.user_id}:${row.feature}`; +} + export function formatFeatureName(feature: string): string { return feature .split('_') diff --git a/apps/admin/src/pages/DashboardPage.tsx b/apps/admin/src/pages/DashboardPage.tsx index aecf042..28b2b41 100644 --- a/apps/admin/src/pages/DashboardPage.tsx +++ b/apps/admin/src/pages/DashboardPage.tsx @@ -12,6 +12,7 @@ import { useRealtimeSubscription } from '@/hooks/useRealtimeSubscription'; import { RealtimeStatusIndicator } from '@/components/RealtimeStatusIndicator'; import { formatFeatureName, + featureInterestKey, toRecentFeatureInterest, type FeatureInterestRow, type RecentFeatureInterest, @@ -80,7 +81,7 @@ export function DashboardPage() { ? adminData.from('feature_interests').select('created_at, user_id').gte('created_at', startDate.toISOString()).lte('created_at', endDate.toISOString()) : Promise.resolve({ data: [] }), canViewUsers - ? adminData.from('feature_interests').select('id, user_id, feature, created_at').order('created_at', { ascending: false }).limit(10) + ? adminData.from('feature_interests').select('user_id, feature, created_at').order('created_at', { ascending: false }).limit(10) : Promise.resolve({ data: [] }), ]); @@ -566,7 +567,7 @@ export function DashboardPage() { .toUpperCase(); return ( -
+