fix: localize plugin operation errors

This commit is contained in:
mirivlad 2026-07-14 22:00:24 +08:00
parent 1f03226378
commit a1b3c31d0d
37 changed files with 330 additions and 154 deletions

View File

@ -563,6 +563,14 @@
var disposed = false; var disposed = false;
var unsubscribers = []; 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 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 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' }); var countEl = el('span', { className: 'activity-count' });
@ -634,8 +642,7 @@
? events.filter(function (item) { return !item._storageKey || item._storageKey === GLOBAL_KEY; }) ? events.filter(function (item) { return !item._storageKey || item._storageKey === GLOBAL_KEY; })
: events; : events;
return api.settings.write(scope.key, storageEvents(toStore)).then(persistSessionRegistry).then(persistCandidateCaches).catch(function (err) { 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))); reportError('ui.saveError', 'Could not save activity. Please try again.', err);
statusClass = 'error';
}); });
} }
@ -668,8 +675,7 @@
statusText = tr('ui.cleared', null, 'Activity cleared'); statusText = tr('ui.cleared', null, 'Activity cleared');
statusClass = ''; statusClass = '';
}).catch(function (err) { }).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))); reportError('ui.clearError', 'Could not clear activity. Please try again.', err);
statusClass = 'error';
}); });
} }
@ -797,8 +803,7 @@
statusText = tr('ui.dismissed', null, 'Candidate dismissed'); statusText = tr('ui.dismissed', null, 'Candidate dismissed');
statusClass = ''; statusClass = '';
}).catch(function (err) { }).catch(function (err) {
statusText = 'Could not dismiss candidate: ' + (err && err.message ? err.message : String(err)); reportError('ui.dismissError', 'Could not dismiss the suggestion. Please try again.', err);
statusClass = 'error';
}).then(render); }).then(render);
} }
@ -870,8 +875,7 @@
updateCandidates(); updateCandidates();
return persistSessionRegistry().then(persistCandidateCaches); return persistSessionRegistry().then(persistCandidateCaches);
}).catch(function (err) { }).catch(function (err) {
statusText = 'Could not load activity: ' + (err && err.message ? err.message : String(err)); reportError('ui.loadError', 'Could not load activity. Please try again.', err);
statusClass = 'error';
}); });
} }
@ -898,8 +902,7 @@
return api.commands.register(WORKLOG_COMMAND_ID, listWorkSessionCandidates).then(function (unregister) { return api.commands.register(WORKLOG_COMMAND_ID, listWorkSessionCandidates).then(function (unregister) {
if (typeof unregister === 'function') unsubscribers.push(unregister); if (typeof unregister === 'function') unsubscribers.push(unregister);
}).catch(function (err) { }).catch(function (err) {
statusText = 'Activity commands unavailable: ' + (err && err.message ? err.message : String(err)); reportError('ui.commandsUnavailable', 'Activity actions are unavailable. Please try again.', err);
statusClass = 'error';
}); });
} }
@ -917,8 +920,7 @@
statusText = scope.mode === 'global' ? 'Listening for all activity' : 'Listening for workspace activity'; statusText = scope.mode === 'global' ? 'Listening for all activity' : 'Listening for workspace activity';
statusClass = ''; statusClass = '';
}).catch(function (err) { }).catch(function (err) {
statusText = 'Activity subscriptions unavailable: ' + (err && err.message ? err.message : String(err)); reportError('ui.subscriptionsUnavailable', 'Activity updates are unavailable. Please try again.', err);
statusClass = 'error';
}); });
} }

View File

@ -14,9 +14,13 @@
"ui.clearWorkspaceWarning": "This permanently deletes recorded activity for {workspace}. Activity in other cases remains.", "ui.clearWorkspaceWarning": "This permanently deletes recorded activity for {workspace}. Activity in other cases remains.",
"ui.confirmClear": "Clear activity", "ui.confirmClear": "Clear activity",
"ui.cancel": "Cancel", "ui.cancel": "Cancel",
"ui.saveError": "Could not save activity: {error}", "ui.saveError": "Could not save activity. Please try again.",
"ui.cleared": "Activity cleared", "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.empty": "No activity events yet",
"ui.emptyHint": "File changes, browser captures, and conversions will appear here.", "ui.emptyHint": "File changes, browser captures, and conversions will appear here.",
"ui.details": "Details", "ui.details": "Details",

View File

@ -14,9 +14,13 @@
"ui.clearWorkspaceWarning": "Будет безвозвратно удалена записанная активность Дела «{workspace}». Активность других Дел останется.", "ui.clearWorkspaceWarning": "Будет безвозвратно удалена записанная активность Дела «{workspace}». Активность других Дел останется.",
"ui.confirmClear": "Очистить активность", "ui.confirmClear": "Очистить активность",
"ui.cancel": "Отмена", "ui.cancel": "Отмена",
"ui.saveError": "Не удалось сохранить активность: {error}", "ui.saveError": "Не удалось сохранить активность. Повторите попытку.",
"ui.cleared": "Активность очищена", "ui.cleared": "Активность очищена",
"ui.clearError": "Не удалось очистить активность: {error}", "ui.clearError": "Не удалось очистить активность. Повторите попытку.",
"ui.dismissError": "Не удалось отклонить предложение. Повторите попытку.",
"ui.loadError": "Не удалось загрузить активность. Повторите попытку.",
"ui.commandsUnavailable": "Действия с активностью недоступны. Повторите попытку.",
"ui.subscriptionsUnavailable": "Обновления активности недоступны. Повторите попытку.",
"ui.empty": "Событий активности пока нет", "ui.empty": "Событий активности пока нет",
"ui.emptyHint": "Здесь появятся изменения файлов, материалы из браузера и преобразования.", "ui.emptyHint": "Здесь появятся изменения файлов, материалы из браузера и преобразования.",
"ui.details": "Подробнее", "ui.details": "Подробнее",

View File

