@nbtca/prompt 1.4.1 → 1.5.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.
- package/LICENSE +1 -1
- package/README.md +27 -58
- package/SECURITY.md +16 -45
- package/dist/app/app.js +53 -55
- package/dist/app/chrome.js +67 -50
- package/dist/app/fields/list-field.js +12 -25
- package/dist/app/fields/text-field.js +3 -8
- package/dist/app/frame.js +2 -21
- package/dist/app/keys.js +10 -2
- package/dist/app/views/docs-render.js +31 -24
- package/dist/app/views/docs.js +211 -67
- package/dist/app/views/events-render.js +19 -26
- package/dist/app/views/events.js +44 -31
- package/dist/app/views/home.js +33 -76
- package/dist/app/views/schedule-grid-cursor.js +9 -18
- package/dist/app/views/schedule-render.js +47 -71
- package/dist/app/views/schedule.js +158 -90
- package/dist/app/views/settings-render.js +8 -19
- package/dist/app/views/settings.js +93 -18
- package/dist/auth/cookie-transport.js +31 -32
- package/dist/auth/errors.js +3 -1
- package/dist/auth/nbt-auth.js +42 -25
- package/dist/auth/session-store.js +17 -9
- package/dist/config/data.js +10 -13
- package/dist/config/preferences.js +14 -7
- package/dist/core/calendar-day.js +37 -0
- package/dist/core/capabilities.js +6 -3
- package/dist/core/components/confirm.js +9 -8
- package/dist/core/components/menu.js +41 -16
- package/dist/core/components/messages.js +12 -4
- package/dist/core/components/painter.js +3 -1
- package/dist/core/components/spinner.js +17 -6
- package/dist/core/components/text-input.js +24 -18
- package/dist/core/icons.js +2 -2
- package/dist/core/logo.js +25 -21
- package/dist/core/motion.js +25 -19
- package/dist/core/text.js +182 -75
- package/dist/core/theme.js +0 -28
- package/dist/core/transitions.js +2 -2
- package/dist/core/ui.js +15 -30
- package/dist/core/vim-keys.js +9 -15
- package/dist/features/about.js +23 -0
- package/dist/features/calendar-heatmap.js +16 -40
- package/dist/features/calendar-query.js +1 -2
- package/dist/features/calendar.js +12 -185
- package/dist/features/docs.js +439 -320
- package/dist/features/schedule-render.js +65 -102
- package/dist/features/schedule-store.js +51 -9
- package/dist/features/schedule-view.js +46 -220
- package/dist/features/status.js +44 -59
- package/dist/features/student-timetable.js +73 -95
- package/dist/features/theme.js +6 -5
- package/dist/features/timetable-sanitize.js +40 -0
- package/dist/features/update.js +9 -37
- package/dist/i18n/index.js +87 -65
- package/dist/i18n/locales/en.json +1 -1
- package/dist/i18n/locales/zh.json +1 -1
- package/dist/index.js +85 -64
- package/dist/logo/ca-dotmatrix.txt +16 -18
- package/dist/main.js +7 -48
- package/package.json +30 -18
- package/bin/nbtca-welcome.js +0 -2
- package/dist/core/components/screen.js +0 -18
- package/dist/core/menu.js +0 -71
- package/dist/features/links.js +0 -39
- package/dist/features/schedule-query.js +0 -47
- package/dist/features/settings.js +0 -130
- package/dist/logo/ca-logo.png +0 -0
|
@@ -1,258 +1,84 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
import { withAuthenticatedSession, resolveTerm, relevantTerms, writePrivateIcs, isSessionExpired, safeMessage, JWXT_ORIGIN, } from './student-timetable.js';
|
|
11
|
-
import { currentWeekNumber, campusWeekday, meetingsOnDay, nextMeeting } from './schedule-query.js';
|
|
12
|
-
import { renderNextClassBanner, renderTodayClasses, renderTodayTimeline, renderWeekGrid } from './schedule-render.js';
|
|
13
|
-
import { termKey, loadWeekOne, saveWeekOne, saveTimetableCache, saveCurrentPointer, loadCurrentPointer, loadTimetableCache, clearScheduleCache, } from './schedule-store.js';
|
|
14
|
-
/** Loads a saved week-one Monday for `key`, or prompts for and persists a new one.
|
|
15
|
-
* Returns null when the user cancels or enters an unparsable date (caller aborts). */
|
|
16
|
-
async function ensureWeekOne(key) {
|
|
17
|
-
const saved = loadWeekOne(key);
|
|
18
|
-
if (saved)
|
|
19
|
-
return saved;
|
|
20
|
-
const value = await runTextInput({
|
|
21
|
-
message: t().timetable.promptWeekOne,
|
|
22
|
-
placeholder: 'YYYY-MM-DD',
|
|
23
|
-
});
|
|
24
|
-
if (value === null)
|
|
1
|
+
import { createTimetableSchedule } from '@nbtca/nbtcal/timetable';
|
|
2
|
+
import { addLocalDays, parseLocalMonday } from '../core/calendar-day.js';
|
|
3
|
+
import { renderNextClassBanner, renderTodayTimeline } from './schedule-render.js';
|
|
4
|
+
import { loadCurrentPointer, loadTimetableCache } from './schedule-store.js';
|
|
5
|
+
import { sanitizeTimetable } from './timetable-sanitize.js';
|
|
6
|
+
const WEEKDAYS = [1, 2, 3, 4, 5, 6, 7];
|
|
7
|
+
function loadCachedTimetable() {
|
|
8
|
+
const pointer = loadCurrentPointer();
|
|
9
|
+
if (!pointer)
|
|
25
10
|
return null;
|
|
26
|
-
const
|
|
27
|
-
if (
|
|
11
|
+
const value = loadTimetableCache(pointer.termKey);
|
|
12
|
+
if (!value || typeof value !== 'object')
|
|
28
13
|
return null;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
* (after reporting the error) on failure so callers can keep the previous state. */
|
|
36
|
-
async function fetchTimetableWithSpinner(client, term, key, weekOne) {
|
|
37
|
-
const trans = t();
|
|
38
|
-
const spinner = createSpinner(trans.calendar.loading);
|
|
39
|
-
try {
|
|
40
|
-
const tt = await client.fetchTerm(term);
|
|
41
|
-
spinner.stop();
|
|
42
|
-
saveTimetableCache(key, tt);
|
|
43
|
-
saveCurrentPointer(key, weekOne);
|
|
44
|
-
return tt;
|
|
45
|
-
}
|
|
46
|
-
catch (err) {
|
|
47
|
-
if (isSessionExpired(err)) {
|
|
48
|
-
spinner.stop();
|
|
49
|
-
throw err;
|
|
50
|
-
}
|
|
51
|
-
spinner.error(trans.timetable.genericError);
|
|
14
|
+
const candidate = value;
|
|
15
|
+
if (!Array.isArray(candidate.meetings) ||
|
|
16
|
+
!Array.isArray(candidate.periods) ||
|
|
17
|
+
!Array.isArray(candidate.unresolvedItems) ||
|
|
18
|
+
!Array.isArray(candidate.calendarDays) ||
|
|
19
|
+
!Array.isArray(candidate.warnings)) {
|
|
52
20
|
return null;
|
|
53
21
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const week = currentWeekNumber(weekOne, now);
|
|
57
|
-
const today = meetingsOnDay(tt.meetings, campusWeekday(now), week);
|
|
58
|
-
const trans = t();
|
|
59
|
-
console.log();
|
|
60
|
-
const banner = renderNextClassBanner(nextMeeting(tt.meetings, tt.periods, weekOne, now), now);
|
|
61
|
-
console.log(banner || `${space.indent}${type.hint(trans.timetable.noNextClass)}`);
|
|
62
|
-
console.log();
|
|
63
|
-
console.log(renderTodayClasses(today, tt.periods, now));
|
|
64
|
-
console.log();
|
|
65
|
-
return { week, today };
|
|
66
|
-
}
|
|
67
|
-
async function switchTerm(client, catalog) {
|
|
68
|
-
const trans = t();
|
|
69
|
-
const picked = await runMenu({
|
|
70
|
-
title: trans.timetable.hubSwitchTerm,
|
|
71
|
-
options: relevantTerms(catalog).map((tm) => ({
|
|
72
|
-
value: `${tm.academicYear}:${tm.semester}`,
|
|
73
|
-
label: tm.academicYearLabel,
|
|
74
|
-
hint: tm.current ? trans.common.current : undefined,
|
|
75
|
-
})),
|
|
76
|
-
footer: menuFooter(),
|
|
77
|
-
});
|
|
78
|
-
if (picked === null)
|
|
22
|
+
const fetchedAt = new Date(candidate.fetchedAt);
|
|
23
|
+
if (Number.isNaN(fetchedAt.getTime()))
|
|
79
24
|
return null;
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
return null;
|
|
85
|
-
const tt = await fetchTimetableWithSpinner(client, term, key, weekOne);
|
|
86
|
-
if (!tt)
|
|
87
|
-
return null;
|
|
88
|
-
return { term, key, weekOne, tt };
|
|
89
|
-
}
|
|
90
|
-
function exportTimetable(tt, term, key, weekOne) {
|
|
91
|
-
const trans = t();
|
|
92
|
-
const ics = timetableToIcs(tt, { weekOneMonday: weekOne, calendarName: `NBT ${term.academicYearLabel}` });
|
|
93
|
-
const out = `timetable-${key}.ics`;
|
|
94
|
-
try {
|
|
95
|
-
writePrivateIcs(out, ics);
|
|
96
|
-
success(fmt(trans.timetable.exported, { count: tt.meetings.length, file: path.resolve(out) }));
|
|
97
|
-
}
|
|
98
|
-
catch {
|
|
99
|
-
error(trans.timetable.genericError);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
/** Interactive schedule hub: login/restore, resolve the current term, load or prompt
|
|
103
|
-
* for the week-one Monday, fetch the timetable, then loop a menu of today / week-grid /
|
|
104
|
-
* switch-term / export actions until the user cancels. */
|
|
105
|
-
export async function showSchedule() {
|
|
106
|
-
const trans = t();
|
|
107
|
-
await enterScreen(breadcrumb(trans.timetable.menuEntry));
|
|
108
|
-
try {
|
|
109
|
-
await withAuthenticatedSession(async (session) => {
|
|
110
|
-
const client = createNbtTimetableClient(session.timetableTransport, { baseUrl: JWXT_ORIGIN });
|
|
111
|
-
const catalog = await client.listTerms();
|
|
112
|
-
let term = resolveTerm(catalog);
|
|
113
|
-
let key = termKey(term);
|
|
114
|
-
let weekOne = await ensureWeekOne(key);
|
|
115
|
-
if (!weekOne)
|
|
116
|
-
return 0;
|
|
117
|
-
const initial = await fetchTimetableWithSpinner(client, term, key, weekOne);
|
|
118
|
-
if (!initial)
|
|
119
|
-
return 1;
|
|
120
|
-
let tt = initial;
|
|
121
|
-
while (true) {
|
|
122
|
-
const now = new Date();
|
|
123
|
-
const { week, today } = renderHub(tt, weekOne, now);
|
|
124
|
-
const action = await runMenu({
|
|
125
|
-
title: `${trans.timetable.menuEntry} ${c.muted(term.academicYearLabel)} ${c.muted(trans.timetable.weekLabel + String(week))}`,
|
|
126
|
-
options: [
|
|
127
|
-
{ value: 'today', label: trans.timetable.hubToday, hint: String(today.length) },
|
|
128
|
-
{ value: 'week', label: trans.timetable.hubWeek },
|
|
129
|
-
{ value: 'term', label: trans.timetable.hubSwitchTerm, hint: term.academicYearLabel },
|
|
130
|
-
{ value: 'export', label: trans.timetable.hubExport },
|
|
131
|
-
{ value: 'logout', label: trans.timetable.hubLogout },
|
|
132
|
-
],
|
|
133
|
-
footer: menuFooter(),
|
|
134
|
-
});
|
|
135
|
-
if (action === null)
|
|
136
|
-
return 0;
|
|
137
|
-
if (action === 'today') {
|
|
138
|
-
continue; // The next loop iteration repaints today's classes.
|
|
139
|
-
}
|
|
140
|
-
if (action === 'week') {
|
|
141
|
-
console.log();
|
|
142
|
-
console.log(renderWeekGrid(tt.meetings, tt.periods, week, now));
|
|
143
|
-
console.log();
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
if (action === 'term') {
|
|
147
|
-
const switched = await switchTerm(client, catalog);
|
|
148
|
-
if (!switched)
|
|
149
|
-
continue;
|
|
150
|
-
term = switched.term;
|
|
151
|
-
key = switched.key;
|
|
152
|
-
weekOne = switched.weekOne;
|
|
153
|
-
tt = switched.tt;
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
156
|
-
if (action === 'export') {
|
|
157
|
-
exportTimetable(tt, term, key, weekOne);
|
|
158
|
-
continue;
|
|
159
|
-
}
|
|
160
|
-
if (action === 'logout') {
|
|
161
|
-
createSessionStore().clear();
|
|
162
|
-
clearScheduleCache();
|
|
163
|
-
return 0;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
}, {
|
|
167
|
-
oneShot: false,
|
|
168
|
-
isInteractive: true,
|
|
169
|
-
store: createSessionStore(),
|
|
170
|
-
stderr: process.stderr,
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
catch (err) {
|
|
174
|
-
error(safeMessage(err));
|
|
175
|
-
}
|
|
25
|
+
return {
|
|
26
|
+
timetable: sanitizeTimetable({ ...candidate, fetchedAt }),
|
|
27
|
+
weekOneMonday: pointer.weekOneMonday,
|
|
28
|
+
};
|
|
176
29
|
}
|
|
177
|
-
/** Best-effort, cache-only startup line: reads the last-used term pointer and its
|
|
178
|
-
* cached timetable (no network) and renders the next-class banner, or '' if the
|
|
179
|
-
* student isn't "set up" yet or anything about the cache is missing/corrupt. */
|
|
180
30
|
export function peekNextClassLine(now = new Date()) {
|
|
181
31
|
try {
|
|
182
|
-
const
|
|
183
|
-
if (!
|
|
32
|
+
const cached = loadCachedTimetable();
|
|
33
|
+
if (!cached)
|
|
184
34
|
return '';
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
return renderNextClassBanner(next, now);
|
|
35
|
+
const schedule = createTimetableSchedule(cached.timetable, {
|
|
36
|
+
weekOneMonday: cached.weekOneMonday,
|
|
37
|
+
});
|
|
38
|
+
return renderNextClassBanner(schedule.next(now), now);
|
|
190
39
|
}
|
|
191
40
|
catch {
|
|
192
41
|
return '';
|
|
193
42
|
}
|
|
194
43
|
}
|
|
195
|
-
/** Cache-only (no network) render of today's classes for the current term, or
|
|
196
|
-
* [] if not set up. Uses the same renderTodayTimeline Schedule's own hub
|
|
197
|
-
* renders, so Home's preview and Schedule's detail view show the exact same
|
|
198
|
-
* visual language for the exact same data instead of drifting apart. */
|
|
199
44
|
export function peekTodayLines(now = new Date()) {
|
|
200
45
|
try {
|
|
201
|
-
const
|
|
202
|
-
if (!
|
|
203
|
-
return [];
|
|
204
|
-
const cached = loadTimetableCache(ptr.termKey);
|
|
205
|
-
if (!cached || !Array.isArray(cached.meetings) || !Array.isArray(cached.periods))
|
|
46
|
+
const cached = loadCachedTimetable();
|
|
47
|
+
if (!cached)
|
|
206
48
|
return [];
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
49
|
+
const schedule = createTimetableSchedule(cached.timetable, {
|
|
50
|
+
weekOneMonday: cached.weekOneMonday,
|
|
51
|
+
});
|
|
52
|
+
const week = schedule.weekAt(now);
|
|
53
|
+
const today = schedule.meetingsOnDay(week, schedule.weekdayAt(now));
|
|
54
|
+
return renderTodayTimeline(today, cached.timetable.periods, now).split('\n');
|
|
210
55
|
}
|
|
211
56
|
catch {
|
|
212
57
|
return [];
|
|
213
58
|
}
|
|
214
59
|
}
|
|
215
|
-
/** Best-effort, cache-only (no network) computation of this week's per-day
|
|
216
|
-
* class signal, for Home's combined week-overview grid. Returns null when
|
|
217
|
-
* there's no set-up personal timetable, the cache is unusable, or the term
|
|
218
|
-
* hasn't started yet (current week < 1) — the same "before term start"
|
|
219
|
-
* guard already fixed once for Schedule's own hub (a future-dated weekOne,
|
|
220
|
-
* auto-inferred while on break, must not render a nonsensical negative
|
|
221
|
-
* week's worth of content). */
|
|
222
60
|
export function peekWeekAheadInfo(now = new Date()) {
|
|
223
61
|
try {
|
|
224
|
-
const
|
|
225
|
-
if (!
|
|
226
|
-
return null;
|
|
227
|
-
const cached = loadTimetableCache(ptr.termKey);
|
|
228
|
-
if (!cached || !Array.isArray(cached.meetings))
|
|
62
|
+
const cached = loadCachedTimetable();
|
|
63
|
+
if (!cached)
|
|
229
64
|
return null;
|
|
230
|
-
const
|
|
65
|
+
const schedule = createTimetableSchedule(cached.timetable, {
|
|
66
|
+
weekOneMonday: cached.weekOneMonday,
|
|
67
|
+
});
|
|
68
|
+
const week = schedule.weekAt(now);
|
|
231
69
|
if (week < 1)
|
|
232
70
|
return null;
|
|
233
|
-
const
|
|
234
|
-
const
|
|
235
|
-
const base = new Date(`${ptr.weekOneMonday}T00:00:00`);
|
|
236
|
-
const weekStartDate = new Date(base.getTime() + (week - 1) * 7 * 86400000);
|
|
71
|
+
const classDays = WEEKDAYS.map((weekday) => schedule.meetingsOnDay(week, weekday).length > 0);
|
|
72
|
+
const weekStartDate = addLocalDays(parseLocalMonday(cached.weekOneMonday), (week - 1) * 7);
|
|
237
73
|
return { weekStartDate, classDays };
|
|
238
74
|
}
|
|
239
75
|
catch {
|
|
240
76
|
return null;
|
|
241
77
|
}
|
|
242
78
|
}
|
|
243
|
-
/** Best-effort, cache-only (no network) read of how many timetable items
|
|
244
|
-
* still need the student's attention — the same data buildHubField()
|
|
245
|
-
* (schedule.ts) already surfaces inside Schedule's own hub menu, exposed
|
|
246
|
-
* here so Home can show it too without a second source of truth. */
|
|
247
79
|
export function peekUnresolvedCount() {
|
|
248
80
|
try {
|
|
249
|
-
|
|
250
|
-
if (!ptr)
|
|
251
|
-
return 0;
|
|
252
|
-
const cached = loadTimetableCache(ptr.termKey);
|
|
253
|
-
if (!cached || !Array.isArray(cached.unresolvedItems))
|
|
254
|
-
return 0;
|
|
255
|
-
return cached.unresolvedItems.length;
|
|
81
|
+
return loadCachedTimetable()?.timetable.unresolvedItems.length ?? 0;
|
|
256
82
|
}
|
|
257
83
|
catch {
|
|
258
84
|
return 0;
|
package/dist/features/status.js
CHANGED
|
@@ -1,53 +1,60 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { APP_INFO, URLS } from '../config/data.js';
|
|
3
|
-
import { getCapabilities } from '../core/capabilities.js';
|
|
4
|
-
import { createPainter } from '../core/components/painter.js';
|
|
5
3
|
import { pickIcon } from '../core/icons.js';
|
|
6
|
-
import { padEndV, visualWidth } from '../core/text.js';
|
|
4
|
+
import { padEndV, sanitizeTerminalLine, visualWidth } from '../core/text.js';
|
|
7
5
|
import { c } from '../core/theme.js';
|
|
8
|
-
import { createSpinner } from '../core/ui.js';
|
|
9
|
-
import { enterScreen, breadcrumb } from '../core/transitions.js';
|
|
10
6
|
import { t } from '../i18n/index.js';
|
|
11
7
|
function getServiceTargets() {
|
|
12
8
|
const trans = t();
|
|
13
9
|
return [
|
|
14
|
-
// NBTCA-owned services
|
|
15
10
|
{ name: trans.status.serviceHomepage, url: URLS.homepage, group: 'nbtca' },
|
|
16
11
|
{ name: trans.status.serviceDocs, url: URLS.docs, group: 'nbtca' },
|
|
17
12
|
{ name: trans.status.serviceIcal, url: URLS.calendar, group: 'nbtca' },
|
|
18
13
|
{ name: trans.status.serviceRepair, url: URLS.repair, group: 'nbtca' },
|
|
19
|
-
// External platforms
|
|
20
14
|
{ name: trans.status.serviceGithub, url: URLS.github, group: 'external' },
|
|
21
15
|
{ name: trans.status.serviceRoadmap, url: URLS.roadmap, group: 'external' },
|
|
22
|
-
// Intranet services (campus LAN only)
|
|
23
16
|
{ name: trans.status.serviceCloud, url: URLS.cloud, group: 'intranet', intranet: true },
|
|
24
17
|
{ name: trans.status.serviceMirror, url: URLS.mirror, group: 'intranet', intranet: true },
|
|
25
18
|
];
|
|
26
19
|
}
|
|
27
20
|
async function checkService(name, url, timeoutMs) {
|
|
28
21
|
const start = Date.now();
|
|
22
|
+
const controller = new AbortController();
|
|
23
|
+
const timeout = setTimeout(() => {
|
|
24
|
+
controller.abort();
|
|
25
|
+
}, timeoutMs);
|
|
26
|
+
timeout.unref();
|
|
27
|
+
let response;
|
|
29
28
|
try {
|
|
30
|
-
|
|
31
|
-
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
32
|
-
const response = await fetch(url, {
|
|
29
|
+
response = await fetch(url, {
|
|
33
30
|
signal: controller.signal,
|
|
34
31
|
redirect: 'follow',
|
|
35
32
|
headers: { 'User-Agent': `NBTCA-CLI/${APP_INFO.version}` },
|
|
36
33
|
});
|
|
37
|
-
clearTimeout(timeout);
|
|
38
34
|
const latencyMs = Date.now() - start;
|
|
39
35
|
const ok = response.status >= 200 && response.status < 400;
|
|
40
36
|
return { name, url, ok, statusCode: response.status, latencyMs };
|
|
41
37
|
}
|
|
42
38
|
catch (err) {
|
|
43
39
|
const latencyMs = Date.now() - start;
|
|
44
|
-
const error = err instanceof Error
|
|
45
|
-
?
|
|
46
|
-
|
|
40
|
+
const error = sanitizeTerminalLine(err instanceof Error
|
|
41
|
+
? err.name === 'AbortError'
|
|
42
|
+
? 'Request timed out'
|
|
43
|
+
: err.message
|
|
44
|
+
: String(err));
|
|
47
45
|
return { name, url, ok: false, latencyMs, error };
|
|
48
46
|
}
|
|
47
|
+
finally {
|
|
48
|
+
clearTimeout(timeout);
|
|
49
|
+
try {
|
|
50
|
+
await response?.body?.cancel();
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// The response may already be closed by the runtime.
|
|
54
|
+
}
|
|
55
|
+
}
|
|
49
56
|
}
|
|
50
|
-
|
|
57
|
+
async function checkServiceWithRetry(target, timeoutMs, retries) {
|
|
51
58
|
let lastResult = await checkService(target.name, target.url, timeoutMs);
|
|
52
59
|
if (!lastResult.ok) {
|
|
53
60
|
for (let attempt = 0; attempt < retries; attempt++) {
|
|
@@ -58,12 +65,16 @@ export async function checkServiceWithRetry(target, timeoutMs, retries) {
|
|
|
58
65
|
break;
|
|
59
66
|
}
|
|
60
67
|
}
|
|
61
|
-
return {
|
|
68
|
+
return {
|
|
69
|
+
...lastResult,
|
|
70
|
+
group: target.group,
|
|
71
|
+
...(target.intranet === undefined ? {} : { intranet: target.intranet }),
|
|
72
|
+
};
|
|
62
73
|
}
|
|
63
74
|
export async function checkServices(options = {}) {
|
|
64
75
|
const timeoutMs = options.timeoutMs ?? 6000;
|
|
65
76
|
const retries = options.retries ?? 1;
|
|
66
|
-
return Promise.all(getServiceTargets().map(t => checkServiceWithRetry(t, timeoutMs, retries)));
|
|
77
|
+
return Promise.all(getServiceTargets().map((t) => checkServiceWithRetry(t, timeoutMs, retries)));
|
|
67
78
|
}
|
|
68
79
|
export function serializeServiceStatus(items) {
|
|
69
80
|
return items.map((item) => ({
|
|
@@ -78,12 +89,12 @@ export function serializeServiceStatus(items) {
|
|
|
78
89
|
}));
|
|
79
90
|
}
|
|
80
91
|
export function hasServiceFailures(items) {
|
|
81
|
-
return items.some(item => !item.ok && !item.intranet);
|
|
92
|
+
return items.some((item) => !item.ok && !item.intranet);
|
|
82
93
|
}
|
|
83
94
|
export function countServiceHealth(items) {
|
|
84
95
|
let up = 0;
|
|
85
96
|
let down = 0;
|
|
86
|
-
for (const item of items.filter(i => !i.intranet)) {
|
|
97
|
+
for (const item of items.filter((i) => !i.intranet)) {
|
|
87
98
|
if (item.ok)
|
|
88
99
|
up++;
|
|
89
100
|
else
|
|
@@ -100,7 +111,7 @@ export function renderServiceStatusTable(items, options) {
|
|
|
100
111
|
const applyCyan = useColor ? chalk.cyan : id;
|
|
101
112
|
const applyLatency = useColor ? c.latency : (ms) => `${ms}ms`;
|
|
102
113
|
const trans = t();
|
|
103
|
-
const nameWidth = Math.max(...items.map(i => visualWidth(i.name)), visualWidth(trans.status.service));
|
|
114
|
+
const nameWidth = Math.max(...items.map((i) => visualWidth(i.name)), visualWidth(trans.status.service));
|
|
104
115
|
const statusWidth = 10;
|
|
105
116
|
const onIcon = pickIcon('●', '+');
|
|
106
117
|
const offIcon = pickIcon('✕', '!');
|
|
@@ -112,14 +123,22 @@ export function renderServiceStatusTable(items, options) {
|
|
|
112
123
|
if (item.group !== currentGroup) {
|
|
113
124
|
if (currentGroup !== undefined)
|
|
114
125
|
lines.push('');
|
|
115
|
-
const groupLabel = item.group === 'nbtca'
|
|
116
|
-
|
|
117
|
-
|
|
126
|
+
const groupLabel = item.group === 'nbtca'
|
|
127
|
+
? trans.status.groupNbtca
|
|
128
|
+
: item.group === 'external'
|
|
129
|
+
? trans.status.groupExternal
|
|
130
|
+
: item.group === 'intranet'
|
|
131
|
+
? trans.status.groupIntranet
|
|
132
|
+
: '';
|
|
118
133
|
lines.push(` ${applyDim(groupLabel)}`);
|
|
119
134
|
lines.push(` ${applyDim(sep.repeat(nameWidth + statusWidth + 12))}`);
|
|
120
135
|
currentGroup = item.group;
|
|
121
136
|
}
|
|
122
|
-
const nameCol = padEndV(item.pending
|
|
137
|
+
const nameCol = padEndV(item.pending
|
|
138
|
+
? applyDim(item.name)
|
|
139
|
+
: item.intranet
|
|
140
|
+
? applyDim(item.name)
|
|
141
|
+
: applyCyan(item.name), nameWidth);
|
|
123
142
|
let statusLabel;
|
|
124
143
|
if (item.pending) {
|
|
125
144
|
statusLabel = applyDim(pickIcon('…', '.'));
|
|
@@ -141,37 +160,3 @@ export function renderServiceStatusTable(items, options) {
|
|
|
141
160
|
}
|
|
142
161
|
return lines.join('\n');
|
|
143
162
|
}
|
|
144
|
-
export async function showServiceStatus() {
|
|
145
|
-
const trans = t();
|
|
146
|
-
await enterScreen(breadcrumb(trans.menu.status));
|
|
147
|
-
const targets = getServiceTargets();
|
|
148
|
-
if (getCapabilities().reducedMotion) {
|
|
149
|
-
const spinner = createSpinner(trans.status.checking);
|
|
150
|
-
const items = await checkServices();
|
|
151
|
-
const hasFailures = hasServiceFailures(items);
|
|
152
|
-
if (hasFailures)
|
|
153
|
-
spinner.error(trans.status.summaryFail);
|
|
154
|
-
else
|
|
155
|
-
spinner.stop(trans.status.summaryOk);
|
|
156
|
-
console.log();
|
|
157
|
-
console.log(renderServiceStatusTable(items, { color: !!process.stdout.isTTY }));
|
|
158
|
-
console.log();
|
|
159
|
-
return items;
|
|
160
|
-
}
|
|
161
|
-
const items = targets.map((tg) => ({
|
|
162
|
-
name: tg.name, url: tg.url, ok: false, group: tg.group, intranet: tg.intranet, pending: true,
|
|
163
|
-
}));
|
|
164
|
-
const paint = createPainter(() => renderServiceStatusTable(items, { color: getCapabilities().color }));
|
|
165
|
-
console.log();
|
|
166
|
-
paint();
|
|
167
|
-
await Promise.all(targets.map(async (target, i) => {
|
|
168
|
-
const status = await checkServiceWithRetry(target, 6000, 1);
|
|
169
|
-
items[i] = { ...status, pending: false };
|
|
170
|
-
paint();
|
|
171
|
-
}));
|
|
172
|
-
console.log('\n');
|
|
173
|
-
const hasFailures = hasServiceFailures(items);
|
|
174
|
-
console.log(hasFailures ? c.warn(trans.status.summaryFail) : c.success(trans.status.summaryOk));
|
|
175
|
-
console.log();
|
|
176
|
-
return items;
|
|
177
|
-
}
|