Skip to content

Commit 1f9d37e

Browse files
authored
Add history page (#141)
1 parent 5f8bad5 commit 1f9d37e

9 files changed

Lines changed: 564 additions & 33 deletions

File tree

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
2+
import { ActivityHistoryApi } from '../monitorApi/activityHistoryApi'
3+
4+
export const useGetRecentlyUsedApps = (limit = 100, offset = 0) => {
5+
return useQuery({
6+
queryKey: ['recentlyUsedApps', limit, offset],
7+
queryFn: () => ActivityHistoryApi.getRecentlyUsedApps(limit, offset),
8+
})
9+
}
10+
11+
export const useGetAppCount = () => {
12+
return useQuery({
13+
queryKey: ['appCount'],
14+
queryFn: () => ActivityHistoryApi.getAppCount(),
15+
})
16+
}
17+
18+
export const useDeleteApp = () => {
19+
const queryClient = useQueryClient()
20+
21+
return useMutation({
22+
mutationFn: (appId: string) => ActivityHistoryApi.deleteApp(appId),
23+
onSuccess: () => {
24+
queryClient.invalidateQueries({ queryKey: ['recentlyUsedApps'] })
25+
queryClient.invalidateQueries({ queryKey: ['appCount'] })
26+
},
27+
})
28+
}
29+
30+
export const useDeleteApps = () => {
31+
const queryClient = useQueryClient()
32+
33+
return useMutation({
34+
mutationFn: (appIds: string[]) => ActivityHistoryApi.deleteApps(appIds),
35+
onSuccess: () => {
36+
queryClient.invalidateQueries({ queryKey: ['recentlyUsedApps'] })
37+
queryClient.invalidateQueries({ queryKey: ['appCount'] })
38+
},
39+
})
40+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { AppRepo, AppWithLastUsed } from '@/db/monitor/appRepo'
2+
3+
const getRecentlyUsedApps = async (limit = 100, offset = 0): Promise<AppWithLastUsed[]> => {
4+
const apps = await AppRepo.getRecentlyUsedApps(limit, offset)
5+
return apps
6+
}
7+
8+
const deleteApp = async (appId: string): Promise<void> => {
9+
await AppRepo.deleteApp(appId)
10+
}
11+
12+
const deleteApps = async (appIds: string[]): Promise<void> => {
13+
await AppRepo.deleteApps(appIds)
14+
}
15+
16+
const getAppCount = async (): Promise<number> => {
17+
return await AppRepo.getAppCount()
18+
}
19+
20+
export const ActivityHistoryApi = {
21+
getRecentlyUsedApps,
22+
deleteApp,
23+
deleteApps,
24+
getAppCount,
25+
}

src/components/UsageSummaryWithTides.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ export const UsageSummaryWithTides = ({
283283
</div>
284284
)}
285285
</CardContent>
286-
<div className="px-6 flex justify-between items-center">
286+
<div className="px-6 pb-4 flex justify-between items-center">
287287
<div className="text-xs text-muted-foreground">
288288
Total Time Active: {formatTime(totalTime.value)}
289289
</div>

src/db/monitor/activityRepo.ts

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,101 @@ export enum ActivityType {
77
}
88

99
export interface Activity {
10+
id?: number
1011
activity_type: ActivityType
1112
app_window_title: string
1213
created_at: string
1314
timestamp: string
1415
bundle_id: string
16+
app_id?: string
17+
}
18+
19+
export interface ActivityWithApp extends Activity {
20+
app_name?: string
21+
app_external_id?: string
1522
}
1623

1724
const getLatestActivity = async (): Promise<Activity | undefined> => {
1825
const monitorDb = await MonitorDb.getMonitorDb()
1926
const query = 'SELECT * FROM activity ORDER BY id DESC LIMIT 1'
2027
const [activity] = await monitorDb.select<Activity[]>(query)
21-
28+
2229
return activity
2330
}
2431

32+
const getActivities = async (limit = 100, offset = 0): Promise<ActivityWithApp[]> => {
33+
const monitorDb = await MonitorDb.getMonitorDb()
34+
const query = `
35+
SELECT
36+
a.id,
37+
a.activity_type,
38+
a.app_window_title,
39+
a.created_at,
40+
a.timestamp,
41+
a.bundle_id,
42+
a.app_id,
43+
app.name as app_name,
44+
app.app_external_id
45+
FROM activity a
46+
LEFT JOIN app ON app.id = a.app_id
47+
ORDER BY a.id DESC
48+
LIMIT ${limit} OFFSET ${offset}
49+
`
50+
const activities = await monitorDb.select<ActivityWithApp[]>(query)
51+
return activities
52+
}
53+
54+
const getActivitiesByDateRange = async (startDate: string, endDate: string): Promise<ActivityWithApp[]> => {
55+
const monitorDb = await MonitorDb.getMonitorDb()
56+
const query = `
57+
SELECT
58+
a.id,
59+
a.activity_type,
60+
a.app_window_title,
61+
a.created_at,
62+
a.timestamp,
63+
a.bundle_id,
64+
a.app_id,
65+
app.name as app_name,
66+
app.app_external_id
67+
FROM activity a
68+
LEFT JOIN app ON app.id = a.app_id
69+
WHERE a.timestamp >= '${startDate}' AND a.timestamp <= '${endDate}'
70+
ORDER BY a.id DESC
71+
`
72+
const activities = await monitorDb.select<ActivityWithApp[]>(query)
73+
return activities
74+
}
75+
76+
const deleteActivity = async (id: number): Promise<void> => {
77+
const monitorDb = await MonitorDb.getMonitorDb()
78+
await monitorDb.execute(`DELETE FROM activity WHERE id = ${id}`)
79+
}
80+
81+
const deleteActivities = async (ids: number[]): Promise<void> => {
82+
if (ids.length === 0) return
83+
const monitorDb = await MonitorDb.getMonitorDb()
84+
const placeholders = ids.map(() => '?').join(',')
85+
await monitorDb.execute(`DELETE FROM activity WHERE id IN (${placeholders})`, ids)
86+
}
87+
88+
const deleteAllActivities = async (): Promise<void> => {
89+
const monitorDb = await MonitorDb.getMonitorDb()
90+
await monitorDb.execute('DELETE FROM activity')
91+
}
92+
93+
const getActivityCount = async (): Promise<number> => {
94+
const monitorDb = await MonitorDb.getMonitorDb()
95+
const result = await monitorDb.select<[{ count: number }]>('SELECT COUNT(*) as count FROM activity')
96+
return result[0]?.count || 0
97+
}
98+
2599
export const ActivityRepo = {
26100
getLatestActivity,
101+
getActivities,
102+
getActivitiesByDateRange,
103+
deleteActivity,
104+
deleteActivities,
105+
deleteAllActivities,
106+
getActivityCount,
27107
}

src/db/monitor/appRepo.ts

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,70 @@ const createApp = async (externalId: string, isBrowser: boolean, name = ''): Pro
100100
return id
101101
}
102102

103+
export interface AppWithLastUsed extends AppDb {
104+
last_used?: string
105+
activity_count?: number
106+
}
107+
108+
const getRecentlyUsedApps = async (limit = 100, offset = 0): Promise<AppWithLastUsed[]> => {
109+
const monitorDb = await MonitorDb.getMonitorDb()
110+
const query = `
111+
SELECT
112+
a.*,
113+
MAX(act.timestamp) as last_used,
114+
COUNT(act.id) as activity_count
115+
FROM app a
116+
LEFT JOIN activity act ON act.app_id = a.id
117+
GROUP BY a.id
118+
HAVING last_used IS NOT NULL
119+
ORDER BY last_used DESC
120+
LIMIT ${limit} OFFSET ${offset}
121+
`
122+
return await monitorDb.select<AppWithLastUsed[]>(query)
123+
}
124+
125+
const deleteApp = async (appId: string): Promise<void> => {
126+
const monitorDb = await MonitorDb.getMonitorDb()
127+
// Delete related records first (in order of foreign key dependencies)
128+
// 1. Delete activity_state_tag records that reference app_tag
129+
await monitorDb.execute(`DELETE FROM activity_state_tag WHERE app_tag_id IN (SELECT id FROM app_tag WHERE app_id = '${appId}')`)
130+
// 2. Delete activities
131+
await monitorDb.execute(`DELETE FROM activity WHERE app_id = '${appId}'`)
132+
// 3. Delete app_tag records
133+
await monitorDb.execute(`DELETE FROM app_tag WHERE app_id = '${appId}'`)
134+
// 4. Delete the app itself
135+
await monitorDb.execute(`DELETE FROM app WHERE id = '${appId}'`)
136+
}
137+
138+
const deleteApps = async (appIds: string[]): Promise<void> => {
139+
if (appIds.length === 0) return
140+
const monitorDb = await MonitorDb.getMonitorDb()
141+
const placeholders = appIds.map(() => '?').join(',')
142+
// Delete in order of foreign key dependencies
143+
// 1. Delete activity_state_tag records that reference app_tag
144+
await monitorDb.execute(`DELETE FROM activity_state_tag WHERE app_tag_id IN (SELECT id FROM app_tag WHERE app_id IN (${placeholders}))`, appIds)
145+
// 2. Delete activities
146+
await monitorDb.execute(`DELETE FROM activity WHERE app_id IN (${placeholders})`, appIds)
147+
// 3. Delete app_tag records
148+
await monitorDb.execute(`DELETE FROM app_tag WHERE app_id IN (${placeholders})`, appIds)
149+
// 4. Delete the app itself
150+
await monitorDb.execute(`DELETE FROM app WHERE id IN (${placeholders})`, appIds)
151+
}
152+
153+
const getAppCount = async (): Promise<number> => {
154+
const monitorDb = await MonitorDb.getMonitorDb()
155+
const result = await monitorDb.select<[{ count: number }]>('SELECT COUNT(DISTINCT app_id) as count FROM activity WHERE app_id IS NOT NULL')
156+
return result[0]?.count || 0
157+
}
158+
103159
export const AppRepo = {
104160
setAppTag,
105161
getApps,
106162
getAppsByIds,
107163
getAppsByCategoryTags,
108-
createApp
164+
createApp,
165+
getRecentlyUsedApps,
166+
deleteApp,
167+
deleteApps,
168+
getAppCount,
109169
}

src/lib/analytics.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,13 @@ export type AnalyticsEvent =
125125
// App Kanban Board Events
126126
| 'app_dragged'
127127

128+
// Activity History Events
129+
| 'activity_history_search_used'
130+
| 'activity_history_delete_clicked'
131+
| 'activity_history_delete_selected_clicked'
132+
| 'activity_history_delete_all_clicked'
133+
| 'activity_history_page_changed'
134+
128135
export interface AnalyticsEventProperties {
129136
// Focus session properties
130137
difficulty?: 'easy' | 'medium' | 'hard'

0 commit comments

Comments
 (0)