diff --git a/docs/session-logs/2026-10-30_barometer-ble-baldav.md b/docs/session-logs/2026-10-30_barometer-ble-baldav.md
deleted file mode 100644
index ce991ec..0000000
--- a/docs/session-logs/2026-10-30_barometer-ble-baldav.md
+++ /dev/null
@@ -1,51 +0,0 @@
-# 2026-10-30 - BLE-барометр: сторона baldav
-
-## Контекст
-Делаем самодельный датчик высоты: ESP32-C3 SuperMini + BMP280, вещает давление и
-температуру по BLE (стандартный Environmental Sensing). Железо заказано, ещё едет.
-Прошивка датчика - в `tools/baro-sensor/` (PlatformIO + Arduino-скетч), схема сборки -
-`tools/baro-sensor/baro-guide.html`. Эта задача - прочитать датчик в baldav и считать высоту.
-
-## Что сделано
-Читаем барометр тем же путём, что и пульсометр (Kotlin BLE-сервис -> файл -> Rust -> JS),
-новым `device_type = "baro"`. Диспетчер команд (`MainActivity`) не менялся - тип проходит насквозь.
-
-- `BleHrService.kt`: тип `baro` - подписка на ESS (`0x181A`): давление `0x2A6D` (uint32, 0.1 Па)
- и температура `0x2A6E` (int16, 0.01 C). Пишет `ble_baro_data.json` + `ble_baro_log.jsonl`.
- Все ветки барометра под `deviceType == "baro"`; HR/GPS-пути не тронуты.
-- `ble.rs`: структура `BaroReading`, определение ESS, команда `ble_get_baro`
- (Android читает файл; десктоп возвращает None). Зарегистрирована в `lib.rs`.
-- `frontend/js/baro.js`: ПАССИВНЫЙ модуль `window.BARO`. Гипсометрический пересчёт давления
- в высоту с реальной температурой датчика, якорь к высоте GPS на старте. Ничего не исполняется,
- пока не вызван `BARO.start()` - без барометра приложение работает как раньше.
-
-## Состояние
-Проверено компиляцией (сборка 0.1.538, без ошибок). На устройстве НЕ проверено - нет железа.
-
-## Продолжение того же дня: обвязка, UI, проверка эмулятором
-
-Железа ещё нет, поэтому эмулировали датчик на ПК (`tools/baro-sensor/emulate_baro.py` -
-BlueZ D-Bus GATT-сервер, вещает ESS `0x181A` с давлением/температурой), телефон читал его
-реальным путём. Проверено на устройстве: `BleHR: HR:70` и `BleBaro: Baro:... Pa` одновременно.
-
-Сделано:
-- `BleBaroService` - отдельный сервис/соединение для барометра, работает параллельно с
- пульсометром (один адаптер держит несколько LE-связей). Баро-подключения маршрутизируются
- в него из `MainActivity`. Авто-подключение при старте: баро из `baro_device.json`, пульс из
- `ble_device.json` (оба сами цепляются).
-- Rust: `ble_baro_disconnect`, `ble_save_baro_device`. JS сохраняет baro-устройство при привязке.
-- Настройки: секция "Датчики (BLE)" - пульсометр и барометр раздельно, свои слоты и кнопки,
- живой показ давления/температуры/высоты.
-- Трекинг: `altitude_baro_m` в точках, предпочитается в статистике высот (null без датчика).
-- Экран записи: "Высота" от барометра (обновление 1 Гц, пометка точкой), температура с датчика
- (белая, с десятой).
-- Починки по пути: directConnect при таймауте скана; Locale.US в JSON (была запятая от RU-локали);
- маршрутизация парсинга по характеристике (баро не писал в пульс); навигация после Стоп в VR/ЧСС
- (на список, а не в детали).
-
-## Осталось (по приходу платы)
-- Активировать модуль: `BARO.start()` при подключении baro-устройства, задать `BARO.getGpsAlt`.
-- Привязать высоту барометра к треку поездки (в точки / поправка высот П.5).
-- UI: показать барометрическую высоту; выбор baro-устройства в scan-UI уже работает (тип проходит).
-- Ограничение: сервис держит одно BLE-соединение, поэтому барометр и пульсометр одновременно
- пока нельзя - отдельный следующий шаг (две связи / второй сервис).
diff --git a/frontend/css/styles.css b/frontend/css/styles.css
index a4e21d9..276915a 100644
--- a/frontend/css/styles.css
+++ b/frontend/css/styles.css
@@ -751,12 +751,7 @@ select {
.full-width { width: 100%; }
/* HR-only workout (зарядка) recording screen: big heart rate, timer, kcal, no map. */
-.workout-screen { display: flex; flex-direction: column; height: 100%; }
-.wk-main { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px; padding: 16px; }
-.wk-panel { flex-shrink: 0; display: flex; flex-direction: column; padding: 10px 16px; padding-bottom: calc(10px + var(--safe-area-bottom, 0px)); }
-/* Workout detail (in a modal): fill the scrollable body so the delete button sits at the bottom. */
-.wkd-wrap { display: flex; flex-direction: column; min-height: 100%; }
-.wkd-del { margin-top: auto; flex: none; align-self: stretch; }
+.workout-screen { padding: 24px 16px; display: flex; flex-direction: column; align-items: center; gap: 8px; }
.wk-hr-big { font-size: 92px; font-weight: 700; line-height: 1; color: var(--primary); }
.wk-hr-lbl { color: var(--muted); margin-top: -4px; }
.wk-stats { display: flex; gap: 18px; margin: 14px 0; flex-wrap: wrap; justify-content: center; }
@@ -1465,7 +1460,7 @@ body.dark-map .poi-badge { background: #111; border-color: #eee; }
background: var(--surface);
}
.rec-topbar .rec-top { font-size: 27px; font-weight: 700; color: var(--on-surface); }
-.rec-topbar .rec-top-mid { color: var(--on-surface); }
+.rec-topbar .rec-top-mid { color: var(--muted); }
.rec-lbl {
font-size: 10px;
diff --git a/frontend/index.html b/frontend/index.html
index b13b9e1..f4550de 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -20,7 +20,6 @@
-
diff --git a/frontend/js/baro.js b/frontend/js/baro.js
deleted file mode 100644
index e99ffb0..0000000
--- a/frontend/js/baro.js
+++ /dev/null
@@ -1,116 +0,0 @@
-// Barometric altitude from the DIY BLE sensor "BaroBalDav" (ESP32-C3 + BMP280).
-// PASSIVE module: it only defines window.BARO. Nothing runs until BARO.start() is called, so with
-// no barometer connected the app behaves exactly as before. Activation (start on a "baro" connect,
-// plus the getGpsAlt hook) is wired in main.js only when a baro device is in use.
-//
-// How it works: the Kotlin BLE service writes the latest {pressure_pa, temp_c, ts} to a file; the
-// Rust command `ble_get_baro` returns it. We turn pressure into altitude with the hypsometric
-// formula using the sensor's own temperature, and anchor the absolute value to the GPS altitude at
-// ride start. The sensor is excellent for relative change (~1 m); weather drift of the absolute
-// baseline is handled by the GPS anchor (optionally re-anchored slowly).
-let BARO = {};
-(function () {
- let R = 287.05; // specific gas constant for dry air, J/(kg*K)
- let G = 9.80665; // standard gravity, m/s^2
-
- let state = {
- anchorPa: null, // reference pressure at the anchor point
- anchorAltM: null, // absolute altitude (m) at the anchor point, from GPS
- lastPa: null, // most recent pressure, Pa
- lastTempC: null, // most recent chip temperature, C (for the hypsometric term)
- lastTs: 0, // unix seconds of the last reading
- lastAltM: null, // computed absolute altitude at the last reading
- polling: false,
- timer: null,
- };
- BARO.state = state;
-
- // Height of a level at pressure `pa` above a level at pressure `refPa`, at air temperature tC.
- // Hypsometric formula: dh = (R*T/g) * ln(refPa/pa). Measured temperature is used instead of a
- // 15 C constant, so a hot or cold day does not bias the climb. Returns metres (positive = higher).
- function deltaH(refPa, pa, tC) {
- let T = (tC == null ? 15 : tC) + 273.15;
- return (R * T / G) * Math.log(refPa / pa);
- }
- BARO.deltaH = deltaH;
-
- // Pin the barometric altitude to a known absolute altitude (from GPS) at a given pressure.
- // Call once at ride start; may be re-called later to slowly correct weather drift.
- BARO.anchor = function (gpsAltM, pa) {
- if (pa == null) pa = state.lastPa;
- if (gpsAltM == null || pa == null) return false;
- state.anchorAltM = gpsAltM;
- state.anchorPa = pa;
- state.lastAltM = BARO.altitude();
- return true;
- };
-
- // Absolute altitude now = anchor altitude + barometric rise since the anchor. null until anchored.
- BARO.altitude = function () {
- if (state.anchorPa == null || state.lastPa == null) return null;
- return state.anchorAltM + deltaH(state.anchorPa, state.lastPa, state.lastTempC);
- };
-
- // Fresh reading within 15 s?
- BARO.isLive = function () {
- return state.lastTs > 0 && (Date.now() / 1000 - state.lastTs) < 15;
- };
-
- // Override this to provide the current GPS altitude (m) for auto-anchoring; return null if no fix.
- // Default: the altitude of the last recorded GPS point, if a recording is active.
- BARO.getGpsAlt = function () {
- try {
- let pts = window.GPS && GPS.recording && GPS.recording.points;
- if (pts && pts.length) {
- let p = pts[pts.length - 1];
- let a = p.altitude_srtm_m != null ? p.altitude_srtm_m : p.altitude;
- return a != null ? a : null;
- }
- } catch (e) {}
- return null;
- };
-
- async function tick() {
- let r = null;
- try { r = await window.__TAURI__.core.invoke("ble_get_baro"); } catch (e) {}
- if (r && r.pressure_pa > 0) {
- state.lastPa = r.pressure_pa;
- if (r.temp_c != null) state.lastTempC = r.temp_c;
- state.lastTs = r.timestamp || Math.floor(Date.now() / 1000);
- // auto-anchor on the first good reading, once GPS has an altitude to anchor to
- if (state.anchorPa == null) {
- let g = null;
- try { g = BARO.getGpsAlt(); } catch (e) {}
- if (g != null) BARO.anchor(g, state.lastPa);
- }
- state.lastAltM = BARO.altitude();
- }
- }
- BARO.tick = tick;
-
- // Begin polling the sensor (~1 Hz). No-op if already running. Call this only when a baro device
- // is connected, so the app does nothing extra when no barometer is in use.
- BARO.start = function () {
- if (state.polling) return;
- state.polling = true;
- let loop = async function () {
- if (!state.polling) return;
- await tick();
- if (state.polling) state.timer = setTimeout(loop, 1000); // 1 s poll interval (a real delay)
- };
- loop();
- };
-
- BARO.stop = function () {
- state.polling = false;
- if (state.timer) { clearTimeout(state.timer); state.timer = null; }
- };
-
- // Clear the anchor and readings (e.g. between rides) so the next ride re-anchors fresh.
- BARO.reset = function () {
- state.anchorPa = null; state.anchorAltM = null;
- state.lastPa = null; state.lastTempC = null; state.lastTs = 0; state.lastAltM = null;
- };
-
- window.BARO = BARO;
-})();
diff --git a/frontend/js/db.js b/frontend/js/db.js
index 2925738..eda3781 100644
--- a/frontend/js/db.js
+++ b/frontend/js/db.js
@@ -745,7 +745,6 @@ async function addGpsPoint(sessionId, ts, lat, lon, altGps, speedMs, seq, bleDat
["accel_rms", sensorData?.accel_rms], ["accel_max", sensorData?.accel_max],
["gyro_pitch", sensorData?.gyro_pitch], ["gyro_yaw", sensorData?.gyro_yaw],
["bearing_deg", sensorData?.bearing], ["accuracy_m", sensorData?.accuracy],
- ["altitude_baro_m", sensorData?.altitude_baro_m],
];
for (const [col, val] of extra) {
if (_gpsPointCols.has(col)) { cols.push(col); vals.push(val ?? null); }
@@ -1677,7 +1676,6 @@ async function runMigrations() {
if (!gpsCols.includes("gyro_yaw")) await execute("ALTER TABLE gps_points ADD COLUMN gyro_yaw REAL");
if (!gpsCols.includes("bearing_deg")) await execute("ALTER TABLE gps_points ADD COLUMN bearing_deg REAL");
if (!gpsCols.includes("accuracy_m")) await execute("ALTER TABLE gps_points ADD COLUMN accuracy_m REAL");
- if (!gpsCols.includes("altitude_baro_m")) await execute("ALTER TABLE gps_points ADD COLUMN altitude_baro_m REAL"); // barometric altitude from the BLE sensor (null without it)
if (!gpsCols.includes("lat_smoothed")) await execute("ALTER TABLE gps_points ADD COLUMN lat_smoothed REAL");
if (!gpsCols.includes("lon_smoothed")) await execute("ALTER TABLE gps_points ADD COLUMN lon_smoothed REAL");
if (!gpsCols.includes("estimated")) await execute("ALTER TABLE gps_points ADD COLUMN estimated INTEGER DEFAULT 0"); // gap-fill (BRouter) points, reversible
diff --git a/frontend/js/gps.js b/frontend/js/gps.js
index ba79d19..9d9a288 100644
--- a/frontend/js/gps.js
+++ b/frontend/js/gps.js
@@ -311,8 +311,7 @@ GPS.computeStats = function(points, weightKg, opts) {
// e.g. [45, 45, 52, 45, 45] — the 52 is a GPS altitude glitch.
// Prefer terrain (SRTM/open-elevation) altitude when available — GPS altitude is noisy
// and the stuck-filter destroys its real signal. Terrain is clean and accurate for slope.
- // Prefer the barometer (best for real climbs/descents) when present, then terrain (SRTM), then GPS.
- const rawAlt = sorted.map(p => p.altitude_baro_m ?? p.altitude_srtm_m ?? p.altitude ?? p.altitude_gps_m ?? null);
+ const rawAlt = sorted.map(p => p.altitude_srtm_m ?? p.altitude ?? p.altitude_gps_m ?? null);
const filteredAlt = rawAlt.slice();
for (let i = 1; i < filteredAlt.length - 1; i++) {
if (filteredAlt[i] == null) continue;
@@ -1171,15 +1170,6 @@ GPS.recoverFromFiles = async function(onProgress) {
// live marker — the same fix is never written twice with different timestamps.
GPS._processIncomingPoint = async function(p, fromNative) {
if (!GPS.recording.active) return;
-
- // Attach barometric altitude to this point when the BLE baro sensor is live and anchored.
- // Null without a barometer, so elevation stats fall back to GPS/SRTM exactly as before.
- try {
- if (window.BARO && BARO.isLive()) {
- let ba = BARO.altitude();
- if (ba != null && isFinite(ba)) p.altitude_baro_m = ba;
- }
- } catch (e) {}
// DB writes come only from the source of truth: native points on Android, or
// watchPosition on desktop (no native service).
const writeDb = !!fromNative || !GPS.recording.nativeService;
diff --git a/frontend/js/main.js b/frontend/js/main.js
index f998d98..7e59ec0 100644
--- a/frontend/js/main.js
+++ b/frontend/js/main.js
@@ -1364,7 +1364,6 @@ function showFoodModal() {
amountG = Math.round(rawAmount * selectedUnit.grams_per_unit * 10) / 10;
}
await DB.addFoodEntry(state.date, time, selectedProduct.id, amountG, null, unitId, unitAmount);
- hydrationOnDrink(); // if it was liquid, quiet the reminder once you've drunk
} else {
let desc = overlay.querySelector("#m-desc").value.trim();
if (!desc) return false;
@@ -1851,7 +1850,6 @@ async function renderProducts(container) {
let note = overlay.querySelector("#m-note").value.trim() || null;
if (isNaN(amount) || amount <= 0) return false;
await DB.addFoodEntry(state.date, time, productId, amount, note);
- hydrationOnDrink(); // quiet the drink reminder if this was liquid
});
}
@@ -2826,46 +2824,23 @@ let gpsMarker = null;
// At session start, seed the native hydration deficit from the FOOD LOG: how far behind on water
// you are for the time of day (need-so-far minus logged intake). During the session the native
// service adds sweat on top. Intake comes from what you log, not a button.
-// Sweat (extra water loss) per activity — cycling sweats far more than a walk, so a walk must
-// NOT accrue the ride rate (that made it falsely hit the threshold after ~1 h of walking).
-let HYDR_SWEAT = { bike: 600, walk: 200, run: 450, workout: 250, vr: 250 };
-function hydrSweatFor(type) { return HYDR_SWEAT[type] || 600; }
-
-// Compute the current sedentary shortfall (need-so-far for the time of day minus what you've
-// logged drinking today). Shared by the session seed and the on-drink re-seed.
-async function hydrBaseDeficit() {
- let target = parseInt(state.profile?.daily_water_ml) || 2000;
- let now = new Date();
- let nowMin = now.getHours() * 60 + now.getMinutes();
- let wakeStart = 7 * 60, wakeEnd = 23 * 60; // assume waking 07:00–23:00
- let frac = Math.max(0, Math.min(1, (nowMin - wakeStart) / (wakeEnd - wakeStart)));
- let intake = await DB.getDailyWaterMl(now.toISOString().slice(0, 10));
- return Math.max(0, Math.min(5000, Math.round(target * frac - intake)));
-}
-
-async function seedHydrationDeficit(activityType) {
+async function seedHydrationDeficit() {
if (localStorage.getItem("baldav_hydr_on") === "0") return; // ON by default unless turned off
try {
- let startDeficit = await hydrBaseDeficit();
+ let target = parseInt(state.profile?.daily_water_ml) || 2000;
+ let now = new Date();
+ let nowMin = now.getHours() * 60 + now.getMinutes();
+ let wakeStart = 7 * 60, wakeEnd = 23 * 60; // assume waking 07:00–23:00
+ let frac = Math.max(0, Math.min(1, (nowMin - wakeStart) / (wakeEnd - wakeStart)));
+ let needSoFar = target * frac;
+ let intake = await DB.getDailyWaterMl(now.toISOString().slice(0, 10));
+ let startDeficit = Math.max(0, Math.min(5000, Math.round(needSoFar - intake)));
let alertMl = parseInt(localStorage.getItem("baldav_hydr_alert")) || 600;
await window.__TAURI__.core.invoke("set_hydration_config",
- { enabled: true, alertMl, sweatMlPerHour: hydrSweatFor(activityType), reAlertMin: 15, startDeficitMl: startDeficit, seedSeq: Date.now() });
+ { enabled: true, alertMl, sweatMlPerHour: 600, reAlertMin: 15, startDeficitMl: startDeficit });
} catch (e) { console.error("seedHydration", e); }
}
-// After you log a drink DURING a session: recompute the base deficit (now lower) and push it with
-// a fresh seedSeq so the native side drops baseDeficit — the reminder goes quiet once you've drunk.
-async function hydrationOnDrink() {
- try {
- if (!(GPS.recording && GPS.recording.active)) return; // only matters during a session
- if (localStorage.getItem("baldav_hydr_on") === "0") return;
- let base = await hydrBaseDeficit();
- let alertMl = parseInt(localStorage.getItem("baldav_hydr_alert")) || 600;
- await window.__TAURI__.core.invoke("set_hydration_config",
- { enabled: true, alertMl, sweatMlPerHour: hydrSweatFor(GPS.recording.activityType), reAlertMin: 15, startDeficitMl: base, seedSeq: Date.now() });
- } catch (e) {}
-}
-
async function renderGpsScreen(container) {
// Check Bluetooth state if BLE device is paired
let bleAddr = await DB.getProfileValue("ble_device_addr");
@@ -2934,7 +2909,6 @@ async function renderGpsScreen(container) {
Выбрано 0
-
`;
@@ -2981,7 +2955,7 @@ async function renderGpsScreen(container) {
heightCm: parseFloat(state.profile?.height_cm) || 178,
} : null;
await GPS.startRecording(type, wkg, hrProfile);
- seedHydrationDeficit(type);
+ seedHydrationDeficit();
renderScreen("recording");
}
};
@@ -2992,13 +2966,11 @@ async function renderGpsScreen(container) {
let selBar = container.querySelector("#gps-sel-bar");
let selCount = container.querySelector("#gps-sel-count");
let selMergeBtn = container.querySelector("#gps-sel-merge");
- let selDeleteBtn = container.querySelector("#gps-sel-delete");
let refreshSel = () => {
container.querySelectorAll(".gps-session-item").forEach(it => it.classList.toggle("selected", selected.has(it.dataset.sid)));
if (selBar) selBar.hidden = !selMode;
if (selCount) selCount.textContent = "Выбрано " + selected.size;
- if (selMergeBtn) selMergeBtn.disabled = selected.size < 2; // merge needs 2+
- if (selDeleteBtn) selDeleteBtn.disabled = selected.size < 1; // delete works from 1
+ if (selMergeBtn) selMergeBtn.disabled = selected.size < 2;
};
let exitSel = () => { selMode = false; selected.clear(); refreshSel(); };
let toggleSel = (sid) => {
@@ -3063,27 +3035,6 @@ async function renderGpsScreen(container) {
selMergeBtn.disabled = false; selMergeBtn.textContent = "Объединить";
}
};
- if (selDeleteBtn) selDeleteBtn.onclick = async () => {
- if (selected.size < 1) return;
- let ids = [...selected];
- let n = ids.length;
- let word = (n % 10 === 1 && n % 100 !== 11) ? "трек"
- : (n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20)) ? "трека" : "треков";
- if (!(await appConfirm(`Удалить ${n} ${word}? Это необратимо.`, "Удалить"))) return;
- try {
- selDeleteBtn.disabled = true; selDeleteBtn.textContent = "Удаляю...";
- let done = 0;
- for (let id of ids) {
- try { await DB.deleteGpsSession(id); done++; } catch (e) { console.error("[bulk delete]", id, e); }
- }
- showToast(`Удалено: ${done}`);
- exitSel();
- renderScreen("gps");
- } catch (e) {
- showToast("Ошибка удаления: " + (e.message || e));
- selDeleteBtn.disabled = false; selDeleteBtn.textContent = "Удалить";
- }
- };
// Pagination: "Показать ещё" loads the next page from the DB and appends it.
let sessOffset = sessions.length;
@@ -4801,7 +4752,6 @@ function renderWorkoutDetail(session) {
let max = hrs.length ? Math.max(...hrs) : null;
let kcal = Math.round(session.total_kcal || 0);
let html = `
-