Skip to content

Add travel gas and per-tank lost-gas contingency planning - #1127

Merged
ericgriffin merged 5 commits into
submersion-app:mainfrom
dotanalon:travel_gas
Aug 18, 2026
Merged

Add travel gas and per-tank lost-gas contingency planning#1127
ericgriffin merged 5 commits into
submersion-app:mainfrom
dotanalon:travel_gas

Conversation

@dotanalon

@dotanalon dotanalon commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

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:

  • 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 v156, 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 (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 test passes
  • flutter analyze passes
  • Manual testing on:

Screenshots

Before:

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>
@github-actions

github-actions Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

📦 Build artifacts for this PR · commit 6a5e16e

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.

@codecov

codecov Bot commented Aug 17, 2026

Copy link
Copy Markdown

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>

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

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.isTravelGas end-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.

Comment thread lib/features/planner/presentation/widgets/plan_status_chips.dart Outdated
Comment thread lib/features/planner/presentation/widgets/plan_results_sheet.dart Outdated
Comment thread test/core/database/migration_v155_travel_gas_test.dart
Comment thread lib/core/database/database.dart
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.
@ericgriffin
ericgriffin merged commit 7a578ed into submersion-app:main Aug 18, 2026
43 of 45 checks passed
@github-project-automation github-project-automation Bot moved this from In review to Done in Submersion Release Tracker Aug 18, 2026
ericgriffin added a commit that referenced this pull request Aug 18, 2026
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.
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.

missing lost gas / travel gas planning

3 participants