Skip to content

Commit 9593358

Browse files
picklist data source u[date
1 parent 1f5ca28 commit 9593358

File tree

2 files changed

+55
-74
lines changed

2 files changed

+55
-74
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,8 @@ Inside each analysis run folder:
261261
Server endpoint `/data/retrieve/analyzed` now serves:
262262

263263
- latest analysis run `06_picklist_payload.json` resolved via pointer
264+
- by default, this is pointer-first in all environments
265+
- set `PICKLIST_ANALYZED_SOURCE=csv` (or `local`/`output`) to force CSV rebuild mode before serving
264266

265267
## Defense Metric (Stage 04)
266268

server/src/server.ts

Lines changed: 53 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,7 @@ import { dataUriToBuffer } from 'data-uri-to-buffer';
3333
import { gameConfig } from './gameConfig.js';
3434
import {
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;
5349
const currentDir = path.dirname(fileURLToPath(import.meta.url));
5450
const repoRoot = path.resolve(currentDir, '../..');
5551
const 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');
6152
const 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
];
7062
const clientDistDir = path.resolve(repoRoot, 'client/dist');
7163
const 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

8274
const app = express();
8375

@@ -131,19 +123,29 @@ const matchTimelineSegments = gameConfig.segments.map(segment => ({
131123

132124
function 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

205220
type 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-
771764
app.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

Comments
 (0)