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.
Files changed (74) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +8 -7
  3. package/bin/adapter/entry-points/cli/index.js +21 -13
  4. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  5. package/bin/adapter/entry-points/console/{consoleServer.js → webServer.js} +11 -11
  6. package/bin/adapter/entry-points/console/webServer.js.map +1 -0
  7. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +41 -2
  8. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  9. package/bin/adapter/entry-points/handlers/dashboardRowWriter.js +34 -0
  10. package/bin/adapter/entry-points/handlers/dashboardRowWriter.js.map +1 -0
  11. package/bin/adapter/entry-points/handlers/machineStatusWriter.js +60 -0
  12. package/bin/adapter/entry-points/handlers/machineStatusWriter.js.map +1 -0
  13. package/bin/adapter/entry-points/handlers/tokenStatusWriter.js +98 -0
  14. package/bin/adapter/entry-points/handlers/tokenStatusWriter.js.map +1 -0
  15. package/bin/adapter/proxy/RateLimitCache.js +3 -0
  16. package/bin/adapter/proxy/RateLimitCache.js.map +1 -1
  17. package/bin/adapter/repositories/ProcHostMetricsRepository.js +136 -0
  18. package/bin/adapter/repositories/ProcHostMetricsRepository.js.map +1 -0
  19. package/bin/adapter/repositories/ProcTakeOwnershipSpawnRepository.js +131 -0
  20. package/bin/adapter/repositories/ProcTakeOwnershipSpawnRepository.js.map +1 -0
  21. package/bin/domain/usecases/adapter-interfaces/TakeOwnershipSpawnRepository.js +3 -0
  22. package/bin/domain/usecases/adapter-interfaces/TakeOwnershipSpawnRepository.js.map +1 -0
  23. package/bin/domain/usecases/dashboard/GenerateDashboardRowUseCase.js +38 -0
  24. package/bin/domain/usecases/dashboard/GenerateDashboardRowUseCase.js.map +1 -0
  25. package/bin/domain/usecases/dashboard/GenerateTokenStatusUseCase.js +115 -0
  26. package/bin/domain/usecases/dashboard/GenerateTokenStatusUseCase.js.map +1 -0
  27. package/package.json +1 -1
  28. package/src/adapter/entry-points/cli/index.test.ts +74 -21
  29. package/src/adapter/entry-points/cli/index.ts +146 -136
  30. package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +2 -2
  31. package/src/adapter/entry-points/console/{consoleServer.test.ts → webServer.test.ts} +32 -32
  32. package/src/adapter/entry-points/console/{consoleServer.ts → webServer.ts} +15 -17
  33. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +52 -0
  34. package/src/adapter/entry-points/handlers/dashboardRowWriter.test.ts +111 -0
  35. package/src/adapter/entry-points/handlers/dashboardRowWriter.ts +51 -0
  36. package/src/adapter/entry-points/handlers/machineStatusWriter.test.ts +100 -0
  37. package/src/adapter/entry-points/handlers/machineStatusWriter.ts +79 -0
  38. package/src/adapter/entry-points/handlers/tokenStatusWriter.test.ts +176 -0
  39. package/src/adapter/entry-points/handlers/tokenStatusWriter.ts +139 -0
  40. package/src/adapter/proxy/RateLimitCache.test.ts +32 -0
  41. package/src/adapter/proxy/RateLimitCache.ts +6 -0
  42. package/src/adapter/repositories/ProcHostMetricsRepository.test.ts +135 -0
  43. package/src/adapter/repositories/ProcHostMetricsRepository.ts +136 -0
  44. package/src/adapter/repositories/ProcTakeOwnershipSpawnRepository.test.ts +130 -0
  45. package/src/adapter/repositories/ProcTakeOwnershipSpawnRepository.ts +118 -0
  46. package/src/domain/usecases/adapter-interfaces/TakeOwnershipSpawnRepository.ts +8 -0
  47. package/src/domain/usecases/dashboard/GenerateDashboardRowUseCase.test.ts +159 -0
  48. package/src/domain/usecases/dashboard/GenerateDashboardRowUseCase.ts +72 -0
  49. package/src/domain/usecases/dashboard/GenerateTokenStatusUseCase.test.ts +209 -0
  50. package/src/domain/usecases/dashboard/GenerateTokenStatusUseCase.ts +195 -0
  51. package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
  52. package/types/adapter/entry-points/console/{consoleServer.d.ts → webServer.d.ts} +7 -7
  53. package/types/adapter/entry-points/console/webServer.d.ts.map +1 -0
  54. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
  55. package/types/adapter/entry-points/handlers/dashboardRowWriter.d.ts +15 -0
  56. package/types/adapter/entry-points/handlers/dashboardRowWriter.d.ts.map +1 -0
  57. package/types/adapter/entry-points/handlers/machineStatusWriter.d.ts +16 -0
  58. package/types/adapter/entry-points/handlers/machineStatusWriter.d.ts.map +1 -0
  59. package/types/adapter/entry-points/handlers/tokenStatusWriter.d.ts +21 -0
  60. package/types/adapter/entry-points/handlers/tokenStatusWriter.d.ts.map +1 -0
  61. package/types/adapter/proxy/RateLimitCache.d.ts +2 -0
  62. package/types/adapter/proxy/RateLimitCache.d.ts.map +1 -1
  63. package/types/adapter/repositories/ProcHostMetricsRepository.d.ts +23 -0
  64. package/types/adapter/repositories/ProcHostMetricsRepository.d.ts.map +1 -0
  65. package/types/adapter/repositories/ProcTakeOwnershipSpawnRepository.d.ts +11 -0
  66. package/types/adapter/repositories/ProcTakeOwnershipSpawnRepository.d.ts.map +1 -0
  67. package/types/domain/usecases/adapter-interfaces/TakeOwnershipSpawnRepository.d.ts +8 -0
  68. package/types/domain/usecases/adapter-interfaces/TakeOwnershipSpawnRepository.d.ts.map +1 -0
  69. package/types/domain/usecases/dashboard/GenerateDashboardRowUseCase.d.ts +19 -0
  70. package/types/domain/usecases/dashboard/GenerateDashboardRowUseCase.d.ts.map +1 -0
  71. package/types/domain/usecases/dashboard/GenerateTokenStatusUseCase.d.ts +53 -0
  72. package/types/domain/usecases/dashboard/GenerateTokenStatusUseCase.d.ts.map +1 -0
  73. package/bin/adapter/entry-points/console/consoleServer.js.map +0 -1
  74. 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
