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.
- package/CHANGELOG.md +7 -0
- package/README.md +5 -0
- package/bin/adapter/entry-points/cli/index.js +18 -1
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/console/dashboardComposeService.js +178 -0
- package/bin/adapter/entry-points/console/dashboardComposeService.js.map +1 -0
- package/bin/adapter/entry-points/console/webServer.js +29 -7
- package/bin/adapter/entry-points/console/webServer.js.map +1 -1
- package/bin/domain/usecases/dashboard/ComposeDashboardUseCase.js +161 -0
- package/bin/domain/usecases/dashboard/ComposeDashboardUseCase.js.map +1 -0
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/index.ts +36 -2
- package/src/adapter/entry-points/console/dashboardComposeService.test.ts +443 -0
- package/src/adapter/entry-points/console/dashboardComposeService.ts +200 -0
- package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +2 -0
- package/src/adapter/entry-points/console/webServer.test.ts +255 -54
- package/src/adapter/entry-points/console/webServer.ts +43 -10
- package/src/domain/usecases/dashboard/ComposeDashboardUseCase.test.ts +405 -0
- package/src/domain/usecases/dashboard/ComposeDashboardUseCase.ts +220 -0
- package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
- package/types/adapter/entry-points/console/dashboardComposeService.d.ts +9 -0
- package/types/adapter/entry-points/console/dashboardComposeService.d.ts.map +1 -0
- package/types/adapter/entry-points/console/webServer.d.ts +4 -0
- package/types/adapter/entry-points/console/webServer.d.ts.map +1 -1
- package/types/domain/usecases/dashboard/ComposeDashboardUseCase.d.ts +28 -0
- 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
|
|
1088
|
-
'
|
|
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
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1183
|
+
const expectedComposed =
|
|
1184
|
+
'<tt>M55% C62% LA 16 23 40 cy14</tt><br>\n' +
|
|
1185
|
+
'<tt>pj unr tdo aqc fal prp aws dep</tt><br>\n' +
|
|
1186
|
+
'<tt>🟢um 3 1 2 0 0 4 1</tt><br>\n' +
|
|
1187
|
+
'<tt>🟢xc 0 0 0 0 0 2 0</tt><br>\n' +
|
|
1188
|
+
'<tt></tt><br>\n' +
|
|
1189
|
+
'<tt>🟢alice 10% 0d01h00 12% 5d00h00 2 1</tt><br>\n';
|
|
1190
|
+
|
|
1191
|
+
const staticDashboardRaw =
|
|
1192
|
+
'<tt>STATIC DASHBOARD</tt><br>\n<tt>pj unr 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:
|
|
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('
|
|
1109
|
-
const
|
|
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(
|
|
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(
|
|
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('
|
|
1127
|
-
const
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
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
|
|
1146
|
-
const { server, tmpDir } = await (
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
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
|
|
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
|
-
|
|
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;
|