From 0a8cb52c94791e374f4e995d7ce4f32c91aff6ea Mon Sep 17 00:00:00 2001 From: Mehul Date: Wed, 14 Oct 2026 21:48:54 +0530 Subject: [PATCH 1/3] Keep the borewell's own water level when the first later reading is added Adding a water reading made the newest reading the borewell's current level, so the level recorded with the borewell (form or Excel) was overwritten and the first comparison over time was lost. add_water_reading now saves that level as the first reading (dated with the borewell's date) before adding the new one, unless the new reading is on the same date. Migration 5 does the same once for borewells already saved with no readings. --- CHANGELOG.md | 4 ++ core/db/src/repo/layers.rs | 12 ++++ core/db/src/schema.rs | 9 +++ core/db/tests/database.rs | 116 +++++++++++++++++++++++++++++++++++-- 4 files changed, 137 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f34247b..b480bef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,10 @@ for a version is copied into its GitHub release notes, so write it for the peopl are no longer offered. What is stored is never changed. ### Changed +- **Adding a later water reading no longer loses the water level recorded with the borewell.** + That level is now kept as the borewell's first reading, dated with the borewell's date, so the + change in water level over time can be seen. Borewells already saved are updated the first time + this version opens the database. - **A title such as "STRETA CHART OF TUBE WELL" above the chart no longer stops a log from being read.** Such logs needed their columns chosen by hand before. diff --git a/core/db/src/repo/layers.rs b/core/db/src/repo/layers.rs index a532439..6a3769e 100644 --- a/core/db/src/repo/layers.rs +++ b/core/db/src/repo/layers.rs @@ -231,6 +231,9 @@ pub fn water_for(conn: &Connection, borewell_id: &str) -> Result>>()?) } +/// Source of a reading made from the water level written on the borewell itself. +const DRILLING_READING_SOURCE: &str = "Water level recorded with the borewell"; + /// Adds a dated water reading. The borewell's current water level follows its newest reading. pub fn add_water_reading( conn: &Connection, @@ -253,6 +256,15 @@ pub fn add_water_reading( { return Err(DbError::Invalid("Water level cannot be negative.".into())); } + // The level written on the borewell itself is its first reading; keep it before a later + // reading takes its place as the current level. Migration 5 did this for older borewells. + conn.execute( + "INSERT INTO water_readings (id, borewell_id, measured_on, static_level, dynamic_level, source) + SELECT ?2, id, date, water_level, dynamic_water_level, ?3 FROM borewells + WHERE id = ?1 AND water_level IS NOT NULL AND trim(date) <> '' AND trim(date) <> ?4 + AND NOT EXISTS (SELECT 1 FROM water_readings WHERE borewell_id = ?1)", + params![borewell_id, new_id(), DRILLING_READING_SOURCE, reading.measured_on.trim()], + )?; let id = new_id(); conn.execute( "INSERT INTO water_readings (id, borewell_id, measured_on, static_level, dynamic_level, source, remarks) diff --git a/core/db/src/schema.rs b/core/db/src/schema.rs index fa53327..a6c7acc 100644 --- a/core/db/src/schema.rs +++ b/core/db/src/schema.rs @@ -199,6 +199,15 @@ pub const MIGRATIONS: &[&str] = &[ ALTER TABLE borewells ADD COLUMN column_pipe_dia REAL; ALTER TABLE borewells ADD COLUMN column_pipe_material TEXT NOT NULL DEFAULT ''; "#, + // 5: the water level written on a borewell becomes its first reading, so a later reading + // no longer replaces it. Only for borewells with no readings yet (see add_water_reading). + r#" + INSERT INTO water_readings (id, borewell_id, measured_on, static_level, dynamic_level, source) + SELECT lower(hex(randomblob(16))), id, date, water_level, dynamic_water_level, 'Water level recorded with the borewell' + FROM borewells b + WHERE water_level IS NOT NULL AND trim(date) <> '' + AND NOT EXISTS (SELECT 1 FROM water_readings w WHERE w.borewell_id = b.id); + "#, ]; /// The schema version this build understands. diff --git a/core/db/tests/database.rs b/core/db/tests/database.rs index 9573f5e..a11e4dc 100644 --- a/core/db/tests/database.rs +++ b/core/db/tests/database.rs @@ -488,12 +488,12 @@ fn pipes_and_water_readings() { static_level: Some(s), ..Default::default() }; - db.with_tx(|tx| layers::add_water_reading(tx, &b.id, &reading("2026-03-01", 104.0))) + db.with_tx(|tx| layers::add_water_reading(tx, &b.id, &reading("2026-09-01", 104.0))) .unwrap(); let newest = db - .with_tx(|tx| layers::add_water_reading(tx, &b.id, &reading("2026-06-01", 118.0))) + .with_tx(|tx| layers::add_water_reading(tx, &b.id, &reading("2026-10-01", 118.0))) .unwrap(); - db.with_tx(|tx| layers::add_water_reading(tx, &b.id, &reading("2025-11-01", 99.0))) + db.with_tx(|tx| layers::add_water_reading(tx, &b.id, &reading("2026-08-20", 99.0))) .unwrap(); assert_eq!( db.with(|c| borewells::get(c, &b.id)).unwrap().water_level, @@ -507,7 +507,115 @@ fn pipes_and_water_readings() { db.with(|c| borewells::get(c, &b.id)).unwrap().water_level, Some(104.0) ); - assert_eq!(db.with(|c| layers::water_for(c, &b.id)).unwrap().len(), 2); + assert_eq!( + db.with(|c| layers::water_for(c, &b.id)).unwrap().len(), + 3, + "the level recorded with the borewell is kept as a reading" + ); +} + +fn levels(db: &strata_db::Database, id: &str) -> Vec<(String, Option)> { + let mut v: Vec<_> = db + .with(|c| layers::water_for(c, id)) + .unwrap() + .into_iter() + .map(|r| (r.measured_on, r.static_level)) + .collect(); + v.sort_by(|a, b| a.0.cmp(&b.0)); + v +} + +#[test] +fn the_first_later_reading_keeps_the_level_recorded_with_the_borewell() { + let (_dir, db) = open_temp(); + let b = db + .with_tx(|tx| borewells::create(tx, &input("BW-W1"))) + .unwrap(); + let later = WaterReading { + measured_on: "2027-08-01".into(), + static_level: Some(125.0), + ..Default::default() + }; + db.with_tx(|tx| layers::add_water_reading(tx, &b.id, &later)) + .unwrap(); + assert_eq!( + levels(&db, &b.id), + vec![ + ("2026-08-08".into(), Some(110.0)), + ("2027-08-01".into(), Some(125.0)) + ] + ); + assert_eq!( + db.with(|c| borewells::get(c, &b.id)).unwrap().water_level, + Some(125.0) + ); + + // A second reading does not copy the borewell's (now current) level again. + let next = WaterReading { + measured_on: "2028-08-01".into(), + ..later + }; + db.with_tx(|tx| layers::add_water_reading(tx, &b.id, &next)) + .unwrap(); + assert_eq!(levels(&db, &b.id).len(), 3); +} + +#[test] +fn a_reading_on_the_borewells_own_date_is_not_doubled() { + let (_dir, db) = open_temp(); + let b = db + .with_tx(|tx| borewells::create(tx, &input("BW-W2"))) + .unwrap(); + let same_day = WaterReading { + measured_on: "2026-08-08".into(), + static_level: Some(112.0), + ..Default::default() + }; + db.with_tx(|tx| layers::add_water_reading(tx, &b.id, &same_day)) + .unwrap(); + assert_eq!(levels(&db, &b.id), vec![("2026-08-08".into(), Some(112.0))]); +} + +#[test] +fn upgrading_turns_a_borewells_own_water_level_into_its_first_reading() { + let dir = tempfile::tempdir().unwrap(); + let (plain, with_reading, no_level) = { + let (db, _) = Database::open(dir.path()).unwrap(); + let plain = db + .with_tx(|tx| borewells::create(tx, &input("BW-M1"))) + .unwrap(); + let with_reading = db + .with_tx(|tx| borewells::create(tx, &input("BW-M2"))) + .unwrap(); + let mut dry = input("BW-M3"); + dry.water_level = None; + let no_level = db.with_tx(|tx| borewells::create(tx, &dry)).unwrap(); + db.with(|c| { + c.execute( + "INSERT INTO water_readings (id, borewell_id, measured_on, static_level) VALUES ('w1', ?1, '2027-01-01', 120)", + [&with_reading.id], + )?; + Ok(c.execute_batch("PRAGMA user_version = 4")?) + }) + .unwrap(); + (plain.id, with_reading.id, no_level.id) + }; + let (db, report) = Database::open(dir.path()).unwrap(); + assert_eq!(report.previous_version, 4); + assert_eq!( + levels(&db, &plain), + vec![("2026-08-08".into(), Some(110.0))] + ); + assert_eq!( + levels(&db, &with_reading), + vec![("2027-01-01".into(), Some(120.0))] + ); + assert!(levels(&db, &no_level).is_empty()); + + // Running the same step again adds nothing. + db.with(|c| Ok(c.execute_batch(schema::MIGRATIONS[4])?)) + .unwrap(); + assert_eq!(levels(&db, &plain).len(), 1); } #[test] From 36d95f710123f3799ea643c2bd9a22d27c1b4986 Mon Sep 17 00:00:00 2001 From: Mehul Date: Wed, 14 Oct 2026 21:55:23 +0530 Subject: [PATCH 2/3] Fix CI: e2e reading dated after the borewell, rustls 0.23.46 The e2e reading was dated before the borewell's own date (today), so the kept drilling-time level was rightly the newest. Date it next year and check both levels are kept. Bump rustls for RUSTSEC-2026-0346..0349, published today. --- Cargo.lock | 4 ++-- e2e/run.mjs | 9 ++++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 32d646e..eb192cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3180,9 +3180,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.45" +version = "0.23.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" +checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf" dependencies = [ "once_cell", "ring", diff --git a/e2e/run.mjs b/e2e/run.mjs index ad3a78f..471137a 100644 --- a/e2e/run.mjs +++ b/e2e/run.mjs @@ -357,9 +357,12 @@ try { check("Files attach", attached.files.includes("site-notes.pdf"), attached.files.join(", ")); const water = await page(` - await __t.invoke('water_reading_add', { borewellId: '${manual.bwid}', reading: { measuredOn: '2026-09-29', staticLevel: 47 } }); - return (await __t.invoke('borewell_get', { id: '${manual.bwid}' })).borewell.waterLevel;`); - check("Water readings over time: the latest sets the water level", water === 47, String(water)); + // A year after the borewell's own date, so it is the newest reading. + const later = (Number(new Date().toISOString().slice(0, 4)) + 1) + '-01-15'; + await __t.invoke('water_reading_add', { borewellId: '${manual.bwid}', reading: { measuredOn: later, staticLevel: 47 } }); + const r = await __t.invoke('borewell_get', { id: '${manual.bwid}' }); + return { level: r.borewell.waterLevel, readings: r.waterReadings.map(w => w.staticLevel).sort((a, b) => a - b).join(',') };`); + check("Water readings over time: the latest sets the water level, the first level is kept", water.level === 47 && water.readings === "45,47", JSON.stringify(water)); // ── Finding borewells ────────────────────────────────────────────────── const search = await page(` From d7258e46754f9a5c125db68785932a537ace3bd0 Mon Sep 17 00:00:00 2001 From: Mehul Date: Wed, 14 Oct 2026 22:11:22 +0530 Subject: [PATCH 3/3] e2e: date the later reading tomorrow, not next year A reading dated next year moved the map's three-year window. Accept any year in the map's 'readings since' note so a run on 31 December passes. --- e2e/run.mjs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/e2e/run.mjs b/e2e/run.mjs index 471137a..394a98a 100644 --- a/e2e/run.mjs +++ b/e2e/run.mjs @@ -357,8 +357,8 @@ try { check("Files attach", attached.files.includes("site-notes.pdf"), attached.files.join(", ")); const water = await page(` - // A year after the borewell's own date, so it is the newest reading. - const later = (Number(new Date().toISOString().slice(0, 4)) + 1) + '-01-15'; + // The day after the borewell's own date (today), so it is the newest reading. + const later = new Date(Date.now() + 86400000).toISOString().slice(0, 10); await __t.invoke('water_reading_add', { borewellId: '${manual.bwid}', reading: { measuredOn: later, staticLevel: 47 } }); const r = await __t.invoke('borewell_get', { id: '${manual.bwid}' }); return { level: r.borewell.waterLevel, readings: r.waterReadings.map(w => w.staticLevel).sort((a, b) => a - b).join(',') };`); @@ -408,7 +408,7 @@ try { __t.btn('SPOT-2016', popup).click(); const card = await __t.until(() => document.querySelector('[role="dialog"][aria-label="SPOT-2016 details"]'), 5000).catch(() => null); // Which readings count: the last 3 years by default (the 2016 one is left out), or all of them. - const since = () => /going by readings since 2023/.test(__t.text()); + const since = () => /going by readings since \\d{4}/.test(__t.text()); const byDefault = since(); await __t.choose(document.querySelector('[aria-label="Which water readings to use"]'), 'All readings'); const all = !since();