github-issue-tower-defence-management 1.106.0 → 1.107.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +5 -0
  3. package/bin/adapter/entry-points/cli/index.js +18 -1
  4. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  5. package/bin/adapter/entry-points/console/dashboardComposeService.js +178 -0
  6. package/bin/adapter/entry-points/console/dashboardComposeService.js.map +1 -0
  7. package/bin/adapter/entry-points/console/webServer.js +29 -7
  8. package/bin/adapter/entry-points/console/webServer.js.map +1 -1
  9. package/bin/domain/usecases/dashboard/ComposeDashboardUseCase.js +161 -0
  10. package/bin/domain/usecases/dashboard/ComposeDashboardUseCase.js.map +1 -0
  11. package/package.json +1 -1
  12. package/src/adapter/entry-points/cli/index.ts +36 -2
  13. package/src/adapter/entry-points/console/dashboardComposeService.test.ts +443 -0
  14. package/src/adapter/entry-points/console/dashboardComposeService.ts +200 -0
  15. package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +2 -0
  16. package/src/adapter/entry-points/console/webServer.test.ts +255 -54
  17. package/src/adapter/entry-points/console/webServer.ts +43 -10
  18. package/src/domain/usecases/dashboard/ComposeDashboardUseCase.test.ts +405 -0
  19. package/src/domain/usecases/dashboard/ComposeDashboardUseCase.ts +220 -0
  20. package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
  21. package/types/adapter/entry-points/console/dashboardComposeService.d.ts +9 -0
  22. package/types/adapter/entry-points/console/dashboardComposeService.d.ts.map +1 -0
  23. package/types/adapter/entry-points/console/webServer.d.ts +4 -0
  24. package/types/adapter/entry-points/console/webServer.d.ts.map +1 -1
  25. package/types/domain/usecases/dashboard/ComposeDashboardUseCase.d.ts +28 -0
  26. package/types/domain/usecases/dashboard/ComposeDashboardUseCase.d.ts.map +1 -0
@@ -181,6 +181,8 @@ describe('webServer integration', () => {
181
181
  consoleDataOutputDir: null,
182
182
  inTmuxDataDir: null,
183
183
  dashboardDir: null,
184
+ dashboardDataDir: null,
185
+ dashboardProjectCodes: [],
184
186
  port: 0,
185
187
  });
186
188
  const address = server.address();
@@ -198,6 +200,8 @@ describe('webServer integration', () => {
198
200
  consoleDataOutputDir: null,
199
201
  inTmuxDataDir: null,
200
202
  dashboardDir: null,
203
+ dashboardDataDir: null,
204
+ dashboardProjectCodes: [],
201
205
  port: 0,
202
206
  });
