136 lines
7.2 KiB
JavaScript
136 lines
7.2 KiB
JavaScript
// storebound desktop: the first script of the window (a plain script, loaded before the app bundle; CSP allows only
|
|
// files of the app). If the bundle fails to load or React never draws (or later unmounts after an error no boundary
|
|
// caught), the window would show nothing but its background colour. This shows a plain error panel instead, with
|
|
// Copy details, Reload and Reset saved state (keeps a backup), and sends the error to logs/webview.log through the
|
|
// app's `log_webview` command until src/lib/crash.ts takes over. No framework, no strings from the network.
|
|
(function () {
|
|
'use strict';
|
|
var errors = [];
|
|
var shown = false;
|
|
|
|
function invoke(cmd, args) {
|
|
try {
|
|
var i = window.__TAURI_INTERNALS__;
|
|
if (i && typeof i.invoke === 'function') return Promise.resolve(i.invoke(cmd, args || {})).catch(function () { return null; });
|
|
} catch (e) { /* no host */ }
|
|
return Promise.resolve(null);
|
|
}
|
|
function text(e) {
|
|
if (!e) return 'unknown error';
|
|
if (e instanceof Error) return e.name + ': ' + e.message;
|
|
if (typeof e === 'string') return e;
|
|
if (typeof e.message === 'string') return e.message;
|
|
try { return JSON.stringify(e); } catch (x) { return String(e); }
|
|
}
|
|
function root() { return document.getElementById('root'); }
|
|
function blank() { var r = root(); return !r || r.childElementCount === 0; }
|
|
function booted() { return document.documentElement.getAttribute('data-booted') === '1'; }
|
|
|
|
function record(where, e, stack) {
|
|
var msg = text(e);
|
|
var entry = where + ': ' + msg + (stack ? '\n' + stack : '');
|
|
errors.push(entry);
|
|
if (errors.length > 20) errors.shift();
|
|
// src/lib/crash.ts logs once the bundle is running; before that, this does
|
|
if (!window.__storeboundCrashHandlers) invoke('log_webview', { level: 'error', message: where + ': ' + msg, detail: stack || null });
|
|
// after start: at once; while starting, give React a moment (a non-fatal start error does not blank the window)
|
|
setTimeout(check, booted() ? 60 : 1500);
|
|
}
|
|
|
|
function el(tag, cls, txt) {
|
|
var n = document.createElement(tag);
|
|
if (cls) n.className = cls;
|
|
if (txt) n.textContent = txt;
|
|
return n;
|
|
}
|
|
|
|
function show(title, lead) {
|
|
if (shown) return;
|
|
shown = true;
|
|
var detail = 'storebound desktop · ' + (booted() ? 'after start' : 'during start') + ' · ' + new Date().toISOString() + '\n' +
|
|
(navigator.userAgent || '') + '\n\n' + (errors.length ? errors.join('\n\n') : 'No error was reported: the window stayed empty.');
|
|
var host = root() || document.body;
|
|
while (host.firstChild) host.removeChild(host.firstChild);
|
|
var wrap = el('div', 'crash crash-full');
|
|
wrap.id = 'boot-fallback';
|
|
wrap.setAttribute('role', 'alert');
|
|
wrap.setAttribute('data-crash', 'boot');
|
|
var box = el('div', 'crash-box');
|
|
box.appendChild(el('h2', '', title));
|
|
box.appendChild(el('p', '', lead));
|
|
box.appendChild(el('pre', 'crash-msg selectable', errors.length ? errors[errors.length - 1].split('\n')[0] : 'The window stayed empty.'));
|
|
var det = el('details');
|
|
det.appendChild(el('summary', '', 'Details'));
|
|
det.appendChild(el('pre', 'crash-detail selectable', detail));
|
|
box.appendChild(det);
|
|
var note = el('p', 'small muted');
|
|
note.setAttribute('role', 'status');
|
|
var actions = el('div', 'actions');
|
|
var copy = el('button', 'btn', 'Copy details');
|
|
copy.onclick = function () {
|
|
var done = function (ok) { note.textContent = ok ? 'Details copied.' : 'Could not copy: select the details and copy them by hand.'; };
|
|
try {
|
|
navigator.clipboard.writeText(detail).then(function () { done(true); }, function () { done(fallbackCopy(detail)); });
|
|
} catch (e) { done(fallbackCopy(detail)); }
|
|
};
|
|
var reload = el('button', 'btn btn-primary', 'Reload');
|
|
reload.onclick = function () { location.reload(); };
|
|
var reset = el('button', 'btn btn-ghost', 'Reset saved state (keeps a backup)');
|
|
reset.title = 'Starts with an empty list. The saved state is kept as a backup file, store keys stay in the keychain, app folders are not touched.';
|
|
reset.onclick = function () {
|
|
invoke('state_reset').then(function (backup) {
|
|
if (backup === null || backup === undefined) return;
|
|
note.textContent = 'Saved state reset. Backup: ' + (backup || '-') + '. Reloading…';
|
|
setTimeout(function () { location.reload(); }, 1200);
|
|
});
|
|
};
|
|
actions.appendChild(copy);
|
|
actions.appendChild(reload);
|
|
actions.appendChild(reset);
|
|
box.appendChild(actions);
|
|
box.appendChild(note);
|
|
box.appendChild(el('p', 'small muted', 'Also written to logs/webview.log in the app data folder (home folder shown as ~, no keys).'));
|
|
wrap.appendChild(box);
|
|
host.appendChild(wrap);
|
|
}
|
|
|
|
function fallbackCopy(s) {
|
|
try {
|
|
var ta = el('textarea', 'sr');
|
|
ta.value = s;
|
|
document.body.appendChild(ta);
|
|
ta.select();
|
|
var ok = document.execCommand('copy');
|
|
ta.remove();
|
|
return ok;
|
|
} catch (e) { return false; }
|
|
}
|
|
|
|
function check() {
|
|
if (shown || !blank() || !document.body) return;
|
|
if (booted()) show('storebound hit an error', 'The window was closed by an error. Your app folders, store keys and saved data are untouched. Reload usually fixes it; if it keeps happening, copy the details into a bug report.');
|
|
else if (errors.length) show('storebound could not start', 'The window failed while starting. Your app folders, store keys and saved data are untouched. Reload, or reset the saved state if it keeps happening, and copy the details into a bug report.');
|
|
}
|
|
|
|
window.addEventListener('error', function (ev) {
|
|
// a script or stylesheet that failed to load has no ev.error
|
|
var t = ev.target;
|
|
if (t && t !== window && (t.src || t.href)) { record('load', 'could not load ' + (t.src || t.href)); return; }
|
|
record('window.onerror', ev.error || ev.message, ev.error && ev.error.stack);
|
|
}, true);
|
|
// tauri-plugin-notification's init script asks for the notification permission at load (macOS, Linux); the webview
|
|
// has no plugin API by design (capabilities/main.json), so that call is refused: expected, not an error
|
|
function pluginProbe(e) { return /plugin:notification\|is_permission_granted not allowed/.test(text(e)); }
|
|
window.addEventListener('unhandledrejection', function (ev) {
|
|
if (pluginProbe(ev.reason)) { ev.preventDefault(); return; }
|
|
if (!booted()) record('unhandledrejection', ev.reason, ev.reason && ev.reason.stack);
|
|
});
|
|
// React's root reports an error no boundary caught (the tree is gone) here: src/main.tsx onUncaughtError
|
|
window.__storeboundBlank = function (message, stack) { record('react', message, stack); };
|
|
// nothing drawn and nothing reported after a while: still say so
|
|
window.addEventListener('load', function () {
|
|
setTimeout(function () {
|
|
if (!booted() && blank()) { errors.push('start: the window was still empty 15 s after loading'); invoke('log_webview', { level: 'error', message: 'start: the window was still empty 15 s after loading', detail: null }); show('storebound could not start', 'The window stayed empty while starting. Your app folders, store keys and saved data are untouched. Reload, or reset the saved state if it keeps happening, and copy the details into a bug report.'); }
|
|
}, 15000);
|
|
});
|
|
})();
|