mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
* fix(desktop): break renderer crash-reload loops with a circuit breaker A renderer that crashes deterministically — a GPU/V8 CHECK failure, a corrupt profile that aborts on every load — is reloaded forever by the desktop poll loop: load → crash → reload → crash. One 0.14.0 device did exactly this, emitting 26,011 desktop_renderer_crash events in a single day (96% of all renderer crashes on the release) while its window stayed blank the whole time. Add RendererCrashLoopBreaker (pure, unit-tested): after 5 crashes inside a 60s rolling window it opens. The poll loop then parks on a static, dependency-free error screen that explains recovery is automatic, rather than cycling the crash; markRendererFailed no-ops while open. Per-crash telemetry is suppressed once open — the tripping crash is still reported, flagged loop_tripped — so one wedged device can't emit tens of thousands of identical events. The breaker re-arms after a 5-minute quiet cooldown so a transient fault still self-heals without a reinstall. * feat(desktop): make crash-loop recovery active + observable Review feedback: the breaker only "waited" — its cooldown re-arm reloaded the same URL from the same cache, and the whole flow was near-invisible. On re-arm it now actively recovers: clears the HTTP + V8 code caches (non-destructive — no cookies or storage) before the retry reload, targeting the most common recoverable cause (a corrupt cached bundle or shader) instead of hoping conditions changed on their own. And it's now observable: a console.warn when the breaker opens (parking), a console.info per recovery attempt, and a bounded recovery-attempt analytics signal (reason:"recovery-attempt", recovery_attempt: N, at most one per cooldown). Triage can tell a chronic looper (index keeps climbing) from a device that recovered (events simply stop) without re-introducing the per-crash spam the breaker was built to kill. * revert(desktop): drop active cache-clear recovery, keep it passive + observable Reviewer worry: clearing caches on every cooldown could amplify churn on a wedged device without helping the dominant crash cause (a GPU/V8 CHECK, exit 0x80000003), i.e. pour oil on the fire. Back the breaker off to "stop the loop + passive retry": on re-arm it reloads without mutating any device state and lets a transient fault clear on its own. The observability added alongside stays: a warn on breaker-open, an info per recovery attempt, and the bounded recovery-attempt analytics signal (recovery_attempt: N) so chronic loopers are still visible. * feat(desktop): add report + save-logs actions to the renderer crash screen The recovery screen was informational only. On a renderer crash the daemon is still alive, so give the wedged user an actionable exit — reusing IPC the preload already exposes, no new surface: - "Report a problem" → window.__od__.openExternal() opens a prefilled GitHub new-issue whose title + body carry the app version, OS, and the renderer exit code (e.g. 0x80000003) a triager needs, and ask the user to attach the logs. - "Save logs…" → window.openDesignDesktop.exportDiagnostics() runs the existing diagnostics-bundle export (save dialog → zip). Neither an issue URL nor mailto can carry a file attachment, so the flow is save-then-attach. Each button hides itself if its bridge is missing (preload failed to load) to avoid a dead control. * feat(desktop): add "Email us" (support@open-design.ai) to the crash screen Alongside "Report a problem" / "Save logs…", the recovery screen now offers a prefilled mailto to our support address for users who'd rather email than open a GitHub account. It reuses the existing openExternal bridge; the shell:open-external handler now also allows a mailto addressed strictly to the support address (isSupportMailtoUrl, unit-tested) — no other scheme or address opens, so widening past http can't be abused into arbitrary-scheme launches. The link hides itself if the bridge is absent, like the other actions. * style(desktop): match crash screen to the app's monochrome brand palette The crash screen used a stray blue accent that didn't match the app. Repaint it with the neutral design tokens from apps/web (tokens.css): warm off-white #faf9f7 + near-black #1a1916, a monochrome near-black primary button, and a plain underlined (not blue) email link. Dark mode inverts the pair to a light button on the dark bg (a near-black button would vanish), mirroring the black/white onboarding look. * fix(desktop): address review — tighten support mailto + reveal crash screen Two Looper findings on the crash-screen actions: - Security: isSupportMailtoUrl validated only protocol + pathname, so `mailto:support@open-design.ai?bcc=attacker@example.com` (or ?to=/?cc=) still passed and shell.openExternal would launch the mail client with extra recipients/headers through the renderer-exposed bridge. Now reject any query key other than the crash screen's own subject/body. - Startup lifecycle: parking on the crash screen left `revealed` false and the splash open when the loop tripped before revealWhenReady(), so the runtime's show() kept focusing the splash — a user re-focusing during a startup crash loop was sent back to the boot splash. Route the park through revealMainWindow() so the recovery screen becomes the revealed, focused window and the splash is torn down (no-op after normal reveal). * feat(diagnostics): make the support bundle root-cause native renderer crashes Audit of "Save logs…" for the crash-recovery screen: on a renderer crash the bundle collected text logs but NOT the one artifact that can explain an opaque native abort like 0x80000003 (a V8/Chromium CHECK) — a minidump. crashReporter was never started (no dumps existed anywhere), the crash-report collector was macOS-only, and there was no GPU info (the usual suspect for such crashes). Close the gap end to end: - apps/desktop: start crashReporter (uploadToServer:false — on-device only) and relocate Electron's crashDumps into <logs/desktop>/crashes, inside the log tree the export already bundles; snapshot GPU + feature-status + runtime info to <logs/desktop>/gpu-info.json at startup. - packages/diagnostics: add a `binary` LogSource kind (copied verbatim, no text decode/redaction so a minidump isn't corrupted) and findCrashDumps() to collect .dmp files; buildDiagnosticsZip gains a `crashDumps` input. - apps/daemon: bundle logs/desktop/gpu-info.json and the relocated crash minidumps (cross-platform, capped, within 14 days). The renderer crash context (reason/exit code/url) is already captured — the main-process console.error lands in logs/desktop/latest.log — so a bundle now carries stack (minidump) + GPU state + context: enough to root-cause. * fix(desktop): mkdir crashDumps dir before setPath so first-crash dumps land in the log tree Review (PerishCode): app.setPath('crashDumps', dir) throws when the dir does not exist; on a fresh runtime logs/desktop/crashes won't exist yet, so setPath threw, we fell back to Electron's default crashDumps location, and the daemon export (which only scans <logs/desktop>/crashes) missed the minidump for exactly the first crash loop this is meant to root-cause. mkdirSync the dir (recursive) before setPath. * fix(desktop): reject CR/LF in the support mailto subject/body Review (PerishCode): the key allowlist rejected to/cc/bcc keys, but a decoded CRLF in an allowed value (subject=ok%0D%0ABcc:attacker@…) could still inject a mail header once the mailto reaches the mail client. Reject any subject/body value containing CR or LF.