@@ -8,6 +8,35 @@ import type { CommandFlags } from '../state/redis-hot-state.ts';
88import type { Config } from '../config/schema.ts' ;
99import type { CollectionOrchestrator , OrchestratorProgress } from '../runtime/collection-orchestrator.ts' ;
1010
11+ // ─────────────────────────────────────────────────────────────────────────────
12+ // Formatting helpers
13+ // ─────────────────────────────────────────────────────────────────────────────
14+
15+ function progressBar ( pct : number , width = 30 ) : string {
16+ const filled = Math . round ( ( pct / 100 ) * width ) ;
17+ const arrow = filled < width ? '>' : '' ;
18+ const empty = Math . max ( 0 , width - filled - ( arrow ? 1 : 0 ) ) ;
19+ return '[' + '=' . repeat ( filled ) + arrow + ' ' . repeat ( empty ) + '] ' + pct + '%' ;
20+ }
21+
22+ function fmtDuration ( ms : number ) : string {
23+ const s = Math . floor ( ms / 1000 ) ;
24+ const h = Math . floor ( s / 3600 ) ;
25+ const m = Math . floor ( ( s % 3600 ) / 60 ) ;
26+ const sec = s % 60 ;
27+ if ( h > 0 ) return `${ h } h ${ m } m ${ sec } s` ;
28+ if ( m > 0 ) return `${ m } m ${ sec } s` ;
29+ return `${ sec } s` ;
30+ }
31+
32+ function fmtNum ( n : number ) : string {
33+ return n . toLocaleString ( 'en-US' ) ;
34+ }
35+
36+ // ─────────────────────────────────────────────────────────────────────────────
37+ // Route
38+ // ─────────────────────────────────────────────────────────────────────────────
39+
1140export interface StatsDeps {
1241 orchestrator : CollectionOrchestrator ;
1342 mongoReader : { isConnected ( ) : boolean } ;
@@ -46,6 +75,7 @@ export function registerStatsRoute(app: FastifyInstance, deps: StatsDeps): void
4675 const batchStats = orchestrator . getStats ( ) ;
4776 const runnerStatus = orchestrator . getStatus ( ) ;
4877 const currentBatchSeq = orchestrator . getCurrentBatchSeq ( ) ;
78+ const estimatedCounts = orchestrator . getEstimatedCounts ( ) ;
4979
5080 // Find the current run ID from the orchestrator's progress
5181 const currentResult = progress . results . find (
@@ -78,6 +108,27 @@ export function registerStatsRoute(app: FastifyInstance, deps: StatsDeps): void
78108 const elapsedMs = batchStats ?. elapsedMs ?? 0 ;
79109 const elapsedSec = elapsedMs / 1000 ;
80110
111+ // ── Progress calculations ───────────────────────────────────────────
112+ const currentCollEstimate = progress . currentCollection
113+ ? estimatedCounts . get ( progress . currentCollection ) ?? 0 : 0 ;
114+ const currentCollDocsRead = totalDocsRead ;
115+ const currentCollPct = currentCollEstimate > 0
116+ ? Math . min ( 100 , Math . round ( ( currentCollDocsRead / currentCollEstimate ) * 100 ) ) : 0 ;
117+
118+ // Overall: completed collections use actual docsRead, fallback to estimate
119+ const totalEstimated = Array . from ( estimatedCounts . values ( ) ) . reduce ( ( a , b ) => a + b , 0 ) ;
120+ const completedDocsRead = progress . results
121+ . filter ( r => r . status === 'completed' || r . status === 'skipped' )
122+ . reduce ( ( sum , r ) => sum + ( r . docsRead ?? estimatedCounts . get ( r . collection ) ?? 0 ) , 0 ) ;
123+ const overallDocsRead = completedDocsRead + currentCollDocsRead ;
124+ const overallPct = totalEstimated > 0
125+ ? Math . min ( 100 , Math . round ( ( overallDocsRead / totalEstimated ) * 100 ) ) : 0 ;
126+
127+ // ETA based on elapsed time and progress percentage
128+ const etaMs = overallPct > 0 && overallPct < 100 && elapsedMs > 0
129+ ? Math . round ( ( ( Date . now ( ) - startedAt . getTime ( ) ) / overallPct ) * ( 100 - overallPct ) )
130+ : null ;
131+
81132 // Commands from Redis (best-effort)
82133 let commands : CommandFlags = { } ;
83134 if ( runId ) {
@@ -91,6 +142,37 @@ export function registerStatsRoute(app: FastifyInstance, deps: StatsDeps): void
91142 const redisMetrics = redisState . getMetrics ( ) ;
92143
93144 const payload = {
145+ // ── Quick-glance summary ──────────────────────────────────────────
146+ summary : {
147+ overall : progressBar ( overallPct ) ,
148+ overallPct,
149+ currentCollection : progress . currentCollection
150+ ? `${ progress . currentCollection } ${ progressBar ( currentCollPct ) } `
151+ : 'idle' ,
152+ currentCollectionPct : currentCollPct ,
153+ collections : `${ progress . completedCollections } /${ progress . totalCollections } done`
154+ + ( progress . failedCollections > 0 ? `, ${ progress . failedCollections } failed` : '' )
155+ + ( progress . skippedCollections > 0 ? `, ${ progress . skippedCollections } skipped` : '' ) ,
156+ docsProgress : `${ fmtNum ( overallDocsRead ) } / ~${ fmtNum ( totalEstimated ) } docs` ,
157+ throughput : `${ fmtNum ( Math . round ( batchStats ?. docsPerSecond ?? 0 ) ) } docs/s` ,
158+ elapsed : fmtDuration ( Date . now ( ) - startedAt . getTime ( ) ) ,
159+ eta : etaMs !== null ? `~${ fmtDuration ( etaMs ) } ` : 'calculating...' ,
160+ status : runnerStatus ,
161+ } ,
162+
163+ // ── Current collection detail ─────────────────────────────────────
164+ currentCollectionProgress : progress . currentCollection ? {
165+ collection : progress . currentCollection ,
166+ estimated : currentCollEstimate ,
167+ docsRead : currentCollDocsRead ,
168+ rowsInserted : totalRowsInserted ,
169+ pct : currentCollPct ,
170+ bar : progressBar ( currentCollPct ) ,
171+ batchSeq : currentBatchSeq ,
172+ skipRate : currentCollDocsRead > 0
173+ ? `${ ( ( totalDocsSkipped / currentCollDocsRead ) * 100 ) . toFixed ( 1 ) } %` : '0%' ,
174+ } : null ,
175+
94176 service : {
95177 name : config . service . name ,
96178 version,
@@ -107,6 +189,13 @@ export function registerStatsRoute(app: FastifyInstance, deps: StatsDeps): void
107189 skippedCollections : progress . skippedCollections ,
108190 currentCollection : progress . currentCollection ,
109191 collections : progress . collections ,
192+ collectionProgress : progress . results . map ( r => ( {
193+ collection : r . collection ,
194+ status : r . status ,
195+ estimated : estimatedCounts . get ( r . collection ) ?? null ,
196+ docsRead : r . docsRead ?? null ,
197+ rowsInserted : r . rowsInserted ?? null ,
198+ } ) ) ,
110199 } ,
111200 run : {
112201 sourceNs : runRecord ?. source_ns ?? null ,
0 commit comments