Skip to content

Feature/duration bottomtime rename sac fix#95

Merged
ericgriffin merged 13 commits into
mainfrom
feature/duration-bottomtime-rename-sac-fix
Mar 28, 2026
Merged

Feature/duration bottomtime rename sac fix#95
ericgriffin merged 13 commits into
mainfrom
feature/duration-bottomtime-rename-sac-fix

Conversation

@ericgriffin

Copy link
Copy Markdown
Member

Summary

Fix SAC (Surface Air Consumption) calculations that were inflated by 1.5-2x because they used bottom time instead of runtime. Also rename Dive.duration to Dive.bottomTime to eliminate the naming confusion that caused this bug.

Fixes #72, fixes #87 (SAC portion).

Changes

  • Rename Dive.duration to Dive.bottomTime across 74 files (domain entities, database column, repository SQL, import/export services, presentation, tests). Pure mechanical refactor with zero behavior change.
  • Database migration v56: ALTER TABLE dives RENAME COLUMN duration TO bottom_time
  • Add Dive.effectiveRuntime getter with fallback chain: runtime -> exitTime - entryTime -> profile-based -> bottomTime
  • Fix Dive.sac and Dive.sacPressure to use effectiveRuntime instead of bottomTime
  • Fix GasAnalysisService.calculateCylinderSac to use effectiveRuntime for tank usage range
  • Fix statistics SQL queries (6 queries) to use COALESCE(d.runtime, d.bottom_time) for SAC calculations
  • Replace calculatedDuration getter with effectiveRuntime — the old getter conflated runtime and bottom time; all 4 callers wanted runtime
  • Rename DiveSortField.duration to DiveSortField.bottomTime, minDurationMinutes/maxDurationMinutes to
    minBottomTimeMinutes/maxBottomTimeMinutes

Test Plan

  • flutter test passes (3039 tests, 0 failures)
  • flutter analyze passes (0 issues)
  • Manual testing on: macOS
  • 38 new unit tests for effectiveRuntime (13 tests) and sac/sacPressure (25 tests)
  • Issue Dive Details - SAC Rate calculations wrong by a factor of 2 or more #72 exact numbers reproduced: 170 bar used, AL80 (11.1L), 20.3m avg, 42 min runtime -> SAC = 14.83 L/min (was 31.5)
  • Verified DO-NOT-RENAME items untouched: DiveDataSource.duration, ImportedDive.duration, IncomingDiveData.durationSeconds,
    ComparisonFieldType.duration
  • Safety grep confirms no remaining Dive.duration or calculatedDuration references

Pure mechanical rename with zero behavior change. Renames
Dive.duration -> Dive.bottomTime and DiveSummary.duration ->
DiveSummary.bottomTime across domain entities, database SQL,
repositories, import/export services, presentation layer, and
all tests. All 3001 tests pass identically.
)

Add Dive.effectiveRuntime getter with fallback chain (runtime ->
exitTime-entryTime -> profile-based -> bottomTime). Fix Dive.sac,
Dive.sacPressure, GasAnalysisService, and statistics SQL queries to
use runtime for accurate consumption rates. Replace calculatedDuration
with effectiveRuntime.
Add edge case tests for effectiveRuntime (negative/zero entry-exit,
missing exit, empty profile, single profile point). Add comprehensive
sac/sacPressure tests covering all null-return branches, tank edge
cases (missing volume/pressures, zero usage), multi-tank aggregation,
and issue regression tests for #72 and #87.

13 effectiveRuntime tests + 25 SAC tests = 38 total (was 11).
@ericgriffin
ericgriffin marked this pull request as ready for review March 28, 2026 04:18
Copilot AI review requested due to automatic review settings March 28, 2026 04:18

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

Fixes inflated SAC/SAC-pressure calculations by using runtime (via a new Dive.effectiveRuntime fallback chain) instead of bottom time, and mechanically renames Dive.durationDive.bottomTime across the codebase (including a DB column rename migration).

