storebound/apps/desktop/src/main.tsx
CRT bd3ab20e1d Desktop: never a blank window (frozen-prototype fix, error screens, logs, state migration, boot check)
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
2026-10-11 09:36:34 +08:00

49 lines
2.5 KiB
TypeScript

import { StrictMode, useCallback, useEffect, useState } from 'react';
import { createRoot } from 'react-dom/client';
import { App } from './App';
import { devSwitch } from './dev/switches';
import { setLang } from './i18n';
import { applyPrefs, loadPrefs, type Prefs, resolveLang, savePrefs } from './prefs';
import { PrefsContext } from './prefs-ctx';
import { installGlobalHandlers, report } from './lib/crash';
import { DataProvider } from './store';
import { ErrorBoundary } from './ui/ErrorBoundary';
import { ExportProvider } from './ui/Exports';
import './styles.css';
const systemLang = (): string => (typeof navigator !== 'undefined' ? navigator.language : 'en');
function Root() {
const [prefs, setPrefs] = useState<Prefs>(() => {
const p = loadPrefs();
// Dev builds only: theme=light|dark and lang=en|tr force them (screenshots without clicking).
const th = devSwitch('theme'), lg = devSwitch('lang');
if (th === 'light' || th === 'dark') p.theme = th;
if (lg === 'en' || lg === 'tr') p.lang = lg;
if (devSwitch('start')) p.onboarded = true;
return p;
});
const lang = resolveLang(prefs.lang, systemLang());
setLang(lang);
useEffect(() => { applyPrefs(prefs, document.documentElement, systemLang()); }, [prefs]);
// React has the window now: public/boot.js stops watching for a blank start
useEffect(() => { document.documentElement.dataset.booted = '1'; }, []);
const update = useCallback((p: Prefs) => { setPrefs(p); savePrefs(p); }, []);
const onTheme = useCallback(() => {
const dark = prefs.theme === 'dark' || (prefs.theme === 'system' && matchMedia('(prefers-color-scheme: dark)').matches);
update({ ...prefs, theme: dark ? 'light' : 'dark' });
}, [prefs, update]);
// a language change re-renders the whole tree in the new language
return <PrefsContext.Provider value={{ prefs, update }}><DataProvider><ExportProvider><App key={lang} onTheme={onTheme} /></ExportProvider></DataProvider></PrefsContext.Provider>;
}
installGlobalHandlers();
createRoot(document.getElementById('root')!, {
// errors no boundary caught (a boundary's own fallback failing): logged; public/boot.js shows its plain panel
onUncaughtError: (e, info) => {
const r = report('react (uncaught)', e, info.componentStack);
(window as { __storeboundBlank?: (m: string, s?: string) => void }).__storeboundBlank?.(r.message, r.detail);
},
onRecoverableError: (e, info) => { report('react (recovered)', e, info.componentStack, 'warn'); },
}).render(<StrictMode><ErrorBoundary where="app" full><Root /></ErrorBoundary></StrictMode>);