203
207
  try {
@@ -224,6 +228,8 @@ describe('webServer integration', () => {
224
228
  consoleDataOutputDir: null,
225
229
  inTmuxDataDir: null,
226
230
  dashboardDir: null,
231
+ dashboardDataDir: null,
232
+ dashboardProjectCodes: [],
227
233
  port: 0,
228
234
  });
229
235
  try {
@@ -254,6 +260,8 @@ describe('webServer integration', () => {
254
260
  consoleDataOutputDir: null,
255
261
  inTmuxDataDir: null,
256
262
  dashboardDir: null,
263
+ dashboardDataDir: null,
264
+ dashboardProjectCodes: [],
257
265
  port: 0,
258
266
  });
259
267
  try {
@@ -286,6 +294,8 @@ describe('webServer integration', () => {
286
294
  consoleDataOutputDir: null,
287
295
  inTmuxDataDir: null,
288
296
  dashboardDir: null,
297
+ dashboardDataDir: null,
298
+ dashboardProjectCodes: [],
289
299
  port: 0,
290
300
  });
291
301
  try {
@@ -315,6 +325,8 @@ describe('webServer integration', () => {
315
325
  consoleDataOutputDir: null,
316
326
  inTmuxDataDir: null,
317
327
  dashboardDir: null,
328
+ dashboardDataDir: null,
329
+ dashboardProjectCodes: [],
318
330
  port: 0,
319
331
  });
320
332
  try {
@@ -338,6 +350,8 @@ describe('webServer integration', () => {
338
350
  consoleDataOutputDir: null,
339
351
  inTmuxDataDir: null,
340
352
  dashboardDir: null,
353
+ dashboardDataDir: null,
354
+ dashboardProjectCodes: [],
341
355
  port: 0,
342
356
  });
343
357
  try {
@@ -364,6 +378,8 @@ describe('webServer integration', () => {
364
378
  consoleDataOutputDir: null,
365
379
  inTmuxDataDir: null,
366
380
  dashboardDir: null,
381
+ dashboardDataDir: null,
382
+ dashboardProjectCodes: [],
367
383
  port: 0,
368
384
  });
369
385
  try {
@@ -502,6 +518,8 @@ describe('webServer new routes integration', () => {
502
518
  consoleDataOutputDir: dataDir,
503
519
  inTmuxDataDir: null,
504
520
  dashboardDir: null,
521
+ dashboardDataDir: null,
522
+ dashboardProjectCodes: [],
505
523
  port: 0,
506
524
  });
507
525
  try {
@@ -539,6 +557,8 @@ describe('webServer new routes integration', () => {
539
557
  consoleDataOutputDir: null,
540
558
  inTmuxDataDir: null,
541
559
  dashboardDir: null,
560
+ dashboardDataDir: null,
561
+ dashboardProjectCodes: [],
542
562
  issueRepository,
543
563
  issueTitleStateCache: new IssueTitleStateCache(),
544
564
  port: 0,
@@ -573,6 +593,8 @@ describe('webServer new routes integration', () => {
573
593
  consoleDataOutputDir: null,
574
594
  inTmuxDataDir: null,
575
595
  dashboardDir: null,
596
+ dashboardDataDir: null,
597
+ dashboardProjectCodes: [],
576
598
  issueRepository,
577
599
  resolveProject: async (pjcode) =>
578
600
  pjcode === 'umino' ? { pjcode, project: buildProject() } : null,
@@ -622,6 +644,8 @@ describe('webServer new routes integration', () => {
622
644
  consoleDataOutputDir: dataDir,
623
645
  inTmuxDataDir: null,
624
646
  dashboardDir: null,
647
+ dashboardDataDir: null,
648
+ dashboardProjectCodes: [],
625
649
  issueRepository,
626
650
  resolveProject: async (pjcode) =>
627
651
  pjcode === 'umino' ? { pjcode, project: buildProject() } : null,
@@ -668,6 +692,8 @@ describe('webServer new routes integration', () => {
668
692
  consoleDataOutputDir: null,
669
693
  inTmuxDataDir: null,
670
694
  dashboardDir: null,
695
+ dashboardDataDir: null,
696
+ dashboardProjectCodes: [],
671
697
  issueRepository,
672
698
  resolveProject: async (pjcode) =>
673
699
  pjcode === 'umino' ? { pjcode, project: buildProject() } : null,
@@ -704,6 +730,8 @@ describe('webServer new routes integration', () => {
704
730
  consoleDataOutputDir: null,
705
731
  inTmuxDataDir: null,
706
732
  dashboardDir: null,
733
+ dashboardDataDir: null,
734
+ dashboardProjectCodes: [],
707
735
  issueRepository,
708
736
  resolveProject: async (pjcode) =>
709
737
  pjcode === 'umino' ? { pjcode, project: buildProject() } : null,
@@ -755,6 +783,8 @@ describe('webServer new routes integration', () => {
755
783
  consoleDataOutputDir: null,
756
784
  inTmuxDataDir: null,
757
785
  dashboardDir: null,
786
+ dashboardDataDir: null,
787
+ dashboardProjectCodes: [],
758
788
  port: 0,
759
789
  });
760
790
  try {
@@ -809,6 +839,23 @@ describe('resolveFlatInTmuxFilePath', () => {
809
839
  });
810
840
  });
811
841
 
842
+ describe('resolveDashboardFilePath', () => {
843
+ const baseDir = path.join(os.tmpdir(), 'dashboard-static');
844
+
845
+ it('resolves /tdpm.txt under the dashboard dir', () => {
846
+ expect(resolveDashboardFilePath(baseDir, '/tdpm.txt')).toBe(
847
+ path.join(path.resolve(baseDir), 'tdpm.txt'),
848
+ );
849
+ });
850
+
851
+ it('returns null for any other path', () => {
852
+ expect(resolveDashboardFilePath(baseDir, '/tdpm.html')).toBeNull();
853
+ expect(resolveDashboardFilePath(baseDir, '/other.txt')).toBeNull();
854
+ expect(resolveDashboardFilePath(baseDir, '/')).toBeNull();
855
+ expect(resolveDashboardFilePath(baseDir, '/sub/tdpm.txt')).toBeNull();
856
+ });
857
+ });
858
+
812
859
  describe('webServer flat in-tmux-by-human route integration', () => {
813
860
  const testToken = 'integration-test-token-value';
814
861
 
@@ -884,6 +931,8 @@ describe('webServer flat in-tmux-by-human route integration', () => {
884
931
  consoleDataOutputDir: null,
885
932
  inTmuxDataDir,
886
933
  dashboardDir: null,
934
+ dashboardDataDir: null,
935
+ dashboardProjectCodes: [],
887
936
  port: 0,
888
937
  });
889
938
  return { server, tmpDir, inTmuxDataDir };
@@ -998,6 +1047,8 @@ describe('webServer flat in-tmux-by-human route integration', () => {
998
1047
  consoleDataOutputDir: null,
999
1048
  inTmuxDataDir: null,
1000
1049
  dashboardDir: null,
1050
+ dashboardDataDir: null,
1051
+ dashboardProjectCodes: [],
1001
1052
  port: 0,
1002
1053
  });
1003
1054
  try {
@@ -1013,23 +1064,6 @@ describe('webServer flat in-tmux-by-human route integration', () => {
1013
1064
  });
1014
1065
  });
1015
1066
 
1016
- describe('resolveDashboardFilePath', () => {
1017
- const baseDir = path.join(os.tmpdir(), 'dashboard-data');
1018
-
1019
- it('resolves /tdpm.txt under the dashboard dir', () => {
1020
- expect(resolveDashboardFilePath(baseDir, '/tdpm.txt')).toBe(
1021
- path.join(path.resolve(baseDir), 'tdpm.txt'),
1022
- );
1023
- });
1024
-
1025
- it('returns null for any other path', () => {
1026
- expect(resolveDashboardFilePath(baseDir, '/tdpm.html')).toBeNull();
1027
- expect(resolveDashboardFilePath(baseDir, '/other.txt')).toBeNull();
1028
- expect(resolveDashboardFilePath(baseDir, '/')).toBeNull();
1029
- expect(resolveDashboardFilePath(baseDir, '/sub/tdpm.txt')).toBeNull();
1030
- });
1031
- });
1032
-
1033
1067
  describe('webServer dashboard /tdpm.txt route integration', () => {
1034
1068
  const testToken = 'integration-test-token-value';
1035
1069
 
@@ -1084,54 +1118,216 @@ describe('webServer dashboard /tdpm.txt route integration', () => {
1084
1118
  });
1085
1119
  });
1086
1120
 
1087
- const dashboardRaw =
1088
- '<tt>MEM&nbsp;30%</tt><br>\n<tt>pj&nbsp;unr&nbsp;tdo</tt><br>\n';
1121
+ const writeDataFiles = (dataDir: string): void => {
1122
+ fs.mkdirSync(path.join(dataDir, 'projects'), { recursive: true });
1123
+ fs.writeFileSync(
1124
+ path.join(dataDir, 'projects', 'um.json'),
1125
+ JSON.stringify({
1126
+ pjcode: 'um',
1127
+ capturedAt: '2026-06-26T00:00:00.000Z',
1128
+ unread: 3,
1129
+ todo: 1,
1130
+ qc: 2,
1131
+ fail: 0,
1132
+ pr: 0,
1133
+ ws: 4,
1134
+ dep: 1,
1135
+ blocker: 0,
1136
+ }),
1137
+ );
1138
+ fs.writeFileSync(
1139
+ path.join(dataDir, 'projects', 'xc.json'),
1140
+ JSON.stringify({
1141
+ pjcode: 'xc',
1142
+ capturedAt: '2026-06-26T00:00:00.000Z',
1143
+ unread: 0,
1144
+ todo: 0,
1145
+ qc: 0,
1146
+ fail: 0,
1147
+ pr: 0,
1148
+ ws: 2,
1149
+ dep: 0,
1150
+ blocker: 0,
1151
+ }),
1152
+ );
1153
+ fs.writeFileSync(
1154
+ path.join(dataDir, 'machine-status.json'),
1155
+ JSON.stringify({
1156
+ memPct: 55,
1157
+ cpuPct: 62,
1158
+ load: [16, 23, 40],
1159
+ cycleMinutes: 14,
1160
+ capturedAt: '2026-06-26T00:00:00.000Z',
1161
+ }),
1162
+ );
1163
+ fs.writeFileSync(
1164
+ path.join(dataDir, 'token-status.json'),
1165
+ JSON.stringify({
1166
+ tokens: [
1167
+ {
1168
+ name: 'alice',
1169
+ fiveHourUtilizationPercent: 10,
1170
+ fiveHourResetSeconds: 3600,
1171
+ sevenDayUtilizationPercent: 12,
1172
+ sevenDayResetSeconds: 432000,
1173
+ color: 'G',
1174
+ prep: 2,
1175
+ hum: 1,
1176
+ },
1177
+ ],
1178
+ capturedAt: '2026-06-26T00:00:00.000Z',
1179
+ }),
1180
+ );
1181
+ };
1089
1182
 
1090
- const startWithDashboard = async (): Promise<{
1091
- server: http.Server;
1092
- tmpDir: string;
1093
- }> => {
1183
+ const expectedComposed =
1184
+ '<tt>M55%&nbsp;C62%&nbsp;LA&nbsp;16&nbsp;23&nbsp;40&nbsp;cy14</tt><br>\n' +
1185
+ '<tt>pj&nbsp;&nbsp;&nbsp;unr&nbsp;tdo&nbsp;aqc&nbsp;fal&nbsp;prp&nbsp;aws&nbsp;dep</tt><br>\n' +
1186
+ '<tt>🟢um&nbsp;&nbsp;&nbsp;3&nbsp;&nbsp;&nbsp;1&nbsp;&nbsp;&nbsp;2&nbsp;&nbsp;&nbsp;0&nbsp;&nbsp;&nbsp;0&nbsp;&nbsp;&nbsp;4&nbsp;&nbsp;&nbsp;1</tt><br>\n' +
1187
+ '<tt>🟢xc&nbsp;&nbsp;&nbsp;0&nbsp;&nbsp;&nbsp;0&nbsp;&nbsp;&nbsp;0&nbsp;&nbsp;&nbsp;0&nbsp;&nbsp;&nbsp;0&nbsp;&nbsp;&nbsp;2&nbsp;&nbsp;&nbsp;0</tt><br>\n' +
1188
+ '<tt></tt><br>\n' +
1189
+ '<tt>🟢alice&nbsp;&nbsp;10%&nbsp;0d01h00&nbsp;&nbsp;12%&nbsp;5d00h00&nbsp;2&nbsp;1</tt><br>\n';
1190
+
1191
+ const staticDashboardRaw =
1192
+ '<tt>STATIC&nbsp;DASHBOARD</tt><br>\n<tt>pj&nbsp;unr&nbsp;tdo</tt><br>\n';
1193
+
1194
+ const writeStaticDashboard = (staticDir: string): void => {
1195
+ fs.writeFileSync(path.join(staticDir, 'tdpm.txt'), staticDashboardRaw);
1196
+ };
1197
+
1198
+ const startServer = async (overrides: {
1199
+ dashboardDir: string | null;
1200
+ dashboardDataDir: string | null;
1201
+ }): Promise<{ server: http.Server; tmpDir: string }> => {
1094
1202
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'console-server-'));
1095
- fs.writeFileSync(path.join(tmpDir, 'tdpm.txt'), dashboardRaw);
1096
- fs.writeFileSync(path.join(tmpDir, 'secret.txt'), 'secret content');
1097
1203
  const server = await startWebServer({
1098
1204
  accessToken: testToken,
1099
1205
  uiDistDir: path.join(tmpDir, 'ui-dist'),
1100
1206
  consoleDataOutputDir: null,
1101
1207
  inTmuxDataDir: null,
1102
- dashboardDir: tmpDir,
1208
+ dashboardDir: overrides.dashboardDir,
1209
+ dashboardDataDir: overrides.dashboardDataDir,
1210
+ dashboardProjectCodes: ['um', 'xc'],
1103
1211
  port: 0,
1104
1212
  });
1105
1213
  return { server, tmpDir };
1106
1214
  };
1107
1215
 
1108
- it('serves /tdpm.txt without a token, byte-identical, as text/html with an explicit Content-Length and no chunked encoding', async () => {
1109
- const { server, tmpDir } = await startWithDashboard();
1216
+ it('composes /tdpm.txt from the data files when dashboardDataDir is set and every file is present', async () => {
1217
+ const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dashboard-data-'));
1218
+ const staticDir = fs.mkdtempSync(
1219
+ path.join(os.tmpdir(), 'dashboard-static-'),
1220
+ );
1221
+ writeDataFiles(dataDir);
1222
+ writeStaticDashboard(staticDir);
1223
+ const { server, tmpDir } = await startServer({
1224
+ dashboardDir: staticDir,
1225
+ dashboardDataDir: dataDir,
1226
+ });
1110
1227
  try {
1111
1228
  const response = await requestServer(server, '/tdpm.txt');
1112
1229
  expect(response.statusCode).toBe(200);
1113
- expect(response.body).toBe(dashboardRaw);
1230
+ expect(response.body).toBe(expectedComposed);
1114
1231
  expect(response.contentType).toBe('text/html; charset=utf-8');
1115
1232
  expect(response.contentLength).toBe(
1116
- String(Buffer.byteLength(dashboardRaw)),
1233
+ String(Buffer.byteLength(expectedComposed)),
1117
1234
  );
1118
1235
  expect(response.transferEncoding).toBeUndefined();
1119
1236
  expect(response.cacheControl).toBe('no-store');
1120
1237
  } finally {
1121
1238
  await closeServer(server);
1122
1239
  fs.rmSync(tmpDir, { recursive: true, force: true });
1240
+ fs.rmSync(dataDir, { recursive: true, force: true });
1241
+ fs.rmSync(staticDir, { recursive: true, force: true });
1123
1242
  }
1124
1243
  });
1125
1244
 
1126
- it('returns 404 for /tdpm.txt when the file is absent', async () => {
1127
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'console-server-'));
1128
- const server = await startWebServer({
1129
- accessToken: testToken,
1130
- uiDistDir: path.join(tmpDir, 'ui-dist'),
1131
- consoleDataOutputDir: null,
1132
- inTmuxDataDir: null,
1133
- dashboardDir: tmpDir,
1134
- port: 0,
1245
+ it('serves the static tdpm.txt unchanged when dashboardDataDir is unset', async () => {
1246
+ const staticDir = fs.mkdtempSync(
1247
+ path.join(os.tmpdir(), 'dashboard-static-'),
1248
+ );
1249
+ writeStaticDashboard(staticDir);
1250
+ const { server, tmpDir } = await startServer({
1251
+ dashboardDir: staticDir,
1252
+ dashboardDataDir: null,
1253
+ });
1254
+ try {
1255
+ const response = await requestServer(server, '/tdpm.txt');
1256
+ expect(response.statusCode).toBe(200);
1257
+ expect(response.body).toBe(staticDashboardRaw);
1258
+ expect(response.contentType).toBe('text/html; charset=utf-8');
1259
+ expect(response.contentLength).toBe(
1260
+ String(Buffer.byteLength(staticDashboardRaw)),
1261
+ );
1262
+ expect(response.cacheControl).toBe('no-store');
1263
+ } finally {
1264
+ await closeServer(server);
1265
+ fs.rmSync(tmpDir, { recursive: true, force: true });
1266
+ fs.rmSync(staticDir, { recursive: true, force: true });
1267
+ }
1268
+ });
1269
+
1270
+ it('falls back to the static tdpm.txt when dashboardDataDir is set but the data files are absent', async () => {
1271
+ const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dashboard-data-'));
1272
+ const staticDir = fs.mkdtempSync(
1273
+ path.join(os.tmpdir(), 'dashboard-static-'),
1274
+ );
1275
+ writeStaticDashboard(staticDir);
1276
+ const { server, tmpDir } = await startServer({
1277
+ dashboardDir: staticDir,
1278
+ dashboardDataDir: dataDir,
1279
+ });
1280
+ try {
1281
+ const response = await requestServer(server, '/tdpm.txt');
1282
+ expect(response.statusCode).toBe(200);
1283
+ expect(response.body).toBe(staticDashboardRaw);
1284
+ } finally {
1285
+ await closeServer(server);
1286
+ fs.rmSync(tmpDir, { recursive: true, force: true });
1287
+ fs.rmSync(dataDir, { recursive: true, force: true });
1288
+ fs.rmSync(staticDir, { recursive: true, force: true });
1289
+ }
1290
+ });
1291
+
1292
+ it('falls back to the static tdpm.txt when only some data files are present', async () => {
1293
+ const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dashboard-data-'));
1294
+ const staticDir = fs.mkdtempSync(
1295
+ path.join(os.tmpdir(), 'dashboard-static-'),
1296
+ );
1297
+ fs.writeFileSync(
1298
+ path.join(dataDir, 'machine-status.json'),
1299
+ JSON.stringify({
1300
+ memPct: 55,
1301
+ cpuPct: 62,
1302
+ load: [16, 23, 40],
1303
+ cycleMinutes: 14,
1304
+ capturedAt: '2026-06-26T00:00:00.000Z',
1305
+ }),
1306
+ );
1307
+ writeStaticDashboard(staticDir);
1308
+ const { server, tmpDir } = await startServer({
1309
+ dashboardDir: staticDir,
1310
+ dashboardDataDir: dataDir,
1311
+ });
1312
+ try {
1313
+ const response = await requestServer(server, '/tdpm.txt');
1314
+ expect(response.statusCode).toBe(200);
1315
+ expect(response.body).toBe(staticDashboardRaw);
1316
+ } finally {
1317
+ await closeServer(server);
1318
+ fs.rmSync(tmpDir, { recursive: true, force: true });
1319
+ fs.rmSync(dataDir, { recursive: true, force: true });
1320
+ fs.rmSync(staticDir, { recursive: true, force: true });
1321
+ }
1322
+ });
1323
+
1324
+ it('returns 404 for /tdpm.txt when the static file is absent and compose mode is inactive', async () => {
1325
+ const staticDir = fs.mkdtempSync(
1326
+ path.join(os.tmpdir(), 'dashboard-static-'),
1327
+ );
1328
+ const { server, tmpDir } = await startServer({
1329
+ dashboardDir: staticDir,
1330
+ dashboardDataDir: null,
1135
1331
  });
1136
1332
  try {
1137
1333
  const response = await requestServer(server, '/tdpm.txt');
@@ -1139,23 +1335,15 @@ describe('webServer dashboard /tdpm.txt route integration', () => {
1139
1335
  } finally {
1140
1336
  await closeServer(server);
1141
1337
  fs.rmSync(tmpDir, { recursive: true, force: true });
1338
+ fs.rmSync(staticDir, { recursive: true, force: true });
1142
1339
  }
1143
1340
  });
1144
1341
 
1145
- it('returns 404 for /tdpm.txt when dashboardDir is null', async () => {
1146
- const { server, tmpDir } = await (async () => {
1147
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'console-server-'));
1148
- fs.writeFileSync(path.join(dir, 'tdpm.txt'), dashboardRaw);
1149
- const srv = await startWebServer({
1150
- accessToken: testToken,
1151
- uiDistDir: path.join(dir, 'ui-dist'),
1152
- consoleDataOutputDir: null,
1153
- inTmuxDataDir: null,
1154
- dashboardDir: null,
1155
- port: 0,
1156
- });
1157
- return { server: srv, tmpDir: dir };
1158
- })();
1342
+ it('returns 404 for /tdpm.txt when both dashboardDir and dashboardDataDir are null', async () => {
1343
+ const { server, tmpDir } = await startServer({
1344
+ dashboardDir: null,
1345
+ dashboardDataDir: null,
1346
+ });
1159
1347
  try {
1160
1348
  const response = await requestServer(server, '/tdpm.txt');
1161
1349
  expect(response.statusCode).toBe(404);
@@ -1166,13 +1354,24 @@ describe('webServer dashboard /tdpm.txt route integration', () => {
1166
1354
  });
1167
1355
 
1168
1356
  it('rejects a non-GET method on /tdpm.txt with 404', async () => {
1169
- const { server, tmpDir } = await startWithDashboard();
1357
+ const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dashboard-data-'));
1358
+ const staticDir = fs.mkdtempSync(
1359
+ path.join(os.tmpdir(), 'dashboard-static-'),
1360
+ );
1361
+ writeDataFiles(dataDir);
1362
+ writeStaticDashboard(staticDir);
1363
+ const { server, tmpDir } = await startServer({
1364
+ dashboardDir: staticDir,
1365
+ dashboardDataDir: dataDir,
1366
+ });
1170
1367
  try {
1171
1368
  const response = await requestServer(server, '/tdpm.txt', 'POST');
1172
1369
  expect(response.statusCode).toBe(404);
1173
1370
  } finally {
1174
1371
  await closeServer(server);
1175
1372
  fs.rmSync(tmpDir, { recursive: true, force: true });
1373
+ fs.rmSync(dataDir, { recursive: true, force: true });
1374
+ fs.rmSync(staticDir, { recursive: true, force: true });
1176
1375
  }
1177
1376
  });
1178
1377
  });
@@ -1250,6 +1449,8 @@ describe('webServer image proxy', () => {
1250
1449
  consoleDataOutputDir: null,
1251
1450
  inTmuxDataDir: null,
1252
1451
  dashboardDir: null,
1452
+ dashboardDataDir: null,
1453
+ dashboardProjectCodes: [],
1253
1454
  githubToken: token,
1254
1455
  imageFetcher: fetcher,
1255
1456
  port: 0,
@@ -26,9 +26,15 @@ import {
26
26
  handleTriage,
27
27
  } from './consoleOperationApi';
28
28
  import { ImageFetcher, fetchProxiedImage } from './consoleImageProxy';
29
+ import {
30
+ composeDashboardText,
31
+ dashboardComposeFilesPresent,
32
+ } from './dashboardComposeService';
29
33
 
30
34
  export const DEFAULT_WEB_PORT = 9981;
31
35
 
36
+ export const DEFAULT_DASHBOARD_PROJECT_CODES = ['um', 'xm', 'xc', 'ut'];
37
+
32
38
  export const CONSOLE_TOKEN_HEADER = 'x-pv-token';
33
39
 
34
40
  const PLACEHOLDER_INDEX_HTML = `<!DOCTYPE html>
@@ -158,6 +164,8 @@ export type WebServerOptions = {
158
164
  consoleDataOutputDir: string | null;
159
165
  inTmuxDataDir: string | null;
160
166
  dashboardDir: string | null;
167
+ dashboardDataDir: string | null;
168
+ dashboardProjectCodes: string[];
161
169
  githubToken?: string | null;
162
170
  imageFetcher?: ImageFetcher | null;
163
171
  issueRepository?: IssueRepository | null;
@@ -525,6 +533,40 @@ const handleTokenedRequest = async (
525
533
  sendNotFound(response);
526
534
  };
527
535
 
536
+ const readStaticDashboardContent = (
537
+ dashboardDir: string | null,
538
+ requestPath: string,
539
+ ): Buffer | null => {
540
+ if (dashboardDir === null) {
541
+ return null;
542
+ }
543
+ const dashboardFilePath = resolveDashboardFilePath(dashboardDir, requestPath);
544
+ if (dashboardFilePath === null) {
545
+ return null;
546
+ }
547
+ return readStaticFile(dashboardFilePath);
548
+ };
549
+
550
+ export const resolveDashboardContent = (
551
+ options: WebServerOptions,
552
+ requestPath: string,
553
+ ): Buffer | null => {
554
+ if (
555
+ options.dashboardDataDir !== null &&
556
+ dashboardComposeFilesPresent({
557
+ dashboardDataDir: options.dashboardDataDir,
558
+ projectCodes: options.dashboardProjectCodes,
559
+ })
560
+ ) {
561
+ const dashboardText = composeDashboardText({
562
+ dashboardDataDir: options.dashboardDataDir,
563
+ projectCodes: options.dashboardProjectCodes,
564
+ });
565
+ return Buffer.from(dashboardText, 'utf-8');
566
+ }
567
+ return readStaticDashboardContent(options.dashboardDir, requestPath);
568
+ };
569
+
528
570
  export const handleWebRequest = async (
529
571
  options: WebServerOptions,
530
572
  request: http.IncomingMessage,
@@ -544,16 +586,7 @@ export const handleWebRequest = async (
544
586
  sendNotFound(response);
545
587
  return;
546
588
  }
547
- if (options.dashboardDir === null) {
548
- sendNotFound(response);
549
- return;
550
- }
551
- const dashboardFilePath = resolveDashboardFilePath(
552
- options.dashboardDir,
553
- requestPath,
554
- );
555
- const dashboardContent =
556
- dashboardFilePath === null ? null : readStaticFile(dashboardFilePath);
589
+ const dashboardContent = resolveDashboardContent(options, requestPath);
557
590
  if (dashboardContent === null) {
558
591
  sendNotFound(response);
559
592
  return;