github-issue-tower-defence-management 1.89.0 → 1.91.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 (160) hide show
  1. package/.github/CODEOWNERS +1 -2
  2. package/CHANGELOG.md +14 -0
  3. package/README.md +15 -2
  4. package/bin/adapter/entry-points/cli/index.js +16 -12
  5. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  6. package/bin/adapter/entry-points/cli/projectConfig.js +2 -0
  7. package/bin/adapter/entry-points/cli/projectConfig.js.map +1 -1
  8. package/bin/adapter/entry-points/console/consoleOperationApi.js +54 -27
  9. package/bin/adapter/entry-points/console/consoleOperationApi.js.map +1 -1
  10. package/bin/adapter/entry-points/console/consoleProjectResolver.js +38 -0
  11. package/bin/adapter/entry-points/console/consoleProjectResolver.js.map +1 -0
  12. package/bin/adapter/entry-points/console/consoleServer.js +43 -17
  13. package/bin/adapter/entry-points/console/consoleServer.js.map +1 -1
  14. package/bin/adapter/entry-points/console/ui-dist/assets/index-BU6p3cGU.css +1 -0
  15. package/bin/adapter/entry-points/console/ui-dist/assets/index-BvuSQN9s.js +100 -0
  16. package/bin/adapter/entry-points/console/ui-dist/index.html +2 -2
  17. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +16 -0
  18. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
  19. package/jest.config.js +57 -9
  20. package/package.json +17 -13
  21. package/src/adapter/entry-points/cli/index.test.ts +12 -2
  22. package/src/adapter/entry-points/cli/index.ts +30 -12
  23. package/src/adapter/entry-points/cli/projectConfig.ts +3 -0
  24. package/src/adapter/entry-points/console/consoleOperationApi.test.ts +129 -15
  25. package/src/adapter/entry-points/console/consoleOperationApi.ts +83 -28
  26. package/src/adapter/entry-points/console/consoleProjectResolver.test.ts +96 -0
  27. package/src/adapter/entry-points/console/consoleProjectResolver.ts +50 -0
  28. package/src/adapter/entry-points/console/consoleServer.test.ts +86 -4
  29. package/src/adapter/entry-points/console/consoleServer.ts +53 -23
  30. package/src/adapter/entry-points/console/ui/jest.setup.ts +1 -0
  31. package/src/adapter/entry-points/console/ui/jest.styleMock.js +1 -0
  32. package/src/adapter/entry-points/console/ui/src/features/console/colors.test.ts +34 -0
  33. package/src/adapter/entry-points/console/ui/src/features/console/colors.ts +73 -0
  34. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleChangedFileList.stories.tsx +28 -0
  35. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleChangedFileList.test.tsx +42 -0
  36. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleChangedFileList.tsx +55 -0
  37. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCloseButtonGroup.stories.tsx +14 -0
  38. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCloseButtonGroup.test.tsx +23 -0
  39. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCloseButtonGroup.tsx +26 -0
  40. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCommentList.stories.tsx +29 -0
  41. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCommentList.test.tsx +55 -0
  42. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCommentList.tsx +66 -0
  43. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCommitList.stories.tsx +25 -0
  44. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCommitList.test.tsx +53 -0
  45. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCommitList.tsx +53 -0
  46. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleItemDetail.stories.tsx +79 -0
  47. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleItemDetail.test.tsx +81 -0
  48. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleItemDetail.tsx +226 -0
  49. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleItemIcon.stories.tsx +82 -0
  50. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleItemIcon.test.tsx +52 -0
  51. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleItemIcon.tsx +32 -0
  52. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListItemRow.stories.tsx +25 -0
  53. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListItemRow.test.tsx +43 -0
  54. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListItemRow.tsx +34 -0
  55. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListView.stories.tsx +22 -6
  56. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListView.test.tsx +87 -0
  57. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListView.tsx +36 -32
  58. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleMarkdownView.stories.tsx +27 -0
  59. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleMarkdownView.test.tsx +36 -0
  60. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleMarkdownView.tsx +50 -0
  61. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleMermaidDiagram.stories.tsx +22 -0
  62. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleMermaidDiagram.test.tsx +38 -0
  63. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleMermaidDiagram.tsx +65 -0
  64. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleNextActionDateGroup.stories.tsx +20 -0
  65. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleNextActionDateGroup.test.tsx +42 -0
  66. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleNextActionDateGroup.tsx +28 -0
  67. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleOperationBar.stories.tsx +55 -0
  68. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleOperationBar.test.tsx +85 -0
  69. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleOperationBar.tsx +55 -0
  70. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePanel.stories.tsx +26 -0
  71. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePanel.test.tsx +32 -0
  72. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePanel.tsx +36 -0
  73. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleProjectHeader.stories.tsx +29 -0
  74. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleProjectHeader.test.tsx +14 -0
  75. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleProjectHeader.tsx +14 -0
  76. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePullRequestReviewGroup.stories.tsx +14 -0
  77. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePullRequestReviewGroup.test.tsx +33 -0
  78. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePullRequestReviewGroup.tsx +34 -0
  79. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePullRequestSection.stories.tsx +31 -0
  80. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePullRequestSection.test.tsx +40 -0
  81. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePullRequestSection.tsx +88 -0
  82. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStatusButtonGroup.stories.tsx +17 -0
  83. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStatusButtonGroup.test.tsx +49 -0
  84. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStatusButtonGroup.tsx +63 -0
  85. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStoryButtonGroup.stories.tsx +17 -0
  86. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStoryButtonGroup.test.tsx +45 -0
  87. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStoryButtonGroup.tsx +42 -0
  88. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStoryGroupHeader.stories.tsx +27 -0
  89. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStoryGroupHeader.test.tsx +24 -0
  90. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStoryGroupHeader.tsx +28 -0
  91. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleTabBar.stories.tsx +41 -5
  92. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleTabBar.test.tsx +59 -0
  93. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleTabBar.tsx +28 -19
  94. package/src/adapter/entry-points/console/ui/src/features/console/fileStatus.test.ts +35 -0
  95. package/src/adapter/entry-points/console/ui/src/features/console/fileStatus.ts +21 -0
  96. package/src/adapter/entry-points/console/ui/src/features/console/fixtures.ts +206 -9
  97. package/src/adapter/entry-points/console/ui/src/features/console/grouping.test.ts +91 -0
  98. package/src/adapter/entry-points/console/ui/src/features/console/grouping.ts +79 -0
  99. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleCaches.test.ts +22 -0
  100. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleCaches.ts +42 -0
  101. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleItemDetailData.test.ts +126 -0
  102. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleItemDetailData.ts +167 -0
  103. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.test.ts +198 -0
  104. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.ts +243 -0
  105. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOverlay.test.ts +40 -0
  106. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOverlay.ts +71 -0
  107. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsolePjcode.test.ts +24 -0
  108. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsolePjcode.ts +17 -0
  109. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleResource.test.ts +41 -0
  110. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleResource.ts +57 -0
  111. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleTabData.test.ts +63 -0
  112. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleTabData.ts +129 -0
  113. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleToken.test.ts +41 -0
  114. package/src/adapter/entry-points/console/ui/src/features/console/itemIcons.test.ts +97 -0
  115. package/src/adapter/entry-points/console/ui/src/features/console/itemIcons.ts +95 -0
  116. package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.test.ts +155 -0
  117. package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.ts +187 -0
  118. package/src/adapter/entry-points/console/ui/src/features/console/lib/markdown.test.ts +76 -0
  119. package/src/adapter/entry-points/console/ui/src/features/console/lib/markdown.ts +73 -0
  120. package/src/adapter/entry-points/console/ui/src/features/console/lib/mermaidLoader.test.ts +27 -0
  121. package/src/adapter/entry-points/console/ui/src/features/console/lib/mermaidLoader.ts +71 -0
  122. package/src/adapter/entry-points/console/ui/src/features/console/lib/resourceCache.test.ts +56 -0
  123. package/src/adapter/entry-points/console/ui/src/features/console/lib/resourceCache.ts +51 -0
  124. package/src/adapter/entry-points/console/ui/src/features/console/operations.test.ts +37 -0
  125. package/src/adapter/entry-points/console/ui/src/features/console/operations.ts +35 -0
  126. package/src/adapter/entry-points/console/ui/src/features/console/overlay.test.ts +124 -0
  127. package/src/adapter/entry-points/console/ui/src/features/console/overlay.ts +101 -0
  128. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.test.tsx +79 -0
  129. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.tsx +109 -0
  130. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.test.tsx +74 -0
  131. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +137 -7
  132. package/src/adapter/entry-points/console/ui/src/features/console/relativeTime.test.ts +52 -0
  133. package/src/adapter/entry-points/console/ui/src/features/console/relativeTime.ts +51 -0
  134. package/src/adapter/entry-points/console/ui/src/features/console/types.ts +73 -1
  135. package/src/adapter/entry-points/console/ui/src/index.css +352 -2
  136. package/src/adapter/entry-points/console/ui/tsconfig.json +3 -1
  137. package/src/adapter/entry-points/console/ui/vite.config.ts +6 -1
  138. package/src/adapter/entry-points/console/ui-dist/assets/index-BU6p3cGU.css +1 -0
  139. package/src/adapter/entry-points/console/ui-dist/assets/index-BvuSQN9s.js +100 -0
  140. package/src/adapter/entry-points/console/ui-dist/index.html +2 -2
  141. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +25 -0
  142. package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +4 -0
  143. package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
  144. package/types/adapter/entry-points/cli/projectConfig.d.ts +1 -0
  145. package/types/adapter/entry-points/cli/projectConfig.d.ts.map +1 -1
  146. package/types/adapter/entry-points/console/consoleOperationApi.d.ts +6 -2
  147. package/types/adapter/entry-points/console/consoleOperationApi.d.ts.map +1 -1
  148. package/types/adapter/entry-points/console/consoleProjectResolver.d.ts +6 -0
  149. package/types/adapter/entry-points/console/consoleProjectResolver.d.ts.map +1 -0
  150. package/types/adapter/entry-points/console/consoleServer.d.ts +3 -3
  151. package/types/adapter/entry-points/console/consoleServer.d.ts.map +1 -1
  152. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +1 -0
  153. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
  154. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +1 -0
  155. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
  156. package/bin/adapter/entry-points/console/ui-dist/assets/index-DFxrSRH4.css +0 -1
  157. package/bin/adapter/entry-points/console/ui-dist/assets/index-DcOZ02ON.js +0 -49
  158. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleList.ts +0 -65
  159. package/src/adapter/entry-points/console/ui-dist/assets/index-DFxrSRH4.css +0 -1
  160. package/src/adapter/entry-points/console/ui-dist/assets/index-DcOZ02ON.js +0 -49
