github-issue-tower-defence-management 1.104.4 → 1.105.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 +13 -0
- package/README.md +8 -7
- package/bin/adapter/entry-points/cli/index.js +21 -13
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/console/{consoleServer.js → webServer.js} +11 -11
- package/bin/adapter/entry-points/console/webServer.js.map +1 -0
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +41 -2
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/adapter/entry-points/handlers/dashboardRowWriter.js +34 -0
- package/bin/adapter/entry-points/handlers/dashboardRowWriter.js.map +1 -0
- package/bin/adapter/entry-points/handlers/machineStatusWriter.js +60 -0
- package/bin/adapter/entry-points/handlers/machineStatusWriter.js.map +1 -0
- package/bin/adapter/entry-points/handlers/tokenStatusWriter.js +98 -0
- package/bin/adapter/entry-points/handlers/tokenStatusWriter.js.map +1 -0
- package/bin/adapter/proxy/RateLimitCache.js +3 -0
- package/bin/adapter/proxy/RateLimitCache.js.map +1 -1
- package/bin/adapter/repositories/ProcHostMetricsRepository.js +136 -0
- package/bin/adapter/repositories/ProcHostMetricsRepository.js.map +1 -0
- package/bin/adapter/repositories/ProcTakeOwnershipSpawnRepository.js +131 -0
- package/bin/adapter/repositories/ProcTakeOwnershipSpawnRepository.js.map +1 -0
- package/bin/domain/usecases/adapter-interfaces/TakeOwnershipSpawnRepository.js +3 -0
- package/bin/domain/usecases/adapter-interfaces/TakeOwnershipSpawnRepository.js.map +1 -0
- package/bin/domain/usecases/dashboard/GenerateDashboardRowUseCase.js +38 -0
- package/bin/domain/usecases/dashboard/GenerateDashboardRowUseCase.js.map +1 -0
- package/bin/domain/usecases/dashboard/GenerateTokenStatusUseCase.js +115 -0
- package/bin/domain/usecases/dashboard/GenerateTokenStatusUseCase.js.map +1 -0
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/index.test.ts +74 -21
- package/src/adapter/entry-points/cli/index.ts +146 -136
- package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +2 -2
- package/src/adapter/entry-points/console/{consoleServer.test.ts → webServer.test.ts} +32 -32
- package/src/adapter/entry-points/console/{consoleServer.ts → webServer.ts} +15 -17
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +52 -0
- package/src/adapter/entry-points/handlers/dashboardRowWriter.test.ts +111 -0
- package/src/adapter/entry-points/handlers/dashboardRowWriter.ts +51 -0
- package/src/adapter/entry-points/handlers/machineStatusWriter.test.ts +100 -0
- package/src/adapter/entry-points/handlers/machineStatusWriter.ts +79 -0
- package/src/adapter/entry-points/handlers/tokenStatusWriter.test.ts +176 -0
- package/src/adapter/entry-points/handlers/tokenStatusWriter.ts +139 -0
- package/src/adapter/proxy/RateLimitCache.test.ts +32 -0
- package/src/adapter/proxy/RateLimitCache.ts +6 -0
- package/src/adapter/repositories/ProcHostMetricsRepository.test.ts +135 -0
- package/src/adapter/repositories/ProcHostMetricsRepository.ts +136 -0
- package/src/adapter/repositories/ProcTakeOwnershipSpawnRepository.test.ts +130 -0
- package/src/adapter/repositories/ProcTakeOwnershipSpawnRepository.ts +118 -0
- package/src/domain/usecases/adapter-interfaces/TakeOwnershipSpawnRepository.ts +8 -0
- package/src/domain/usecases/dashboard/GenerateDashboardRowUseCase.test.ts +159 -0
- package/src/domain/usecases/dashboard/GenerateDashboardRowUseCase.ts +72 -0
- package/src/domain/usecases/dashboard/GenerateTokenStatusUseCase.test.ts +209 -0
- package/src/domain/usecases/dashboard/GenerateTokenStatusUseCase.ts +195 -0
- package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
- package/types/adapter/entry-points/console/{consoleServer.d.ts → webServer.d.ts} +7 -7
- package/types/adapter/entry-points/console/webServer.d.ts.map +1 -0
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/dashboardRowWriter.d.ts +15 -0
- package/types/adapter/entry-points/handlers/dashboardRowWriter.d.ts.map +1 -0
- package/types/adapter/entry-points/handlers/machineStatusWriter.d.ts +16 -0
- package/types/adapter/entry-points/handlers/machineStatusWriter.d.ts.map +1 -0
- package/types/adapter/entry-points/handlers/tokenStatusWriter.d.ts +21 -0
- package/types/adapter/entry-points/handlers/tokenStatusWriter.d.ts.map +1 -0
- package/types/adapter/proxy/RateLimitCache.d.ts +2 -0
- package/types/adapter/proxy/RateLimitCache.d.ts.map +1 -1
- package/types/adapter/repositories/ProcHostMetricsRepository.d.ts +23 -0
- package/types/adapter/repositories/ProcHostMetricsRepository.d.ts.map +1 -0
- package/types/adapter/repositories/ProcTakeOwnershipSpawnRepository.d.ts +11 -0
- package/types/adapter/repositories/ProcTakeOwnershipSpawnRepository.d.ts.map +1 -0
- package/types/domain/usecases/adapter-interfaces/TakeOwnershipSpawnRepository.d.ts +8 -0
- package/types/domain/usecases/adapter-interfaces/TakeOwnershipSpawnRepository.d.ts.map +1 -0
- package/types/domain/usecases/dashboard/GenerateDashboardRowUseCase.d.ts +19 -0
- package/types/domain/usecases/dashboard/GenerateDashboardRowUseCase.d.ts.map +1 -0
- package/types/domain/usecases/dashboard/GenerateTokenStatusUseCase.d.ts +53 -0
- package/types/domain/usecases/dashboard/GenerateTokenStatusUseCase.d.ts.map +1 -0
- package/bin/adapter/entry-points/console/consoleServer.js.map +0 -1
- package/types/adapter/entry-points/console/consoleServer.d.ts.map +0 -1
|
@@ -4,7 +4,7 @@ import * as os from 'os';
|
|
|
4
4
|
import * as path from 'path';
|
|
5
5
|
import { mock } from 'jest-mock-extended';
|
|
6
6
|
import {
|
|
7
|
-
|
|
7
|
+
DEFAULT_WEB_PORT,
|
|
8
8
|
CONSOLE_TOKEN_HEADER,
|
|
9
9
|
hasDotSegment,
|
|
10
10
|
requiresToken,
|
|
@@ -13,8 +13,8 @@ import {
|
|
|
13
13
|
extractProvidedToken,
|
|
14
14
|
resolveFlatInTmuxFilePath,
|
|
15
15
|
resolveDashboardFilePath,
|
|
16
|
-
|
|
17
|
-
} from './
|
|
16
|
+
startWebServer,
|
|
17
|
+
} from './webServer';
|
|
18
18
|
import type { ImageFetcher } from './consoleImageProxy';
|
|
19
19
|
import { IssueTitleStateCache } from './consoleReadApi';
|
|
20
20
|
import { readDoneProjectItemIds } from './consoleDoneStore';
|
|
@@ -22,10 +22,10 @@ import { IssueRepository } from '../../../domain/usecases/adapter-interfaces/Iss
|
|
|
22
22
|
import { Project } from '../../../domain/entities/Project';
|
|
23
23
|
import { Issue } from '../../../domain/entities/Issue';
|
|
24
24
|
|
|
25
|
-
describe('
|
|
26
|
-
describe('
|
|
25
|
+
describe('webServer pure helpers', () => {
|
|
26
|
+
describe('DEFAULT_WEB_PORT', () => {
|
|
27
27
|
it('is 9981', () => {
|
|
28
|
-
expect(
|
|
28
|
+
expect(DEFAULT_WEB_PORT).toBe(9981);
|
|
29
29
|
});
|
|
30
30
|
});
|
|
31
31
|
|
|
@@ -123,7 +123,7 @@ describe('consoleServer pure helpers', () => {
|
|
|
123
123
|
});
|
|
124
124
|
});
|
|
125
125
|
|
|
126
|
-
describe('
|
|
126
|
+
describe('webServer integration', () => {
|
|
127
127
|
const testToken = 'integration-test-token-value';
|
|
128
128
|
|
|
129
129
|
const requestServer = (
|
|
@@ -175,7 +175,7 @@ describe('consoleServer integration', () => {
|
|
|
175
175
|
|
|
176
176
|
it('starts on an ephemeral port and closes gracefully', async () => {
|
|
177
177
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'console-server-'));
|
|
178
|
-
const server = await
|
|
178
|
+
const server = await startWebServer({
|
|
179
179
|
accessToken: testToken,
|
|
180
180
|
uiDistDir: path.join(tmpDir, 'ui-dist'),
|
|
181
181
|
consoleDataOutputDir: null,
|
|
@@ -192,7 +192,7 @@ describe('consoleServer integration', () => {
|
|
|
192
192
|
|
|
193
193
|
it('serves the placeholder index without a token when ui-dist is absent', async () => {
|
|
194
194
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'console-server-'));
|
|
195
|
-
const server = await
|
|
195
|
+
const server = await startWebServer({
|
|
196
196
|
accessToken: testToken,
|
|
197
197
|
uiDistDir: path.join(tmpDir, 'missing-ui-dist'),
|
|
198
198
|
consoleDataOutputDir: null,
|
|
@@ -218,7 +218,7 @@ describe('consoleServer integration', () => {
|
|
|
218
218
|
|
|
219
219
|
it('returns 404 for a missing non-index file when ui-dist is absent', async () => {
|
|
220
220
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'console-server-'));
|
|
221
|
-
const server = await
|
|
221
|
+
const server = await startWebServer({
|
|
222
222
|
accessToken: testToken,
|
|
223
223
|
uiDistDir: path.join(tmpDir, 'missing-ui-dist'),
|
|
224
224
|
consoleDataOutputDir: null,
|
|
@@ -248,7 +248,7 @@ describe('consoleServer integration', () => {
|
|
|
248
248
|
path.join(uiDistDir, 'assets', 'app.js'),
|
|
249
249
|
'console.log("app");',
|
|
250
250
|
);
|
|
251
|
-
const server = await
|
|
251
|
+
const server = await startWebServer({
|
|
252
252
|
accessToken: testToken,
|
|
253
253
|
uiDistDir,
|
|
254
254
|
consoleDataOutputDir: null,
|
|
@@ -280,7 +280,7 @@ describe('consoleServer integration', () => {
|
|
|
280
280
|
path.join(uiDistDir, 'index.html'),
|
|
281
281
|
'<!DOCTYPE html><title>spa</title><div id="root"></div>',
|
|
282
282
|
);
|
|
283
|
-
const server = await
|
|
283
|
+
const server = await startWebServer({
|
|
284
284
|
accessToken: testToken,
|
|
285
285
|
uiDistDir,
|
|
286
286
|
consoleDataOutputDir: null,
|
|
@@ -309,7 +309,7 @@ describe('consoleServer integration', () => {
|
|
|
309
309
|
|
|
310
310
|
it('serves the placeholder index for per-project routes when ui-dist is absent', async () => {
|
|
311
311
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'console-server-'));
|
|
312
|
-
const server = await
|
|
312
|
+
const server = await startWebServer({
|
|
313
313
|
accessToken: testToken,
|
|
314
314
|
uiDistDir: path.join(tmpDir, 'missing-ui-dist'),
|
|
315
315
|
consoleDataOutputDir: null,
|
|
@@ -332,7 +332,7 @@ describe('consoleServer integration', () => {
|
|
|
332
332
|
const uiDistDir = path.join(tmpDir, 'ui-dist');
|
|
333
333
|
fs.mkdirSync(uiDistDir, { recursive: true });
|
|
334
334
|
fs.writeFileSync(path.join(uiDistDir, '.env'), 'SECRET=should-not-serve');
|
|
335
|
-
const server = await
|
|
335
|
+
const server = await startWebServer({
|
|
336
336
|
accessToken: testToken,
|
|
337
337
|
uiDistDir,
|
|
338
338
|
consoleDataOutputDir: null,
|
|
@@ -358,7 +358,7 @@ describe('consoleServer integration', () => {
|
|
|
358
358
|
|
|
359
359
|
it('rejects .json and /api/* without a token and passes the gate with a valid token', async () => {
|
|
360
360
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'console-server-'));
|
|
361
|
-
const server = await
|
|
361
|
+
const server = await startWebServer({
|
|
362
362
|
accessToken: testToken,
|
|
363
363
|
uiDistDir: path.join(tmpDir, 'ui-dist'),
|
|
364
364
|
consoleDataOutputDir: null,
|
|
@@ -403,7 +403,7 @@ describe('consoleServer integration', () => {
|
|
|
403
403
|
});
|
|
404
404
|
});
|
|
405
405
|
|
|
406
|
-
describe('
|
|
406
|
+
describe('webServer new routes integration', () => {
|
|
407
407
|
const testToken = 'integration-test-token-value';
|
|
408
408
|
|
|
409
409
|
const closeServer = (server: http.Server): Promise<void> =>
|
|
@@ -496,7 +496,7 @@ describe('consoleServer new routes integration', () => {
|
|
|
496
496
|
path.join(listDir, '.done.json'),
|
|
497
497
|
JSON.stringify({ projectItemIds: ['PVTI_drop'] }),
|
|
498
498
|
);
|
|
499
|
-
const server = await
|
|
499
|
+
const server = await startWebServer({
|
|
500
500
|
accessToken: testToken,
|
|
501
501
|
uiDistDir: path.join(tmpDir, 'ui-dist'),
|
|
502
502
|
consoleDataOutputDir: dataDir,
|
|
@@ -533,7 +533,7 @@ describe('consoleServer new routes integration', () => {
|
|
|
533
533
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'console-server-'));
|
|
534
534
|
const issueRepository = mock<IssueRepository>();
|
|
535
535
|
issueRepository.getIssueOrPullRequestBody.mockResolvedValue('body text');
|
|
536
|
-
const server = await
|
|
536
|
+
const server = await startWebServer({
|
|
537
537
|
accessToken: testToken,
|
|
538
538
|
uiDistDir: path.join(tmpDir, 'ui-dist'),
|
|
539
539
|
consoleDataOutputDir: null,
|
|
@@ -567,7 +567,7 @@ describe('consoleServer new routes integration', () => {
|
|
|
567
567
|
createdAt: new Date('2026-06-18T03:21:00.000Z'),
|
|
568
568
|
},
|
|
569
569
|
]);
|
|
570
|
-
const server = await
|
|
570
|
+
const server = await startWebServer({
|
|
571
571
|
accessToken: testToken,
|
|
572
572
|
uiDistDir: path.join(tmpDir, 'ui-dist'),
|
|
573
573
|
consoleDataOutputDir: null,
|
|
@@ -616,7 +616,7 @@ describe('consoleServer new routes integration', () => {
|
|
|
616
616
|
...mock<Issue>(),
|
|
617
617
|
itemId: 'PVTI_loaded',
|
|
618
618
|
});
|
|
619
|
-
const server = await
|
|
619
|
+
const server = await startWebServer({
|
|
620
620
|
accessToken: testToken,
|
|
621
621
|
uiDistDir: path.join(tmpDir, 'ui-dist'),
|
|
622
622
|
consoleDataOutputDir: dataDir,
|
|
@@ -662,7 +662,7 @@ describe('consoleServer new routes integration', () => {
|
|
|
662
662
|
issueRepository.approvePullRequest.mockRejectedValue(
|
|
663
663
|
new Error('Failed to approve PR https://github.com/o/r/pull/1: HTTP 422'),
|
|
664
664
|
);
|
|
665
|
-
const server = await
|
|
665
|
+
const server = await startWebServer({
|
|
666
666
|
accessToken: testToken,
|
|
667
667
|
uiDistDir: path.join(tmpDir, 'ui-dist'),
|
|
668
668
|
consoleDataOutputDir: null,
|
|
@@ -698,7 +698,7 @@ describe('consoleServer new routes integration', () => {
|
|
|
698
698
|
it('rejects an operation api with a malformed json body', async () => {
|
|
699
699
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'console-server-'));
|
|
700
700
|
const issueRepository = mock<IssueRepository>();
|
|
701
|
-
const server = await
|
|
701
|
+
const server = await startWebServer({
|
|
702
702
|
accessToken: testToken,
|
|
703
703
|
uiDistDir: path.join(tmpDir, 'ui-dist'),
|
|
704
704
|
consoleDataOutputDir: null,
|
|
@@ -749,7 +749,7 @@ describe('consoleServer new routes integration', () => {
|
|
|
749
749
|
|
|
750
750
|
it('returns 404 for a read api when no repository is injected', async () => {
|
|
751
751
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'console-server-'));
|
|
752
|
-
const server = await
|
|
752
|
+
const server = await startWebServer({
|
|
753
753
|
accessToken: testToken,
|
|
754
754
|
uiDistDir: path.join(tmpDir, 'ui-dist'),
|
|
755
755
|
consoleDataOutputDir: null,
|
|
@@ -809,7 +809,7 @@ describe('resolveFlatInTmuxFilePath', () => {
|
|
|
809
809
|
});
|
|
810
810
|
});
|
|
811
811
|
|
|
812
|
-
describe('
|
|
812
|
+
describe('webServer flat in-tmux-by-human route integration', () => {
|
|
813
813
|
const testToken = 'integration-test-token-value';
|
|
814
814
|
|
|
815
815
|
const requestServer = (
|
|
@@ -878,7 +878,7 @@ describe('consoleServer flat in-tmux-by-human route integration', () => {
|
|
|
878
878
|
fs.writeFileSync(path.join(inTmuxDataDir, 'index.v4.json'), indexV4Raw);
|
|
879
879
|
fs.writeFileSync(path.join(inTmuxDataDir, 'index.v3.json'), indexV3Raw);
|
|
880
880
|
fs.writeFileSync(path.join(tmpDir, 'secret.json'), '{"secret":true}');
|
|
881
|
-
const server = await
|
|
881
|
+
const server = await startWebServer({
|
|
882
882
|
accessToken: testToken,
|
|
883
883
|
uiDistDir: path.join(tmpDir, 'ui-dist'),
|
|
884
884
|
consoleDataOutputDir: null,
|
|
@@ -992,7 +992,7 @@ describe('consoleServer flat in-tmux-by-human route integration', () => {
|
|
|
992
992
|
|
|
993
993
|
it('returns 404 for the flat route when inTmuxDataDir is null', async () => {
|
|
994
994
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'console-server-'));
|
|
995
|
-
const server = await
|
|
995
|
+
const server = await startWebServer({
|
|
996
996
|
accessToken: testToken,
|
|
997
997
|
uiDistDir: path.join(tmpDir, 'ui-dist'),
|
|
998
998
|
consoleDataOutputDir: null,
|
|
@@ -1030,7 +1030,7 @@ describe('resolveDashboardFilePath', () => {
|
|
|
1030
1030
|
});
|
|
1031
1031
|
});
|
|
1032
1032
|
|
|
1033
|
-
describe('
|
|
1033
|
+
describe('webServer dashboard /tdpm.txt route integration', () => {
|
|
1034
1034
|
const testToken = 'integration-test-token-value';
|
|
1035
1035
|
|
|
1036
1036
|
const requestServer = (
|
|
@@ -1094,7 +1094,7 @@ describe('consoleServer dashboard /tdpm.txt route integration', () => {
|
|
|
1094
1094
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'console-server-'));
|
|
1095
1095
|
fs.writeFileSync(path.join(tmpDir, 'tdpm.txt'), dashboardRaw);
|
|
1096
1096
|
fs.writeFileSync(path.join(tmpDir, 'secret.txt'), 'secret content');
|
|
1097
|
-
const server = await
|
|
1097
|
+
const server = await startWebServer({
|
|
1098
1098
|
accessToken: testToken,
|
|
1099
1099
|
uiDistDir: path.join(tmpDir, 'ui-dist'),
|
|
1100
1100
|
consoleDataOutputDir: null,
|
|
@@ -1125,7 +1125,7 @@ describe('consoleServer dashboard /tdpm.txt route integration', () => {
|
|
|
1125
1125
|
|
|
1126
1126
|
it('returns 404 for /tdpm.txt when the file is absent', async () => {
|
|
1127
1127
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'console-server-'));
|
|
1128
|
-
const server = await
|
|
1128
|
+
const server = await startWebServer({
|
|
1129
1129
|
accessToken: testToken,
|
|
1130
1130
|
uiDistDir: path.join(tmpDir, 'ui-dist'),
|
|
1131
1131
|
consoleDataOutputDir: null,
|
|
@@ -1146,7 +1146,7 @@ describe('consoleServer dashboard /tdpm.txt route integration', () => {
|
|
|
1146
1146
|
const { server, tmpDir } = await (async () => {
|
|
1147
1147
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'console-server-'));
|
|
1148
1148
|
fs.writeFileSync(path.join(dir, 'tdpm.txt'), dashboardRaw);
|
|
1149
|
-
const srv = await
|
|
1149
|
+
const srv = await startWebServer({
|
|
1150
1150
|
accessToken: testToken,
|
|
1151
1151
|
uiDistDir: path.join(dir, 'ui-dist'),
|
|
1152
1152
|
consoleDataOutputDir: null,
|
|
@@ -1177,7 +1177,7 @@ describe('consoleServer dashboard /tdpm.txt route integration', () => {
|
|
|
1177
1177
|
});
|
|
1178
1178
|
});
|
|
1179
1179
|
|
|
1180
|
-
describe('
|
|
1180
|
+
describe('webServer image proxy', () => {
|
|
1181
1181
|
const testToken = 'image-proxy-token-value';
|
|
1182
1182
|
const githubToken = 'gh-token-value';
|
|
1183
1183
|
const allowedUrl = 'https://github.com/user-attachments/assets/abc-123';
|
|
@@ -1244,7 +1244,7 @@ describe('consoleServer image proxy', () => {
|
|
|
1244
1244
|
): Promise<{ server: http.Server; tmpDir: string }> =>
|
|
1245
1245
|
(async () => {
|
|
1246
1246
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'console-server-'));
|
|
1247
|
-
const server = await
|
|
1247
|
+
const server = await startWebServer({
|
|
1248
1248
|
accessToken: testToken,
|
|
1249
1249
|
uiDistDir: path.join(tmpDir, 'ui-dist'),
|
|
1250
1250
|
consoleDataOutputDir: null,
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
} from './consoleOperationApi';
|
|
28
28
|
import { ImageFetcher, fetchProxiedImage } from './consoleImageProxy';
|
|
29
29
|
|
|
30
|
-
export const
|
|
30
|
+
export const DEFAULT_WEB_PORT = 9981;
|
|
31
31
|
|
|
32
32
|
export const CONSOLE_TOKEN_HEADER = 'x-pv-token';
|
|
33
33
|
|
|
@@ -152,7 +152,7 @@ const readStaticFile = (filePath: string): Buffer | null => {
|
|
|
152
152
|
}
|
|
153
153
|
};
|
|
154
154
|
|
|
155
|
-
export type
|
|
155
|
+
export type WebServerOptions = {
|
|
156
156
|
accessToken: string;
|
|
157
157
|
uiDistDir: string;
|
|
158
158
|
consoleDataOutputDir: string | null;
|
|
@@ -236,7 +236,7 @@ const serveBootstrapIndex = (response: http.ServerResponse): void => {
|
|
|
236
236
|
};
|
|
237
237
|
|
|
238
238
|
const serveIndexHtml = (
|
|
239
|
-
options:
|
|
239
|
+
options: WebServerOptions,
|
|
240
240
|
response: http.ServerResponse,
|
|
241
241
|
): void => {
|
|
242
242
|
const indexFilePath = resolveStaticFilePath(options.uiDistDir, '/index.html');
|
|
@@ -279,7 +279,7 @@ const sendImage = (
|
|
|
279
279
|
};
|
|
280
280
|
|
|
281
281
|
const handleImageProxy = async (
|
|
282
|
-
options:
|
|
282
|
+
options: WebServerOptions,
|
|
283
283
|
response: http.ServerResponse,
|
|
284
284
|
searchParams: URLSearchParams,
|
|
285
285
|
): Promise<void> => {
|
|
@@ -342,7 +342,7 @@ const parseRequestBody = (raw: string): Record<string, unknown> | null => {
|
|
|
342
342
|
};
|
|
343
343
|
|
|
344
344
|
const handleReadApi = async (
|
|
345
|
-
options:
|
|
345
|
+
options: WebServerOptions,
|
|
346
346
|
requestPath: string,
|
|
347
347
|
searchParams: URLSearchParams,
|
|
348
348
|
): Promise<{ statusCode: number; body: unknown } | null> => {
|
|
@@ -402,7 +402,7 @@ const dispatchOperation = (
|
|
|
402
402
|
};
|
|
403
403
|
|
|
404
404
|
const handleOperationApi = async (
|
|
405
|
-
options:
|
|
405
|
+
options: WebServerOptions,
|
|
406
406
|
requestPath: string,
|
|
407
407
|
body: Record<string, unknown>,
|
|
408
408
|
): Promise<{ statusCode: number; body: unknown } | null> => {
|
|
@@ -432,7 +432,7 @@ const handleOperationApi = async (
|
|
|
432
432
|
};
|
|
433
433
|
|
|
434
434
|
const handleTokenedRequest = async (
|
|
435
|
-
options:
|
|
435
|
+
options: WebServerOptions,
|
|
436
436
|
request: http.IncomingMessage,
|
|
437
437
|
response: http.ServerResponse,
|
|
438
438
|
requestPath: string,
|
|
@@ -525,8 +525,8 @@ const handleTokenedRequest = async (
|
|
|
525
525
|
sendNotFound(response);
|
|
526
526
|
};
|
|
527
527
|
|
|
528
|
-
export const
|
|
529
|
-
options:
|
|
528
|
+
export const handleWebRequest = async (
|
|
529
|
+
options: WebServerOptions,
|
|
530
530
|
request: http.IncomingMessage,
|
|
531
531
|
response: http.ServerResponse,
|
|
532
532
|
): Promise<void> => {
|
|
@@ -624,25 +624,23 @@ const sendInternalServerError = (response: http.ServerResponse): void => {
|
|
|
624
624
|
response.end('Internal Server Error');
|
|
625
625
|
};
|
|
626
626
|
|
|
627
|
-
export const
|
|
628
|
-
options: ConsoleServerOptions,
|
|
629
|
-
): http.Server =>
|
|
627
|
+
export const createWebServer = (options: WebServerOptions): http.Server =>
|
|
630
628
|
http.createServer((request, response) => {
|
|
631
|
-
|
|
629
|
+
handleWebRequest(options, request, response).catch((error) => {
|
|
632
630
|
console.error('console request failed', error);
|
|
633
631
|
sendInternalServerError(response);
|
|
634
632
|
});
|
|
635
633
|
});
|
|
636
634
|
|
|
637
|
-
export type
|
|
635
|
+
export type StartWebServerOptions = WebServerOptions & {
|
|
638
636
|
port: number;
|
|
639
637
|
};
|
|
640
638
|
|
|
641
|
-
export const
|
|
642
|
-
options:
|
|
639
|
+
export const startWebServer = (
|
|
640
|
+
options: StartWebServerOptions,
|
|
643
641
|
): Promise<http.Server> =>
|
|
644
642
|
new Promise((resolve, reject) => {
|
|
645
|
-
const server =
|
|
643
|
+
const server = createWebServer(options);
|
|
646
644
|
server.once('error', reject);
|
|
647
645
|
server.listen(options.port, () => {
|
|
648
646
|
server.removeListener('error', reject);
|
|
@@ -3,6 +3,9 @@ import TYPIA from 'typia';
|
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import { writeSituationFile } from './situationFileWriter';
|
|
5
5
|
import { writeConsoleLists } from './consoleListsWriter';
|
|
6
|
+
import { writeDashboardRow } from './dashboardRowWriter';
|
|
7
|
+
import { writeMachineStatus } from './machineStatusWriter';
|
|
8
|
+
import { writeTokenStatus } from './tokenStatusWriter';
|
|
6
9
|
import { writeInTmuxByHumanData } from './inTmuxByHumanDataWriter';
|
|
7
10
|
import { reconcileInTmuxByHumanSessions } from './inTmuxByHumanSessionReconciler';
|
|
8
11
|
import { writeRotationOrderFile } from './rotationOrderFileWriter';
|
|
@@ -55,6 +58,8 @@ import {
|
|
|
55
58
|
PREPARATION_STATUS_NAME,
|
|
56
59
|
} from '../../../domain/entities/WorkflowStatus';
|
|
57
60
|
|
|
61
|
+
const DEFAULT_DASHBOARD_DATA_DIR: string | null = null;
|
|
62
|
+
|
|
58
63
|
export class HandleScheduledEventUseCaseHandler {
|
|
59
64
|
handle = async (
|
|
60
65
|
configFilePath: string,
|
|
@@ -74,6 +79,7 @@ export class HandleScheduledEventUseCaseHandler {
|
|
|
74
79
|
allowedIssueAuthors?: string | string[] | null;
|
|
75
80
|
claudeCodeOauthTokenListJsonPath?: string;
|
|
76
81
|
consoleDataOutputDir?: string;
|
|
82
|
+
dashboardDataDir?: string;
|
|
77
83
|
workflowBlockerStoryName?: string;
|
|
78
84
|
inTmuxDataOutputDir?: string;
|
|
79
85
|
inTmuxConsoleBaseUrl?: string;
|
|
@@ -416,6 +422,52 @@ export class HandleScheduledEventUseCaseHandler {
|
|
|
416
422
|
);
|
|
417
423
|
}
|
|
418
424
|
|
|
425
|
+
const dashboardDataDir =
|
|
426
|
+
mergedInput.dashboardDataDir ?? DEFAULT_DASHBOARD_DATA_DIR;
|
|
427
|
+
|
|
428
|
+
try {
|
|
429
|
+
writeDashboardRow({
|
|
430
|
+
dashboardDataDir,
|
|
431
|
+
pjcode: input.projectName,
|
|
432
|
+
assigneeLogin: input.manager,
|
|
433
|
+
issues: result.issues,
|
|
434
|
+
});
|
|
435
|
+
} catch (error) {
|
|
436
|
+
console.error(
|
|
437
|
+
`Failed to write dashboard row: ${
|
|
438
|
+
error instanceof Error ? error.message : String(error)
|
|
439
|
+
}`,
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
try {
|
|
444
|
+
await writeMachineStatus({
|
|
445
|
+
dashboardDataDir,
|
|
446
|
+
allIssuesCacheDir: `${cachePath}/allIssues-${result.project.id}`,
|
|
447
|
+
});
|
|
448
|
+
} catch (error) {
|
|
449
|
+
console.error(
|
|
450
|
+
`Failed to write machine status: ${
|
|
451
|
+
error instanceof Error ? error.message : String(error)
|
|
452
|
+
}`,
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
try {
|
|
457
|
+
writeTokenStatus({
|
|
458
|
+
dashboardDataDir,
|
|
459
|
+
tokenListJsonPath:
|
|
460
|
+
mergedInput.claudeCodeOauthTokenListJsonPath ?? null,
|
|
461
|
+
issues: result.issues,
|
|
462
|
+
});
|
|
463
|
+
} catch (error) {
|
|
464
|
+
console.error(
|
|
465
|
+
`Failed to write token status: ${
|
|
466
|
+
error instanceof Error ? error.message : String(error)
|
|
467
|
+
}`,
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
419
471
|
const inTmuxNow = new Date();
|
|
420
472
|
|
|
421
473
|
try {
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { Issue } from '../../../domain/entities/Issue';
|
|
5
|
+
import { DashboardRowFile, writeDashboardRow } from './dashboardRowWriter';
|
|
6
|
+
|
|
7
|
+
const readJson = (filePath: string): unknown =>
|
|
8
|
+
JSON.parse(fs.readFileSync(filePath, 'utf8')) as unknown;
|
|
9
|
+
|
|
10
|
+
const ASSIGNEE = 'HiromiShikata';
|
|
11
|
+
|
|
12
|
+
let issueCounter = 0;
|
|
13
|
+
const makeIssue = (overrides: Partial<Issue>): Issue => {
|
|
14
|
+
issueCounter += 1;
|
|
15
|
+
return {
|
|
16
|
+
nameWithOwner: 'demo/repo',
|
|
17
|
+
number: issueCounter,
|
|
18
|
+
title: `Issue ${issueCounter}`,
|
|
19
|
+
state: 'OPEN',
|
|
20
|
+
status: null,
|
|
21
|
+
story: null,
|
|
22
|
+
nextActionDate: null,
|
|
23
|
+
nextActionHour: null,
|
|
24
|
+
estimationMinutes: null,
|
|
25
|
+
dependedIssueUrls: [],
|
|
26
|
+
completionDate50PercentConfidence: null,
|
|
27
|
+
url: `https://github.com/demo/repo/issues/${issueCounter}`,
|
|
28
|
+
assignees: [ASSIGNEE],
|
|
29
|
+
labels: [],
|
|
30
|
+
org: 'demo',
|
|
31
|
+
repo: 'repo',
|
|
32
|
+
body: '',
|
|
33
|
+
itemId: `item-${issueCounter}`,
|
|
34
|
+
isPr: false,
|
|
35
|
+
isInProgress: false,
|
|
36
|
+
isClosed: false,
|
|
37
|
+
createdAt: new Date('2026-06-13T08:18:45.000Z'),
|
|
38
|
+
author: 'someone',
|
|
39
|
+
closingIssueReferenceUrls: [],
|
|
40
|
+
...overrides,
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
describe('writeDashboardRow', () => {
|
|
45
|
+
let dir: string;
|
|
46
|
+
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
issueCounter = 0;
|
|
49
|
+
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'dashboard-row-'));
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
afterEach(() => {
|
|
53
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('writes a per-project row file with counts, pjcode and capturedAt', () => {
|
|
57
|
+
writeDashboardRow({
|
|
58
|
+
dashboardDataDir: dir,
|
|
59
|
+
pjcode: 'um',
|
|
60
|
+
assigneeLogin: ASSIGNEE,
|
|
61
|
+
issues: [
|
|
62
|
+
makeIssue({ status: 'Unread' }),
|
|
63
|
+
makeIssue({ status: 'Awaiting Quality Check' }),
|
|
64
|
+
],
|
|
65
|
+
generatedAt: '2026-06-26T12:00:00.000Z',
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const written = readJson(path.join(dir, 'projects', 'um.json'));
|
|
69
|
+
const expected: DashboardRowFile = {
|
|
70
|
+
pjcode: 'um',
|
|
71
|
+
capturedAt: '2026-06-26T12:00:00.000Z',
|
|
72
|
+
unread: 1,
|
|
73
|
+
todo: 0,
|
|
74
|
+
qc: 1,
|
|
75
|
+
fail: 0,
|
|
76
|
+
pr: 0,
|
|
77
|
+
ws: 0,
|
|
78
|
+
dep: 0,
|
|
79
|
+
blocker: 0,
|
|
80
|
+
};
|
|
81
|
+
expect(written).toEqual(expected);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('is a no-op when dashboardDataDir is unset', () => {
|
|
85
|
+
writeDashboardRow({
|
|
86
|
+
dashboardDataDir: null,
|
|
87
|
+
pjcode: 'um',
|
|
88
|
+
assigneeLogin: ASSIGNEE,
|
|
89
|
+
issues: [makeIssue({ status: 'Unread' })],
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(fs.readdirSync(dir)).toEqual([]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('is a no-op when pjcode or assigneeLogin is missing', () => {
|
|
96
|
+
writeDashboardRow({
|
|
97
|
+
dashboardDataDir: dir,
|
|
98
|
+
pjcode: null,
|
|
99
|
+
assigneeLogin: ASSIGNEE,
|
|
100
|
+
issues: [makeIssue({ status: 'Unread' })],
|
|
101
|
+
});
|
|
102
|
+
writeDashboardRow({
|
|
103
|
+
dashboardDataDir: dir,
|
|
104
|
+
pjcode: 'um',
|
|
105
|
+
assigneeLogin: null,
|
|
106
|
+
issues: [makeIssue({ status: 'Unread' })],
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
expect(fs.readdirSync(dir)).toEqual([]);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import type { Issue } from '../../../domain/entities/Issue';
|
|
4
|
+
import {
|
|
5
|
+
DashboardRow,
|
|
6
|
+
GenerateDashboardRowUseCase,
|
|
7
|
+
} from '../../../domain/usecases/dashboard/GenerateDashboardRowUseCase';
|
|
8
|
+
|
|
9
|
+
export type DashboardRowWriterParams = {
|
|
10
|
+
dashboardDataDir: string | null | undefined;
|
|
11
|
+
pjcode: string | null | undefined;
|
|
12
|
+
assigneeLogin: string | null | undefined;
|
|
13
|
+
issues: Issue[];
|
|
14
|
+
generatedAt?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type DashboardRowFile = DashboardRow & {
|
|
18
|
+
pjcode: string;
|
|
19
|
+
capturedAt: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const writeJsonAtomic = (filePath: string, data: unknown): void => {
|
|
23
|
+
const dir = path.dirname(filePath);
|
|
24
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
25
|
+
const tmpPath = `${filePath}.tmp`;
|
|
26
|
+
fs.writeFileSync(tmpPath, JSON.stringify(data));
|
|
27
|
+
fs.renameSync(tmpPath, filePath);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const writeDashboardRow = (params: DashboardRowWriterParams): void => {
|
|
31
|
+
const { dashboardDataDir, pjcode, assigneeLogin, issues } = params;
|
|
32
|
+
if (!dashboardDataDir || !pjcode || !assigneeLogin) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const row: DashboardRow = new GenerateDashboardRowUseCase().run({
|
|
37
|
+
issues,
|
|
38
|
+
assigneeLogin,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const file: DashboardRowFile = {
|
|
42
|
+
pjcode,
|
|
43
|
+
capturedAt: params.generatedAt ?? new Date().toISOString(),
|
|
44
|
+
...row,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
writeJsonAtomic(
|
|
48
|
+
path.join(dashboardDataDir, 'projects', `${pjcode}.json`),
|
|
49
|
+
file,
|
|
50
|
+
);
|
|
51
|
+
};
|