@nbtca/prompt 1.3.2 → 1.4.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.
Files changed (65) hide show
  1. package/README.md +44 -0
  2. package/SECURITY.md +47 -0
  3. package/dist/app/app.js +202 -0
  4. package/dist/app/chrome.js +104 -0
  5. package/dist/app/fields/list-field.js +174 -0
  6. package/dist/app/fields/text-field.js +38 -0
  7. package/dist/app/frame.js +48 -0
  8. package/dist/app/keys.js +20 -0
  9. package/dist/app/tabs.js +11 -0
  10. package/dist/app/view.js +1 -0
  11. package/dist/app/views/docs-render.js +82 -0
  12. package/dist/app/views/docs.js +457 -0
  13. package/dist/app/views/events-render.js +111 -0
  14. package/dist/app/views/events.js +228 -0
  15. package/dist/app/views/home.js +236 -0
  16. package/dist/app/views/schedule-grid-cursor.js +52 -0
  17. package/dist/app/views/schedule-render.js +317 -0
  18. package/dist/app/views/schedule.js +472 -0
  19. package/dist/app/views/settings-render.js +53 -0
  20. package/dist/app/views/settings.js +153 -0
  21. package/dist/auth/cookie-transport.js +222 -0
  22. package/dist/auth/errors.js +18 -0
  23. package/dist/auth/nbt-auth.js +239 -0
  24. package/dist/auth/session-store.js +118 -0
  25. package/dist/config/paths.js +22 -2
  26. package/dist/core/canvas.js +23 -0
  27. package/dist/core/capabilities.js +42 -0
  28. package/dist/core/components/confirm.js +75 -0
  29. package/dist/core/components/input-session.js +24 -0
  30. package/dist/core/components/menu.js +122 -0
  31. package/dist/core/components/messages.js +16 -0
  32. package/dist/core/components/note.js +18 -0
  33. package/dist/core/components/painter.js +26 -0
  34. package/dist/core/components/screen.js +18 -0
  35. package/dist/core/components/spinner.js +47 -0
  36. package/dist/core/components/text-input.js +98 -0
  37. package/dist/core/logo.js +40 -15
  38. package/dist/core/menu.js +24 -6
  39. package/dist/core/motion.js +86 -0
  40. package/dist/core/text.js +127 -5
  41. package/dist/core/theme.js +61 -0
  42. package/dist/core/transitions.js +19 -0
  43. package/dist/core/ui.js +5 -29
  44. package/dist/features/calendar-heatmap.js +29 -27
  45. package/dist/features/calendar-query.js +50 -0
  46. package/dist/features/calendar.js +192 -98
  47. package/dist/features/docs.js +258 -55
  48. package/dist/features/links.js +7 -4
  49. package/dist/features/schedule-query.js +47 -0
  50. package/dist/features/schedule-render.js +574 -0
  51. package/dist/features/schedule-store.js +73 -0
  52. package/dist/features/schedule-view.js +260 -0
  53. package/dist/features/settings.js +41 -30
  54. package/dist/features/status.js +37 -13
  55. package/dist/features/student-timetable.js +346 -0
  56. package/dist/features/update.js +16 -8
  57. package/dist/i18n/locales/en.json +149 -6
  58. package/dist/i18n/locales/zh.json +149 -6
  59. package/dist/index.js +59 -5
  60. package/dist/logo/ca-dotmatrix-large.txt +26 -0
  61. package/dist/logo/ca-dotmatrix-small.txt +12 -0
  62. package/dist/logo/ca-dotmatrix.txt +18 -16
  63. package/dist/logo/ca-logo.png +0 -0
  64. package/dist/main.js +33 -13
  65. package/package.json +10 -7
