Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
587 changes: 587 additions & 0 deletions docs/superpowers/plans/2026-07-05-dive-planner-phase2-domain.md

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions lib/core/data/repositories/sync_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ class SyncRepository {
'checklistTemplates': (table: 'checklist_templates', pk: 'id'),
'checklistTemplateItems': (table: 'checklist_template_items', pk: 'id'),
'tripChecklistItems': (table: 'trip_checklist_items', pk: 'id'),
'divePlans': (table: 'dive_plans', pk: 'id'),
'divePlanTanks': (table: 'dive_plan_tanks', pk: 'id'),
'divePlanSegments': (table: 'dive_plan_segments', pk: 'id'),
'equipment': (table: 'equipment', pk: 'id'),
'equipmentSets': (table: 'equipment_sets', pk: 'id'),
'diveTypes': (table: 'dive_types', pk: 'id'),
Expand Down
170 changes: 169 additions & 1 deletion lib/core/database/database.dart
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,137 @@ class TripChecklistItems extends Table {
// coverage:ignore-end
}

/// Saved dive plans (dive planner redesign, Phase 2)
class DivePlans extends Table {
// coverage:ignore-start
TextColumn get id => text()();
TextColumn get diverId => text().nullable().references(Divers, #id)();
TextColumn get name => text()();
TextColumn get notes => text().withDefault(const Constant(''))();

/// PlanMode enum name: 'oc' | 'ccr'.
TextColumn get mode => text().withDefault(const Constant('oc'))();
TextColumn get siteId => text().nullable().references(DiveSites, #id)();

/// Tissue-seeding source dive (repetitive planning, Phase 6).
TextColumn get sourceDiveId => text().nullable().references(Dives, #id)();

/// Executed dive this plan is linked to (plan-vs-actual, Phase 6).
TextColumn get linkedDiveId => text().nullable().references(Dives, #id)();
RealColumn get altitude => real().nullable()();

/// WaterType enum name; null = unspecified (EN13319 density).
TextColumn get waterType => text().nullable()();
IntColumn get gfLow => integer()();
IntColumn get gfHigh => integer()();
RealColumn get descentRate => real().withDefault(const Constant(18.0))();
RealColumn get ascentRate => real().withDefault(const Constant(9.0))();
RealColumn get lastStopDepth => real().withDefault(const Constant(3.0))();
IntColumn get gasSwitchStopSeconds =>
integer().withDefault(const Constant(0))();

/// Air-break policy; both null = no air breaks.
IntColumn get airBreakO2Seconds => integer().nullable()();
IntColumn get airBreakBreakSeconds => integer().nullable()();
RealColumn get sacBottom => real().withDefault(const Constant(15.0))();

/// Null = derive 0.8x / 2.5x of sacBottom.
RealColumn get sacDeco => real().nullable()();
RealColumn get sacStressed => real().nullable()();
RealColumn get reservePressure => real().withDefault(const Constant(50.0))();
IntColumn get surfaceIntervalSeconds => integer().nullable()();

/// CCR setpoints (Phase 4 UI; persisted now to avoid a later migration).
RealColumn get setpointLow => real().nullable()();
RealColumn get setpointHigh => real().nullable()();
RealColumn get setpointSwitchDepth => real().nullable()();

/// Contingency config (Phase 5 UI).
RealColumn get deviationDepthDelta =>
real().withDefault(const Constant(5.0))();
IntColumn get deviationTimeMinutes =>
integer().withDefault(const Constant(5))();

/// TurnPressureRule enum name; null = none.
TextColumn get turnPressureRule => text().nullable()();
RealColumn get turnPressureFraction => real().nullable()();

/// Denormalized list-display summary (no engine run per list row).
RealColumn get summaryMaxDepth => real().nullable()();
IntColumn get summaryRuntimeSeconds => integer().nullable()();
IntColumn get summaryTtsSeconds => integer().nullable()();
IntColumn get createdAt => integer()();
IntColumn get updatedAt => integer()();

/// Hybrid Logical Clock for cross-device conflict resolution
/// (nullable: rows written before HLC rollout fall back to updatedAt).
TextColumn get hlc => text().nullable()();

@override
Set<Column> get primaryKey => {id};
// coverage:ignore-end
}

/// Tanks carried on a saved dive plan
class DivePlanTanks extends Table {
// coverage:ignore-start
TextColumn get id => text()();
TextColumn get planId => text().references(DivePlans, #id)();
TextColumn get name => text().nullable()();
RealColumn get volume => real().nullable()();
RealColumn get workingPressure => real().nullable()();
RealColumn get startPressure => real().nullable()();
RealColumn get gasO2 => real().withDefault(const Constant(21.0))();
RealColumn get gasHe => real().withDefault(const Constant(0.0))();

/// TankRole enum name.
TextColumn get role => text().withDefault(const Constant('backGas'))();

/// TankMaterial enum name; null = unspecified.
TextColumn get material => text().nullable()();
TextColumn get presetName => text().nullable()();
IntColumn get sortOrder => integer().withDefault(const Constant(0))();
IntColumn get createdAt => integer()();
IntColumn get updatedAt => integer()();

/// Hybrid Logical Clock for cross-device conflict resolution
/// (nullable: rows written before HLC rollout fall back to updatedAt).
TextColumn get hlc => text().nullable()();

@override
Set<Column> get primaryKey => {id};
// coverage:ignore-end
}

/// User-authored segments (the bottom portion) of a saved dive plan
class DivePlanSegments extends Table {
// coverage:ignore-start
TextColumn get id => text()();
TextColumn get planId => text().references(DivePlans, #id)();

/// SegmentType enum name.
TextColumn get type => text()();
RealColumn get startDepth => real()();
RealColumn get endDepth => real()();
IntColumn get durationSeconds => integer()();
TextColumn get tankId => text().references(DivePlanTanks, #id)();
RealColumn get gasO2 => real()();
RealColumn get gasHe => real()();
RealColumn get rate => real().nullable()();
TextColumn get switchToTankId => text().nullable()();
IntColumn get sortOrder => integer().withDefault(const Constant(0))();
IntColumn get createdAt => integer()();
IntColumn get updatedAt => integer()();

/// Hybrid Logical Clock for cross-device conflict resolution
/// (nullable: rows written before HLC rollout fall back to updatedAt).
TextColumn get hlc => text().nullable()();

@override
Set<Column> get primaryKey => {id};
// coverage:ignore-end
}

/// Dive log entries
class Dives extends Table {
TextColumn get id => text()();
Expand Down Expand Up @@ -1825,6 +1956,10 @@ class FieldPresets extends Table {
ChecklistTemplates,
ChecklistTemplateItems,
TripChecklistItems,
// Saved dive plans (planner redesign Phase 2)
DivePlans,
DivePlanTanks,
DivePlanSegments,
// CSV import presets (local-only)
CsvPresets,
// Column view configuration
Expand All @@ -1844,7 +1979,7 @@ class AppDatabase extends _$AppDatabase {

/// The current schema version as a static constant so that pre-open checks
/// (e.g. version-mismatch guard) can reference it without an instance.
static const int currentSchemaVersion = 99;
static const int currentSchemaVersion = 100;

/// Every schema version that has a migration block in onUpgrade.
/// Used to calculate progress step counts. When adding a new migration,
Expand Down Expand Up @@ -1947,6 +2082,7 @@ class AppDatabase extends _$AppDatabase {
97,
98,
99,
100,
];

/// Tables that carry a per-row Hybrid Logical Clock for cross-device conflict
Expand Down Expand Up @@ -4622,6 +4758,24 @@ class AppDatabase extends _$AppDatabase {
await m.createTable(buddyRoles);
}
if (from < 99) await reportProgress();
if (from < 100) {
// Saved dive plans (planner redesign Phase 2): three synced tables.
// createTable is IF NOT EXISTS and the indexes are guarded, so this
// block is idempotent; the beforeOpen backstop re-asserts the same
// objects against schema-version collisions.
await m.createTable(divePlans);
await m.createTable(divePlanTanks);
await m.createTable(divePlanSegments);
await customStatement('''
CREATE INDEX IF NOT EXISTS idx_dive_plan_tanks_plan_id
ON dive_plan_tanks(plan_id)
''');
await customStatement('''
CREATE INDEX IF NOT EXISTS idx_dive_plan_segments_plan_id
ON dive_plan_segments(plan_id)
''');
}
if (from < 100) await reportProgress();
},
beforeOpen: (details) async {
// Enable foreign keys
Expand All @@ -4647,6 +4801,20 @@ class AppDatabase extends _$AppDatabase {
);
}
await createMigrator().createTable(buddyRoles);

// v100 backstop: re-assert the dive plan tables and their indexes
// (same collision disease; all DDL idempotent).
await createMigrator().createTable(divePlans);
await createMigrator().createTable(divePlanTanks);
await createMigrator().createTable(divePlanSegments);
await customStatement('''
CREATE INDEX IF NOT EXISTS idx_dive_plan_tanks_plan_id
ON dive_plan_tanks(plan_id)
''');
await customStatement('''
CREATE INDEX IF NOT EXISTS idx_dive_plan_segments_plan_id
ON dive_plan_segments(plan_id)
''');
},
);
}
Expand Down
34 changes: 34 additions & 0 deletions lib/core/deco/gas_density.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/// Gas density at depth (work-of-breathing limit checks).
///
/// High breathing-gas density increases CO2 retention risk. Community
/// guidance (Anthony & Mitchell): keep density at or below 5.2 g/L; 6.2 g/L
/// is the hard ceiling.
library;

/// Recommended maximum gas density (g/L).
const double gasDensityWarnGPerL = 5.2;

/// Hard maximum gas density (g/L).
const double gasDensityCriticalGPerL = 6.2;

/// Molecular weights (g/mol) and molar volume shared with the dive-details
/// density curve — keep these in lockstep with any display of density.
const double _o2MolWeight = 32.0;
const double _n2MolWeight = 28.0;
const double _heMolWeight = 4.0;
const double _molarVolumeLPerMol = 24.04; // L/mol at STP

/// Density in g/L of a breathing gas at [ambientPressureBar].
///
/// Formula: ambient x sum(fraction x molecular weight) / 24.04, with
/// fN2 = 1 - fO2 - fHe.
double gasDensityGPerL({
required double fO2,
required double fHe,
required double ambientPressureBar,
}) {
final fN2 = 1.0 - fO2 - fHe;
final avgMolWeight =
(fO2 * _o2MolWeight) + (fN2 * _n2MolWeight) + (fHe * _heMolWeight);
return avgMolWeight / _molarVolumeLPerMol * ambientPressureBar;
}
Loading