Changes:

  • Rename Dive.duration to Dive.bottomTime across domain/data/presentation/tests and update DB schema + SQL queries accordingly.
  • Add Dive.effectiveRuntime and update SAC/SAC-pressure (and related gas analysis/statistics) to use runtime semantics.
  • Add new unit tests for effectiveRuntime and SAC regressions.

Reviewed changes

Copilot reviewed 78 out of 78 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
test/integration/uddf_test_importer.dart Update test UDDF importer to populate bottomTime and related profile fallback.
test/helpers/performance_data_generator.dart Update generated test data to write bottomTime column.
test/features/universal_import/data/services/import_duplicate_checker_test.dart Update duplicate-checker tests to use bottomTime.
test/features/media/data/services/trip_media_scanner_test.dart Update scanner tests to use bottomTime.
test/features/import_wizard/data/adapters/healthkit_adapter_test.dart Update HealthKit adapter tests to use bottomTime.
test/features/import_wizard/data/adapters/fit_adapter_test.dart Update FIT adapter tests to use bottomTime.
test/features/dive_log/presentation/widgets/merge_dive_dialog_test.dart Update merge dialog widget tests to use bottomTime.
test/features/dive_log/integration/multi_computer_integration_test.dart Update integration seeding to write bottomTime.
test/features/dive_log/domain/services/field_attribution_service_test.dart Update attribution test keys from duration to bottomTime.
test/features/dive_log/domain/models/dive_filter_state_test.dart Rename filter fields to min/maxBottomTimeMinutes and update tests.
test/features/dive_log/domain/entities/dive_sac_fix_test.dart New SAC/SAC-pressure regression tests using runtime semantics.
test/features/dive_log/domain/entities/dive_effective_runtime_test.dart New unit tests validating effectiveRuntime fallback chain and edge cases.
test/features/dive_log/data/repositories/dive_repository_test.dart Update repository tests to assert bottomTime.
test/features/dive_log/data/repositories/dive_repository_new_methods_test.dart Update repository method tests to use bottomTime DB field.
test/features/dive_log/data/repositories/dive_consolidation_test.dart Update consolidation tests to write/verify bottomTime.
test/features/dive_log/data/repositories/dive_computer_repository_impl_test.dart Update computer repo tests to write bottomTime.
test/features/dive_log/data/repositories/dive_computer_data_repository_test.dart Update computer data repo tests to write bottomTime.
test/features/dive_import/presentation/providers/dive_import_notifier_test.dart Update import notifier test dive construction to use bottomTime.
test/features/dive_import/domain/services/imported_dive_converter_test.dart Update converter tests to assert bottomTime behavior.
test/features/dive_import/data/services/uddf_duplicate_checker_test.dart Update UDDF duplicate-checker tests to use bottomTime.
test/core/domain/models/dive_comparison_result_test.dart Update comparison result test dive construction to use bottomTime.
test/core/constants/card_color_test.dart Update card color test to use bottomTime.
lib/features/universal_import/data/services/import_duplicate_checker.dart Use bottomTime for duration fallback when runtime/entry/exit aren’t available.
lib/features/trips/presentation/widgets/trip_overview_tab.dart Display bottomTime instead of duration in trip overview UI.
lib/features/trips/presentation/widgets/trip_daily_breakdown.dart Aggregate trip total bottom time using bottomTime.
lib/features/trips/presentation/pages/trip_detail_page.dart Display bottomTime instead of duration in trip detail UI.
lib/features/trips/data/repositories/trip_repository.dart Update trip summary SQL to sum bottom_time.
lib/features/statistics/data/repositories/statistics_repository.dart Update SAC-related SQL to use COALESCE(runtime, bottom_time); update avg duration to bottom_time.
lib/features/settings/presentation/providers/export_providers.dart Update export provider test/fixture mapping to bottomTime.
lib/features/media/presentation/helpers/photo_import_helper.dart Switch photo matching window to effectiveRuntime (replacing calculatedDuration).
lib/features/media/data/services/trip_media_scanner.dart Update exit-time fallback to use bottomTime.
lib/features/maps/presentation/pages/dive_activity_map_page.dart Update map UI chips to show bottomTime.
lib/features/import_wizard/data/adapters/healthkit_adapter.dart Update duration fallback to use bottomTime.
lib/features/import_wizard/data/adapters/fit_adapter.dart Update duration fallback to use bottomTime.
lib/features/divers/data/repositories/diver_repository.dart Update total-time SQL to sum bottom_time.
lib/features/dive_planner/presentation/providers/dive_planner_providers.dart Create planned dives with bottomTime rather than duration.
lib/features/dive_log/presentation/widgets/merge_dive_dialog.dart Display bottomTime in merge candidate UI.
lib/features/dive_log/presentation/widgets/dive_map_content.dart Update map content to show bottomTime.
lib/features/dive_log/presentation/widgets/dive_list_content.dart Prefer runtime ?? bottomTime when displaying “duration” in list tiles.
lib/features/dive_log/presentation/providers/dive_providers.dart Sort by DiveSortField.bottomTime using bottomTime.
lib/features/dive_log/presentation/pages/dive_search_page.dart Wire UI state to min/maxBottomTimeMinutes filter fields.
lib/features/dive_log/presentation/pages/dive_list_page.dart Wire filter sheet to min/maxBottomTimeMinutes and list tiles to bottomTime.
lib/features/dive_log/presentation/pages/dive_edit_page.dart Update edit form initialization/save paths to use bottomTime; runtime calc uses entry/exit.
lib/features/dive_log/presentation/pages/dive_detail_page.dart Display bottomTime + attribution key; use effectiveRuntime for profile chart duration.
lib/features/dive_log/domain/services/field_attribution_service.dart Attribute bottomTime field key from multi-source DiveDataSource.duration.
lib/features/dive_log/domain/models/dive_filter_state.dart Rename filter fields to min/maxBottomTimeMinutes and apply to bottomTime.
lib/features/dive_log/domain/entities/dive_summary.dart Rename summary field to bottomTime.
lib/features/dive_log/domain/entities/dive.dart Rename durationbottomTime; add effectiveRuntime; update sac/sacPressure to use runtime semantics.
lib/features/dive_log/data/services/gas_analysis_service.dart Use effectiveRuntime for analysis range end time.
lib/features/dive_log/data/repositories/dive_repository_impl.dart Persist/load bottom_time; update sort/filter SQL; update records queries and data-source backfill to map bottom time.
lib/features/dive_log/data/repositories/dive_computer_repository_impl.dart Update queries/inserts to use bottom_time and persist bottom time seconds.
lib/features/dive_import/presentation/providers/dive_import_providers.dart Use runtime ?? bottomTime for duplicate matching scoring inputs.
lib/features/dive_import/domain/services/imported_dive_converter.dart Store computed bottom time via copyWith(bottomTime: ...).
lib/features/dive_import/data/services/uddf_entity_importer.dart Map UDDF duration to bottomTime, and persist to DB correctly.
lib/features/dive_import/data/services/uddf_duplicate_checker.dart Update duration fallback to use bottomTime.
lib/features/dashboard/presentation/widgets/recent_dives_card.dart Prefer runtime ?? bottomTime for dashboard “duration” display.
lib/features/dashboard/presentation/widgets/personal_records_card.dart Display longest dive based on bottomTime.
lib/features/dashboard/presentation/providers/dashboard_providers.dart Compute longest dive using bottomTime.
lib/features/buddies/presentation/pages/buddy_detail_page.dart Display bottomTime in buddy detail dive list items.
lib/core/services/sync/sync_data_serializer.dart Serialize bottomTime under the existing duration key for sync payloads.
lib/core/services/pdf_templates/pdf_template_simple.dart PDF output uses bottomTime for duration field.
lib/core/services/pdf_templates/pdf_template_professional.dart PDF output uses bottomTime for duration field.
lib/core/services/pdf_templates/pdf_template_padi.dart PADI PDF output uses bottomTime.
lib/core/services/pdf_templates/pdf_template_naui.dart NAUI PDF output totals/fields use bottomTime.
lib/core/services/pdf_templates/pdf_template_detailed.dart Detailed PDF output uses bottomTime.
lib/core/services/pdf_templates/pdf_shared_components.dart Shared PDF totals use bottomTime.
lib/core/services/export/uddf/uddf_export_service.dart UDDF export uses bottomTime and generated profile duration uses bottom time seconds.
lib/core/services/export/uddf/uddf_export_builders.dart UDDF builders use bottomTime.
lib/core/services/export/pdf/pdf_export_service.dart PDF export uses bottomTime for duration fields and totals.
lib/core/services/export/pdf/pdf_course_export_service.dart Course PDF totals/fields use bottomTime.
lib/core/services/export/kml/kml_export_service.dart KML export uses bottomTime for duration string.
lib/core/services/export/excel/excel_export_service.dart Excel export uses bottomTime for duration + summary stats.
lib/core/services/export/csv/csv_export_service.dart CSV export uses bottomTime for duration column.
lib/core/database/database.dart DB schema version bump + dives.duration column rename migration to bottom_time; drift column renamed to bottomTime.
lib/core/constants/sort_options.dart Rename sort field enum value to bottomTime.
lib/core/constants/card_color.dart Card color attribute “duration” now uses bottomTime minutes.
integration_test/helpers/uddf_screenshot_helper.dart Update screenshot importer seed data to use bottomTime.
integration_test/helpers/screenshot_test_data.dart Update screenshot seeding to write bottomTime.

