@@ -27,12 +27,22 @@ export interface ExternalDataSourceCoverage {
2727 total : number
2828 covered : number
2929 byCountry : ExternalDataCoverageCountryRow [ ]
30+ /** Most recent `checkedAt` this source actually succeeded at, across every
31+ * auction — null when never tracked or never succeeded. A transient
32+ * fetch failure keeps the previous run's value (external-enrichment.ts's
33+ * mergeLocationContextWithPrevious), so unlike `covered` this only stalls
34+ * when the source has stopped succeeding for every auction, surfacing a
35+ * dead provider that a stable coverage percentage would otherwise hide. */
36+ lastSuccessAt : string | null
3037}
3138
3239// One row per country, geocoded_total plus one covered-count column per
3340// source in COVERAGE_SOURCE_IDS (column name = source id with '-' -> '_').
3441// A single query grouped by country avoids six-way N+1 scans over
35- // location_enrichment.
42+ // location_enrichment. lastSuccessAt columns are only meaningful for the
43+ // three sources subject to that transient-failure merge above — hazards and
44+ // market comparison already fall back to their previous whole value on
45+ // failure (external-enrichment.ts), so they're never tracked here.
3646const COVERAGE_QUERY = `
3747 SELECT
3848 a.country,
@@ -41,14 +51,20 @@ const COVERAGE_QUERY = `
4151 WHERE a.lat IS NOT NULL AND a.lng IS NOT NULL
4252 AND nullif(le.enrichment->'locationContext'->'environment'->'airQuality', 'null'::jsonb) IS NOT NULL
4353 ) AS cams_air_quality,
54+ max(le.enrichment->'locationContext'->'environment'->'airQuality'->>'checkedAt') AS cams_air_quality_last_success_at,
4455 count(*) FILTER (
4556 WHERE a.lat IS NOT NULL AND a.lng IS NOT NULL
4657 AND le.enrichment->'locationContext'->'environment'->'climateNormals' IS NOT NULL
4758 ) AS open_meteo_climate_normals,
59+ max(le.enrichment->'locationContext'->'environment'->'climateNormals'->>'checkedAt') AS open_meteo_climate_normals_last_success_at,
4860 count(*) FILTER (
4961 WHERE a.lat IS NOT NULL AND a.lng IS NOT NULL
5062 AND jsonb_array_length(coalesce(le.enrichment->'locationContext'->'environment'->'reportedNoise', '[]'::jsonb)) > 0
5163 ) AS eea_environmental_noise_directive,
64+ max((
65+ SELECT max(n->>'checkedAt')
66+ FROM jsonb_array_elements(coalesce(le.enrichment->'locationContext'->'environment'->'reportedNoise', '[]'::jsonb)) n
67+ )) AS eea_environmental_noise_directive_last_success_at,
5268 count(*) FILTER (
5369 WHERE a.lat IS NOT NULL AND a.lng IS NOT NULL
5470 AND EXISTS (
@@ -84,13 +100,19 @@ const COVERAGE_COLUMN_BY_SOURCE_ID: Record<CoverageSourceId, string> = {
84100 'fr-dvf-geolocated' : 'fr_dvf_geolocated' ,
85101}
86102
87- type CoverageRow = { country : string ; geocoded_total : string } & Record < string , string >
103+ const LAST_SUCCESS_COLUMN_BY_SOURCE_ID : Partial < Record < CoverageSourceId , string > > = {
104+ 'cams-air-quality' : 'cams_air_quality_last_success_at' ,
105+ 'open-meteo-climate-normals' : 'open_meteo_climate_normals_last_success_at' ,
106+ 'eea-environmental-noise-directive' : 'eea_environmental_noise_directive_last_success_at' ,
107+ }
108+
109+ type CoverageRow = { country : string ; geocoded_total : string } & Record < string , string | null >
88110
89111export async function computeExternalDataCoverage ( db : Pool ) : Promise < ExternalDataSourceCoverage [ ] > {
90112 const { rows } = await db . query < CoverageRow > ( COVERAGE_QUERY )
91113
92114 const bySource = new Map < CoverageSourceId , ExternalDataSourceCoverage > (
93- COVERAGE_SOURCE_IDS . map ( ( id ) => [ id , { id, total : 0 , covered : 0 , byCountry : [ ] } ] ) ,
115+ COVERAGE_SOURCE_IDS . map ( ( id ) => [ id , { id, total : 0 , covered : 0 , byCountry : [ ] , lastSuccessAt : null } ] ) ,
94116 )
95117
96118 for ( const row of rows ) {
@@ -103,6 +125,11 @@ export async function computeExternalDataCoverage(db: Pool): Promise<ExternalDat
103125 entry . total += total
104126 entry . covered += covered
105127 entry . byCountry . push ( { country : row . country , total, covered } )
128+ const lastSuccessColumn = LAST_SUCCESS_COLUMN_BY_SOURCE_ID [ id ]
129+ const rowLastSuccessAt = lastSuccessColumn ? row [ lastSuccessColumn ] : null
130+ if ( rowLastSuccessAt && ( ! entry . lastSuccessAt || rowLastSuccessAt > entry . lastSuccessAt ) ) {
131+ entry . lastSuccessAt = rowLastSuccessAt
132+ }
106133 }
107134 }
108135
0 commit comments