@@ -0,0 +1,96 @@
1
+ import { mock } from 'jest-mock-extended';
2
+ import { Project } from '../../../domain/entities/Project';
3
+ import {
4
+ buildPjcodeToProjectUrl,
5
+ createConsoleProjectResolver,
6
+ } from './consoleProjectResolver';
7
+
8
+ describe('buildPjcodeToProjectUrl', () => {
9
+ it('adds the default pjcode entry when it is not already present', () => {
10
+ const mapping = buildPjcodeToProjectUrl(
11
+ 'umino',
12
+ 'https://github.com/orgs/umino/projects/1',
13
+ { xmile: 'https://github.com/orgs/xmile/projects/2' },
14
+ );
15
+ expect(mapping).toEqual({
16
+ umino: 'https://github.com/orgs/umino/projects/1',
17
+ xmile: 'https://github.com/orgs/xmile/projects/2',
18
+ });
19
+ });
20
+
21
+ it('keeps an explicit default pjcode entry from consoleProjects', () => {
22
+ const mapping = buildPjcodeToProjectUrl(
23
+ 'umino',
24
+ 'https://github.com/orgs/umino/projects/1',
25
+ { umino: 'https://github.com/orgs/umino/projects/9' },
26
+ );
27
+ expect(mapping.umino).toBe('https://github.com/orgs/umino/projects/9');
28
+ });
29
+
30
+ it('uses only the default entry when no consoleProjects mapping is configured', () => {
31
+ const mapping = buildPjcodeToProjectUrl(
32
+ 'umino',
33
+ 'https://github.com/orgs/umino/projects/1',
34
+ null,
35
+ );
36
+ expect(mapping).toEqual({
37
+ umino: 'https://github.com/orgs/umino/projects/1',
38
+ });
39
+ });
40
+ });
41
+
42
+ describe('createConsoleProjectResolver', () => {
43
+ const uminoProject: Project = { ...mock<Project>(), id: 'PVT_umino' };
44
+ const xmileProject: Project = { ...mock<Project>(), id: 'PVT_xmile' };
45
+
46
+ it('resolves a known pjcode to its loaded project', async () => {
47
+ const loadProject = jest.fn(async (url: string) =>
48
+ url.includes('umino') ? uminoProject : xmileProject,
49
+ );
50
+ const resolver = createConsoleProjectResolver(
51
+ {
52
+ umino: 'https://github.com/orgs/umino/projects/1',
53
+ xmile: 'https://github.com/orgs/xmile/projects/2',
54
+ },
55
+ loadProject,
56
+ );
57
+ await expect(resolver('umino')).resolves.toEqual({
58
+ pjcode: 'umino',
59
+ project: uminoProject,
60
+ });
61
+ await expect(resolver('xmile')).resolves.toEqual({
62
+ pjcode: 'xmile',
63
+ project: xmileProject,
64
+ });
65
+ });
66
+
67
+ it('returns null for a pjcode that has no configured project url', async () => {
68
+ const loadProject = jest.fn(async () => uminoProject);
69
+ const resolver = createConsoleProjectResolver(
70
+ { umino: 'https://github.com/orgs/umino/projects/1' },
71
+ loadProject,
72
+ );
73
+ await expect(resolver('unknown')).resolves.toBeNull();
74
+ expect(loadProject).not.toHaveBeenCalled();
75
+ });
76
+
77
+ it('returns null when the project fails to load', async () => {
78
+ const loadProject = jest.fn(async () => null);
79
+ const resolver = createConsoleProjectResolver(
80
+ { umino: 'https://github.com/orgs/umino/projects/1' },
81
+ loadProject,
82
+ );
83
+ await expect(resolver('umino')).resolves.toBeNull();
84
+ });
85
+
86
+ it('loads each project at most once and serves later calls from cache', async () => {
87
+ const loadProject = jest.fn(async () => uminoProject);
88
+ const resolver = createConsoleProjectResolver(
89
+ { umino: 'https://github.com/orgs/umino/projects/1' },
90
+ loadProject,
91
+ );
92
+ await resolver('umino');
93
+ await resolver('umino');
94
+ expect(loadProject).toHaveBeenCalledTimes(1);
95
+ });
96
+ });
@@ -0,0 +1,50 @@
1
+ import { Project } from '../../../domain/entities/Project';
2
+ import {
3
+ ConsoleProjectBinding,
4
+ ConsoleProjectResolver,
5
+ } from './consoleOperationApi';
6
+
7
+ export type ConsoleProjectLoader = (
8
+ projectUrl: string,
9
+ ) => Promise<Project | null>;
10
+
11
+ export const buildPjcodeToProjectUrl = (
12
+ defaultPjcode: string,
13
+ defaultProjectUrl: string,
14
+ consoleProjects: Record<string, string> | null,
15
+ ): Record<string, string> => {
16
+ const mapping: Record<string, string> = {};
17
+ if (consoleProjects !== null) {
18
+ for (const [pjcode, projectUrl] of Object.entries(consoleProjects)) {
19
+ mapping[pjcode] = projectUrl;
20
+ }
21
+ }
22
+ if (!(defaultPjcode in mapping)) {
23
+ mapping[defaultPjcode] = defaultProjectUrl;
24
+ }
25
+ return mapping;
26
+ };
27
+
28
+ export const createConsoleProjectResolver = (
29
+ pjcodeToProjectUrl: Record<string, string>,
30
+ loadProject: ConsoleProjectLoader,
31
+ ): ConsoleProjectResolver => {
32
+ const cache = new Map<string, ConsoleProjectBinding>();
33
+ return async (pjcode: string): Promise<ConsoleProjectBinding | null> => {
34
+ const cached = cache.get(pjcode);
35
+ if (cached !== undefined) {
36
+ return cached;
37
+ }
38
+ const projectUrl = pjcodeToProjectUrl[pjcode];
39
+ if (projectUrl === undefined) {
40
+ return null;
41
+ }
42
+ const project = await loadProject(projectUrl);
43
+ if (project === null) {
44
+ return null;
45
+ }
46
+ const binding: ConsoleProjectBinding = { pjcode, project };
47
+ cache.set(pjcode, binding);
48
+ return binding;
49
+ };
50
+ };
@@ -9,6 +9,7 @@ import {
9
9
  hasDotSegment,
10
10
  requiresToken,
11
11
  isTokenValid,
12
+ isConsoleAppRoute,
12
13
  extractProvidedToken,
13
14
  startConsoleServer,
14
15
  } from './consoleServer';
