@@ -33,11 +33,7 @@ import { dataUriToBuffer } from 'data-uri-to-buffer';
3333import { gameConfig } from './gameConfig.js' ;
3434import {
3535 getLatestAnalysisRunDir ,
36- readMatchSchedule ,
37- readTeamsList ,
38- } from './appSettings.js' ;
39- import {
40- getLatestAnalysisRunDir ,
36+ settingsPath ,
4137 readMatchSchedule ,
4238 readTeamsList ,
4339} from './appSettings.js' ;
@@ -53,19 +49,15 @@ const DB_ENABLED = process.env.NODE_ENV !== 'dev' || DEV_USE_DOCKER;
5349const currentDir = path . dirname ( fileURLToPath ( import . meta. url ) ) ;
5450const repoRoot = path . resolve ( currentDir , '../..' ) ;
5551const staticDir = path . resolve ( currentDir , '../static' ) ;
56- const analyzedPayloadPath = path . resolve (
57- repoRoot ,
58- 'data-analysis/output/06_picklist_payload.json'
59- ) ;
60- const pipelineConfigPath = path . resolve ( repoRoot , 'data-analysis/pipeline_config.json' ) ;
6152const exportPayloadScriptPath = path . resolve ( repoRoot , 'data-analysis/06_export_app_payloads.py' ) ;
62- const analyzedInputCsvPaths = [
63- path . resolve ( repoRoot , 'data-analysis/output/03_match_features.csv' ) ,
64- path . resolve ( repoRoot , 'data-analysis/output/03_timeseries_long.csv' ) ,
65- path . resolve ( repoRoot , 'data-analysis/output/03_auto_path_points.csv' ) ,
66- path . resolve ( repoRoot , 'data-analysis/output/04_team_aggregates.csv' ) ,
67- path . resolve ( repoRoot , 'data-analysis/output/05_picklist_scores.csv' ) ,
68- path . resolve ( repoRoot , 'data-analysis/output/05_metric_contributions.csv' ) ,
53+ const analyzedPayloadFilename = '06_picklist_payload.json' ;
54+ const analyzedInputFilenames = [
55+ '03_match_features.csv' ,
56+ '03_timeseries_long.csv' ,
57+ '03_auto_path_points.csv' ,
58+ '04_team_aggregates.csv' ,
59+ '05_picklist_scores.csv' ,
60+ '05_metric_contributions.csv' ,
6961] ;
7062const clientDistDir = path . resolve ( repoRoot , 'client/dist' ) ;
7163const DEFAULT_BALLS_PER_SECOND = 5 ;
@@ -77,7 +69,7 @@ const pythonCommand =
7769 process . env . PYTHON_CMD ??
7870 process . env . PYTHON ??
7971 ( process . platform === 'win32' ? 'python' : 'python3' ) ;
80- let analyzedCsvBuildInFlight : Promise < void > | null = null ;
72+ const analyzedCsvBuildInFlightByRun = new Map < string , Promise < void > > ( ) ;
8173
8274const app = express ( ) ;
8375
@@ -131,19 +123,29 @@ const matchTimelineSegments = gameConfig.segments.map(segment => ({
131123
132124function shouldServeAnalyzedFromLocalCsv ( ) {
133125 const mode = String ( process . env . PICKLIST_ANALYZED_SOURCE ?? '' ) . toLowerCase ( ) ;
134- if ( mode === 'mongo' ) return false ;
135- if ( mode === 'csv' || mode === 'local' || mode === 'output' ) return true ;
136- return DEV ;
126+ return mode === 'csv' || mode === 'local' || mode === 'output' ;
137127}
138128
139- function runLocalCsvExport ( ) {
129+ function getAnalyzedPayloadPath ( analysisRunDir : string ) {
130+ return path . resolve ( analysisRunDir , analyzedPayloadFilename ) ;
131+ }
132+
133+ function getAnalyzedInputCsvPaths ( analysisRunDir : string ) {
134+ return analyzedInputFilenames . map ( filename =>
135+ path . resolve ( analysisRunDir , filename )
136+ ) ;
137+ }
138+
139+ function runLocalCsvExport ( analysisRunDir : string ) {
140140 return new Promise < void > ( ( resolve , reject ) => {
141141 const child = spawn (
142142 pythonCommand ,
143143 [
144144 exportPayloadScriptPath ,
145- '--config' ,
146- pipelineConfigPath ,
145+ '--settings' ,
146+ settingsPath ,
147+ '--analysis-run' ,
148+ analysisRunDir ,
147149 ] ,
148150 { cwd : repoRoot }
149151 ) ;
@@ -171,35 +173,48 @@ function runLocalCsvExport() {
171173 } ) ;
172174}
173175
174- async function shouldRebuildAnalyzedPayloadFromLocalCsv ( ) {
176+ async function shouldRebuildAnalyzedPayloadFromLocalCsv ( analysisRunDir : string ) {
177+ const payloadPath = getAnalyzedPayloadPath ( analysisRunDir ) ;
175178 let payloadMtimeMs = 0 ;
176179 try {
177- payloadMtimeMs = ( await fs . promises . stat ( analyzedPayloadPath ) ) . mtimeMs ;
180+ payloadMtimeMs = ( await fs . promises . stat ( payloadPath ) ) . mtimeMs ;
178181 } catch {
179182 return true ;
180183 }
181184
182185 const inputStats = await Promise . all (
183- analyzedInputCsvPaths . map ( csvPath => fs . promises . stat ( csvPath ) )
186+ getAnalyzedInputCsvPaths ( analysisRunDir ) . map ( async csvPath => {
187+ try {
188+ return await fs . promises . stat ( csvPath ) ;
189+ } catch {
190+ return null ;
191+ }
192+ } )
184193 ) ;
194+ const existingInputStats = inputStats . filter (
195+ ( entry ) : entry is fs . Stats => entry !== null
196+ ) ;
197+ if ( ! existingInputStats . length ) return false ;
185198 const newestInputMtimeMs = Math . max (
186- ...inputStats . map ( entry => entry . mtimeMs )
199+ ...existingInputStats . map ( entry => entry . mtimeMs )
187200 ) ;
188201 return newestInputMtimeMs > payloadMtimeMs ;
189202}
190203
191- async function ensureAnalyzedPayloadFromLocalCsv ( ) {
204+ async function ensureAnalyzedPayloadFromLocalCsv ( analysisRunDir : string ) {
192205 if ( ! shouldServeAnalyzedFromLocalCsv ( ) ) return ;
193- if ( analyzedCsvBuildInFlight ) {
194- await analyzedCsvBuildInFlight ;
206+ const inFlight = analyzedCsvBuildInFlightByRun . get ( analysisRunDir ) ;
207+ if ( inFlight ) {
208+ await inFlight ;
195209 return ;
196210 }
197- if ( ! ( await shouldRebuildAnalyzedPayloadFromLocalCsv ( ) ) ) return ;
211+ if ( ! ( await shouldRebuildAnalyzedPayloadFromLocalCsv ( analysisRunDir ) ) ) return ;
198212
199- analyzedCsvBuildInFlight = runLocalCsvExport ( ) . finally ( ( ) => {
200- analyzedCsvBuildInFlight = null ;
213+ const buildPromise = runLocalCsvExport ( analysisRunDir ) . finally ( ( ) => {
214+ analyzedCsvBuildInFlightByRun . delete ( analysisRunDir ) ;
201215 } ) ;
202- await analyzedCsvBuildInFlight ;
216+ analyzedCsvBuildInFlightByRun . set ( analysisRunDir , buildPromise ) ;
217+ await buildPromise ;
203218}
204219
205220type AutoPathTrace = NonNullable < MatchData [ 'autoPath' ] > ;
@@ -746,28 +761,6 @@ app.get('/config/teams-list', (_req, res) => {
746761 }
747762} ) ;
748763
749- app . get ( '/config/match-schedule' , ( _req , res ) => {
750- try {
751- res . send ( readMatchSchedule ( ) ) ;
752- } catch ( error ) {
753- res . status ( 500 ) . send ( {
754- message : 'Failed to load match schedule from app_settings.' ,
755- error : error instanceof Error ? error . message : String ( error ) ,
756- } ) ;
757- }
758- } ) ;
759-
760- app . get ( '/config/teams-list' , ( _req , res ) => {
761- try {
762- res . send ( readTeamsList ( ) ) ;
763- } catch ( error ) {
764- res . status ( 500 ) . send ( {
765- message : 'Failed to load teams list from app_settings.' ,
766- error : error instanceof Error ? error . message : String ( error ) ,
767- } ) ;
768- }
769- } ) ;
770-
771764app . post ( '/config/auto-field-orientation' , async ( req , res ) => {
772765 if ( ! DB_ENABLED ) {
773766 sendDbDisabled ( res ) ;
@@ -856,37 +849,23 @@ app.get('/data/retrieve/analyzed', async (_req, res) => {
856849 return ;
857850 }
858851
859- const payloadPath = path . resolve ( latestRunDir , '06_picklist_payload.json' ) ;
860- const latestRunDir = getLatestAnalysisRunDir ( ) ;
861- if ( ! latestRunDir ) {
862- res . status ( 404 ) . send ( {
863- message :
864- 'No analysis run pointer found. Run 02 -> 06 in data-analysis first.' ,
865- } ) ;
866- return ;
867- }
868-
869- const payloadPath = path . resolve ( latestRunDir , '06_picklist_payload.json' ) ;
852+ const payloadPath = getAnalyzedPayloadPath ( latestRunDir ) ;
870853 try {
871- await ensureAnalyzedPayloadFromLocalCsv ( ) ;
872- const payloadRaw = await fs . promises . readFile ( analyzedPayloadPath , 'utf8' ) ;
854+ await ensureAnalyzedPayloadFromLocalCsv ( latestRunDir ) ;
855+ const payloadRaw = await fs . promises . readFile ( payloadPath , 'utf8' ) ;
873856 if ( shouldServeAnalyzedFromLocalCsv ( ) ) {
874857 const payload = JSON . parse ( payloadRaw ) as Record < string , unknown > ;
875858 payload . sourceMode = 'csv' ;
876859 res . json ( payload ) ;
877860 return ;
878861 }
879862 res . type ( 'application/json' ) . send ( payloadRaw ) ;
880- } catch ( error ) {
881863 } catch ( error ) {
882864 res . status ( 404 ) . send ( {
883- message :
884- 'Analyzed payload not found in latest analysis run. Run stage 06_export_app_payloads.py.' ,
885865 message :
886866 'Analyzed payload not found in latest analysis run. Run stage 06_export_app_payloads.py.' ,
887867 path : payloadPath ,
888868 error : error instanceof Error ? error . message : String ( error ) ,
889- error : error instanceof Error ? error . message : String ( error ) ,
890869 } ) ;
891870 }
892871} ) ;
0 commit comments