@ -376,6 +376,16 @@
if (api && api.i18n && typeof api.i18n.t === 'function') return api.i18n.t(key, params, fallback); if (api && api.i18n && typeof api.i18n.t === 'function') return api.i18n.t(key, params, fallback);
return fallback || key; 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...'); statusText = tr('ui.connecting', null, 'Connecting to receiver events...');
var toolbar = el('div', { className: 'browser-inbox-toolbar' }); var toolbar = el('div', { className: 'browser-inbox-toolbar' });
@ -496,9 +506,7 @@
function publishMutation(action, payload, verify, verifySettings) { function publishMutation(action, payload, verify, verifySettings) {
if (!api || !api.events || typeof api.events.publish !== 'function') { if (!api || !api.events || typeof api.events.publish !== 'function') {
statusText = 'Could not save inbox: events API unavailable'; reportError('ui.saveError', 'Could not update the browser inbox. Please try again.');
statusClass = 'error';
render();
return Promise.resolve(false); return Promise.resolve(false);
} }
return api.events.publish(MUTATION_EVENT, Object.assign({ action: action }, payload || {})).then(function () { return api.events.publish(MUTATION_EVENT, Object.assign({ action: action }, payload || {})).then(function () {
@ -516,9 +524,7 @@
} }
return true; return true;
}).catch(function (err) { }).catch(function (err) {
statusText = 'Could not save inbox: ' + (err && err.message ? err.message : String(err)); reportError('ui.saveError', 'Could not update the browser inbox. Please try again.', err);
statusClass = 'error';
render();
return false; return false;
}); });
} }
@ -646,9 +652,7 @@
function createNoteFromCapture(capture) { function createNoteFromCapture(capture) {
if (!capture || !capture.workspaceRootPath) return Promise.resolve(); if (!capture || !capture.workspaceRootPath) return Promise.resolve();
if (!api || !api.files || typeof api.files.writeText !== 'function') { if (!api || !api.files || typeof api.files.writeText !== 'function') {
statusText = 'Could not create note: files API unavailable'; reportError('ui.createNoteError', 'Could not create the note. Please try again.');
statusClass = 'error';
render();
return Promise.resolve(); return Promise.resolve();
} }
var title = noteTitle(capture); var title = noteTitle(capture);
@ -677,18 +681,14 @@
statusClass = ''; statusClass = '';
return archiveCapture(capture.captureId); return archiveCapture(capture.captureId);
}).catch(function (err) { }).catch(function (err) {
statusText = 'Could not create note: ' + (err && err.message ? err.message : String(err)); reportError('ui.createNoteError', 'Could not create the note. Please try again.', err);
statusClass = 'error';
render();
}); });
} }
function createLinkFromCapture(capture) { function createLinkFromCapture(capture) {
if (!capture || !capture.workspaceRootPath || !capture.url) return Promise.resolve(); if (!capture || !capture.workspaceRootPath || !capture.url) return Promise.resolve();
if (!api || !api.files || typeof api.files.writeText !== 'function') { if (!api || !api.files || typeof api.files.writeText !== 'function') {
statusText = 'Could not create link: files API unavailable'; reportError('ui.createLinkError', 'Could not create the link. Please try again.');
statusClass = 'error';
render();
return Promise.resolve(); return Promise.resolve();
} }
var title = noteTitle(capture); var title = noteTitle(capture);
@ -729,27 +729,21 @@
statusClass = ''; statusClass = '';
return archiveCapture(capture.captureId); return archiveCapture(capture.captureId);
}).catch(function (err) { }).catch(function (err) {
statusText = 'Could not create link: ' + (err && err.message ? err.message : String(err)); reportError('ui.createLinkError', 'Could not create the link. Please try again.', err);
statusClass = 'error';
render();
}); });
} }
function openCaptureURL(capture) { function openCaptureURL(capture) {
if (!capture || !capture.url || !api || !api.files || typeof api.files.openURL !== 'function') return Promise.resolve(); if (!capture || !capture.url || !api || !api.files || typeof api.files.openURL !== 'function') return Promise.resolve();
return api.files.openURL(capture.url).catch(function (err) { return api.files.openURL(capture.url).catch(function (err) {
statusText = 'Could not open link: ' + (err && err.message ? err.message : String(err)); reportError('ui.openLinkError', 'Could not open the link. Please try again.', err);
statusClass = 'error';
render();
}); });
} }
function createFileFromCapture(capture) { function createFileFromCapture(capture) {
if (!capture || !capture.workspaceRootPath || capture.kind !== 'file' || !capture.fileName || (!capture.fileText && !capture.fileDataBase64)) return Promise.resolve(); 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')) { if (!api || !api.files || (capture.fileDataBase64 ? typeof api.files.writeBytes !== 'function' : typeof api.files.writeText !== 'function')) {
statusText = 'Could not create file: files API unavailable'; reportError('ui.createFileError', 'Could not create the file. Please try again.');
statusClass = 'error';
render();
return Promise.resolve(); return Promise.resolve();
} }
var fileName = safeFileFilename(capture.fileName); var fileName = safeFileFilename(capture.fileName);
@ -785,9 +779,7 @@
statusClass = ''; statusClass = '';
return archiveCapture(capture.captureId); return archiveCapture(capture.captureId);
}).catch(function (err) { }).catch(function (err) {
statusText = 'Could not create file: ' + (err && err.message ? err.message : String(err)); reportError('ui.createFileError', 'Could not create the file. Please try again.', err);
statusClass = 'error';
render();
}); });
} }
@ -1034,8 +1026,7 @@
}); });
}); });
}).catch(function (err) { }).catch(function (err) {
statusText = 'Could not load inbox: ' + (err && err.message ? err.message : String(err)); reportError('ui.loadError', 'Could not load the browser inbox. Please try again.', err);
statusClass = 'error';
}); });
} }
@ -1093,8 +1084,7 @@
statusText = scope.mode === 'global' ? 'Receiver ready for all workspaces' : 'Receiver ready for workspace'; statusText = scope.mode === 'global' ? 'Receiver ready for all workspaces' : 'Receiver ready for workspace';
statusClass = ''; statusClass = '';
}).catch(function (err) { }).catch(function (err) {
statusText = 'Receiver unavailable: ' + (err && err.message ? err.message : String(err)); reportError('ui.receiverError', 'The browser receiver is unavailable. Please try again.', err);
statusClass = 'error';
}); });
} }
@ -1201,6 +1191,13 @@
statusEl.className = 'browser-inbox-settings-status' + (isError ? ' error' : ''); 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) { function setBusy(busy) {
copyURLButton.disabled = busy; copyURLButton.disabled = busy;
copyTokenButton.disabled = busy; copyTokenButton.disabled = busy;
@ -1226,7 +1223,7 @@
applyPairing(pairing); applyPairing(pairing);
setStatus('', false); setStatus('', false);
}).catch(function (err) { }).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 () { }).then(function () {
setBusy(false); setBusy(false);
}); });
@ -1241,7 +1238,7 @@
navigator.clipboard.writeText(value).then(function () { navigator.clipboard.writeText(value).then(function () {
setStatus(tr('ui.copied', { label: label }, '{label} copied'), false); setStatus(tr('ui.copied', { label: label }, '{label} copied'), false);
}).catch(function (err) { }).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); applyPairing(pairing);
setStatus(tr('ui.tokenRotated', null, 'Token rotated'), false); setStatus(tr('ui.tokenRotated', null, 'Token rotated'), false);
}).catch(function (err) { }).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 () { }).then(function () {
setBusy(false); setBusy(false);
}); });

