@@ -2,6 +2,63 @@ import { Express, Response } from "express";
22import { IAdminForth , IAdminUserExpressRequest } from "adminforth" ;
33import * as z from "zod" ;
44
5+ const DASHBOARD_CAR_SOURCES = [
6+ { resourceId : 'cars_sl' , label : 'SQLite' } ,
7+ { resourceId : 'cars_mysql' , label : 'MySQL' } ,
8+ { resourceId : 'cars_pg' , label : 'PostgreSQL' } ,
9+ { resourceId : 'cars_mongo' , label : 'MongoDB' } ,
10+ { resourceId : 'cars_ch' , label : 'ClickHouse' } ,
11+ ] as const ;
12+
13+ type DashboardCarRecord = {
14+ model : string ;
15+ price : string | number ;
16+ listed : boolean ;
17+ mileage : string | number | null ;
18+ production_year : number ;
19+ engine_type : string ;
20+ body_type : string ;
21+ } ;
22+
23+ function toNumber ( value : string | number | null ) : number {
24+ if ( typeof value === 'number' ) {
25+ return value ;
26+ }
27+ if ( typeof value === 'string' ) {
28+ return Number ( value ) ;
29+ }
30+ return 0 ;
31+ }
32+
33+ function roundMetric ( value : number ) : number {
34+ return Number ( value . toFixed ( 0 ) ) ;
35+ }
36+
37+ function getBreakdown ( records : DashboardCarRecord [ ] , fieldName : 'engine_type' | 'body_type' ) {
38+ const counts : Record < string , number > = { } ;
39+
40+ records . forEach ( ( record ) => {
41+ counts [ record [ fieldName ] ] = ( counts [ record [ fieldName ] ] ?? 0 ) + 1 ;
42+ } ) ;
43+
44+ return Object . entries ( counts )
45+ . map ( ( [ label , amount ] ) => ( { label, amount } ) )
46+ . sort ( ( left , right ) => right . amount - left . amount || left . label . localeCompare ( right . label ) ) ;
47+ }
48+
49+ function getTopModels ( records : DashboardCarRecord [ ] ) {
50+ const counts : Record < string , number > = { } ;
51+
52+ records . forEach ( ( record ) => {
53+ counts [ record . model ] = ( counts [ record . model ] ?? 0 ) + 1 ;
54+ } ) ;
55+
56+ return Object . entries ( counts )
57+ . map ( ( [ x , count ] ) => ( { x, count } ) )
58+ . sort ( ( left , right ) => right . count - left . count || left . x . localeCompare ( right . x ) )
59+ . slice ( 0 , 6 ) ;
60+ }
61+
562export function initApi ( app : Express , admin : IAdminForth ) {
663 app . get ( `${ admin . config . baseUrl } /api/hello/` ,
764 admin . express . withSchema (
@@ -19,4 +76,100 @@ export function initApi(app: Express, admin: IAdminForth) {
1976 )
2077 )
2178 ) ;
79+
80+ app . get ( `${ admin . config . baseUrl } /api/dashboard/` ,
81+ admin . express . withSchema (
82+ {
83+ description : 'Returns aggregated car metrics used by the dev demo dashboard custom page.' ,
84+ response : z . object ( {
85+ summary : z . object ( {
86+ totalCars : z . number ( ) ,
87+ listedCars : z . number ( ) ,
88+ unlistedCars : z . number ( ) ,
89+ averagePrice : z . number ( ) ,
90+ averageMileage : z . number ( ) ,
91+ } ) ,
92+ operations : z . object ( {
93+ adminUsers : z . number ( ) ,
94+ sessions : z . number ( ) ,
95+ backgroundJobs : z . number ( ) ,
96+ } ) ,
97+ sourceTotals : z . array (
98+ z . object ( {
99+ source : z . string ( ) ,
100+ count : z . number ( ) ,
101+ listed : z . number ( ) ,
102+ avgPrice : z . number ( ) ,
103+ } )
104+ ) ,
105+ engineTypeBreakdown : z . array (
106+ z . object ( {
107+ label : z . string ( ) ,
108+ amount : z . number ( ) ,
109+ } )
110+ ) ,
111+ bodyTypeBreakdown : z . array (
112+ z . object ( {
113+ label : z . string ( ) ,
114+ amount : z . number ( ) ,
115+ } )
116+ ) ,
117+ topModels : z . array (
118+ z . object ( {
119+ x : z . string ( ) ,
120+ count : z . number ( ) ,
121+ } )
122+ ) ,
123+ } ) ,
124+ } ,
125+ admin . express . authorize (
126+ async ( _req : IAdminUserExpressRequest , res : Response ) => {
127+ const [ carsBySource , adminUsers , sessions , backgroundJobs ] = await Promise . all ( [
128+ Promise . all (
129+ DASHBOARD_CAR_SOURCES . map ( async ( { resourceId, label } ) => {
130+ const records = await admin . resource ( resourceId ) . list ( [ ] ) as DashboardCarRecord [ ] ;
131+
132+ return { label, records } ;
133+ } )
134+ ) ,
135+ admin . resource ( 'adminuser' ) . count ( ) ,
136+ admin . resource ( 'sessions' ) . count ( ) ,
137+ admin . resource ( 'jobs' ) . count ( ) ,
138+ ] ) ;
139+
140+ const allCars = carsBySource . flatMap ( ( { records } ) => records ) ;
141+ const totalCars = allCars . length ;
142+ const listedCars = allCars . filter ( ( record ) => record . listed ) . length ;
143+ const totalPrice = allCars . reduce ( ( sum , record ) => sum + toNumber ( record . price ) , 0 ) ;
144+ const totalMileage = allCars . reduce ( ( sum , record ) => sum + toNumber ( record . mileage ) , 0 ) ;
145+
146+ res . json ( {
147+ summary : {
148+ totalCars,
149+ listedCars,
150+ unlistedCars : totalCars - listedCars ,
151+ averagePrice : totalCars > 0 ? roundMetric ( totalPrice / totalCars ) : 0 ,
152+ averageMileage : totalCars > 0 ? roundMetric ( totalMileage / totalCars ) : 0 ,
153+ } ,
154+ operations : {
155+ adminUsers,
156+ sessions,
157+ backgroundJobs,
158+ } ,
159+ sourceTotals : carsBySource . map ( ( { label, records } ) => ( {
160+ source : label ,
161+ count : records . length ,
162+ listed : records . filter ( ( record ) => record . listed ) . length ,
163+ avgPrice : records . length > 0
164+ ? roundMetric ( records . reduce ( ( sum , record ) => sum + toNumber ( record . price ) , 0 ) / records . length )
165+ : 0 ,
166+ } ) ) ,
167+ engineTypeBreakdown : getBreakdown ( allCars , 'engine_type' ) ,
168+ bodyTypeBreakdown : getBreakdown ( allCars , 'body_type' ) ,
169+ topModels : getTopModels ( allCars ) ,
170+ } ) ;
171+ }
172+ )
173+ )
174+ ) ;
22175}
0 commit comments