github-issue-tower-defence-management 1.167.0 → 1.168.1
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/.github/workflows/console-ui.yml +1 -0
- package/CHANGELOG.md +20 -0
- package/bin/adapter/entry-points/console/ui-dist/assets/index-DkAuY63u.js +86 -0
- package/bin/adapter/entry-points/console/ui-dist/index.html +1 -1
- package/bin/adapter/entry-points/console/ui-dist/sw.js +60 -0
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js +0 -6
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js.map +1 -1
- package/bin/domain/usecases/console/GenerateConsoleListsUseCase.js +7 -1
- package/bin/domain/usecases/console/GenerateConsoleListsUseCase.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/ci/consoleUiWorkflowTimeout.test.ts +61 -0
- package/src/adapter/entry-points/console/ui/public/sw.js +60 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleTabList.stories.tsx +9 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleTabList.test.tsx +31 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleTabList.tsx +8 -1
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleTabData.test.ts +111 -15
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleTabData.ts +100 -15
- package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.test.ts +70 -1
- package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.ts +42 -6
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +2 -0
- package/src/adapter/entry-points/console/ui/src/main.tsx +6 -0
- package/src/adapter/entry-points/console/ui-dist/assets/index-DkAuY63u.js +86 -0
- package/src/adapter/entry-points/console/ui-dist/index.html +1 -1
- package/src/adapter/entry-points/console/ui-dist/sw.js +60 -0
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +11 -30
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +0 -13
- package/src/domain/usecases/console/GenerateConsoleListsUseCase.test.ts +13 -0
- package/src/domain/usecases/console/GenerateConsoleListsUseCase.ts +6 -1
- package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/console/GenerateConsoleListsUseCase.d.ts.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-CrSqMAVR.js +0 -86
- package/src/adapter/entry-points/console/ui-dist/assets/index-CrSqMAVR.js +0 -86
|
@@ -16,6 +16,7 @@ export type ConsoleTabSnapshot = {
|
|
|
16
16
|
storyColors: ConsoleStoryColorSource;
|
|
17
17
|
stories: ConsoleStoryEntry[];
|
|
18
18
|
defaultNameWithOwner: string | null;
|
|
19
|
+
fromCache: boolean;
|
|
19
20
|
};
|
|
20
21
|
|
|
21
22
|
export type ConsoleTabDataState = {
|
|
@@ -61,7 +62,9 @@ const parseStoryEntries = (value: unknown): ConsoleStoryEntry[] => {
|
|
|
61
62
|
return value.filter(isRecord) as unknown as ConsoleStoryEntry[];
|
|
62
63
|
};
|
|
63
64
|
|
|
64
|
-
const
|
|
65
|
+
const parseSnapshotData = (
|
|
66
|
+
payload: unknown,
|
|
67
|
+
): Omit<ConsoleTabSnapshot, 'fromCache'> => ({
|
|
65
68
|
items: parseItems(payload),
|
|
66
69
|
generatedAt:
|
|
67
70
|
isRecord(payload) && typeof payload.generatedAt === 'string'
|
|
@@ -96,24 +99,106 @@ const buildListUrl = (pjcode: string, tab: ConsoleTabName): string =>
|
|
|
96
99
|
|
|
97
100
|
export const CONSOLE_TAB_REFRESH_INTERVAL_MS = 60000;
|
|
98
101
|
|
|
102
|
+
const CONSOLE_LIST_CACHE_NAME = 'console-list-v1';
|
|
103
|
+
|
|
104
|
+
const persistListSnapshot = (url: string, payload: unknown): void => {
|
|
105
|
+
try {
|
|
106
|
+
if (!('caches' in globalThis)) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
globalThis.caches
|
|
110
|
+
.open(CONSOLE_LIST_CACHE_NAME)
|
|
111
|
+
.then((cache) =>
|
|
112
|
+
cache.put(
|
|
113
|
+
url,
|
|
114
|
+
new Response(JSON.stringify(payload), {
|
|
115
|
+
headers: { 'Content-Type': 'application/json' },
|
|
116
|
+
}),
|
|
117
|
+
),
|
|
118
|
+
)
|
|
119
|
+
.catch((e: unknown) => {
|
|
120
|
+
console.warn('Failed to persist tab snapshot to cache:', e);
|
|
121
|
+
});
|
|
122
|
+
} catch (e: unknown) {
|
|
123
|
+
console.warn('Failed to access cache storage:', e);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const loadListSnapshotFromCache = async (
|
|
128
|
+
url: string,
|
|
129
|
+
): Promise<ConsoleTabSnapshot | null> => {
|
|
130
|
+
if (!('caches' in globalThis)) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
const cache = await globalThis.caches.open(CONSOLE_LIST_CACHE_NAME);
|
|
135
|
+
const cached = await cache.match(url);
|
|
136
|
+
if (cached === undefined) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
const payload: unknown = await cached.json();
|
|
140
|
+
return { ...parseSnapshotData(payload), fromCache: true };
|
|
141
|
+
} catch {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const fetchSingleSnapshot = async (
|
|
147
|
+
pjcode: string,
|
|
148
|
+
tabName: ConsoleTabName,
|
|
149
|
+
): Promise<{ snapshot: ConsoleTabSnapshot | null; error: Error | null }> => {
|
|
150
|
+
const url = buildListUrl(pjcode, tabName);
|
|
151
|
+
try {
|
|
152
|
+
const response = await fetch(url);
|
|
153
|
+
if (!response.ok) {
|
|
154
|
+
throw new Error(`HTTP ${response.status}`);
|
|
155
|
+
}
|
|
156
|
+
const payload: unknown = await response.json();
|
|
157
|
+
persistListSnapshot(url, payload);
|
|
158
|
+
return {
|
|
159
|
+
snapshot: { ...parseSnapshotData(payload), fromCache: false },
|
|
160
|
+
error: null,
|
|
161
|
+
};
|
|
162
|
+
} catch (e: unknown) {
|
|
163
|
+
const cached = await loadListSnapshotFromCache(url);
|
|
164
|
+
if (cached !== null) {
|
|
165
|
+
return { snapshot: cached, error: null };
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
snapshot: null,
|
|
169
|
+
error: e instanceof Error ? e : new Error(String(e)),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
99
174
|
const fetchSnapshots = async (
|
|
100
175
|
pjcode: string,
|
|
101
|
-
): Promise<
|
|
102
|
-
|
|
176
|
+
): Promise<{
|
|
177
|
+
snapshots: Record<ConsoleTabName, ConsoleTabSnapshot | null>;
|
|
178
|
+
firstError: Error | null;
|
|
179
|
+
}> => {
|
|
180
|
+
const results = await Promise.all(
|
|
103
181
|
CONSOLE_TABS.map(async (tab) => {
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
throw new Error(`HTTP ${response.status}`);
|
|
107
|
-
}
|
|
108
|
-
const payload: unknown = await response.json();
|
|
109
|
-
return [tab.name, parseSnapshot(payload)] as const;
|
|
182
|
+
const { snapshot, error } = await fetchSingleSnapshot(pjcode, tab.name);
|
|
183
|
+
return { tabName: tab.name, snapshot, error };
|
|
110
184
|
}),
|
|
111
185
|
);
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
186
|
+
|
|
187
|
+
const snapshots = emptySnapshots();
|
|
188
|
+
let firstError: Error | null = null;
|
|
189
|
+
|
|
190
|
+
for (const { tabName, snapshot, error } of results) {
|
|
191
|
+
snapshots[tabName] = snapshot;
|
|
192
|
+
if (error !== null && firstError === null) {
|
|
193
|
+
firstError = error;
|
|
194
|
+
}
|
|
115
195
|
}
|
|
116
|
-
|
|
196
|
+
|
|
197
|
+
const allNull = Object.values(snapshots).every((s) => s === null);
|
|
198
|
+
return {
|
|
199
|
+
snapshots,
|
|
200
|
+
firstError: allNull ? firstError : null,
|
|
201
|
+
};
|
|
117
202
|
};
|
|
118
203
|
|
|
119
204
|
export const useConsoleTabData = (
|
|
@@ -140,12 +225,12 @@ export const useConsoleTabData = (
|
|
|
140
225
|
|
|
141
226
|
const load = (): void => {
|
|
142
227
|
fetchSnapshots(pjcode)
|
|
143
|
-
.then((next) => {
|
|
228
|
+
.then(({ snapshots: next, firstError }) => {
|
|
144
229
|
if (cancelled) {
|
|
145
230
|
return;
|
|
146
231
|
}
|
|
147
232
|
setSnapshots(next);
|
|
148
|
-
setError(null);
|
|
233
|
+
setError(firstError !== null ? firstError.message : null);
|
|
149
234
|
setIsLoading(false);
|
|
150
235
|
})
|
|
151
236
|
.catch((cause: unknown) => {
|
|
@@ -25,7 +25,40 @@ const mockFetchFailureOnce = (status: number, rawBody: string): jest.Mock => {
|
|
|
25
25
|
return fetchMock;
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
+
type MockCacheEntry = { json: jest.Mock };
|
|
29
|
+
type MockCache = {
|
|
30
|
+
match: jest.Mock<Promise<MockCacheEntry | undefined>>;
|
|
31
|
+
put: jest.Mock<Promise<void>>;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const installMockCaches = (
|
|
35
|
+
matchResult: MockCacheEntry | undefined,
|
|
36
|
+
): MockCache => {
|
|
37
|
+
const mockCache: MockCache = {
|
|
38
|
+
match: jest.fn(async () => matchResult),
|
|
39
|
+
put: jest.fn(async () => undefined),
|
|
40
|
+
};
|
|
41
|
+
Object.defineProperty(global, 'caches', {
|
|
42
|
+
value: { open: jest.fn(async () => mockCache) },
|
|
43
|
+
writable: true,
|
|
44
|
+
configurable: true,
|
|
45
|
+
});
|
|
46
|
+
return mockCache;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const removeMockCaches = (): void => {
|
|
50
|
+
Reflect.deleteProperty(global as Record<string, unknown>, 'caches');
|
|
51
|
+
};
|
|
52
|
+
|
|
28
53
|
describe('createConsoleApiClient', () => {
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
removeMockCaches();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
afterEach(() => {
|
|
59
|
+
removeMockCaches();
|
|
60
|
+
});
|
|
61
|
+
|
|
29
62
|
it('reads the item body from the api root without a token query', async () => {
|
|
30
63
|
const fetchMock = mockFetchOnce({ body: '# Title' });
|
|
31
64
|
const client = createConsoleApiClient();
|
|
@@ -243,13 +276,49 @@ describe('createConsoleApiClient', () => {
|
|
|
243
276
|
});
|
|
244
277
|
});
|
|
245
278
|
|
|
246
|
-
it('throws on a non-ok response', async () => {
|
|
279
|
+
it('throws on a non-ok response when no cache is available', async () => {
|
|
247
280
|
mockFetchOnce({}, false);
|
|
248
281
|
const client = createConsoleApiClient();
|
|
249
282
|
await expect(
|
|
250
283
|
client.fetchComments('https://github.com/o/r/issues/1'),
|
|
251
284
|
).rejects.toThrow('HTTP 500');
|
|
252
285
|
});
|
|
286
|
+
|
|
287
|
+
it('returns cached comments when the network fetch rejects', async () => {
|
|
288
|
+
const cachedBody = {
|
|
289
|
+
comments: [
|
|
290
|
+
{
|
|
291
|
+
author: 'alice',
|
|
292
|
+
body: 'hello',
|
|
293
|
+
createdAt: '2026-06-19T00:00:00.000Z',
|
|
294
|
+
},
|
|
295
|
+
],
|
|
296
|
+
};
|
|
297
|
+
installMockCaches({ json: jest.fn(async () => cachedBody) });
|
|
298
|
+
global.fetch = jest
|
|
299
|
+
.fn()
|
|
300
|
+
.mockRejectedValue(new Error('offline')) as unknown as typeof fetch;
|
|
301
|
+
|
|
302
|
+
const client = createConsoleApiClient();
|
|
303
|
+
const comments = await client.fetchComments(
|
|
304
|
+
'https://github.com/o/r/issues/1',
|
|
305
|
+
);
|
|
306
|
+
expect(comments).toEqual([
|
|
307
|
+
{ author: 'alice', body: 'hello', createdAt: '2026-06-19T00:00:00.000Z' },
|
|
308
|
+
]);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('throws when the network fails and no cache entry exists', async () => {
|
|
312
|
+
installMockCaches(undefined);
|
|
313
|
+
global.fetch = jest
|
|
314
|
+
.fn()
|
|
315
|
+
.mockRejectedValue(new Error('offline')) as unknown as typeof fetch;
|
|
316
|
+
|
|
317
|
+
const client = createConsoleApiClient();
|
|
318
|
+
await expect(
|
|
319
|
+
client.fetchComments('https://github.com/o/r/issues/1'),
|
|
320
|
+
).rejects.toThrow('offline');
|
|
321
|
+
});
|
|
253
322
|
});
|
|
254
323
|
|
|
255
324
|
describe('postConsoleOperation', () => {
|
|
@@ -70,17 +70,53 @@ const getNumber = (value: unknown): number =>
|
|
|
70
70
|
|
|
71
71
|
const getBoolean = (value: unknown): boolean => value === true;
|
|
72
72
|
|
|
73
|
+
const CONSOLE_API_CACHE_NAME = 'console-api-v1';
|
|
74
|
+
|
|
73
75
|
const requestJson = async (
|
|
74
76
|
apiPath: string,
|
|
75
77
|
resourceUrl: string,
|
|
76
78
|
): Promise<unknown> => {
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
const url = `${apiPath}?url=${encodeURIComponent(resourceUrl)}`;
|
|
80
|
+
try {
|
|
81
|
+
const response = await fetch(url);
|
|
82
|
+
if (!response.ok) {
|
|
83
|
+
throw new Error(`HTTP ${response.status}`);
|
|
84
|
+
}
|
|
85
|
+
const payload: unknown = await response.json();
|
|
86
|
+
try {
|
|
87
|
+
if ('caches' in globalThis) {
|
|
88
|
+
globalThis.caches
|
|
89
|
+
.open(CONSOLE_API_CACHE_NAME)
|
|
90
|
+
.then((cache) =>
|
|
91
|
+
cache.put(
|
|
92
|
+
url,
|
|
93
|
+
new Response(JSON.stringify(payload), {
|
|
94
|
+
headers: { 'Content-Type': 'application/json' },
|
|
95
|
+
}),
|
|
96
|
+
),
|
|
97
|
+
)
|
|
98
|
+
.catch((e: unknown) => {
|
|
99
|
+
console.warn('Failed to persist API response to cache:', e);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
} catch (e: unknown) {
|
|
103
|
+
console.warn('Failed to access cache storage:', e);
|
|
104
|
+
}
|
|
105
|
+
return payload;
|
|
106
|
+
} catch (e: unknown) {
|
|
107
|
+
try {
|
|
108
|
+
if ('caches' in globalThis) {
|
|
109
|
+
const cache = await globalThis.caches.open(CONSOLE_API_CACHE_NAME);
|
|
110
|
+
const cached = await cache.match(url);
|
|
111
|
+
if (cached !== undefined) {
|
|
112
|
+
return cached.json();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
} catch {
|
|
116
|
+
// cache read failure, fall through to original error
|
|
117
|
+
}
|
|
118
|
+
throw e;
|
|
82
119
|
}
|
|
83
|
-
return response.json();
|
|
84
120
|
};
|
|
85
121
|
|
|
86
122
|
const parseComments = (payload: unknown): ConsoleComment[] => {
|
|
@@ -126,6 +126,7 @@ export const ConsolePage = () => {
|
|
|
126
126
|
const statusOptions = activeSnapshot?.statusOptions ?? [];
|
|
127
127
|
const storyOptions = activeSnapshot?.storyOptions ?? [];
|
|
128
128
|
const generatedAt = activeSnapshot?.generatedAt ?? null;
|
|
129
|
+
const fromCache = activeSnapshot?.fromCache ?? false;
|
|
129
130
|
|
|
130
131
|
const selectedItem = useMemo<ConsoleListItem | null>(() => {
|
|
131
132
|
if (selectedItemKey === null || activeSnapshot === null) {
|
|
@@ -274,6 +275,7 @@ export const ConsolePage = () => {
|
|
|
274
275
|
counts={counts}
|
|
275
276
|
pjcode={pjcode}
|
|
276
277
|
generatedAt={generatedAt}
|
|
278
|
+
fromCache={fromCache}
|
|
277
279
|
tabHref={navigation.tabHref}
|
|
278
280
|
onSelectTab={navigation.selectTab}
|
|
279
281
|
/>
|
|
@@ -3,6 +3,12 @@ import { createRoot } from 'react-dom/client';
|
|
|
3
3
|
import { ConsolePage } from '@/features/console/pages/ConsolePage';
|
|
4
4
|
import './index.css';
|
|
5
5
|
|
|
6
|
+
if ('serviceWorker' in navigator) {
|
|
7
|
+
navigator.serviceWorker.register('/sw.js').catch((e: unknown) => {
|
|
8
|
+
console.warn('Service worker registration failed:', e);
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
|
|
6
12
|
const container = document.getElementById('root');
|
|
7
13
|
if (container === null) {
|
|
8
14
|
throw new Error('Root container #root not found');
|