Comment thread lib/features/media/data/services/trip_media_scanner.dart
Comment thread test/features/dive_log/domain/entities/dive_sac_fix_test.dart
Comment thread lib/features/dive_log/data/repositories/dive_repository_impl.dart
@claude

claude Bot commented Mar 28, 2026

Copy link
Copy Markdown

Code Review - Stale attribution key for runtime stat

File: lib/features/dive_log/presentation/pages/dive_detail_page.dart, line 712

This PR correctly updates field_attribution_service.dart to write attribution[bottomTime] instead of attribution[duration], and updates the bottom time stat item lookup. However, the runtime stat item (line 712) was not updated and still reads attribution?[duration], which is no longer set by the attribution service.

Since attribution[duration] is no longer populated, the source attribution badge will silently never appear for the runtime stat on multi-source dives.

See:

_buildStatItem(
context,
Icons.timelapse,
_formatRuntime(dive),
context.l10n.diveLog_detail_stat_runtime,
sourceName: attribution?['duration'],
),
_buildStatItem(

Fix: Change line 712 to sourceName: attribution?[bottomTime],

@ericgriffin

ericgriffin commented Mar 28, 2026

Copy link
Copy Markdown
Member Author

Code review

Found 2 issues:

  1. Attribution key mismatch breaks Runtime stat source badge. field_attribution_service.dart:37 was updated to write the key 'bottomTime' instead of 'duration', and line 721 of dive_detail_page.dart was correctly updated to read attribution?['bottomTime']. However, line 712 (the Runtime stat) still reads attribution?['duration'], which no longer exists in the map. The Runtime stat's data source badge will silently show as null for all multi-source dives.

context.l10n.diveLog_detail_stat_runtime,
sourceName: attribution?['duration'],
),

  1. dive_list_content.dart uses runtime ?? bottomTime instead of effectiveRuntime. The PR establishes dive.effectiveRuntime as the canonical runtime getter (used in dive_detail_page.dart, photo_import_helper.dart, dive_repository_impl.dart), but 3 locations in dive_list_content.dart still use the two-step dive.runtime ?? dive.bottomTime fallback. Dives with entry/exit times but no explicit runtime will show bottomTime in the list view but the computed entry-exit duration in the detail view -- an observable UI inconsistency.