@@ -0,0 +1,260 @@
1
+ import path from 'node:path';
2
+ import { createNbtTimetableClient, timetableToIcs, } from '@nbtca/nbtcal/timetable';
3
+ import { runMenu, menuFooter } from '../core/components/menu.js';
4
+ import { runTextInput } from '../core/components/text-input.js';
5
+ import { enterScreen, breadcrumb } from '../core/transitions.js';
6
+ import { createSpinner, success, error } from '../core/ui.js';
7
+ import { c, type, space } from '../core/theme.js';
8
+ import { t, fmt } from '../i18n/index.js';
9
+ import { createSessionStore } from '../auth/session-store.js';
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)
25
+ return null;
26
+ const trimmed = value.trim();
27
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(trimmed))
28
+ return null;
29
+ if (Number.isNaN(new Date(`${trimmed}T00:00:00`).getTime()))
30
+ return null;
31
+ saveWeekOne(key, trimmed);
32
+ return trimmed;
33
+ }
34
+ /** Fetches a term's timetable behind a spinner, caching it on success. Returns null
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);
52
+ return null;
53
+ }
54
+ }
55
+ function renderHub(tt, weekOne, now) {
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)
79
+ return null;
80
+ const term = resolveTerm(catalog, picked);
81
+ const key = termKey(term);
82
+ const weekOne = await ensureWeekOne(key);
83
+ if (!weekOne)
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
+ }
176
+ }
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
+ export function peekNextClassLine(now = new Date()) {
181
+ try {
182
+ const ptr = loadCurrentPointer();
183
+ if (!ptr)
184
+ return '';
185
+ const cached = loadTimetableCache(ptr.termKey);
186
+ if (!cached || !Array.isArray(cached.meetings) || !Array.isArray(cached.periods))
187
+ return '';
188
+ const next = nextMeeting(cached.meetings, cached.periods, ptr.weekOneMonday, now);
189
+ return renderNextClassBanner(next, now);
190
+ }
191
+ catch {
192
+ return '';
193
+ }
194
+ }
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
+ export function peekTodayLines(now = new Date()) {
200
+ try {
201
+ const ptr = loadCurrentPointer();
202
+ if (!ptr)
203
+ return [];
204
+ const cached = loadTimetableCache(ptr.termKey);
205
+ if (!cached || !Array.isArray(cached.meetings) || !Array.isArray(cached.periods))
206
+ return [];
207
+ const week = currentWeekNumber(ptr.weekOneMonday, now);
208
+ const today = meetingsOnDay(cached.meetings, campusWeekday(now), week);
209
+ return renderTodayTimeline(today, cached.periods, now).split('\n');
210
+ }
211
+ catch {
212
+ return [];
213
+ }
214
+ }
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
+ export function peekWeekAheadInfo(now = new Date()) {
223
+ try {
224
+ const ptr = loadCurrentPointer();
225
+ if (!ptr)
226
+ return null;
227
+ const cached = loadTimetableCache(ptr.termKey);
228
+ if (!cached || !Array.isArray(cached.meetings))
229
+ return null;
230
+ const week = currentWeekNumber(ptr.weekOneMonday, now);
231
+ if (week < 1)
232
+ return null;
233
+ const meetings = cached.meetings;
234
+ const classDays = [1, 2, 3, 4, 5, 6, 7].map((wd) => meetingsOnDay(meetings, wd, week).length > 0);
235
+ const base = new Date(`${ptr.weekOneMonday}T00:00:00`);
236
+ const weekStartDate = new Date(base.getTime() + (week - 1) * 7 * 86400000);
237
+ return { weekStartDate, classDays };
238
+ }
239
+ catch {
240
+ return null;
241
+ }
242
+ }
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
+ export function peekUnresolvedCount() {
248
+ try {
249
+ const ptr = loadCurrentPointer();
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;
256
+ }
257
+ catch {
258
+ return 0;
259
+ }
260
+ }
@@ -1,7 +1,6 @@
1
1
  /**
2
2
  * Unified settings — language, theme, about
3
3
  */
4
- import { select, isCancel, note } from '@clack/prompts';
5
4
  import chalk from 'chalk';
6
5
  import { applyColorModePreference, loadPreferences, resetPreferences, setColorMode, setIconMode, } from '../config/preferences.js';
7
6
  import { pickIcon } from '../core/icons.js';
@@ -10,6 +9,9 @@ import { padEndV } from '../core/text.js';
10
9
  import { success, warning } from '../core/ui.js';
11
10
  import { APP_INFO, URLS } from '../config/data.js';
12
11
  import { t, getCurrentLanguage, setLanguage, clearTranslationCache } from '../i18n/index.js';
