Skip to content

Commit 19eb108

Browse files
fix: address all PR review comments on admin dashboard
- Fix $socket -> $socketio in useAdminStats.ts (bug: live updates never worked) - Remove unused useAdminTimeSeries composable from useAdminStats.ts - Replace Promise.all with Promise.allSettled in admin/index.vue fetchAllSeries - Move cleanup assignment before donut chart early-return in AdminChart.vue - Fix encryption issue: replace raw SQL JSON operators with TypeORM find() in querySyncHealthSummary and getSyncHealthData (connection_details is encrypted) - Fix hardcoded failedSources=0: compute stale count with 72h threshold - Use redis.scanIterator instead of blocking redis.keys for AI session count Co-authored-by: mustafaneguib <1302045+mustafaneguib@users.noreply.github.com>
1 parent 5926b62 commit 19eb108

4 files changed

Lines changed: 92 additions & 115 deletions

File tree

backend/src/processors/AdminStatsProcessor.ts

Lines changed: 47 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { AppDataSource } from '../datasources/PostgresDS.js';
2+
import { DRADataSource } from '../models/DRADataSource.js';
23
import { getRedisClient } from '../config/redis.config.js';
34
import { ScheduledBackupProcessor } from './ScheduledBackupProcessor.js';
45
import { ScheduledBackupService } from '../services/ScheduledBackupService.js';
@@ -151,8 +152,9 @@ export class AdminStatsProcessor {
151152
let activeRedisSessions = 0;
152153
try {
153154
const redis = getRedisClient();
154-
const keys = await redis.keys('dra:ai:*session*');
155-
activeRedisSessions = keys.length;
155+
for await (const _key of redis.scanIterator({ MATCH: 'dra:ai:*session*' })) {
156+
activeRedisSessions++;
157+
}
156158
} catch {
157159
activeRedisSessions = 0;
158160
}
@@ -183,63 +185,63 @@ export class AdminStatsProcessor {
183185
};
184186
}
185187

