@@ -28,6 +28,9 @@ import config from "../../config";
2828const logger = createLogger ( "admin" ) ;
2929
3030let errorLogClient : RedisClientType | null = null ;
31+
32+ let errorStatsCache : { data : unknown ; ts : number } | null = null ;
33+ const ERROR_STATS_CACHE_TTL = 30_000 ;
3134async function getErrorLogClient ( ) : Promise < RedisClientType | null > {
3235 if ( errorLogClient && errorLogClient . isOpen ) return errorLogClient ;
3336 try {
@@ -69,6 +72,15 @@ router.use(
6972
7073router . use ( "/tokens" , adminTokensRouter ) ;
7174
75+ function dashboardCache (
76+ _req : express . Request ,
77+ res : express . Response ,
78+ next : express . NextFunction
79+ ) {
80+ res . set ( "Cache-Control" , "private, max-age=10, must-revalidate" ) ;
81+ next ( ) ;
82+ }
83+
7284const QUEUE_STATES = [
7385 "waiting" ,
7486 "active" ,
@@ -256,7 +268,16 @@ router.post("/queues/pause-all", async (_req, res) => {
256268 }
257269} ) ;
258270
271+ const queueStatsCache = new Map <
272+ string ,
273+ { data : Record < string , unknown > ; ts : number }
274+ > ( ) ;
275+ const QUEUE_STATS_CACHE_TTL = 15_000 ;
276+
259277async function queueStats ( queueKey : string , queue : Queue ) {
278+ const cached = queueStatsCache . get ( queueKey ) ;
279+ if ( cached && Date . now ( ) - cached . ts < QUEUE_STATS_CACHE_TTL ) return cached . data ;
280+
260281 const [ counts , workers , paused , metrics24h ] =
261282 await Promise . all ( [
262283 queue . getJobCounts ( ...QUEUE_STATES ) ,
@@ -275,14 +296,16 @@ async function queueStats(queueKey: string, queue: Queue) {
275296 failed24h += p . failed ;
276297 }
277298
278- return {
299+ const data = {
279300 counts,
280301 paused,
281302 workers : workerCount ,
282303 concurrency,
283304 completed24h,
284305 failed24h,
285306 } ;
307+ queueStatsCache . set ( queueKey , { data, ts : Date . now ( ) } ) ;
308+ return data ;
286309}
287310
288311const RANGE_MINUTES : Record < string , number > = {
@@ -292,7 +315,7 @@ const RANGE_MINUTES: Record<string, number> = {
292315 "7d" : 10080 ,
293316} ;
294317
295- router . get ( "/queues/metrics" , async ( req , res ) => {
318+ router . get ( "/queues/metrics" , dashboardCache , async ( req , res ) => {
296319 const queueName = String ( req . query . queue || "download" ) ;
297320 if ( ! pickQueue ( queueName ) ) return res . status ( 404 ) . json ( { error : "queue_not_found" } ) ;
298321 const range = String ( req . query . range || "1h" ) ;
@@ -305,63 +328,70 @@ router.get("/queues/metrics", async (req, res) => {
305328 }
306329} ) ;
307330
308- router . get ( "/queues" , async ( req , res ) => {
331+ const queuesCache = new Map < string , { data : unknown ; ts : number } > ( ) ;
332+ const QUEUES_CACHE_TTL = 10_000 ;
333+
334+ router . get ( "/queues" , dashboardCache , async ( req , res ) => {
309335 const search = req . query . search ? String ( req . query . search ) . toLowerCase ( ) : "" ;
310336 const queueName = req . query . queue ? String ( req . query . queue ) : "" ;
337+ const cacheKey = `${ queueName } |${ search } ` ;
338+ const cached = queuesCache . get ( cacheKey ) ;
339+ if ( cached && Date . now ( ) - cached . ts < QUEUES_CACHE_TTL ) {
340+ return res . json ( cached . data ) ;
341+ }
311342
312343 const allQueues : { key : string ; label : string ; queue : Queue } [ ] = [
313344 { key : "download" , label : "Download" , queue : downloadQueue } ,
314345 { key : "remove" , label : "Remove" , queue : removeQueue } ,
315346 { key : "cache" , label : "Cache cleanup" , queue : cacheQueue } ,
316347 ] ;
317348
318- const statsResults = await Promise . all (
319- allQueues . map ( async ( q ) => ( {
320- key : q . key ,
321- label : q . label ,
322- ...( await queueStats ( q . key , q . queue ) ) ,
323- } ) )
324- ) ;
325-
326349 const target = queueName
327350 ? allQueues . find ( ( q ) => q . key === queueName )
328351 : allQueues [ 0 ] ;
329352 const targetQueue = target ? target . queue : downloadQueue ;
330353
331- const matches = ( job : { id ?: string | undefined ; name ?: string } ) => {
332- if ( ! search ) return true ;
333- return (
334- ( job . id || "" ) . toLowerCase ( ) . includes ( search ) ||
335- ( job . name || "" ) . toLowerCase ( ) . includes ( search )
336- ) ;
337- } ;
338-
339- // Fetch all states in parallel, tag each job with its state
340- const jobsByState = await Promise . all (
341- QUEUE_STATES . map ( async ( state ) => {
342- const jobs = await targetQueue . getJobs ( [ state ] ) ;
354+ const [ statsResults , ...jobsByState ] = await Promise . all ( [
355+ Promise . all (
356+ allQueues . map ( async ( q ) => ( {
357+ key : q . key ,
358+ label : q . label ,
359+ ...( await queueStats ( q . key , q . queue ) ) ,
360+ } ) )
361+ ) ,
362+ ...QUEUE_STATES . map ( async ( state ) => {
363+ const jobs = await targetQueue . getJobs ( [ state ] , 0 , 199 ) ;
343364 return jobs . map ( ( j ) => {
344365 const json : Record < string , unknown > = { ...j . asJSON ( ) , _state : state } ;
345366 if ( state === "delayed" && j . delay > 0 ) {
346367 json . delayUntil = j . timestamp + j . delay ;
347368 }
348369 return json ;
349370 } ) ;
350- } )
351- ) ;
352- const allJobs = jobsByState . flat ( ) . filter ( matches ) ;
371+ } ) ,
372+ ] ) ;
373+
374+ const matches = ( job : { id ?: string | undefined ; name ?: string } ) => {
375+ if ( ! search ) return true ;
376+ return (
377+ ( job . id || "" ) . toLowerCase ( ) . includes ( search ) ||
378+ ( job . name || "" ) . toLowerCase ( ) . includes ( search )
379+ ) ;
380+ } ;
381+ const allJobs = ( jobsByState as Record < string , unknown > [ ] [ ] ) . flat ( ) . filter ( matches ) ;
353382
354- // Sort: active first, then waiting, delayed, failed, completed
355383 const stateOrder : Record < string , number > = {
356384 active : 0 , waiting : 1 , delayed : 2 , failed : 3 , completed : 4 ,
357385 } ;
358386 allJobs . sort ( ( a , b ) => ( stateOrder [ a . _state as string ] ?? 9 ) - ( stateOrder [ b . _state as string ] ?? 9 ) ) ;
359387
360- res . json ( {
388+ const data = {
361389 queues : statsResults ,
362390 selectedQueue : target ?. key || "download" ,
363391 jobs : allJobs ,
364- } ) ;
392+ } ;
393+ queuesCache . set ( cacheKey , { data, ts : Date . now ( ) } ) ;
394+ res . json ( data ) ;
365395} ) ;
366396
367397// Errors captured by the logger sink. Server-paginated to avoid pulling
@@ -412,22 +442,29 @@ router.get("/errors", async (req, res) => {
412442
413443// Aggregated stats from the precomputed hourly counters (HINCRBY on each
414444// persistError). No JSON parsing of stored entries — O(48 small HGETALLs).
415- router . get ( "/errors/stats" , async ( req , res ) => {
445+ router . get ( "/errors/stats" , dashboardCache , async ( req , res ) => {
416446 try {
447+ if (
448+ errorStatsCache &&
449+ Date . now ( ) - errorStatsCache . ts < ERROR_STATS_CACHE_TTL
450+ ) {
451+ return res . json ( errorStatsCache . data ) ;
452+ }
453+
417454 const client = await getErrorLogClient ( ) ;
418455 if ( ! client ) {
419- return res . json ( {
456+ const data = {
420457 available : false ,
421458 last24h : 0 ,
422459 prev24h : 0 ,
423460 severity : { error : 0 , warn : 0 , info : 0 } ,
424461 unique : { error : 0 , warn : 0 , info : 0 } ,
425462 buckets : [ ] ,
426463 dropped : getInProcessDropped ( ) ,
427- } ) ;
464+ } ;
465+ return res . json ( data ) ;
428466 }
429467 const now = new Date ( ) ;
430- // Build the 48 hour keys to fetch (24 for current window + 24 for prev).
431468 function hourKey ( d : Date ) {
432469 const y = d . getUTCFullYear ( ) ;
433470 const m = String ( d . getUTCMonth ( ) + 1 ) . padStart ( 2 , "0" ) ;
@@ -440,8 +477,6 @@ router.get("/errors/stats", async (req, res) => {
440477 const bucketHourTs : number [ ] = [ ] ;
441478 for ( let i = 23 ; i >= 0 ; i -- ) {
442479 const d = new Date ( now . getTime ( ) - i * 3600 * 1000 ) ;
443- // Anchor each bar at the end of its hour so a "9s ago" event lands in
444- // the rightmost bar.
445480 const anchor = new Date (
446481 Date . UTC (
447482 d . getUTCFullYear ( ) ,
@@ -467,17 +502,17 @@ router.get("/errors/stats", async (req, res) => {
467502 }
468503 const pipe = client . multi ( ) ;
469504 for ( const k of currentKeys ) pipe . hGetAll ( k ) ;
470- for ( const k of prevKeys ) pipe . hGetAll ( k ) ;
505+ for ( const k of prevKeys ) pipe . hmGet ( k , [ "total" ] ) ;
471506 pipe . get ( ERROR_LOG_DROPPED_KEY ) ;
472507 const results = ( await pipe . exec ( ) ) as unknown [ ] ;
473508 const currentHashes = results . slice ( 0 , currentKeys . length ) as Record <
474509 string ,
475510 string
476511 > [ ] ;
477- const prevHashes = results . slice (
512+ const prevTotals = results . slice (
478513 currentKeys . length ,
479514 currentKeys . length + prevKeys . length
480- ) as Record < string , string > [ ] ;
515+ ) as ( string | null ) [ ] [ ] ;
481516 const droppedRedis =
482517 parseInt ( String ( results [ results . length - 1 ] || "0" ) , 10 ) || 0 ;
483518
@@ -504,7 +539,6 @@ router.get("/errors/stats", async (req, res) => {
504539 sev . warn += w ;
505540 sev . info += inf ;
506541 last24h += parseInt ( flat . total || "0" , 10 ) || 0 ;
507- // cb:<bucket>:<code> fields → unique code sets.
508542 for ( const k of Object . keys ( flat ) ) {
509543 if ( ! k . startsWith ( "cb:" ) ) continue ;
510544 const sep = k . indexOf ( ":" , 3 ) ;
@@ -515,11 +549,11 @@ router.get("/errors/stats", async (req, res) => {
515549 }
516550 } ) ;
517551 let prev24h = 0 ;
518- for ( const h of prevHashes ) {
519- prev24h += parseInt ( ( h || { } ) . total || "0" , 10 ) || 0 ;
552+ for ( const row of prevTotals ) {
553+ prev24h += parseInt ( ( row && row [ 0 ] ) || "0" , 10 ) || 0 ;
520554 }
521555
522- res . json ( {
556+ const data = {
523557 available : true ,
524558 last24h,
525559 prev24h,
@@ -531,7 +565,9 @@ router.get("/errors/stats", async (req, res) => {
531565 } ,
532566 buckets,
533567 dropped : droppedRedis + getInProcessDropped ( ) ,
534- } ) ;
568+ } ;
569+ errorStatsCache = { data, ts : Date . now ( ) } ;
570+ res . json ( data ) ;
535571 } catch ( error ) {
536572 handleError ( error , res , req ) ;
537573 }
@@ -559,6 +595,7 @@ router.delete("/errors", async (req, res) => {
559595 pipe . del ( ERROR_LOG_DROPPED_KEY ) ;
560596 if ( hourlyKeys . length ) pipe . del ( hourlyKeys ) ;
561597 await pipe . exec ( ) ;
598+ errorStatsCache = null ;
562599 res . json ( { ok : true , cleared : len , hourlyCleared : hourlyKeys . length } ) ;
563600 } catch ( error ) {
564601 handleError ( error , res , req ) ;
0 commit comments