@@ -61,6 +62,35 @@ describe('consoleServer pure helpers', () => {
61
62
  });
62
63
  });
63
64
 
65
+ describe('isConsoleAppRoute', () => {
66
+ it('matches a per-project root route', () => {
67
+ expect(isConsoleAppRoute('/projects/umino')).toBe(true);
68
+ expect(isConsoleAppRoute('/projects/umino/')).toBe(true);
69
+ });
70
+
71
+ it('matches a per-project tab route for every list tab', () => {
72
+ expect(isConsoleAppRoute('/projects/umino/prs')).toBe(true);
73
+ expect(isConsoleAppRoute('/projects/xmile/triage')).toBe(true);
74
+ expect(isConsoleAppRoute('/projects/xcare/unread')).toBe(true);
75
+ expect(isConsoleAppRoute('/projects/utage3/failed-preparation')).toBe(
76
+ true,
77
+ );
78
+ expect(isConsoleAppRoute('/projects/utage3/todo-by-human')).toBe(true);
79
+ });
80
+
81
+ it('does not match data, api, or unknown tab routes', () => {
82
+ expect(isConsoleAppRoute('/projects/umino/prs/list.json')).toBe(false);
83
+ expect(isConsoleAppRoute('/projects/umino/unknown')).toBe(false);
84
+ expect(isConsoleAppRoute('/projects')).toBe(false);
85
+ expect(isConsoleAppRoute('/api/review')).toBe(false);
86
+ expect(isConsoleAppRoute('/')).toBe(false);
87
+ });
88
+
89
+ it('does not match a dot-prefixed pjcode', () => {
90
+ expect(isConsoleAppRoute('/projects/.git')).toBe(false);
91
+ });
92
+ });
93
+
64
94
  describe('isTokenValid', () => {
65
95
  it('accepts a matching token', () => {
66
96
  expect(isTokenValid('expected', 'expected')).toBe(true);
@@ -230,6 +260,57 @@ describe('consoleServer integration', () => {
230
260
  }
231
261
  });
232
262
 
263
+ it('serves the SPA index for per-project app routes without a token', async () => {
264
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'console-server-'));
265
+ const uiDistDir = path.join(tmpDir, 'ui-dist');
266
+ fs.mkdirSync(uiDistDir, { recursive: true });
267
+ fs.writeFileSync(
268
+ path.join(uiDistDir, 'index.html'),
269
+ '<!DOCTYPE html><title>spa</title><div id="root"></div>',
270
+ );
271
+ const server = await startConsoleServer({
272
+ accessToken: testToken,
273
+ uiDistDir,
274
+ consoleDataOutputDir: null,
275
+ port: 0,
276
+ });
277
+ try {
278
+ const projectRoot = await requestServer(server, '/projects/umino');
279
+ expect(projectRoot.statusCode).toBe(200);
280
+ expect(projectRoot.body).toContain('spa');
281
+ expect(projectRoot.contentType).toContain('text/html');
282
+ expect(projectRoot.cacheControl).toBe('no-store');
283
+
284
+ const projectTab = await requestServer(server, '/projects/xmile/prs');
285
+ expect(projectTab.statusCode).toBe(200);
286
+ expect(projectTab.body).toContain('spa');
287
+
288
+ const unknownTab = await requestServer(server, '/projects/xmile/unknown');
289
+ expect(unknownTab.statusCode).toBe(404);
290
+ } finally {
291
+ await closeServer(server);
292
+ fs.rmSync(tmpDir, { recursive: true, force: true });
293
+ }
294
+ });
295
+
296
+ it('serves the placeholder index for per-project routes when ui-dist is absent', async () => {
297
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'console-server-'));
298
+ const server = await startConsoleServer({
299
+ accessToken: testToken,
300
+ uiDistDir: path.join(tmpDir, 'missing-ui-dist'),
301
+ consoleDataOutputDir: null,
302
+ port: 0,
303
+ });
304
+ try {
305
+ const projectRoot = await requestServer(server, '/projects/umino/triage');
306
+ expect(projectRoot.statusCode).toBe(200);
307
+ expect(projectRoot.body).toContain('TDPM Console');
308
+ } finally {
309
+ await closeServer(server);
310
+ fs.rmSync(tmpDir, { recursive: true, force: true });
311
+ }
312
+ });
313
+
233
314
  it('rejects dot-prefixed paths with 404', async () => {
234
315
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'console-server-'));
235
316
  const uiDistDir = path.join(tmpDir, 'ui-dist');
