diff --git a/plugins/activity/frontend/src/index.js b/plugins/activity/frontend/src/index.js index e7abeec..27b1e3f 100644 --- a/plugins/activity/frontend/src/index.js +++ b/plugins/activity/frontend/src/index.js @@ -563,6 +563,14 @@ var disposed = false; var unsubscribers = []; + function reportError(key, fallback, err) { + if (typeof console !== 'undefined' && typeof console.warn === 'function') { + console.warn('[verstak.activity] ' + key, err); + } + statusText = tr(key, null, fallback); + statusClass = 'error'; + } + var toolbar = el('div', { className: 'activity-toolbar' }); var titleEl = el('span', { className: 'activity-title', textContent: scope.mode === 'global' ? tr('ui.title', null, 'Activity') : tr('ui.workspaceTitle', { workspace: scope.label }, 'Activity · ' + scope.label) }); var countEl = el('span', { className: 'activity-count' }); @@ -634,8 +642,7 @@ ? events.filter(function (item) { return !item._storageKey || item._storageKey === GLOBAL_KEY; }) : events; return api.settings.write(scope.key, storageEvents(toStore)).then(persistSessionRegistry).then(persistCandidateCaches).catch(function (err) { - statusText = tr('ui.saveError', { error: err && err.message ? err.message : String(err) }, 'Could not save activity: ' + (err && err.message ? err.message : String(err))); - statusClass = 'error'; + reportError('ui.saveError', 'Could not save activity. Please try again.', err); }); } @@ -668,8 +675,7 @@ statusText = tr('ui.cleared', null, 'Activity cleared'); statusClass = ''; }).catch(function (err) { - statusText = tr('ui.clearError', { error: err && err.message ? err.message : String(err) }, 'Could not clear activity: ' + (err && err.message ? err.message : String(err))); - statusClass = 'error'; + reportError('ui.clearError', 'Could not clear activity. Please try again.', err); }); } @@ -797,8 +803,7 @@ statusText = tr('ui.dismissed', null, 'Candidate dismissed'); statusClass = ''; }).catch(function (err) { - statusText = 'Could not dismiss candidate: ' + (err && err.message ? err.message : String(err)); - statusClass = 'error'; + reportError('ui.dismissError', 'Could not dismiss the suggestion. Please try again.', err); }).then(render); } @@ -870,8 +875,7 @@ updateCandidates(); return persistSessionRegistry().then(persistCandidateCaches); }).catch(function (err) { - statusText = 'Could not load activity: ' + (err && err.message ? err.message : String(err)); - statusClass = 'error'; + reportError('ui.loadError', 'Could not load activity. Please try again.', err); }); } @@ -898,8 +902,7 @@ return api.commands.register(WORKLOG_COMMAND_ID, listWorkSessionCandidates).then(function (unregister) { if (typeof unregister === 'function') unsubscribers.push(unregister); }).catch(function (err) { - statusText = 'Activity commands unavailable: ' + (err && err.message ? err.message : String(err)); - statusClass = 'error'; + reportError('ui.commandsUnavailable', 'Activity actions are unavailable. Please try again.', err); }); } @@ -917,8 +920,7 @@ statusText = scope.mode === 'global' ? 'Listening for all activity' : 'Listening for workspace activity'; statusClass = ''; }).catch(function (err) { - statusText = 'Activity subscriptions unavailable: ' + (err && err.message ? err.message : String(err)); - statusClass = 'error'; + reportError('ui.subscriptionsUnavailable', 'Activity updates are unavailable. Please try again.', err); }); } diff --git a/plugins/activity/locales/en.json b/plugins/activity/locales/en.json index 2dd6ad1..05f4baa 100644 --- a/plugins/activity/locales/en.json +++ b/plugins/activity/locales/en.json @@ -14,9 +14,13 @@ "ui.clearWorkspaceWarning": "This permanently deletes recorded activity for {workspace}. Activity in other cases remains.", "ui.confirmClear": "Clear activity", "ui.cancel": "Cancel", - "ui.saveError": "Could not save activity: {error}", + "ui.saveError": "Could not save activity. Please try again.", "ui.cleared": "Activity cleared", - "ui.clearError": "Could not clear activity: {error}", + "ui.clearError": "Could not clear activity. Please try again.", + "ui.dismissError": "Could not dismiss the suggestion. Please try again.", + "ui.loadError": "Could not load activity. Please try again.", + "ui.commandsUnavailable": "Activity actions are unavailable. Please try again.", + "ui.subscriptionsUnavailable": "Activity updates are unavailable. Please try again.", "ui.empty": "No activity events yet", "ui.emptyHint": "File changes, browser captures, and conversions will appear here.", "ui.details": "Details", diff --git a/plugins/activity/locales/ru.json b/plugins/activity/locales/ru.json index d2d6db4..e1d4f7d 100644 --- a/plugins/activity/locales/ru.json +++ b/plugins/activity/locales/ru.json @@ -14,9 +14,13 @@ "ui.clearWorkspaceWarning": "Будет безвозвратно удалена записанная активность Дела «{workspace}». Активность других Дел останется.", "ui.confirmClear": "Очистить активность", "ui.cancel": "Отмена", - "ui.saveError": "Не удалось сохранить активность: {error}", + "ui.saveError": "Не удалось сохранить активность. Повторите попытку.", "ui.cleared": "Активность очищена", - "ui.clearError": "Не удалось очистить активность: {error}", + "ui.clearError": "Не удалось очистить активность. Повторите попытку.", + "ui.dismissError": "Не удалось отклонить предложение. Повторите попытку.", + "ui.loadError": "Не удалось загрузить активность. Повторите попытку.", + "ui.commandsUnavailable": "Действия с активностью недоступны. Повторите попытку.", + "ui.subscriptionsUnavailable": "Обновления активности недоступны. Повторите попытку.", "ui.empty": "Событий активности пока нет", "ui.emptyHint": "Здесь появятся изменения файлов, материалы из браузера и преобразования.", "ui.details": "Подробнее", diff --git a/plugins/browser-inbox/frontend/src/index.js b/plugins/browser-inbox/frontend/src/index.js index 423a773..be1699d 100644 --- a/plugins/browser-inbox/frontend/src/index.js +++ b/plugins/browser-inbox/frontend/src/index.js @@ -376,6 +376,16 @@ if (api && api.i18n && typeof api.i18n.t === 'function') return api.i18n.t(key, params, fallback); return fallback || key; } + + function reportError(key, fallback, err) { + if (typeof console !== 'undefined' && typeof console.warn === 'function') { + console.warn('[verstak.browser-inbox] ' + key, err); + } + statusText = tr(key, null, fallback); + statusClass = 'error'; + render(); + } + statusText = tr('ui.connecting', null, 'Connecting to receiver events...'); var toolbar = el('div', { className: 'browser-inbox-toolbar' }); @@ -496,9 +506,7 @@ function publishMutation(action, payload, verify, verifySettings) { if (!api || !api.events || typeof api.events.publish !== 'function') { - statusText = 'Could not save inbox: events API unavailable'; - statusClass = 'error'; - render(); + reportError('ui.saveError', 'Could not update the browser inbox. Please try again.'); return Promise.resolve(false); } return api.events.publish(MUTATION_EVENT, Object.assign({ action: action }, payload || {})).then(function () { @@ -516,9 +524,7 @@ } return true; }).catch(function (err) { - statusText = 'Could not save inbox: ' + (err && err.message ? err.message : String(err)); - statusClass = 'error'; - render(); + reportError('ui.saveError', 'Could not update the browser inbox. Please try again.', err); return false; }); } @@ -646,9 +652,7 @@ function createNoteFromCapture(capture) { if (!capture || !capture.workspaceRootPath) return Promise.resolve(); if (!api || !api.files || typeof api.files.writeText !== 'function') { - statusText = 'Could not create note: files API unavailable'; - statusClass = 'error'; - render(); + reportError('ui.createNoteError', 'Could not create the note. Please try again.'); return Promise.resolve(); } var title = noteTitle(capture); @@ -677,18 +681,14 @@ statusClass = ''; return archiveCapture(capture.captureId); }).catch(function (err) { - statusText = 'Could not create note: ' + (err && err.message ? err.message : String(err)); - statusClass = 'error'; - render(); + reportError('ui.createNoteError', 'Could not create the note. Please try again.', err); }); } function createLinkFromCapture(capture) { if (!capture || !capture.workspaceRootPath || !capture.url) return Promise.resolve(); if (!api || !api.files || typeof api.files.writeText !== 'function') { - statusText = 'Could not create link: files API unavailable'; - statusClass = 'error'; - render(); + reportError('ui.createLinkError', 'Could not create the link. Please try again.'); return Promise.resolve(); } var title = noteTitle(capture); @@ -729,27 +729,21 @@ statusClass = ''; return archiveCapture(capture.captureId); }).catch(function (err) { - statusText = 'Could not create link: ' + (err && err.message ? err.message : String(err)); - statusClass = 'error'; - render(); + reportError('ui.createLinkError', 'Could not create the link. Please try again.', err); }); } function openCaptureURL(capture) { if (!capture || !capture.url || !api || !api.files || typeof api.files.openURL !== 'function') return Promise.resolve(); return api.files.openURL(capture.url).catch(function (err) { - statusText = 'Could not open link: ' + (err && err.message ? err.message : String(err)); - statusClass = 'error'; - render(); + reportError('ui.openLinkError', 'Could not open the link. Please try again.', err); }); } function createFileFromCapture(capture) { if (!capture || !capture.workspaceRootPath || capture.kind !== 'file' || !capture.fileName || (!capture.fileText && !capture.fileDataBase64)) return Promise.resolve(); if (!api || !api.files || (capture.fileDataBase64 ? typeof api.files.writeBytes !== 'function' : typeof api.files.writeText !== 'function')) { - statusText = 'Could not create file: files API unavailable'; - statusClass = 'error'; - render(); + reportError('ui.createFileError', 'Could not create the file. Please try again.'); return Promise.resolve(); } var fileName = safeFileFilename(capture.fileName); @@ -785,9 +779,7 @@ statusClass = ''; return archiveCapture(capture.captureId); }).catch(function (err) { - statusText = 'Could not create file: ' + (err && err.message ? err.message : String(err)); - statusClass = 'error'; - render(); + reportError('ui.createFileError', 'Could not create the file. Please try again.', err); }); } @@ -1034,8 +1026,7 @@ }); }); }).catch(function (err) { - statusText = 'Could not load inbox: ' + (err && err.message ? err.message : String(err)); - statusClass = 'error'; + reportError('ui.loadError', 'Could not load the browser inbox. Please try again.', err); }); } @@ -1093,8 +1084,7 @@ statusText = scope.mode === 'global' ? 'Receiver ready for all workspaces' : 'Receiver ready for workspace'; statusClass = ''; }).catch(function (err) { - statusText = 'Receiver unavailable: ' + (err && err.message ? err.message : String(err)); - statusClass = 'error'; + reportError('ui.receiverError', 'The browser receiver is unavailable. Please try again.', err); }); } @@ -1201,6 +1191,13 @@ statusEl.className = 'browser-inbox-settings-status' + (isError ? ' error' : ''); } + function reportError(key, fallback, err) { + if (typeof console !== 'undefined' && typeof console.warn === 'function') { + console.warn('[verstak.browser-inbox.settings] ' + key, err); + } + setStatus(tr(key, null, fallback), true); + } + function setBusy(busy) { copyURLButton.disabled = busy; copyTokenButton.disabled = busy; @@ -1226,7 +1223,7 @@ applyPairing(pairing); setStatus('', false); }).catch(function (err) { - setStatus(text(err && err.message ? err.message : err), true); + reportError('ui.pairingLoadError', 'Could not load browser connection settings. Please try again.', err); }).then(function () { setBusy(false); }); @@ -1241,7 +1238,7 @@ navigator.clipboard.writeText(value).then(function () { setStatus(tr('ui.copied', { label: label }, '{label} copied'), false); }).catch(function (err) { - setStatus(text(err && err.message ? err.message : err), true); + reportError('ui.clipboardError', 'Could not copy to the clipboard. Please try again.', err); }); } @@ -1261,7 +1258,7 @@ applyPairing(pairing); setStatus(tr('ui.tokenRotated', null, 'Token rotated'), false); }).catch(function (err) { - setStatus(text(err && err.message ? err.message : err), true); + reportError('ui.tokenRotateError', 'Could not rotate the pairing token. Please try again.', err); }).then(function () { setBusy(false); }); diff --git a/plugins/browser-inbox/locales/en.json b/plugins/browser-inbox/locales/en.json index 84456c6..4e4d1d6 100644 --- a/plugins/browser-inbox/locales/en.json +++ b/plugins/browser-inbox/locales/en.json @@ -35,5 +35,15 @@ "ui.copied": "{label} copied", "ui.rotateConfirm": "Rotate pairing token?", "ui.rotating": "Rotating...", - "ui.tokenRotated": "Token rotated" + "ui.tokenRotated": "Token rotated", + "ui.saveError": "Could not update the browser inbox. Please try again.", + "ui.createNoteError": "Could not create the note. Please try again.", + "ui.createLinkError": "Could not create the link. Please try again.", + "ui.openLinkError": "Could not open the link. Please try again.", + "ui.createFileError": "Could not create the file. Please try again.", + "ui.loadError": "Could not load the browser inbox. Please try again.", + "ui.receiverError": "The browser receiver is unavailable. Please try again.", + "ui.pairingLoadError": "Could not load browser connection settings. Please try again.", + "ui.clipboardError": "Could not copy to the clipboard. Please try again.", + "ui.tokenRotateError": "Could not rotate the pairing token. Please try again." } diff --git a/plugins/browser-inbox/locales/ru.json b/plugins/browser-inbox/locales/ru.json index 3cb6de2..7f0abbd 100644 --- a/plugins/browser-inbox/locales/ru.json +++ b/plugins/browser-inbox/locales/ru.json @@ -35,5 +35,15 @@ "ui.copied": "{label} скопирован", "ui.rotateConfirm": "Сменить токен сопряжения?", "ui.rotating": "Смена токена...", - "ui.tokenRotated": "Токен изменён" + "ui.tokenRotated": "Токен изменён", + "ui.saveError": "Не удалось обновить входящие из браузера. Повторите попытку.", + "ui.createNoteError": "Не удалось создать заметку. Повторите попытку.", + "ui.createLinkError": "Не удалось создать ссылку. Повторите попытку.", + "ui.openLinkError": "Не удалось открыть ссылку. Повторите попытку.", + "ui.createFileError": "Не удалось создать файл. Повторите попытку.", + "ui.loadError": "Не удалось загрузить входящие из браузера. Повторите попытку.", + "ui.receiverError": "Приёмник браузера недоступен. Повторите попытку.", + "ui.pairingLoadError": "Не удалось загрузить параметры подключения браузера. Повторите попытку.", + "ui.clipboardError": "Не удалось скопировать в буфер обмена. Повторите попытку.", + "ui.tokenRotateError": "Не удалось сменить токен сопряжения. Повторите попытку." } diff --git a/plugins/default-editor/frontend/src/index.js b/plugins/default-editor/frontend/src/index.js index 806d528..bc3c0db 100644 --- a/plugins/default-editor/frontend/src/index.js +++ b/plugins/default-editor/frontend/src/index.js @@ -495,10 +495,10 @@ rebuildEditorArea(); }).catch(function (err) { if (disposed) return; + console.warn('[default-editor] load error:', err); editorWrap.innerHTML = ''; editorWrap.appendChild(el('div', { className: 'de-error' }, [ - el('div', {}, [tr('ui.loadFailed', null, 'Failed to load file')]), - el('div', { className: 'de-error-msg' }, [(err && err.message) ? err.message : String(err)]) + el('div', {}, [tr('ui.loadFailed', null, 'Could not load the file. Please try again.')]) ])); }); } diff --git a/plugins/default-editor/locales/en.json b/plugins/default-editor/locales/en.json index 4729b54..3e5dc19 100644 --- a/plugins/default-editor/locales/en.json +++ b/plugins/default-editor/locales/en.json @@ -18,5 +18,5 @@ "ui.saved": "Saved", "ui.discardConfirm": "Discard unsaved changes and reload from disk?", "ui.loading": "Loading...", - "ui.loadFailed": "Failed to load file" + "ui.loadFailed": "Could not load the file. Please try again." } diff --git a/plugins/default-editor/locales/ru.json b/plugins/default-editor/locales/ru.json index 984a52c..48a58ce 100644 --- a/plugins/default-editor/locales/ru.json +++ b/plugins/default-editor/locales/ru.json @@ -18,5 +18,5 @@ "ui.saved": "Сохранено", "ui.discardConfirm": "Отменить несохранённые изменения и перечитать файл с диска?", "ui.loading": "Загрузка...", - "ui.loadFailed": "Не удалось загрузить файл" + "ui.loadFailed": "Не удалось загрузить файл. Повторите попытку." } diff --git a/plugins/file-preview/frontend/src/index.js b/plugins/file-preview/frontend/src/index.js index 7ac52c5..4003b57 100644 --- a/plugins/file-preview/frontend/src/index.js +++ b/plugins/file-preview/frontend/src/index.js @@ -143,8 +143,9 @@ return null; }); }).catch(function (err) { + console.warn('[file-preview] preview error:', err); body.className = 'fp-error'; - body.textContent = tr('ui.error', { error: err && err.message ? err.message : String(err) }, 'Preview error: ' + (err && err.message ? err.message : String(err))); + body.textContent = tr('ui.error', null, 'Could not preview this file. Please try again.'); }); if (api.i18n && typeof api.i18n.onDidChangeLocale === 'function') { api.i18n.onDidChangeLocale(function () { diff --git a/plugins/file-preview/locales/en.json b/plugins/file-preview/locales/en.json index d291af8..5f2ce08 100644 --- a/plugins/file-preview/locales/en.json +++ b/plugins/file-preview/locales/en.json @@ -5,5 +5,5 @@ "ui.openExternal": "Open External", "ui.preview": "Preview", "ui.loading": "Loading...", - "ui.error": "Preview error: {error}" + "ui.error": "Could not preview this file. Please try again." } diff --git a/plugins/file-preview/locales/ru.json b/plugins/file-preview/locales/ru.json index 0f1596e..a38ad56 100644 --- a/plugins/file-preview/locales/ru.json +++ b/plugins/file-preview/locales/ru.json @@ -5,5 +5,5 @@ "ui.openExternal": "Открыть во внешнем приложении", "ui.preview": "Просмотр", "ui.loading": "Загрузка...", - "ui.error": "Ошибка просмотра: {error}" + "ui.error": "Не удалось открыть предварительный просмотр файла. Повторите попытку." } diff --git a/plugins/files/frontend/src/index.js b/plugins/files/frontend/src/index.js index 09c692d..2d6f7d9 100644 --- a/plugins/files/frontend/src/index.js +++ b/plugins/files/frontend/src/index.js @@ -320,14 +320,14 @@ return Promise.reject(new Error('clipboard unavailable')); } - function showExternalFallback(entry, mode, reason) { + function showExternalFallback(entry, mode) { if (!entry) return; var pathToShow = entry.relativePath; if (mode === 'explorer' && entry.type !== 'folder') { pathToShow = parentPath(entry.relativePath) || entry.relativePath; } var title = mode === 'explorer' ? 'Show in Explorer' : 'Open External'; - var message = title + ' failed.\n' + (reason ? String(reason) + '\n' : '') + 'Vault-relative path:\n' + pathToShow; + var message = title + ' failed.\nVault-relative path:\n' + pathToShow; confirmModal(message, { confirmText: 'Copy Path', cancelText: 'Close' }).then(function (copy) { if (!copy) return; copyTextToClipboard(pathToShow).catch(function (err) { @@ -374,6 +374,13 @@ return fallback || key; } + function reportError(key, fallback, err) { + if (typeof console !== 'undefined' && typeof console.warn === 'function') { + console.warn('[verstak.files] ' + key, err); + } + return tr(key, null, fallback); + } + function scopedPath(local) { local = cleanPath(local); return workspaceRoot ? (local ? workspaceRoot + '/' + local : workspaceRoot) : local; @@ -714,10 +721,10 @@ renderList(); }).catch(function (err) { if (disposed) return; + console.warn('[verstak.files] load error:', err); listContainer.innerHTML = ''; listContainer.appendChild(el('div', { className: 'files-error' }, [ - el('div', {}, [tr('ui.loadFailed', null, 'Failed to load files')]), - el('div', { className: 'files-error-msg' }, [(err && err.message) ? err.message : String(err)]) + el('div', {}, [tr('ui.loadFailed', null, 'Could not load files. Please try again.')]) ])); }); } @@ -863,7 +870,7 @@ api.workbench.openResource({ kind: 'vault-file', path: full, mode: 'edit', extension: ext ? '.' + ext : '', context: { sourcePluginId: 'verstak.files', sourceView: 'files' } }).catch(function () {}); } }).catch(function (err) { - setCreateError('Error: ' + ((err && err.message) ? err.message : String(err))); + setCreateError(reportError('ui.createError', 'Could not create this item. Please try again.', err)); }); } @@ -923,7 +930,7 @@ setRenameError('A file with that name already exists'); return; } - setRenameError('Error: ' + ((err && err.message) ? err.message : String(err))); + setRenameError(reportError('ui.renameError', 'Could not rename this item. Please try again.', err)); }); }); } @@ -935,7 +942,9 @@ if (!ok) return; api.files.trash(entry.relativePath).then(function () { loadEntries(); - }).catch(function (err) { window.alert((err && err.message) ? err.message : String(err)); }); + }).catch(function (err) { + window.alert(reportError('ui.trashError', 'Could not move this item to trash. Please try again.', err)); + }); }); } else if (count > 1) { confirmModal('Move ' + count + ' items to trash?', { danger: true }).then(function (ok) { @@ -994,11 +1003,12 @@ var filesApi = api && api.files; var action = mode === 'explorer' ? filesApi && filesApi.showInFolder : filesApi && filesApi.openExternal; if (typeof action !== 'function') { - showExternalFallback(entry, mode, 'files external-open API is unavailable.'); + showExternalFallback(entry, mode); return; } action(entry.relativePath).catch(function (err) { - showExternalFallback(entry, mode, err && err.message ? err.message : err); + console.warn('[verstak.files] external open error:', err); + showExternalFallback(entry, mode); }); } diff --git a/plugins/files/locales/en.json b/plugins/files/locales/en.json index dc8ff8a..c8f900d 100644 --- a/plugins/files/locales/en.json +++ b/plugins/files/locales/en.json @@ -28,5 +28,8 @@ "ui.noMatches": "No matches", "ui.clearFilter": "Clear filter", "ui.loading": "Loading...", - "ui.loadFailed": "Failed to load files" + "ui.loadFailed": "Could not load files. Please try again.", + "ui.createError": "Could not create this item. Please try again.", + "ui.renameError": "Could not rename this item. Please try again.", + "ui.trashError": "Could not move this item to trash. Please try again." } diff --git a/plugins/files/locales/ru.json b/plugins/files/locales/ru.json index 15dea45..099a4f0 100644 --- a/plugins/files/locales/ru.json +++ b/plugins/files/locales/ru.json @@ -28,5 +28,8 @@ "ui.noMatches": "Совпадений нет", "ui.clearFilter": "Сбросить фильтр", "ui.loading": "Загрузка...", - "ui.loadFailed": "Не удалось загрузить файлы" + "ui.loadFailed": "Не удалось загрузить файлы. Повторите попытку.", + "ui.createError": "Не удалось создать этот элемент. Повторите попытку.", + "ui.renameError": "Не удалось переименовать этот элемент. Повторите попытку.", + "ui.trashError": "Не удалось переместить элемент в корзину. Повторите попытку." } diff --git a/plugins/journal/frontend/src/index.js b/plugins/journal/frontend/src/index.js index 933b817..aaa6e58 100644 --- a/plugins/journal/frontend/src/index.js +++ b/plugins/journal/frontend/src/index.js @@ -285,6 +285,14 @@ var statusClass = ''; var modalHost = el('div', { className: 'journal-modal-host', hidden: 'hidden' }); + function reportError(key, fallback, err) { + if (typeof console !== 'undefined' && typeof console.warn === 'function') { + console.warn('[verstak.journal] ' + key, err); + } + statusText = tr(key, null, fallback); + statusClass = 'error'; + } + var toolbar = el('div', { className: 'journal-toolbar' }); var titleEl = el('span', { className: 'journal-title', textContent: scope.mode === 'global' ? tr('ui.title', null, 'Journal') : tr('ui.workspaceTitle', { workspace: scope.label }, 'Journal · ' + scope.label) }); var countEl = el('span', { className: 'journal-count' }); @@ -311,8 +319,7 @@ var target = cleanWorkspace(workspaceRoot || scope.workspaceRoot); if (!target) return Promise.resolve(); return api.settings.write(WORKLOG_PREFIX + encodeKey(target), storageEntries(values || entries)).catch(function (err) { - statusText = tr('ui.saveError', { error: err && err.message ? err.message : String(err) }, 'Could not save journal: ' + (err && err.message ? err.message : String(err))); - statusClass = 'error'; + reportError('ui.saveError', 'Could not save journal. Please try again.', err); }); } @@ -339,8 +346,7 @@ statusText = tr('ui.aggregating', null, 'Aggregating worklogs'); statusClass = ''; }).catch(function (err) { - statusText = tr('ui.loadError', { error: err && err.message ? err.message : String(err) }, 'Could not load journal: ' + (err && err.message ? err.message : String(err))); - statusClass = 'error'; + reportError('ui.loadError', 'Could not load journal. Please try again.', err); }); } return api.settings.read(scope.key).then(function (stored) { @@ -348,8 +354,7 @@ statusText = tr('ui.ready', null, 'Ready'); statusClass = ''; }).catch(function (err) { - statusText = tr('ui.loadError', { error: err && err.message ? err.message : String(err) }, 'Could not load journal: ' + (err && err.message ? err.message : String(err))); - statusClass = 'error'; + reportError('ui.loadError', 'Could not load journal. Please try again.', err); }); } diff --git a/plugins/journal/locales/en.json b/plugins/journal/locales/en.json index 4c74be4..80d3e29 100644 --- a/plugins/journal/locales/en.json +++ b/plugins/journal/locales/en.json @@ -8,9 +8,9 @@ "ui.title": "Journal", "ui.workspaceTitle": "Journal · {workspace}", "ui.add": "Add", - "ui.saveError": "Could not save journal: {error}", + "ui.saveError": "Could not save journal. Please try again.", "ui.aggregating": "Aggregating worklogs", - "ui.loadError": "Could not load journal: {error}", + "ui.loadError": "Could not load journal. Please try again.", "ui.ready": "Ready", "ui.workItem": "Work item", "ui.body": "Body", diff --git a/plugins/journal/locales/ru.json b/plugins/journal/locales/ru.json index 6f955bf..60fe0a0 100644 --- a/plugins/journal/locales/ru.json +++ b/plugins/journal/locales/ru.json @@ -8,9 +8,9 @@ "ui.title": "Журнал", "ui.workspaceTitle": "Журнал · {workspace}", "ui.add": "Добавить", - "ui.saveError": "Не удалось сохранить журнал: {error}", + "ui.saveError": "Не удалось сохранить журнал. Повторите попытку.", "ui.aggregating": "Сбор записей о работе", - "ui.loadError": "Не удалось загрузить журнал: {error}", + "ui.loadError": "Не удалось загрузить журнал. Повторите попытку.", "ui.ready": "Готово", "ui.workItem": "Выполненная работа", "ui.body": "Описание", diff --git a/plugins/notes/frontend/src/index.js b/plugins/notes/frontend/src/index.js index 5013921..af5de61 100644 --- a/plugins/notes/frontend/src/index.js +++ b/plugins/notes/frontend/src/index.js @@ -324,6 +324,13 @@ } } + function userFacingError(key, fallback, err) { + if (typeof console !== 'undefined' && typeof console.warn === 'function') { + console.warn('[verstak.notes] ' + key, err); + } + return tr(key, null, fallback); + } + function loadNotes() { listContainer.innerHTML = ''; listContainer.appendChild(el('div', { className: 'notes-empty' }, [tr('ui.loading', null, 'Loading...')])); @@ -335,7 +342,7 @@ renderList(); }).catch(function (err) { if (disposed) return; - renderEmpty('Error: ' + (err.message || err)); + renderEmpty(userFacingError('ui.loadError', 'Could not load notes. Please try again.', err)); }); } @@ -379,8 +386,7 @@ }).then(function () { if (!disposed) setStatus(action.label + ' complete', 'success'); }).catch(function (err) { - console.error('[notes] contribution action failed:', err); - if (!disposed) setStatus('Error: ' + (err && err.message ? err.message : err), 'error'); + if (!disposed) setStatus(userFacingError('ui.actionError', 'Could not complete this note action. Please try again.', err), 'error'); }); } @@ -533,7 +539,7 @@ }).catch(function () {}); } }).catch(function (err) { - setStatus('Error: ' + (err.message || err), 'error'); + setStatus(userFacingError('ui.createError', 'Could not create the note. Please try again.', err), 'error'); }); } @@ -568,7 +574,7 @@ setStatus(tr('ui.renamed', null, 'Note renamed'), 'success'); loadNotes(); }).catch(function (err) { - setStatus('Error: ' + (err.message || err), 'error'); + setStatus(userFacingError('ui.renameError', 'Could not rename the note. Please try again.', err), 'error'); }); } @@ -585,7 +591,7 @@ setStatus(tr('ui.trashed', null, 'Note moved to trash'), 'success'); loadNotes(); }).catch(function (err) { - setStatus('Error: ' + (err.message || err), 'error'); + setStatus(userFacingError('ui.trashError', 'Could not move the note to trash. Please try again.', err), 'error'); }); }); } diff --git a/plugins/notes/locales/en.json b/plugins/notes/locales/en.json index f941530..451c9f3 100644 --- a/plugins/notes/locales/en.json +++ b/plugins/notes/locales/en.json @@ -28,5 +28,10 @@ "ui.existingFile": " Existing file: {path}.", "ui.chooseDifferent": " Please choose a different title.", "ui.trashTitle": "Move Note to Trash", - "ui.trashConfirm": "Move \"{title}\" to trash?" + "ui.trashConfirm": "Move \"{title}\" to trash?", + "ui.loadError": "Could not load notes. Please try again.", + "ui.actionError": "Could not complete this note action. Please try again.", + "ui.createError": "Could not create the note. Please try again.", + "ui.renameError": "Could not rename the note. Please try again.", + "ui.trashError": "Could not move the note to trash. Please try again." } diff --git a/plugins/notes/locales/ru.json b/plugins/notes/locales/ru.json index 3987a83..267b864 100644 --- a/plugins/notes/locales/ru.json +++ b/plugins/notes/locales/ru.json @@ -28,5 +28,10 @@ "ui.existingFile": " Существующий файл: {path}.", "ui.chooseDifferent": " Выберите другое название.", "ui.trashTitle": "Переместить заметку в корзину", - "ui.trashConfirm": "Переместить «{title}» в корзину?" + "ui.trashConfirm": "Переместить «{title}» в корзину?", + "ui.loadError": "Не удалось загрузить заметки. Повторите попытку.", + "ui.actionError": "Не удалось выполнить действие с заметкой. Повторите попытку.", + "ui.createError": "Не удалось создать заметку. Повторите попытку.", + "ui.renameError": "Не удалось переименовать заметку. Повторите попытку.", + "ui.trashError": "Не удалось переместить заметку в корзину. Повторите попытку." } diff --git a/plugins/search/frontend/src/index.js b/plugins/search/frontend/src/index.js index 20f64a5..3413fbd 100644 --- a/plugins/search/frontend/src/index.js +++ b/plugins/search/frontend/src/index.js @@ -277,7 +277,8 @@ try { providers = await api.contributions.list('searchProviders'); } catch (err) { - output.errors.push(err && err.message ? err.message : String(err)); + console.warn('[verstak.search] list search providers:', err); + output.errors.push(true); return output; } providers = Array.isArray(providers) ? providers : []; @@ -294,7 +295,8 @@ var normalized = normalizeProviderResults(provider, response && response.result); output.results = output.results.concat(normalized.slice(0, remaining - output.results.length)); } catch (err) { - output.errors.push(err && err.message ? err.message : String(err)); + console.warn('[verstak.search] provider search:', err); + output.errors.push(true); } } return output; @@ -371,7 +373,9 @@ if (typeof alertEl.removeAttribute === 'function') alertEl.removeAttribute('hidden'); alertEl.appendChild(el('details', {}, [ el('summary', {}, [tr('ui.providersFailed', null, 'Some plugin search providers did not respond')]), - el('div', {}, [state.providerErrors.join('; ')]) + el('div', {}, [state.providerErrors.map(function () { + return tr('ui.providerUnavailable', null, 'A search provider is unavailable.'); + }).join(' ')]) ])); } else if (typeof alertEl.setAttribute === 'function') { alertEl.setAttribute('hidden', 'hidden'); @@ -460,8 +464,9 @@ state.providerErrors = external.errors; } catch (err) { if (seq !== searchSeq) return; + console.warn('[verstak.search] search:', err); state.results = []; - state.error = err && err.message ? err.message : String(err); + state.error = tr('ui.searchError', null, 'Could not search the vault. Please try again.'); state.providerErrors = []; } finally { if (seq !== searchSeq) return; diff --git a/plugins/search/locales/en.json b/plugins/search/locales/en.json index e5e50a6..d55bbf5 100644 --- a/plugins/search/locales/en.json +++ b/plugins/search/locales/en.json @@ -10,6 +10,8 @@ "ui.searching": "Searching...", "ui.search": "Search", "ui.providersFailed": "Some plugin search providers did not respond", + "ui.providerUnavailable": "A search provider is unavailable.", + "ui.searchError": "Could not search the vault. Please try again.", "ui.noResults": "No results", "ui.open": "Open", "ui.count": "{count} result(s)" diff --git a/plugins/search/locales/ru.json b/plugins/search/locales/ru.json index 5417d72..8eef0b5 100644 --- a/plugins/search/locales/ru.json +++ b/plugins/search/locales/ru.json @@ -10,6 +10,8 @@ "ui.searching": "Поиск...", "ui.search": "Искать", "ui.providersFailed": "Некоторые поставщики поиска не ответили", + "ui.providerUnavailable": "Один из источников поиска недоступен.", + "ui.searchError": "Не удалось выполнить поиск в хранилище. Повторите попытку.", "ui.noResults": "Ничего не найдено", "ui.open": "Открыть", "ui.count": "Результатов: {count}" diff --git a/plugins/secrets/frontend/src/index.js b/plugins/secrets/frontend/src/index.js index 27752d6..18c3aea 100644 --- a/plugins/secrets/frontend/src/index.js +++ b/plugins/secrets/frontend/src/index.js @@ -169,8 +169,8 @@ function userFacingError(action, err) { var raw = (err && err.message) ? err.message : String(err || ''); - if (typeof console !== 'undefined' && typeof console.error === 'function') { - console.error('[verstak.secrets] ' + action + ' failed', err); + if (typeof console !== 'undefined' && typeof console.warn === 'function') { + console.warn('[verstak.secrets] ' + action + ' failed', err); } if (action === 'unlock') { var minimumLength = raw.match(/master password must be at least (\d+) characters/i); diff --git a/plugins/sync/frontend/src/SyncSettings.svelte b/plugins/sync/frontend/src/SyncSettings.svelte index 5e47601..c81cf00 100644 --- a/plugins/sync/frontend/src/SyncSettings.svelte +++ b/plugins/sync/frontend/src/SyncSettings.svelte @@ -28,11 +28,9 @@ const INPUT_STYLE = 'width:100%;background:#0f3460;border:1px solid #1a3a5c;color:#e0e0f0;padding:8px 10px;border-radius:4px;font-size:0.85rem;box-sizing:border-box;height:36px;' const INPUT_FOCUS_STYLE = INPUT_STYLE + 'outline:none;border-color:#4ecca3;' - function sanitizeError(msg) { - if (!msg) return tr('ui.unknownError', null, 'Unknown error') - let s = String(msg).replace(/<[^>]+>/g, '') - if (s.length > 200) s = s.substring(0, 200) + '...' - return s + function reportError(key, fallback, error) { + console.warn('[verstak.sync] operation failed:', error) + return tr(key, null, fallback) } function syncAPI() { @@ -51,15 +49,21 @@ syncInterval = saved.syncInterval || 5 } } - } catch (_) {} + } catch (error) { + console.warn('[verstak.sync] settings load failed:', error) + } try { settings = await syncAPI().status() if (settings) { if (settings.serverUrl) serverUrl = settings.serverUrl if (settings.syncInterval != null) syncInterval = settings.syncInterval if (settings.syncInterval > 0) autoSync = true + if (settings.lastError) console.warn('[verstak.sync] last sync failed:', settings.lastError) } - } catch (_) { settings = null } + } catch (error) { + console.warn('[verstak.sync] status load failed:', error) + settings = null + } } load() @@ -80,7 +84,7 @@ resultMsg = tr('ui.settingsSaved', null, 'Settings saved.') resultKind = '' } catch (e) { - errorMsg = sanitizeError(e.message || e) + errorMsg = reportError('ui.saveFailed', 'Could not save sync settings. Please try again.', e) } loading = false } @@ -97,7 +101,7 @@ connectionResult = tr('ui.connectionSuccessful', null, 'Connection successful.') } catch (e) { connectionOk = false - connectionResult = tr('ui.connectionFailed', { error: sanitizeError(e.message || e) }, 'Connection failed: {error}') + connectionResult = reportError('ui.connectionFailed', 'Could not connect. Check the server address and credentials.', e) } loading = false } @@ -115,7 +119,7 @@ password = '' await load() } catch (e) { - errorMsg = sanitizeError(e.message || e) + errorMsg = reportError('ui.connectFailed', 'Could not connect this device. Please try again.', e) } loading = false } @@ -129,23 +133,8 @@ return parts.join(' · ') } - function conflictField(conflict, keys) { - for (const key of keys) { - const value = conflict && conflict[key] - if (value != null && String(value).trim()) return String(value) - } - return '' - } - - function formatSyncConflict(conflict) { - const entityType = conflictField(conflict, ['entity_type', 'entityType']) || 'item' - const entityId = conflictField(conflict, ['entity_id', 'entityId', 'path']) || 'unknown' - const opId = conflictField(conflict, ['op_id', 'opId']) - const reason = conflictField(conflict, ['reason', 'message']) - const parts = [entityType + ': ' + entityId] - if (opId) parts.push('op ' + opId) - if (reason) parts.push(reason) - return parts.join(' · ') + function formatSyncConflict() { + return tr('ui.syncConflictItem', null, 'A synchronization conflict needs attention.') } async function runSyncNow() { @@ -163,7 +152,7 @@ resultKind = warning ? 'warning' : '' await load() } catch (e) { - errorMsg = sanitizeError(e.message || e) + errorMsg = reportError('ui.syncFailed', 'Could not synchronize. Please try again.', e) } loading = false } @@ -194,7 +183,7 @@ settings = null await load() } catch (e) { - errorMsg = sanitizeError(e.message || e) + errorMsg = reportError('ui.disconnectFailed', 'Could not disconnect from the server. Please try again.', e) } loading = false } @@ -209,7 +198,7 @@ resultKind = '' await load() } catch (e) { - errorMsg = sanitizeError(e.message || e) + errorMsg = reportError('ui.resetKeyFailed', 'Could not reset the sync key. Please try again.', e) } loading = false } @@ -243,7 +232,7 @@ {/if} {#if settings && settings.lastError && !errorMsg}
- {tr('ui.lastSyncError', { error: sanitizeError(settings.lastError) }, 'Last sync error: {error}')} + {tr('ui.lastSyncError', null, 'The last synchronization did not finish. Try again.')}
{/if} diff --git a/plugins/sync/locales/en.json b/plugins/sync/locales/en.json index 97b7fea..4237cb9 100644 --- a/plugins/sync/locales/en.json +++ b/plugins/sync/locales/en.json @@ -5,12 +5,17 @@ "ui.title": "Sync", "ui.description": "Synchronize your vault across devices.", "ui.unknownError": "Unknown error", - "ui.apiUnavailable": "Plugin API sync namespace not available", + "ui.apiUnavailable": "Synchronization is unavailable right now.", "ui.intervalError": "Sync interval must be between 1 and 1440 minutes.", "ui.settingsSaved": "Settings saved.", "ui.serverRequired": "Server URL is required.", "ui.connectionSuccessful": "Connection successful.", - "ui.connectionFailed": "Connection failed: {error}", + "ui.connectionFailed": "Could not connect. Check the server address and credentials.", + "ui.saveFailed": "Could not save sync settings. Please try again.", + "ui.connectFailed": "Could not connect this device. Please try again.", + "ui.syncFailed": "Could not synchronize. Please try again.", + "ui.disconnectFailed": "Could not disconnect from the server. Please try again.", + "ui.resetKeyFailed": "Could not reset the sync key. Please try again.", "ui.connectedSuccessfully": "Connected successfully.", "ui.conflictsCount": "{count} conflict(s)", "ui.errorsCount": "{count} error(s)", @@ -18,7 +23,8 @@ "ui.disconnected": "Disconnected from server.", "ui.keyReset": "Sync key reset. Connect again to pair this device.", "ui.syncConflicts": "Sync conflicts", - "ui.lastSyncError": "Last sync error: {error}", + "ui.lastSyncError": "The last synchronization did not finish. Try again.", + "ui.syncConflictItem": "A synchronization conflict needs attention.", "ui.server": "Server", "ui.serverUrl": "Server URL", "ui.username": "Username", diff --git a/plugins/sync/locales/ru.json b/plugins/sync/locales/ru.json index 7ef79c3..721370b 100644 --- a/plugins/sync/locales/ru.json +++ b/plugins/sync/locales/ru.json @@ -5,12 +5,17 @@ "ui.title": "Синхронизация", "ui.description": "Синхронизируйте хранилище между устройствами.", "ui.unknownError": "Неизвестная ошибка", - "ui.apiUnavailable": "API синхронизации плагина недоступен", + "ui.apiUnavailable": "Синхронизация сейчас недоступна.", "ui.intervalError": "Интервал синхронизации должен быть от 1 до 1440 минут.", "ui.settingsSaved": "Настройки сохранены.", "ui.serverRequired": "Укажите URL сервера.", "ui.connectionSuccessful": "Подключение успешно.", - "ui.connectionFailed": "Не удалось подключиться: {error}", + "ui.connectionFailed": "Не удалось подключиться. Проверьте адрес сервера и учётные данные.", + "ui.saveFailed": "Не удалось сохранить настройки синхронизации. Повторите попытку.", + "ui.connectFailed": "Не удалось подключить это устройство. Повторите попытку.", + "ui.syncFailed": "Не удалось синхронизировать данные. Повторите попытку.", + "ui.disconnectFailed": "Не удалось отключиться от сервера. Повторите попытку.", + "ui.resetKeyFailed": "Не удалось сбросить ключ синхронизации. Повторите попытку.", "ui.connectedSuccessfully": "Подключение установлено.", "ui.conflictsCount": "Конфликтов: {count}", "ui.errorsCount": "Ошибок: {count}", @@ -18,7 +23,8 @@ "ui.disconnected": "Сервер отключён.", "ui.keyReset": "Ключ синхронизации сброшен. Подключитесь снова, чтобы связать устройство.", "ui.syncConflicts": "Конфликты синхронизации", - "ui.lastSyncError": "Ошибка последней синхронизации: {error}", + "ui.lastSyncError": "Последняя синхронизация не завершилась. Повторите попытку.", + "ui.syncConflictItem": "Конфликт синхронизации требует внимания.", "ui.server": "Сервер", "ui.serverUrl": "URL сервера", "ui.username": "Имя пользователя", diff --git a/plugins/todo/frontend/src/index.js b/plugins/todo/frontend/src/index.js index 2b1d3ed..4a8f2e7 100644 --- a/plugins/todo/frontend/src/index.js +++ b/plugins/todo/frontend/src/index.js @@ -402,11 +402,18 @@ }).filter(function (item) { return item !== null; }); } + function reportError(key, fallback, err) { + if (typeof console !== 'undefined' && typeof console.warn === 'function') { + console.warn('[verstak.todo] ' + key, err); + } + statusText = tr(key, null, fallback); + statusClass = 'error'; + } + function syncNotifications() { if (!api || !api.notifications || typeof api.notifications.replace !== 'function') return Promise.resolve(); return api.notifications.replace(notificationRequests()).catch(function (err) { - statusText = tr('ui.notificationError', { error: err && err.message ? err.message : String(err) }, 'Could not schedule reminders: ' + (err && err.message ? err.message : String(err))); - statusClass = 'error'; + reportError('ui.notificationError', 'Could not schedule reminders. Please try again.', err); }); } @@ -415,8 +422,7 @@ return api.settings.write(GLOBAL_KEY, storageTodos(sortTodos(todos))).then(function () { return syncNotifications(); }).catch(function (err) { - statusText = tr('ui.saveError', { error: err && err.message ? err.message : String(err) }, 'Could not save todos: ' + (err && err.message ? err.message : String(err))); - statusClass = 'error'; + reportError('ui.saveError', 'Could not save tasks. Please try again.', err); }); } @@ -426,8 +432,7 @@ todos = sortTodos(normalizeTodos((settings || {})[GLOBAL_KEY])); return syncNotifications(); }).catch(function (err) { - statusText = tr('ui.loadError', { error: err && err.message ? err.message : String(err) }, 'Could not load todos: ' + (err && err.message ? err.message : String(err))); - statusClass = 'error'; + reportError('ui.loadError', 'Could not load tasks. Please try again.', err); }); } diff --git a/plugins/todo/locales/en.json b/plugins/todo/locales/en.json index fc6d182..08b7a19 100644 --- a/plugins/todo/locales/en.json +++ b/plugins/todo/locales/en.json @@ -17,9 +17,9 @@ "ui.add": "Add Todo", "ui.allWorkspaces": "All workspaces", "ui.unassigned": "Unassigned", - "ui.saveError": "Could not save todos: {error}", - "ui.loadError": "Could not load todos: {error}", - "ui.notificationError": "Could not schedule reminders: {error}", + "ui.saveError": "Could not save tasks. Please try again.", + "ui.loadError": "Could not load tasks. Please try again.", + "ui.notificationError": "Could not schedule reminders. Please try again.", "ui.notificationTitle": "Todo reminder", "ui.notificationBody": "{title}", "ui.titlePlaceholder": "Todo title", diff --git a/plugins/todo/locales/ru.json b/plugins/todo/locales/ru.json index 9ed708e..e73f33a 100644 --- a/plugins/todo/locales/ru.json +++ b/plugins/todo/locales/ru.json @@ -17,9 +17,9 @@ "ui.add": "Добавить задачу", "ui.allWorkspaces": "Все рабочие пространства", "ui.unassigned": "Не назначено", - "ui.saveError": "Не удалось сохранить задачи: {error}", - "ui.loadError": "Не удалось загрузить задачи: {error}", - "ui.notificationError": "Не удалось запланировать напоминания: {error}", + "ui.saveError": "Не удалось сохранить задачи. Повторите попытку.", + "ui.loadError": "Не удалось загрузить задачи. Повторите попытку.", + "ui.notificationError": "Не удалось запланировать напоминания. Повторите попытку.", "ui.notificationTitle": "Напоминание о задаче", "ui.notificationBody": "{title}", "ui.titlePlaceholder": "Название задачи", diff --git a/scripts/check-user-facing-errors.js b/scripts/check-user-facing-errors.js new file mode 100644 index 0000000..624a27a --- /dev/null +++ b/scripts/check-user-facing-errors.js @@ -0,0 +1,51 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const path = require('path'); + +const root = path.resolve(__dirname, '..'); +function frontendSources(dir) { + if (!fs.existsSync(dir)) return []; + return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => { + const target = path.join(dir, entry.name); + if (entry.isDirectory()) return frontendSources(target); + return /\.(?:js|svelte)$/.test(entry.name) ? [target] : []; + }); +} + +const files = fs.readdirSync(path.join(root, 'plugins')) + .flatMap((plugin) => frontendSources(path.join(root, 'plugins', plugin, 'frontend', 'src'))); + +const renderPatterns = [ + /statusText\s*=/, + /state\.error\s*=/, + /errorMsg\s*=/, + /setStatus\(/, + /setCreateError\(/, + /setRenameError\(/, + /renderEmpty\(/, + /output\.errors\.push\(/, + /window\.alert\(/, + /showExternalFallback\(/, + /de-error-msg/, + /files-error-msg/, + /body\.textContent\s*=/ +]; + +const violations = []; +for (const file of files) { + const lines = fs.readFileSync(file, 'utf8').split('\n'); + lines.forEach((line, index) => { + if (!/(?:err(?:or)?\.message|String\(err(?:or)?\b)/.test(line)) return; + if (!renderPatterns.some((pattern) => pattern.test(line))) return; + violations.push(`${path.relative(root, file)}:${index + 1}: technical error text reaches a user-facing UI sink`); + }); +} + +if (violations.length) { + console.error('User-facing UI must not render raw backend/plugin errors:'); + violations.forEach((violation) => console.error(` ${violation}`)); + process.exit(1); +} + +console.log('user-facing error messages do not expose raw backend details'); diff --git a/scripts/check.sh b/scripts/check.sh index 8412f33..2aa94d1 100755 --- a/scripts/check.sh +++ b/scripts/check.sh @@ -101,6 +101,18 @@ else echo " ⚠️ node not available — skipping select style validation" fi +echo "" +echo "[user-facing errors]" +if command -v node &>/dev/null; then + set +e + node "$ROOT/scripts/check-user-facing-errors.js" + STATUS=$? + set -e + report "user-facing errors" "$STATUS" +else + echo " ⚠️ node not available — skipping user-facing error validation" +fi + echo "" # Guard official plugins against bypassing the v2 plugin API for note features. echo "[frontend API boundary]" diff --git a/scripts/smoke-browser-inbox-plugin.js b/scripts/smoke-browser-inbox-plugin.js index 53717b7..a0bfb01 100644 --- a/scripts/smoke-browser-inbox-plugin.js +++ b/scripts/smoke-browser-inbox-plugin.js @@ -6,6 +6,7 @@ const vm = require('vm'); const root = path.resolve(__dirname, '..'); const sourcePath = path.join(root, 'plugins', 'browser-inbox', 'frontend', 'src', 'index.js'); const source = fs.readFileSync(sourcePath, 'utf8'); +const technicalErrors = []; class FakeNode { constructor(tagName) { @@ -113,7 +114,15 @@ function makeDocument() { function loadComponents(document) { const registry = {}; const sandbox = { - console, + console: { + ...console, + warn(...args) { + technicalErrors.push(args.map((value) => String(value)).join(' ')); + }, + error(...args) { + technicalErrors.push(args.map((value) => String(value)).join(' ')); + }, + }, Date, document, window: { @@ -738,9 +747,12 @@ async function mountSettingsWithApi(api, document = makeDocument()) { if (!failedConversionApi.getStoredCaptures(globalKey).some((capture) => capture.captureId === 'convert-conflict')) { throw new Error('failed conversion removed capture from queue'); } - if (!failedConversionView.container.textContent.includes('Could not create note')) { + if (!failedConversionView.container.textContent.includes('Could not create the note. Please try again.')) { throw new Error('failed conversion did not render an error status'); } + if (failedConversionView.container.textContent.includes('file already exists')) { + throw new Error('failed conversion exposed a raw backend error'); + } if (failedConversionApi.publishedEvents.some((event) => event.name === 'browser.capture.converted')) { throw new Error('failed conversion published converted event'); } @@ -827,7 +839,7 @@ async function mountSettingsWithApi(api, document = makeDocument()) { if (!failedLinkApi.getStoredCaptures(globalKey).some((capture) => capture.captureId === 'convert-link-conflict')) { throw new Error('failed link conversion removed capture from queue'); } - if (!failedLinkView.container.textContent.includes('Could not create link')) { + if (!failedLinkView.container.textContent.includes('Could not create the link. Please try again.')) { throw new Error('failed link conversion did not render an error status'); } if (failedLinkApi.publishedEvents.some((event) => event.name === 'browser.capture.converted')) { @@ -934,7 +946,7 @@ async function mountSettingsWithApi(api, document = makeDocument()) { if (!failedFileApi.getStoredCaptures(globalKey).some((capture) => capture.captureId === 'convert-file-conflict')) { throw new Error('failed file conversion removed capture from queue'); } - if (!failedFileView.container.textContent.includes('Could not create file')) { + if (!failedFileView.container.textContent.includes('Could not create the file. Please try again.')) { throw new Error('failed file conversion did not render an error status'); } if (failedFileApi.publishedEvents.some((event) => event.name === 'browser.capture.converted')) { @@ -942,6 +954,10 @@ async function mountSettingsWithApi(api, document = makeDocument()) { } component.unmount && component.unmount(failedFileView.container); + if (!technicalErrors.some((entry) => entry.includes('file already exists'))) { + throw new Error('failed conversion did not retain its technical details in the console log'); + } + console.log('browser inbox plugin smoke passed'); })().catch((err) => { console.error(err); diff --git a/scripts/smoke-search-plugin.js b/scripts/smoke-search-plugin.js index 0ae5c4a..2e6050f 100644 --- a/scripts/smoke-search-plugin.js +++ b/scripts/smoke-search-plugin.js @@ -8,6 +8,7 @@ const sourcePath = path.join(root, 'plugins', 'search', 'frontend', 'src', 'inde const manifestPath = path.join(root, 'plugins', 'search', 'plugin.json'); const source = fs.readFileSync(sourcePath, 'utf8'); const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); +const technicalErrors = []; class FakeNode { constructor(tagName) { @@ -100,7 +101,15 @@ function makeDocument() { function loadComponent(document) { const registry = {}; vm.runInNewContext(source, { - console, + console: { + ...console, + warn(...args) { + technicalErrors.push(args.map((value) => String(value)).join(' ')); + }, + error(...args) { + technicalErrors.push(args.map((value) => String(value)).join(' ')); + }, + }, document, window: { VerstakPluginRegister(pluginId, bundle) { @@ -253,11 +262,13 @@ async function wait(ms) { if (!container.textContent.includes('Project/Target Assets')) throw new Error('typing should search folder paths'); if (!container.textContent.includes('Project/External/target.note')) throw new Error('external provider result should be rendered'); if (!container.textContent.includes('External Notes')) throw new Error('external provider label should be rendered'); - if (!container.textContent.includes('provider unavailable')) throw new Error('provider failure should be reported without failing search'); + if (!container.textContent.includes('A search provider is unavailable.')) throw new Error('provider failure should be reported without failing search'); + if (container.textContent.includes('provider unavailable')) throw new Error('provider failure leaked a raw backend error'); if (!container.textContent.includes('Content match')) throw new Error('content result type was not rendered'); if (!container.textContent.includes('Folder name')) throw new Error('folder result type was not rendered'); if (!pluginData['search-index'] || !Array.isArray(pluginData['search-index'].files)) throw new Error('search index was not written to plugin data storage'); if (providerCalls.some((call) => call.pluginId === 'verstak.search')) throw new Error('search must not call itself as an external provider'); + if (!technicalErrors.some((entry) => entry.includes('provider unavailable'))) throw new Error('provider failure was not retained in the console log'); input = queryInput(); input.value = 'image'; diff --git a/scripts/smoke-secrets-plugin.js b/scripts/smoke-secrets-plugin.js index 2676ff6..48c24fc 100755 --- a/scripts/smoke-secrets-plugin.js +++ b/scripts/smoke-secrets-plugin.js @@ -101,6 +101,9 @@ function loadComponent(document, errorLog) { vm.runInNewContext(source, { console: { ...console, + warn(...args) { + errorLog.push(args.map((value) => String(value)).join(' ')); + }, error(...args) { errorLog.push(args.map((value) => String(value)).join(' ')); }, diff --git a/scripts/smoke-sync-plugin.js b/scripts/smoke-sync-plugin.js index 6232bb6..966e669 100644 --- a/scripts/smoke-sync-plugin.js +++ b/scripts/smoke-sync-plugin.js @@ -9,14 +9,17 @@ const source = fs.readFileSync(sourcePath, 'utf8'); if (!source.includes('settings.lastError')) { throw new Error('SyncSettings must render persisted settings.lastError'); } -if (!source.includes('sanitizeError(settings.lastError)')) { - throw new Error('SyncSettings must sanitize persisted sync errors before rendering'); +if (!source.includes('function reportError')) { + throw new Error('SyncSettings must map technical failures to localized action-specific errors'); } -if (!source.includes('Last sync error')) { +if (/sanitizeError\(/.test(source) || /\{ error: sanitizeError/.test(source)) { + throw new Error('SyncSettings must not interpolate raw technical errors into user-facing messages'); +} +if (!source.includes("tr('ui.lastSyncError'")) { throw new Error('SyncSettings must label the persisted sync error'); } -if (!source.includes('function formatSyncConflict')) { - throw new Error('SyncSettings must format individual sync conflicts'); +if (!source.includes("tr('ui.syncConflictItem'")) { + throw new Error('SyncSettings must hide technical conflict identifiers behind a user-facing summary'); } if (!source.includes('conflictDetails')) { throw new Error('SyncSettings must store sync conflict details after Sync Now');