maxDepth: dive.maxDepth,
duration: dive.runtime ?? dive.bottomTime,
waterTemp: dive.waterTemp,

- Fix attribution key mismatch: dive_detail_page.dart:712 now reads
  attribution['bottomTime'] instead of stale 'duration' key
- Fix dive planner: totalTime (all segments) stored as runtime instead
  of bottomTime, since it includes descent/ascent/deco phases
- Rename DiveRecord.duration to DiveRecord.bottomTime for consistency
- Fix trip_media_scanner to use effectiveRuntime for dive bounds
- Clarify issue #87 regression test description and comments
- Update records_page and dive_summary_widget for DiveRecord rename
Add 82 new tests across 21 test files covering the duration-to-bottomTime
rename and SAC calculation fixes. Tests span all layers:

- Entity tests: DiveSummary with bottomTime field
- Service tests: GasAnalysisService.calculateCylinderSac with effectiveRuntime
- Repository tests: DiveRepository sort/filter/mapping, StatisticsRepository
  SAC queries with COALESCE(runtime, bottom_time)
- Export tests: Excel, PDF, KML, CSV exports with bottomTime data,
  all 5 PDF templates (simple, detailed, professional, padi, naui)
- Provider tests: dive sort provider, dashboard personal records
- Widget tests: DiveDetailPage, DiveEditPage, DiveListPage,
  DiveSearchPage, DiveSummaryWidget, PersonalRecordsCard,
  RecentDivesCard, BuddyDetailPage, TripOverviewTab, TripDetailPage

