@homebridge-plugins/homebridge-eufy-security 5.0.0-beta.16 → 5.0.0-beta.17
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/dist/diagnostics.js +103 -19
- package/dist/diagnostics.js.map +1 -1
- package/dist/platform.js +1 -0
- package/dist/platform.js.map +1 -1
- package/dist/runtime/channel.js +6 -2
- package/dist/runtime/channel.js.map +1 -1
- package/dist/runtime/owner.js +3 -0
- package/dist/runtime/owner.js.map +1 -1
- package/dist/ui/dashboard.js +1 -0
- package/dist/ui/dashboard.js.map +1 -1
- package/dist/ui/server.js +1 -0
- package/dist/ui/server.js.map +1 -1
- package/homebridge-ui/public/app.css +117 -229
- package/homebridge-ui/public/i18n/en.json +85 -79
- package/homebridge-ui/public/i18n/fr.json +86 -80
- package/homebridge-ui/public/index.html +84 -94
- package/homebridge-ui/public/js/app.js +246 -423
- package/homebridge-ui/public/js/dashboard.js +42 -4
- package/homebridge-ui/public/js/profile-wizard.js +6 -55
- package/i18n/runtime/en.json +1 -1
- package/package.json +1 -1
|
@@ -24,6 +24,11 @@ const dashboardSummary = document.querySelector('[data-dashboard-summary]');
|
|
|
24
24
|
const dashboardAuthenticate = document.querySelector('[data-dashboard-authenticate]');
|
|
25
25
|
const reauthDialog = document.querySelector('[data-reauth-dialog]');
|
|
26
26
|
const reauthAction = document.querySelector('[data-reauth-action]');
|
|
27
|
+
const attentionDialog = document.querySelector('[data-attention-dialog]');
|
|
28
|
+
const attentionTitle = document.querySelector('[data-attention-title]');
|
|
29
|
+
const attentionSummary = document.querySelector('[data-attention-summary]');
|
|
30
|
+
const attentionDiagnose = document.querySelector('[data-attention-diagnose]');
|
|
31
|
+
const attentionChoose = document.querySelector('[data-attention-choose]');
|
|
27
32
|
const deviceGroups = document.querySelector('[data-device-groups]');
|
|
28
33
|
const updatePending = document.querySelector('[data-update-pending]');
|
|
29
34
|
const updatePendingSummary = document.querySelector('[data-update-pending-summary]');
|
|
@@ -40,42 +45,27 @@ const diagnosticsPanel = document.querySelector('[data-diagnostics]');
|
|
|
40
45
|
const mastheadDiagnostics = document.querySelector('[data-masthead-diagnostics]');
|
|
41
46
|
const diagnosticsClose = document.querySelector('[data-diagnostics-close]');
|
|
42
47
|
const diagnosticsWizardPanel = document.querySelector('[data-diagnostics-wizard]');
|
|
43
|
-
const diagnosticsQuestion = document.querySelector('[data-diagnostics-question]');
|
|
44
48
|
const diagnosticsQuestionText = document.querySelector('[data-diagnostics-question-text]');
|
|
45
49
|
const diagnosticsTiles = [...document.querySelectorAll('[data-diagnostics-tile]')];
|
|
46
|
-
const
|
|
47
|
-
const diagnosticsDeviceList = document.querySelector('[data-diagnostics-device-list]');
|
|
48
|
-
const diagnosticsDevicesEvery = document.querySelector('[data-diagnostics-devices-every]');
|
|
49
|
-
const diagnosticsDevicesChosen = document.querySelector('[data-diagnostics-devices-chosen]');
|
|
50
|
-
const diagnosticsFrequency = document.querySelector('[data-diagnostics-frequency]');
|
|
51
|
-
const diagnosticsFrequencyHeading = document.querySelector('#diagnostics-frequency-heading');
|
|
52
|
-
const diagnosticsFrequencyNow = document.querySelector('[data-diagnostics-frequency-answer="now"]');
|
|
53
|
-
const diagnosticsFrequencyIntermittent = document.querySelector('[data-diagnostics-frequency-answer="intermittent"]');
|
|
54
|
-
const diagnosticsFrequencyBack = document.querySelector('[data-diagnostics-frequency-back]');
|
|
55
|
-
const diagnosticsMatch = document.querySelector('[data-diagnostics-match]');
|
|
56
|
-
const diagnosticsReject = document.querySelector('[data-diagnostics-reject]');
|
|
57
|
-
const diagnosticsAuthorize = document.querySelector('[data-diagnostics-authorize]');
|
|
50
|
+
const diagnosticsCancel = document.querySelector('[data-diagnostics-cancel]');
|
|
58
51
|
const diagnosticsReproduction = document.querySelector('[data-diagnostics-reproduction]');
|
|
59
52
|
const diagnosticsStatus = document.querySelector('[data-diagnostics-status]');
|
|
60
53
|
const diagnosticsIssue = document.querySelector('[data-diagnostics-issue]');
|
|
61
|
-
const
|
|
54
|
+
const diagnosticsExistingIssue = document.querySelector('[data-diagnostics-existing-issue]');
|
|
62
55
|
const diagnosticsResult = document.querySelector('[data-diagnostics-result]');
|
|
63
56
|
const diagnosticsActions = document.querySelector('[data-diagnostics-actions]');
|
|
64
|
-
const diagnosticsGuidanceTitle = document.querySelector('[data-diagnostics-guidance-title]');
|
|
65
|
-
const diagnosticsModeSummary = document.querySelector('[data-diagnostics-mode-summary]');
|
|
66
57
|
const diagnosticsGuidance = document.querySelector('[data-diagnostics-guidance]');
|
|
67
58
|
const diagnosticsPhaseTitle = document.querySelector('[data-diagnostics-phase-title]');
|
|
68
|
-
const diagnosticsGuidanceBeforeSection = document.querySelector('[data-diagnostics-guidance-before-section]');
|
|
69
|
-
const diagnosticsGuidanceBefore = document.querySelector('[data-diagnostics-guidance-before]');
|
|
70
59
|
const diagnosticsGuidanceAction = document.querySelector('[data-diagnostics-guidance-action]');
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
const
|
|
60
|
+
const diagnosticsCaptureNote = document.querySelector('[data-diagnostics-capture-note]');
|
|
61
|
+
const diagnosticsHandoff = document.querySelector('[data-diagnostics-handoff]');
|
|
62
|
+
const diagnosticsHandoffNote = document.querySelector('[data-diagnostics-handoff-note]');
|
|
63
|
+
const diagnosticsResultStatus = document.querySelector('[data-diagnostics-result-status]');
|
|
74
64
|
const diagnosticsExport = document.querySelector('[data-diagnostics-export]');
|
|
75
65
|
const diagnosticsResultHeading = document.querySelector('[data-diagnostics-result-heading]');
|
|
76
66
|
const diagnosticsStartAnother = document.querySelector('[data-diagnostics-start-another]');
|
|
67
|
+
const diagnosticsDownloadAgain = document.querySelector('[data-diagnostics-download-again]');
|
|
77
68
|
const advancedPanel = document.querySelector('[data-advanced-settings]');
|
|
78
|
-
const dashboardDiagnose = document.querySelector('[data-dashboard-diagnose]');
|
|
79
69
|
const devicePanel = document.querySelector('[data-device-settings]');
|
|
80
70
|
const deviceClose = document.querySelector('[data-device-close]');
|
|
81
71
|
const deviceSettingsTitle = document.querySelector('[data-device-settings-title]');
|
|
@@ -103,22 +93,21 @@ let legacyAcknowledged = false;
|
|
|
103
93
|
let diagnosticsState = { status: 'inactive', missingEvidence: [] };
|
|
104
94
|
/** Whether the reporter already downloaded this session's archive, which is what the report offer follows. */
|
|
105
95
|
let diagnosticsArchiveDownloaded = false;
|
|
106
|
-
/** The session whose
|
|
96
|
+
/** The session whose archive is reviewed and ready to export, so the review is fetched once rather than per render. */
|
|
107
97
|
let diagnosticsReviewedCaseId = '';
|
|
108
|
-
/**
|
|
109
|
-
let
|
|
98
|
+
/** The one fetch in flight, so a burst of renders does not assemble the archive several times over. */
|
|
99
|
+
let diagnosticsReviewRequest;
|
|
110
100
|
let diagnosticsReviewId = '';
|
|
111
|
-
let diagnosticsStartingAnother = false;
|
|
112
101
|
let panelReturn;
|
|
102
|
+
/** The problem the dashboard last drew, which the dashboard's diagnostics action points at while there is one. */
|
|
103
|
+
let dashboardAttention;
|
|
113
104
|
let dashboardPanelTrigger;
|
|
114
105
|
const dashboardView = window.HomebridgeEufyDashboard;
|
|
115
106
|
const legacySettingsView = window.HomebridgeEufyLegacySettings;
|
|
116
107
|
const diagnosticsWizard = window.HomebridgeEufyDiagnosticsWizard;
|
|
117
|
-
let diagnosticsWizardState = diagnosticsWizard.start();
|
|
118
108
|
const dashboardElements = {
|
|
119
109
|
dashboard,
|
|
120
110
|
title: dashboardTitle,
|
|
121
|
-
diagnose: dashboardDiagnose,
|
|
122
111
|
summary: dashboardSummary,
|
|
123
112
|
authenticate: dashboardAuthenticate,
|
|
124
113
|
groups: deviceGroups,
|
|
@@ -136,12 +125,17 @@ const dashboardElements = {
|
|
|
136
125
|
|
|
137
126
|
const REQUEST_TIMED_OUT = 'Request timed out';
|
|
138
127
|
|
|
128
|
+
/**
|
|
129
|
+
* A request the page stops waiting for after `timeoutMs`. The timeout error carries the request as `late`, since
|
|
130
|
+
* the plugin may still carry it out after the page gave up.
|
|
131
|
+
*/
|
|
139
132
|
function requestWithinDeadline(path, body, timeoutMs = 320000) {
|
|
140
133
|
let timer;
|
|
134
|
+
const request = homebridge.request(path, body);
|
|
141
135
|
const timeout = new Promise((_, reject) => {
|
|
142
|
-
timer = setTimeout(() => reject(new Error(REQUEST_TIMED_OUT)), timeoutMs);
|
|
136
|
+
timer = setTimeout(() => reject(Object.assign(new Error(REQUEST_TIMED_OUT), { late: request })), timeoutMs);
|
|
143
137
|
});
|
|
144
|
-
return Promise.race([
|
|
138
|
+
return Promise.race([request, timeout]).finally(() => clearTimeout(timer));
|
|
145
139
|
}
|
|
146
140
|
|
|
147
141
|
function isDashboardUiReproducing(state = diagnosticsState) {
|
|
@@ -220,144 +214,21 @@ function setBusy(busy) {
|
|
|
220
214
|
}
|
|
221
215
|
|
|
222
216
|
const diagnosticsProfiles = {
|
|
223
|
-
'startup-authentication': {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
},
|
|
229
|
-
'
|
|
230
|
-
title: 'diagnosticsProfileDevices',
|
|
231
|
-
summary: 'diagnosticsDevicesSummary',
|
|
232
|
-
before: 'diagnosticsDevicesBefore',
|
|
233
|
-
action: 'diagnosticsDevicesAction',
|
|
234
|
-
},
|
|
235
|
-
'control-state': {
|
|
236
|
-
title: 'diagnosticsProfileControl',
|
|
237
|
-
summary: 'diagnosticsControlSummary',
|
|
238
|
-
before: 'diagnosticsControlBefore',
|
|
239
|
-
action: 'diagnosticsControlAction',
|
|
240
|
-
},
|
|
241
|
-
'live-media': {
|
|
242
|
-
title: 'diagnosticsProfileLiveMedia',
|
|
243
|
-
summary: 'diagnosticsLiveSummary',
|
|
244
|
-
before: 'diagnosticsLiveBefore',
|
|
245
|
-
action: 'diagnosticsLiveAction',
|
|
246
|
-
},
|
|
247
|
-
'hksv-recording': {
|
|
248
|
-
title: 'diagnosticsProfileRecording',
|
|
249
|
-
summary: 'diagnosticsRecordingSummary',
|
|
250
|
-
before: 'diagnosticsRecordingBefore',
|
|
251
|
-
action: 'diagnosticsRecordingAction',
|
|
252
|
-
},
|
|
253
|
-
'dashboard-ui': {
|
|
254
|
-
title: 'diagnosticsProfileDashboard',
|
|
255
|
-
summary: 'diagnosticsDashboardSummary',
|
|
256
|
-
before: 'diagnosticsDashboardBefore',
|
|
257
|
-
action: 'diagnosticsDashboardAction',
|
|
258
|
-
},
|
|
259
|
-
other: {
|
|
260
|
-
title: 'diagnosticsProfileOther',
|
|
261
|
-
summary: 'diagnosticsOtherSummary',
|
|
262
|
-
before: 'diagnosticsOtherBefore',
|
|
263
|
-
action: 'diagnosticsOtherAction',
|
|
264
|
-
},
|
|
217
|
+
'startup-authentication': { title: 'diagnosticsProfileStartup', action: 'diagnosticsStartupAction' },
|
|
218
|
+
'device-representation': { title: 'diagnosticsProfileDevices', action: 'diagnosticsDevicesAction' },
|
|
219
|
+
'control-state': { title: 'diagnosticsProfileControl', action: 'diagnosticsControlAction' },
|
|
220
|
+
'live-media': { title: 'diagnosticsProfileLiveMedia', action: 'diagnosticsLiveAction' },
|
|
221
|
+
'hksv-recording': { title: 'diagnosticsProfileRecording', action: 'diagnosticsRecordingAction' },
|
|
222
|
+
'dashboard-ui': { title: 'diagnosticsProfileDashboard', action: 'diagnosticsDashboardAction' },
|
|
223
|
+
other: { title: 'diagnosticsProfileOther', action: 'diagnosticsOtherAction' },
|
|
265
224
|
};
|
|
266
225
|
|
|
267
226
|
function renderDiagnosticsGuidance(profile) {
|
|
268
227
|
const guidance = diagnosticsProfiles[profile] ?? diagnosticsProfiles.other;
|
|
269
|
-
diagnosticsGuidanceTitle.textContent = messages[guidance.title] ?? '';
|
|
270
228
|
diagnosticsPhaseTitle.textContent = messages[guidance.title] ?? '';
|
|
271
|
-
diagnosticsGuidanceBefore.textContent = messages[guidance.before] ?? '';
|
|
272
229
|
diagnosticsGuidanceAction.textContent = messages[guidance.action] ?? '';
|
|
273
230
|
}
|
|
274
231
|
|
|
275
|
-
/**
|
|
276
|
-
* The devices the reporter may name, one tile each.
|
|
277
|
-
*
|
|
278
|
-
* A tile carries the device's own name above its retained image where it has one, and the icon the devices
|
|
279
|
-
* view uses for a device where it does not. The image is capped at half the tile: it is there to be recognised
|
|
280
|
-
* at a glance, and a picture that fills the tile leaves the name it belongs to competing with it.
|
|
281
|
-
*
|
|
282
|
-
* Only the devices the chosen area's fault could be about are offered, so the answer cannot name a device that
|
|
283
|
-
* never had the thing being reported.
|
|
284
|
-
*
|
|
285
|
-
* The list is asked for when it is empty rather than relying on the devices view having been opened first, and
|
|
286
|
-
* a request that goes unanswered leaves the step able to say every device, which is a complete answer.
|
|
287
|
-
*/
|
|
288
|
-
async function renderDiagnosticsDeviceList() {
|
|
289
|
-
if (dashboardDevices.length === 0) {
|
|
290
|
-
try {
|
|
291
|
-
const snapshot = await requestWithinDeadline('/dashboard', { representationPreferences: {} }, 12000);
|
|
292
|
-
dashboardDevices = snapshot.devices ?? [];
|
|
293
|
-
} catch {
|
|
294
|
-
dashboardDevices = [];
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
diagnosticsDeviceList.replaceChildren();
|
|
298
|
-
for (const device of diagnosticsWizard.affectable(diagnosticsWizardState.profile, dashboardDevices)) {
|
|
299
|
-
const tile = document.createElement('button');
|
|
300
|
-
tile.type = 'button';
|
|
301
|
-
tile.dataset.serial = device.serial;
|
|
302
|
-
tile.setAttribute('aria-pressed', 'false');
|
|
303
|
-
const art = document.createElement('img');
|
|
304
|
-
art.alt = '';
|
|
305
|
-
if (device.artwork) {
|
|
306
|
-
art.className = 'device-photo';
|
|
307
|
-
art.loading = 'lazy';
|
|
308
|
-
art.src = device.artwork;
|
|
309
|
-
} else {
|
|
310
|
-
art.src = 'assets/icons/inventory.svg';
|
|
311
|
-
}
|
|
312
|
-
const name = document.createElement('span');
|
|
313
|
-
name.textContent = device.name;
|
|
314
|
-
tile.append(art, name);
|
|
315
|
-
tile.addEventListener('click', () => {
|
|
316
|
-
tile.setAttribute('aria-pressed', tile.getAttribute('aria-pressed') === 'true' ? 'false' : 'true');
|
|
317
|
-
renderDiagnosticsDeviceChoice();
|
|
318
|
-
});
|
|
319
|
-
diagnosticsDeviceList.append(tile);
|
|
320
|
-
}
|
|
321
|
-
renderDiagnosticsDeviceChoice();
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
/**
|
|
325
|
-
* The two controls that follow the pressed tiles: whether continuing is possible, and what the escape does.
|
|
326
|
-
*
|
|
327
|
-
* With every tile pressed there is nothing left for a select-all to select, so the same control clears instead —
|
|
328
|
-
* otherwise the only way back from a full grid is pressing each tile again.
|
|
329
|
-
*/
|
|
330
|
-
function renderDiagnosticsDeviceChoice() {
|
|
331
|
-
const tiles = diagnosticsDeviceList.querySelectorAll('[data-serial]').length;
|
|
332
|
-
const chosen = chosenDiagnosticsDevices().length;
|
|
333
|
-
diagnosticsDevicesChosen.disabled = chosen === 0;
|
|
334
|
-
diagnosticsDevicesEvery.textContent =
|
|
335
|
-
messages[chosen > 0 && chosen === tiles ? 'diagnosticsDevicesNone' : 'diagnosticsDevicesEvery'] ?? '';
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
/** The serials pressed in the device step, in the order the grid drew them. */
|
|
339
|
-
function chosenDiagnosticsDevices() {
|
|
340
|
-
return [...diagnosticsDeviceList.querySelectorAll('[data-serial]')]
|
|
341
|
-
.filter((tile) => tile.getAttribute('aria-pressed') === 'true')
|
|
342
|
-
.map((tile) => tile.dataset.serial);
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
function renderDiagnosticsWizard() {
|
|
346
|
-
diagnosticsQuestion.hidden = diagnosticsWizardState.mode !== 'tiles';
|
|
347
|
-
diagnosticsDevices.hidden = diagnosticsWizardState.mode !== 'devices';
|
|
348
|
-
diagnosticsFrequency.hidden = diagnosticsWizardState.mode !== 'frequency';
|
|
349
|
-
diagnosticsMatch.hidden = diagnosticsWizardState.mode !== 'match';
|
|
350
|
-
if (diagnosticsWizardState.mode === 'match') {
|
|
351
|
-
renderDiagnosticsGuidance(diagnosticsWizardState.profile);
|
|
352
|
-
diagnosticsModeSummary.textContent =
|
|
353
|
-
messages[
|
|
354
|
-
diagnosticsWizardState.reproductionMode === 'intermittent'
|
|
355
|
-
? 'diagnosticsModeIntermittentSummary'
|
|
356
|
-
: 'diagnosticsModeNowSummary'
|
|
357
|
-
] ?? '';
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
232
|
/**
|
|
362
233
|
* The report step, reachable or not.
|
|
363
234
|
*
|
|
@@ -367,175 +238,126 @@ function renderDiagnosticsWizard() {
|
|
|
367
238
|
*/
|
|
368
239
|
function setIssueStepReachable(url) {
|
|
369
240
|
diagnosticsIssue.href = url;
|
|
370
|
-
|
|
371
|
-
|
|
241
|
+
diagnosticsExistingIssue.href = url.replace(/\/issues\/new.*$/, '/issues?q=is%3Aissue+involves%3A%40me');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Leaves the archive dialog once its file is handed over, which is what ends the session: the plugin keeps it until
|
|
246
|
+
* then so a download that never arrived can be tried again.
|
|
247
|
+
*/
|
|
248
|
+
async function endDiagnosticsCase() {
|
|
249
|
+
try {
|
|
250
|
+
renderDiagnostics(await requestWithinDeadline('/diagnostics/cancel', undefined, 12000));
|
|
251
|
+
} catch (error) {
|
|
252
|
+
await redrawDiagnosticsAfterFailure('inactive', error);
|
|
253
|
+
}
|
|
254
|
+
diagnosticsQuestionText.focus?.();
|
|
372
255
|
}
|
|
373
256
|
|
|
374
257
|
function renderDiagnostics(state) {
|
|
375
258
|
if (state.supportCaseId !== diagnosticsState.supportCaseId) diagnosticsArchiveDownloaded = false;
|
|
376
259
|
diagnosticsState = state;
|
|
377
|
-
const screen = diagnosticsWizard.screen(state
|
|
260
|
+
const screen = diagnosticsWizard.screen(state);
|
|
378
261
|
const choosing = screen === 'choose';
|
|
379
262
|
const reviewing = screen === 'review';
|
|
380
|
-
|
|
381
|
-
const collecting = isDashboardUiReproducing(state);
|
|
382
|
-
for (const entry of [mastheadDiagnostics, menuDiagnostics]) {
|
|
383
|
-
if (collecting) entry.dataset.collecting = 'true';
|
|
384
|
-
else delete entry.dataset.collecting;
|
|
385
|
-
entry.setAttribute(
|
|
386
|
-
'aria-label',
|
|
387
|
-
messages[collecting ? 'diagnosticsCollectingFinishHere' : 'menuDiagnostics'] ?? '',
|
|
388
|
-
);
|
|
389
|
-
}
|
|
263
|
+
renderDiagnosticsEntries();
|
|
390
264
|
diagnosticsWizardPanel.hidden = !choosing;
|
|
391
265
|
diagnosticsGuidance.hidden = screen !== 'reproduce';
|
|
392
|
-
diagnosticsGuidanceBeforeSection.hidden = state.status === 'reproducing';
|
|
393
266
|
diagnosticsActions.hidden = screen !== 'reproduce';
|
|
394
|
-
diagnosticsReproduction.disabled =
|
|
395
|
-
diagnosticsReproduction.textContent =
|
|
396
|
-
reproductionMode === 'intermittent'
|
|
397
|
-
? state.status === 'reproducing'
|
|
398
|
-
? messages.diagnosticsIntermittentIssueHappened
|
|
399
|
-
: messages.diagnosticsIntermittentStartWaiting
|
|
400
|
-
: state.status === 'reproducing'
|
|
401
|
-
? messages.diagnosticsNowFinish
|
|
402
|
-
: messages.diagnosticsStartRecording;
|
|
403
|
-
diagnosticsIssue.hidden = true;
|
|
267
|
+
diagnosticsReproduction.disabled = state.status !== 'reproducing';
|
|
404
268
|
setIssueStepReachable(diagnosticsArchiveDownloaded ? (state.issueUrl ?? '') : '');
|
|
405
|
-
|
|
269
|
+
const offering = (reviewing || diagnosticsArchiveDownloaded) && !diagnosticsPanel.hidden;
|
|
270
|
+
if (offering && !diagnosticsResult.open) diagnosticsResult.showModal?.();
|
|
271
|
+
if (!offering && diagnosticsResult.open) diagnosticsResult.close?.();
|
|
272
|
+
diagnosticsResultHeading.textContent =
|
|
273
|
+
messages[
|
|
274
|
+
diagnosticsArchiveDownloaded
|
|
275
|
+
? 'diagnosticsArchiveDownloaded'
|
|
276
|
+
: state.reproductionEndedAt && state.reproductionEndedAt === state.expiresAt
|
|
277
|
+
? 'diagnosticsCaptureTimedOut'
|
|
278
|
+
: 'diagnosticsEvidenceReady'
|
|
279
|
+
] ?? '';
|
|
406
280
|
const reviewed = reviewing && diagnosticsReviewedCaseId === (state.supportCaseId ?? '');
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
281
|
+
diagnosticsExport.hidden = !reviewed || diagnosticsArchiveDownloaded;
|
|
282
|
+
diagnosticsHandoff.hidden = !diagnosticsArchiveDownloaded;
|
|
283
|
+
diagnosticsHandoffNote.hidden = !diagnosticsArchiveDownloaded;
|
|
284
|
+
if (!offering) diagnosticsResultStatus.textContent = '';
|
|
285
|
+
else if (!diagnosticsArchiveDownloaded && state.missingEvidence?.length) {
|
|
286
|
+
diagnosticsResultStatus.textContent = messages.diagnosticsMissingEvidence ?? '';
|
|
287
|
+
}
|
|
288
|
+
diagnosticsStartAnother.hidden = !diagnosticsArchiveDownloaded;
|
|
289
|
+
diagnosticsDownloadAgain.hidden = !diagnosticsArchiveDownloaded;
|
|
411
290
|
if (!reviewed) {
|
|
412
|
-
diagnosticsReviewConfirm.checked = false;
|
|
413
291
|
diagnosticsExport.disabled = true;
|
|
414
292
|
diagnosticsReviewId = '';
|
|
415
|
-
if (reviewing) void ensureArchiveReview(state.supportCaseId ?? '');
|
|
293
|
+
if (reviewing && !diagnosticsArchiveDownloaded) void ensureArchiveReview(state.supportCaseId ?? '');
|
|
416
294
|
}
|
|
417
295
|
if (screen === 'reproduce') {
|
|
418
296
|
renderDiagnosticsGuidance(state.profile);
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
: 'diagnosticsIntermittentReadyGuidance'
|
|
426
|
-
] ?? '';
|
|
427
|
-
}
|
|
297
|
+
const stopsAt = new Date(state.expiresAt).toLocaleString(shell.lang || undefined, {
|
|
298
|
+
weekday: 'long',
|
|
299
|
+
hour: 'numeric',
|
|
300
|
+
minute: '2-digit',
|
|
301
|
+
});
|
|
302
|
+
diagnosticsCaptureNote.textContent = (messages.diagnosticsCaptureKeepsRunning ?? '').replace('{time}', stopsAt);
|
|
428
303
|
}
|
|
429
|
-
|
|
430
|
-
const statusKey = reviewing
|
|
431
|
-
? state.missingEvidence?.length
|
|
432
|
-
? 'diagnosticsMissingEvidence'
|
|
433
|
-
: 'diagnosticsComplete'
|
|
434
|
-
: {
|
|
435
|
-
inactive: 'diagnosticsInactive',
|
|
436
|
-
authorized:
|
|
437
|
-
reproductionMode === 'intermittent' ? 'diagnosticsIntermittentAuthorized' : 'diagnosticsAuthorized',
|
|
438
|
-
reproducing:
|
|
439
|
-
reproductionMode === 'intermittent' ? 'diagnosticsIntermittentReproducing' : 'diagnosticsReproducing',
|
|
440
|
-
complete: state.missingEvidence?.length ? 'diagnosticsMissingEvidence' : 'diagnosticsComplete',
|
|
441
|
-
expired: 'diagnosticsExpired',
|
|
442
|
-
}[state.status];
|
|
443
|
-
diagnosticsStatus.textContent = (
|
|
444
|
-
choosing && (state.status === 'inactive' || diagnosticsStartingAnother) ? '' : (messages[statusKey] ?? '')
|
|
445
|
-
).replace('{evidence}', state.missingEvidence?.join(', ') ?? '');
|
|
304
|
+
diagnosticsStatus.textContent = '';
|
|
446
305
|
}
|
|
447
306
|
|
|
448
307
|
/**
|
|
449
|
-
*
|
|
450
|
-
*
|
|
451
|
-
* A class with no entry falls back to its own identifier, which is visible rather than absent, so a newly
|
|
452
|
-
* declared class shows up as something to translate instead of quietly disappearing from the list.
|
|
308
|
+
* Marks both diagnostics actions while a capture runs, whichever area it is on, and otherwise the dashboard's own
|
|
309
|
+
* while it shows a problem.
|
|
453
310
|
*/
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
messages[manifest.reproductionMode === 'intermittent' ? 'diagnosticsModeIntermittent' : 'diagnosticsModeNow'] ??
|
|
468
|
-
manifest.reproductionMode;
|
|
469
|
-
summary.textContent = `${manifest.archiveFormat} v${manifest.version} · ${manifest.keyId} · ${messages.diagnosticsArchiveMode ?? ''} ${mode} · ${(messages.diagnosticsArchiveExpires ?? '').replace('{expiresAt}', new Date(manifest.archiveExpiresAt).toLocaleString(shell.lang || 'en'))}`;
|
|
470
|
-
const evidence = document.createElement('ul');
|
|
471
|
-
for (const item of manifest.evidence ?? []) {
|
|
472
|
-
const row = document.createElement('li');
|
|
473
|
-
const size = item.bytes === undefined ? '' : ` · ${item.bytes} B`;
|
|
474
|
-
const truncated = item.truncated ? ` · ${messages.diagnosticsArchiveTruncated ?? ''}` : '';
|
|
475
|
-
const missing = item.missingReason ? ` · ${item.missingReason}` : '';
|
|
476
|
-
const fields = (item.fields ?? []).map((field) => `${field.field}: ${field.privacyClass}`).join(', ');
|
|
477
|
-
row.textContent = `${item.evidence} · ${item.privacyClass} · ${item.status}${missing}${size}${truncated}${fields ? ` · ${messages.diagnosticsArchiveFields ?? ''} ${fields}` : ''}`;
|
|
478
|
-
evidence.append(row);
|
|
479
|
-
}
|
|
480
|
-
const detail = document.createElement('details');
|
|
481
|
-
const detailLabel = document.createElement('summary');
|
|
482
|
-
detailLabel.textContent = messages.diagnosticsArchiveDetail ?? '';
|
|
483
|
-
detail.append(detailLabel, summary, evidence);
|
|
484
|
-
const exclusions = document.createElement('p');
|
|
485
|
-
const neverCollected = (manifest.excludedClasses ?? []).map(
|
|
486
|
-
(excluded) => messages[NEVER_COLLECTED_LABELS[excluded]] ?? excluded,
|
|
487
|
-
);
|
|
488
|
-
exclusions.textContent = `${messages.diagnosticsArchiveExcluded ?? ''} ${neverCollected.join(', ')}.`;
|
|
489
|
-
diagnosticsManifest.append(detail, exclusions);
|
|
490
|
-
const uncovered = (manifest.evidence ?? []).filter((item) => item.coversReproduction === false);
|
|
491
|
-
if (uncovered.length > 0) {
|
|
492
|
-
const gap = document.createElement('p');
|
|
493
|
-
gap.textContent = (messages.diagnosticsArchiveCoverageGap ?? '')
|
|
494
|
-
.replace('{classes}', uncovered.map((item) => item.evidence).join(', '))
|
|
495
|
-
.replace(
|
|
496
|
-
'{retainedFrom}',
|
|
497
|
-
new Date(
|
|
498
|
-
Math.min(...uncovered.map((item) => Date.parse(item.retainedFrom))),
|
|
499
|
-
).toLocaleString(shell.lang || 'en'),
|
|
500
|
-
);
|
|
501
|
-
diagnosticsManifest.append(gap);
|
|
311
|
+
function renderDiagnosticsEntries() {
|
|
312
|
+
const collecting = diagnosticsState.status === 'reproducing';
|
|
313
|
+
for (const entry of [mastheadDiagnostics, menuDiagnostics]) {
|
|
314
|
+
const attending = !collecting && entry === menuDiagnostics && dashboardAttention !== undefined;
|
|
315
|
+
if (collecting) entry.dataset.collecting = 'true';
|
|
316
|
+
else delete entry.dataset.collecting;
|
|
317
|
+
if (attending) entry.dataset.attention = 'true';
|
|
318
|
+
else delete entry.dataset.attention;
|
|
319
|
+
entry.setAttribute(
|
|
320
|
+
'aria-label',
|
|
321
|
+
messages[collecting ? 'diagnosticsCollectingFinishHere' : attending ? 'diagnosticsAttention' : 'menuDiagnostics'] ??
|
|
322
|
+
'',
|
|
323
|
+
);
|
|
502
324
|
}
|
|
503
|
-
diagnosticsManifest.hidden = false;
|
|
504
325
|
}
|
|
505
326
|
|
|
506
327
|
/**
|
|
507
|
-
* The
|
|
328
|
+
* The reviewed archive for the session on screen, fetched once.
|
|
508
329
|
*
|
|
509
|
-
* A completed reproduction that the reporter is looking at is a file they came for, so the
|
|
510
|
-
*
|
|
511
|
-
* reads every collected log. Exporting spends it,
|
|
512
|
-
* asks for a fresh one.
|
|
330
|
+
* A completed reproduction that the reporter is looking at is a file they came for, so the archive is
|
|
331
|
+
* reviewed without being asked for. It is reviewed once per session rather than per render, because
|
|
332
|
+
* assembling it reads every collected log. Exporting spends it, and ends the session with it.
|
|
513
333
|
*/
|
|
514
|
-
|
|
515
|
-
if (!caseId || diagnosticsReviewedCaseId === caseId
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
}
|
|
334
|
+
function ensureArchiveReview(caseId) {
|
|
335
|
+
if (!caseId || diagnosticsReviewedCaseId === caseId) return Promise.resolve();
|
|
336
|
+
diagnosticsReviewRequest ??= (async () => {
|
|
337
|
+
try {
|
|
338
|
+
const review = await requestWithinDeadline('/diagnostics/archive/review', undefined, 12000);
|
|
339
|
+
diagnosticsReviewId = review.reviewId;
|
|
340
|
+
diagnosticsReviewedCaseId = caseId;
|
|
341
|
+
diagnosticsExport.hidden = false;
|
|
342
|
+
diagnosticsExport.disabled = false;
|
|
343
|
+
} catch {
|
|
344
|
+
diagnosticsResultStatus.textContent = messages.diagnosticsFailed ?? '';
|
|
345
|
+
} finally {
|
|
346
|
+
diagnosticsReviewRequest = undefined;
|
|
347
|
+
}
|
|
348
|
+
})();
|
|
349
|
+
return diagnosticsReviewRequest;
|
|
531
350
|
}
|
|
532
351
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
352
|
+
/**
|
|
353
|
+
* Hands the archive of the session on screen over as a download.
|
|
354
|
+
*
|
|
355
|
+
* Finishing a reproduction calls this without being asked, since the file is what the reporter finished for; the
|
|
356
|
+
* dialog's own button is the way to try again after a failed attempt.
|
|
357
|
+
*/
|
|
358
|
+
async function downloadDiagnosticsArchive() {
|
|
359
|
+
await ensureArchiveReview(diagnosticsState.supportCaseId ?? '');
|
|
360
|
+
if (!diagnosticsReviewId) return;
|
|
539
361
|
diagnosticsExport.disabled = true;
|
|
540
362
|
try {
|
|
541
363
|
const exported = await requestWithinDeadline(
|
|
@@ -546,140 +368,119 @@ diagnosticsExport.addEventListener('click', async () => {
|
|
|
546
368
|
const download = document.createElement('a');
|
|
547
369
|
download.href = `data:${exported.mediaType};base64,${exported.archive}`;
|
|
548
370
|
download.download = exported.filename;
|
|
371
|
+
diagnosticsHandoffNote.textContent = (messages.diagnosticsArchiveHandoff ?? '').replace(
|
|
372
|
+
'{filename}',
|
|
373
|
+
exported.filename,
|
|
374
|
+
);
|
|
375
|
+
diagnosticsResultStatus.textContent = '';
|
|
549
376
|
document.body.appendChild(download);
|
|
550
377
|
download.click();
|
|
551
378
|
document.body.removeChild(download);
|
|
552
379
|
diagnosticsReviewId = '';
|
|
553
380
|
diagnosticsReviewedCaseId = '';
|
|
554
|
-
diagnosticsReviewConfirm.checked = false;
|
|
555
381
|
diagnosticsArchiveDownloaded = true;
|
|
556
|
-
|
|
382
|
+
renderDiagnostics(diagnosticsState);
|
|
383
|
+
diagnosticsResultHeading.focus?.();
|
|
557
384
|
} catch {
|
|
558
|
-
|
|
385
|
+
// The plugin spends a review on any attempt, so trying again asks for a fresh one.
|
|
386
|
+
diagnosticsReviewId = '';
|
|
387
|
+
diagnosticsReviewedCaseId = '';
|
|
388
|
+
diagnosticsExport.disabled = false;
|
|
389
|
+
diagnosticsResultStatus.textContent = messages.diagnosticsFailed ?? '';
|
|
559
390
|
}
|
|
560
|
-
}
|
|
391
|
+
}
|
|
561
392
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
12000,
|
|
575
|
-
);
|
|
576
|
-
diagnosticsStartingAnother = false;
|
|
577
|
-
renderDiagnostics(authorized);
|
|
578
|
-
if (diagnosticsState.status === 'authorized') diagnosticsGuidance.focus?.();
|
|
579
|
-
} catch {
|
|
580
|
-
try {
|
|
581
|
-
const refreshed = await requestWithinDeadline('/diagnostics/status', undefined, 12000);
|
|
582
|
-
if (refreshed.status !== previousStatus) diagnosticsStartingAnother = false;
|
|
583
|
-
renderDiagnostics(refreshed);
|
|
584
|
-
if (refreshed.status === previousStatus) {
|
|
585
|
-
diagnosticsStatus.textContent = messages.diagnosticsFailed ?? '';
|
|
586
|
-
focusAuthorizeAfter = true;
|
|
587
|
-
} else if (refreshed.status === 'authorized') diagnosticsGuidance.focus?.();
|
|
588
|
-
} catch {
|
|
589
|
-
diagnosticsStatus.textContent = messages.diagnosticsFailed ?? '';
|
|
590
|
-
}
|
|
591
|
-
} finally {
|
|
592
|
-
diagnosticsAuthorize.disabled = false;
|
|
593
|
-
if (focusAuthorizeAfter) diagnosticsAuthorize.focus?.();
|
|
393
|
+
diagnosticsExport.addEventListener('click', downloadDiagnosticsArchive);
|
|
394
|
+
diagnosticsDownloadAgain.addEventListener('click', downloadDiagnosticsArchive);
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Draws the session as the plugin holds it after `error`, and says so unless it reached `expected`, inside the
|
|
398
|
+
* download dialog while that covers the panel. A request that timed out may still land, so its late answer draws
|
|
399
|
+
* the session again.
|
|
400
|
+
*/
|
|
401
|
+
async function redrawDiagnosticsAfterFailure(expected, error) {
|
|
402
|
+
const refreshed = await settleDiagnostics().catch(() => undefined);
|
|
403
|
+
if (refreshed?.status !== expected) {
|
|
404
|
+
(diagnosticsResult.open ? diagnosticsResultStatus : diagnosticsStatus).textContent = messages.diagnosticsFailed ?? '';
|
|
594
405
|
}
|
|
595
|
-
|
|
406
|
+
error?.late?.then(settleDiagnostics).catch(() => undefined);
|
|
407
|
+
}
|
|
596
408
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
409
|
+
/**
|
|
410
|
+
* Draws the session the plugin holds. One opened but never started is what a failed or late pick leaves behind;
|
|
411
|
+
* nothing on screen stands for it and it keeps detailed logging on, so it is cancelled rather than drawn.
|
|
412
|
+
*/
|
|
413
|
+
async function settleDiagnostics() {
|
|
414
|
+
let state = await requestWithinDeadline('/diagnostics/status', undefined, 12000);
|
|
415
|
+
if (state.status === 'authorized') state = await requestWithinDeadline('/diagnostics/cancel', undefined, 12000);
|
|
416
|
+
renderDiagnostics(state);
|
|
417
|
+
return state;
|
|
604
418
|
}
|
|
605
419
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
420
|
+
/**
|
|
421
|
+
* Captures the area picked. The area is the only answer a capture needs, so the pick opens the session and starts
|
|
422
|
+
* it in one go.
|
|
423
|
+
*/
|
|
424
|
+
async function startDiagnosticsCapture(profile) {
|
|
425
|
+
for (const tile of diagnosticsTiles) tile.disabled = true;
|
|
426
|
+
try {
|
|
427
|
+
await requestWithinDeadline('/diagnostics/authorize', { profile }, 12000);
|
|
428
|
+
const state = await requestWithinDeadline('/diagnostics/reproduction/start', undefined, 12000);
|
|
429
|
+
renderDiagnostics(state);
|
|
430
|
+
if (isDashboardUiReproducing(state)) {
|
|
431
|
+
await recordActiveUiEvent('background-started').catch(() => undefined);
|
|
432
|
+
closeDashboardPanel();
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
diagnosticsPhaseTitle.focus?.();
|
|
436
|
+
} catch (error) {
|
|
437
|
+
await redrawDiagnosticsAfterFailure('reproducing', error);
|
|
438
|
+
} finally {
|
|
439
|
+
for (const tile of diagnosticsTiles) tile.disabled = false;
|
|
613
440
|
}
|
|
614
|
-
const clearing = chosenDiagnosticsDevices().length === tiles.length;
|
|
615
|
-
for (const tile of tiles) tile.setAttribute('aria-pressed', clearing ? 'false' : 'true');
|
|
616
|
-
renderDiagnosticsDeviceChoice();
|
|
617
|
-
});
|
|
618
|
-
|
|
619
|
-
diagnosticsDevicesChosen.addEventListener('click', () => {
|
|
620
|
-
diagnosticsWizardState = diagnosticsWizard.chooseDevices(diagnosticsWizardState, chosenDiagnosticsDevices());
|
|
621
|
-
renderDiagnosticsWizard();
|
|
622
|
-
diagnosticsFrequencyHeading.focus?.();
|
|
623
|
-
});
|
|
624
|
-
|
|
625
|
-
function chooseDiagnosticsReproductionMode(reproductionMode) {
|
|
626
|
-
diagnosticsWizardState = diagnosticsWizard.chooseReproductionMode(diagnosticsWizardState, reproductionMode);
|
|
627
|
-
renderDiagnosticsWizard();
|
|
628
|
-
diagnosticsGuidanceTitle.focus?.();
|
|
629
441
|
}
|
|
630
442
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
diagnosticsFrequencyBack.addEventListener('click', () => {
|
|
635
|
-
diagnosticsWizardState = diagnosticsWizard.backFromFrequency(diagnosticsWizardState);
|
|
636
|
-
renderDiagnosticsWizard();
|
|
637
|
-
diagnosticsQuestionText.focus?.();
|
|
638
|
-
});
|
|
639
|
-
|
|
640
|
-
diagnosticsReject.addEventListener('click', () => {
|
|
641
|
-
diagnosticsWizardState = diagnosticsWizard.reject(diagnosticsWizardState);
|
|
642
|
-
renderDiagnosticsWizard();
|
|
643
|
-
diagnosticsQuestionText.focus?.();
|
|
644
|
-
});
|
|
443
|
+
for (const tile of diagnosticsTiles) {
|
|
444
|
+
tile.addEventListener('click', () => startDiagnosticsCapture(tile.dataset.diagnosticsTile));
|
|
445
|
+
}
|
|
645
446
|
|
|
646
|
-
diagnosticsStartAnother.addEventListener('click',
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
447
|
+
diagnosticsStartAnother.addEventListener('click', endDiagnosticsCase);
|
|
448
|
+
/** Escape leaves the archive dialog only once its file is downloaded, and leaving it finishes the session. */
|
|
449
|
+
diagnosticsResult.addEventListener('cancel', (event) => {
|
|
450
|
+
event.preventDefault();
|
|
451
|
+
if (diagnosticsArchiveDownloaded) endDiagnosticsCase();
|
|
651
452
|
});
|
|
652
453
|
|
|
653
454
|
diagnosticsReproduction.addEventListener('click', async () => {
|
|
654
455
|
diagnosticsReproduction.disabled = true;
|
|
655
|
-
const previousStatus = diagnosticsState.status;
|
|
656
|
-
const path =
|
|
657
|
-
diagnosticsState.status === 'reproducing' ? '/diagnostics/reproduction/end' : '/diagnostics/reproduction/start';
|
|
658
456
|
try {
|
|
659
|
-
let state = await requestWithinDeadline(
|
|
457
|
+
let state = await requestWithinDeadline('/diagnostics/reproduction/end', undefined, 12000);
|
|
660
458
|
if (state.status === 'complete' && state.missingEvidence?.length) {
|
|
661
459
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
662
460
|
state = await requestWithinDeadline('/diagnostics/status', undefined, 12000);
|
|
663
461
|
}
|
|
664
462
|
renderDiagnostics(state);
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
463
|
+
} catch (error) {
|
|
464
|
+
await redrawDiagnosticsAfterFailure('complete', error);
|
|
465
|
+
} finally {
|
|
466
|
+
diagnosticsReproduction.disabled = diagnosticsState.status !== 'reproducing';
|
|
467
|
+
}
|
|
468
|
+
if (diagnosticsState.partialExportAvailable) {
|
|
469
|
+
diagnosticsResultHeading.focus?.();
|
|
470
|
+
await downloadDiagnosticsArchive();
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
/** Cancelling a capture deletes its session without a file, and puts the areas back on the panel. */
|
|
475
|
+
diagnosticsCancel.addEventListener('click', async () => {
|
|
476
|
+
diagnosticsCancel.disabled = true;
|
|
477
|
+
try {
|
|
478
|
+
renderDiagnostics(await requestWithinDeadline('/diagnostics/cancel', undefined, 12000));
|
|
479
|
+
diagnosticsQuestionText.focus?.();
|
|
480
|
+
} catch (error) {
|
|
481
|
+
await redrawDiagnosticsAfterFailure('inactive', error);
|
|
681
482
|
} finally {
|
|
682
|
-
|
|
483
|
+
diagnosticsCancel.disabled = false;
|
|
683
484
|
}
|
|
684
485
|
});
|
|
685
486
|
|
|
@@ -736,29 +537,46 @@ function closeDashboardPanel() {
|
|
|
736
537
|
}
|
|
737
538
|
}
|
|
738
539
|
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
/*
|
|
746
|
-
* Opened from the sign-in screen, the area is not in question: what is going wrong is getting signed in. The
|
|
747
|
-
* answer is presumed rather than fixed, so the frequency step's way back still reaches the other six.
|
|
748
|
-
*/
|
|
540
|
+
/**
|
|
541
|
+
* Opens the diagnostics panel. A preset is an area the reporter already chose to diagnose from a problem the
|
|
542
|
+
* dashboard named beside the privacy note, so a panel with nothing under way captures it at once. Opened over the
|
|
543
|
+
* sign-in screen, the panel asks like any other and puts focus on the startup area.
|
|
544
|
+
*/
|
|
545
|
+
async function openDiagnostics(preset) {
|
|
749
546
|
const signingIn = !setupContent.hidden;
|
|
750
547
|
openDashboardPanel(diagnosticsPanel, menuDiagnostics);
|
|
751
|
-
if (signingIn) {
|
|
752
|
-
diagnosticsWizardState = diagnosticsWizard.select(diagnosticsWizard.start(), 'startup-authentication');
|
|
753
|
-
}
|
|
754
|
-
// Drawn on opening, because the wizard's first question is only ever put on the page by this call.
|
|
755
|
-
renderDiagnosticsWizard();
|
|
756
548
|
try {
|
|
757
549
|
renderDiagnostics(await requestWithinDeadline('/diagnostics/status', undefined, 12000));
|
|
758
550
|
} catch {
|
|
759
551
|
diagnosticsStatus.textContent = messages.diagnosticsFailed ?? '';
|
|
760
552
|
recordActiveUiEventBestEffort('request-failed');
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
if (diagnosticsWizard.screen(diagnosticsState) !== 'choose') return;
|
|
556
|
+
if (preset) await startDiagnosticsCapture(preset.profile);
|
|
557
|
+
else if (signingIn) {
|
|
558
|
+
diagnosticsTiles.find((tile) => tile.dataset.diagnosticsTile === 'startup-authentication')?.focus?.();
|
|
761
559
|
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
mastheadDiagnostics.addEventListener('click', () => openDiagnostics());
|
|
563
|
+
|
|
564
|
+
/** While the dashboard shows a problem and no session is under way, the action names it before opening the wizard. */
|
|
565
|
+
menuDiagnostics.addEventListener('click', () => {
|
|
566
|
+
if (!menuDiagnostics.dataset.attention || diagnosticsWizard.screen(diagnosticsState) !== 'choose') {
|
|
567
|
+
return openDiagnostics();
|
|
568
|
+
}
|
|
569
|
+
attentionTitle.textContent = dashboardAttention.title;
|
|
570
|
+
attentionSummary.textContent = dashboardAttention.summary;
|
|
571
|
+
attentionDialog.showModal?.();
|
|
572
|
+
});
|
|
573
|
+
attentionDiagnose.addEventListener('click', () => {
|
|
574
|
+
attentionDialog.close?.();
|
|
575
|
+
return openDiagnostics(dashboardAttention);
|
|
576
|
+
});
|
|
577
|
+
attentionChoose.addEventListener('click', () => {
|
|
578
|
+
attentionDialog.close?.();
|
|
579
|
+
return openDiagnostics();
|
|
762
580
|
});
|
|
763
581
|
menuAdvanced.addEventListener('click', async () => {
|
|
764
582
|
const config = configuredBlock() ?? {};
|
|
@@ -785,9 +603,8 @@ menuAdvanced.addEventListener('click', async () => {
|
|
|
785
603
|
}
|
|
786
604
|
});
|
|
787
605
|
diagnosticsClose.addEventListener('click', () => {
|
|
788
|
-
diagnosticsStartingAnother = false;
|
|
789
|
-
renderDiagnostics(diagnosticsState);
|
|
790
606
|
closeDashboardPanel();
|
|
607
|
+
renderDiagnostics(diagnosticsState);
|
|
791
608
|
});
|
|
792
609
|
advancedClose.addEventListener('click', () => {
|
|
793
610
|
closeDashboardPanel();
|
|
@@ -1041,7 +858,7 @@ async function showDashboard() {
|
|
|
1041
858
|
warmUpCandidates = Array.isArray(snapshot.warmUpCandidates) ? snapshot.warmUpCandidates : [];
|
|
1042
859
|
// Kept beside the render that drew them, so opening a tile finds the device that tile stands for.
|
|
1043
860
|
dashboardDevices = snapshot.devices ?? [];
|
|
1044
|
-
dashboardView.render(snapshot, configuredBlock() ?? {}, messages, dashboardElements);
|
|
861
|
+
dashboardAttention = dashboardView.render(snapshot, configuredBlock() ?? {}, messages, dashboardElements);
|
|
1045
862
|
if (snapshot.state === 'authentication-required' && snapshot.devices?.length > 0 && !reauthDialog.open) {
|
|
1046
863
|
reauthDialog.showModal?.();
|
|
1047
864
|
}
|
|
@@ -1050,9 +867,15 @@ async function showDashboard() {
|
|
|
1050
867
|
.applyDeviceImages(dashboardElements, (serial) => requestWithinDeadline('/device/image', { serial }, 12000))
|
|
1051
868
|
.catch(() => undefined);
|
|
1052
869
|
} catch {
|
|
1053
|
-
dashboardView.render(
|
|
870
|
+
dashboardAttention = dashboardView.render(
|
|
871
|
+
{ state: 'unreachable', devices: [] },
|
|
872
|
+
configuredBlock() ?? {},
|
|
873
|
+
messages,
|
|
874
|
+
dashboardElements,
|
|
875
|
+
);
|
|
1054
876
|
recordActiveUiEventBestEffort('request-failed');
|
|
1055
877
|
}
|
|
878
|
+
renderDiagnosticsEntries();
|
|
1056
879
|
recordActiveUiEventBestEffort('dashboard-opened');
|
|
1057
880
|
}
|
|
1058
881
|
|