12
+ import { runMenu, menuFooter } from '../core/components/menu.js';
13
+ import { note } from '../core/components/note.js';
14
+ import { enterScreen, breadcrumb } from '../core/transitions.js';
13
15
  function notifyResult(saved, successMsg, warningMsg) {
14
16
  if (saved) {
15
17
  success(successMsg);
@@ -37,12 +39,14 @@ function showAbout() {
37
39
  note(content, trans.about.title);
38
40
  }
39
41
  export async function showSettingsMenu() {
42
+ await enterScreen(breadcrumb(t().menu.settings));
40
43
  while (true) {
41
44
  const trans = t();
42
45
  const prefs = loadPreferences();
43
46
  const currentLang = getCurrentLanguage();
44
- const action = await select({
45
- message: trans.theme.chooseAction,
47
+ const footer = menuFooter();
48
+ const action = await runMenu({
49
+ title: trans.theme.chooseAction,
46
50
  options: [
47
51
  { value: 'language', label: trans.language.selectLanguage, hint: currentLang === 'zh' ? trans.language.zh : trans.language.en },
48
52
  { value: 'icon', label: trans.theme.iconMode, hint: prefs.iconMode },
@@ -50,23 +54,26 @@ export async function showSettingsMenu() {
50
54
  { value: 'reset', label: trans.theme.resetLabel },
51
55
  { value: 'about', label: trans.about.title },
52
56
  ],
57
+ footer,
53
58
  });
54
- if (isCancel(action))
59
+ if (action === null)
55
60
  return;
56
61
  if (action === 'about') {
57
62
  showAbout();
58
63
  continue;
59
64
  }
60
65
  if (action === 'language') {
61
- const language = await select({
62
- message: trans.language.selectLanguage,
63
- options: [
64
- { value: 'zh', label: trans.language.zh, hint: currentLang === 'zh' ? trans.common.current : undefined },
65
- { value: 'en', label: trans.language.en, hint: currentLang === 'en' ? trans.common.current : undefined },
66
- ],
67
- initialValue: currentLang,
66
+ const langOptions = [
67
+ { value: 'zh', label: trans.language.zh, hint: currentLang === 'zh' ? trans.common.current : undefined },
68
+ { value: 'en', label: trans.language.en, hint: currentLang === 'en' ? trans.common.current : undefined },
69
+ ];
70
+ const language = await runMenu({
71
+ title: trans.language.selectLanguage,
72
+ options: langOptions,
73
+ footer,
74
+ initialIndex: Math.max(0, langOptions.findIndex(o => o.value === currentLang)),
68
75
  });
69
- if (isCancel(language))
76
+ if (language === null)
70
77
  continue;
71
78
  if (language !== currentLang) {
72
79
  const saved = setLanguage(language);
@@ -76,16 +83,18 @@ export async function showSettingsMenu() {
76
83
  continue;
77
84
  }
78
85
  if (action === 'icon') {
79
- const mode = await select({
80
- message: trans.theme.chooseIconMode,
81
- options: [
82
- { value: 'auto', label: trans.theme.modeAuto, hint: prefs.iconMode === 'auto' ? trans.common.current : undefined },
83
- { value: 'ascii', label: trans.theme.modeAscii, hint: prefs.iconMode === 'ascii' ? trans.common.current : undefined },
84
- { value: 'unicode', label: trans.theme.modeUnicode, hint: prefs.iconMode === 'unicode' ? trans.common.current : undefined },
85
- ],
86
- initialValue: prefs.iconMode,
86
+ const iconOptions = [
87
+ { value: 'auto', label: trans.theme.modeAuto, hint: prefs.iconMode === 'auto' ? trans.common.current : undefined },
88
+ { value: 'ascii', label: trans.theme.modeAscii, hint: prefs.iconMode === 'ascii' ? trans.common.current : undefined },
89
+ { value: 'unicode', label: trans.theme.modeUnicode, hint: prefs.iconMode === 'unicode' ? trans.common.current : undefined },
90
+ ];
91
+ const mode = await runMenu({
92
+ title: trans.theme.chooseIconMode,
93
+ options: iconOptions,
94
+ footer,
95
+ initialIndex: Math.max(0, iconOptions.findIndex(o => o.value === prefs.iconMode)),
87
96
  });
88
- if (isCancel(mode))
97
+ if (mode === null)
89
98
  continue;
90
99
  const saved = setIconMode(mode);
91
100
  resetIconCache();
@@ -93,16 +102,18 @@ export async function showSettingsMenu() {
93
102
  continue;
94
103
  }
95
104
  if (action === 'color') {
96
- const mode = await select({
97
- message: trans.theme.chooseColorMode,
98
- options: [
99
- { value: 'auto', label: trans.theme.modeAuto, hint: prefs.colorMode === 'auto' ? trans.common.current : undefined },
100
- { value: 'on', label: trans.theme.modeOn, hint: prefs.colorMode === 'on' ? trans.common.current : undefined },
101
- { value: 'off', label: trans.theme.modeOff, hint: prefs.colorMode === 'off' ? trans.common.current : undefined },
102
- ],
103
- initialValue: prefs.colorMode,
105
+ const colorOptions = [
106
+ { value: 'auto', label: trans.theme.modeAuto, hint: prefs.colorMode === 'auto' ? trans.common.current : undefined },
107
+ { value: 'on', label: trans.theme.modeOn, hint: prefs.colorMode === 'on' ? trans.common.current : undefined },
108
+ { value: 'off', label: trans.theme.modeOff, hint: prefs.colorMode === 'off' ? trans.common.current : undefined },
109
+ ];
110
+ const mode = await runMenu({
111
+ title: trans.theme.chooseColorMode,
112
+ options: colorOptions,
113
+ footer,
114
+ initialIndex: Math.max(0, colorOptions.findIndex(o => o.value === prefs.colorMode)),
104
115
  });
105
- if (isCancel(mode))
116
+ if (mode === null)
106
117
  continue;
107
118
  const saved = setColorMode(mode);
108
119
  applyColorModePreference(false);
@@ -1,9 +1,12 @@
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';
3
5
  import { pickIcon } from '../core/icons.js';
4
6
  import { padEndV, visualWidth } from '../core/text.js';
5
7
  import { c } from '../core/theme.js';
6
8
  import { createSpinner } from '../core/ui.js';
9
+ import { enterScreen, breadcrumb } from '../core/transitions.js';
7
10
  import { t } from '../i18n/index.js';
8
11
  function getServiceTargets() {
9
12
  const trans = t();
@@ -44,7 +47,7 @@ async function checkService(name, url, timeoutMs) {
44
47
  return { name, url, ok: false, latencyMs, error };
45
48
  }
46
49
  }
47
- async function checkServiceWithRetry(target, timeoutMs, retries) {
50
+ export async function checkServiceWithRetry(target, timeoutMs, retries) {
48
51
  let lastResult = await checkService(target.name, target.url, timeoutMs);
49
52
  if (!lastResult.ok) {
50
53
  for (let attempt = 0; attempt < retries; attempt++) {
@@ -116,9 +119,12 @@ export function renderServiceStatusTable(items, options) {
116
119
  lines.push(` ${applyDim(sep.repeat(nameWidth + statusWidth + 12))}`);
117
120
  currentGroup = item.group;
118
121
  }
119
- const nameCol = padEndV(item.intranet ? applyDim(item.name) : applyCyan(item.name), nameWidth);
122
+ const nameCol = padEndV(item.pending ? applyDim(item.name) : (item.intranet ? applyDim(item.name) : applyCyan(item.name)), nameWidth);
120
123
  let statusLabel;
121
- if (item.ok) {
124
+ if (item.pending) {
125
+ statusLabel = applyDim(pickIcon('…', '.'));
126
+ }
127
+ else if (item.ok) {
122
128
  statusLabel = applyGreen(`${onIcon} ${trans.status.up}`);
123
129
  }
124
130
  else if (item.intranet) {
@@ -128,7 +134,7 @@ export function renderServiceStatusTable(items, options) {
128
134
  statusLabel = applyRed(`${offIcon} ${trans.status.down}`);
129
135
  }
130
136
  const statusCol = padEndV(statusLabel, statusWidth);
131
- const latencyCol = item.ok && item.latencyMs != null
137
+ const latencyCol = !item.pending && item.ok && item.latencyMs != null
132
138
  ? applyLatency(item.latencyMs)
133
139
  : applyDim('—');
134
140
  lines.push(` ${nameCol} ${statusCol} ${latencyCol}`);
@@ -137,17 +143,35 @@ export function renderServiceStatusTable(items, options) {
137
143
  }
138
144
  export async function showServiceStatus() {
139
145
  const trans = t();
140
- const spinner = createSpinner(trans.status.checking);
141
- const items = await checkServices();
142
- const hasFailures = hasServiceFailures(items);
143
- if (hasFailures) {
144
- spinner.error(trans.status.summaryFail);
145
- }
146
- else {
147
- spinner.stop(trans.status.summaryOk);
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;
148
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 }));
149
165
  console.log();
150
- console.log(renderServiceStatusTable(items, { color: !!process.stdout.isTTY }));
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));
151
175
  console.log();
152
176
  return items;
153
177
  }