Also adds shared mock_providers.dart test helper with MockSettingsNotifier,
MockCurrentDiverIdNotifier, and common test utilities.
Tests that the filter sheet initializes minBottomTimeMinutes and
maxBottomTimeMinutes fields from the pre-set DiveFilterState.
- Add surface interval test exercising effectiveRuntime fallback
- Add gas analysis test for profile-timestamp diveEnd fallback
- Add UDDF export builder test for synthetic profile from bottomTime
- Add dive detail page test with attribution badges and data sources
- Improve dive edit page profile test with realistic depth data
- Add TripDailyBreakdown test exercising _totalBottomTimeMinutes
- Add liveaboard trip detail test covering _buildDivesTab bottomTime
@ericgriffin
ericgriffin merged commit c2d9e79 into main Mar 28, 2026
14 checks passed
@ericgriffin
ericgriffin deleted the feature/duration-bottomtime-rename-sac-fix branch March 28, 2026 21:09
ericgriffin added a commit that referenced this pull request Mar 28, 2026
PR #95 (duration->bottomTime rename) claimed migration v56. Bump the
dive_detail_sections column addition to v57 and update schemaVersion.
ericgriffin added a commit that referenced this pull request Mar 28, 2026
- Bump migration from v56 to v57 (PR #95 claimed v56 for the
  duration->bottom_time column rename)
- Regenerate Drift code against updated schema
- Add missing setDiveDetailSections/resetDiveDetailSections stubs
  to MockSettingsNotifier
ericgriffin added a commit that referenced this pull request Mar 28, 2026
* feat: add DiveDetailSectionId enum and DiveDetailSectionConfig model

Define the 17 configurable section IDs, their display metadata, and
JSON serialization for database storage. Includes ensureAllSections
for forward-compatible new-section detection.

* feat: add diveDetailSections column to diver_settings (v56)

Nullable TEXT column stores JSON section config. Null means use defaults.
No data backfill needed for existing divers.

* feat: add diveDetailSections to AppSettings and SettingsNotifier