View File

@ -35,5 +35,15 @@
"ui.copied": "{label} copied", "ui.copied": "{label} copied",
"ui.rotateConfirm": "Rotate pairing token?", "ui.rotateConfirm": "Rotate pairing token?",
"ui.rotating": "Rotating...", "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."
} }

View File

@ -35,5 +35,15 @@
"ui.copied": "{label} скопирован", "ui.copied": "{label} скопирован",
"ui.rotateConfirm": "Сменить токен сопряжения?", "ui.rotateConfirm": "Сменить токен сопряжения?",
"ui.rotating": "Смена токена...", "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": "Не удалось сменить токен сопряжения. Повторите попытку."
} }

View File

@ -495,10 +495,10 @@
rebuildEditorArea(); rebuildEditorArea();
}).catch(function (err) { }).catch(function (err) {
if (disposed) return; if (disposed) return;
console.warn('[default-editor] load error:', err);
editorWrap.innerHTML = ''; editorWrap.innerHTML = '';
editorWrap.appendChild(el('div', { className: 'de-error' }, [ editorWrap.appendChild(el('div', { className: 'de-error' }, [
el('div', {}, [tr('ui.loadFailed', null, 'Failed to load file')]), el('div', {}, [tr('ui.loadFailed', null, 'Could not load the file. Please try again.')])
el('div', { className: 'de-error-msg' }, [(err && err.message) ? err.message : String(err)])
])); ]));
}); });
} }

View File

@ -18,5 +18,5 @@
"ui.saved": "Saved", "ui.saved": "Saved",
"ui.discardConfirm": "Discard unsaved changes and reload from disk?", "ui.discardConfirm": "Discard unsaved changes and reload from disk?",
"ui.loading": "Loading...", "ui.loading": "Loading...",
"ui.loadFailed": "Failed to load file" "ui.loadFailed": "Could not load the file. Please try again."
} }

View File

@ -18,5 +18,5 @@
"ui.saved": "Сохранено", "ui.saved": "Сохранено",
"ui.discardConfirm": "Отменить несохранённые изменения и перечитать файл с диска?", "ui.discardConfirm": "Отменить несохранённые изменения и перечитать файл с диска?",
"ui.loading": "Загрузка...", "ui.loading": "Загрузка...",
"ui.loadFailed": "Не удалось загрузить файл" "ui.loadFailed": "Не удалось загрузить файл. Повторите попытку."
} }

View File