- DEFAULT_CONSOLE_PORT,
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
- startConsoleServer,
17
- } from './consoleServer';
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('consoleServer pure helpers', () => {
26
- describe('DEFAULT_CONSOLE_PORT', () => {
25
+ describe('webServer pure helpers', () => {
26
+ describe('DEFAULT_WEB_PORT', () => {
27
27
  it('is 9981', () => {
28
- expect(DEFAULT_CONSOLE_PORT).toBe(9981);
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('consoleServer integration', () => {
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 startConsoleServer({
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 startConsoleServer({
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 startConsoleServer({
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 startConsoleServer({
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 startConsoleServer({
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 startConsoleServer({
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 startConsoleServer({
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 startConsoleServer({
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('consoleServer new routes integration', () => {
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 startConsoleServer({
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 startConsoleServer({
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 startConsoleServer({
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 startConsoleServer({
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 startConsoleServer({
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 startConsoleServer({
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 startConsoleServer({
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('consoleServer flat in-tmux-by-human route integration', () => {
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 startConsoleServer({
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 startConsoleServer({
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('consoleServer dashboard /tdpm.txt route integration', () => {
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 startConsoleServer({
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 startConsoleServer({
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 startConsoleServer({
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('consoleServer image proxy', () => {
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 startConsoleServer({
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 DEFAULT_CONSOLE_PORT = 9981;
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 ConsoleServerOptions = {
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: ConsoleServerOptions,
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: ConsoleServerOptions,
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: ConsoleServerOptions,
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: ConsoleServerOptions,
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: ConsoleServerOptions,
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 handleConsoleRequest = async (
529
- options: ConsoleServerOptions,
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 createConsoleServer = (
628
- options: ConsoleServerOptions,
629
- ): http.Server =>
627
+ export const createWebServer = (options: WebServerOptions): http.Server =>
630
628
  http.createServer((request, response) => {
631
- handleConsoleRequest(options, request, response).catch((error) => {
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 StartConsoleServerOptions = ConsoleServerOptions & {
635
+ export type StartWebServerOptions = WebServerOptions & {
638
636
  port: number;
639
637
  };
640
638
 
641
- export const startConsoleServer = (
642
- options: StartConsoleServerOptions,
639
+ export const startWebServer = (
640
+ options: StartWebServerOptions,
643
641
  ): Promise<http.Server> =>
644
642
  new Promise((resolve, reject) => {
645
- const server = createConsoleServer(options);
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
+ };