The automatic crash-report submit prompt over-triggers. After updating to a build that fixes a crash, the app still pops the "LibreMail crashed" dialog on the next launch — because pre-update crash reports remain in the file-backed store and are re-surfaced on every launch. It reads as "the new build just crashed," when it didn't.
Observed live (2026-07-03): a device on the pre-fix build crash-looped on the SQLCipher UnsatisfiedLinkError (4 saved crash reports at 09:02). After installing the fix (592a797) the process ran stably (8m+ uptime, no new crash anywhere — confirmed the two restarts were killDueToPackageUpdate + a task-removal kill, not crashes), yet on relaunch the dialog popped for those stale 09:02 reports.
Required behavior (product owner)
The automatic submit UX must:
Trigger only on the FIRST app re-open after a crash — offered at most once, then it must not re-nag on subsequent launches. The report still remains in the Problem Reports list for manual review/submit.
Only for a crash report < 24 hours old — older crashes never auto-pop.
Only for a legitimate crash — an app update, a user-initiated close (swipe-away / task removal), or a force-stop must NOT be treated as a crash or trigger the prompt.
Current behavior / gap
StartupReportViewModel.pendingCrash (app/src/main/kotlin/org/libremail/ui/reporting/StartupReportViewModel.kt) surfaces the newest ReportKind.CRASH report on every launch until dismissed/discarded, with no age bound → re-nags and shows stale crashes.
Reports are created only by the uncaught-exception handler (CrashReporter), so update/force-stop/user-close already do not create a crash report. Requirement #3 is therefore mostly already satisfied by construction — but it must be made explicit and test-covered, and the surfacing gates (#1, #2) added.
Proposed implementation
First-re-open-only: persist a per-report "surfaced" marker (a flag on the stored DebugReport, or a small seen-id set) so a given crash is auto-offered at most once. dismiss() marks it surfaced (not just an in-memory hide). The report stays in Problem Reports.
<24h age gate:pendingCrash filters to createdAtMillis >= now - 24h.
Legitimate-crash distinction: verify + test that killDueToPackageUpdate, force-stop, and task-removal yield no crash report and no prompt. Optional hardening: a clean-shutdown sentinel (write a marker on normal stop/update; only surface a crash lacking a matching clean marker) if we want defense beyond the uncaught-handler guarantee.
Definition of done (per CLAUDE.md)
Unit tests (StartupReportViewModel): fresh (<24h) unseen crash → shown; same report after being surfaced once → NOT shown again across relaunches (first-open-only); crash ≥24h old → not shown; non-CRASH kinds unaffected; 24h boundary.
Instrumented/E2E: a legitimate recent crash pops the dialog exactly once and not again on the next launch; a stale/old report does not pop it.
Not done until unit and E2E pass.
Sequencing
Depends on #249 (coverage lane 4) landing first — that lane adds StartupReportViewModel unit tests, and this change modifies that VM + its tests, so implementing before #249 merges would collide semantically. Implement after #249 lands and update those tests to the new behavior.
## Problem
The automatic crash-report submit prompt over-triggers. After updating to a build that **fixes** a crash, the app still pops the "LibreMail crashed" dialog on the next launch — because pre-update crash reports remain in the file-backed store and are re-surfaced on every launch. It reads as "the new build just crashed," when it didn't.
**Observed live (2026-07-03):** a device on the pre-fix build crash-looped on the SQLCipher `UnsatisfiedLinkError` (4 saved crash reports at 09:02). After installing the fix (`592a797`) the process ran stably (8m+ uptime, no new crash anywhere — confirmed the two restarts were `killDueToPackageUpdate` + a task-removal kill, not crashes), yet on relaunch the dialog popped for those stale 09:02 reports.
## Required behavior (product owner)
The automatic submit UX must:
1. **Trigger only on the FIRST app re-open after a crash** — offered at most once, then it must not re-nag on subsequent launches. The report still remains in the Problem Reports list for manual review/submit.
2. **Only for a crash report < 24 hours old** — older crashes never auto-pop.
3. **Only for a legitimate crash** — an app update, a user-initiated close (swipe-away / task removal), or a **force-stop** must NOT be treated as a crash or trigger the prompt.
## Current behavior / gap
- `StartupReportViewModel.pendingCrash` (`app/src/main/kotlin/org/libremail/ui/reporting/StartupReportViewModel.kt`) surfaces the newest `ReportKind.CRASH` report on **every** launch until dismissed/discarded, with no age bound → re-nags and shows stale crashes.
- Reports are created only by the uncaught-exception handler (`CrashReporter`), so update/force-stop/user-close already do **not** create a crash report. Requirement #3 is therefore mostly already satisfied by construction — but it must be made **explicit and test-covered**, and the surfacing gates (#1, #2) added.
## Proposed implementation
- **First-re-open-only:** persist a per-report "surfaced" marker (a flag on the stored `DebugReport`, or a small seen-id set) so a given crash is auto-offered at most once. `dismiss()` marks it surfaced (not just an in-memory hide). The report stays in Problem Reports.
- **<24h age gate:** `pendingCrash` filters to `createdAtMillis >= now - 24h`.
- **Legitimate-crash distinction:** verify + test that `killDueToPackageUpdate`, force-stop, and task-removal yield no crash report and no prompt. Optional hardening: a clean-shutdown sentinel (write a marker on normal stop/update; only surface a crash lacking a matching clean marker) if we want defense beyond the uncaught-handler guarantee.
## Definition of done (per CLAUDE.md)
- **Unit tests** (`StartupReportViewModel`): fresh (<24h) unseen crash → shown; same report after being surfaced once → NOT shown again across relaunches (first-open-only); crash ≥24h old → not shown; non-`CRASH` kinds unaffected; 24h boundary.
- **Instrumented/E2E**: a legitimate recent crash pops the dialog exactly once and not again on the next launch; a stale/old report does not pop it.
- Not done until unit **and** E2E pass.
## Sequencing
Depends on **#249 (coverage lane 4)** landing first — that lane adds `StartupReportViewModel` unit tests, and this change modifies that VM + its tests, so implementing before #249 merges would collide semantically. Implement after #249 lands and update those tests to the new behavior.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Problem
The automatic crash-report submit prompt over-triggers. After updating to a build that fixes a crash, the app still pops the "LibreMail crashed" dialog on the next launch — because pre-update crash reports remain in the file-backed store and are re-surfaced on every launch. It reads as "the new build just crashed," when it didn't.
Observed live (2026-07-03): a device on the pre-fix build crash-looped on the SQLCipher
UnsatisfiedLinkError(4 saved crash reports at 09:02). After installing the fix (592a797) the process ran stably (8m+ uptime, no new crash anywhere — confirmed the two restarts werekillDueToPackageUpdate+ a task-removal kill, not crashes), yet on relaunch the dialog popped for those stale 09:02 reports.Required behavior (product owner)
The automatic submit UX must:
Current behavior / gap
StartupReportViewModel.pendingCrash(app/src/main/kotlin/org/libremail/ui/reporting/StartupReportViewModel.kt) surfaces the newestReportKind.CRASHreport on every launch until dismissed/discarded, with no age bound → re-nags and shows stale crashes.CrashReporter), so update/force-stop/user-close already do not create a crash report. Requirement #3 is therefore mostly already satisfied by construction — but it must be made explicit and test-covered, and the surfacing gates (#1, #2) added.Proposed implementation
DebugReport, or a small seen-id set) so a given crash is auto-offered at most once.dismiss()marks it surfaced (not just an in-memory hide). The report stays in Problem Reports.pendingCrashfilters tocreatedAtMillis >= now - 24h.killDueToPackageUpdate, force-stop, and task-removal yield no crash report and no prompt. Optional hardening: a clean-shutdown sentinel (write a marker on normal stop/update; only surface a crash lacking a matching clean marker) if we want defense beyond the uncaught-handler guarantee.Definition of done (per CLAUDE.md)
StartupReportViewModel): fresh (<24h) unseen crash → shown; same report after being surfaced once → NOT shown again across relaunches (first-open-only); crash ≥24h old → not shown; non-CRASHkinds unaffected; 24h boundary.Sequencing
Depends on #249 (coverage lane 4) landing first — that lane adds
StartupReportViewModelunit tests, and this change modifies that VM + its tests, so implementing before #249 merges would collide semantically. Implement after #249 lands and update those tests to the new behavior.