@ -143,8 +143,9 @@
return null; return null;
}); });
}).catch(function (err) { }).catch(function (err) {
console.warn('[file-preview] preview error:', err);
body.className = 'fp-error'; 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') { if (api.i18n && typeof api.i18n.onDidChangeLocale === 'function') {
api.i18n.onDidChangeLocale(function () { api.i18n.onDidChangeLocale(function () {

View File

@ -5,5 +5,5 @@
"ui.openExternal": "Open External", "ui.openExternal": "Open External",
"ui.preview": "Preview", "ui.preview": "Preview",
"ui.loading": "Loading...", "ui.loading": "Loading...",
"ui.error": "Preview error: {error}" "ui.error": "Could not preview this file. Please try again."
} }

View File

@ -5,5 +5,5 @@
"ui.openExternal": "Открыть во внешнем приложении", "ui.openExternal": "Открыть во внешнем приложении",
"ui.preview": "Просмотр", "ui.preview": "Просмотр",
"ui.loading": "Загрузка...", "ui.loading": "Загрузка...",
"ui.error": "Ошибка просмотра: {error}" "ui.error": "Не удалось открыть предварительный просмотр файла. Повторите попытку."
} }

View File

@ -320,14 +320,14 @@
return Promise.reject(new Error('clipboard unavailable')); return Promise.reject(new Error('clipboard unavailable'));
} }
function showExternalFallback(entry, mode, reason) { function showExternalFallback(entry, mode) {
if (!entry) return; if (!entry) return;
var pathToShow = entry.relativePath; var pathToShow = entry.relativePath;
if (mode === 'explorer' && entry.type !== 'folder') { if (mode === 'explorer' && entry.type !== 'folder') {
pathToShow = parentPath(entry.relativePath) || entry.relativePath; pathToShow = parentPath(entry.relativePath) || entry.relativePath;
} }
var title = mode === 'explorer' ? 'Show in Explorer' : 'Open External'; 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) { confirmModal(message, { confirmText: 'Copy Path', cancelText: 'Close' }).then(function (copy) {
if (!copy) return; if (!copy) return;
copyTextToClipboard(pathToShow).catch(function (err) { copyTextToClipboard(pathToShow).catch(function (err) {
@ -374,6 +374,13 @@
return fallback || key; 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) { function scopedPath(local) {
local = cleanPath(local); local = cleanPath(local);
return workspaceRoot ? (local ? workspaceRoot + '/' + local : workspaceRoot) : local; return workspaceRoot ? (local ? workspaceRoot + '/' + local : workspaceRoot) : local;
@ -714,10 +721,10 @@
renderList(); renderList();
}).catch(function (err) { }).catch(function (err) {
if (disposed) return; if (disposed) return;
console.warn('[verstak.files] load error:', err);
listContainer.innerHTML = ''; listContainer.innerHTML = '';
listContainer.appendChild(el('div', { className: 'files-error' }, [ listContainer.appendChild(el('div', { className: 'files-error' }, [
el('div', {}, [tr('ui.loadFailed', null, 'Failed to load files')]), el('div', {}, [tr('ui.loadFailed', null, 'Could not load files. Please try again.')])
el('div', { className: 'files-error-msg' }, [(err && err.message) ? err.message : String(err)])
])); ]));
}); });
} }
@ -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 () {}); api.workbench.openResource({ kind: 'vault-file', path: full, mode: 'edit', extension: ext ? '.' + ext : '', context: { sourcePluginId: 'verstak.files', sourceView: 'files' } }).catch(function () {});
} }
}).catch(function (err) { }).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'); setRenameError('A file with that name already exists');
return; 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; if (!ok) return;
api.files.trash(entry.relativePath).then(function () { api.files.trash(entry.relativePath).then(function () {
loadEntries(); 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) { } else if (count > 1) {
confirmModal('Move ' + count + ' items to trash?', { danger: true }).then(function (ok) { confirmModal('Move ' + count + ' items to trash?', { danger: true }).then(function (ok) {
@ -994,11 +1003,12 @@
var filesApi = api && api.files; var filesApi = api && api.files;
var action = mode === 'explorer' ? filesApi && filesApi.showInFolder : filesApi && filesApi.openExternal; var action = mode === 'explorer' ? filesApi && filesApi.showInFolder : filesApi && filesApi.openExternal;
if (typeof action !== 'function') { if (typeof action !== 'function') {
showExternalFallback(entry, mode, 'files external-open API is unavailable.'); showExternalFallback(entry, mode);
return; return;
} }
action(entry.relativePath).catch(function (err) { 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);
}); });
} }

View File

@ -28,5 +28,8 @@
"ui.noMatches": "No matches", "ui.noMatches": "No matches",
"ui.clearFilter": "Clear filter", "ui.clearFilter": "Clear filter",
"ui.loading": "Loading...", "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."
} }

View File

@ -28,5 +28,8 @@
"ui.noMatches": "Совпадений нет", "ui.noMatches": "Совпадений нет",
"ui.clearFilter": "Сбросить фильтр", "ui.clearFilter": "Сбросить фильтр",
"ui.loading": "Загрузка...", "ui.loading": "Загрузка...",
"ui.loadFailed": "Не удалось загрузить файлы" "ui.loadFailed": "Не удалось загрузить файлы. Повторите попытку.",
"ui.createError": "Не удалось создать этот элемент. Повторите попытку.",
"ui.renameError": "Не удалось переименовать этот элемент. Повторите попытку.",
"ui.trashError": "Не удалось переместить элемент в корзину. Повторите попытку."
} }

View File

@ -285,6 +285,14 @@
var statusClass = ''; var statusClass = '';
var modalHost = el('div', { className: 'journal-modal-host', hidden: 'hidden' }); 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 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 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' }); var countEl = el('span', { className: 'journal-count' });
@ -311,8 +319,7 @@
var target = cleanWorkspace(workspaceRoot || scope.workspaceRoot); var target = cleanWorkspace(workspaceRoot || scope.workspaceRoot);
if (!target) return Promise.resolve(); if (!target) return Promise.resolve();
return api.settings.write(WORKLOG_PREFIX + encodeKey(target), storageEntries(values || entries)).catch(function (err) { 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))); reportError('ui.saveError', 'Could not save journal. Please try again.', err);
statusClass = 'error';
}); });
} }
@ -339,8 +346,7 @@
statusText = tr('ui.aggregating', null, 'Aggregating worklogs'); statusText = tr('ui.aggregating', null, 'Aggregating worklogs');
statusClass = ''; statusClass = '';
}).catch(function (err) { }).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))); reportError('ui.loadError', 'Could not load journal. Please try again.', err);
statusClass = 'error';
}); });
} }
return api.settings.read(scope.key).then(function (stored) { return api.settings.read(scope.key).then(function (stored) {
@ -348,8 +354,7 @@
statusText = tr('ui.ready', null, 'Ready'); statusText = tr('ui.ready', null, 'Ready');
statusClass = ''; statusClass = '';
}).catch(function (err) { }).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))); reportError('ui.loadError', 'Could not load journal. Please try again.', err);
statusClass = 'error';
}); });
} }

View File

@ -8,9 +8,9 @@
"ui.title": "Journal", "ui.title": "Journal",
"ui.workspaceTitle": "Journal · {workspace}", "ui.workspaceTitle": "Journal · {workspace}",
"ui.add": "Add", "ui.add": "Add",
"ui.saveError": "Could not save journal: {error}", "ui.saveError": "Could not save journal. Please try again.",
"ui.aggregating": "Aggregating worklogs", "ui.aggregating": "Aggregating worklogs",
"ui.loadError": "Could not load journal: {error}", "ui.loadError": "Could not load journal. Please try again.",
"ui.ready": "Ready", "ui.ready": "Ready",
"ui.workItem": "Work item", "ui.workItem": "Work item",
"ui.body": "Body", "ui.body": "Body",

View File

@ -8,9 +8,9 @@
"ui.title": "Журнал", "ui.title": "Журнал",
"ui.workspaceTitle": "Журнал · {workspace}", "ui.workspaceTitle": "Журнал · {workspace}",
"ui.add": "Добавить", "ui.add": "Добавить",
"ui.saveError": "Не удалось сохранить журнал: {error}", "ui.saveError": "Не удалось сохранить журнал. Повторите попытку.",
"ui.aggregating": "Сбор записей о работе", "ui.aggregating": "Сбор записей о работе",
"ui.loadError": "Не удалось загрузить журнал: {error}", "ui.loadError": "Не удалось загрузить журнал. Повторите попытку.",
"ui.ready": "Готово", "ui.ready": "Готово",
"ui.workItem": "Выполненная работа", "ui.workItem": "Выполненная работа",
"ui.body": "Описание", "ui.body": "Описание",

View File

@ -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() { function loadNotes() {
listContainer.innerHTML = ''; listContainer.innerHTML = '';
listContainer.appendChild(el('div', { className: 'notes-empty' }, [tr('ui.loading', null, 'Loading...')])); listContainer.appendChild(el('div', { className: 'notes-empty' }, [tr('ui.loading', null, 'Loading...')]));
@ -335,7 +342,7 @@
renderList(); renderList();
}).catch(function (err) { }).catch(function (err) {
if (disposed) return; 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 () { }).then(function () {
if (!disposed) setStatus(action.label + ' complete', 'success'); if (!disposed) setStatus(action.label + ' complete', 'success');
}).catch(function (err) { }).catch(function (err) {
console.error('[notes] contribution action failed:', err); if (!disposed) setStatus(userFacingError('ui.actionError', 'Could not complete this note action. Please try again.', err), 'error');
if (!disposed) setStatus('Error: ' + (err && err.message ? err.message : err), 'error');
}); });
} }
@ -533,7 +539,7 @@
}).catch(function () {}); }).catch(function () {});
} }
}).catch(function (err) { }).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'); setStatus(tr('ui.renamed', null, 'Note renamed'), 'success');
loadNotes(); loadNotes();
}).catch(function (err) { }).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'); setStatus(tr('ui.trashed', null, 'Note moved to trash'), 'success');
loadNotes(); loadNotes();
}).catch(function (err) { }).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');
}); });
}); });
} }

View File

@ -28,5 +28,10 @@
"ui.existingFile": " Existing file: {path}.", "ui.existingFile": " Existing file: {path}.",
"ui.chooseDifferent": " Please choose a different title.", "ui.chooseDifferent": " Please choose a different title.",
"ui.trashTitle": "Move Note to Trash", "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."
} }

View File

@ -28,5 +28,10 @@
"ui.existingFile": " Существующий файл: {path}.", "ui.existingFile": " Существующий файл: {path}.",
"ui.chooseDifferent": " Выберите другое название.", "ui.chooseDifferent": " Выберите другое название.",
"ui.trashTitle": "Переместить заметку в корзину", "ui.trashTitle": "Переместить заметку в корзину",
"ui.trashConfirm": "Переместить «{title}» в корзину?" "ui.trashConfirm": "Переместить «{title}» в корзину?",
"ui.loadError": "Не удалось загрузить заметки. Повторите попытку.",
"ui.actionError": "Не удалось выполнить действие с заметкой. Повторите попытку.",
"ui.createError": "Не удалось создать заметку. Повторите попытку.",
"ui.renameError": "Не удалось переименовать заметку. Повторите попытку.",
"ui.trashError": "Не удалось переместить заметку в корзину. Повторите попытку."
} }

View File

