11import { randomUUID } from 'crypto' ;
22
33import { getPostgresInstance } from '../../database/services/PostgresService.js' ;
4+ import { toError } from '../../utils/errors/index.js' ;
45import { createLogger } from '../../utils/logger.js' ;
56import redisClient from '../../utils/redis/client.js' ;
67
@@ -169,46 +170,80 @@ export async function refreshMonitor(): Promise<MonitorSnapshot> {
169170 // Store articles in normalized table + snapshot aggregates
170171 await Promise . all ( [ upsertArticles ( allArticles ) , saveSnapshotAggregates ( snapshot ) ] ) ;
171172
172- // Cache snapshot in Redis
173+ // Cache snapshot in Redis. Non-fatal: DB still has canonical data, but a
174+ // failure here means /monitor/latest pays the rebuild cost on every request.
173175 try {
174176 await redisClient . set ( REDIS_SNAPSHOT_KEY , JSON . stringify ( snapshot ) , { EX : REDIS_TTL_SECONDS } ) ;
175177 } catch ( error ) {
176- log . error ( `Failed to cache snapshot in Redis: ${ error } ` ) ;
178+ log . error ( `Failed to cache snapshot in Redis: ${ toError ( error ) . message } ` ) ;
177179 }
178180
179- // Warm all caches in background (fire-and-forget)
181+ const warmTasks : Array < { name : string ; run : ( ) => Promise < unknown > } > = [ ] ;
182+
180183 if ( keywords . length > 0 ) {
181184 for ( const locale of [ 'de' , 'at' ] as const ) {
182- generateKeywordInsights ( keywords , locale ) . catch ( ( err ) =>
183- log . warn ( `Background keyword-insights warm (${ locale } ) failed: ${ err } ` )
184- ) ;
185+ warmTasks . push ( {
186+ name : `keyword-insights:${ locale } ` ,
187+ run : ( ) => generateKeywordInsights ( keywords , locale ) ,
188+ } ) ;
185189 }
186190 }
187191
188192 for ( const locale of [ 'de' , 'at' ] as const ) {
189- getStimmung ( locale ) . catch ( ( err ) =>
190- log . warn ( `Background stimmung warm (${ locale } ) failed: ${ err } ` )
191- ) ;
193+ warmTasks . push ( { name : `stimmung:${ locale } ` , run : ( ) => getStimmung ( locale ) } ) ;
192194 }
193195
194- getPolls ( ) . catch ( ( err ) => log . warn ( `Background polls warm failed: ${ err } ` ) ) ;
196+ warmTasks . push ( { name : 'polls' , run : ( ) => getPolls ( ) } ) ;
195197
196198 for ( const locale of [ 'de' , 'at' ] as const ) {
197- Promise . all ( [ getLatestSnapshot ( locale ) , getStimmung ( locale ) , getPolls ( ) ] )
198- . then ( ( [ snap , stimmung , polls ] ) => {
199- if ( snap ) return generateMonitorBriefing ( locale , snap , stimmung , polls ?. average ?? { } ) ;
200- } )
201- . catch ( ( err ) => log . warn ( `Background briefing warm (${ locale } ) failed: ${ err } ` ) ) ;
199+ warmTasks . push ( {
200+ name : `briefing:${ locale } ` ,
201+ run : async ( ) => {
202+ const [ snap , stimmung , polls ] = await Promise . all ( [
203+ getLatestSnapshot ( locale ) ,
204+ getStimmung ( locale ) ,
205+ getPolls ( ) ,
206+ ] ) ;
207+ if ( ! snap ) throw new Error ( 'no snapshot available' ) ;
208+ return generateMonitorBriefing ( locale , snap , stimmung , polls ?. average ?? { } ) ;
209+ } ,
210+ } ) ;
202211 }
203212
204213 for ( const entity of WATCHER_ENTITIES ) {
205- searchArticlesByKeywords ( entity . keywords , entity . locale , 50 , entity . excludePatterns )
206- . then ( ( articles ) => getEntitySummary ( entity , articles , entity . locale ) )
207- . catch ( ( err ) =>
208- log . warn ( `Background entity summary warm (${ entity . id } /${ entity . locale } ) failed: ${ err } ` )
209- ) ;
214+ warmTasks . push ( {
215+ name : `entity-summary:${ entity . id } /${ entity . locale } ` ,
216+ run : async ( ) => {
217+ const articles = await searchArticlesByKeywords (
218+ entity . keywords ,
219+ entity . locale ,
220+ 50 ,
221+ entity . excludePatterns
222+ ) ;
223+ return getEntitySummary ( entity , articles , entity . locale ) ;
224+ } ,
225+ } ) ;
210226 }
211227
228+ // Run warmers in parallel, but don't block the refresh response. Aggregate
229+ // failures into a single structured ERROR so a single watch on logs catches
230+ // any background regression instead of 13 independent WARN lines.
231+ void Promise . allSettled ( warmTasks . map ( ( t ) => t . run ( ) ) ) . then ( ( results ) => {
232+ const failures = results
233+ . map ( ( r , i ) => ( r . status === 'rejected' ? { name : warmTasks [ i ] . name , reason : r . reason } : null ) )
234+ . filter ( ( x ) : x is { name : string ; reason : unknown } => x !== null ) ;
235+
236+ if ( failures . length === 0 ) {
237+ log . info ( `Background warm: ${ warmTasks . length } /${ warmTasks . length } succeeded` ) ;
238+ return ;
239+ }
240+
241+ const detail = failures . map ( ( f ) => `${ f . name } : ${ toError ( f . reason ) . message } ` ) . join ( '; ' ) ;
242+ log . error (
243+ `Background warm: ${ failures . length } /${ warmTasks . length } failed — ${ detail } `
244+ ) ;
245+ } ) ;
246+
212247 const durationMs = Date . now ( ) - startTime ;
213248 log . info (
214249 `Monitor refresh: ${ allArticles . length } total, ${ classifiedArticles . length } classified, ${ keywords . length } keywords (${ durationMs } ms)`
@@ -298,7 +333,8 @@ async function upsertArticles(articles: MonitorArticle[]): Promise<void> {
298333
299334 log . info ( `Upserted ${ articles . length } articles into monitor_articles` ) ;
300335 } catch ( error ) {
301- log . error ( `Failed to upsert articles: ${ error } ` ) ;
336+ log . error ( `Failed to upsert articles: ${ toError ( error ) . message } ` ) ;
337+ throw error ;
302338 }
303339}
304340
@@ -318,7 +354,8 @@ async function saveSnapshotAggregates(snapshot: MonitorSnapshot): Promise<void>
318354 ]
319355 ) ;
320356 } catch ( error ) {
321- log . error ( `Failed to save snapshot: ${ error } ` ) ;
357+ log . error ( `Failed to save snapshot: ${ toError ( error ) . message } ` ) ;
358+ throw error ;
322359 }
323360}
324361
@@ -330,8 +367,8 @@ export async function getLatestSnapshot(locale?: MonitorLocale): Promise<Monitor
330367 try {
331368 const cached = await redisClient . get ( REDIS_SNAPSHOT_KEY ) ;
332369 if ( cached ) return JSON . parse ( cached ) as MonitorSnapshot ;
333- } catch {
334- // Fall through
370+ } catch ( error ) {
371+ log . warn ( `Redis snapshot read failed, falling back to DB: ${ toError ( error ) . message } ` ) ;
335372 }
336373 }
337374
@@ -367,8 +404,8 @@ export async function getLatestSnapshot(locale?: MonitorLocale): Promise<Monitor
367404 if ( loc === 'de' ) snapshot . articlesByLocale . de = cnt ;
368405 if ( loc === 'at' ) snapshot . articlesByLocale . at = cnt ;
369406 }
370- } catch {
371- // Non-critical
407+ } catch ( error ) {
408+ log . warn ( `Failed to load locale article counts: ${ toError ( error ) . message } ` ) ;
372409 }
373410
374411 if ( locale ) {
@@ -397,13 +434,13 @@ export async function getLatestSnapshot(locale?: MonitorLocale): Promise<Monitor
397434 await redisClient . set ( REDIS_SNAPSHOT_KEY , JSON . stringify ( snapshot ) , {
398435 EX : REDIS_TTL_SECONDS ,
399436 } ) ;
400- } catch {
401- // Ignore
437+ } catch ( error ) {
438+ log . warn ( `Failed to repopulate Redis snapshot cache: ${ toError ( error ) . message } ` ) ;
402439 }
403440
404441 return snapshot ;
405442 } catch ( error ) {
406- log . error ( `Failed to fetch snapshot: ${ error } ` ) ;
443+ log . error ( `Failed to fetch snapshot: ${ toError ( error ) . message } ` ) ;
407444 return null ;
408445 }
409446}
@@ -425,8 +462,8 @@ export async function getStimmung(locale?: MonitorLocale): Promise<StimmungResul
425462 try {
426463 const cached = await redisClient . get ( stimmungCacheKey ) ;
427464 if ( cached ) return JSON . parse ( cached ) as StimmungResult ;
428- } catch {
429- // Fall through to DB
465+ } catch ( error ) {
466+ log . warn ( `Redis stimmung read failed, falling back to DB: ${ toError ( error ) . message } ` ) ;
430467 }
431468
432469 try {
@@ -539,13 +576,13 @@ export async function getStimmung(locale?: MonitorLocale): Promise<StimmungResul
539576
540577 try {
541578 await redisClient . set ( stimmungCacheKey , JSON . stringify ( result ) , { EX : REDIS_TTL_SECONDS } ) ;
542- } catch {
543- // Non-critical
579+ } catch ( error ) {
580+ log . warn ( `Failed to cache stimmung in Redis: ${ toError ( error ) . message } ` ) ;
544581 }
545582
546583 return result ;
547584 } catch ( error ) {
548- log . error ( `Failed to get stimmung: ${ error } ` ) ;
585+ log . error ( `Failed to get stimmung: ${ toError ( error ) . message } ` ) ;
549586 return { overall : { } , byTopic : [ ] , bySource : [ ] , byKeyword : [ ] , dominantEmotion : null } ;
550587 }
551588}
0 commit comments