github-issue-tower-defence-management 1.106.0 → 1.107.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 (26) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +5 -0
  3. package/bin/adapter/entry-points/cli/index.js +18 -1
  4. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  5. package/bin/adapter/entry-points/console/dashboardComposeService.js +178 -0
  6. package/bin/adapter/entry-points/console/dashboardComposeService.js.map +1 -0
  7. package/bin/adapter/entry-points/console/webServer.js +29 -7
  8. package/bin/adapter/entry-points/console/webServer.js.map +1 -1
  9. package/bin/domain/usecases/dashboard/ComposeDashboardUseCase.js +161 -0
  10. package/bin/domain/usecases/dashboard/ComposeDashboardUseCase.js.map +1 -0
  11. package/package.json +1 -1
  12. package/src/adapter/entry-points/cli/index.ts +36 -2
  13. package/src/adapter/entry-points/console/dashboardComposeService.test.ts +443 -0
  14. package/src/adapter/entry-points/console/dashboardComposeService.ts +200 -0
  15. package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +2 -0
  16. package/src/adapter/entry-points/console/webServer.test.ts +255 -54
  17. package/src/adapter/entry-points/console/webServer.ts +43 -10
  18. package/src/domain/usecases/dashboard/ComposeDashboardUseCase.test.ts +405 -0
  19. package/src/domain/usecases/dashboard/ComposeDashboardUseCase.ts +220 -0
  20. package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
  21. package/types/adapter/entry-points/console/dashboardComposeService.d.ts +9 -0
  22. package/types/adapter/entry-points/console/dashboardComposeService.d.ts.map +1 -0
  23. package/types/adapter/entry-points/console/webServer.d.ts +4 -0
  24. package/types/adapter/entry-points/console/webServer.d.ts.map +1 -1
  25. package/types/domain/usecases/dashboard/ComposeDashboardUseCase.d.ts +28 -0
  26. package/types/domain/usecases/dashboard/ComposeDashboardUseCase.d.ts.map +1 -0