@ -277,7 +277,8 @@
try { try {
providers = await api.contributions.list('searchProviders'); providers = await api.contributions.list('searchProviders');
} catch (err) { } 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; return output;
} }
providers = Array.isArray(providers) ? providers : []; providers = Array.isArray(providers) ? providers : [];
@ -294,7 +295,8 @@
var normalized = normalizeProviderResults(provider, response && response.result); var normalized = normalizeProviderResults(provider, response && response.result);
output.results = output.results.concat(normalized.slice(0, remaining - output.results.length)); output.results = output.results.concat(normalized.slice(0, remaining - output.results.length));
} catch (err) { } 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; return output;
@ -371,7 +373,9 @@
if (typeof alertEl.removeAttribute === 'function') alertEl.removeAttribute('hidden'); if (typeof alertEl.removeAttribute === 'function') alertEl.removeAttribute('hidden');
alertEl.appendChild(el('details', {}, [ alertEl.appendChild(el('details', {}, [
el('summary', {}, [tr('ui.providersFailed', null, 'Some plugin search providers did not respond')]), 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') { } else if (typeof alertEl.setAttribute === 'function') {
alertEl.setAttribute('hidden', 'hidden'); alertEl.setAttribute('hidden', 'hidden');
@ -460,8 +464,9 @@
state.providerErrors = external.errors; state.providerErrors = external.errors;
} catch (err) { } catch (err) {
if (seq !== searchSeq) return; if (seq !== searchSeq) return;
console.warn('[verstak.search] search:', err);
state.results = []; 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 = []; state.providerErrors = [];
} finally { } finally {
if (seq !== searchSeq) return; if (seq !== searchSeq) return;

View File

@ -10,6 +10,8 @@
"ui.searching": "Searching...", "ui.searching": "Searching...",
"ui.search": "Search", "ui.search": "Search",
"ui.providersFailed": "Some plugin search providers did not respond", "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.noResults": "No results",
"ui.open": "Open", "ui.open": "Open",
"ui.count": "{count} result(s)" "ui.count": "{count} result(s)"

View File

@ -10,6 +10,8 @@
"ui.searching": "Поиск...", "ui.searching": "Поиск...",
"ui.search": "Искать", "ui.search": "Искать",
"ui.providersFailed": "Некоторые поставщики поиска не ответили", "ui.providersFailed": "Некоторые поставщики поиска не ответили",
"ui.providerUnavailable": "Один из источников поиска недоступен.",
"ui.searchError": "Не удалось выполнить поиск в хранилище. Повторите попытку.",
"ui.noResults": "Ничего не найдено", "ui.noResults": "Ничего не найдено",
"ui.open": "Открыть", "ui.open": "Открыть",
"ui.count": "Результатов: {count}" "ui.count": "Результатов: {count}"

View File

@ -169,8 +169,8 @@
function userFacingError(action, err) { function userFacingError(action, err) {
var raw = (err && err.message) ? err.message : String(err || ''); var raw = (err && err.message) ? err.message : String(err || '');
if (typeof console !== 'undefined' && typeof console.error === 'function') { if (typeof console !== 'undefined' && typeof console.warn === 'function') {
console.error('[verstak.secrets] ' + action + ' failed', err); console.warn('[verstak.secrets] ' + action + ' failed', err);
} }
if (action === 'unlock') { if (action === 'unlock') {
var minimumLength = raw.match(/master password must be at least (\d+) characters/i); var minimumLength = raw.match(/master password must be at least (\d+) characters/i);

View File

@ -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_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;' const INPUT_FOCUS_STYLE = INPUT_STYLE + 'outline:none;border-color:#4ecca3;'
function sanitizeError(msg) { function reportError(key, fallback, error) {
if (!msg) return tr('ui.unknownError', null, 'Unknown error') console.warn('[verstak.sync] operation failed:', error)
let s = String(msg).replace(/<[^>]+>/g, '') return tr(key, null, fallback)
if (s.length > 200) s = s.substring(0, 200) + '...'
return s
} }
function syncAPI() { function syncAPI() {
@ -51,15 +49,21 @@
syncInterval = saved.syncInterval || 5 syncInterval = saved.syncInterval || 5
} }
} }
} catch (_) {} } catch (error) {
console.warn('[verstak.sync] settings load failed:', error)
}
try { try {
settings = await syncAPI().status() settings = await syncAPI().status()
if (settings) { if (settings) {
if (settings.serverUrl) serverUrl = settings.serverUrl if (settings.serverUrl) serverUrl = settings.serverUrl
if (settings.syncInterval != null) syncInterval = settings.syncInterval if (settings.syncInterval != null) syncInterval = settings.syncInterval
if (settings.syncInterval > 0) autoSync = true 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() load()
@ -80,7 +84,7 @@
resultMsg = tr('ui.settingsSaved', null, 'Settings saved.') resultMsg = tr('ui.settingsSaved', null, 'Settings saved.')
resultKind = '' resultKind = ''
} catch (e) { } catch (e) {
errorMsg = sanitizeError(e.message || e) errorMsg = reportError('ui.saveFailed', 'Could not save sync settings. Please try again.', e)
} }
loading = false loading = false
} }
@ -97,7 +101,7 @@
connectionResult = tr('ui.connectionSuccessful', null, 'Connection successful.') connectionResult = tr('ui.connectionSuccessful', null, 'Connection successful.')
} catch (e) { } catch (e) {
connectionOk = false 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 loading = false
} }
@ -115,7 +119,7 @@
password = '' password = ''
await load() await load()
} catch (e) { } catch (e) {
errorMsg = sanitizeError(e.message || e) errorMsg = reportError('ui.connectFailed', 'Could not connect this device. Please try again.', e)
} }
loading = false loading = false
} }
@ -129,23 +133,8 @@
return parts.join(' · ') return parts.join(' · ')
} }
function conflictField(conflict, keys) { function formatSyncConflict() {
for (const key of keys) { return tr('ui.syncConflictItem', null, 'A synchronization conflict needs attention.')
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(' · ')
} }
async function runSyncNow() { async function runSyncNow() {
@ -163,7 +152,7 @@
resultKind = warning ? 'warning' : '' resultKind = warning ? 'warning' : ''
await load() await load()
} catch (e) { } catch (e) {
errorMsg = sanitizeError(e.message || e) errorMsg = reportError('ui.syncFailed', 'Could not synchronize. Please try again.', e)
} }
loading = false loading = false
} }
@ -194,7 +183,7 @@
settings = null settings = null
await load() await load()
} catch (e) { } catch (e) {
errorMsg = sanitizeError(e.message || e) errorMsg = reportError('ui.disconnectFailed', 'Could not disconnect from the server. Please try again.', e)
} }
loading = false loading = false
} }
@ -209,7 +198,7 @@
resultKind = '' resultKind = ''
await load() await load()
} catch (e) { } catch (e) {
errorMsg = sanitizeError(e.message || e) errorMsg = reportError('ui.resetKeyFailed', 'Could not reset the sync key. Please try again.', e)
} }
loading = false loading = false
} }
@ -243,7 +232,7 @@
{/if} {/if}
{#if settings && settings.lastError && !errorMsg} {#if settings && settings.lastError && !errorMsg}
<div style="padding:0.5rem 0.75rem;margin-bottom:0.75rem;background:rgba(255,107,107,0.1);border:1px solid rgba(255,107,107,0.3);border-radius:6px;color:#ff6b6b;font-size:0.85rem;"> <div style="padding:0.5rem 0.75rem;margin-bottom:0.75rem;background:rgba(255,107,107,0.1);border:1px solid rgba(255,107,107,0.3);border-radius:6px;color:#ff6b6b;font-size:0.85rem;">
{tr('ui.lastSyncError', { error: sanitizeError(settings.lastError) }, 'Last sync error: {error}')} {tr('ui.lastSyncError', null, 'The last synchronization did not finish. Try again.')}
</div> </div>
{/if} {/if}

View File

@ -5,12 +5,17 @@
"ui.title": "Sync", "ui.title": "Sync",
"ui.description": "Synchronize your vault across devices.", "ui.description": "Synchronize your vault across devices.",
"ui.unknownError": "Unknown error", "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.intervalError": "Sync interval must be between 1 and 1440 minutes.",
"ui.settingsSaved": "Settings saved.", "ui.settingsSaved": "Settings saved.",
"ui.serverRequired": "Server URL is required.", "ui.serverRequired": "Server URL is required.",
"ui.connectionSuccessful": "Connection successful.", "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.connectedSuccessfully": "Connected successfully.",
"ui.conflictsCount": "{count} conflict(s)", "ui.conflictsCount": "{count} conflict(s)",
"ui.errorsCount": "{count} error(s)", "ui.errorsCount": "{count} error(s)",
@ -18,7 +23,8 @@
"ui.disconnected": "Disconnected from server.", "ui.disconnected": "Disconnected from server.",
"ui.keyReset": "Sync key reset. Connect again to pair this device.", "ui.keyReset": "Sync key reset. Connect again to pair this device.",
"ui.syncConflicts": "Sync conflicts", "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.server": "Server",
"ui.serverUrl": "Server URL", "ui.serverUrl": "Server URL",
"ui.username": "Username", "ui.username": "Username",

View File

@ -5,12 +5,17 @@
"ui.title": "Синхронизация", "ui.title": "Синхронизация",
"ui.description": "Синхронизируйте хранилище между устройствами.", "ui.description": "Синхронизируйте хранилище между устройствами.",
"ui.unknownError": "Неизвестная ошибка", "ui.unknownError": "Неизвестная ошибка",
"ui.apiUnavailable": "API синхронизации плагина недоступен", "ui.apiUnavailable": "Синхронизация сейчас недоступна.",
"ui.intervalError": "Интервал синхронизации должен быть от 1 до 1440 минут.", "ui.intervalError": "Интервал синхронизации должен быть от 1 до 1440 минут.",
"ui.settingsSaved": "Настройки сохранены.", "ui.settingsSaved": "Настройки сохранены.",
"ui.serverRequired": "Укажите URL сервера.", "ui.serverRequired": "Укажите URL сервера.",
"ui.connectionSuccessful": "Подключение успешно.", "ui.connectionSuccessful": "Подключение успешно.",
"ui.connectionFailed": "Не удалось подключиться: {error}", "ui.connectionFailed": "Не удалось подключиться. Проверьте адрес сервера и учётные данные.",
"ui.saveFailed": "Не удалось сохранить настройки синхронизации. Повторите попытку.",
"ui.connectFailed": "Не удалось подключить это устройство. Повторите попытку.",
"ui.syncFailed": "Не удалось синхронизировать данные. Повторите попытку.",
"ui.disconnectFailed": "Не удалось отключиться от сервера. Повторите попытку.",
"ui.resetKeyFailed": "Не удалось сбросить ключ синхронизации. Повторите попытку.",
"ui.connectedSuccessfully": "Подключение установлено.", "ui.connectedSuccessfully": "Подключение установлено.",
"ui.conflictsCount": "Конфликтов: {count}", "ui.conflictsCount": "Конфликтов: {count}",
"ui.errorsCount": "Ошибок: {count}", "ui.errorsCount": "Ошибок: {count}",
@ -18,7 +23,8 @@
"ui.disconnected": "Сервер отключён.", "ui.disconnected": "Сервер отключён.",
"ui.keyReset": "Ключ синхронизации сброшен. Подключитесь снова, чтобы связать устройство.", "ui.keyReset": "Ключ синхронизации сброшен. Подключитесь снова, чтобы связать устройство.",
"ui.syncConflicts": "Конфликты синхронизации", "ui.syncConflicts": "Конфликты синхронизации",
"ui.lastSyncError": "Ошибка последней синхронизации: {error}", "ui.lastSyncError": "Последняя синхронизация не завершилась. Повторите попытку.",
"ui.syncConflictItem": "Конфликт синхронизации требует внимания.",
"ui.server": "Сервер", "ui.server": "Сервер",
"ui.serverUrl": "URL сервера", "ui.serverUrl": "URL сервера",
"ui.username": "Имя пользователя", "ui.username": "Имя пользователя",

View File

@ -402,11 +402,18 @@
}).filter(function (item) { return item !== null; }); }).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() { function syncNotifications() {
if (!api || !api.notifications || typeof api.notifications.replace !== 'function') return Promise.resolve(); if (!api || !api.notifications || typeof api.notifications.replace !== 'function') return Promise.resolve();
return api.notifications.replace(notificationRequests()).catch(function (err) { 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))); reportError('ui.notificationError', 'Could not schedule reminders. Please try again.', err);
statusClass = 'error';
}); });
} }
@ -415,8 +422,7 @@
return api.settings.write(GLOBAL_KEY, storageTodos(sortTodos(todos))).then(function () { return api.settings.write(GLOBAL_KEY, storageTodos(sortTodos(todos))).then(function () {
return syncNotifications(); return syncNotifications();
}).catch(function (err) { }).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))); reportError('ui.saveError', 'Could not save tasks. Please try again.', err);
statusClass = 'error';
}); });
} }
@ -426,8 +432,7 @@
todos = sortTodos(normalizeTodos((settings || {})[GLOBAL_KEY])); todos = sortTodos(normalizeTodos((settings || {})[GLOBAL_KEY]));
return syncNotifications(); return syncNotifications();
}).catch(function (err) { }).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))); reportError('ui.loadError', 'Could not load tasks. Please try again.', err);
statusClass = 'error';
}); });
} }

