github-issue-tower-defence-management 1.109.0 → 1.109.2
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 +19 -0
- package/README.md +6 -7
- package/bin/adapter/entry-points/cli/index.js +10 -10
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/console/dashboardComposeService.js +7 -6
- package/bin/adapter/entry-points/console/dashboardComposeService.js.map +1 -1
- package/bin/adapter/entry-points/console/webServer.js +5 -4
- package/bin/adapter/entry-points/console/webServer.js.map +1 -1
- package/bin/adapter/repositories/GraphqlProjectRepository.js +3 -58
- package/bin/adapter/repositories/GraphqlProjectRepository.js.map +1 -1
- package/bin/adapter/repositories/issue/GraphqlProjectItemRepository.js +1 -1
- package/bin/adapter/repositories/issue/GraphqlProjectItemRepository.js.map +1 -1
- package/bin/domain/usecases/dashboard/DashboardProjectCode.js +19 -0
- package/bin/domain/usecases/dashboard/DashboardProjectCode.js.map +1 -0
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/index.ts +14 -14
- package/src/adapter/entry-points/console/dashboardComposeService.test.ts +31 -28
- package/src/adapter/entry-points/console/dashboardComposeService.ts +13 -10
- package/src/adapter/entry-points/console/dashboardEmitterComposerKey.test.ts +175 -0
- package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +1 -1
- package/src/adapter/entry-points/console/webServer.test.ts +23 -23
- package/src/adapter/entry-points/console/webServer.ts +5 -4
- package/src/adapter/repositories/GraphqlProjectRepository.diskCache.test.ts +5 -105
- package/src/adapter/repositories/GraphqlProjectRepository.ts +1 -73
- package/src/adapter/repositories/issue/GraphqlProjectItemRepository.test.ts +3 -6
- package/src/adapter/repositories/issue/GraphqlProjectItemRepository.ts +1 -1
- package/src/domain/usecases/dashboard/DashboardProjectCode.test.ts +32 -0
- package/src/domain/usecases/dashboard/DashboardProjectCode.ts +20 -0
- package/types/adapter/entry-points/console/dashboardComposeService.d.ts +1 -1
- package/types/adapter/entry-points/console/dashboardComposeService.d.ts.map +1 -1
- package/types/adapter/entry-points/console/webServer.d.ts +2 -2
- package/types/adapter/entry-points/console/webServer.d.ts.map +1 -1
- package/types/adapter/repositories/GraphqlProjectRepository.d.ts +1 -5
- package/types/adapter/repositories/GraphqlProjectRepository.d.ts.map +1 -1
- package/types/adapter/repositories/issue/GraphqlProjectItemRepository.d.ts +1 -1
- package/types/adapter/repositories/issue/GraphqlProjectItemRepository.d.ts.map +1 -1
- package/types/domain/usecases/dashboard/DashboardProjectCode.d.ts +4 -0
- package/types/domain/usecases/dashboard/DashboardProjectCode.d.ts.map +1 -0
|
@@ -182,7 +182,7 @@ describe('webServer integration', () => {
|
|
|
182
182
|
inTmuxDataDir: null,
|
|
183
183
|
dashboardDir: null,
|
|
184
184
|
dashboardDataDir: null,
|
|
185
|
-
|
|
185
|
+
dashboardProjectNames: [],
|
|
186
186
|
port: 0,
|
|
187
187
|
});
|
|
188
188
|
const address = server.address();
|
|
@@ -201,7 +201,7 @@ describe('webServer integration', () => {
|
|
|
201
201
|
inTmuxDataDir: null,
|
|
202
202
|
dashboardDir: null,
|
|
203
203
|
dashboardDataDir: null,
|
|
204
|
-
|
|
204
|
+
dashboardProjectNames: [],
|
|
205
205
|
port: 0,
|
|
206
206
|
});
|
|
207
207
|
try {
|
|
@@ -229,7 +229,7 @@ describe('webServer integration', () => {
|
|
|
229
229
|
inTmuxDataDir: null,
|
|
230
230
|
dashboardDir: null,
|
|
231
231
|
dashboardDataDir: null,
|
|
232
|
-
|
|
232
|
+
dashboardProjectNames: [],
|
|
233
233
|
port: 0,
|
|
234
234
|
});
|
|
235
235
|
try {
|
|
@@ -261,7 +261,7 @@ describe('webServer integration', () => {
|
|
|
261
261
|
inTmuxDataDir: null,
|
|
262
262
|
dashboardDir: null,
|
|
263
263
|
dashboardDataDir: null,
|
|
264
|
-
|
|
264
|
+
dashboardProjectNames: [],
|
|
265
265
|
port: 0,
|
|
266
266
|
});
|
|
267
267
|
try {
|
|
@@ -295,7 +295,7 @@ describe('webServer integration', () => {
|
|
|
295
295
|
inTmuxDataDir: null,
|
|
296
296
|
dashboardDir: null,
|
|
297
297
|
dashboardDataDir: null,
|
|
298
|
-
|
|
298
|
+
dashboardProjectNames: [],
|
|
299
299
|
port: 0,
|
|
300
300
|
});
|
|
301
301
|
try {
|
|
@@ -326,7 +326,7 @@ describe('webServer integration', () => {
|
|
|
326
326
|
inTmuxDataDir: null,
|
|
327
327
|
dashboardDir: null,
|
|
328
328
|
dashboardDataDir: null,
|
|
329
|
-
|
|
329
|
+
dashboardProjectNames: [],
|
|
330
330
|
port: 0,
|
|
331
331
|
});
|
|
332
332
|
try {
|
|
@@ -351,7 +351,7 @@ describe('webServer integration', () => {
|
|
|
351
351
|
inTmuxDataDir: null,
|
|
352
352
|
dashboardDir: null,
|
|
353
353
|
dashboardDataDir: null,
|
|
354
|
-
|
|
354
|
+
dashboardProjectNames: [],
|
|
355
355
|
port: 0,
|
|
356
356
|
});
|
|
357
357
|
try {
|
|
@@ -379,7 +379,7 @@ describe('webServer integration', () => {
|
|
|
379
379
|
inTmuxDataDir: null,
|
|
380
380
|
dashboardDir: null,
|
|
381
381
|
dashboardDataDir: null,
|
|
382
|
-
|
|
382
|
+
dashboardProjectNames: [],
|
|
383
383
|
port: 0,
|
|
384
384
|
});
|
|
385
385
|
try {
|
|
@@ -519,7 +519,7 @@ describe('webServer new routes integration', () => {
|
|
|
519
519
|
inTmuxDataDir: null,
|
|
520
520
|
dashboardDir: null,
|
|
521
521
|
dashboardDataDir: null,
|
|
522
|
-
|
|
522
|
+
dashboardProjectNames: [],
|
|
523
523
|
port: 0,
|
|
524
524
|
});
|
|
525
525
|
try {
|
|
@@ -558,7 +558,7 @@ describe('webServer new routes integration', () => {
|
|
|
558
558
|
inTmuxDataDir: null,
|
|
559
559
|
dashboardDir: null,
|
|
560
560
|
dashboardDataDir: null,
|
|
561
|
-
|
|
561
|
+
dashboardProjectNames: [],
|
|
562
562
|
issueRepository,
|
|
563
563
|
issueTitleStateCache: new IssueTitleStateCache(),
|
|
564
564
|
port: 0,
|
|
@@ -594,7 +594,7 @@ describe('webServer new routes integration', () => {
|
|
|
594
594
|
inTmuxDataDir: null,
|
|
595
595
|
dashboardDir: null,
|
|
596
596
|
dashboardDataDir: null,
|
|
597
|
-
|
|
597
|
+
dashboardProjectNames: [],
|
|
598
598
|
issueRepository,
|
|
599
599
|
resolveProject: async (pjcode) =>
|
|
600
600
|
pjcode === 'umino' ? { pjcode, project: buildProject() } : null,
|
|
@@ -645,7 +645,7 @@ describe('webServer new routes integration', () => {
|
|
|
645
645
|
inTmuxDataDir: null,
|
|
646
646
|
dashboardDir: null,
|
|
647
647
|
dashboardDataDir: null,
|
|
648
|
-
|
|
648
|
+
dashboardProjectNames: [],
|
|
649
649
|
issueRepository,
|
|
650
650
|
resolveProject: async (pjcode) =>
|
|
651
651
|
pjcode === 'umino' ? { pjcode, project: buildProject() } : null,
|
|
@@ -693,7 +693,7 @@ describe('webServer new routes integration', () => {
|
|
|
693
693
|
inTmuxDataDir: null,
|
|
694
694
|
dashboardDir: null,
|
|
695
695
|
dashboardDataDir: null,
|
|
696
|
-
|
|
696
|
+
dashboardProjectNames: [],
|
|
697
697
|
issueRepository,
|
|
698
698
|
resolveProject: async (pjcode) =>
|
|
699
699
|
pjcode === 'umino' ? { pjcode, project: buildProject() } : null,
|
|
@@ -731,7 +731,7 @@ describe('webServer new routes integration', () => {
|
|
|
731
731
|
inTmuxDataDir: null,
|
|
732
732
|
dashboardDir: null,
|
|
733
733
|
dashboardDataDir: null,
|
|
734
|
-
|
|
734
|
+
dashboardProjectNames: [],
|
|
735
735
|
issueRepository,
|
|
736
736
|
resolveProject: async (pjcode) =>
|
|
737
737
|
pjcode === 'umino' ? { pjcode, project: buildProject() } : null,
|
|
@@ -784,7 +784,7 @@ describe('webServer new routes integration', () => {
|
|
|
784
784
|
inTmuxDataDir: null,
|
|
785
785
|
dashboardDir: null,
|
|
786
786
|
dashboardDataDir: null,
|
|
787
|
-
|
|
787
|
+
dashboardProjectNames: [],
|
|
788
788
|
port: 0,
|
|
789
789
|
});
|
|
790
790
|
try {
|
|
@@ -932,7 +932,7 @@ describe('webServer flat in-tmux-by-human route integration', () => {
|
|
|
932
932
|
inTmuxDataDir,
|
|
933
933
|
dashboardDir: null,
|
|
934
934
|
dashboardDataDir: null,
|
|
935
|
-
|
|
935
|
+
dashboardProjectNames: [],
|
|
936
936
|
port: 0,
|
|
937
937
|
});
|
|
938
938
|
return { server, tmpDir, inTmuxDataDir };
|
|
@@ -1048,7 +1048,7 @@ describe('webServer flat in-tmux-by-human route integration', () => {
|
|
|
1048
1048
|
inTmuxDataDir: null,
|
|
1049
1049
|
dashboardDir: null,
|
|
1050
1050
|
dashboardDataDir: null,
|
|
1051
|
-
|
|
1051
|
+
dashboardProjectNames: [],
|
|
1052
1052
|
port: 0,
|
|
1053
1053
|
});
|
|
1054
1054
|
try {
|
|
@@ -1121,9 +1121,9 @@ describe('webServer dashboard /tdpm.txt route integration', () => {
|
|
|
1121
1121
|
const writeDataFiles = (dataDir: string): void => {
|
|
1122
1122
|
fs.mkdirSync(path.join(dataDir, 'projects'), { recursive: true });
|
|
1123
1123
|
fs.writeFileSync(
|
|
1124
|
-
path.join(dataDir, 'projects', '
|
|
1124
|
+
path.join(dataDir, 'projects', 'umino.json'),
|
|
1125
1125
|
JSON.stringify({
|
|
1126
|
-
pjcode: '
|
|
1126
|
+
pjcode: 'umino',
|
|
1127
1127
|
capturedAt: '2026-06-26T00:00:00.000Z',
|
|
1128
1128
|
unread: 3,
|
|
1129
1129
|
todo: 1,
|
|
@@ -1136,9 +1136,9 @@ describe('webServer dashboard /tdpm.txt route integration', () => {
|
|
|
1136
1136
|
}),
|
|
1137
1137
|
);
|
|
1138
1138
|
fs.writeFileSync(
|
|
1139
|
-
path.join(dataDir, 'projects', '
|
|
1139
|
+
path.join(dataDir, 'projects', 'xcare.json'),
|
|
1140
1140
|
JSON.stringify({
|
|
1141
|
-
pjcode: '
|
|
1141
|
+
pjcode: 'xcare',
|
|
1142
1142
|
capturedAt: '2026-06-26T00:00:00.000Z',
|
|
1143
1143
|
unread: 0,
|
|
1144
1144
|
todo: 0,
|
|
@@ -1209,7 +1209,7 @@ describe('webServer dashboard /tdpm.txt route integration', () => {
|
|
|
1209
1209
|
inTmuxDataDir: null,
|
|
1210
1210
|
dashboardDir: overrides.dashboardDir,
|
|
1211
1211
|
dashboardDataDir: overrides.dashboardDataDir,
|
|
1212
|
-
|
|
1212
|
+
dashboardProjectNames: ['umino', 'xcare'],
|
|
1213
1213
|
port: 0,
|
|
1214
1214
|
});
|
|
1215
1215
|
return { server, tmpDir };
|
|
@@ -1453,7 +1453,7 @@ describe('webServer image proxy', () => {
|
|
|
1453
1453
|
inTmuxDataDir: null,
|
|
1454
1454
|
dashboardDir: null,
|
|
1455
1455
|
dashboardDataDir: null,
|
|
1456
|
-
|
|
1456
|
+
dashboardProjectNames: [],
|
|
1457
1457
|
githubToken: token,
|
|
1458
1458
|
imageFetcher: fetcher,
|
|
1459
1459
|
port: 0,
|
|
@@ -30,10 +30,11 @@ import {
|
|
|
30
30
|
composeDashboardText,
|
|
31
31
|
dashboardComposeFilesPresent,
|
|
32
32
|
} from './dashboardComposeService';
|
|
33
|
+
import { DASHBOARD_PROJECT_NAMES } from '../../../domain/usecases/dashboard/DashboardProjectCode';
|
|
33
34
|
|
|
34
35
|
export const DEFAULT_WEB_PORT = 9981;
|
|
35
36
|
|
|
36
|
-
export const
|
|
37
|
+
export const DEFAULT_DASHBOARD_PROJECT_NAMES = DASHBOARD_PROJECT_NAMES;
|
|
37
38
|
|
|
38
39
|
export const CONSOLE_TOKEN_HEADER = 'x-pv-token';
|
|
39
40
|
|
|
@@ -165,7 +166,7 @@ export type WebServerOptions = {
|
|
|
165
166
|
inTmuxDataDir: string | null;
|
|
166
167
|
dashboardDir: string | null;
|
|
167
168
|
dashboardDataDir: string | null;
|
|
168
|
-
|
|
169
|
+
dashboardProjectNames: string[];
|
|
169
170
|
githubToken?: string | null;
|
|
170
171
|
imageFetcher?: ImageFetcher | null;
|
|
171
172
|
issueRepository?: IssueRepository | null;
|
|
@@ -555,12 +556,12 @@ export const resolveDashboardContent = (
|
|
|
555
556
|
options.dashboardDataDir !== null &&
|
|
556
557
|
dashboardComposeFilesPresent({
|
|
557
558
|
dashboardDataDir: options.dashboardDataDir,
|
|
558
|
-
|
|
559
|
+
projectNames: options.dashboardProjectNames,
|
|
559
560
|
})
|
|
560
561
|
) {
|
|
561
562
|
const dashboardText = composeDashboardText({
|
|
562
563
|
dashboardDataDir: options.dashboardDataDir,
|
|
563
|
-
|
|
564
|
+
projectNames: options.dashboardProjectNames,
|
|
564
565
|
});
|
|
565
566
|
return Buffer.from(dashboardText, 'utf-8');
|
|
566
567
|
}
|
|
@@ -14,10 +14,7 @@ jest.mock('ky', () => ({
|
|
|
14
14
|
__esModule: true,
|
|
15
15
|
}));
|
|
16
16
|
|
|
17
|
-
import {
|
|
18
|
-
GraphqlProjectRepository,
|
|
19
|
-
resolveProjectCacheTtlMs,
|
|
20
|
-
} from './GraphqlProjectRepository';
|
|
17
|
+
import { GraphqlProjectRepository } from './GraphqlProjectRepository';
|
|
21
18
|
import { LocalStorageCacheRepository } from './LocalStorageCacheRepository';
|
|
22
19
|
import { LocalStorageRepository } from './LocalStorageRepository';
|
|
23
20
|
import { Project } from '../../domain/entities/Project';
|
|
@@ -98,7 +95,6 @@ const buildCacheStub = (overrides?: {
|
|
|
98
95
|
|
|
99
96
|
describe('GraphqlProjectRepository disk cache', () => {
|
|
100
97
|
const localStorageRepository = new LocalStorageRepository();
|
|
101
|
-
const thirtyMinutesMs = 30 * 60 * 1000;
|
|
102
98
|
|
|
103
99
|
beforeEach(() => {
|
|
104
100
|
jest.useFakeTimers();
|
|
@@ -109,23 +105,8 @@ describe('GraphqlProjectRepository disk cache', () => {
|
|
|
109
105
|
jest.useRealTimers();
|
|
110
106
|
});
|
|
111
107
|
|
|
112
|
-
describe('resolveProjectCacheTtlMs', () => {
|
|
113
|
-
it('defaults to 30 minutes when unset', () => {
|
|
114
|
-
expect(resolveProjectCacheTtlMs(undefined)).toBe(thirtyMinutesMs);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it('uses the configured millisecond value when valid', () => {
|
|
118
|
-
expect(resolveProjectCacheTtlMs('60000')).toBe(60000);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it('falls back to the default for non-numeric or negative values', () => {
|
|
122
|
-
expect(resolveProjectCacheTtlMs('abc')).toBe(thirtyMinutesMs);
|
|
123
|
-
expect(resolveProjectCacheTtlMs('-1')).toBe(thirtyMinutesMs);
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
|
|
127
108
|
describe('getProject', () => {
|
|
128
|
-
it('fetches via GraphQL
|
|
109
|
+
it('always fetches the project fresh via GraphQL without reading or writing the disk cache', async () => {
|
|
129
110
|
const cache = buildCacheStub();
|
|
130
111
|
mockPost.mockReturnValueOnce(mockJsonResponse(getProjectResponse));
|
|
131
112
|
const repository = new GraphqlProjectRepository(
|
|
@@ -138,75 +119,14 @@ describe('GraphqlProjectRepository disk cache', () => {
|
|
|
138
119
|
|
|
139
120
|
expect(project).toEqual(expectedProject);
|
|
140
121
|
expect(mockPost).toHaveBeenCalledTimes(1);
|
|
141
|
-
expect(cache.
|
|
142
|
-
`project-${projectId}`,
|
|
143
|
-
expectedProject,
|
|
144
|
-
);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it('returns the cached project without any GraphQL call when the cache is fresh', async () => {
|
|
148
|
-
const cache = buildCacheStub({
|
|
149
|
-
getLatest: jest.fn().mockResolvedValue({
|
|
150
|
-
value: expectedProject,
|
|
151
|
-
timestamp: new Date(),
|
|
152
|
-
} satisfies CacheEntry),
|
|
153
|
-
});
|
|
154
|
-
const repository = new GraphqlProjectRepository(
|
|
155
|
-
localStorageRepository,
|
|
156
|
-
'dummy',
|
|
157
|
-
cache,
|
|
158
|
-
);
|
|
159
|
-
|
|
160
|
-
const project = await repository.getProject(projectId);
|
|
161
|
-
|
|
162
|
-
expect(project).toEqual(expectedProject);
|
|
163
|
-
expect(mockPost).not.toHaveBeenCalled();
|
|
122
|
+
expect(cache.getLatest).not.toHaveBeenCalled();
|
|
164
123
|
expect(cache.set).not.toHaveBeenCalled();
|
|
165
124
|
});
|
|
166
125
|
|
|
167
|
-
it('
|
|
168
|
-
const staleTimestamp = new Date(Date.now() - thirtyMinutesMs - 1);
|
|
126
|
+
it('does not serve a project from the disk cache even when one is present', async () => {
|
|
169
127
|
const cache = buildCacheStub({
|
|
170
128
|
getLatest: jest.fn().mockResolvedValue({
|
|
171
129
|
value: expectedProject,
|
|
172
|
-
timestamp: staleTimestamp,
|
|
173
|
-
} satisfies CacheEntry),
|
|
174
|
-
});
|
|
175
|
-
mockPost.mockReturnValueOnce(mockJsonResponse(getProjectResponse));
|
|
176
|
-
const repository = new GraphqlProjectRepository(
|
|
177
|
-
localStorageRepository,
|
|
178
|
-
'dummy',
|
|
179
|
-
cache,
|
|
180
|
-
);
|
|
181
|
-
|
|
182
|
-
const project = await repository.getProject(projectId);
|
|
183
|
-
|
|
184
|
-
expect(project).toEqual(expectedProject);
|
|
185
|
-
expect(mockPost).toHaveBeenCalledTimes(1);
|
|
186
|
-
expect(cache.set).toHaveBeenCalledTimes(1);
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
it('falls back to a live GraphQL fetch when the cache read throws', async () => {
|
|
190
|
-
const cache = buildCacheStub({
|
|
191
|
-
getLatest: jest.fn().mockRejectedValue(new Error('corrupted file')),
|
|
192
|
-
});
|
|
193
|
-
mockPost.mockReturnValueOnce(mockJsonResponse(getProjectResponse));
|
|
194
|
-
const repository = new GraphqlProjectRepository(
|
|
195
|
-
localStorageRepository,
|
|
196
|
-
'dummy',
|
|
197
|
-
cache,
|
|
198
|
-
);
|
|
199
|
-
|
|
200
|
-
const project = await repository.getProject(projectId);
|
|
201
|
-
|
|
202
|
-
expect(project).toEqual(expectedProject);
|
|
203
|
-
expect(mockPost).toHaveBeenCalledTimes(1);
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it('falls back to a live GraphQL fetch when the cached value is malformed', async () => {
|
|
207
|
-
const cache = buildCacheStub({
|
|
208
|
-
getLatest: jest.fn().mockResolvedValue({
|
|
209
|
-
value: { unexpected: 'shape' },
|
|
210
130
|
timestamp: new Date(),
|
|
211
131
|
} satisfies CacheEntry),
|
|
212
132
|
});
|
|
@@ -221,27 +141,7 @@ describe('GraphqlProjectRepository disk cache', () => {
|
|
|
221
141
|
|
|
222
142
|
expect(project).toEqual(expectedProject);
|
|
223
143
|
expect(mockPost).toHaveBeenCalledTimes(1);
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
it('respects a custom TTL passed to the constructor', async () => {
|
|
227
|
-
const cache = buildCacheStub({
|
|
228
|
-
getLatest: jest.fn().mockResolvedValue({
|
|
229
|
-
value: expectedProject,
|
|
230
|
-
timestamp: new Date(Date.now() - 2000),
|
|
231
|
-
} satisfies CacheEntry),
|
|
232
|
-
});
|
|
233
|
-
mockPost.mockReturnValueOnce(mockJsonResponse(getProjectResponse));
|
|
234
|
-
const repository = new GraphqlProjectRepository(
|
|
235
|
-
localStorageRepository,
|
|
236
|
-
'dummy',
|
|
237
|
-
cache,
|
|
238
|
-
1000,
|
|
239
|
-
);
|
|
240
|
-
|
|
241
|
-
const project = await repository.getProject(projectId);
|
|
242
|
-
|
|
243
|
-
expect(project).toEqual(expectedProject);
|
|
244
|
-
expect(mockPost).toHaveBeenCalledTimes(1);
|
|
144
|
+
expect(cache.set).not.toHaveBeenCalled();
|
|
245
145
|
});
|
|
246
146
|
});
|
|
247
147
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import ky from 'ky';
|
|
2
|
-
import typia from 'typia';
|
|
3
2
|
import { BaseGitHubRepository } from './BaseGitHubRepository';
|
|
4
3
|
import { LocalStorageCacheRepository } from './LocalStorageCacheRepository';
|
|
5
4
|
import { LocalStorageRepository } from './LocalStorageRepository';
|
|
@@ -9,23 +8,7 @@ import { normalizeFieldName } from './utils';
|
|
|
9
8
|
|
|
10
9
|
const ONE_HOUR_MS = 60 * 60 * 1000;
|
|
11
10
|
|
|
12
|
-
const DEFAULT_PROJECT_CACHE_TTL_MS = 30 * 60 * 1000;
|
|
13
|
-
|
|
14
|
-
export const resolveProjectCacheTtlMs = (
|
|
15
|
-
rawValue: string | undefined,
|
|
16
|
-
): number => {
|
|
17
|
-
if (rawValue === undefined) {
|
|
18
|
-
return DEFAULT_PROJECT_CACHE_TTL_MS;
|
|
19
|
-
}
|
|
20
|
-
const parsed = Number(rawValue);
|
|
21
|
-
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
22
|
-
return DEFAULT_PROJECT_CACHE_TTL_MS;
|
|
23
|
-
}
|
|
24
|
-
return parsed;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
11
|
const PROJECT_ID_DISK_CACHE_KEY_PREFIX = 'projectId';
|
|
28
|
-
const PROJECT_DISK_CACHE_KEY_PREFIX = 'project';
|
|
29
12
|
|
|
30
13
|
export const convertToFieldOptionColor = (
|
|
31
14
|
color: string,
|
|
@@ -63,19 +46,14 @@ export class GraphqlProjectRepository
|
|
|
63
46
|
LocalStorageCacheRepository,
|
|
64
47
|
'getLatest' | 'set'
|
|
65
48
|
>;
|
|
66
|
-
private readonly projectCacheTtlMs: number;
|
|
67
49
|
|
|
68
50
|
constructor(
|
|
69
51
|
localStorageRepository: LocalStorageRepository,
|
|
70
52
|
ghToken: string = process.env.GH_TOKEN || 'dummy',
|
|
71
53
|
projectCache?: Pick<LocalStorageCacheRepository, 'getLatest' | 'set'>,
|
|
72
|
-
projectCacheTtlMs: number = resolveProjectCacheTtlMs(
|
|
73
|
-
process.env.TDPM_PROJECT_CACHE_TTL_MS,
|
|
74
|
-
),
|
|
75
54
|
) {
|
|
76
55
|
super(localStorageRepository, ghToken);
|
|
77
56
|
this.projectCache = projectCache;
|
|
78
|
-
this.projectCacheTtlMs = projectCacheTtlMs;
|
|
79
57
|
}
|
|
80
58
|
|
|
81
59
|
private readProjectIdFromDiskCache = async (
|
|
@@ -121,50 +99,6 @@ export class GraphqlProjectRepository
|
|
|
121
99
|
}
|
|
122
100
|
};
|
|
123
101
|
|
|
124
|
-
private readProjectFromDiskCache = async (
|
|
125
|
-
projectId: string,
|
|
126
|
-
): Promise<Project | null> => {
|
|
127
|
-
if (!this.projectCache) {
|
|
128
|
-
return null;
|
|
129
|
-
}
|
|
130
|
-
let cache: { value: object; timestamp: Date } | null;
|
|
131
|
-
try {
|
|
132
|
-
cache = await this.projectCache.getLatest(
|
|
133
|
-
`${PROJECT_DISK_CACHE_KEY_PREFIX}-${projectId}`,
|
|
134
|
-
);
|
|
135
|
-
} catch (error) {
|
|
136
|
-
return null;
|
|
137
|
-
}
|
|
138
|
-
if (!cache) {
|
|
139
|
-
return null;
|
|
140
|
-
}
|
|
141
|
-
const age = Date.now() - cache.timestamp.getTime();
|
|
142
|
-
if (age >= this.projectCacheTtlMs) {
|
|
143
|
-
return null;
|
|
144
|
-
}
|
|
145
|
-
if (!typia.is<Project>(cache.value)) {
|
|
146
|
-
return null;
|
|
147
|
-
}
|
|
148
|
-
return cache.value;
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
private writeProjectToDiskCache = async (
|
|
152
|
-
projectId: string,
|
|
153
|
-
project: Project,
|
|
154
|
-
): Promise<void> => {
|
|
155
|
-
if (!this.projectCache) {
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
try {
|
|
159
|
-
await this.projectCache.set(
|
|
160
|
-
`${PROJECT_DISK_CACHE_KEY_PREFIX}-${projectId}`,
|
|
161
|
-
project,
|
|
162
|
-
);
|
|
163
|
-
} catch (error) {
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
};
|
|
167
|
-
|
|
168
102
|
extractProjectFromUrl = (
|
|
169
103
|
projectUrl: string,
|
|
170
104
|
): {
|
|
@@ -280,10 +214,6 @@ export class GraphqlProjectRepository
|
|
|
280
214
|
return await this.fetchProjectId(owner, projectNumber);
|
|
281
215
|
};
|
|
282
216
|
getProject = async (projectId: Project['id']): Promise<Project | null> => {
|
|
283
|
-
const diskCached = await this.readProjectFromDiskCache(projectId);
|
|
284
|
-
if (diskCached) {
|
|
285
|
-
return diskCached;
|
|
286
|
-
}
|
|
287
217
|
const query = `query GetProjectV2($projectId: ID!) {
|
|
288
218
|
node(id: $projectId) {
|
|
289
219
|
... on ProjectV2 {
|
|
@@ -428,7 +358,7 @@ export class GraphqlProjectRepository
|
|
|
428
358
|
const completionDate50PercentConfidence = project.fields.nodes.find(
|
|
429
359
|
(field) => normalizeFieldName(field.name).startsWith('completiondate'),
|
|
430
360
|
);
|
|
431
|
-
|
|
361
|
+
return {
|
|
432
362
|
id: project.id,
|
|
433
363
|
url: project.url,
|
|
434
364
|
databaseId: project.databaseId,
|
|
@@ -489,8 +419,6 @@ export class GraphqlProjectRepository
|
|
|
489
419
|
}
|
|
490
420
|
: null,
|
|
491
421
|
};
|
|
492
|
-
await this.writeProjectToDiskCache(projectId, result);
|
|
493
|
-
return result;
|
|
494
422
|
};
|
|
495
423
|
getByUrl = async (url: string): Promise<Project> => {
|
|
496
424
|
const projectId = await this.findProjectIdByUrl(url);
|
|
@@ -466,7 +466,7 @@ describe('GraphqlProjectItemRepository', () => {
|
|
|
466
466
|
expect(requestedFirstSeries).toEqual([100, 50, 25, 12, 6, 3, 1]);
|
|
467
467
|
});
|
|
468
468
|
|
|
469
|
-
it('should
|
|
469
|
+
it('should sleep the 5000ms blanket delay between pages', async () => {
|
|
470
470
|
const localStorageRepository = new LocalStorageRepository();
|
|
471
471
|
const repository = new GraphqlProjectItemRepository(
|
|
472
472
|
localStorageRepository,
|
|
@@ -483,11 +483,8 @@ describe('GraphqlProjectItemRepository', () => {
|
|
|
483
483
|
const result = await resultPromise;
|
|
484
484
|
|
|
485
485
|
expect(result).toHaveLength(2);
|
|
486
|
-
expect(PAGINATION_DELAY_MS).
|
|
487
|
-
expect(setTimeoutSpy).
|
|
488
|
-
expect.any(Function),
|
|
489
|
-
5000,
|
|
490
|
-
);
|
|
486
|
+
expect(PAGINATION_DELAY_MS).toBe(5000);
|
|
487
|
+
expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 5000);
|
|
491
488
|
setTimeoutSpy.mockRestore();
|
|
492
489
|
});
|
|
493
490
|
|
|
@@ -20,7 +20,7 @@ export type ProjectItem = {
|
|
|
20
20
|
value: string | null;
|
|
21
21
|
}[];
|
|
22
22
|
};
|
|
23
|
-
export const PAGINATION_DELAY_MS =
|
|
23
|
+
export const PAGINATION_DELAY_MS = 5000;
|
|
24
24
|
export const FETCH_PROJECT_ITEMS_INITIAL_PAGE_SIZE = 100;
|
|
25
25
|
export const FETCH_PROJECT_ITEMS_GRAPHQL_ERROR_PAYLOAD_MAX_LENGTH = 4000;
|
|
26
26
|
export const RATE_LIMIT_MAX_RETRIES = 6;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DASHBOARD_DISPLAY_LABEL_BY_PROJECT_NAME,
|
|
3
|
+
DASHBOARD_PROJECT_NAMES,
|
|
4
|
+
toDashboardDisplayLabel,
|
|
5
|
+
} from './DashboardProjectCode';
|
|
6
|
+
|
|
7
|
+
describe('DashboardProjectCode', () => {
|
|
8
|
+
it('maps each full project name to its 2-char display label', () => {
|
|
9
|
+
expect(toDashboardDisplayLabel('umino')).toBe('um');
|
|
10
|
+
expect(toDashboardDisplayLabel('xmile')).toBe('xm');
|
|
11
|
+
expect(toDashboardDisplayLabel('xcare')).toBe('xc');
|
|
12
|
+
expect(toDashboardDisplayLabel('utage3')).toBe('ut');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('exposes the project names in the same order as the label mapping', () => {
|
|
16
|
+
expect(DASHBOARD_PROJECT_NAMES).toEqual([
|
|
17
|
+
'umino',
|
|
18
|
+
'xmile',
|
|
19
|
+
'xcare',
|
|
20
|
+
'utage3',
|
|
21
|
+
]);
|
|
22
|
+
expect(DASHBOARD_PROJECT_NAMES).toEqual(
|
|
23
|
+
Object.keys(DASHBOARD_DISPLAY_LABEL_BY_PROJECT_NAME),
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('throws for an unknown project name rather than guessing a label', () => {
|
|
28
|
+
expect(() => toDashboardDisplayLabel('unknown')).toThrow(
|
|
29
|
+
'Unknown dashboard project name: unknown',
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const DASHBOARD_DISPLAY_LABEL_BY_PROJECT_NAME: Record<string, string> = {
|
|
2
|
+
umino: 'um',
|
|
3
|
+
xmile: 'xm',
|
|
4
|
+
xcare: 'xc',
|
|
5
|
+
utage3: 'ut',
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const DASHBOARD_PROJECT_NAMES: string[] = Object.keys(
|
|
9
|
+
DASHBOARD_DISPLAY_LABEL_BY_PROJECT_NAME,
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
export const toDashboardDisplayLabel = (projectName: string): string => {
|
|
13
|
+
const label = DASHBOARD_DISPLAY_LABEL_BY_PROJECT_NAME[projectName];
|
|
14
|
+
if (label === undefined) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
`Unknown dashboard project name: ${projectName}. Add it to DASHBOARD_DISPLAY_LABEL_BY_PROJECT_NAME.`,
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
return label;
|
|
20
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ComposeDashboardInput } from '../../../domain/usecases/dashboard/ComposeDashboardUseCase';
|
|
2
2
|
export type DashboardComposeOptions = {
|
|
3
3
|
dashboardDataDir: string;
|
|
4
|
-
|
|
4
|
+
projectNames: string[];
|
|
5
5
|
};
|
|
6
6
|
export declare const buildComposeDashboardInput: (options: DashboardComposeOptions) => ComposeDashboardInput;
|
|
7
7
|
export declare const dashboardComposeFilesPresent: (options: DashboardComposeOptions) => boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dashboardComposeService.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/console/dashboardComposeService.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,qBAAqB,EAItB,MAAM,4DAA4D,CAAC;
|
|
1
|
+
{"version":3,"file":"dashboardComposeService.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/console/dashboardComposeService.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,qBAAqB,EAItB,MAAM,4DAA4D,CAAC;AAQpE,MAAM,MAAM,uBAAuB,GAAG;IACpC,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB,CAAC;AA8IF,eAAO,MAAM,0BAA0B,GACrC,SAAS,uBAAuB,KAC/B,qBAYF,CAAC;AAUF,eAAO,MAAM,4BAA4B,GACvC,SAAS,uBAAuB,KAC/B,OAYF,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAC/B,SAAS,uBAAuB,KAC/B,MACqE,CAAC"}
|
|
@@ -4,7 +4,7 @@ import { IssueTitleStateCache } from './consoleReadApi';
|
|
|
4
4
|
import { ConsoleProjectResolver } from './consoleOperationApi';
|
|
5
5
|
import { ImageFetcher } from './consoleImageProxy';
|
|
6
6
|
export declare const DEFAULT_WEB_PORT = 9981;
|
|
7
|
-
export declare const
|
|
7
|
+
export declare const DEFAULT_DASHBOARD_PROJECT_NAMES: string[];
|
|
8
8
|
export declare const CONSOLE_TOKEN_HEADER = "x-pv-token";
|
|
9
9
|
export declare const hasDotSegment: (requestPath: string) => boolean;
|
|
10
10
|
export declare const requiresToken: (requestPath: string) => boolean;
|
|
@@ -18,7 +18,7 @@ export type WebServerOptions = {
|
|
|
18
18
|
inTmuxDataDir: string | null;
|
|
19
19
|
dashboardDir: string | null;
|
|
20
20
|
dashboardDataDir: string | null;
|
|
21
|
-
|
|
21
|
+
dashboardProjectNames: string[];
|
|
22
22
|
githubToken?: string | null;
|
|
23
23
|
imageFetcher?: ImageFetcher | null;
|
|
24
24
|
issueRepository?: IssueRepository | null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webServer.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/console/webServer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAG7B,OAAO,EAAE,eAAe,EAAE,MAAM,6DAA6D,CAAC;AAM9F,OAAO,EACL,oBAAoB,EAOrB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAEL,sBAAsB,EAMvB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,YAAY,EAAqB,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"webServer.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/console/webServer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAG7B,OAAO,EAAE,eAAe,EAAE,MAAM,6DAA6D,CAAC;AAM9F,OAAO,EACL,oBAAoB,EAOrB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAEL,sBAAsB,EAMvB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,YAAY,EAAqB,MAAM,qBAAqB,CAAC;AAOtE,eAAO,MAAM,gBAAgB,OAAO,CAAC;AAErC,eAAO,MAAM,+BAA+B,UAA0B,CAAC;AAEvE,eAAO,MAAM,oBAAoB,eAAe,CAAC;AAmCjD,eAAO,MAAM,aAAa,GAAI,aAAa,MAAM,KAAG,OAGiB,CAAC;AAEtE,eAAO,MAAM,aAAa,GAAI,aAAa,MAAM,KAAG,OAGrB,CAAC;AAIhC,eAAO,MAAM,iBAAiB,GAAI,aAAa,MAAM,KAAG,OAmBvD,CAAC;AAEF,eAAO,MAAM,YAAY,GACvB,eAAe,MAAM,EACrB,eAAe,MAAM,GAAG,IAAI,KAC3B,OAAoE,CAAC;AAExE,eAAO,MAAM,oBAAoB,GAC/B,YAAY,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,EACpC,aAAa,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,KACzC,MAAM,GAAG,IAQX,CAAC;AAuCF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,qBAAqB,EAAE,MAAM,EAAE,CAAC;IAChC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,YAAY,CAAC,EAAE,YAAY,GAAG,IAAI,CAAC;IACnC,eAAe,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;IACzC,cAAc,CAAC,EAAE,sBAAsB,GAAG,IAAI,CAAC;IAC/C,oBAAoB,CAAC,EAAE,oBAAoB,GAAG,IAAI,CAAC;CACpD,CAAC;AAMF,eAAO,MAAM,sBAAsB,cAAc,CAAC;AAElD,eAAO,MAAM,wBAAwB,aAAa,CAAC;AAInD,eAAO,MAAM,wBAAwB,GACnC,cAAc,MAAM,EACpB,aAAa,MAAM,KAClB,MAAM,GAAG,IAWX,CAAC;AAEF,eAAO,MAAM,yBAAyB,GACpC,eAAe,MAAM,EACrB,aAAa,MAAM,KAClB,MAAM,GAAG,IAeX,CAAC;AA0UF,eAAO,MAAM,uBAAuB,GAClC,SAAS,gBAAgB,EACzB,aAAa,MAAM,KAClB,MAAM,GAAG,IAeX,CAAC;AAEF,eAAO,MAAM,gBAAgB,GAC3B,SAAS,gBAAgB,EACzB,SAAS,IAAI,CAAC,eAAe,EAC7B,UAAU,IAAI,CAAC,cAAc,KAC5B,OAAO,CAAC,IAAI,CAwEd,CAAC;AAcF,eAAO,MAAM,eAAe,GAAI,SAAS,gBAAgB,KAAG,IAAI,CAAC,MAM7D,CAAC;AAEL,MAAM,MAAM,qBAAqB,GAAG,gBAAgB,GAAG;IACrD,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,eAAO,MAAM,cAAc,GACzB,SAAS,qBAAqB,KAC7B,OAAO,CAAC,IAAI,CAAC,MAAM,CAQlB,CAAC"}
|
|
@@ -3,18 +3,14 @@ import { LocalStorageCacheRepository } from './LocalStorageCacheRepository';
|
|
|
3
3
|
import { LocalStorageRepository } from './LocalStorageRepository';
|
|
4
4
|
import { ProjectRepository } from '../../domain/usecases/adapter-interfaces/ProjectRepository';
|
|
5
5
|
import { FieldOption, Project } from '../../domain/entities/Project';
|
|
6
|
-
export declare const resolveProjectCacheTtlMs: (rawValue: string | undefined) => number;
|
|
7
6
|
export declare const convertToFieldOptionColor: (color: string) => FieldOption["color"];
|
|
8
7
|
export declare class GraphqlProjectRepository extends BaseGitHubRepository implements Pick<ProjectRepository, 'getProject' | 'findProjectIdByUrl' | 'getByUrl' | 'updateStoryList' | 'updateStatusList'> {
|
|
9
8
|
private readonly projectIdCache;
|
|
10
9
|
private readonly fetchProjectIdFailedAt;
|
|
11
10
|
private readonly projectCache?;
|
|
12
|
-
|
|
13
|
-
constructor(localStorageRepository: LocalStorageRepository, ghToken?: string, projectCache?: Pick<LocalStorageCacheRepository, 'getLatest' | 'set'>, projectCacheTtlMs?: number);
|
|
11
|
+
constructor(localStorageRepository: LocalStorageRepository, ghToken?: string, projectCache?: Pick<LocalStorageCacheRepository, 'getLatest' | 'set'>);
|
|
14
12
|
private readProjectIdFromDiskCache;
|
|
15
13
|
private writeProjectIdToDiskCache;
|
|
16
|
-
private readProjectFromDiskCache;
|
|
17
|
-
private writeProjectToDiskCache;
|
|
18
14
|
extractProjectFromUrl: (projectUrl: string) => {
|
|
19
15
|
owner: string;
|
|
20
16
|
projectNumber: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GraphqlProjectRepository.d.ts","sourceRoot":"","sources":["../../../src/adapter/repositories/GraphqlProjectRepository.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"GraphqlProjectRepository.d.ts","sourceRoot":"","sources":["../../../src/adapter/repositories/GraphqlProjectRepository.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAAE,2BAA2B,EAAE,MAAM,+BAA+B,CAAC;AAC5E,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,EAAE,iBAAiB,EAAE,MAAM,4DAA4D,CAAC;AAC/F,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,+BAA+B,CAAC;AAOrE,eAAO,MAAM,yBAAyB,GACpC,OAAO,MAAM,KACZ,WAAW,CAAC,OAAO,CAcrB,CAAC;AAEF,qBAAa,wBACX,SAAQ,oBACR,YACE,IAAI,CACF,iBAAiB,EACf,YAAY,GACZ,oBAAoB,GACpB,UAAU,GACV,iBAAiB,GACjB,kBAAkB,CACrB;IAEH,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA6B;IAC5D,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAA6B;IACpE,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC,CAG5B;gBAGA,sBAAsB,EAAE,sBAAsB,EAC9C,OAAO,GAAE,MAAwC,EACjD,YAAY,CAAC,EAAE,IAAI,CAAC,2BAA2B,EAAE,WAAW,GAAG,KAAK,CAAC;IAMvE,OAAO,CAAC,0BAA0B,CAwBhC;IAEF,OAAO,CAAC,yBAAyB,CAe/B;IAEF,qBAAqB,GACnB,YAAY,MAAM,KACjB;QACD,KAAK,EAAE,MAAM,CAAC;QACd,aAAa,EAAE,MAAM,CAAC;KACvB,CAMC;IACF,cAAc,GACZ,OAAO,MAAM,EACb,eAAe,MAAM,KACpB,OAAO,CAAC,MAAM,CAAC,CA4FhB;IACF,kBAAkB,GAChB,YAAY,MAAM,KACjB,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,CAG9B;IACF,UAAU,GAAU,WAAW,OAAO,CAAC,IAAI,CAAC,KAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CA8MpE;IACF,QAAQ,GAAU,KAAK,MAAM,KAAG,OAAO,CAAC,OAAO,CAAC,CAU9C;IACF,eAAe,GACb,SAAS,OAAO,EAChB,cAAc,CAAC,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,GAAG;QACvC,EAAE,EAAE,WAAW,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;KAC9B,CAAC,EAAE,KACH,OAAO,CAAC,WAAW,EAAE,CAAC,CA+CvB;IACF,gBAAgB,GACd,SAAS,OAAO,EAChB,eAAe,CAAC,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,GAAG;QACxC,EAAE,EAAE,WAAW,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;KAC9B,CAAC,EAAE,KACH,OAAO,CAAC,WAAW,EAAE,CAAC,CA4CvB;CACH"}
|