@@ -399,7 +480,6 @@ describe('consoleServer new routes integration', () => {
399
480
  accessToken: testToken,
400
481
  uiDistDir: path.join(tmpDir, 'ui-dist'),
401
482
  consoleDataOutputDir: dataDir,
402
- pjcode: 'umino',
403
483
  port: 0,
404
484
  });
405
485
  try {
@@ -465,9 +545,9 @@ describe('consoleServer new routes integration', () => {
465
545
  accessToken: testToken,
466
546
  uiDistDir: path.join(tmpDir, 'ui-dist'),
467
547
  consoleDataOutputDir: dataDir,
468
- pjcode: 'umino',
469
548
  issueRepository,
470
- project: buildProject(),
549
+ resolveProject: async (pjcode) =>
550
+ pjcode === 'umino' ? { pjcode, project: buildProject() } : null,
471
551
  port: 0,
472
552
  });
473
553
  try {
@@ -476,6 +556,7 @@ describe('consoleServer new routes integration', () => {
476
556
  'POST',
477
557
  `/api/review?k=${testToken}`,
478
558
  {
559
+ pjcode: 'umino',
479
560
  action: 'approve',
480
561
  prUrl: 'https://github.com/o/r/pull/1',
481
562
  projectItemId: 'PVTI_op',
@@ -502,7 +583,8 @@ describe('consoleServer new routes integration', () => {
502
583
  uiDistDir: path.join(tmpDir, 'ui-dist'),
503
584
  consoleDataOutputDir: null,
504
585
  issueRepository,
505
- project: buildProject(),
586
+ resolveProject: async (pjcode) =>
587
+ pjcode === 'umino' ? { pjcode, project: buildProject() } : null,
506
588
  port: 0,
507
589
  });
508
590
  try {
@@ -2,8 +2,8 @@ import * as http from 'http';
2
2
  import * as fs from 'fs';
3
3
  import * as path from 'path';
4
4
  import { IssueRepository } from '../../../domain/usecases/adapter-interfaces/IssueRepository';
5
- import { Project } from '../../../domain/entities/Project';
6
5
  import {
6
+ CONSOLE_LIST_TAB_NAMES,
7
7
  buildConsoleDataResponse,
8
8
  parseConsoleDataRoute,
9
9
  } from './consoleDataDelivery';
@@ -18,6 +18,7 @@ import {
18
18
  } from './consoleReadApi';
19
19
  import {
20
20
  ConsoleOperationContext,
21
+ ConsoleProjectResolver,
21
22
  handleIntmux,
22
23
  handleReview,
23
24
  handleTriage,
@@ -70,6 +71,29 @@ export const requiresToken = (requestPath: string): boolean =>
70
71
  requestPath === '/api' ||
71
72
  requestPath.endsWith('.json');
72
73
 
74
+ const SAFE_PJCODE = /^[A-Za-z0-9._-]+$/;
75
+
76
+ export const isConsoleAppRoute = (requestPath: string): boolean => {
77
+ const segments = requestPath
78
+ .split('/')
79
+ .filter((segment) => segment.length > 0);
80
+ if (segments.length < 2 || segments[0] !== 'projects') {
81
+ return false;
82
+ }
83
+ const pjcode = segments[1];
84
+ if (!SAFE_PJCODE.test(pjcode) || pjcode.startsWith('.')) {
85
+ return false;
86
+ }
87
+ if (segments.length === 2) {
88
+ return true;
89
+ }
90
+ if (segments.length !== 3) {
91
+ return false;
92
+ }
93
+ const tab = segments[2];
94
+ return CONSOLE_LIST_TAB_NAMES.includes(tab);
95
+ };
96
+
73
97
  export const isTokenValid = (
74
98
  expectedToken: string,
75
99
  providedToken: string | null,
@@ -129,9 +153,8 @@ export type ConsoleServerOptions = {
129
153
  accessToken: string;
130
154
  uiDistDir: string;
131
155
  consoleDataOutputDir: string | null;
132
- pjcode?: string | null;
133
156
  issueRepository?: IssueRepository | null;
134
- project?: Project | null;
157
+ resolveProject?: ConsoleProjectResolver | null;
135
158
  issueTitleStateCache?: IssueTitleStateCache | null;
136
159
  };
137
160
 
@@ -159,6 +182,24 @@ const serveBootstrapIndex = (response: http.ServerResponse): void => {
159
182
  response.end(PLACEHOLDER_INDEX_HTML);
160
183
  };
161
184
 
185
+ const serveIndexHtml = (
186
+ options: ConsoleServerOptions,
187
+ response: http.ServerResponse,
188
+ ): void => {
189
+ const indexFilePath = resolveStaticFilePath(options.uiDistDir, '/index.html');
190
+ const indexContent =
191
+ indexFilePath === null ? null : readStaticFile(indexFilePath);
192
+ if (indexContent === null) {
193
+ serveBootstrapIndex(response);
194
+ return;
195
+ }
196
+ response.writeHead(200, {
197
+ 'Content-Type': 'text/html; charset=utf-8',
198
+ 'Cache-Control': 'no-store',
199
+ });
200
+ response.end(indexContent);
201
+ };
202
+
162
203
  const sendJson = (
163
204
  response: http.ServerResponse,
164
205
  statusCode: number,
@@ -249,15 +290,14 @@ const handleOperationApi = async (
249
290
  body: Record<string, unknown>,
250
291
  ): Promise<{ statusCode: number; body: unknown } | null> => {
251
292
  const issueRepository = options.issueRepository ?? null;
252
- const project = options.project ?? null;
253
- if (issueRepository === null || project === null) {
293
+ const resolveProject = options.resolveProject ?? null;
294
+ if (issueRepository === null || resolveProject === null) {
254
295
  return null;
255
296
  }
256
297
  const context: ConsoleOperationContext = {
257
298
  issueRepository,
258
- project,
299
+ resolveProject,
259
300
  consoleDataOutputDir: options.consoleDataOutputDir,
260
- pjcode: options.pjcode ?? null,
261
301
  };
262
302
  switch (requestPath) {
263
303
  case '/api/review':
@@ -369,22 +409,12 @@ export const handleConsoleRequest = async (
369
409
  return;
370
410
  }
371
411
 
372
- if (requestPath === '/' || requestPath === '/index.html') {
373
- const indexFilePath = resolveStaticFilePath(
374
- options.uiDistDir,
375
- '/index.html',
376
- );
377
- const indexContent =
378
- indexFilePath === null ? null : readStaticFile(indexFilePath);
379
- if (indexContent === null) {
380
- serveBootstrapIndex(response);
381
- return;
382
- }
383
- response.writeHead(200, {
384
- 'Content-Type': 'text/html; charset=utf-8',
385
- 'Cache-Control': 'no-store',
386
- });
387
- response.end(indexContent);
412
+ if (
413
+ requestPath === '/' ||
414
+ requestPath === '/index.html' ||
415
+ isConsoleAppRoute(requestPath)
416
+ ) {
417
+ serveIndexHtml(options, response);
388
418
  return;
389
419
  }
390
420
 
@@ -0,0 +1 @@
1
+ import '@testing-library/jest-dom';
@@ -0,0 +1 @@
1
+ module.exports = {};
@@ -0,0 +1,34 @@
1
+ import { CONSOLE_COLOR_PALETTE, colorFromEnum } from './colors';
2
+
3
+ describe('colorFromEnum', () => {
4
+ it('returns the exact palette entry for a known enum', () => {
5
+ expect(colorFromEnum('GREEN')).toEqual({
6
+ dot: '#3fb950',
7
+ bg: 'rgba(46,160,67,0.15)',
8
+ fg: '#3fb950',
9
+ border: 'rgba(46,160,67,0.4)',
10
+ });
11
+ });
12
+
13
+ it('is case insensitive', () => {
14
+ expect(colorFromEnum('purple')).toBe(CONSOLE_COLOR_PALETTE.PURPLE);
15
+ });
16
+
17
+ it('falls back to GRAY for an unknown enum', () => {
18
+ expect(colorFromEnum('MAGENTA')).toBe(CONSOLE_COLOR_PALETTE.GRAY);
19
+ });
20
+
21
+ it('falls back to GRAY for null', () => {
22
+ expect(colorFromEnum(null)).toBe(CONSOLE_COLOR_PALETTE.GRAY);
23
+ });
24
+
25
+ it('uses the documented dot colors for every enum', () => {
26
+ expect(CONSOLE_COLOR_PALETTE.GRAY.dot).toBe('#848d97');
27
+ expect(CONSOLE_COLOR_PALETTE.BLUE.dot).toBe('#4493f8');
28
+ expect(CONSOLE_COLOR_PALETTE.YELLOW.dot).toBe('#d29922');
29
+ expect(CONSOLE_COLOR_PALETTE.ORANGE.dot).toBe('#db6d28');
30
+ expect(CONSOLE_COLOR_PALETTE.RED.dot).toBe('#f85149');
31
+ expect(CONSOLE_COLOR_PALETTE.PINK.dot).toBe('#db61a2');
32
+ expect(CONSOLE_COLOR_PALETTE.PURPLE.dot).toBe('#a371f7');
33
+ });
34
+ });
@@ -0,0 +1,73 @@
1
+ import type { ConsoleColor } from './types';
2
+
3
+ export type ConsolePaletteEntry = {
4
+ dot: string;
5
+ bg: string;
6
+ fg: string;
7
+ border: string;
8
+ };
9
+
10
+ export const CONSOLE_COLOR_PALETTE: Record<ConsoleColor, ConsolePaletteEntry> =
11
+ {
12
+ GRAY: {
13
+ dot: '#848d97',
14
+ bg: 'rgba(110,118,129,0.1)',
15
+ fg: '#8b949e',
16
+ border: 'rgba(110,118,129,0.4)',
17
+ },
18
+ BLUE: {
19
+ dot: '#4493f8',
20
+ bg: 'rgba(56,139,253,0.1)',
21
+ fg: '#388bfd',
22
+ border: 'rgba(56,139,253,0.4)',
23
+ },
24
+ GREEN: {
25
+ dot: '#3fb950',
26
+ bg: 'rgba(46,160,67,0.15)',
27
+ fg: '#3fb950',
28
+ border: 'rgba(46,160,67,0.4)',
29
+ },
30
+ YELLOW: {
31
+ dot: '#d29922',
32
+ bg: 'rgba(187,128,9,0.15)',
33
+ fg: '#d29922',
34
+ border: 'rgba(187,128,9,0.4)',
35
+ },
36
+ ORANGE: {
37
+ dot: '#db6d28',
38
+ bg: 'rgba(219,109,40,0.1)',
39
+ fg: '#db6d28',
40
+ border: 'rgba(219,109,40,0.4)',
41
+ },
42
+ RED: {
43
+ dot: '#f85149',
44
+ bg: 'rgba(248,81,73,0.1)',
45
+ fg: '#f85149',
46
+ border: 'rgba(248,81,73,0.4)',
47
+ },
48
+ PINK: {
49
+ dot: '#db61a2',
50
+ bg: 'rgba(219,97,162,0.1)',
51
+ fg: '#db61a2',
52
+ border: 'rgba(219,97,162,0.4)',
53
+ },
54
+ PURPLE: {
55
+ dot: '#a371f7',
56
+ bg: 'rgba(163,113,247,0.1)',
57
+ fg: '#a371f7',
58
+ border: 'rgba(163,113,247,0.4)',
59
+ },
60
+ };
61
+
62
+ export const colorFromEnum = (
63
+ colorEnum: string | null,
64
+ ): ConsolePaletteEntry => {
65
+ if (colorEnum === null) {
66
+ return CONSOLE_COLOR_PALETTE.GRAY;
67
+ }
68
+ const key = colorEnum.toUpperCase();
69
+ if (key in CONSOLE_COLOR_PALETTE) {
70
+ return CONSOLE_COLOR_PALETTE[key as ConsoleColor];
71
+ }
72
+ return CONSOLE_COLOR_PALETTE.GRAY;
73
+ };
@@ -0,0 +1,28 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { consoleChangedFilesFixture } from '../fixtures';
3
+ import { ConsoleChangedFileList } from './ConsoleChangedFileList';
4
+
5
+ const meta: Meta<typeof ConsoleChangedFileList> = {
6
+ title: 'Console/ConsoleChangedFileList',
7
+ component: ConsoleChangedFileList,
8
+ };
9
+
10
+ export default meta;
11
+
12
+ type Story = StoryObj<typeof ConsoleChangedFileList>;
13
+
14
+ export const WithFiles: Story = {
15
+ args: { files: consoleChangedFilesFixture, isLoading: false, error: null },
16
+ };
17
+
18
+ export const Loading: Story = {
19
+ args: { files: [], isLoading: true, error: null },
20
+ };
21
+
22
+ export const Empty: Story = {
23
+ args: { files: [], isLoading: false, error: null },
24
+ };
25
+
26
+ export const ErrorState: Story = {
27
+ args: { files: [], isLoading: false, error: 'HTTP 502' },
28
+ };
@@ -0,0 +1,42 @@
1
+ import { render } from '@testing-library/react';
2
+ import { consoleChangedFilesFixture } from '../fixtures';
3
+ import { ConsoleChangedFileList } from './ConsoleChangedFileList';
4
+
5
+ describe('ConsoleChangedFileList', () => {
6
+ it('renders each file path, status badge and additions/deletions', () => {
7
+ const { getByText, getAllByText } = render(
8
+ <ConsoleChangedFileList
9
+ files={consoleChangedFilesFixture}
10
+ isLoading={false}
11
+ error={null}
12
+ />,
13
+ );
14
+ expect(
15
+ getByText('src/adapter/entry-points/console/consoleServer.ts'),
16
+ ).toBeInTheDocument();
17
+ expect(getByText('+312')).toBeInTheDocument();
18
+ expect(getAllByText('A').length).toBe(2);
19
+ expect(getByText('M')).toBeInTheDocument();
20
+ });
21
+
22
+ it('shows the loading state', () => {
23
+ const { getByText } = render(
24
+ <ConsoleChangedFileList files={[]} isLoading error={null} />,
25
+ );
26
+ expect(getByText('Loading changed files...')).toBeInTheDocument();
27
+ });
28
+
29
+ it('shows the empty state', () => {
30
+ const { getByText } = render(
31
+ <ConsoleChangedFileList files={[]} isLoading={false} error={null} />,
32
+ );
33
+ expect(getByText('No changed files.')).toBeInTheDocument();
34
+ });
35
+
36
+ it('shows the error state', () => {
37
+ const { getByRole } = render(
38
+ <ConsoleChangedFileList files={[]} isLoading={false} error="HTTP 502" />,
39
+ );
40
+ expect(getByRole('alert')).toHaveTextContent('HTTP 502');
41
+ });
42
+ });
@@ -0,0 +1,55 @@
1
+ import { fileStatusBadge } from '../fileStatus';
2
+ import type { ConsoleChangedFile } from '../types';
3
+
4
+ export type ConsoleChangedFileListProps = {
5
+ files: ConsoleChangedFile[];
6
+ isLoading: boolean;
7
+ error: string | null;
8
+ };
9
+
10
+ export const ConsoleChangedFileList = ({
11
+ files,
12
+ isLoading,
13
+ error,
14
+ }: ConsoleChangedFileListProps) => {
15
+ if (error !== null) {
16
+ return (
17
+ <p role="alert" className="console-files-error">
18
+ Failed to load changed files: {error}
19
+ </p>
20
+ );
21
+ }
22
+
23
+ if (isLoading) {
24
+ return <p className="console-files-loading">Loading changed files...</p>;
25
+ }
26
+
27
+ if (files.length === 0) {
28
+ return <p className="console-files-empty">No changed files.</p>;
29
+ }
30
+
31
+ return (
32
+ <ul className="console-files">
33
+ {files.map((file) => {
34
+ const badge = fileStatusBadge(file.status);
35
+ return (
36
+ <li key={file.path} className="console-file">
37
+ <span
38
+ className="console-file-badge"
39
+ style={{ color: badge.color, borderColor: badge.color }}
40
+ >
41
+ {badge.label}
42
+ </span>
43
+ <span className="console-file-path">{file.path}</span>
44
+ <span className="console-file-stat console-file-add">
45
+ +{file.additions}
46
+ </span>
47
+ <span className="console-file-stat console-file-del">
48
+ -{file.deletions}
49
+ </span>
50
+ </li>
51
+ );
52
+ })}
53
+ </ul>
54
+ );
55
+ };
@@ -0,0 +1,14 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { ConsoleCloseButtonGroup } from './ConsoleCloseButtonGroup';
3
+
4
+ const meta: Meta<typeof ConsoleCloseButtonGroup> = {
5
+ title: 'Console/ConsoleCloseButtonGroup',
6
+ component: ConsoleCloseButtonGroup,
7
+ args: { onClose: () => {} },
8
+ };
9
+
10
+ export default meta;
11
+
12
+ type Story = StoryObj<typeof ConsoleCloseButtonGroup>;
13
+
14
+ export const Default: Story = {};