View File

@ -17,9 +17,9 @@
"ui.add": "Add Todo", "ui.add": "Add Todo",
"ui.allWorkspaces": "All workspaces", "ui.allWorkspaces": "All workspaces",
"ui.unassigned": "Unassigned", "ui.unassigned": "Unassigned",
"ui.saveError": "Could not save todos: {error}", "ui.saveError": "Could not save tasks. Please try again.",
"ui.loadError": "Could not load todos: {error}", "ui.loadError": "Could not load tasks. Please try again.",
"ui.notificationError": "Could not schedule reminders: {error}", "ui.notificationError": "Could not schedule reminders. Please try again.",
"ui.notificationTitle": "Todo reminder", "ui.notificationTitle": "Todo reminder",
"ui.notificationBody": "{title}", "ui.notificationBody": "{title}",
"ui.titlePlaceholder": "Todo title", "ui.titlePlaceholder": "Todo title",

View File

@ -17,9 +17,9 @@
"ui.add": "Добавить задачу", "ui.add": "Добавить задачу",
"ui.allWorkspaces": "Все рабочие пространства", "ui.allWorkspaces": "Все рабочие пространства",
"ui.unassigned": "Не назначено", "ui.unassigned": "Не назначено",
"ui.saveError": "Не удалось сохранить задачи: {error}", "ui.saveError": "Не удалось сохранить задачи. Повторите попытку.",
"ui.loadError": "Не удалось загрузить задачи: {error}", "ui.loadError": "Не удалось загрузить задачи. Повторите попытку.",
"ui.notificationError": "Не удалось запланировать напоминания: {error}", "ui.notificationError": "Не удалось запланировать напоминания. Повторите попытку.",
"ui.notificationTitle": "Напоминание о задаче", "ui.notificationTitle": "Напоминание о задаче",
"ui.notificationBody": "{title}", "ui.notificationBody": "{title}",
"ui.titlePlaceholder": "Название задачи", "ui.titlePlaceholder": "Название задачи",

