Add travel gas and per-tank lost-gas contingency planning - #1127
Conversation
Lets a diver flag any carried cylinder (stage, deco, diluent, pony, sidemount, etc.) as travel gas -- breathed on the descent before switching to its primary role's gas -- and turns "what if I lose this cylinder" into an interactive, per-tank contingency preview instead of a static report. Domain / persistence: - DiveTank.isTravelGas: a new bool, independent of role (a stage or diluent cylinder can double as travel gas without changing what it's otherwise used for). Planning-only, mirroring the existing decoSwitchDepth field's precedent. - New dive_plan_tanks.is_travel_gas column, schema v152, with the idempotent onUpgrade + beforeOpen backstop pattern used throughout database.dart. No change needed to the logged-dive tanks table or to TankRole -- the column is additive and role stays untouched. - Round-tripped through DivePlanRepository and the .subplan JSON codec. Contingency engine: - ContingencyService.isLosable(tank): true for deco/stage tanks (as before) or any tank flagged isTravelGas, regardless of role -- so a diluent or pony bottle used as travel gas now also gets a lost-gas schedule, which previously only fired for deco/stage roles. - ContingencyService.lostGasFor(plan, tankId): a single-tank sibling to lostGas()/deviationFor(), letting callers price just the one variant a diver selects instead of recomputing every carried cylinder. - LostGasOutcome now carries the remapped plan (not just the outcome), matching DeviationOutcome, so downstream consumers (chart ghost) draw the actual post-loss gas switches instead of the original ones. Planner UI: - Tank editor: "Also used as travel gas" checkbox, independent of the role dropdown. - Results sheet: every deviation and lost-gas row is now individually collapsible (a chevron toggles just that row) and tappable (a chip toggles it as the previewed contingency), replacing the previous all-or-nothing, non-interactive lost-gas table dump. - Fixed a bug where selecting a contingency only ghosted the chart -- the headline stats (Runtime/TTS/NDL/Deco/CNS) kept showing the base plan's numbers even though losing a deco/stage/travel gas changes the whole schedule. selectedContingencyProvider is now the single source both the chart ghost and activePlanOutcomeProvider read from, so headline stats and the chart stay in sync with whichever contingency (a depth/time deviation or a lost cylinder) is selected. - A "Previewing: X" chip (mirroring the existing "Following" chip) makes clear the headline numbers are a what-if, with a tap to clear back to the live plan. - Selecting a deviation and marking a tank lost are mutually exclusive (only one preview/ghost at a time). Tests: DB migration (v152), DivePlanRepository and .subplan codec round-trip, ContingencyService eligibility/lost-gas coverage, and a widget test covering select/deselect, collapse, and the deviation/lost-gas mutual exclusion. Signed-off-by: Alon Dotan <alon1.dotan@gmail.com>
|
📦 Build artifacts for this PR · commit
Artifacts expire in 7 days. Downloading requires being signed in to GitHub. macOS needs two extractions: unzip the downloaded artifact, then unzip the Updated automatically on each push. |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
…test coverage CI (Test shard 2) was failing on test/l10n/arb_parity_test.dart: the two new keys from the travel-gas/lost-gas-contingency feature (divePlanner_field_travelGas, plannerCanvas_contingency_previewing) were only added to app_en.arb, not to the ten other locale files, which the parity test requires. Added translations for both keys to app_ar/de/es/fr/he/hu/it/nl/pt/zh.arb and regenerated the localizations. Also addressed the Codecov patch-coverage comment on the same PR by adding tests for the previously-untested new code: - ContingencyPreviewChip (plan_status_chips.dart): new widget test file coverage for the no-selection, deviation-preview (all three deviation labels), and lost-gas-preview cases, plus the clear-on-tap behavior. - Selecting/collapsing a deviation row from within the results sheet's Contingencies section itself (plan_results_sheet.dart's toggleDeviation was only ever exercised via the separate ContingencyChips widget). - The "Also used as travel gas" checkbox in the tank editor (plan_tank_list.dart): saving it and displaying it when re-editing. - ContingencyService.lostGasFor's fallback branch when no back-gas tank remains among the carried cylinders. All 417 planner/dive_planner/l10n tests and the v153 migration test pass; flutter analyze is clean. Signed-off-by: Alon Dotan <alon1.dotan@gmail.com>
There was a problem hiding this comment.
Pull request overview
Adds “travel gas” as a per-tank planning flag and upgrades contingency planning so divers can preview a single selected deviation or a single lost-cylinder variant (including travel-gas cylinders), keeping headline stats and chart ghost in sync.
Changes:
- Add
DiveTank.isTravelGasend-to-end (DB schema + repository + file codec + UI editor + i18n). - Expand contingency engine/UI to support per-row selection, collapse state, and single-variant computation (
lostGasFor+selectedContingencyProvider+activePlanOutcomeProvider). - Add/extend tests for DB migration, repository/codec round-trip, contingency eligibility, and widget interaction.
Reviewed changes
Copilot reviewed 43 out of 43 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| test/features/planner/plan_results_widgets_test.dart | Widget coverage for “Previewing” chip behavior and clearing selection |
| test/features/planner/plan_file_codec_test.dart | Validates .subplan JSON round-trip includes isTravelGas |
| test/features/planner/dive_plan_repository_test.dart | Validates DB persistence/restore of isTravelGas |
| test/features/planner/contingency_ui_test.dart | Exercises select/deselect + collapse + mutual exclusion in results UI |
| test/features/planner/contingency_service_test.dart | Covers eligibility/lost-gas generation for travel-flagged tanks |
| test/features/dive_planner/presentation/widgets/plan_tank_list_test.dart | Verifies travel-gas checkbox saves and rehydrates in editor |
| test/core/database/migration_v153_travel_gas_test.dart | Migration ladder + column existence/backstop assertions |
| lib/l10n/arb/app_en.arb | Adds travel-gas field + “Previewing” contingency string (source ARB) |
| lib/l10n/arb/app_de.arb | Adds translations for travel-gas field + “Previewing” |
| lib/l10n/arb/app_ar.arb | Adds translations for travel-gas field + “Previewing” |
| lib/l10n/arb/app_es.arb | Adds translations for travel-gas field + “Previewing” |
| lib/l10n/arb/app_fr.arb | Adds translations for travel-gas field + “Previewing” |
| lib/l10n/arb/app_he.arb | Adds translations for travel-gas field + “Previewing” |
| lib/l10n/arb/app_hu.arb | Adds translations for travel-gas field + “Previewing” |
| lib/l10n/arb/app_it.arb | Adds translations for travel-gas field + “Previewing” |
| lib/l10n/arb/app_nl.arb | Adds translations for travel-gas field + “Previewing” |
| lib/l10n/arb/app_pt.arb | Adds translations for travel-gas field + “Previewing” |
| lib/l10n/arb/app_zh.arb | Adds translations for travel-gas field + “Previewing” |
| lib/l10n/arb/app_localizations.dart | Regenerates localization API with new keys |
| lib/l10n/arb/app_localizations_en.dart | Regenerates English localization implementation |
| lib/l10n/arb/app_localizations_de.dart | Regenerates German localization implementation |
| lib/l10n/arb/app_localizations_ar.dart | Regenerates Arabic localization implementation |
| lib/l10n/arb/app_localizations_es.dart | Regenerates Spanish localization implementation |
| lib/l10n/arb/app_localizations_fr.dart | Regenerates French localization implementation |
| lib/l10n/arb/app_localizations_he.dart | Regenerates Hebrew localization implementation |
| lib/l10n/arb/app_localizations_hu.dart | Regenerates Hungarian localization implementation |
| lib/l10n/arb/app_localizations_it.dart | Regenerates Italian localization implementation |
| lib/l10n/arb/app_localizations_nl.dart | Regenerates Dutch localization implementation |
| lib/l10n/arb/app_localizations_pt.dart | Regenerates Portuguese localization implementation |
| lib/l10n/arb/app_localizations_zh.dart | Regenerates Chinese localization implementation |
| lib/features/planner/presentation/widgets/plan_status_chips.dart | Adds preview chip + switches headline stats to activePlanOutcomeProvider |
| lib/features/planner/presentation/widgets/plan_results_sheet.dart | Per-row collapsible/selectable contingency section + activePlanOutcomeProvider |
| lib/features/planner/presentation/widgets/plan_chart_readouts.dart | Switches readouts to activePlanOutcomeProvider |
| lib/features/planner/presentation/widgets/contingency_chips.dart | Enforces deviation/lost-gas mutual exclusion |
| lib/features/planner/presentation/providers/plan_canvas_providers.dart | Adds selection/collapse providers + selectedContingencyProvider/activePlanOutcomeProvider |
| lib/features/planner/presentation/panes/plan_results_pane.dart | Switches pane stats to activePlanOutcomeProvider |
| lib/features/planner/presentation/chart/plan_profile_chart.dart | Uses new contingency ghost series provider |
| lib/features/planner/domain/services/contingency_service.dart | Adds travel-gas eligibility + lostGasFor + carries remapped plan in outcome |
| lib/features/planner/data/services/plan_file_codec.dart | Persists isTravelGas in .subplan JSON |
| lib/features/planner/data/repositories/dive_plan_repository.dart | Persists isTravelGas in DB and hydrates it on load |
| lib/features/dive_planner/presentation/widgets/plan_tank_list.dart | Adds “Also used as travel gas” checkbox and saves to DiveTank |
| lib/features/dive_log/domain/entities/dive.dart | Adds DiveTank.isTravelGas field + copyWith/props updates |
| lib/core/database/database.dart | Adds dive_plan_tanks.is_travel_gas column + schema bump + upgrade/backstop |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
main shipped two migrations from parallel branches while this PR was open: v153 (raw O2 cell millivolts, issue submersion-app#810) and v154 (site entry/exit method, issue submersion-app#1104). Both landed on the version number this branch had claimed, so every conflict in database.dart is the same schema-ladder collision. Resolved by keeping both sides and moving the travel-gas column to v155: currentSchemaVersion, the migrationVersions ladder entry, the onUpgrade block, and the beforeOpen backstop. The column is additive and defaulted, so minimumCompatibleSchemaVersion stays at main's 137. Renumbering matters beyond cosmetics: a database that already ran main's v153 would never re-enter a v153 upgrade block, and would have been left without is_travel_gas. The beforeOpen backstop covers exactly that case, but the ladder should not be relying on it. Also renames migration_v153_travel_gas_test.dart to v155 to match, and tightens its "defaults false" assertion to evaluate the declared SQL default rather than only checking NOT NULL (review feedback) -- verified by mutation: flipping the backstop's DEFAULT to 1 now fails the test.
…ubmersion-app#1127) Two review findings on the contingency results UI. ContingencyPreviewChip gated on the raw selection ids, but a selection can outlive the contingency it names: lostGasFor returns null when the tank was removed, stopped being losable (its role changed, or its travel-gas flag was cleared), or is the last cylinder carried. In those cases the headline stats fall back to the live plan while the chip still announced "Previewing: Lost --", so the chip claimed a what-if that was not in effect. It now watches selectedContingencyProvider, the same provider the stats read, and that provider carries the lost tank so the label cannot name one the preview did not actually drop. The collapse chevron was a bare InkWell around a 20px icon, giving it a 20x20 hit area. It is now an IconButton at the app's established compact size: theme-default padded tap target (kMinInteractiveDimension, 48) trimmed by VisualDensity.compact to 40x40, the same treatment the dive-detail header icons use. The row's top padding comes out to compensate, since the button now carries its own vertical slack. Tests: a stale lost-gas selection renders no chip and leaves activePlanOutcomeProvider identical to planOutcomeProvider, plus a size assertion on the chevron so the hit area cannot silently shrink again.
Second ladder collision on this branch. PR submersion-app#1157 (selectable gas model, issue submersion-app#828) also renumbered itself to v155 and merged first, so the number this branch took in the previous merge is gone again. Travel gas moves to v156: currentSchemaVersion, the migrationVersions entry, the onUpgrade block, and the beforeOpen backstop, plus the migration test file. Worth recording how this one presented, because it is the dangerous shape: currentSchemaVersion did NOT conflict. Both branches had independently written the literal 155, so git auto-merged that line clean and only the three surrounding blocks carried markers. Resolving just the marked conflicts would have left the scalar at 155 with two separate migrations claiming it, which compiles, analyzes, and passes most tests. After any schema merge, re-read the scalar itself rather than trusting the absence of a marker. Receipt: git diff origin/main -- lib/core/database/database.dart now contains only the travel-gas additions, nothing of main's gas model work.
Second collision in one day: #1127 (travel gas) landed on main and took 156, the number this branch had just renumbered to. The migration moves to 157. The dangerous part was what did NOT conflict. Both sides had independently written `currentSchemaVersion = 156`, so git auto-merged that line with no marker while the two 156s meant different migrations; only the comment blocks around the ladder entries conflicted. The scalar had to be re-grepped and bumped by hand. Ladder audited afterwards: contiguous through 157, every block paired with a reportProgress, every new helper carried into beforeOpen. Migration test renamed to migration_v157_service_cost_test.dart with its fixture stamped at 156.
Summary
Lets a diver flag any carried cylinder (stage, deco, diluent, pony, sidemount, etc.) as travel gas -- breathed on the descent before switching to its primary role's gas -- and turns "what if I lose this cylinder" into an interactive, per-tank contingency preview instead of a static report.
Domain / persistence:
Contingency engine:
Planner UI:
Tests: DB migration (v156), DivePlanRepository and .subplan codec round-trip, ContingencyService eligibility/lost-gas coverage, and a widget test covering select/deselect, collapse, and the deviation/lost-gas mutual exclusion.
Fixes #1126
Test Plan
flutter testpassesflutter analyzepassesScreenshots
Before: