Skip to content

Run the safety review automatically after a backup restore - #916

Merged
ericgriffin merged 10 commits into
mainfrom
worktree-safety-review-after-restore
Aug 9, 2026
Merged

Run the safety review automatically after a backup restore#916
ericgriffin merged 10 commits into
mainfrom
worktree-safety-review-after-restore

Conversation

@ericgriffin

@ericgriffin ericgriffin commented Aug 9, 2026

Copy link
Copy Markdown
Member

Summary

Restoring a backup left the safety review empty for the entire logbook. No dive
showed findings and no dive-list row showed a finding badge until the user found
Settings > Safety > "Analyze all dives" and ran it by hand.

The safety review has only two triggers, and a restore is neither: lazy compute
when a dive detail page is opened (safetyReviewProvider), and the manual
Settings sweep. A backup is a whole-file SQLite copy, so the
dive_safety_reviews / dive_safety_findings tables travel with it — but
carrying only whatever rows the source database happened to have. A backup that
predates the feature restores those tables empty (the migration ladder creates
them), and dives the source device never opened were never analyzed. Either way
DiveSummary.safetyFindingCount reports zero across the library.

This runs the sweep automatically as the last step of a restore, over every
diver, with progress and a Skip button on the restore barrier.

Changes

  • Extract the "Analyze all dives" loop from safety_settings_page.dart into a
    shared SafetyReviewSweep provider. Both Settings and the restore path use
    it, so the load-bearing ref.invalidate()-before-read invariant (needed
    because safetyReviewProvider is not autoDispose and would otherwise return
    a stale cached AsyncValue) lives in one place.
  • Call it from BackupOperationNotifier — the single seam every restore entry
    point funnels through, including the setup wizard — between the existing
    active-diver realignment and the restore-complete transition.
  • Run the sweep in a short-lived ProviderContainer (PostRestoreSafetyReview).
    The live container still holds settings loaded from the database being
    replaced: gradient factors shape the ceiling curve that the missedDecoStop
    and highSurfaceGf rules grade against, and ProfileLegend's metric-source
    defaults feed overlayComputerDecoData. Persisting findings computed from
    those would stamp the current engineVersion onto wrong results that then
    never recompute.
  • Extract rootProviderOverrides so the scratch container cannot drift from the
    real ProviderScope (logFileServiceProvider throws unless overridden).
  • Show determinate progress and a Skip button on the restore barrier. Skipping
    is lossless: unswept dives still compute lazily on first view, and the
    Settings sweep remains available.
  • A sweep failure can never fail the restore. By the time it runs, the database
    swap and sync re-baseline have already succeeded, so errors are logged and
    swallowed.

Two latent bugs fixed along the way, both independent of backups

  • SettingsNotifier starts at the AppSettings defaults and replaces its
    state asynchronously, so any consumer reading gradient factors before that
    first load completes silently grades against defaults. Added initialLoad,
    which the sweep awaits before running.
  • _loadSettings assigned state after an await with no mounted check,
    throwing "Tried to use SettingsNotifier after dispose" whenever a
    ProviderScope is torn down mid-load — reachable today via restartApp()'s
    soft restart, not just the new container. Added the guard.

Notes

  • No schema change. Both safety tables already exist and already sync.
  • No safety rule or SafetyReviewService.engineVersion change.
  • Merge and Replace restores both sweep. rebaselineAfterRestore has already
    cleared the sync position, so every row is pending regardless; the sweep's
    parent-dive HLC bumps add no meaningful extra push.
  • Three new strings translated across all eleven locales.

Design and implementation notes, including an as-built deviations section, are
in docs/superpowers/specs/2026-08-08-post-restore-safety-review-design.md and
docs/superpowers/plans/2026-08-08-post-restore-safety-review.md.

Test Plan

  • flutter test passes — 15,724 passing, 15 skipped, 0 failures
  • flutter analyze passes — clean across the whole project
  • Manual testing on: not yet run against a real device

New coverage:

  • safety_review_sweep_test.dart (new, 7 cases): sweeps every diver when
    diverId is null; scopes correctly when set; stops on isCancelled; counts a
    failing dive without aborting; no-ops when the master toggle is off; reports
    monotonic progress; handles an empty logbook.
  • post_restore_safety_review_test.dart (new, 3 cases): including
    reads settings from the restored database, not the defaults, which switches
    the master toggle off in the restored diver's row and asserts the sweep
    no-ops — the assertion that actually proves the scratch container reads
    restored settings rather than defaults.
  • backup_providers_restore_test.dart (+5): both restore entry points run the
    sweep; progress reaches the barrier with isRestoring still true; a throwing
    sweep still reaches restoreComplete; the skip flag reaches the running sweep.
  • restore_barrier_test.dart (+2): progress label and Skip button render; the
    plain spinner is unchanged when no sweep is running.
  • root_overrides_test.dart (new).

Suggested manual check: restore a backup created before the safety review
feature existed. The barrier should show "Running the safety review" with a
moving determinate bar, then the restore-complete screen. After the restart,
dive-list rows should show finding badges without opening each dive first.

Adds PostRestoreSafetyReview, which runs the whole-library safety sweep in a
short-lived ProviderContainer built against the restored database rather than
the live one, whose settings were loaded from the database being replaced.

Two supporting fixes in SettingsNotifier, both reachable independently of
backups:
- Expose initialLoad. State starts at the AppSettings defaults and is replaced
  asynchronously, so any consumer reading gradient factors before the first
  load completes silently grades against defaults.
- Guard the post-await state assignment with a mounted check. Tearing down a
  ProviderScope mid-load (restartApp's soft restart, or the sweep's throwaway
  container) previously threw 'Tried to use SettingsNotifier after dispose'.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Automatically runs a safety-review sweep after restoring a backup so restored logbooks immediately show safety findings/badges without requiring the user to manually run “Analyze all dives” in Settings. This fits into the backup/restore flow by adding a post-restore analysis phase with progress UI and cancellation.

Changes:

  • Extracts the “Analyze all dives” logic into a reusable SafetyReviewSweep provider and reuses it from both Settings and post-restore flow.
  • Adds a post-restore runner that executes the sweep in a short-lived ProviderContainer with shared root overrides, and surfaces progress + Skip on the restore barrier.
  • Fixes SettingsNotifier lifecycle/initial-load issues by exposing initialLoad and guarding state writes after async reads.

Reviewed changes

Copilot reviewed 42 out of 42 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
test/helpers/mock_providers.dart Updates mock settings notifier to implement initialLoad.
test/features/statistics/presentation/pages/records_page_test.dart Updates local settings mock to implement initialLoad.
test/features/settings/presentation/pages/settings_page_test.dart Updates local settings mock to implement initialLoad.
test/features/settings/presentation/pages/settings_page_shared_data_test.dart Updates local settings mock to implement initialLoad.
test/features/dive_log/presentation/providers/safety_review_sweep_test.dart New unit tests for SafetyReviewSweep behavior (scope, progress, cancellation, failures).
test/features/backup/presentation/widgets/restore_barrier_test.dart Adds widget coverage for sweep progress UI + Skip button rendering.
test/features/backup/presentation/providers/post_restore_safety_review_test.dart New tests ensuring post-restore sweep uses restored DB settings and reports progress/cancellation.
test/features/backup/presentation/providers/backup_providers_restore_test.dart Extends restore notifier tests to verify sweep invocation, progress publication, skip propagation, and failure-swallowing.
test/core/providers/root_overrides_test.dart New test for shared root override helper.
lib/main.dart Switches root ProviderScope overrides to rootProviderOverrides(...).
lib/core/providers/root_overrides.dart New shared helper producing the root provider overrides used by both app scope and scratch containers.
lib/features/settings/presentation/providers/settings_providers.dart Adds SettingsNotifier.initialLoad and mounted guard after async settings read.
lib/features/settings/presentation/pages/safety_settings_page.dart Delegates manual “Analyze all dives” to SafetyReviewSweep.
lib/features/dive_log/presentation/providers/safety_review_sweep.dart New reusable sweep runner for safety-review analysis.
lib/features/backup/presentation/providers/post_restore_safety_review.dart New post-restore runner that uses a scratch container and awaits settings initial load.
lib/features/backup/presentation/providers/backup_providers.dart Wires post-restore sweep into restore flows; publishes structured progress; adds skip support.
lib/features/backup/presentation/widgets/restore_barrier.dart Shows localized determinate progress + Skip button when sweep is running.
lib/l10n/arb/app_en.arb Adds restore-safety-sweep title/progress/skip strings.
lib/l10n/arb/app_de.arb Adds restore-safety-sweep title/progress/skip strings (de).
lib/l10n/arb/app_es.arb Adds restore-safety-sweep title/progress/skip strings (es).
lib/l10n/arb/app_fr.arb Adds restore-safety-sweep title/progress/skip strings (fr).
lib/l10n/arb/app_it.arb Adds restore-safety-sweep title/progress/skip strings (it).
lib/l10n/arb/app_hu.arb Adds restore-safety-sweep title/progress/skip strings (hu).
lib/l10n/arb/app_he.arb Adds restore-safety-sweep title/progress/skip strings (he).
lib/l10n/arb/app_nl.arb Adds restore-safety-sweep title/progress/skip strings (nl).
lib/l10n/arb/app_pt.arb Adds restore-safety-sweep title/progress/skip strings (pt).
lib/l10n/arb/app_zh.arb Adds restore-safety-sweep title/progress/skip strings (zh).
lib/l10n/arb/app_ar.arb Adds restore-safety-sweep title/progress/skip strings (ar).
lib/l10n/arb/app_localizations.dart Updates generated localization interface for new keys.
lib/l10n/arb/app_localizations_en.dart Updates generated en localizations for new keys.
lib/l10n/arb/app_localizations_de.dart Updates generated de localizations for new keys.
lib/l10n/arb/app_localizations_es.dart Updates generated es localizations for new keys.
lib/l10n/arb/app_localizations_fr.dart Updates generated fr localizations for new keys.
lib/l10n/arb/app_localizations_it.dart Updates generated it localizations for new keys.
lib/l10n/arb/app_localizations_hu.dart Updates generated hu localizations for new keys.
lib/l10n/arb/app_localizations_he.dart Updates generated he localizations for new keys.
lib/l10n/arb/app_localizations_nl.dart Updates generated nl localizations for new keys.
lib/l10n/arb/app_localizations_pt.dart Updates generated pt localizations for new keys.
lib/l10n/arb/app_localizations_zh.dart Updates generated zh localizations for new keys.
lib/l10n/arb/app_localizations_ar.dart Updates generated ar localizations for new keys.
docs/superpowers/specs/2026-08-08-post-restore-safety-review-design.md New design/spec documenting restore-triggered safety sweep.
docs/superpowers/plans/2026-08-08-post-restore-safety-review.md New implementation plan + as-built deviations/verification checklist.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread lib/features/backup/presentation/providers/backup_providers.dart
Comment thread lib/features/dive_log/presentation/providers/safety_review_sweep.dart Outdated
@ericgriffin ericgriffin added the enhancement New feature or request label Aug 9, 2026
@ericgriffin ericgriffin moved this from Backlog to In review in Submersion Release Tracker Aug 9, 2026
@codecov

codecov Bot commented Aug 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.36111% with 11 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...s/backup/presentation/widgets/restore_barrier.dart 87.50% 4 Missing ⚠️
lib/main.dart 0.00% 4 Missing ⚠️
...tings/presentation/pages/safety_settings_page.dart 85.71% 2 Missing ⚠️
...entation/providers/post_restore_safety_review.dart 97.82% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

📦 Build artifacts for this PR · commit 61abb85

Platform Download
Android (APK) android-apk
macOS macos-build
Windows windows-build
Linux linux-build

Artifacts expire in 7 days. Downloading requires being signed in to GitHub. macOS needs two extractions: unzip the downloaded artifact, then unzip the submersion-macos.zip inside it to get a runnable submersion.app. The build is ad-hoc signed — right-click → Open on first launch.

Updated automatically on each push.

…preserve sweep progress

Addresses PR #916 review.

Decompression settings are per-diver (diver_settings.gf_low/gf_high, ppO2
ceilings, deco stop increment), and computeAnalysisForProfile falls back to
gfLowProvider/gfHighProvider whenever a dive carries no dive-specific GFs.
A single all-divers pass therefore graded every non-active diver's dives with
the ACTIVE diver's gradient factors and persisted the result stamped with the
current engineVersion, so it would never be recomputed.

The sweep now runs one pass per diver, each in a container whose
settingsProvider is pinned to that diver via a new SettingsNotifier.preloaded
constructor. Overriding the single root provider covers every derived provider
(gradient factors, ppO2 ceilings, deco stop increment, ProfileLegend's
metric-source defaults) instead of enumerating a dozen overrides that would rot
as the analysis pipeline grows. Dives with a null diver_id get a trailing pass.

Settings are read with getSettingsForDiver, not getOrCreateSettingsForDiver:
the latter writes a defaults row, and a restore must not mint rows that would
sync out as real edits.

BackupOperationState.copyWith now preserves sweepProgress when omitted and
takes an explicit clearSweepProgress flag.
Copilot AI review requested due to automatic review settings August 9, 2026 03:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 42 out of 42 changed files in this pull request and generated no new comments.

Suppressed comments (1)

lib/features/backup/presentation/providers/post_restore_safety_review.dart:52

  • run() emits onProgress(0, total) immediately after counting all dives, before knowing whether any sweep pass will actually run. Because SafetyReviewSweep.run() returns early when safetyReviewEnabledProvider is false (and does not call onProgress), a restore can still publish sweepProgress and show the “Running the safety review” UI even when the restored diver has the safety review disabled, and progress may never reach total when some passes are skipped due to per-diver settings.

Consider computing the progress total as the number of dives that will actually be swept (based on each diver’s safetyReviewEnabled), and if that computed total is 0, return SafetyReviewSweepResult.empty without calling onProgress so the restore barrier stays in its normal restore-spinner mode.

    final diveRepo = _ref.read(diveRepositoryProvider);
    final allIds = await diveRepo.getOrderedDiveIds();
    final total = allIds.length;
    onProgress?.call(0, total);
    if (total == 0) return SafetyReviewSweepResult.empty;

@ericgriffin
ericgriffin merged commit 8ceeda7 into main Aug 9, 2026
25 checks passed
@github-project-automation github-project-automation Bot moved this from In review to Done in Submersion Release Tracker Aug 9, 2026
@ericgriffin
ericgriffin deleted the worktree-safety-review-after-restore branch August 9, 2026 04:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants