@homebridge-plugins/homebridge-eufy-security 5.0.0-beta.3 → 5.0.0-beta.30
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/config.schema.json +48 -0
- package/dist/account/temporary-authentication.js +3 -1
- package/dist/account/temporary-authentication.js.map +1 -1
- package/dist/configuration.js +44 -2
- package/dist/configuration.js.map +1 -1
- package/dist/device/member-evidence.js +10 -3
- package/dist/device/member-evidence.js.map +1 -1
- package/dist/device/snapshot.js +1 -1
- package/dist/device/snapshot.js.map +1 -1
- package/dist/diagnostics.js +694 -83
- package/dist/diagnostics.js.map +1 -1
- package/dist/homekit/adapters/camera-streaming.js +56 -27
- package/dist/homekit/adapters/camera-streaming.js.map +1 -1
- package/dist/homekit/adapters/lock.js +45 -16
- package/dist/homekit/adapters/lock.js.map +1 -1
- package/dist/homekit/adapters/motion.js +1 -1
- package/dist/homekit/adapters/motion.js.map +1 -1
- package/dist/homekit/adapters/package-sensor.js +63 -0
- package/dist/homekit/adapters/package-sensor.js.map +1 -0
- package/dist/homekit/adapters/registry.js +2 -0
- package/dist/homekit/adapters/registry.js.map +1 -1
- package/dist/homekit/adapters/security-system.js +101 -45
- package/dist/homekit/adapters/security-system.js.map +1 -1
- package/dist/homekit/coverage-matrix.js +1 -3
- package/dist/homekit/coverage-matrix.js.map +1 -1
- package/dist/homekit/reconciler.js +21 -5
- package/dist/homekit/reconciler.js.map +1 -1
- package/dist/media/last-successful-image.js +5 -3
- package/dist/media/last-successful-image.js.map +1 -1
- package/dist/media/live-stream.js +200 -84
- package/dist/media/live-stream.js.map +1 -1
- package/dist/media/recording.js +11 -5
- package/dist/media/recording.js.map +1 -1
- package/dist/media/snapshot.js +21 -8
- package/dist/media/snapshot.js.map +1 -1
- package/dist/media/station-live-sessions.js +47 -43
- package/dist/media/station-live-sessions.js.map +1 -1
- package/dist/platform.js +27 -7
- package/dist/platform.js.map +1 -1
- package/dist/runtime/channel.js +116 -4
- package/dist/runtime/channel.js.map +1 -1
- package/dist/runtime/device-settings.js +88 -0
- package/dist/runtime/device-settings.js.map +1 -0
- package/dist/runtime/owner.js +12 -0
- package/dist/runtime/owner.js.map +1 -1
- package/dist/runtime/sdk-client.js +19 -4
- package/dist/runtime/sdk-client.js.map +1 -1
- package/dist/ui/dashboard.js +32 -14
- package/dist/ui/dashboard.js.map +1 -1
- package/dist/ui/runtime-channel-client.js +64 -1
- package/dist/ui/runtime-channel-client.js.map +1 -1
- package/dist/ui/server.js +64 -5
- package/dist/ui/server.js.map +1 -1
- package/homebridge-ui/public/app.css +359 -88
- package/homebridge-ui/public/assets/icons/battery_0.svg +1 -0
- package/homebridge-ui/public/assets/icons/battery_1.svg +1 -0
- package/homebridge-ui/public/assets/icons/battery_2.svg +1 -0
- package/homebridge-ui/public/assets/icons/battery_3.svg +1 -0
- package/homebridge-ui/public/assets/icons/battery_4.svg +1 -0
- package/homebridge-ui/public/assets/icons/battery_5.svg +1 -0
- package/homebridge-ui/public/assets/icons/battery_6.svg +1 -0
- package/homebridge-ui/public/assets/icons/battery_full.svg +1 -0
- package/homebridge-ui/public/assets/icons/contact_support.svg +1 -0
- package/homebridge-ui/public/assets/icons/delete.svg +1 -0
- package/homebridge-ui/public/assets/icons/download.svg +1 -0
- package/homebridge-ui/public/assets/icons/extension_off.svg +1 -0
- package/homebridge-ui/public/assets/icons/info.svg +1 -0
- package/homebridge-ui/public/assets/icons/key_off.svg +1 -0
- package/homebridge-ui/public/assets/icons/keyboard_arrow_left.svg +1 -0
- package/homebridge-ui/public/assets/icons/keyboard_arrow_right.svg +1 -0
- package/homebridge-ui/public/assets/icons/keyboard_double_arrow_left.svg +1 -0
- package/homebridge-ui/public/assets/icons/keyboard_double_arrow_right.svg +1 -0
- package/homebridge-ui/public/assets/icons/movie_off.svg +1 -0
- package/homebridge-ui/public/assets/icons/refresh.svg +1 -0
- package/homebridge-ui/public/assets/icons/satellite_alt.svg +1 -0
- package/homebridge-ui/public/assets/icons/search_off.svg +1 -0
- package/homebridge-ui/public/assets/icons/sensors_off.svg +1 -0
- package/homebridge-ui/public/assets/icons/settings_photo_camera.svg +1 -0
- package/homebridge-ui/public/assets/icons/solar_power.svg +1 -0
- package/homebridge-ui/public/assets/icons/videocam_off.svg +1 -0
- package/homebridge-ui/public/i18n/en.json +119 -78
- package/homebridge-ui/public/i18n/fr.json +119 -78
- package/homebridge-ui/public/index.html +115 -106
- package/homebridge-ui/public/js/app.js +350 -358
- package/homebridge-ui/public/js/dashboard.js +139 -10
- package/homebridge-ui/public/js/profile-wizard.js +17 -54
- package/i18n/runtime/en.json +8 -3
- package/package.json +9 -6
|
@@ -22,6 +22,13 @@ const dashboardTitle = document.querySelector('[data-dashboard-title]');
|
|
|
22
22
|
const dashboardBadge = document.querySelector('[data-dashboard-badge]');
|
|
23
23
|
const dashboardSummary = document.querySelector('[data-dashboard-summary]');
|
|
24
24
|
const dashboardAuthenticate = document.querySelector('[data-dashboard-authenticate]');
|
|
25
|
+
const reauthDialog = document.querySelector('[data-reauth-dialog]');
|
|
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]');
|
|
25
32
|
const deviceGroups = document.querySelector('[data-device-groups]');
|
|
26
33
|
const updatePending = document.querySelector('[data-update-pending]');
|
|
27
34
|
const updatePendingSummary = document.querySelector('[data-update-pending-summary]');
|
|
@@ -31,50 +38,34 @@ const pageTitle = document.querySelector('[data-page-title]');
|
|
|
31
38
|
const legacyNotice = document.querySelector('[data-legacy-notice]');
|
|
32
39
|
const legacySettings = document.querySelector('[data-legacy-settings]');
|
|
33
40
|
const legacyAcknowledge = document.querySelector('[data-legacy-acknowledge]');
|
|
41
|
+
const legacyStatus = document.querySelector('[data-legacy-status]');
|
|
34
42
|
const menuDiagnostics = document.querySelector('[data-menu-diagnostics]');
|
|
35
43
|
const menuAdvanced = document.querySelector('[data-menu-advanced]');
|
|
36
44
|
const diagnosticsPanel = document.querySelector('[data-diagnostics]');
|
|
37
45
|
const mastheadDiagnostics = document.querySelector('[data-masthead-diagnostics]');
|
|
38
46
|
const diagnosticsClose = document.querySelector('[data-diagnostics-close]');
|
|
39
47
|
const diagnosticsWizardPanel = document.querySelector('[data-diagnostics-wizard]');
|
|
40
|
-
const diagnosticsQuestion = document.querySelector('[data-diagnostics-question]');
|
|
41
48
|
const diagnosticsQuestionText = document.querySelector('[data-diagnostics-question-text]');
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
const diagnosticsDirect = document.querySelector('[data-diagnostics-direct]');
|
|
45
|
-
const diagnosticsDirectPanel = document.querySelector('[data-diagnostics-direct-panel]');
|
|
46
|
-
const diagnosticsDirectChoose = document.querySelector('[data-diagnostics-direct-choose]');
|
|
47
|
-
const diagnosticsDirectBack = document.querySelector('[data-diagnostics-direct-back]');
|
|
48
|
-
const diagnosticsFrequency = document.querySelector('[data-diagnostics-frequency]');
|
|
49
|
-
const diagnosticsFrequencyHeading = document.querySelector('#diagnostics-frequency-heading');
|
|
50
|
-
const diagnosticsFrequencyNow = document.querySelector('[data-diagnostics-frequency-answer="now"]');
|
|
51
|
-
const diagnosticsFrequencyIntermittent = document.querySelector('[data-diagnostics-frequency-answer="intermittent"]');
|
|
52
|
-
const diagnosticsFrequencyBack = document.querySelector('[data-diagnostics-frequency-back]');
|
|
53
|
-
const diagnosticsMatch = document.querySelector('[data-diagnostics-match]');
|
|
54
|
-
const diagnosticsReject = document.querySelector('[data-diagnostics-reject]');
|
|
55
|
-
const diagnosticsProfile = document.querySelector('[data-diagnostics-profile]');
|
|
56
|
-
const diagnosticsAuthorize = document.querySelector('[data-diagnostics-authorize]');
|
|
49
|
+
const diagnosticsTiles = [...document.querySelectorAll('[data-diagnostics-tile]')];
|
|
50
|
+
const diagnosticsCancel = document.querySelector('[data-diagnostics-cancel]');
|
|
57
51
|
const diagnosticsReproduction = document.querySelector('[data-diagnostics-reproduction]');
|
|
58
52
|
const diagnosticsStatus = document.querySelector('[data-diagnostics-status]');
|
|
59
53
|
const diagnosticsIssue = document.querySelector('[data-diagnostics-issue]');
|
|
54
|
+
const diagnosticsExistingIssue = document.querySelector('[data-diagnostics-existing-issue]');
|
|
60
55
|
const diagnosticsResult = document.querySelector('[data-diagnostics-result]');
|
|
61
56
|
const diagnosticsActions = document.querySelector('[data-diagnostics-actions]');
|
|
62
|
-
const diagnosticsGuidanceTitle = document.querySelector('[data-diagnostics-guidance-title]');
|
|
63
|
-
const diagnosticsModeSummary = document.querySelector('[data-diagnostics-mode-summary]');
|
|
64
57
|
const diagnosticsGuidance = document.querySelector('[data-diagnostics-guidance]');
|
|
65
58
|
const diagnosticsPhaseTitle = document.querySelector('[data-diagnostics-phase-title]');
|
|
66
|
-
const diagnosticsGuidanceBeforeSection = document.querySelector('[data-diagnostics-guidance-before-section]');
|
|
67
|
-
const diagnosticsGuidanceBefore = document.querySelector('[data-diagnostics-guidance-before]');
|
|
68
59
|
const diagnosticsGuidanceAction = document.querySelector('[data-diagnostics-guidance-action]');
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
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]');
|
|
73
64
|
const diagnosticsExport = document.querySelector('[data-diagnostics-export]');
|
|
74
65
|
const diagnosticsResultHeading = document.querySelector('[data-diagnostics-result-heading]');
|
|
75
66
|
const diagnosticsStartAnother = document.querySelector('[data-diagnostics-start-another]');
|
|
67
|
+
const diagnosticsDownloadAgain = document.querySelector('[data-diagnostics-download-again]');
|
|
76
68
|
const advancedPanel = document.querySelector('[data-advanced-settings]');
|
|
77
|
-
const dashboardDiagnose = document.querySelector('[data-dashboard-diagnose]');
|
|
78
69
|
const devicePanel = document.querySelector('[data-device-settings]');
|
|
79
70
|
const deviceClose = document.querySelector('[data-device-close]');
|
|
80
71
|
const deviceSettingsTitle = document.querySelector('[data-device-settings-title]');
|
|
@@ -84,6 +75,10 @@ const advancedClose = document.querySelector('[data-advanced-close]');
|
|
|
84
75
|
const advancedPolling = document.querySelector('[data-advanced-polling]');
|
|
85
76
|
const advancedConcurrentMedia = document.querySelector('[data-advanced-concurrent-media]');
|
|
86
77
|
const advancedFfmpeg = document.querySelector('[data-advanced-ffmpeg]');
|
|
78
|
+
const advancedSmallVideoPackets = document.querySelector('[data-advanced-small-video-packets]');
|
|
79
|
+
const advancedLiveVideoPassthrough = document.querySelector('[data-advanced-live-video-passthrough]');
|
|
80
|
+
const advancedHdLiveVideo = document.querySelector('[data-advanced-hd-live-video]');
|
|
81
|
+
const advancedIgnoreHomeBitRate = document.querySelector('[data-advanced-ignore-home-bit-rate]');
|
|
87
82
|
const warmUpAvailable = document.querySelector('[data-warm-up-available]');
|
|
88
83
|
const warmUpChosen = document.querySelector('[data-warm-up-chosen]');
|
|
89
84
|
const warmUpAdd = document.querySelector('[data-warm-up-add]');
|
|
@@ -100,18 +95,23 @@ let challenge = '';
|
|
|
100
95
|
let legacyNames = [];
|
|
101
96
|
let legacyAcknowledged = false;
|
|
102
97
|
let diagnosticsState = { status: 'inactive', missingEvidence: [] };
|
|
98
|
+
/** Whether the reporter already downloaded this session's archive, which is what the report offer follows. */
|
|
99
|
+
let diagnosticsArchiveDownloaded = false;
|
|
100
|
+
/** The session whose archive is reviewed and ready to export, so the review is fetched once rather than per render. */
|
|
101
|
+
let diagnosticsReviewedCaseId = '';
|
|
102
|
+
/** The one fetch in flight, so a burst of renders does not assemble the archive several times over. */
|
|
103
|
+
let diagnosticsReviewRequest;
|
|
103
104
|
let diagnosticsReviewId = '';
|
|
104
|
-
let diagnosticsStartingAnother = false;
|
|
105
105
|
let panelReturn;
|
|
106
|
+
/** The problem the dashboard last drew, which the dashboard's diagnostics action points at while there is one. */
|
|
107
|
+
let dashboardAttention;
|
|
106
108
|
let dashboardPanelTrigger;
|
|
107
109
|
const dashboardView = window.HomebridgeEufyDashboard;
|
|
108
110
|
const legacySettingsView = window.HomebridgeEufyLegacySettings;
|
|
109
111
|
const diagnosticsWizard = window.HomebridgeEufyDiagnosticsWizard;
|
|
110
|
-
let diagnosticsWizardState = diagnosticsWizard.start();
|
|
111
112
|
const dashboardElements = {
|
|
112
113
|
dashboard,
|
|
113
114
|
title: dashboardTitle,
|
|
114
|
-
diagnose: dashboardDiagnose,
|
|
115
115
|
summary: dashboardSummary,
|
|
116
116
|
authenticate: dashboardAuthenticate,
|
|
117
117
|
groups: deviceGroups,
|
|
@@ -127,12 +127,19 @@ const dashboardElements = {
|
|
|
127
127
|
masthead,
|
|
128
128
|
};
|
|
129
129
|
|
|
130
|
+
const REQUEST_TIMED_OUT = 'Request timed out';
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* A request the page stops waiting for after `timeoutMs`. The timeout error carries the request as `late`, since
|
|
134
|
+
* the plugin may still carry it out after the page gave up.
|
|
135
|
+
*/
|
|
130
136
|
function requestWithinDeadline(path, body, timeoutMs = 320000) {
|
|
131
137
|
let timer;
|
|
138
|
+
const request = homebridge.request(path, body);
|
|
132
139
|
const timeout = new Promise((_, reject) => {
|
|
133
|
-
timer = setTimeout(() => reject(new Error(
|
|
140
|
+
timer = setTimeout(() => reject(Object.assign(new Error(REQUEST_TIMED_OUT), { late: request })), timeoutMs);
|
|
134
141
|
});
|
|
135
|
-
return Promise.race([
|
|
142
|
+
return Promise.race([request, timeout]).finally(() => clearTimeout(timer));
|
|
136
143
|
}
|
|
137
144
|
|
|
138
145
|
function isDashboardUiReproducing(state = diagnosticsState) {
|
|
@@ -148,6 +155,11 @@ function recordActiveUiEventBestEffort(event) {
|
|
|
148
155
|
void recordActiveUiEvent(event).catch(() => undefined);
|
|
149
156
|
}
|
|
150
157
|
|
|
158
|
+
/** A country code that is not two letters is refused where it is typed, in the page's own words. */
|
|
159
|
+
countryInput.addEventListener('input', () => {
|
|
160
|
+
countryInput.setCustomValidity(countryInput.validity.patternMismatch ? (messages.countryInvalid ?? '') : '');
|
|
161
|
+
});
|
|
162
|
+
|
|
151
163
|
acknowledgement.addEventListener('change', () => {
|
|
152
164
|
continueButton.disabled = !acknowledgement.checked;
|
|
153
165
|
});
|
|
@@ -206,211 +218,150 @@ function setBusy(busy) {
|
|
|
206
218
|
}
|
|
207
219
|
|
|
208
220
|
const diagnosticsProfiles = {
|
|
209
|
-
'startup-authentication': {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
},
|
|
215
|
-
'
|
|
216
|
-
title: 'diagnosticsProfileDevices',
|
|
217
|
-
summary: 'diagnosticsDevicesSummary',
|
|
218
|
-
before: 'diagnosticsDevicesBefore',
|
|
219
|
-
action: 'diagnosticsDevicesAction',
|
|
220
|
-
},
|
|
221
|
-
'control-state': {
|
|
222
|
-
title: 'diagnosticsProfileControl',
|
|
223
|
-
summary: 'diagnosticsControlSummary',
|
|
224
|
-
before: 'diagnosticsControlBefore',
|
|
225
|
-
action: 'diagnosticsControlAction',
|
|
226
|
-
},
|
|
227
|
-
'live-media': {
|
|
228
|
-
title: 'diagnosticsProfileLiveMedia',
|
|
229
|
-
summary: 'diagnosticsLiveSummary',
|
|
230
|
-
before: 'diagnosticsLiveBefore',
|
|
231
|
-
action: 'diagnosticsLiveAction',
|
|
232
|
-
},
|
|
233
|
-
'hksv-recording': {
|
|
234
|
-
title: 'diagnosticsProfileRecording',
|
|
235
|
-
summary: 'diagnosticsRecordingSummary',
|
|
236
|
-
before: 'diagnosticsRecordingBefore',
|
|
237
|
-
action: 'diagnosticsRecordingAction',
|
|
238
|
-
},
|
|
239
|
-
'dashboard-ui': {
|
|
240
|
-
title: 'diagnosticsProfileDashboard',
|
|
241
|
-
summary: 'diagnosticsDashboardSummary',
|
|
242
|
-
before: 'diagnosticsDashboardBefore',
|
|
243
|
-
action: 'diagnosticsDashboardAction',
|
|
244
|
-
},
|
|
245
|
-
other: {
|
|
246
|
-
title: 'diagnosticsProfileOther',
|
|
247
|
-
summary: 'diagnosticsOtherSummary',
|
|
248
|
-
before: 'diagnosticsOtherBefore',
|
|
249
|
-
action: 'diagnosticsOtherAction',
|
|
250
|
-
},
|
|
221
|
+
'startup-authentication': { title: 'diagnosticsProfileStartup', action: 'diagnosticsStartupAction' },
|
|
222
|
+
'device-representation': { title: 'diagnosticsProfileDevices', action: 'diagnosticsDevicesAction' },
|
|
223
|
+
'control-state': { title: 'diagnosticsProfileControl', action: 'diagnosticsControlAction' },
|
|
224
|
+
'live-media': { title: 'diagnosticsProfileLiveMedia', action: 'diagnosticsLiveAction' },
|
|
225
|
+
'hksv-recording': { title: 'diagnosticsProfileRecording', action: 'diagnosticsRecordingAction' },
|
|
226
|
+
'dashboard-ui': { title: 'diagnosticsProfileDashboard', action: 'diagnosticsDashboardAction' },
|
|
227
|
+
other: { title: 'diagnosticsProfileOther', action: 'diagnosticsOtherAction' },
|
|
251
228
|
};
|
|
252
229
|
|
|
253
230
|
function renderDiagnosticsGuidance(profile) {
|
|
254
231
|
const guidance = diagnosticsProfiles[profile] ?? diagnosticsProfiles.other;
|
|
255
|
-
diagnosticsGuidanceTitle.textContent = messages[guidance.title] ?? '';
|
|
256
232
|
diagnosticsPhaseTitle.textContent = messages[guidance.title] ?? '';
|
|
257
|
-
diagnosticsGuidanceBefore.textContent = messages[guidance.before] ?? '';
|
|
258
233
|
diagnosticsGuidanceAction.textContent = messages[guidance.action] ?? '';
|
|
259
234
|
}
|
|
260
235
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
236
|
+
/**
|
|
237
|
+
* The report step, reachable or not.
|
|
238
|
+
*
|
|
239
|
+
* Without an archive there is nothing to attach, so the anchor carries no `href` — it is not a link — and
|
|
240
|
+
* `aria-disabled` states the same thing to a reader who cannot see it greyed. The hint says why, because a
|
|
241
|
+
* control that cannot be used and does not explain itself reads as broken rather than as not yet.
|
|
242
|
+
*/
|
|
243
|
+
function setIssueStepReachable(url) {
|
|
244
|
+
diagnosticsIssue.href = url;
|
|
245
|
+
diagnosticsExistingIssue.href = url.replace(/\/issues\/new.*$/, '/issues?q=is%3Aissue+involves%3A%40me');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Leaves the archive dialog once its file is handed over, which is what ends the session: the plugin keeps it until
|
|
250
|
+
* then so a download that never arrived can be tried again.
|
|
251
|
+
*/
|
|
252
|
+
async function endDiagnosticsCase() {
|
|
253
|
+
try {
|
|
254
|
+
renderDiagnostics(await requestWithinDeadline('/diagnostics/cancel', undefined, 12000));
|
|
255
|
+
} catch (error) {
|
|
256
|
+
await redrawDiagnosticsAfterFailure('inactive', error);
|
|
279
257
|
}
|
|
258
|
+
diagnosticsQuestionText.focus?.();
|
|
280
259
|
}
|
|
281
260
|
|
|
282
261
|
function renderDiagnostics(state) {
|
|
262
|
+
if (state.supportCaseId !== diagnosticsState.supportCaseId) diagnosticsArchiveDownloaded = false;
|
|
283
263
|
diagnosticsState = state;
|
|
284
|
-
const screen = diagnosticsWizard.screen(state
|
|
264
|
+
const screen = diagnosticsWizard.screen(state);
|
|
285
265
|
const choosing = screen === 'choose';
|
|
286
266
|
const reviewing = screen === 'review';
|
|
287
|
-
|
|
288
|
-
const collecting = isDashboardUiReproducing(state);
|
|
289
|
-
for (const entry of [mastheadDiagnostics, menuDiagnostics]) {
|
|
290
|
-
if (collecting) entry.dataset.collecting = 'true';
|
|
291
|
-
else delete entry.dataset.collecting;
|
|
292
|
-
entry.setAttribute(
|
|
293
|
-
'aria-label',
|
|
294
|
-
messages[collecting ? 'diagnosticsCollectingFinishHere' : 'menuDiagnostics'] ?? '',
|
|
295
|
-
);
|
|
296
|
-
}
|
|
267
|
+
renderDiagnosticsEntries();
|
|
297
268
|
diagnosticsWizardPanel.hidden = !choosing;
|
|
298
269
|
diagnosticsGuidance.hidden = screen !== 'reproduce';
|
|
299
|
-
diagnosticsGuidanceBeforeSection.hidden = state.status === 'reproducing';
|
|
300
270
|
diagnosticsActions.hidden = screen !== 'reproduce';
|
|
301
|
-
diagnosticsReproduction.disabled =
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
271
|
+
diagnosticsReproduction.disabled = state.status !== 'reproducing';
|
|
272
|
+
setIssueStepReachable(diagnosticsArchiveDownloaded ? (state.issueUrl ?? '') : '');
|
|
273
|
+
const offering = (reviewing || diagnosticsArchiveDownloaded) && !diagnosticsPanel.hidden;
|
|
274
|
+
if (offering && !diagnosticsResult.open) diagnosticsResult.showModal?.();
|
|
275
|
+
if (!offering && diagnosticsResult.open) diagnosticsResult.close?.();
|
|
276
|
+
diagnosticsResultHeading.textContent =
|
|
277
|
+
messages[
|
|
278
|
+
diagnosticsArchiveDownloaded
|
|
279
|
+
? 'diagnosticsArchiveDownloaded'
|
|
280
|
+
: state.reproductionEndedAt && state.reproductionEndedAt === state.expiresAt
|
|
281
|
+
? 'diagnosticsCaptureTimedOut'
|
|
282
|
+
: 'diagnosticsEvidenceReady'
|
|
283
|
+
] ?? '';
|
|
284
|
+
const reviewed = reviewing && diagnosticsReviewedCaseId === (state.supportCaseId ?? '');
|
|
285
|
+
diagnosticsExport.hidden = !reviewed || diagnosticsArchiveDownloaded;
|
|
286
|
+
diagnosticsHandoff.hidden = !diagnosticsArchiveDownloaded;
|
|
287
|
+
diagnosticsHandoffNote.hidden = !diagnosticsArchiveDownloaded;
|
|
288
|
+
if (!offering) diagnosticsResultStatus.textContent = '';
|
|
289
|
+
else if (!diagnosticsArchiveDownloaded && state.missingEvidence?.length) {
|
|
290
|
+
diagnosticsResultStatus.textContent = messages.diagnosticsMissingEvidence ?? '';
|
|
291
|
+
}
|
|
292
|
+
diagnosticsStartAnother.hidden = !diagnosticsArchiveDownloaded;
|
|
293
|
+
diagnosticsDownloadAgain.hidden = !diagnosticsArchiveDownloaded;
|
|
294
|
+
if (!reviewed) {
|
|
295
|
+
diagnosticsExport.disabled = true;
|
|
296
|
+
diagnosticsReviewId = '';
|
|
297
|
+
if (reviewing && !diagnosticsArchiveDownloaded) void ensureArchiveReview(state.supportCaseId ?? '');
|
|
298
|
+
}
|
|
321
299
|
if (screen === 'reproduce') {
|
|
322
300
|
renderDiagnosticsGuidance(state.profile);
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
: 'diagnosticsIntermittentReadyGuidance'
|
|
330
|
-
] ?? '';
|
|
331
|
-
}
|
|
301
|
+
const stopsAt = new Date(state.expiresAt).toLocaleString(shell.lang || undefined, {
|
|
302
|
+
weekday: 'long',
|
|
303
|
+
hour: 'numeric',
|
|
304
|
+
minute: '2-digit',
|
|
305
|
+
});
|
|
306
|
+
diagnosticsCaptureNote.textContent = (messages.diagnosticsCaptureKeepsRunning ?? '').replace('{time}', stopsAt);
|
|
332
307
|
}
|
|
333
|
-
|
|
334
|
-
const statusKey = reviewing
|
|
335
|
-
? state.missingEvidence?.length
|
|
336
|
-
? 'diagnosticsMissingEvidence'
|
|
337
|
-
: 'diagnosticsComplete'
|
|
338
|
-
: {
|
|
339
|
-
inactive: 'diagnosticsInactive',
|
|
340
|
-
authorized:
|
|
341
|
-
reproductionMode === 'intermittent' ? 'diagnosticsIntermittentAuthorized' : 'diagnosticsAuthorized',
|
|
342
|
-
reproducing:
|
|
343
|
-
reproductionMode === 'intermittent' ? 'diagnosticsIntermittentReproducing' : 'diagnosticsReproducing',
|
|
344
|
-
complete: state.missingEvidence?.length ? 'diagnosticsMissingEvidence' : 'diagnosticsComplete',
|
|
345
|
-
expired: 'diagnosticsExpired',
|
|
346
|
-
}[state.status];
|
|
347
|
-
diagnosticsStatus.textContent = (
|
|
348
|
-
choosing && (state.status === 'inactive' || diagnosticsStartingAnother) ? '' : (messages[statusKey] ?? '')
|
|
349
|
-
).replace('{evidence}', state.missingEvidence?.join(', ') ?? '');
|
|
308
|
+
diagnosticsStatus.textContent = '';
|
|
350
309
|
}
|
|
351
310
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
const exclusions = document.createElement('p');
|
|
370
|
-
exclusions.textContent = `${messages.diagnosticsArchiveExcluded ?? ''} ${(manifest.excludedClasses ?? []).join(', ')}`;
|
|
371
|
-
diagnosticsManifest.append(summary, evidence, exclusions);
|
|
372
|
-
const uncovered = (manifest.evidence ?? []).filter((item) => item.coversReproduction === false);
|
|
373
|
-
if (uncovered.length > 0) {
|
|
374
|
-
const gap = document.createElement('p');
|
|
375
|
-
gap.textContent = (messages.diagnosticsArchiveCoverageGap ?? '')
|
|
376
|
-
.replace('{classes}', uncovered.map((item) => item.evidence).join(', '))
|
|
377
|
-
.replace(
|
|
378
|
-
'{retainedFrom}',
|
|
379
|
-
new Date(
|
|
380
|
-
Math.min(...uncovered.map((item) => Date.parse(item.retainedFrom))),
|
|
381
|
-
).toLocaleString(shell.lang || 'en'),
|
|
382
|
-
);
|
|
383
|
-
diagnosticsManifest.append(gap);
|
|
311
|
+
/**
|
|
312
|
+
* Marks both diagnostics actions while a capture runs, whichever area it is on, and otherwise the dashboard's own
|
|
313
|
+
* while it shows a problem.
|
|
314
|
+
*/
|
|
315
|
+
function renderDiagnosticsEntries() {
|
|
316
|
+
const collecting = diagnosticsState.status === 'reproducing';
|
|
317
|
+
for (const entry of [mastheadDiagnostics, menuDiagnostics]) {
|
|
318
|
+
const attending = !collecting && entry === menuDiagnostics && dashboardAttention !== undefined;
|
|
319
|
+
if (collecting) entry.dataset.collecting = 'true';
|
|
320
|
+
else delete entry.dataset.collecting;
|
|
321
|
+
if (attending) entry.dataset.attention = 'true';
|
|
322
|
+
else delete entry.dataset.attention;
|
|
323
|
+
entry.setAttribute(
|
|
324
|
+
'aria-label',
|
|
325
|
+
messages[collecting ? 'diagnosticsCollectingFinishHere' : attending ? 'diagnosticsAttention' : 'menuDiagnostics'] ??
|
|
326
|
+
'',
|
|
327
|
+
);
|
|
384
328
|
}
|
|
385
|
-
diagnosticsManifest.hidden = false;
|
|
386
329
|
}
|
|
387
330
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
331
|
+
/**
|
|
332
|
+
* The reviewed archive for the session on screen, fetched once.
|
|
333
|
+
*
|
|
334
|
+
* A completed reproduction that the reporter is looking at is a file they came for, so the archive is
|
|
335
|
+
* reviewed without being asked for. It is reviewed once per session rather than per render, because
|
|
336
|
+
* assembling it reads every collected log. Exporting spends it, and ends the session with it.
|
|
337
|
+
*/
|
|
338
|
+
function ensureArchiveReview(caseId) {
|
|
339
|
+
if (!caseId || diagnosticsReviewedCaseId === caseId) return Promise.resolve();
|
|
340
|
+
diagnosticsReviewRequest ??= (async () => {
|
|
397
341
|
try {
|
|
398
|
-
|
|
399
|
-
|
|
342
|
+
const review = await requestWithinDeadline('/diagnostics/archive/review', undefined, 12000);
|
|
343
|
+
diagnosticsReviewId = review.reviewId;
|
|
344
|
+
diagnosticsReviewedCaseId = caseId;
|
|
345
|
+
diagnosticsExport.hidden = false;
|
|
346
|
+
diagnosticsExport.disabled = false;
|
|
400
347
|
} catch {
|
|
401
|
-
|
|
348
|
+
diagnosticsResultStatus.textContent = messages.diagnosticsFailed ?? '';
|
|
349
|
+
} finally {
|
|
350
|
+
diagnosticsReviewRequest = undefined;
|
|
402
351
|
}
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
diagnosticsReviewConfirm.addEventListener('change', () => {
|
|
409
|
-
diagnosticsExport.disabled = !diagnosticsReviewConfirm.checked || !diagnosticsReviewId;
|
|
410
|
-
});
|
|
352
|
+
})();
|
|
353
|
+
return diagnosticsReviewRequest;
|
|
354
|
+
}
|
|
411
355
|
|
|
412
|
-
|
|
413
|
-
|
|
356
|
+
/**
|
|
357
|
+
* Hands the archive of the session on screen over as a download.
|
|
358
|
+
*
|
|
359
|
+
* Finishing a reproduction calls this without being asked, since the file is what the reporter finished for; the
|
|
360
|
+
* dialog's own button is the way to try again after a failed attempt.
|
|
361
|
+
*/
|
|
362
|
+
async function downloadDiagnosticsArchive() {
|
|
363
|
+
await ensureArchiveReview(diagnosticsState.supportCaseId ?? '');
|
|
364
|
+
if (!diagnosticsReviewId) return;
|
|
414
365
|
diagnosticsExport.disabled = true;
|
|
415
366
|
try {
|
|
416
367
|
const exported = await requestWithinDeadline(
|
|
@@ -421,141 +372,119 @@ diagnosticsExport.addEventListener('click', async () => {
|
|
|
421
372
|
const download = document.createElement('a');
|
|
422
373
|
download.href = `data:${exported.mediaType};base64,${exported.archive}`;
|
|
423
374
|
download.download = exported.filename;
|
|
375
|
+
diagnosticsHandoffNote.textContent = (messages.diagnosticsArchiveHandoff ?? '').replace(
|
|
376
|
+
'{filename}',
|
|
377
|
+
exported.filename,
|
|
378
|
+
);
|
|
379
|
+
diagnosticsResultStatus.textContent = '';
|
|
424
380
|
document.body.appendChild(download);
|
|
425
381
|
download.click();
|
|
426
382
|
document.body.removeChild(download);
|
|
427
383
|
diagnosticsReviewId = '';
|
|
428
|
-
|
|
384
|
+
diagnosticsReviewedCaseId = '';
|
|
385
|
+
diagnosticsArchiveDownloaded = true;
|
|
386
|
+
renderDiagnostics(diagnosticsState);
|
|
387
|
+
diagnosticsResultHeading.focus?.();
|
|
429
388
|
} catch {
|
|
430
|
-
|
|
389
|
+
// The plugin spends a review on any attempt, so trying again asks for a fresh one.
|
|
390
|
+
diagnosticsReviewId = '';
|
|
391
|
+
diagnosticsReviewedCaseId = '';
|
|
392
|
+
diagnosticsExport.disabled = false;
|
|
393
|
+
diagnosticsResultStatus.textContent = messages.diagnosticsFailed ?? '';
|
|
431
394
|
}
|
|
432
|
-
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
diagnosticsExport.addEventListener('click', downloadDiagnosticsArchive);
|
|
398
|
+
diagnosticsDownloadAgain.addEventListener('click', downloadDiagnosticsArchive);
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Draws the session as the plugin holds it after `error`, and says so unless it reached `expected`, inside the
|
|
402
|
+
* download dialog while that covers the panel. A request that timed out may still land, so its late answer draws
|
|
403
|
+
* the session again.
|
|
404
|
+
*/
|
|
405
|
+
async function redrawDiagnosticsAfterFailure(expected, error) {
|
|
406
|
+
const refreshed = await settleDiagnostics().catch(() => undefined);
|
|
407
|
+
if (refreshed?.status !== expected) {
|
|
408
|
+
(diagnosticsResult.open ? diagnosticsResultStatus : diagnosticsStatus).textContent = messages.diagnosticsFailed ?? '';
|
|
409
|
+
}
|
|
410
|
+
error?.late?.then(settleDiagnostics).catch(() => undefined);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Draws the session the plugin holds. One opened but never started is what a failed or late pick leaves behind;
|
|
415
|
+
* nothing on screen stands for it and it keeps detailed logging on, so it is cancelled rather than drawn.
|
|
416
|
+
*/
|
|
417
|
+
async function settleDiagnostics() {
|
|
418
|
+
let state = await requestWithinDeadline('/diagnostics/status', undefined, 12000);
|
|
419
|
+
if (state.status === 'authorized') state = await requestWithinDeadline('/diagnostics/cancel', undefined, 12000);
|
|
420
|
+
renderDiagnostics(state);
|
|
421
|
+
return state;
|
|
422
|
+
}
|
|
433
423
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
424
|
+
/**
|
|
425
|
+
* Captures the area picked. The area is the only answer a capture needs, so the pick opens the session and starts
|
|
426
|
+
* it in one go.
|
|
427
|
+
*/
|
|
428
|
+
async function startDiagnosticsCapture(profile) {
|
|
429
|
+
for (const tile of diagnosticsTiles) tile.disabled = true;
|
|
438
430
|
try {
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
);
|
|
447
|
-
diagnosticsStartingAnother = false;
|
|
448
|
-
renderDiagnostics(authorized);
|
|
449
|
-
if (diagnosticsState.status === 'authorized') diagnosticsGuidance.focus?.();
|
|
450
|
-
} catch {
|
|
451
|
-
try {
|
|
452
|
-
const refreshed = await requestWithinDeadline('/diagnostics/status', undefined, 12000);
|
|
453
|
-
if (refreshed.status !== previousStatus) diagnosticsStartingAnother = false;
|
|
454
|
-
renderDiagnostics(refreshed);
|
|
455
|
-
if (refreshed.status === previousStatus) {
|
|
456
|
-
diagnosticsStatus.textContent = messages.diagnosticsFailed ?? '';
|
|
457
|
-
focusAuthorizeAfter = true;
|
|
458
|
-
} else if (refreshed.status === 'authorized') diagnosticsGuidance.focus?.();
|
|
459
|
-
} catch {
|
|
460
|
-
diagnosticsStatus.textContent = messages.diagnosticsFailed ?? '';
|
|
431
|
+
await requestWithinDeadline('/diagnostics/authorize', { profile }, 12000);
|
|
432
|
+
const state = await requestWithinDeadline('/diagnostics/reproduction/start', undefined, 12000);
|
|
433
|
+
renderDiagnostics(state);
|
|
434
|
+
if (isDashboardUiReproducing(state)) {
|
|
435
|
+
await recordActiveUiEvent('background-started').catch(() => undefined);
|
|
436
|
+
closeDashboardPanel();
|
|
437
|
+
return;
|
|
461
438
|
}
|
|
439
|
+
diagnosticsPhaseTitle.focus?.();
|
|
440
|
+
} catch (error) {
|
|
441
|
+
await redrawDiagnosticsAfterFailure('reproducing', error);
|
|
462
442
|
} finally {
|
|
463
|
-
|
|
464
|
-
if (focusAuthorizeAfter) diagnosticsAuthorize.focus?.();
|
|
443
|
+
for (const tile of diagnosticsTiles) tile.disabled = false;
|
|
465
444
|
}
|
|
466
|
-
});
|
|
467
|
-
|
|
468
|
-
diagnosticsYes.addEventListener('click', () => {
|
|
469
|
-
diagnosticsWizardState = diagnosticsWizard.answer(diagnosticsWizardState, true);
|
|
470
|
-
renderDiagnosticsWizard();
|
|
471
|
-
diagnosticsFrequencyHeading.focus?.();
|
|
472
|
-
});
|
|
473
|
-
|
|
474
|
-
diagnosticsNo.addEventListener('click', () => {
|
|
475
|
-
diagnosticsWizardState = diagnosticsWizard.answer(diagnosticsWizardState, false);
|
|
476
|
-
renderDiagnosticsWizard();
|
|
477
|
-
if (diagnosticsWizardState.mode === 'frequency') diagnosticsFrequencyHeading.focus?.();
|
|
478
|
-
else diagnosticsQuestionText.focus?.();
|
|
479
|
-
});
|
|
480
|
-
|
|
481
|
-
diagnosticsDirect.addEventListener('click', () => {
|
|
482
|
-
diagnosticsWizardState = diagnosticsWizard.direct();
|
|
483
|
-
renderDiagnosticsWizard();
|
|
484
|
-
diagnosticsProfile.focus?.();
|
|
485
|
-
});
|
|
486
|
-
|
|
487
|
-
diagnosticsDirectChoose.addEventListener('click', () => {
|
|
488
|
-
diagnosticsWizardState = diagnosticsWizard.selectDirect(diagnosticsWizardState, diagnosticsProfile.value);
|
|
489
|
-
renderDiagnosticsWizard();
|
|
490
|
-
diagnosticsFrequencyHeading.focus?.();
|
|
491
|
-
});
|
|
492
|
-
|
|
493
|
-
diagnosticsDirectBack.addEventListener('click', () => {
|
|
494
|
-
diagnosticsWizardState = diagnosticsWizard.start();
|
|
495
|
-
renderDiagnosticsWizard();
|
|
496
|
-
diagnosticsQuestionText.focus?.();
|
|
497
|
-
});
|
|
498
|
-
|
|
499
|
-
function chooseDiagnosticsReproductionMode(reproductionMode) {
|
|
500
|
-
diagnosticsWizardState = diagnosticsWizard.chooseReproductionMode(diagnosticsWizardState, reproductionMode);
|
|
501
|
-
renderDiagnosticsWizard();
|
|
502
|
-
diagnosticsGuidanceTitle.focus?.();
|
|
503
445
|
}
|
|
504
446
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
diagnosticsFrequencyBack.addEventListener('click', () => {
|
|
509
|
-
diagnosticsWizardState = diagnosticsWizard.backFromFrequency(diagnosticsWizardState);
|
|
510
|
-
renderDiagnosticsWizard();
|
|
511
|
-
if (diagnosticsWizardState.mode === 'direct') diagnosticsProfile.focus?.();
|
|
512
|
-
else diagnosticsQuestionText.focus?.();
|
|
513
|
-
});
|
|
514
|
-
|
|
515
|
-
diagnosticsReject.addEventListener('click', () => {
|
|
516
|
-
diagnosticsWizardState = diagnosticsWizard.reject(diagnosticsWizardState);
|
|
517
|
-
renderDiagnosticsWizard();
|
|
518
|
-
if (diagnosticsWizardState.mode === 'direct') diagnosticsProfile.focus?.();
|
|
519
|
-
else diagnosticsQuestionText.focus?.();
|
|
520
|
-
});
|
|
447
|
+
for (const tile of diagnosticsTiles) {
|
|
448
|
+
tile.addEventListener('click', () => startDiagnosticsCapture(tile.dataset.diagnosticsTile));
|
|
449
|
+
}
|
|
521
450
|
|
|
522
|
-
diagnosticsStartAnother.addEventListener('click',
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
451
|
+
diagnosticsStartAnother.addEventListener('click', endDiagnosticsCase);
|
|
452
|
+
/** Escape leaves the archive dialog only once its file is downloaded, and leaving it finishes the session. */
|
|
453
|
+
diagnosticsResult.addEventListener('cancel', (event) => {
|
|
454
|
+
event.preventDefault();
|
|
455
|
+
if (diagnosticsArchiveDownloaded) endDiagnosticsCase();
|
|
527
456
|
});
|
|
528
457
|
|
|
529
458
|
diagnosticsReproduction.addEventListener('click', async () => {
|
|
530
459
|
diagnosticsReproduction.disabled = true;
|
|
531
|
-
const previousStatus = diagnosticsState.status;
|
|
532
|
-
const path =
|
|
533
|
-
diagnosticsState.status === 'reproducing' ? '/diagnostics/reproduction/end' : '/diagnostics/reproduction/start';
|
|
534
460
|
try {
|
|
535
|
-
let state = await requestWithinDeadline(
|
|
461
|
+
let state = await requestWithinDeadline('/diagnostics/reproduction/end', undefined, 12000);
|
|
536
462
|
if (state.status === 'complete' && state.missingEvidence?.length) {
|
|
537
463
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
538
464
|
state = await requestWithinDeadline('/diagnostics/status', undefined, 12000);
|
|
539
465
|
}
|
|
540
466
|
renderDiagnostics(state);
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
closeDashboardPanel();
|
|
544
|
-
return;
|
|
545
|
-
}
|
|
546
|
-
if (state.status === 'complete' || state.partialExportAvailable) diagnosticsResultHeading.focus?.();
|
|
547
|
-
} catch {
|
|
548
|
-
try {
|
|
549
|
-
const refreshed = await requestWithinDeadline('/diagnostics/status', undefined, 12000);
|
|
550
|
-
renderDiagnostics(refreshed);
|
|
551
|
-
if (refreshed.status === previousStatus) diagnosticsStatus.textContent = messages.diagnosticsFailed ?? '';
|
|
552
|
-
if (refreshed.status === 'complete' || refreshed.partialExportAvailable) diagnosticsResultHeading.focus?.();
|
|
553
|
-
else if (refreshed.status === 'authorized' || refreshed.status === 'reproducing') diagnosticsGuidance.focus?.();
|
|
554
|
-
} catch {
|
|
555
|
-
diagnosticsStatus.textContent = messages.diagnosticsFailed ?? '';
|
|
556
|
-
}
|
|
467
|
+
} catch (error) {
|
|
468
|
+
await redrawDiagnosticsAfterFailure('complete', error);
|
|
557
469
|
} finally {
|
|
558
|
-
diagnosticsReproduction.disabled =
|
|
470
|
+
diagnosticsReproduction.disabled = diagnosticsState.status !== 'reproducing';
|
|
471
|
+
}
|
|
472
|
+
if (diagnosticsState.partialExportAvailable) {
|
|
473
|
+
diagnosticsResultHeading.focus?.();
|
|
474
|
+
await downloadDiagnosticsArchive();
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
/** Cancelling a capture deletes its session without a file, and puts the areas back on the panel. */
|
|
479
|
+
diagnosticsCancel.addEventListener('click', async () => {
|
|
480
|
+
diagnosticsCancel.disabled = true;
|
|
481
|
+
try {
|
|
482
|
+
renderDiagnostics(await requestWithinDeadline('/diagnostics/cancel', undefined, 12000));
|
|
483
|
+
diagnosticsQuestionText.focus?.();
|
|
484
|
+
} catch (error) {
|
|
485
|
+
await redrawDiagnosticsAfterFailure('inactive', error);
|
|
486
|
+
} finally {
|
|
487
|
+
diagnosticsCancel.disabled = false;
|
|
559
488
|
}
|
|
560
489
|
});
|
|
561
490
|
|
|
@@ -612,21 +541,46 @@ function closeDashboardPanel() {
|
|
|
612
541
|
}
|
|
613
542
|
}
|
|
614
543
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
544
|
+
/**
|
|
545
|
+
* Opens the diagnostics panel. A preset is an area the reporter already chose to diagnose from a problem the
|
|
546
|
+
* dashboard named beside the privacy note, so a panel with nothing under way captures it at once. Opened over the
|
|
547
|
+
* sign-in screen, the panel asks like any other and puts focus on the startup area.
|
|
548
|
+
*/
|
|
549
|
+
async function openDiagnostics(preset) {
|
|
550
|
+
const signingIn = !setupContent.hidden;
|
|
621
551
|
openDashboardPanel(diagnosticsPanel, menuDiagnostics);
|
|
622
|
-
// Drawn on opening, because the wizard's first question is only ever put on the page by this call.
|
|
623
|
-
renderDiagnosticsWizard();
|
|
624
552
|
try {
|
|
625
553
|
renderDiagnostics(await requestWithinDeadline('/diagnostics/status', undefined, 12000));
|
|
626
554
|
} catch {
|
|
627
555
|
diagnosticsStatus.textContent = messages.diagnosticsFailed ?? '';
|
|
628
556
|
recordActiveUiEventBestEffort('request-failed');
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
if (diagnosticsWizard.screen(diagnosticsState) !== 'choose') return;
|
|
560
|
+
if (preset) await startDiagnosticsCapture(preset.profile);
|
|
561
|
+
else if (signingIn) {
|
|
562
|
+
diagnosticsTiles.find((tile) => tile.dataset.diagnosticsTile === 'startup-authentication')?.focus?.();
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
mastheadDiagnostics.addEventListener('click', () => openDiagnostics());
|
|
567
|
+
|
|
568
|
+
/** While the dashboard shows a problem and no session is under way, the action names it before opening the wizard. */
|
|
569
|
+
menuDiagnostics.addEventListener('click', () => {
|
|
570
|
+
if (!menuDiagnostics.dataset.attention || diagnosticsWizard.screen(diagnosticsState) !== 'choose') {
|
|
571
|
+
return openDiagnostics();
|
|
629
572
|
}
|
|
573
|
+
attentionTitle.textContent = dashboardAttention.title;
|
|
574
|
+
attentionSummary.textContent = dashboardAttention.summary;
|
|
575
|
+
attentionDialog.showModal?.();
|
|
576
|
+
});
|
|
577
|
+
attentionDiagnose.addEventListener('click', () => {
|
|
578
|
+
attentionDialog.close?.();
|
|
579
|
+
return openDiagnostics(dashboardAttention);
|
|
580
|
+
});
|
|
581
|
+
attentionChoose.addEventListener('click', () => {
|
|
582
|
+
attentionDialog.close?.();
|
|
583
|
+
return openDiagnostics();
|
|
630
584
|
});
|
|
631
585
|
menuAdvanced.addEventListener('click', async () => {
|
|
632
586
|
const config = configuredBlock() ?? {};
|
|
@@ -636,6 +590,10 @@ menuAdvanced.addEventListener('click', async () => {
|
|
|
636
590
|
warmUpMarked = new Set();
|
|
637
591
|
renderWarmUp();
|
|
638
592
|
advancedFfmpeg.value = config.ffmpegPath ?? '';
|
|
593
|
+
advancedSmallVideoPackets.checked = config.smallVideoPackets === true;
|
|
594
|
+
advancedLiveVideoPassthrough.checked = config.liveVideoPassthrough === true;
|
|
595
|
+
advancedHdLiveVideo.checked = config.hdLiveVideo === true;
|
|
596
|
+
advancedIgnoreHomeBitRate.checked = config.ignoreHomeBitRate === true;
|
|
639
597
|
openDashboardPanel(advancedPanel, menuAdvanced);
|
|
640
598
|
/**
|
|
641
599
|
* The panel asks for its own candidates rather than relying on the devices view having been opened first.
|
|
@@ -653,9 +611,8 @@ menuAdvanced.addEventListener('click', async () => {
|
|
|
653
611
|
}
|
|
654
612
|
});
|
|
655
613
|
diagnosticsClose.addEventListener('click', () => {
|
|
656
|
-
diagnosticsStartingAnother = false;
|
|
657
|
-
renderDiagnostics(diagnosticsState);
|
|
658
614
|
closeDashboardPanel();
|
|
615
|
+
renderDiagnostics(diagnosticsState);
|
|
659
616
|
});
|
|
660
617
|
advancedClose.addEventListener('click', () => {
|
|
661
618
|
closeDashboardPanel();
|
|
@@ -692,6 +649,14 @@ async function updateAdvancedSettings() {
|
|
|
692
649
|
else next.maxConcurrentMediaSessions = maxConcurrentMediaSessions;
|
|
693
650
|
if (ffmpegPath) next.ffmpegPath = ffmpegPath;
|
|
694
651
|
else delete next.ffmpegPath;
|
|
652
|
+
if (advancedSmallVideoPackets.checked) next.smallVideoPackets = true;
|
|
653
|
+
else delete next.smallVideoPackets;
|
|
654
|
+
if (advancedLiveVideoPassthrough.checked) next.liveVideoPassthrough = true;
|
|
655
|
+
else delete next.liveVideoPassthrough;
|
|
656
|
+
if (advancedHdLiveVideo.checked) next.hdLiveVideo = true;
|
|
657
|
+
else delete next.hdLiveVideo;
|
|
658
|
+
if (advancedIgnoreHomeBitRate.checked) next.ignoreHomeBitRate = true;
|
|
659
|
+
else delete next.ignoreHomeBitRate;
|
|
695
660
|
// The default is what the plugin applies when the key is absent, so storing it would only pin today's default.
|
|
696
661
|
if (warmUpSelection.length === 1 && warmUpSelection[0] === 'doorbellPress') delete next.warmUpEvents;
|
|
697
662
|
else next.warmUpEvents = [...warmUpSelection];
|
|
@@ -707,6 +672,10 @@ async function updateAdvancedSettings() {
|
|
|
707
672
|
advancedPolling.addEventListener('change', updateAdvancedSettings);
|
|
708
673
|
advancedConcurrentMedia.addEventListener('change', updateAdvancedSettings);
|
|
709
674
|
advancedFfmpeg.addEventListener('change', updateAdvancedSettings);
|
|
675
|
+
advancedSmallVideoPackets.addEventListener('change', updateAdvancedSettings);
|
|
676
|
+
advancedLiveVideoPassthrough.addEventListener('change', updateAdvancedSettings);
|
|
677
|
+
advancedHdLiveVideo.addEventListener('change', updateAdvancedSettings);
|
|
678
|
+
advancedIgnoreHomeBitRate.addEventListener('change', updateAdvancedSettings);
|
|
710
679
|
warmUpAdd.addEventListener('click', () => moveWarmUp('available'));
|
|
711
680
|
warmUpAddAll.addEventListener('click', () => moveWarmUp('available', true));
|
|
712
681
|
warmUpRemove.addEventListener('click', () => moveWarmUp('chosen'));
|
|
@@ -838,7 +807,7 @@ dashboardView.bindPreferences(dashboardElements, configuredBlock, updateConfig,
|
|
|
838
807
|
openDashboardPanel(devicePanel, trigger);
|
|
839
808
|
});
|
|
840
809
|
|
|
841
|
-
|
|
810
|
+
function openAuthentication() {
|
|
842
811
|
diagnosticsPanel.hidden = true;
|
|
843
812
|
advancedPanel.hidden = true;
|
|
844
813
|
dashboard.hidden = true;
|
|
@@ -847,6 +816,16 @@ dashboardAuthenticate.addEventListener('click', () => {
|
|
|
847
816
|
pageTitle.textContent = messages.pageTitle;
|
|
848
817
|
accountInput.focus?.();
|
|
849
818
|
recordActiveUiEventBestEffort('authentication-opened');
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
dashboardAuthenticate.addEventListener('click', openAuthentication);
|
|
822
|
+
|
|
823
|
+
// A saved session the plugin can no longer use stops every device, so nothing on the page works until the user
|
|
824
|
+
// signs in again. The dialog says so in front of everything else, and Escape does not dismiss it.
|
|
825
|
+
reauthDialog.addEventListener('cancel', (event) => event.preventDefault());
|
|
826
|
+
reauthAction.addEventListener('click', () => {
|
|
827
|
+
reauthDialog.close?.();
|
|
828
|
+
openAuthentication();
|
|
850
829
|
});
|
|
851
830
|
|
|
852
831
|
legacyAcknowledge.addEventListener('click', async () => {
|
|
@@ -856,12 +835,19 @@ legacyAcknowledge.addEventListener('click', async () => {
|
|
|
856
835
|
try {
|
|
857
836
|
await updateConfig({ ...existing, discardedV4Settings: legacyNames, discardedV4Acknowledged: true });
|
|
858
837
|
} catch {
|
|
838
|
+
legacyStatus.textContent = messages.preferenceSaveFailed ?? '';
|
|
859
839
|
return;
|
|
860
840
|
}
|
|
861
841
|
}
|
|
862
842
|
legacyNotice.hidden = true;
|
|
863
843
|
});
|
|
864
844
|
|
|
845
|
+
/**
|
|
846
|
+
* Writes the signed-in account into the Homebridge configuration, reporting whether it was saved.
|
|
847
|
+
*
|
|
848
|
+
* The Save button is enabled before the attempt and never disabled by it, so a write that fails leaves the user
|
|
849
|
+
* the one control that retries it.
|
|
850
|
+
*/
|
|
865
851
|
async function saveAuthenticatedConfig() {
|
|
866
852
|
homebridge.enableSaveButton();
|
|
867
853
|
try {
|
|
@@ -870,7 +856,9 @@ async function saveAuthenticatedConfig() {
|
|
|
870
856
|
pluginConfig = [pendingConfig];
|
|
871
857
|
savedConfigSignature = configSignature(pluginConfig);
|
|
872
858
|
homebridge.enableSaveButton();
|
|
859
|
+
return true;
|
|
873
860
|
} catch {
|
|
861
|
+
return false;
|
|
874
862
|
} finally {
|
|
875
863
|
passwordInput.value = '';
|
|
876
864
|
pendingConfig = undefined;
|
|
@@ -890,18 +878,35 @@ async function showDashboard() {
|
|
|
890
878
|
warmUpCandidates = Array.isArray(snapshot.warmUpCandidates) ? snapshot.warmUpCandidates : [];
|
|
891
879
|
// Kept beside the render that drew them, so opening a tile finds the device that tile stands for.
|
|
892
880
|
dashboardDevices = snapshot.devices ?? [];
|
|
893
|
-
dashboardView.render(snapshot, configuredBlock() ?? {}, messages, dashboardElements);
|
|
881
|
+
dashboardAttention = dashboardView.render(snapshot, configuredBlock() ?? {}, messages, dashboardElements);
|
|
882
|
+
if (snapshot.state === 'authentication-required' && snapshot.devices?.length > 0 && !reauthDialog.open) {
|
|
883
|
+
reauthDialog.showModal?.();
|
|
884
|
+
}
|
|
894
885
|
// The image pass runs beside the rendered dashboard, so its failure is dropped here rather than escaping.
|
|
895
886
|
void dashboardView
|
|
896
887
|
.applyDeviceImages(dashboardElements, (serial) => requestWithinDeadline('/device/image', { serial }, 12000))
|
|
897
888
|
.catch(() => undefined);
|
|
898
889
|
} catch {
|
|
899
|
-
dashboardView.render(
|
|
890
|
+
dashboardAttention = dashboardView.render(
|
|
891
|
+
{ state: 'unreachable', devices: [] },
|
|
892
|
+
configuredBlock() ?? {},
|
|
893
|
+
messages,
|
|
894
|
+
dashboardElements,
|
|
895
|
+
);
|
|
900
896
|
recordActiveUiEventBestEffort('request-failed');
|
|
901
897
|
}
|
|
898
|
+
renderDiagnosticsEntries();
|
|
902
899
|
recordActiveUiEventBestEffort('dashboard-opened');
|
|
903
900
|
}
|
|
904
901
|
|
|
902
|
+
/** The message each unsuccessful sign-in outcome is stated with; any other outcome is a failed sign-in. */
|
|
903
|
+
const AUTH_OUTCOMES = {
|
|
904
|
+
blocked: 'dashboardOwnerConflictSummary',
|
|
905
|
+
'plugin-running': 'authPluginRunning',
|
|
906
|
+
'commit-failed': 'authCommitFailed',
|
|
907
|
+
'timed-out': 'authTimedOut',
|
|
908
|
+
};
|
|
909
|
+
|
|
905
910
|
async function handleResult(result) {
|
|
906
911
|
if (result.status === 'captcha') {
|
|
907
912
|
challenge = 'captcha';
|
|
@@ -910,7 +915,7 @@ async function handleResult(result) {
|
|
|
910
915
|
challengeImage.hidden = false;
|
|
911
916
|
challengeLabel.textContent = messages.captchaLabel ?? '';
|
|
912
917
|
challengeForm.hidden = false;
|
|
913
|
-
authStatus.textContent = '';
|
|
918
|
+
authStatus.textContent = result.retry ? (messages.captchaRetry ?? '') : '';
|
|
914
919
|
return;
|
|
915
920
|
}
|
|
916
921
|
if (result.status === 'two-factor') {
|
|
@@ -919,7 +924,7 @@ async function handleResult(result) {
|
|
|
919
924
|
challengeImage.hidden = true;
|
|
920
925
|
challengeLabel.textContent = messages.twoFactorLabel ?? '';
|
|
921
926
|
challengeForm.hidden = false;
|
|
922
|
-
authStatus.textContent =
|
|
927
|
+
authStatus.textContent = messages.twoFactorSent ?? '';
|
|
923
928
|
return;
|
|
924
929
|
}
|
|
925
930
|
|
|
@@ -927,38 +932,27 @@ async function handleResult(result) {
|
|
|
927
932
|
if (result.status === 'restart-required') {
|
|
928
933
|
authForm.hidden = true;
|
|
929
934
|
authStatus.textContent = messages.authSuccess ?? '';
|
|
930
|
-
await saveAuthenticatedConfig();
|
|
931
935
|
// The devices this sign-in discovered are already recorded, so the flow ends on them rather than on a notice.
|
|
932
|
-
await showDashboard();
|
|
933
|
-
|
|
934
|
-
authForm.hidden = false;
|
|
935
|
-
authStatus.textContent = messages.authBlocked ?? '';
|
|
936
|
-
} else if (result.status === 'timed-out') {
|
|
937
|
-
authForm.hidden = false;
|
|
938
|
-
authStatus.textContent = messages.authTimedOut ?? '';
|
|
936
|
+
if (await saveAuthenticatedConfig()) await showDashboard();
|
|
937
|
+
else authStatus.textContent = messages.authSaveFailed ?? '';
|
|
939
938
|
} else {
|
|
940
939
|
authForm.hidden = false;
|
|
941
|
-
authStatus.textContent = messages.authFailed ?? '';
|
|
940
|
+
authStatus.textContent = messages[AUTH_OUTCOMES[result.status] ?? 'authFailed'] ?? '';
|
|
942
941
|
}
|
|
943
942
|
}
|
|
944
943
|
|
|
944
|
+
/** A sign-in request that did not come back: the page's own deadline elapsing is said to be a timeout. */
|
|
945
|
+
function reportAuthRequestFailure(error) {
|
|
946
|
+
authStatus.textContent = messages[error?.message === REQUEST_TIMED_OUT ? 'authTimedOut' : 'authFailed'] ?? '';
|
|
947
|
+
recordActiveUiEventBestEffort('request-failed');
|
|
948
|
+
}
|
|
949
|
+
|
|
945
950
|
authForm.addEventListener('submit', async (event) => {
|
|
946
951
|
event.preventDefault();
|
|
952
|
+
// A sign-in replaces the account and nothing else: every other key of the block, the child bridge included, is kept.
|
|
947
953
|
const existing = pluginConfig.find((block) => block.platform === 'HomebridgeEufy') ?? {};
|
|
948
|
-
const retained = Object.fromEntries(
|
|
949
|
-
[
|
|
950
|
-
'pollingIntervalMinutes',
|
|
951
|
-
'sessionWarmUp',
|
|
952
|
-
'ffmpegPath',
|
|
953
|
-
'entityPreferences',
|
|
954
|
-
'discardedV4Settings',
|
|
955
|
-
'discardedV4Acknowledged',
|
|
956
|
-
]
|
|
957
|
-
.filter((key) => Object.hasOwn(existing, key))
|
|
958
|
-
.map((key) => [key, existing[key]]),
|
|
959
|
-
);
|
|
960
954
|
pendingConfig = {
|
|
961
|
-
...
|
|
955
|
+
...existing,
|
|
962
956
|
platform: 'HomebridgeEufy',
|
|
963
957
|
username: accountInput.value.trim(),
|
|
964
958
|
password: passwordInput.value,
|
|
@@ -970,9 +964,8 @@ authForm.addEventListener('submit', async (event) => {
|
|
|
970
964
|
setBusy(true);
|
|
971
965
|
try {
|
|
972
966
|
await handleResult(await requestWithinDeadline('/auth/start', { configuration: pendingConfig }));
|
|
973
|
-
} catch {
|
|
974
|
-
|
|
975
|
-
recordActiveUiEventBestEffort('request-failed');
|
|
967
|
+
} catch (error) {
|
|
968
|
+
reportAuthRequestFailure(error);
|
|
976
969
|
} finally {
|
|
977
970
|
setBusy(false);
|
|
978
971
|
}
|
|
@@ -986,9 +979,8 @@ challengeForm.addEventListener('submit', async (event) => {
|
|
|
986
979
|
setBusy(true);
|
|
987
980
|
try {
|
|
988
981
|
await handleResult(await requestWithinDeadline(path, body));
|
|
989
|
-
} catch {
|
|
990
|
-
|
|
991
|
-
recordActiveUiEventBestEffort('request-failed');
|
|
982
|
+
} catch (error) {
|
|
983
|
+
reportAuthRequestFailure(error);
|
|
992
984
|
} finally {
|
|
993
985
|
setBusy(false);
|
|
994
986
|
}
|