New field with copyWith support and clear-to-defaults flag.
SettingsNotifier gets set and reset methods for the section config.

* feat: wire diveDetailSections through DiverSettingsRepository

JSON serialization via DiveDetailSectionConfig.sectionsFromJson/toJson.
Stores null when config matches defaults to avoid unnecessary writes.

* feat: add DiveDetailSectionsPage with drag-and-drop reordering

ReorderableListView with visibility toggle switches per section.
Reset to Default in overflow menu. Dimmed styling for hidden sections.

* feat: add Dive Details section to Appearance page with route

Navigation tile in Appearance links to /settings/dive-detail-sections.
New GoRoute wires the DiveDetailSectionsPage into the router.

* feat: config-driven section rendering on dive detail page

Replace hardcoded section order with a registry map and loop over the
user's diveDetailSections config. Fixed sections (header, profile)
remain at the top. All data-driven auto-hide conditions preserved.

* style: fix prefer_const lint hints in tests

* fix: always serialize diveDetailSections to JSON in repository

The previous code used List == (reference equality) to detect defaults
and write null. This always evaluates to false for loaded lists,
breaking the reset-to-defaults flow. Simplify by always writing JSON.

* fix: add Dive Details section to desktop appearance settings

The settings page uses a separate _AppearanceSectionContent widget
for the master-detail layout on desktop. The new section was only
added to the mobile AppearancePage, not the desktop inline version.

* test: increase coverage for dive detail section config

Add 17 new tests (32 -> 49 total) covering:
- Model: fromJson missing visible field, fromJson unknown ID throws,
  copyWith no-args, round-trip serialization, ensureAllSections with
  empty list and custom visibility preservation
- Widget: toggle switch updates visibility, hidden section has reduced
  opacity, reset-to-default menu, initial switch state, app bar title,
  section descriptions
- AppSettings: copyWith preserves when unset, defaults match static
  const, clearDiveDetailSections takes precedence

* test: expand coverage for dive detail section config

Add 20 more tests (49 -> 69 total) covering:
- Model: fromJson explicit false, single-item round-trip, tryFromJson
  success/missing-id/non-string-id, sectionsFromJson with map-JSON/
  non-map-list-items/all-unknown-IDs/mixed-types, ensureAllSections
  preserves custom visibility, every displayName/description value,
  unique displayName constraint
- Widget: toggle back on restores opacity, multiple independent
  toggles, overflow menu present, subheading text, custom initial
  section order via provider override
- AppSettings: sequential copyWith independence, empty list, custom
  constructor

* feat: localize dive detail section names and descriptions

Add localizedDisplayName() and localizedDescription() to DiveDetailSectionId
backed by ARB strings for all supported locales. Update settings and appearance pages to use localized section metadata. Add DiveDetailPage widget tests verifying section config visibility, ordering, and content rendering.

* test: expand dive detail page section config coverage

Add widget tests for dataSources, buddies, sightings, signatures, details, and all-sections-together rendering. Extract _buildTestWidget
extraOverrides parameter and _alwaysRenderOverrides helper for sections that require additional provider stubs.

* test: add appearance page tests and expand dive detail section config coverage

Add widget tests for AppearancePage verifying the Dive Details section header, tile content, and reorder icon. Extend section config tests with
dataSources, buddies, sightings, signatures, details, and all-sections rendering using extraOverrides for provider-dependent sections.

* fix: resolve merge conflicts with main after PR #95 merge

- Bump migration from v56 to v57 (PR #95 claimed v56 for the
  duration->bottom_time column rename)
- Regenerate Drift code against updated schema
- Add missing setDiveDetailSections/resetDiveDetailSections stubs
  to MockSettingsNotifier
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Mix Rounding, SAC Rate, Deco Calculations Import Issues from Subsurface Dive Details - SAC Rate calculations wrong by a factor of 2 or more

2 participants