186-
private async querySyncHealthSummary(manager: any) {
187-
const rows = await manager.query(`
188-
SELECT
189-
COUNT(*)::int AS total,
190-
COUNT(*) FILTER (
191-
WHERE data_type NOT IN ('postgresql','mysql','mariadb','mongodb','csv','excel','pdf')
192-
AND (
193-
connection_details->'api_connection_details'->'api_config'->>'last_sync' IS NULL
194-
OR connection_details->'api_connection_details'->'api_config'->>'last_sync' = 'null'
195-
)
196-
)::int AS never_synced,
197-
0::int AS failed
198-
FROM dra_data_sources
199-
`);
200-
const r = rows[0] || {};
188+
private async querySyncHealthSummary(_manager: any) {
189+
const FILE_DB_TYPES = ['postgresql', 'mysql', 'mariadb', 'mongodb', 'csv', 'excel', 'pdf'];
190+
const dataSources = await AppDataSource.manager.find(DRADataSource);
191+
192+
const total = dataSources.length;
193+
let neverSynced = 0;
194+
let failedSources = 0;
195+
196+
for (const ds of dataSources) {
197+
if (FILE_DB_TYPES.includes(ds.data_type)) continue;
198+
const lastSync = ds.connection_details?.api_connection_details?.api_config?.last_sync;
199+
if (!lastSync) {
200+
neverSynced++;
201+
} else {
202+
const hoursSinceSync = (Date.now() - new Date(lastSync as any).getTime()) / 3600000;
203+
if (hoursSinceSync > 72) failedSources++;
204+
}
205+
}
206+
201207
return {
202-
totalSources: r.total || 0,
203-
failedSources: r.failed || 0,
204-
neverSynced: r.never_synced || 0,
208+
totalSources: total,
209+
failedSources,
210+
neverSynced,
205211
};
206212
}
207213

208214
async getSyncHealthData(): Promise<IDataSourceSyncRow[]> {
209-
const manager = AppDataSource.manager;
210-
const rows = await manager.query(`
211-
SELECT
212-
ds.id,
213-
ds.name,
214-
ds.data_type,
215-
ds.created_at,
216-
u.email AS owner_email,
217-
ds.connection_details->'api_connection_details'->'api_config'->>'last_sync' AS last_sync
218-
FROM dra_data_sources ds
219-
LEFT JOIN dra_users_platform u ON ds.users_platform_id = u.id
220-
ORDER BY ds.id DESC
221-
LIMIT 200
222-
`);
215+
const FILE_DB_TYPES = ['postgresql', 'mysql', 'mariadb', 'mongodb', 'csv', 'excel', 'pdf'];
216+
217+
const dataSources = await AppDataSource.manager.find(DRADataSource, {
218+
relations: ['users_platform'],
219+
order: { id: 'DESC' },
220+
take: 200,
221+
});
222+
223+
return dataSources.map((ds) => {
224+
const isFileOrDb = FILE_DB_TYPES.includes(ds.data_type);
225+
const lastSyncRaw = ds.connection_details?.api_connection_details?.api_config?.last_sync;
226+
const lastSync = lastSyncRaw ? String(lastSyncRaw) : null;
223227

224-
return rows.map((r: any) => {
225-
const isFileOrDb = ['postgresql', 'mysql', 'mariadb', 'mongodb', 'csv', 'excel', 'pdf'].includes(r.data_type);
226228
let status: 'synced' | 'failed' | 'never' = 'synced';
227229
if (!isFileOrDb) {
228-
if (!r.last_sync || r.last_sync === 'null') {
230+
if (!lastSync || lastSync === 'null') {
229231
status = 'never';
230232
} else {
231-
const lastSyncDate = new Date(r.last_sync);
232-
const hoursSinceSync = (Date.now() - lastSyncDate.getTime()) / 3600000;
233+
const hoursSinceSync = (Date.now() - new Date(lastSync).getTime()) / 3600000;
233234
status = hoursSinceSync > 72 ? 'failed' : 'synced';
234235
}
235236
}
237+
236238
return {
237-
id: r.id,
238-
name: r.name,
239-
data_type: r.data_type,
240-
owner_email: r.owner_email || 'Unknown',
241-
last_sync: r.last_sync || null,
242-
created_at: r.created_at || null,
239+
id: ds.id,
240+
name: ds.name,
241+
data_type: ds.data_type,
242+
owner_email: (ds as any).users_platform?.email || 'Unknown',
243+
last_sync: lastSync,
244+
created_at: ds.created_at ? String(ds.created_at) : null,
243245
status,
244246
};
245247
});

frontend/components/AdminChart.vue

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ async function renderChart() {
3333
.attr('width', w)
3434
.attr('height', h);
3535
36+
cleanup = () => {
37+
svg.remove();
38+
};
39+
3640
const g = svg.append('g').attr('transform', `translate(${margin.left},${margin.top})`);
3741
3842
if (props.type === 'donut') {
@@ -138,10 +142,6 @@ async function renderChart() {
138142
.attr('r', 3)
139143
.attr('fill', color);
140144
}
141-
142-
cleanup = () => {
143-
svg.remove();
144-
};
145145
}
146146
147147
onMounted(() => {

frontend/composables/useAdminStats.ts

Lines changed: 6 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,11 @@ import type {
33
IAdminOverviewStats,
44
IDataSourceSyncRow,
55
ISystemHealthStatus,
6-
ITimeSeriesPoint,
76
} from '~/types/admin/stats';
87

98
export const useAdminStats = () => {
109
const config = useRuntimeConfig();
11-
const { $socket } = useNuxtApp() as any;
10+
const { $socketio } = useNuxtApp() as any;
1211

1312
const overviewStats = ref<IAdminOverviewStats | null>(null);
1413
const syncHealthData = ref<IDataSourceSyncRow[]>([]);
@@ -75,19 +74,19 @@ export const useAdminStats = () => {
7574
if (!import.meta.client) return;
7675
loadAll();
7776

78-
if ($socket) {
79-
$socket.emit('join-admin-room');
77+
if ($socketio) {
78+
$socketio.emit('join-admin-room');
8079
statsUpdateHandler = () => {
8180
refreshStats();
8281
};
83-
$socket.on('admin-stats-update', statsUpdateHandler);
82+
$socketio.on('admin-stats-update', statsUpdateHandler);
8483
}
8584
});
8685

8786
onUnmounted(() => {
8887
if (!import.meta.client) return;
89-
if ($socket && statsUpdateHandler) {
90-
$socket.off('admin-stats-update', statsUpdateHandler);
88+
if ($socketio && statsUpdateHandler) {
89+
$socketio.off('admin-stats-update', statsUpdateHandler);
9190
}
9291
});
9392

@@ -100,53 +99,3 @@ export const useAdminStats = () => {
10099
refreshStats,
101100
};
102101
};
103-
104-
export const useAdminTimeSeries = (metric: string, days = 30) => {
105-
const config = useRuntimeConfig();
106-
const data = ref<ITimeSeriesPoint[]>([]);
107-
const dsTypeBreakdown = ref<{ data_type: string; count: number }[]>([]);
108-
const isLoading = ref(false);
109-
110-
const authHeaders = (): Record<string, string> => {
111-
const token = getAuthToken();
112-
if (!token) throw new Error('Authentication required');
113-
return {
114-
Authorization: `Bearer ${token}`,
115-
'Authorization-Type': 'auth',
116-
};
117-
};
118-
119-
const fetchTimeSeries = async () => {
120-
const res = await $fetch<{ success: boolean; data: ITimeSeriesPoint[] }>(
121-
`${config.public.apiBase}/admin/stats/timeseries?metric=${metric}&days=${days}`,
122-
{ headers: authHeaders() }
123-
);
124-
if (res.success) data.value = res.data;
125-
};
126-
127-
const fetchDsTypeBreakdown = async () => {
128-
const res = await $fetch<{ success: boolean; data: { data_type: string; count: number }[] }>(
129-
`${config.public.apiBase}/admin/stats/datasource-types`,
130-
{ headers: authHeaders() }
131-
);
132-
if (res.success) dsTypeBreakdown.value = res.data;
133-
};
134-
135-
onMounted(async () => {
136-
if (!import.meta.client) return;
137-
isLoading.value = true;
138-
try {
139-
if (metric === 'datasource_types') {
140-
await fetchDsTypeBreakdown();
141-
} else {
142-
await fetchTimeSeries();
143-
}
144-
} catch (err) {
145-
console.error('[useAdminTimeSeries] Load failed:', err);
146-
} finally {
147-
isLoading.value = false;
148-
}
149-
});
150-
151-
return { data, dsTypeBreakdown, isLoading };
152-
};

frontend/pages/admin/index.vue

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,22 +34,48 @@ const authHeaders = () => {
3434
const fetchAllSeries = async () => {
3535
isSeriesLoading.value = true;
3636
try {
37-
const [signups, projects, ai, cancellations, dsTypes] = await Promise.all([
37+
const results = await Promise.allSettled([
3838
$fetch<any>(`${config.public.apiBase}/admin/stats/timeseries?metric=signups&days=30`, { headers: authHeaders() }),
3939
$fetch<any>(`${config.public.apiBase}/admin/stats/timeseries?metric=projects&days=30`, { headers: authHeaders() }),
4040
$fetch<any>(`${config.public.apiBase}/admin/stats/timeseries?metric=ai_messages&days=30`, { headers: authHeaders() }),
4141
$fetch<any>(`${config.public.apiBase}/admin/stats/timeseries?metric=cancellations&days=30`, { headers: authHeaders() }),
4242
$fetch<any>(`${config.public.apiBase}/admin/stats/datasource-types`, { headers: authHeaders() }),
4343
]);
44-
if (signups.success) signupSeries.value = signups.data;
45-
if (projects.success) projectSeries.value = projects.data;
46-
if (ai.success) aiSeries.value = ai.data;
47-
if (cancellations.success) cancellationSeries.value = cancellations.data;
48-
if (dsTypes.success) {
49-
dsTypeSeries.value = dsTypes.data.map((d: any) => ({ label: d.data_type, value: d.count }));
44+
45+
const [signupsResult, projectsResult, aiResult, cancellationsResult, dsTypesResult] = results;
46+
47+
if (signupsResult.status === 'fulfilled' && signupsResult.value.success) {
48+
signupSeries.value = signupsResult.value.data;
49+
} else if (signupsResult.status === 'rejected') {
50+
console.error('[AdminDashboard] Failed to load signups time-series:', signupsResult.reason);
51+
}
52+
53+
if (projectsResult.status === 'fulfilled' && projectsResult.value.success) {
54+
projectSeries.value = projectsResult.value.data;
55+
} else if (projectsResult.status === 'rejected') {
56+
console.error('[AdminDashboard] Failed to load projects time-series:', projectsResult.reason);
57+
}
58+
59+
if (aiResult.status === 'fulfilled' && aiResult.value.success) {
60+
aiSeries.value = aiResult.value.data;
61+
} else if (aiResult.status === 'rejected') {
62+
console.error('[AdminDashboard] Failed to load AI messages time-series:', aiResult.reason);
63+
}
64+
65+
if (cancellationsResult.status === 'fulfilled' && cancellationsResult.value.success) {
66+
cancellationSeries.value = cancellationsResult.value.data;
67+
} else if (cancellationsResult.status === 'rejected') {
68+
console.error('[AdminDashboard] Failed to load cancellations time-series:', cancellationsResult.reason);
69+
}
70+
71+
if (dsTypesResult.status === 'fulfilled' && dsTypesResult.value.success) {
72+
dsTypeSeries.value = dsTypesResult.value.data.map((d: any) => ({
73+
label: d.data_type,
74+
value: d.count,
75+
}));
76+
} else if (dsTypesResult.status === 'rejected') {
77+
console.error('[AdminDashboard] Failed to load data source type stats:', dsTypesResult.reason);
5078
}
51-
} catch (err) {
52-
console.error('[AdminDashboard] Failed to load time-series:', err);
5379
} finally {
5480
isSeriesLoading.value = false;
5581
}

0 commit comments

Comments
 (0)