From 1e41798a06cfcf73c2796e176d5306ec96a58696 Mon Sep 17 00:00:00 2001 From: Riyadh Date: Tue, 21 Apr 2026 06:11:28 +0000 Subject: [PATCH] Show top apps and most-active users on the admin dashboard (Task #21) Added two leaderboard panels to the admin dashboard that surface which apps are most popular and which users drive the most activity in the selected time range. Backend (artifacts/api-server/src/routes/stats.ts): - Extended GET /api/stats/admin to also return: - topApps: top 5 apps by app_opens count, with id, slug, names, iconName, color, count - mostActiveUsers: top 5 users by app_opens count, with id, username, displayNames, avatarUrl, count - Both lists honor the existing `range` query param (7d/30d/90d) so they stay in sync with the trend charts. Task wording said "last 7 days" because 7d is the default; using the selected range is a small intentional improvement that matches the rest of the dashboard. API spec (lib/api-spec/openapi.yaml): - Added TopAppItem and TopUserItem schemas. - Added topApps and mostActiveUsers to AdminStats and made them required. Regenerated api-client-react and api-zod via codegen. Frontend (artifacts/teaboy-os/src/pages/admin.tsx): - Added two new panels to DashboardSection rendered in a 2-column grid between the trend charts and the recent activity card. - Each row shows rank, color/initial, name (i18n), count, and a proportional progress bar. Empty state when no activity yet. i18n (artifacts/teaboy-os/src/locales/{ar,en}.json): - Added admin.dashboard.topApps, mostActiveUsers, *Subtitle, leaderboardEmpty, openCount keys. Verified with end-to-end test: admin login, dashboard renders both panels with seeded data, range switch updates subtitles to "Last 30 days". --- artifacts/api-server/src/routes/stats.ts | 67 ++++++++++ artifacts/teaboy-os/public/opengraph.jpg | Bin 25906 -> 26029 bytes artifacts/teaboy-os/src/locales/ar.json | 8 +- artifacts/teaboy-os/src/locales/en.json | 8 +- artifacts/teaboy-os/src/pages/admin.tsx | 119 ++++++++++++++++++ .../src/generated/api.schemas.ts | 24 ++++ lib/api-spec/openapi.yaml | 56 +++++++++ lib/api-zod/src/generated/api.ts | 21 ++++ 8 files changed, 301 insertions(+), 2 deletions(-) diff --git a/artifacts/api-server/src/routes/stats.ts b/artifacts/api-server/src/routes/stats.ts index 32f75beb..8b7feaf1 100644 --- a/artifacts/api-server/src/routes/stats.ts +++ b/artifacts/api-server/src/routes/stats.ts @@ -215,6 +215,71 @@ router.get("/stats/admin", requireAuth, requireAdmin, async (req, res): Promise< .from(servicesTable) .where(sql`${servicesTable.createdAt} >= ${rangeStart.toISOString()}`); + const topAppsRows = await db + .select({ + appId: appsTable.id, + slug: appsTable.slug, + nameAr: appsTable.nameAr, + nameEn: appsTable.nameEn, + iconName: appsTable.iconName, + color: appsTable.color, + count: sql`count(${appOpensTable.id})`, + }) + .from(appOpensTable) + .innerJoin(appsTable, eq(appOpensTable.appId, appsTable.id)) + .where(sql`${appOpensTable.createdAt} >= ${rangeStart.toISOString()}`) + .groupBy( + appsTable.id, + appsTable.slug, + appsTable.nameAr, + appsTable.nameEn, + appsTable.iconName, + appsTable.color, + ) + .orderBy(sql`count(${appOpensTable.id}) desc`) + .limit(5); + + const topApps = topAppsRows.map((r) => ({ + appId: r.appId, + slug: r.slug, + nameAr: r.nameAr, + nameEn: r.nameEn, + iconName: r.iconName, + color: r.color, + count: Number(r.count), + })); + + const mostActiveUsersRows = await db + .select({ + userId: usersTable.id, + username: usersTable.username, + displayNameAr: usersTable.displayNameAr, + displayNameEn: usersTable.displayNameEn, + avatarUrl: usersTable.avatarUrl, + count: sql`count(${appOpensTable.id})`, + }) + .from(appOpensTable) + .innerJoin(usersTable, eq(appOpensTable.userId, usersTable.id)) + .where(sql`${appOpensTable.createdAt} >= ${rangeStart.toISOString()}`) + .groupBy( + usersTable.id, + usersTable.username, + usersTable.displayNameAr, + usersTable.displayNameEn, + usersTable.avatarUrl, + ) + .orderBy(sql`count(${appOpensTable.id}) desc`) + .limit(5); + + const mostActiveUsers = mostActiveUsersRows.map((r) => ({ + userId: r.userId, + username: r.username, + displayNameAr: r.displayNameAr, + displayNameEn: r.displayNameEn, + avatarUrl: r.avatarUrl, + count: Number(r.count), + })); + res.json({ range, rangeDays, @@ -228,6 +293,8 @@ router.get("/stats/admin", requireAuth, requireAdmin, async (req, res): Promise< appOpensPrevRange: Number(appOpensPrevRangeRow?.count ?? 0), servicesCreatedByDay, servicesCreatedInRange: Number(servicesCreatedInRangeRow?.count ?? 0), + topApps, + mostActiveUsers, }); }); diff --git a/artifacts/teaboy-os/public/opengraph.jpg b/artifacts/teaboy-os/public/opengraph.jpg index 70c131e1a6161e00094d46744458c533d5f8edfa..f07cb0984336c385697b23265bd82a3cf9bd4f15 100644 GIT binary patch literal 26029 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 s.count)); const servicesCreatedTotal = adminStats?.servicesCreatedInRange ?? 0; + const topApps = adminStats?.topApps ?? []; + const maxTopAppCount = Math.max(1, ...topApps.map((a) => a.count)); + const mostActiveUsers = adminStats?.mostActiveUsers ?? []; + const maxActiveUserCount = Math.max(1, ...mostActiveUsers.map((u) => u.count)); const rangeDays = adminStats?.rangeDays ?? (statsRange === "90d" ? 90 : statsRange === "30d" ? 30 : 7); const rangeLabel = t(`admin.dashboard.range.${statsRange}`); const prevRangeLabel = t(`admin.dashboard.prevRange.${statsRange}`); @@ -1102,6 +1106,121 @@ function DashboardSection({ )} +
+
+
+
+ {t("admin.dashboard.topApps")} +
+
+ {t("admin.dashboard.topAppsSubtitle", { range: rangeLabel })} +
+
+ {topApps.length === 0 ? ( +
+ {t("admin.dashboard.leaderboardEmpty")} +
+ ) : ( +
    + {topApps.map((app, idx) => { + const widthPct = (app.count / maxTopAppCount) * 100; + return ( +
  1. +
    + {idx + 1} +
    +
    +
    +
    +
    + {lang === "ar" ? app.nameAr : app.nameEn} +
    +
    + {app.count} +
    +
    +
    +
    +
    +
    +
  2. + ); + })} +
+ )} +
+ +
+
+
+ {t("admin.dashboard.mostActiveUsers")} +
+
+ {t("admin.dashboard.mostActiveUsersSubtitle", { range: rangeLabel })} +
+
+ {mostActiveUsers.length === 0 ? ( +
+ {t("admin.dashboard.leaderboardEmpty")} +
+ ) : ( +
    + {mostActiveUsers.map((u, idx) => { + const widthPct = (u.count / maxActiveUserCount) * 100; + const displayName = + (lang === "ar" ? u.displayNameAr : u.displayNameEn) ?? u.username; + const initial = (displayName || u.username || "?").trim().charAt(0).toUpperCase(); + return ( +
  1. +
    + {idx + 1} +
    +
    + {initial} +
    +
    +
    +
    +
    + {displayName} +
    + {displayName !== u.username && ( +
    + @{u.username} +
    + )} +
    +
    + {u.count} +
    +
    +
    +
    +
    +
    +
  2. + ); + })} +
+ )} +
+
+
{t("admin.dashboard.recentActivity")} diff --git a/lib/api-client-react/src/generated/api.schemas.ts b/lib/api-client-react/src/generated/api.schemas.ts index e6d7d570..575b08f2 100644 --- a/lib/api-client-react/src/generated/api.schemas.ts +++ b/lib/api-client-react/src/generated/api.schemas.ts @@ -390,6 +390,28 @@ export type AdminStatsServicesCreatedByDayItem = { count: number; }; +export interface TopAppItem { + appId: number; + slug: string; + nameAr: string; + nameEn: string; + iconName: string; + color: string; + count: number; +} + +export interface TopUserItem { + userId: number; + username: string; + /** @nullable */ + displayNameAr?: string | null; + /** @nullable */ + displayNameEn?: string | null; + /** @nullable */ + avatarUrl?: string | null; + count: number; +} + export interface AdminStats { range: AdminStatsRange; rangeDays: number; @@ -403,6 +425,8 @@ export interface AdminStats { appOpensPrevRange: number; servicesCreatedByDay: AdminStatsServicesCreatedByDayItem[]; servicesCreatedInRange: number; + topApps: TopAppItem[]; + mostActiveUsers: TopUserItem[]; } export type GetAdminStatsParams = { diff --git a/lib/api-spec/openapi.yaml b/lib/api-spec/openapi.yaml index 1ed961bd..278ddd06 100644 --- a/lib/api-spec/openapi.yaml +++ b/lib/api-spec/openapi.yaml @@ -1607,6 +1607,14 @@ components: - count servicesCreatedInRange: type: integer + topApps: + type: array + items: + $ref: "#/components/schemas/TopAppItem" + mostActiveUsers: + type: array + items: + $ref: "#/components/schemas/TopUserItem" required: - range - rangeDays @@ -1620,3 +1628,51 @@ components: - appOpensPrevRange - servicesCreatedByDay - servicesCreatedInRange + - topApps + - mostActiveUsers + + TopAppItem: + type: object + properties: + appId: + type: integer + slug: + type: string + nameAr: + type: string + nameEn: + type: string + iconName: + type: string + color: + type: string + count: + type: integer + required: + - appId + - slug + - nameAr + - nameEn + - iconName + - color + - count + + TopUserItem: + type: object + properties: + userId: + type: integer + username: + type: string + displayNameAr: + type: ["string", "null"] + displayNameEn: + type: ["string", "null"] + avatarUrl: + type: ["string", "null"] + count: + type: integer + required: + - userId + - username + - count diff --git a/lib/api-zod/src/generated/api.ts b/lib/api-zod/src/generated/api.ts index b1c4c34f..8f284dab 100644 --- a/lib/api-zod/src/generated/api.ts +++ b/lib/api-zod/src/generated/api.ts @@ -885,4 +885,25 @@ export const GetAdminStatsResponse = zod.object({ }), ), servicesCreatedInRange: zod.number(), + topApps: zod.array( + zod.object({ + appId: zod.number(), + slug: zod.string(), + nameAr: zod.string(), + nameEn: zod.string(), + iconName: zod.string(), + color: zod.string(), + count: zod.number(), + }), + ), + mostActiveUsers: zod.array( + zod.object({ + userId: zod.number(), + username: zod.string(), + displayNameAr: zod.string().nullish(), + displayNameEn: zod.string().nullish(), + avatarUrl: zod.string().nullish(), + count: zod.number(), + }), + ), });