@@ -0,0 +1,405 @@
1
+ import {
2
+ ComposeDashboardInput,
3
+ ComposeDashboardUseCase,
4
+ PROJECT_ROW_WIDTH_BUDGET,
5
+ formatMachineStatusLine,
6
+ formatProjectHeaderLine,
7
+ formatProjectRowLine,
8
+ formatResetCountdown,
9
+ formatTokenRowLine,
10
+ roundHalfToEven,
11
+ } from './ComposeDashboardUseCase';
12
+ import { DashboardRow } from './GenerateDashboardRowUseCase';
13
+ import { TokenStatus } from './GenerateTokenStatusUseCase';
14
+
15
+ const codePointLength = (value: string): number => [...value].length;
16
+
17
+ const projectRow = (overrides: Partial<DashboardRow>): DashboardRow => ({
18
+ unread: 0,
19
+ todo: 0,
20
+ qc: 0,
21
+ fail: 0,
22
+ pr: 0,
23
+ ws: 0,
24
+ dep: 0,
25
+ blocker: 0,
26
+ ...overrides,
27
+ });
28
+
29
+ const tokenStatus = (overrides: Partial<TokenStatus>): TokenStatus => ({
30
+ name: 'token',
31
+ fiveHourUtilizationPercent: 0,
32
+ fiveHourResetSeconds: 0,
33
+ sevenDayUtilizationPercent: 0,
34
+ sevenDayResetSeconds: 0,
35
+ color: 'G',
36
+ prep: 0,
37
+ hum: 0,
38
+ ...overrides,
39
+ });
40
+
41
+ describe('roundHalfToEven', () => {
42
+ it('rounds halves to the nearest even integer like Python round()', () => {
43
+ expect(roundHalfToEven(0.5)).toBe(0);
44
+ expect(roundHalfToEven(1.5)).toBe(2);
45
+ expect(roundHalfToEven(2.5)).toBe(2);
46
+ expect(roundHalfToEven(3.5)).toBe(4);
47
+ expect(roundHalfToEven(16.0)).toBe(16);
48
+ expect(roundHalfToEven(0.49)).toBe(0);
49
+ expect(roundHalfToEven(0.51)).toBe(1);
50
+ });
51
+ });
52
+
53
+ describe('formatResetCountdown', () => {
54
+ it('renders d/h/m with zero-padded hours and minutes', () => {
55
+ expect(formatResetCountdown(0)).toBe('0d00h00');
56
+ expect(formatResetCountdown(3600)).toBe('0d01h00');
57
+ expect(formatResetCountdown(7200)).toBe('0d02h00');
58
+ expect(formatResetCountdown(86400 * 5)).toBe('5d00h00');
59
+ expect(formatResetCountdown(86400 + 3600 + 60)).toBe('1d01h01');
60
+ });
61
+
62
+ it('renders zero for a negative remaining countdown', () => {
63
+ expect(formatResetCountdown(-10)).toBe('0d00h00');
64
+ });
65
+ });
66
+
67
+ describe('formatMachineStatusLine', () => {
68
+ it('renders the host-metrics line from a machine status', () => {
69
+ expect(
70
+ formatMachineStatusLine({
71
+ memPct: 55,
72
+ cpuPct: 62,
73
+ load: [16, 23, 40],
74
+ cycleMinutes: 14,
75
+ }),
76
+ ).toBe('M55% C62% LA 16 23 40 cy14');
77
+ });
78
+
79
+ it('rounds loads with half-to-even and renders integers', () => {
80
+ expect(
81
+ formatMachineStatusLine({
82
+ memPct: 62,
83
+ cpuPct: 31,
84
+ load: [1.2, 0.98, 0.75],
85
+ cycleMinutes: 14,
86
+ }),
87
+ ).toBe('M62% C31% LA 1 1 1 cy14');
88
+ });
89
+
90
+ it('falls back to placeholders when the machine status is absent', () => {
91
+ expect(formatMachineStatusLine(null)).toBe('M?% C?% LA ? ? ? cy-');
92
+ });
93
+
94
+ it('renders cy- when cycle minutes is null', () => {
95
+ expect(
96
+ formatMachineStatusLine({
97
+ memPct: 1,
98
+ cpuPct: 2,
99
+ load: [0, 0, 0],
100
+ cycleMinutes: null,
101
+ }),
102
+ ).toBe('M1% C2% LA 0 0 0 cy-');
103
+ });
104
+
105
+ it('stays within the 32 character width budget at worst case', () => {
106
+ const line = formatMachineStatusLine({
107
+ memPct: 100,
108
+ cpuPct: 100,
109
+ load: [108.5, 120.25, 95.1],
110
+ cycleMinutes: 999,
111
+ });
112
+ expect(line).toBe('M100% C100% LA 108 120 95 cy999');
113
+ expect(codePointLength(line)).toBeLessThanOrEqual(PROJECT_ROW_WIDTH_BUDGET);
114
+ });
115
+ });
116
+
117
+ describe('formatProjectHeaderLine', () => {
118
+ it('renders the fixed project grid header', () => {
119
+ expect(formatProjectHeaderLine()).toBe('pj unr tdo aqc fal prp aws dep');
120
+ });
121
+
122
+ it('fits the 32 character width budget exactly', () => {
123
+ expect(codePointLength(formatProjectHeaderLine())).toBe(
124
+ PROJECT_ROW_WIDTH_BUDGET,
125
+ );
126
+ });
127
+ });
128
+
129
+ describe('formatProjectRowLine', () => {
130
+ it('renders a present row with its severity dot and counts', () => {
131
+ expect(
132
+ formatProjectRowLine({
133
+ code: 'um',
134
+ row: projectRow({ unread: 3, todo: 1, qc: 2, ws: 4, dep: 1 }),
135
+ }),
136
+ ).toBe('🟢um 3 1 2 0 0 4 1');
137
+ });
138
+
139
+ it('renders placeholder cells with a blank dot for an absent project file', () => {
140
+ expect(formatProjectRowLine({ code: 'xc', row: null })).toBe(
141
+ ' xc -- -- -- -- -- -- --',
142
+ );
143
+ });
144
+
145
+ it('caps a count above 999 at 999', () => {
146
+ expect(
147
+ formatProjectRowLine({ code: 'um', row: projectRow({ unread: 1500 }) }),
148
+ ).toBe('🟠um 999 0 0 0 0 0 0');
149
+ });
150
+
151
+ it('applies the five level severity dot rules in descending order', () => {
152
+ expect(
153
+ formatProjectRowLine({ code: 'um', row: projectRow({ blocker: 2 }) }),
154
+ ).toContain('🔴');
155
+ expect(
156
+ formatProjectRowLine({ code: 'um', row: projectRow({ blocker: 1 }) }),
157
+ ).toContain('🟣');
158
+ expect(
159
+ formatProjectRowLine({ code: 'um', row: projectRow({ unread: 10 }) }),
160
+ ).toContain('🟠');
161
+ expect(
162
+ formatProjectRowLine({ code: 'um', row: projectRow({ qc: 15 }) }),
163
+ ).toContain('🟠');
164
+ expect(
165
+ formatProjectRowLine({ code: 'um', row: projectRow({ fail: 5 }) }),
166
+ ).toContain('🟠');
167
+ expect(
168
+ formatProjectRowLine({ code: 'um', row: projectRow({ unread: 5 }) }),
169
+ ).toContain('🟡');
170
+ expect(
171
+ formatProjectRowLine({ code: 'um', row: projectRow({ qc: 10 }) }),
172
+ ).toContain('🟡');
173
+ expect(
174
+ formatProjectRowLine({ code: 'um', row: projectRow({ fail: 3 }) }),
175
+ ).toContain('🟡');
176
+ expect(
177
+ formatProjectRowLine({
178
+ code: 'um',
179
+ row: projectRow({ unread: 4, qc: 9, fail: 2 }),
180
+ }),
181
+ ).toContain('🟢');
182
+ });
183
+
184
+ it('keeps present and absent rows within the 32 code point width budget', () => {
185
+ const present = formatProjectRowLine({
186
+ code: 'xm',
187
+ row: projectRow({
188
+ unread: 999,
189
+ todo: 999,
190
+ qc: 999,
191
+ fail: 999,
192
+ pr: 999,
193
+ ws: 999,
194
+ dep: 999,
195
+ }),
196
+ });
197
+ const absent = formatProjectRowLine({ code: 'xc', row: null });
198
+ expect(codePointLength(present)).toBeLessThanOrEqual(
199
+ PROJECT_ROW_WIDTH_BUDGET,
200
+ );
201
+ expect(codePointLength(absent)).toBeLessThanOrEqual(
202
+ PROJECT_ROW_WIDTH_BUDGET,
203
+ );
204
+ });
205
+ });
206
+
207
+ describe('formatTokenRowLine', () => {
208
+ it('renders a token row with utilization, reset countdown, prep and hum', () => {
209
+ expect(
210
+ formatTokenRowLine(
211
+ tokenStatus({
212
+ name: 'alice',
213
+ fiveHourUtilizationPercent: 10,
214
+ fiveHourResetSeconds: 3600,
215
+ sevenDayUtilizationPercent: 12,
216
+ sevenDayResetSeconds: 86400 * 5,
217
+ color: 'G',
218
+ prep: 2,
219
+ hum: 1,
220
+ }),
221
+ ),
222
+ ).toBe('🟢alice 10% 0d01h00 12% 5d00h00 2 1');
223
+ });
224
+
225
+ it('pads short names to four characters with underscores', () => {
226
+ expect(
227
+ formatTokenRowLine(
228
+ tokenStatus({
229
+ name: 'bob',
230
+ fiveHourUtilizationPercent: 100,
231
+ fiveHourResetSeconds: 0,
232
+ sevenDayUtilizationPercent: 95,
233
+ sevenDayResetSeconds: 7200,
234
+ color: 'K',
235
+ }),
236
+ ),
237
+ ).toBe('⚪bob_ 100% 0d00h00 95% 0d02h00 0 0');
238
+ });
239
+
240
+ it('renders question marks when window data is unavailable', () => {
241
+ expect(
242
+ formatTokenRowLine(
243
+ tokenStatus({
244
+ name: 'carolxx',
245
+ fiveHourUtilizationPercent: null,
246
+ fiveHourResetSeconds: null,
247
+ sevenDayUtilizationPercent: null,
248
+ sevenDayResetSeconds: null,
249
+ color: 'Y',
250
+ prep: 1,
251
+ hum: 0,
252
+ }),
253
+ ),
254
+ ).toBe('🟡carolxx ? ? ? ? 1 0');
255
+ });
256
+ });
257
+
258
+ describe('ComposeDashboardUseCase', () => {
259
+ const representativeInput: ComposeDashboardInput = {
260
+ machineStatus: {
261
+ memPct: 55,
262
+ cpuPct: 62,
263
+ load: [16, 23, 40],
264
+ cycleMinutes: 14,
265
+ },
266
+ projects: [
267
+ {
268
+ code: 'um',
269
+ row: projectRow({ unread: 3, todo: 1, qc: 2, ws: 4, dep: 1 }),
270
+ },
271
+ {
272
+ code: 'xm',
273
+ row: projectRow({ unread: 12, qc: 16, fail: 6, pr: 1 }),
274
+ },
275
+ { code: 'xc', row: null },
276
+ { code: 'ut', row: projectRow({ blocker: 1 }) },
277
+ ],
278
+ tokens: [
279
+ tokenStatus({
280
+ name: 'alice',
281
+ fiveHourUtilizationPercent: 10,
282
+ fiveHourResetSeconds: 3600,
283
+ sevenDayUtilizationPercent: 12,
284
+ sevenDayResetSeconds: 86400 * 5,
285
+ color: 'G',
286
+ prep: 2,
287
+ hum: 1,
288
+ }),
289
+ tokenStatus({
290
+ name: 'bob',
291
+ fiveHourUtilizationPercent: 100,
292
+ fiveHourResetSeconds: 0,
293
+ sevenDayUtilizationPercent: 95,
294
+ sevenDayResetSeconds: 7200,
295
+ color: 'K',
296
+ prep: 0,
297
+ hum: 0,
298
+ }),
299
+ tokenStatus({
300
+ name: 'carolxx',
301
+ fiveHourUtilizationPercent: null,
302
+ fiveHourResetSeconds: null,
303
+ sevenDayUtilizationPercent: null,
304
+ sevenDayResetSeconds: null,
305
+ color: 'Y',
306
+ prep: 1,
307
+ hum: 0,
308
+ }),
309
+ ],
310
+ };
311
+
312
+ const expectedBody =
313
+ '<tt>M55%&nbsp;C62%&nbsp;LA&nbsp;16&nbsp;23&nbsp;40&nbsp;cy14</tt><br>\n' +
314
+ '<tt>pj&nbsp;&nbsp;&nbsp;unr&nbsp;tdo&nbsp;aqc&nbsp;fal&nbsp;prp&nbsp;aws&nbsp;dep</tt><br>\n' +
315
+ '<tt>🟢um&nbsp;&nbsp;&nbsp;3&nbsp;&nbsp;&nbsp;1&nbsp;&nbsp;&nbsp;2&nbsp;&nbsp;&nbsp;0&nbsp;&nbsp;&nbsp;0&nbsp;&nbsp;&nbsp;4&nbsp;&nbsp;&nbsp;1</tt><br>\n' +
316
+ '<tt>🟠xm&nbsp;&nbsp;12&nbsp;&nbsp;&nbsp;0&nbsp;&nbsp;16&nbsp;&nbsp;&nbsp;6&nbsp;&nbsp;&nbsp;1&nbsp;&nbsp;&nbsp;0&nbsp;&nbsp;&nbsp;0</tt><br>\n' +
317
+ '<tt>&nbsp;&nbsp;xc&nbsp;&nbsp;--&nbsp;&nbsp;--&nbsp;&nbsp;--&nbsp;&nbsp;--&nbsp;&nbsp;--&nbsp;&nbsp;--&nbsp;&nbsp;--</tt><br>\n' +
318
+ '<tt>🟣ut&nbsp;&nbsp;&nbsp;0&nbsp;&nbsp;&nbsp;0&nbsp;&nbsp;&nbsp;0&nbsp;&nbsp;&nbsp;0&nbsp;&nbsp;&nbsp;0&nbsp;&nbsp;&nbsp;0&nbsp;&nbsp;&nbsp;0</tt><br>\n' +
319
+ '<tt></tt><br>\n' +
320
+ '<tt>⚪bob_&nbsp;100%&nbsp;0d00h00&nbsp;&nbsp;95%&nbsp;0d02h00&nbsp;0&nbsp;0</tt><br>\n' +
321
+ '<tt>🟢alice&nbsp;&nbsp;10%&nbsp;0d01h00&nbsp;&nbsp;12%&nbsp;5d00h00&nbsp;2&nbsp;1</tt><br>\n' +
322
+ '<tt>🟡carolxx&nbsp;&nbsp;&nbsp;&nbsp;?&nbsp;?&nbsp;&nbsp;&nbsp;&nbsp;?&nbsp;?&nbsp;1&nbsp;0</tt><br>\n';
323
+
324
+ it('composes byte-identical dashboard text for representative inputs', () => {
325
+ expect(new ComposeDashboardUseCase().run(representativeInput)).toBe(
326
+ expectedBody,
327
+ );
328
+ });
329
+
330
+ it('sorts token rows by seven day reset ascending with null resets last', () => {
331
+ const output = new ComposeDashboardUseCase().run(representativeInput);
332
+ const bobIndex = output.indexOf('⚪bob_');
333
+ const aliceIndex = output.indexOf('🟢alice');
334
+ const carolIndex = output.indexOf('🟡carolxx');
335
+ expect(bobIndex).toBeLessThan(aliceIndex);
336
+ expect(aliceIndex).toBeLessThan(carolIndex);
337
+ });
338
+
339
+ it('preserves input order for tokens with equal seven day reset', () => {
340
+ const output = new ComposeDashboardUseCase().run({
341
+ machineStatus: null,
342
+ projects: [],
343
+ tokens: [
344
+ tokenStatus({ name: 'first', sevenDayResetSeconds: 100 }),
345
+ tokenStatus({ name: 'second', sevenDayResetSeconds: 100 }),
346
+ tokenStatus({ name: 'third', sevenDayResetSeconds: 100 }),
347
+ ],
348
+ });
349
+ expect(output.indexOf('first')).toBeLessThan(output.indexOf('second'));
350
+ expect(output.indexOf('second')).toBeLessThan(output.indexOf('third'));
351
+ });
352
+
353
+ it('renders host placeholders when the machine status file is absent', () => {
354
+ const output = new ComposeDashboardUseCase().run({
355
+ ...representativeInput,
356
+ machineStatus: null,
357
+ });
358
+ expect(
359
+ output.startsWith(
360
+ '<tt>M?%&nbsp;C?%&nbsp;LA&nbsp;?&nbsp;?&nbsp;?&nbsp;cy-</tt><br>\n',
361
+ ),
362
+ ).toBe(true);
363
+ });
364
+
365
+ it('keeps every unwrapped composed line within the 32 code point width budget', () => {
366
+ const denseInput: ComposeDashboardInput = {
367
+ machineStatus: {
368
+ memPct: 100,
369
+ cpuPct: 100,
370
+ load: [108.5, 120.25, 95.1],
371
+ cycleMinutes: 999,
372
+ },
373
+ projects: [
374
+ {
375
+ code: 'um',
376
+ row: projectRow({
377
+ unread: 999,
378
+ todo: 999,
379
+ qc: 999,
380
+ fail: 999,
381
+ pr: 999,
382
+ ws: 999,
383
+ dep: 999,
384
+ }),
385
+ },
386
+ ],
387
+ tokens: [],
388
+ };
389
+ const output = new ComposeDashboardUseCase().run(denseInput);
390
+ const unwrappedLines = output
391
+ .split('\n')
392
+ .filter((line) => line.length > 0)
393
+ .map((line) =>
394
+ line
395
+ .replace(/^<tt>/, '')
396
+ .replace(/<\/tt><br>$/, '')
397
+ .replace(/&nbsp;/g, ' '),
398
+ );
399
+ for (const line of unwrappedLines) {
400
+ expect(codePointLength(line)).toBeLessThanOrEqual(
401
+ PROJECT_ROW_WIDTH_BUDGET,
402
+ );
403
+ }
404
+ });
405
+ });
@@ -0,0 +1,220 @@
1
+ import { DashboardRow } from './GenerateDashboardRowUseCase';
2
+ import { TokenStatus, TokenStatusColor } from './GenerateTokenStatusUseCase';
3
+
4
+ export const PROJECT_ROW_WIDTH_BUDGET = 32;
5
+
6
+ export type ComposeDashboardProject = {
7
+ code: string;
8
+ row: DashboardRow | null;
9
+ };
10
+
11
+ export type ComposeDashboardMachineStatus = {
12
+ memPct: number | null;
13
+ cpuPct: number | null;
14
+ load: [number, number, number] | null;
15
+ cycleMinutes: number | null;
16
+ };
17
+
18
+ export type ComposeDashboardInput = {
19
+ projects: ComposeDashboardProject[];
20
+ machineStatus: ComposeDashboardMachineStatus | null;
21
+ tokens: TokenStatus[];
22
+ };
23
+
24
+ type ProjectColumn = {
25
+ header: string;
26
+ key: keyof DashboardRow;
27
+ };
28
+
29
+ const PROJECT_COLUMNS: ProjectColumn[] = [
30
+ { header: 'unr', key: 'unread' },
31
+ { header: 'tdo', key: 'todo' },
32
+ { header: 'aqc', key: 'qc' },
33
+ { header: 'fal', key: 'fail' },
34
+ { header: 'prp', key: 'pr' },
35
+ { header: 'aws', key: 'ws' },
36
+ { header: 'dep', key: 'dep' },
37
+ ];
38
+
39
+ const PROJECT_COLUMN_WIDTH = 3;
40
+
41
+ const SEVERITY_BLANK = ' ';
42
+
43
+ const TOKEN_COLOR_DOT: Record<TokenStatusColor, string> = {
44
+ G: '🟢',
45
+ Y: '🟡',
46
+ K: '⚪',
47
+ };
48
+
49
+ const padEnd = (value: string, width: number, fill: string): string => {
50
+ let result = value;
51
+ while (result.length < width) {
52
+ result = result + fill;
53
+ }
54
+ return result;
55
+ };
56
+
57
+ const padStart = (value: string, width: number): string => {
58
+ let result = value;
59
+ while (result.length < width) {
60
+ result = ' ' + result;
61
+ }
62
+ return result;
63
+ };
64
+
65
+ const padStartZero = (value: string, width: number): string => {
66
+ let result = value;
67
+ while (result.length < width) {
68
+ result = '0' + result;
69
+ }
70
+ return result;
71
+ };
72
+
73
+ export const roundHalfToEven = (value: number): number => {
74
+ const floor = Math.floor(value);
75
+ const difference = value - floor;
76
+ if (difference < 0.5) {
77
+ return floor;
78
+ }
79
+ if (difference > 0.5) {
80
+ return floor + 1;
81
+ }
82
+ return floor % 2 === 0 ? floor : floor + 1;
83
+ };
84
+
85
+ export const formatResetCountdown = (totalSeconds: number): string => {
86
+ if (totalSeconds < 0) {
87
+ return '0d00h00';
88
+ }
89
+ const whole = Math.trunc(totalSeconds);
90
+ const days = Math.trunc(whole / 86400);
91
+ const afterDays = whole % 86400;
92
+ const hours = Math.trunc(afterDays / 3600);
93
+ const minutes = Math.trunc((afterDays % 3600) / 60);
94
+ return `${days}d${padStartZero(String(hours), 2)}h${padStartZero(
95
+ String(minutes),
96
+ 2,
97
+ )}`;
98
+ };
99
+
100
+ export const formatMachineStatusLine = (
101
+ machineStatus: ComposeDashboardMachineStatus | null,
102
+ ): string => {
103
+ const memText =
104
+ machineStatus !== null && machineStatus.memPct !== null
105
+ ? `${machineStatus.memPct}%`
106
+ : '?%';
107
+ const cpuText =
108
+ machineStatus !== null && machineStatus.cpuPct !== null
109
+ ? `${machineStatus.cpuPct}%`
110
+ : '?%';
111
+ const load = machineStatus !== null ? machineStatus.load : null;
112
+ const oneMinute = load === null ? '?' : String(roundHalfToEven(load[0]));
113
+ const fiveMinute = load === null ? '?' : String(roundHalfToEven(load[1]));
114
+ const fifteenMinute = load === null ? '?' : String(roundHalfToEven(load[2]));
115
+ const cycle =
116
+ machineStatus !== null && machineStatus.cycleMinutes !== null
117
+ ? `cy${machineStatus.cycleMinutes}`
118
+ : 'cy-';
119
+ return `M${memText} C${cpuText} LA ${oneMinute} ${fiveMinute} ${fifteenMinute} ${cycle}`;
120
+ };
121
+
122
+ const capThreeDigits = (value: number): string =>
123
+ value > 999 ? '999' : String(value);
124
+
125
+ export const formatProjectHeaderLine = (): string => {
126
+ const head = padEnd('pj', 4, ' ');
127
+ const columns = PROJECT_COLUMNS.map(
128
+ (column) => ' ' + padStart(column.header, PROJECT_COLUMN_WIDTH),
129
+ ).join('');
130
+ return head + columns;
131
+ };
132
+
133
+ const severityDot = (row: DashboardRow): string => {
134
+ if (row.blocker >= 2) {
135
+ return '🔴';
136
+ }
137
+ if (row.blocker === 1) {
138
+ return '🟣';
139
+ }
140
+ if (row.unread >= 10 || row.qc >= 15 || row.fail >= 5) {
141
+ return '🟠';
142
+ }
143
+ if (row.unread >= 5 || row.qc >= 10 || row.fail >= 3) {
144
+ return '🟡';
145
+ }
146
+ return '🟢';
147
+ };
148
+
149
+ export const formatProjectRowLine = (
150
+ project: ComposeDashboardProject,
151
+ ): string => {
152
+ const mark = project.row === null ? SEVERITY_BLANK : severityDot(project.row);
153
+ const cells = PROJECT_COLUMNS.map((column) => {
154
+ const cell =
155
+ project.row === null ? '--' : capThreeDigits(project.row[column.key]);
156
+ return ' ' + padStart(cell, PROJECT_COLUMN_WIDTH);
157
+ }).join('');
158
+ return mark + padEnd(project.code, 2, ' ') + cells;
159
+ };
160
+
161
+ const formatUtilization = (percent: number | null): string =>
162
+ padStart(percent === null ? '?' : `${percent}%`, 4);
163
+
164
+ const formatReset = (resetSeconds: number | null): string =>
165
+ resetSeconds === null ? '?' : formatResetCountdown(resetSeconds);
166
+
167
+ const tokenSortKey = (token: TokenStatus): [number, number] =>
168
+ token.sevenDayResetSeconds === null
169
+ ? [1, 0]
170
+ : [0, token.sevenDayResetSeconds];
171
+
172
+ const sortTokens = (tokens: TokenStatus[]): TokenStatus[] =>
173
+ tokens
174
+ .map((token, index) => ({ token, index }))
175
+ .sort((left, right) => {
176
+ const leftKey = tokenSortKey(left.token);
177
+ const rightKey = tokenSortKey(right.token);
178
+ if (leftKey[0] !== rightKey[0]) {
179
+ return leftKey[0] - rightKey[0];
180
+ }
181
+ if (leftKey[1] !== rightKey[1]) {
182
+ return leftKey[1] - rightKey[1];
183
+ }
184
+ return left.index - right.index;
185
+ })
186
+ .map((entry) => entry.token);
187
+
188
+ export const formatTokenRowLine = (token: TokenStatus): string => {
189
+ const dot = TOKEN_COLOR_DOT[token.color];
190
+ const name = padEnd(token.name, 4, '_');
191
+ const fiveHourUtilization = formatUtilization(
192
+ token.fiveHourUtilizationPercent,
193
+ );
194
+ const fiveHourReset = formatReset(token.fiveHourResetSeconds);
195
+ const sevenDayUtilization = formatUtilization(
196
+ token.sevenDayUtilizationPercent,
197
+ );
198
+ const sevenDayReset = formatReset(token.sevenDayResetSeconds);
199
+ const prep = String(token.prep);
200
+ const hum = String(token.hum);
201
+ return `${dot}${name} ${fiveHourUtilization} ${fiveHourReset} ${sevenDayUtilization} ${sevenDayReset} ${prep} ${hum}`;
202
+ };
203
+
204
+ const wrapLine = (line: string): string =>
205
+ `<tt>${line.replace(/ /g, '&nbsp;')}</tt><br>`;
206
+
207
+ export class ComposeDashboardUseCase {
208
+ run = (input: ComposeDashboardInput): string => {
209
+ const statsLine = formatMachineStatusLine(input.machineStatus);
210
+ const projectLines = [
211
+ formatProjectHeaderLine(),
212
+ ...input.projects.map((project) => formatProjectRowLine(project)),
213
+ ];
214
+ const tokenLines = sortTokens(input.tokens).map((token) =>
215
+ formatTokenRowLine(token),
216
+ );
217
+ const lines = [statsLine, ...projectLines, '', ...tokenLines];
218
+ return lines.map((line) => wrapLine(line)).join('\n') + '\n';
219
+ };
220
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/cli/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EACL,UAAU,EACV,cAAc,EACd,wBAAwB,EACxB,YAAY,EACZ,kBAAkB,GACnB,MAAM,iBAAiB,CAAC;AA4GzB,eAAO,MAAM,OAAO,SAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/cli/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EACL,UAAU,EACV,cAAc,EACd,wBAAwB,EACxB,YAAY,EACZ,kBAAkB,GACnB,MAAM,iBAAiB,CAAC;AA+HzB,eAAO,MAAM,OAAO,SAAgB,CAAC"}
@@ -0,0 +1,9 @@
1
+ import { ComposeDashboardInput } from '../../../domain/usecases/dashboard/ComposeDashboardUseCase';
2
+ export type DashboardComposeOptions = {
3
+ dashboardDataDir: string;
4
+ projectCodes: string[];
5
+ };
6
+ export declare const buildComposeDashboardInput: (options: DashboardComposeOptions) => ComposeDashboardInput;
7
+ export declare const dashboardComposeFilesPresent: (options: DashboardComposeOptions) => boolean;
8
+ export declare const composeDashboardText: (options: DashboardComposeOptions) => string;
9
+ //# sourceMappingURL=dashboardComposeService.d.ts.map
@@ -0,0 +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;AAOpE,MAAM,MAAM,uBAAuB,GAAG;IACpC,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB,CAAC;AA2IF,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,6 +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 DEFAULT_DASHBOARD_PROJECT_CODES: string[];
7
8
  export declare const CONSOLE_TOKEN_HEADER = "x-pv-token";
8
9
  export declare const hasDotSegment: (requestPath: string) => boolean;
9
10
  export declare const requiresToken: (requestPath: string) => boolean;
@@ -16,6 +17,8 @@ export type WebServerOptions = {
16
17
  consoleDataOutputDir: string | null;
17
18
  inTmuxDataDir: string | null;
18
19
  dashboardDir: string | null;
20
+ dashboardDataDir: string | null;
21
+ dashboardProjectCodes: string[];
19
22
  githubToken?: string | null;
20
23
  imageFetcher?: ImageFetcher | null;
21
24
  issueRepository?: IssueRepository | null;
@@ -26,6 +29,7 @@ export declare const DASHBOARD_REQUEST_PATH = "/tdpm.txt";
26
29
  export declare const IMAGE_PROXY_REQUEST_PATH = "/api/img";
27
30
  export declare const resolveDashboardFilePath: (dashboardDir: string, requestPath: string) => string | null;
28
31
  export declare const resolveFlatInTmuxFilePath: (inTmuxDataDir: string, requestPath: string) => string | null;
32
+ export declare const resolveDashboardContent: (options: WebServerOptions, requestPath: string) => Buffer | null;
29
33
  export declare const handleWebRequest: (options: WebServerOptions, request: http.IncomingMessage, response: http.ServerResponse) => Promise<void>;
30
34
  export declare const createWebServer: (options: WebServerOptions) => http.Server;
31
35
  export type StartWebServerOptions = WebServerOptions & {
@@ -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;AAEtE,eAAO,MAAM,gBAAgB,OAAO,CAAC;AAErC,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,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;AA4TF,eAAO,MAAM,gBAAgB,GAC3B,SAAS,gBAAgB,EACzB,SAAS,IAAI,CAAC,eAAe,EAC7B,UAAU,IAAI,CAAC,cAAc,KAC5B,OAAO,CAAC,IAAI,CAiFd,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"}
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;AAMtE,eAAO,MAAM,gBAAgB,OAAO,CAAC;AAErC,eAAO,MAAM,+BAA+B,UAA2B,CAAC;AAExE,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"}