View File

@ -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');

View File

@ -101,6 +101,18 @@ else
echo " ⚠️ node not available — skipping select style validation" echo " ⚠️ node not available — skipping select style validation"
fi 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 "" echo ""
# Guard official plugins against bypassing the v2 plugin API for note features. # Guard official plugins against bypassing the v2 plugin API for note features.
echo "[frontend API boundary]" echo "[frontend API boundary]"

View File

@ -6,6 +6,7 @@ const vm = require('vm');
const root = path.resolve(__dirname, '..'); const root = path.resolve(__dirname, '..');
const sourcePath = path.join(root, 'plugins', 'browser-inbox', 'frontend', 'src', 'index.js'); const sourcePath = path.join(root, 'plugins', 'browser-inbox', 'frontend', 'src', 'index.js');
const source = fs.readFileSync(sourcePath, 'utf8'); const source = fs.readFileSync(sourcePath, 'utf8');
const technicalErrors = [];
class FakeNode { class FakeNode {
constructor(tagName) { constructor(tagName) {
@ -113,7 +114,15 @@ function makeDocument() {
function loadComponents(document) { function loadComponents(document) {
const registry = {}; const registry = {};
const sandbox = { 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, Date,
document, document,
window: { window: {
@ -738,9 +747,12 @@ async function mountSettingsWithApi(api, document = makeDocument()) {
if (!failedConversionApi.getStoredCaptures(globalKey).some((capture) => capture.captureId === 'convert-conflict')) { if (!failedConversionApi.getStoredCaptures(globalKey).some((capture) => capture.captureId === 'convert-conflict')) {
throw new Error('failed conversion removed capture from queue'); 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'); 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')) { if (failedConversionApi.publishedEvents.some((event) => event.name === 'browser.capture.converted')) {
throw new Error('failed conversion published converted event'); 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')) { if (!failedLinkApi.getStoredCaptures(globalKey).some((capture) => capture.captureId === 'convert-link-conflict')) {
throw new Error('failed link conversion removed capture from queue'); 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'); throw new Error('failed link conversion did not render an error status');
} }
if (failedLinkApi.publishedEvents.some((event) => event.name === 'browser.capture.converted')) { 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')) { if (!failedFileApi.getStoredCaptures(globalKey).some((capture) => capture.captureId === 'convert-file-conflict')) {
throw new Error('failed file conversion removed capture from queue'); 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'); throw new Error('failed file conversion did not render an error status');
} }
if (failedFileApi.publishedEvents.some((event) => event.name === 'browser.capture.converted')) { 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); 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'); console.log('browser inbox plugin smoke passed');
})().catch((err) => { })().catch((err) => {
console.error(err); console.error(err);

View File

@ -8,6 +8,7 @@ const sourcePath = path.join(root, 'plugins', 'search', 'frontend', 'src', 'inde
const manifestPath = path.join(root, 'plugins', 'search', 'plugin.json'); const manifestPath = path.join(root, 'plugins', 'search', 'plugin.json');
const source = fs.readFileSync(sourcePath, 'utf8'); const source = fs.readFileSync(sourcePath, 'utf8');
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
const technicalErrors = [];
class FakeNode { class FakeNode {
constructor(tagName) { constructor(tagName) {
@ -100,7 +101,15 @@ function makeDocument() {
function loadComponent(document) { function loadComponent(document) {
const registry = {}; const registry = {};
vm.runInNewContext(source, { 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, document,
window: { window: {
VerstakPluginRegister(pluginId, bundle) { 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/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('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('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('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 (!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 (!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 (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 = queryInput();
input.value = 'image'; input.value = 'image';

View File

@ -101,6 +101,9 @@ function loadComponent(document, errorLog) {
vm.runInNewContext(source, { vm.runInNewContext(source, {
console: { console: {
...console, ...console,
warn(...args) {
errorLog.push(args.map((value) => String(value)).join(' '));
},
error(...args) { error(...args) {
errorLog.push(args.map((value) => String(value)).join(' ')); errorLog.push(args.map((value) => String(value)).join(' '));
}, },

View File

@ -9,14 +9,17 @@ const source = fs.readFileSync(sourcePath, 'utf8');
if (!source.includes('settings.lastError')) { if (!source.includes('settings.lastError')) {
throw new Error('SyncSettings must render persisted settings.lastError'); throw new Error('SyncSettings must render persisted settings.lastError');
} }
if (!source.includes('sanitizeError(settings.lastError)')) { if (!source.includes('function reportError')) {
throw new Error('SyncSettings must sanitize persisted sync errors before rendering'); 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'); throw new Error('SyncSettings must label the persisted sync error');
} }
if (!source.includes('function formatSyncConflict')) { if (!source.includes("tr('ui.syncConflictItem'")) {
throw new Error('SyncSettings must format individual sync conflicts'); throw new Error('SyncSettings must hide technical conflict identifiers behind a user-facing summary');
} }
if (!source.includes('conflictDetails')) { if (!source.includes('conflictDetails')) {
throw new Error('SyncSettings must store sync conflict details after Sync Now'); throw new Error('SyncSettings must store sync conflict details after Sync Now');