The signed release showed only its background colour and never started the sidecar. Tauri's freezePrototype freezes Object.prototype before the bundle runs; d3-color and d3-format (under the visx charts) assign `constructor` / `toString` on plain objects while loading, which throws on a frozen prototype, so the bundle stopped before React drew anything. The saved state was not the cause. - build/freeze-safe.ts: Vite plugin that rewrites those assignments in dependencies into a helper that defines the property when the frozen prototype would refuse it - error boundaries for the window, each screen and each app tab (Copy details, Reload, Reset saved state with a backup); public/boot.js shows the same panel when the bundle fails or the window stays empty - logs/host.log (start, saved state, sidecar start/stop, panics) and logs/webview.log (log_webview), rotated, home as ~, keys, tokens and e-mail addresses replaced - state.json `version` 2 with an explicit 1 -> 2 migration (original kept as state.json.v1.bak), per-entry tolerant parsing, partial settings keep their fields, unreadable files kept aside - folders that are gone: "Folder not found" with Locate... (project_locate) / Remove; portfolio reads skip them - tools/desktop-boot-check.ts: the built window with the release CSP and the frozen prototype, a mocked host from the state fixtures (empty, D1 with folders gone, D3) and the real sidecar, through every screen; in bun run ci, the Forgejo CI workflow and the macOS release
132 lines
6.8 KiB
JavaScript
132 lines
6.8 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);
|
|
window.addEventListener('unhandledrejection', function (ev) {
|
|
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);
|
|
});
|
|
})();
|