From 1daf757bcb4c732fdee0a0ff6a6b7c8d7b9b7e15 Mon Sep 17 00:00:00 2001 From: 4aiman Date: Sat, 16 May 2026 21:21:13 +0300 Subject: [PATCH] GPS point ordering + GPX export + chart zoom fixes GPS background recording: - Batched writes (10s flush) in native service - Polling lock prevents parallel processing - Track by timestamp instead of line count - Recovery idempotent via max DB timestamp - Seq numbers on live recorded points GPX export: - Button in session detail - SAF file picker on Android (ACTION_CREATE_DOCUMENT) - GPS config in metadata (accuracy, minSpeed, minDist) Charts: - Pinch zoom enabled at creation (Hammer.js registers handlers) - touch-action: pan-y blocks pinch in normal view (page scrolls) - Fullscreen: CSS-only expand (no canvas manipulation) - Downsampled to 500 points for smooth rendering - Back gesture collapses fullscreen before closing modal Build: --real flag finds first non-emulator device --- data.js | 339 +++++++ frontend/css/styles.css | 35 + frontend/index.html | 2 +- frontend/js/gps.js | 47 +- frontend/js/main.js | 89 +- index.html | 13 + package-lock.json | 1849 +++++++++++++++++++++++++++++++++++++ src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/src/lib.rs | 24 +- src-tauri/tauri.conf.json | 2 +- src/activity-table.js | 35 + src/bmr.js | 14 + src/date-utils.js | 43 + src/db.js | 338 +++++++ src/food-table.js | 91 ++ src/main.js | 526 +++++++++++ src/measures.js | 44 + src/parser.js | 112 +++ src/styles.css | 664 +++++++++++++ 20 files changed, 4234 insertions(+), 37 deletions(-) create mode 100644 data.js create mode 100644 index.html create mode 100644 package-lock.json create mode 100644 src/activity-table.js create mode 100644 src/bmr.js create mode 100644 src/date-utils.js create mode 100644 src/db.js create mode 100644 src/food-table.js create mode 100644 src/main.js create mode 100644 src/measures.js create mode 100644 src/parser.js create mode 100644 src/styles.css diff --git a/data.js b/data.js new file mode 100644 index 0000000..c15b47e --- /dev/null +++ b/data.js @@ -0,0 +1,339 @@ +const DATA = { + profile: { + height_cm: 178, + birth_year: 1988, + sex: "m", + bmr_formula: 1800, + bmr_estimated: 1580 + }, + days: [ + // ===== 03.04 ===== + { + date: "2026-04-03", + weights: [{ time: null, kg: 88.7 }], + sleep: { bed: "23:00", asleep: "23:40", wake: "09:00" }, + food: [], + activity: [], + water: 0, + notes: "" + }, + // ===== 04.04 ===== + { + date: "2026-04-04", + weights: [ + { time: "10:39", kg: 88.7 }, + { time: "12:49", kg: 88.7 }, + { time: "17:44", kg: 87.8 } + ], + sleep: { bed: "23:10", asleep: "23:39", wake: "08:30" }, + food: [ + { time: "10:50", desc: "чай с сахаром", kcal: 20 }, + { time: "12:34", desc: "чай, булочка 40г, хачапури с сыром 100г (неслоёное тесто)", kcal: 390 }, + { time: "18:00", desc: "салат (капуста, огурец, подсолнечное масло), редиска, чай с сахаром, хлеб", kcal: 295 }, + { time: "19:20", desc: "чай, охотничьи колбаски 50г", kcal: 180 } + ], + activity: [ + { time: "11:00", desc: "поход за продуктами (нёс 10кг половину пути)", minutes: 68, kcal: 300 }, + { time: "14:19", desc: "раскройка/распил ДВП, пробивание к стенке", minutes: 100, kcal: 350 }, + { time: "16:00", desc: "починка рулетки", minutes: 20, kcal: 30 }, + { time: "16:30", desc: "распил брусков 4x4", minutes: 74, kcal: 250 }, + { time: "18:40", desc: "прогулка", minutes: 40, kcal: 175 } + ], + water: 3, + notes: "головная боль вечером (тупая давящая в лобных долях), жажда" + }, + // ===== 05.04 ===== + { + date: "2026-04-05", + weights: [ + { time: "10:35", kg: 88.1 }, + { time: "21:00", kg: 88.9 } + ], + sleep: { bed: null, asleep: "01:40", wake: "08:45", nap_minutes: 150 }, + food: [ + { time: "10:40", desc: "салат (капуста, огурцы, редиска), 0.6л чая с сахаром", kcal: 120 }, + { time: "16:10", desc: "чай, карамельный попкорн 60г, творог со сметаной/сахаром/колбасой", kcal: 640 }, + { time: "21:00", desc: "чай, попкорн, редис", kcal: 150 } + ], + activity: [ + { time: "12:00", desc: "сборка каркаса шкафа (саморезы, уголки, брусья), перенос 2300x860x518", minutes: 240, kcal: 600 }, + { time: "20:20", desc: "зарядка", minutes: 20, kcal: 100 } + ], + water: 0, + notes: "тяжесть в голове с утра (проснулся с болью), после стройки и дневного сна прошло" + }, + // ===== 06.04 ===== + { + date: "2026-04-06", + weights: [ + { time: "14:30", kg: 87.1 }, + { time: null, kg: 88.9 } + ], + sleep: { bed: null, asleep: null, wake: "09:20" }, + food: [ + { time: "12:30", desc: "пирожок бездрожжевой с капустой, сметана, чай с 1 ч.л. сахара", kcal: 320 }, + { time: "14:30", desc: "чай", kcal: 20 }, + { time: "14:35", desc: "6 редисок, кусочек жареной курицы", kcal: 170 }, + { time: "14:40", desc: "ещё 100г курицы", kcal: 200 }, + { time: null, desc: "стакан молока 200мл", kcal: 106 }, + { time: "18:30", desc: "картошка 100г жареная в масле, 2 огурца свежих", kcal: 220 }, + { time: "18:30", desc: "чай с сахаром ~700мл", kcal: 80 } + ], + activity: [ + { time: "12:55", desc: "велосипед (отвёз ребёнка + круг, ~10км, пробил колесо)", minutes: 50, kcal: 350 }, + { time: "13:55", desc: "пешком с 10кг воды и великом", minutes: 17, kcal: 100 }, + { time: "14:12", desc: "затащил воду и велик на 5 этаж", minutes: 3, kcal: 50 } + ], + water: 0, + notes: "" + }, + // ===== 07.04 ===== + { + date: "2026-04-07", + weights: [{ time: "12:55", kg: 88.3 }], + sleep: { bed: null, asleep: "00:00", wake: "09:20" }, + food: [ + { time: "10:00", desc: "гречка с молоком и сахаром (дес. ложка), ел час", kcal: 370 }, + { time: "12:30", desc: "творог со сметаной и сахаром", kcal: 280 }, + { time: "16:00", desc: "кружка чая с 2 ч.л. сахара", kcal: 40 }, + { time: "17:50", desc: "100г сметаны, пирожок с тушёной капустой", kcal: 460 }, + { time: "18:40", desc: "чай с сахаром, банан", kcal: 150 }, + { time: "20:00", desc: "35г колбасок", kcal: 115 }, + { time: "23:00", desc: "чай с сахаром", kcal: 40 } + ], + activity: [ + { time: "13:20", desc: "прогулка", minutes: 30, kcal: 130 } + ], + water: 2, + notes: "удалёнка IT, витамины" + }, + // ===== 08.04 ===== + { + date: "2026-04-08", + weights: [{ time: "19:30", kg: 88.3 }], + sleep: { bed: "22:30", asleep: "22:30", wake: "09:20" }, + food: [ + { time: "10:30", desc: "творог 180г с сахаром, сметаной и колбасками 35г", kcal: 430 }, + { time: "14:00", desc: "пол литра молока", kcal: 275 }, + { time: "17:00", desc: "40г жареного арахиса", kcal: 235 }, + { time: "19:30", desc: "гречка с молоком и сахаром, 150г батона, 350мл чая", kcal: 785 } + ], + activity: [ + { time: "12:40", desc: "прогулка", minutes: 50, kcal: 220 }, + { time: "18:30", desc: "прогулка", minutes: 60, kcal: 260 } + ], + water: 0, + notes: "утром полудрёма 08:20-08:40. Ночью будили, поднялись ЧСС и давление, не мог спать до 03:23" + }, + // ===== 09.04 ===== + { + date: "2026-04-09", + weights: [], + sleep: { bed: null, asleep: null, wake: "09:20", nap_minutes: 25 }, + food: [ + { time: "11:30", desc: "гречка с молоком и сахаром (дес. ложка)", kcal: 370 }, + { time: "12:40", desc: "100г варёной картошки, 4 шпротинки, 50г салата (капуста, зелень, масло)", kcal: 170 }, + { time: "14:20", desc: "40г жареного арахиса", kcal: 235 }, + { time: "20:00", desc: "сырный багет 200г, 2 яйца глазунья, чай 600мл", kcal: 850 }, + { time: "20:15", desc: "15г морковки", kcal: 5 } + ], + activity: [ + { time: "18:50", desc: "прогулка + магазин", minutes: 50, kcal: 220 } + ], + water: 2, + notes: "" + }, + // ===== 10.04 ===== + { + date: "2026-04-10", + weights: [{ time: "17:30", kg: 87.7 }], + sleep: { bed: "01:24", asleep: "01:24", wake: "09:30" }, + food: [ + { time: "10:20", desc: "50г творога со сметаной и сахаром", kcal: 110 }, + { time: "12:10", desc: "150г пюре, 120г творога со сметаной/сахаром, 50г морковки со сметаной/чесноком", kcal: 420 }, + { time: "13:32", desc: "чай с сахаром 220мл", kcal: 25 }, + { time: "17:20", desc: "2 чашки чая, 30г плавленного сыра, 3 кусочка батона", kcal: 320 }, + { time: "18:00", desc: "60г колбасок (в прогулке)", kcal: 195 }, + { time: "20:30", desc: "картошка, макароны, яйцо глазунья, 4 кусочка батона (250г всего)", kcal: 420 }, + { time: "22:00", desc: "чай", kcal: 40 } + ], + activity: [ + { time: "14:30", desc: "замена покрышки и камеры на велике", minutes: 20, kcal: 50 }, + { time: "16:05", desc: "прогулка", minutes: 60, kcal: 260 }, + { time: "18:00", desc: "прогулка, магазины", minutes: 120, kcal: 520 } + ], + water: 0, + notes: "витамины, омега, железо" + }, + // ===== 11.04 ===== + { + date: "2026-04-11", + weights: [], + sleep: { bed: null, asleep: "01:07", wake: "09:00" }, + food: [ + { time: "10:30", desc: "чай, 30г протеинового батончика, витамины, железо, омега", kcal: 120 }, + { time: "12:00", desc: "150г сырного багета, чай", kcal: 475 }, + { time: "15:00", desc: "30г протеинового батончика", kcal: 80 }, + { time: "20:00", desc: "картошка, горошек, 15г колбасок, 2 чая", kcal: 320 } + ], + activity: [ + { time: "11:00", desc: "прогулка", minutes: 60, kcal: 260 }, + { time: "13:30", desc: "велосипед 10+ км", minutes: 70, kcal: 400 }, + { time: "14:50", desc: "подъём велика на 5 этаж", minutes: 3, kcal: 50 } + ], + water: 0, + notes: "ванна 15:30-16:50" + }, + // ===== 12.04 ===== + { + date: "2026-04-12", + weights: [{ time: null, kg: 88.7 }], + sleep: { bed: null, asleep: null, wake: "09:00" }, + food: [ + { time: "11:00", desc: "пюре 200г, сметана, 5 песочных печенек домашних", kcal: 560 }, + { time: "14:00", desc: "2 чая, 2 протеиновых батончика по 30г (80ккал)", kcal: 240 }, + { time: "18:00", desc: "100г кулича без изюма", kcal: 320 }, + { time: "20:00", desc: "пюре 150г, горошек 100г, 20г колбасок, 2 чая, 150г кулича", kcal: 845 } + ], + activity: [ + { time: "10:00", desc: "зарядка", minutes: 20, kcal: 80 }, + { time: "16:50", desc: "прогулка", minutes: 40, kcal: 175 }, + { time: "18:00", desc: "зарядка", minutes: 15, kcal: 60 } + ], + water: 0, + notes: "" + }, + // ===== 13.04 ===== + { + date: "2026-04-13", + weights: [{ time: null, kg: 88.7 }], + sleep: { bed: null, asleep: "22:14", wake: "09:20" }, + food: [ + { time: "11:00", desc: "300г молока", kcal: 165 }, + { time: "12:30", desc: "150г молока, 200г кулича, протеиновый батончик 30г", kcal: 803 }, + { time: "19:20", desc: "творог+сметана, пирожное (бисквит+масло) 70г, 2 чая, 6 кусочков колбасы, 5 кусочков батона", kcal: 965 } + ], + activity: [ + { time: "12:50", desc: "велосипед", minutes: 65, kcal: 400 }, + { time: "14:00", desc: "подъём велика на 5 этаж, душ", minutes: 5, kcal: 50 }, + { time: "18:00", desc: "прогулка + магазин", minutes: 60, kcal: 260 } + ], + water: 0, + notes: "" + }, + // ===== 14.04 ===== + { + date: "2026-04-14", + weights: [{ time: null, kg: 87.8 }], + sleep: { bed: null, asleep: null, wake: "09:20" }, + food: [ + { time: "10:20", desc: "яблоко 200г, молоко 350мл, протеиновый батончик", kcal: 367 }, + { time: null, desc: "творог 200г со сметаной, 4 кружки чая, колбаса 50г", kcal: 610 }, + { time: null, desc: "лапша б/п (без бульона), 3 кусочка хлеба с плавленным сыром", kcal: 590 } + ], + activity: [], + water: 0, + notes: "" + }, + // ===== 15.04 ===== + { + date: "2026-04-15", + weights: [], + sleep: { bed: "02:30", asleep: "02:30", wake: "09:20" }, + food: [ + { time: "10:30", desc: "протеиновый батончик, 350мл молока", kcal: 273 }, + { time: "13:20", desc: "гречка с молоком, чай, витамины, омега", kcal: 410 }, + { time: "19:00", desc: "чай, домашние вафли с цукатами", kcal: 390 } + ], + activity: [ + { time: "17:40", desc: "велосипед 10км", minutes: 50, kcal: 350 } + ], + water: 0, + notes: "кодинг до 02:30" + }, + // ===== 16.04 ===== + { + date: "2026-04-16", + weights: [], + sleep: { bed: "22:40", asleep: "22:40", wake: null }, + food: [ + { time: "10:30", desc: "протеиновый батончик, 350мл молока", kcal: 273 }, + { time: "12:40", desc: "макароны с сыром, домашняя вафля", kcal: 500 }, + { time: "13:30", desc: "чай 350мл", kcal: 40 }, + { time: "20:00", desc: "гречка с молоком, чай, плавленый сыр 90г, 15шт арахиса", kcal: 750 } + ], + activity: [ + { time: "19:00", desc: "прогулка", minutes: 40, kcal: 175 } + ], + water: 0, + notes: "" + }, + // ===== 17.04 ===== + { + date: "2026-04-17", + weights: [{ time: null, kg: 87.6 }], + sleep: { bed: null, asleep: null, wake: null, nap_minutes: 30 }, + food: [ + { time: "10:30", desc: "арахис 30г, молоко", kcal: 286 }, + { time: "12:30", desc: "картошка, маринованный перец (испорчен)", kcal: 145 }, + { time: "20:20", desc: "пломбир в шоколаде с орехами 65г, чай", kcal: 235 } + ], + activity: [ + { time: "16:10", desc: "прогулка", minutes: 60, kcal: 260 }, + { time: "18:50", desc: "прогулка", minutes: 30, kcal: 130 } + ], + water: 0, + notes: "отравление маринованным перцем, пронесло в 16:00 и 17:10" + }, + // ===== 18.04 ===== + { + date: "2026-04-18", + weights: [], + sleep: { bed: "22:00", asleep: "22:00", wake: "08:40" }, + food: [ + { time: null, desc: "2 огурца, немного сушек, яблоко, 2 кружки чая (весь день)", kcal: 350 } + ], + activity: [], + water: 1, + notes: "температура 36.8-37.0, много спал, собака воет за стенкой 9-21, стресс, болит голова от лежания и недостатка еды. Ночью проснулся в 02:30, комп до 04:30, затем сон" + }, + // ===== 19.04 ===== + { + date: "2026-04-19", + weights: [{ time: null, kg: 86.7 }], + sleep: { bed: null, asleep: "00:00", wake: "09:20" }, + food: [ + { time: null, desc: "огурец, десяток редисок, творог со сметаной и сахаром, много чая, газировка 0.5л, жареная курица в специях", kcal: 845 } + ], + activity: [ + { time: null, desc: "прогулка", minutes: 30, kcal: 130 }, + { time: null, desc: "распил ХДФ лобзиком, сборка рамы шкафа", minutes: 60, kcal: 200 }, + { time: null, desc: "зарядка", minutes: 20, kcal: 80 } + ], + water: 0, + notes: "тело тяжёлое, много лежал" + }, + // ===== 20.04 ===== + { + date: "2026-04-20", + weights: [{ time: null, kg: 87.0 }], + sleep: { bed: null, asleep: null, wake: "09:20" }, + food: [ + { time: "10:00", desc: "чай", kcal: 40 } + ], + activity: [], + water: 0, + notes: "неполные данные" + }, + // ===== 27.04 ===== + { + date: "2026-04-27", + weights: [{ time: null, kg: 88.0 }], + sleep: { bed: null, asleep: null, wake: null }, + food: [], + activity: [], + water: 0, + notes: "только вес (был 86.7, поел много)" + } + ] +}; diff --git a/frontend/css/styles.css b/frontend/css/styles.css index 06708bf..26040b8 100644 --- a/frontend/css/styles.css +++ b/frontend/css/styles.css @@ -1208,6 +1208,41 @@ body { border-radius: 4px; } +.chart-wrap { + position: relative; + cursor: pointer; + height: 200px; + width: 100%; +} + +.chart-full { + position: fixed; + left: 0; + top: var(--safe-area-top); + z-index: 9999; + background: var(--bg); + width: 100vw; + height: calc(100vh - var(--safe-area-top) - var(--safe-area-bottom)); +} + +.chart-close-btn { + position: absolute; + top: calc(12px + var(--safe-area-top)); + right: calc(12px + var(--safe-area-right, 0px)); + width: 36px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + color: var(--on-bg); + font-size: 18px; + cursor: pointer; + z-index: 10000; +} + .gps-download-status { text-align: center; font-size: 12px; diff --git a/frontend/index.html b/frontend/index.html index a1cbc49..94fce5f 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -13,8 +13,8 @@ - + diff --git a/frontend/js/gps.js b/frontend/js/gps.js index 2d812db..2616b69 100644 --- a/frontend/js/gps.js +++ b/frontend/js/gps.js @@ -362,16 +362,23 @@ GPS.startRecording = async function(activityType, weightKg) { // Poll native service output file if (GPS.recording.nativeService) { + let polling = false; GPS.recording._pollTimer = setInterval(async () => { if (!GPS.recording.active) { clearInterval(GPS.recording._pollTimer); return; } - const nativePoints = await GPS._readNativePoints(); - // Process only points newer than last processed (by timestamp) - for (const p of nativePoints) { - const pTs = new Date(p.ts).getTime(); - if (pTs <= GPS.recording._lastProcessedTs) continue; - await GPS._processIncomingPoint(p); - GPS.recording._lastProcessedTs = pTs; + if (polling) return; // skip if previous poll still running + polling = true; + try { + const nativePoints = await GPS._readNativePoints(); + for (const p of nativePoints) { + const pTs = new Date(p.ts).getTime(); + if (pTs <= GPS.recording._lastProcessedTs) continue; + await GPS._processIncomingPoint(p); + GPS.recording._lastProcessedTs = pTs; + } + } catch (e) { + console.error("[GPS] poll error:", e); } + polling = false; }, 2000); } @@ -689,9 +696,14 @@ GPS.exportGPX = async function(sessionId) { const esc = s => String(s).replace(/&/g,"&").replace(//g,">"); const name = esc(session.activity_type || "Track") + " " + esc(session.date); + const cfg = GPS.config; let gpx = ` - ${name} + + ${name} + + minSpeed=${(cfg.minSpeedMs*3.6).toFixed(1)}kmh maxAcc=${cfg.minAccuracyM}m minDist=${cfg.minDistanceM}m points=${points.length} + ${name} ${esc(session.activity_type || "other")} @@ -711,16 +723,15 @@ GPS.exportGPX = async function(sessionId) { `; - // Download - const blob = new Blob([gpx], { type: "application/gpx+xml" }); - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = `baldav-${session.date}-${session.activity_type || "track"}.gpx`; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); + // Save via Rust to Downloads folder + const time = (session.start_time || "").replace(/:/g, "-"); + const filename = `baldav-${session.date}_${time}-${session.activity_type || "track"}.gpx`; + try { + const path = await window.__TAURI__.core.invoke("save_file", { filename, content: gpx }); + console.log("[GPX] Saved to:", path); + } catch (e) { + console.error("[GPX] Save error:", e); + } }; window.GPS = GPS; diff --git a/frontend/js/main.js b/frontend/js/main.js index 1d403e0..2efe81f 100644 --- a/frontend/js/main.js +++ b/frontend/js/main.js @@ -308,20 +308,30 @@ function showModal(html, onSubmit) { history.pushState({ modal: true }, ""); const popHandler = () => { - // If fullscreen map is open inside this modal, collapse it instead of closing modal + // Fullscreen map inside modal? Collapse it first. const fsMap = overlay.querySelector(".sd-map-full"); if (fsMap) { fsMap.classList.remove("sd-map-full"); const expandBtn = fsMap.querySelector(".sd-map-expand-btn"); if (expandBtn) expandBtn.innerHTML = "⛶"; - try { map.dragging.disable(); } catch {} + try { map.dragging.disable(); map.scrollWheelZoom.disable(); } catch {} history.pushState({ modal: true }, ""); - window.addEventListener("popstate", popHandler, { once: true }); return; } + // Fullscreen chart inside modal? Collapse it first. + const fsChart = document.querySelector(".chart-full"); + if (fsChart) { + fsChart.classList.remove("chart-full"); + const btn = fsChart.querySelector(".chart-close-btn"); + if (btn) btn.remove(); + history.pushState({ modal: true }, ""); + return; + } + // Nothing fullscreen — close modal + window.removeEventListener("popstate", popHandler); closeModal(false); }; - window.addEventListener("popstate", popHandler, { once: true }); + window.addEventListener("popstate", popHandler); function closeModal(goBack) { overlay.remove(); @@ -1244,7 +1254,7 @@ async function showSessionDetail(sessionId) {
${stats.avgSpeed} км/чСредняя
${stats.maxSpeed} км/чМакс.
- +
@@ -1266,7 +1276,7 @@ async function showSessionDetail(sessionId) {
${stats.elevGain} мПодъём
${stats.elevLoss} мСпуск
- +
` : ""} @@ -1344,11 +1354,13 @@ async function showSessionDetail(sessionId) { if (mapIsFullscreen) { collapseMap(); map.dragging.disable(); + map.scrollWheelZoom.disable(); } else { mapIsFullscreen = true; mapEl.classList.add("sd-map-full"); fsDiv.innerHTML = "✕"; map.dragging.enable(); + map.scrollWheelZoom.enable(); // zoom without ctrl in fullscreen setTimeout(() => { map.invalidateSize(); map.fitBounds(L.latLngBounds(allCoords).pad(0.05)); }, 100); } } @@ -1391,6 +1403,26 @@ async function showSessionDetail(sessionId) { }); }, 200); + // Downsample arrays for smooth chart rendering (max ~500 points) + function downsample(arr, maxPts) { + if (arr.length <= maxPts) return arr; + const step = arr.length / maxPts; + const result = []; + for (let i = 0; i < maxPts; i++) { + const idx = Math.floor(i * step); + result.push(arr[idx]); + } + return result; + } + const MAX_CHART_PTS = 500; + const dsSmoothed = downsample(smoothed, MAX_CHART_PTS); + const dsTimeLabels = downsample(timeLabels, MAX_CHART_PTS); + const dsElevations = downsample(elevations, MAX_CHART_PTS); + const dsElevLabels = downsample( + points.filter(p => p.altitude_gps_m != null).map(p => ((new Date(p.ts).getTime() - t0) / 60000).toFixed(1)), + MAX_CHART_PTS + ); + // Render speed chart setTimeout(() => { const ctx = document.querySelector("#sd-speed-chart"); @@ -1401,9 +1433,9 @@ async function showSessionDetail(sessionId) { new Chart(ctx, { type: "line", data: { - labels: timeLabels, + labels: dsTimeLabels, datasets: [{ - data: smoothed, + data: dsSmoothed, borderColor: "#bb86fc", borderWidth: 1.5, pointRadius: 0, @@ -1412,11 +1444,16 @@ async function showSessionDetail(sessionId) { }], }, options: { - responsive: true, + responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, zoom: { - zoom: { wheel: { enabled: true, modifierKey: "ctrl" }, pinch: { enabled: true }, drag: { enabled: false }, mode: "x" }, + zoom: { + wheel: { enabled: true, modifierKey: "ctrl", speed: 0.02 }, + pinch: { enabled: true }, + mode: "x", + limits: { x: { minRange: 0.5 } }, + }, pan: { enabled: false }, }, }, @@ -1439,9 +1476,9 @@ async function showSessionDetail(sessionId) { new Chart(ctx, { type: "line", data: { - labels: elevLabels, + labels: dsElevLabels, datasets: [{ - data: elevations, + data: dsElevations, borderColor: "#ffb74d", borderWidth: 1.5, pointRadius: 0, @@ -1451,11 +1488,16 @@ async function showSessionDetail(sessionId) { }], }, options: { - responsive: true, + responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, zoom: { - zoom: { wheel: { enabled: true, modifierKey: "ctrl" }, pinch: { enabled: true }, drag: { enabled: false }, mode: "x" }, + zoom: { + wheel: { enabled: true, modifierKey: "ctrl", speed: 0.02 }, + pinch: { enabled: true }, + mode: "x", + limits: { x: { minRange: 0.5 } }, + }, pan: { enabled: false }, }, }, @@ -1474,6 +1516,25 @@ async function showSessionDetail(sessionId) { const exp = document.querySelector("#sd-export"); if (exp) exp.onclick = () => GPS.exportGPX(sessionId); + // Chart fullscreen toggle — tap to expand, tap X or back to collapse + document.querySelectorAll(".chart-wrap").forEach(wrap => { + wrap.onclick = () => { + if (wrap.classList.contains("chart-full")) return; + wrap.classList.add("chart-full"); + // Push history so back gesture collapses chart, not modal + history.pushState({ chartFull: true }, ""); + const closeBtn = document.createElement("div"); + closeBtn.className = "chart-close-btn"; + closeBtn.innerHTML = "✕"; + closeBtn.onclick = (e) => { + e.stopPropagation(); + wrap.classList.remove("chart-full"); + closeBtn.remove(); + }; + wrap.appendChild(closeBtn); + }; + }); + const del = document.querySelector("#sd-delete"); if (del) del.onclick = async () => { await DB.deleteGpsSession(sessionId); diff --git a/index.html b/index.html new file mode 100644 index 0000000..f385640 --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + BalDav + + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..fcc5b43 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1849 @@ +{ + "name": "baldav", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "baldav", + "version": "0.1.0", + "dependencies": { + "puppeteer": "^24.43.1", + "puppeteer-extra": "^3.3.6", + "puppeteer-extra-plugin-stealth": "^2.11.2" + }, + "devDependencies": { + "@tauri-apps/api": "^2.11.0", + "@tauri-apps/cli": "^2" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@puppeteer/browsers": { + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.2.tgz", + "integrity": "sha512-5EUZSUIc37H6aIXyWO0Z4y8NlF8NnjgmqeQgOGiswAU7pY0HOo16ho4+alIWmSfdZnjqBRawMsP3I5YqLSn6kw==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.3", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.7.4", + "tar-fs": "^3.1.1", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@tauri-apps/api": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.0.tgz", + "integrity": "sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA==", + "dev": true, + "license": "Apache-2.0 OR MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + } + }, + "node_modules/@tauri-apps/cli": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.10.1.tgz", + "integrity": "sha512-jQNGF/5quwORdZSSLtTluyKQ+o6SMa/AUICfhf4egCGFdMHqWssApVgYSbg+jmrZoc8e1DscNvjTnXtlHLS11g==", + "dev": true, + "license": "Apache-2.0 OR MIT", + "bin": { + "tauri": "tauri.js" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + }, + "optionalDependencies": { + "@tauri-apps/cli-darwin-arm64": "2.10.1", + "@tauri-apps/cli-darwin-x64": "2.10.1", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.10.1", + "@tauri-apps/cli-linux-arm64-gnu": "2.10.1", + "@tauri-apps/cli-linux-arm64-musl": "2.10.1", + "@tauri-apps/cli-linux-riscv64-gnu": "2.10.1", + "@tauri-apps/cli-linux-x64-gnu": "2.10.1", + "@tauri-apps/cli-linux-x64-musl": "2.10.1", + "@tauri-apps/cli-win32-arm64-msvc": "2.10.1", + "@tauri-apps/cli-win32-ia32-msvc": "2.10.1", + "@tauri-apps/cli-win32-x64-msvc": "2.10.1" + } + }, + "node_modules/@tauri-apps/cli-darwin-arm64": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.10.1.tgz", + "integrity": "sha512-Z2OjCXiZ+fbYZy7PmP3WRnOpM9+Fy+oonKDEmUE6MwN4IGaYqgceTjwHucc/kEEYZos5GICve35f7ZiizgqEnQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-darwin-x64": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.10.1.tgz", + "integrity": "sha512-V/irQVvjPMGOTQqNj55PnQPVuH4VJP8vZCN7ajnj+ZS8Kom1tEM2hR3qbbIRoS3dBKs5mbG8yg1WC+97dq17Pw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.10.1.tgz", + "integrity": "sha512-Hyzwsb4VnCWKGfTw+wSt15Z2pLw2f0JdFBfq2vHBOBhvg7oi6uhKiF87hmbXOBXUZaGkyRDkCHsdzJcIfoJC2w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-gnu": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.10.1.tgz", + "integrity": "sha512-OyOYs2t5GkBIvyWjA1+h4CZxTcdz1OZPCWAPz5DYEfB0cnWHERTnQ/SLayQzncrT0kwRoSfSz9KxenkyJoTelA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-musl": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.10.1.tgz", + "integrity": "sha512-MIj78PDDGjkg3NqGptDOGgfXks7SYJwhiMh8SBoZS+vfdz7yP5jN18bNaLnDhsVIPARcAhE1TlsZe/8Yxo2zqg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.10.1.tgz", + "integrity": "sha512-X0lvOVUg8PCVaoEtEAnpxmnkwlE1gcMDTqfhbefICKDnOTJ5Est3qL0SrWxizDackIOKBcvtpejrSiVpuJI1kw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-gnu": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.10.1.tgz", + "integrity": "sha512-2/12bEzsJS9fAKybxgicCDFxYD1WEI9kO+tlDwX5znWG2GwMBaiWcmhGlZ8fi+DMe9CXlcVarMTYc0L3REIRxw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-musl": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.10.1.tgz", + "integrity": "sha512-Y8J0ZzswPz50UcGOFuXGEMrxbjwKSPgXftx5qnkuMs2rmwQB5ssvLb6tn54wDSYxe7S6vlLob9vt0VKuNOaCIQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-arm64-msvc": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.10.1.tgz", + "integrity": "sha512-iSt5B86jHYAPJa/IlYw++SXtFPGnWtFJriHn7X0NFBVunF6zu9+/zOn8OgqIWSl8RgzhLGXQEEtGBdR4wzpVgg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-ia32-msvc": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.10.1.tgz", + "integrity": "sha512-gXyxgEzsFegmnWywYU5pEBURkcFN/Oo45EAwvZrHMh+zUSEAvO5E8TXsgPADYm31d1u7OQU3O3HsYfVBf2moHw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-x64-msvc": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.10.1.tgz", + "integrity": "sha512-6Cn7YpPFwzChy0ERz6djKEmUehWrYlM+xTaNzGPgZocw3BD7OfwfWHKVWxXzdjEW2KfKkHddfdxK1XXTYqBRLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "license": "MIT" + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.7.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.7.0.tgz", + "integrity": "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg==", + "license": "MIT", + "optional": true, + "dependencies": { + "undici-types": "~7.21.0" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/b4a": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", + "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/bare-events": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.1.tgz", + "integrity": "sha512-WDRsyVN52eAx/lBamKD6uyw8H4228h/x0sGGGegOamM2cd7Pag88GfMQalobXI+HaEUxpCkbKQUDOQqt9wawRw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.9.1.tgz", + "integrity": "sha512-6M5XjcnsygQNPMCMPXSK379xrJFiZ/AEMNBmFEmQW8d/789VQATvriyi5r0HYTL9TkQ26rn3kgdTG3aisbrXkQ==", + "license": "Apache-2.0", + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "license": "Apache-2.0", + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.1.tgz", + "integrity": "sha512-Vp0cnjYyrEC4whYTymQ+YZi6pBpfiICZO3cfRG8sy67ZNWe951urv1x4eW1BKNngw3U+3fPYb5JQvHbCtxH7Ow==", + "license": "Apache-2.0", + "dependencies": { + "streamx": "^2.25.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-abort-controller": "*", + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.3.tgz", + "integrity": "sha512-Kccpc7ACfXaxfeInfqKcZtW4pT5YBn1mesc4sCsun6sRwtbJ4h+sNOaksUpYEJUKfN65YWC6Bw2OJEFiKxq8nQ==", + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, + "node_modules/basic-ftp": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", + "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chromium-bidi": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz", + "integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==", + "license": "Apache-2.0", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone-deep": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-0.2.4.tgz", + "integrity": "sha512-we+NuQo2DHhSl+DP6jlUiAhyAjBQrYnpOk15rN6c6JSPScjiCLh8IbSU+VTcph6YS3o7mASE8a0+gbZ7ChLpgg==", + "license": "MIT", + "dependencies": { + "for-own": "^0.1.3", + "is-plain-object": "^2.0.1", + "kind-of": "^3.0.2", + "lazy-cache": "^1.0.3", + "shallow-clone": "^0.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz", + "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==", + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/devtools-protocol": { + "version": "0.0.1608973", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1608973.tgz", + "integrity": "sha512-Tpm17fxYzt+J7VrGdc1k8YdRqS3YV7se/M6KeemEqvUbq/n7At1rWVuXMxQgpWkdwSdIEKYbU//Bve+Shm4YNQ==", + "license": "BSD-3-Clause" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha512-SKmowqGTJoPzLO1T0BBJpkfp3EMacCMOuH40hOUbrbzElVktk4DioXVM99QkLCyKoiuOmyjgcWMpVz2xjE7LZw==", + "license": "MIT", + "dependencies": { + "for-in": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "license": "MIT" + }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha512-RE2g0b5VGZsOCFOCgP7omTRYFqydmZkBwl5oNnQ1lDYC57uyO9KqNnNVxT7COSHTxrRCWVcAVOcbjk+tvh/rgQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/merge-deep": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/merge-deep/-/merge-deep-3.0.3.tgz", + "integrity": "sha512-qtmzAS6t6grwEkNrunqTBdn0qKwFgNWvlxUbAV8es9M7Ot1EbyApytCnvE0jALPa46ZpKDUo527kKiaWplmlFA==", + "license": "MIT", + "dependencies": { + "arr-union": "^3.1.0", + "clone-deep": "^0.2.4", + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/mixin-object": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mixin-object/-/mixin-object-2.0.1.tgz", + "integrity": "sha512-ALGF1Jt9ouehcaXaHhn6t1yGWRqGaHkPFndtFVHfZXOvkIZ/yoGaSi0AHVTafb3ZBGg4dr/bDwnaEKqCXzchMA==", + "license": "MIT", + "dependencies": { + "for-in": "^0.1.3", + "is-extendable": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mixin-object/node_modules/for-in": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-0.1.8.tgz", + "integrity": "sha512-F0to7vbBSHP8E3l6dCjxNOLuSFAACIxFy3UehTUlG7svlXi37HHsDkyVcHo0Pq8QwrE+pXvWSVX3ZT1T9wAZ9g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/netmask": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", + "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/puppeteer": { + "version": "24.43.1", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.43.1.tgz", + "integrity": "sha512-/FSOViCrqRdb1HDocpsM9Z1giA71gTQPUt3SpHGVRALKAy/rJr1fLFYZW9F23qPxqVxTHQnbh/5B5opJST3kAw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.13.2", + "chromium-bidi": "14.0.0", + "cosmiconfig": "^9.0.0", + "devtools-protocol": "0.0.1608973", + "puppeteer-core": "24.43.1", + "typed-query-selector": "^2.12.2" + }, + "bin": { + "puppeteer": "lib/cjs/puppeteer/node/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core": { + "version": "24.43.1", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.43.1.tgz", + "integrity": "sha512-T5ScUMAsmhdNbgDR41AGESYeS6V9MSgetkSnVhhW+gXvzC42VesKCn5ld87gAZDJ6vLHL9GkRvY9WtQWSnwFbw==", + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.13.2", + "chromium-bidi": "14.0.0", + "debug": "^4.4.3", + "devtools-protocol": "0.0.1608973", + "typed-query-selector": "^2.12.2", + "webdriver-bidi-protocol": "0.4.1", + "ws": "^8.20.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-extra": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/puppeteer-extra/-/puppeteer-extra-3.3.6.tgz", + "integrity": "sha512-rsLBE/6mMxAjlLd06LuGacrukP2bqbzKCLzV1vrhHFavqQE/taQ2UXv3H5P0Ls7nsrASa+6x3bDbXHpqMwq+7A==", + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.0", + "debug": "^4.1.1", + "deepmerge": "^4.2.2" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "@types/puppeteer": "*", + "puppeteer": "*", + "puppeteer-core": "*" + }, + "peerDependenciesMeta": { + "@types/puppeteer": { + "optional": true + }, + "puppeteer": { + "optional": true + }, + "puppeteer-core": { + "optional": true + } + } + }, + "node_modules/puppeteer-extra-plugin": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/puppeteer-extra-plugin/-/puppeteer-extra-plugin-3.2.3.tgz", + "integrity": "sha512-6RNy0e6pH8vaS3akPIKGg28xcryKscczt4wIl0ePciZENGE2yoaQJNd17UiEbdmh5/6WW6dPcfRWT9lxBwCi2Q==", + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.0", + "debug": "^4.1.1", + "merge-deep": "^3.0.1" + }, + "engines": { + "node": ">=9.11.2" + }, + "peerDependencies": { + "playwright-extra": "*", + "puppeteer-extra": "*" + }, + "peerDependenciesMeta": { + "playwright-extra": { + "optional": true + }, + "puppeteer-extra": { + "optional": true + } + } + }, + "node_modules/puppeteer-extra-plugin-stealth": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/puppeteer-extra-plugin-stealth/-/puppeteer-extra-plugin-stealth-2.11.2.tgz", + "integrity": "sha512-bUemM5XmTj9i2ZerBzsk2AN5is0wHMNE6K0hXBzBXOzP5m5G3Wl0RHhiqKeHToe/uIH8AoZiGhc1tCkLZQPKTQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "puppeteer-extra-plugin": "^3.2.3", + "puppeteer-extra-plugin-user-preferences": "^2.4.1" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "playwright-extra": "*", + "puppeteer-extra": "*" + }, + "peerDependenciesMeta": { + "playwright-extra": { + "optional": true + }, + "puppeteer-extra": { + "optional": true + } + } + }, + "node_modules/puppeteer-extra-plugin-user-data-dir": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/puppeteer-extra-plugin-user-data-dir/-/puppeteer-extra-plugin-user-data-dir-2.4.1.tgz", + "integrity": "sha512-kH1GnCcqEDoBXO7epAse4TBPJh9tEpVEK/vkedKfjOVOhZAvLkHGc9swMs5ChrJbRnf8Hdpug6TJlEuimXNQ+g==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^10.0.0", + "puppeteer-extra-plugin": "^3.2.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "playwright-extra": "*", + "puppeteer-extra": "*" + }, + "peerDependenciesMeta": { + "playwright-extra": { + "optional": true + }, + "puppeteer-extra": { + "optional": true + } + } + }, + "node_modules/puppeteer-extra-plugin-user-preferences": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/puppeteer-extra-plugin-user-preferences/-/puppeteer-extra-plugin-user-preferences-2.4.1.tgz", + "integrity": "sha512-i1oAZxRbc1bk8MZufKCruCEC3CCafO9RKMkkodZltI4OqibLFXF3tj6HZ4LZ9C5vCXZjYcDWazgtY69mnmrQ9A==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "deepmerge": "^4.2.2", + "puppeteer-extra-plugin": "^3.2.3", + "puppeteer-extra-plugin-user-data-dir": "^2.4.1" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "playwright-extra": "*", + "puppeteer-extra": "*" + }, + "peerDependenciesMeta": { + "playwright-extra": { + "optional": true + }, + "puppeteer-extra": { + "optional": true + } + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shallow-clone": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-0.1.2.tgz", + "integrity": "sha512-J1zdXCky5GmNnuauESROVu31MQSnLoYvlyEn6j2Ztk6Q5EHFIhxkMhYcv6vuDzl2XEzoRr856QwzMgWM/TmZgw==", + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.1", + "kind-of": "^2.0.1", + "lazy-cache": "^0.2.3", + "mixin-object": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shallow-clone/node_modules/kind-of": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-2.0.1.tgz", + "integrity": "sha512-0u8i1NZ/mg0b+W3MGGw5I7+6Eib2nx72S/QvXa0hYjEkjTknYmEYQJwGu3mLC0BrhtJjtQafTkyRUQ75Kx0LVg==", + "license": "MIT", + "dependencies": { + "is-buffer": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shallow-clone/node_modules/lazy-cache": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-0.2.7.tgz", + "integrity": "sha512-gkX52wvU/R8DVMMt78ATVPFMJqfW8FPz1GZ1sVHBVQHmu/WvhIWE4cE1GBzhJNFicDeYhnwp6Rl35BcAIM3YOQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/streamx": { + "version": "2.25.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz", + "integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==", + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar-fs": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz", + "integrity": "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz", + "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typed-query-selector": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz", + "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.21.0.tgz", + "integrity": "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ==", + "license": "MIT", + "optional": true + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/webdriver-bidi-protocol": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz", + "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==", + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 98247b6..627bf82 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -128,7 +128,7 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "baldav" -version = "0.1.59" +version = "0.1.74" dependencies = [ "rusqlite", "serde", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 90af130..6222a71 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "baldav" -version = "0.1.59" +version = "0.1.74" edition = "2021" [lib] diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 0082109..a9af8e7 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -56,6 +56,28 @@ fn gps_service_read_points(app: tauri::AppHandle) -> Result { } } +#[tauri::command] +fn save_file(app: tauri::AppHandle, filename: String, content: String) -> Result { + let data_dir = app.path().app_data_dir().map_err(|e| e.to_string())?; + std::fs::create_dir_all(&data_dir).ok(); + + // Write content to temp file for Kotlin to read + let content_path = data_dir.join("save_content.txt"); + std::fs::write(&content_path, &content).map_err(|e| e.to_string())?; + + // Send save command to Kotlin (triggers SAF file picker on Android) + let cmd_path = data_dir.join("gps_command.txt"); + std::fs::write(&cmd_path, format!("save:{}", filename)).map_err(|e| e.to_string())?; + + // Also write to app root for Kotlin FileObserver + if let Some(root) = data_dir.parent().and_then(|p| p.parent()) { + let _ = std::fs::write(root.join("save_content.txt"), &content); + let _ = std::fs::write(root.join("gps_command.txt"), format!("save:{}", filename)); + } + + Ok("save_requested".to_string()) +} + #[tauri::command] fn import_seed_products(app: tauri::AppHandle) -> Result { // Find seed DB — try multiple locations @@ -392,7 +414,7 @@ pub fn run() { .add_migrations("sqlite:baldav.db", migrations) .build(), ) - .invoke_handler(tauri::generate_handler![get_safe_insets, import_seed_products, get_app_info, gps_service_command, gps_service_read_points]) + .invoke_handler(tauri::generate_handler![get_safe_insets, import_seed_products, get_app_info, gps_service_command, gps_service_read_points, save_file]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 08b4359..721e6a3 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/nicegui/tauri-v2/refs/heads/main/tooling/cli/schema.json", "productName": "BalDav", - "version": "0.1.59", + "version": "0.1.74", "identifier": "dev.baldav.app", "build": { "frontendDist": "../frontend" diff --git a/src/activity-table.js b/src/activity-table.js new file mode 100644 index 0000000..71c483e --- /dev/null +++ b/src/activity-table.js @@ -0,0 +1,35 @@ +// Activity calorie expenditure rates +// kcal_per_min_per_kg: calories burned per minute per kg of body weight +// (beyond basal metabolic rate) +// default_min: typical duration in minutes + +export const ACTIVITIES = [ + { key: "walk", name: "прогулка", aliases: ["прогулк", "ходьб", "гулял", "пешком"], kcal_min_kg: 0.05, default_min: 30 }, + { key: "bike", name: "велосипед", aliases: ["велосипед", "вело", "велик"], kcal_min_kg: 0.08, default_min: 60 }, + { key: "run", name: "бег", aliases: ["бег", "пробежк", "бежал"], kcal_min_kg: 0.12, default_min: 30 }, + { key: "sleep", name: "сон", aliases: ["сон", "спал", "сплю"], kcal_min_kg: 0.015, default_min: 480 }, + { key: "chore", name: "домашние дела", aliases: ["убор", "уборк", "стирк", "готовк", "мыл"], kcal_min_kg: 0.04, default_min: 30 }, + { key: "desk_work", name: "работа за столом", aliases: ["работ", "сидел"], kcal_min_kg: 0.02, default_min: 60 }, + { key: "stairs", name: "лестница", aliases: ["лестниц", "ступен"], kcal_min_kg: 0.10, default_min: 5 }, +]; + +export const ACTIVITY_MAP = Object.fromEntries( + ACTIVITIES.map((a) => [a.key, a]) +); + +export function matchActivity(text) { + const lower = text.toLowerCase(); + for (const act of ACTIVITIES) { + for (const alias of act.aliases) { + if (lower.includes(alias.toLowerCase())) { + return act; + } + } + } + return null; +} + +// Calculate kcal for an activity given weight and duration +export function activityKcal(activity, weightKg, minutes) { + return Math.round(activity.kcal_min_kg * weightKg * minutes); +} diff --git a/src/bmr.js b/src/bmr.js new file mode 100644 index 0000000..0db6fd9 --- /dev/null +++ b/src/bmr.js @@ -0,0 +1,14 @@ +// Mifflin-St Jeor equation for Basal Metabolic Rate +// Most accurate BMR formula validated by research (Mifflin et al., 1990) +// +// Males: BMR = 10 * weight(kg) + 6.25 * height(cm) - 5 * age(years) - 5 +// Females: BMR = 10 * weight(kg) + 6.25 * height(cm) - 5 * age(years) + 161 + +export function calculateBmr(weightKg, heightCm, age, sex) { + const base = 10 * weightKg + 6.25 * heightCm - 5 * age; + return Math.round(sex === "m" ? base - 5 : base + 161); +} + +export function getAge(birthYear) { + return new Date().getFullYear() - birthYear; +} diff --git a/src/date-utils.js b/src/date-utils.js new file mode 100644 index 0000000..bed2181 --- /dev/null +++ b/src/date-utils.js @@ -0,0 +1,43 @@ +const DAY_NAMES = ["вс", "пн", "вт", "ср", "чт", "пт", "сб"]; +const MONTH_NAMES = [ + "января", "февраля", "марта", "апреля", "мая", "июня", + "июля", "августа", "сентября", "октября", "ноября", "декабря", +]; + +// Format: "2026-04-05" +export function toIsoDate(date) { + const y = date.getFullYear(); + const m = String(date.getMonth() + 1).padStart(2, "0"); + const d = String(date.getDate()).padStart(2, "0"); + return `${y}-${m}-${d}`; +} + +// Format: "сб, 5 апреля" +export function toHumanDate(dateStr) { + const d = new Date(dateStr + "T00:00:00"); + const day = DAY_NAMES[d.getDay()]; + const num = d.getDate(); + const month = MONTH_NAMES[d.getMonth()]; + return `${day}, ${num} ${month}`; +} + +export function today() { + return toIsoDate(new Date()); +} + +export function shiftDate(dateStr, days) { + const d = new Date(dateStr + "T00:00:00"); + d.setDate(d.getDate() + days); + return toIsoDate(d); +} + +export function currentTime() { + const now = new Date(); + const h = String(now.getHours()).padStart(2, "0"); + const m = String(now.getMinutes()).padStart(2, "0"); + return `${h}:${m}`; +} + +export function isToday(dateStr) { + return dateStr === today(); +} diff --git a/src/db.js b/src/db.js new file mode 100644 index 0000000..b1468be --- /dev/null +++ b/src/db.js @@ -0,0 +1,338 @@ +// Database layer — wraps Tauri SQL plugin +// All queries go through this module; no raw SQL elsewhere + +let db = null; + +export async function initDb() { + const Database = (await import("@tauri-apps/plugin-sql")).default; + db = await Database.load("sqlite:baldav.db"); + return db; +} + +export async function getDb() { + if (!db) await initDb(); + return db; +} + +// --------------------------------------------------------------------------- +// Profile (key-value store) +// --------------------------------------------------------------------------- + +export async function getProfileValue(key) { + const d = await getDb(); + const rows = await d.select("SELECT value FROM profile WHERE key = $1", [key]); + return rows.length ? rows[0].value : null; +} + +export async function setProfileValue(key, value) { + const d = await getDb(); + await d.execute( + "INSERT OR REPLACE INTO profile (key, value) VALUES ($1, $2)", + [key, String(value)] + ); +} + +export async function getFullProfile() { + const d = await getDb(); + const rows = await d.select("SELECT key, value FROM profile"); + const p = {}; + for (const r of rows) p[r.key] = r.value; + return p; +} + +// --------------------------------------------------------------------------- +// Weight log +// --------------------------------------------------------------------------- + +export async function addWeight(date, time, kg) { + const d = await getDb(); + await d.execute( + "INSERT INTO weight_log (date, time, weight_kg) VALUES ($1, $2, $3)", + [date, time, kg] + ); +} + +export async function deleteWeight(id) { + const d = await getDb(); + await d.execute("DELETE FROM weight_log WHERE id = $1", [id]); +} + +export async function getWeightsForDate(date) { + const d = await getDb(); + return d.select( + "SELECT * FROM weight_log WHERE date = $1 ORDER BY time ASC", + [date] + ); +} + +export async function getLatestWeight() { + const d = await getDb(); + const rows = await d.select( + "SELECT * FROM weight_log ORDER BY date DESC, time DESC LIMIT 1" + ); + return rows.length ? rows[0] : null; +} + +export async function getWeightsRange(from, to) { + const d = await getDb(); + return d.select( + "SELECT * FROM weight_log WHERE date >= $1 AND date <= $2 ORDER BY date ASC, time ASC", + [from, to] + ); +} + +// --------------------------------------------------------------------------- +// Sleep log +// --------------------------------------------------------------------------- + +export async function getSleepForDate(date) { + const d = await getDb(); + const rows = await d.select("SELECT * FROM sleep_log WHERE date = $1", [date]); + return rows.length ? rows[0] : null; +} + +export async function saveSleep(date, bedTime, asleepTime, wakeTime, napMinutes, notes) { + const d = await getDb(); + const existing = await getSleepForDate(date); + if (existing) { + await d.execute( + `UPDATE sleep_log SET bed_time=$1, asleep_time=$2, wake_time=$3, + nap_minutes=$4, notes=$5 WHERE date=$6`, + [bedTime, asleepTime, wakeTime, napMinutes || 0, notes || null, date] + ); + } else { + await d.execute( + `INSERT INTO sleep_log (date, bed_time, asleep_time, wake_time, nap_minutes, notes) + VALUES ($1, $2, $3, $4, $5, $6)`, + [date, bedTime, asleepTime, wakeTime, napMinutes || 0, notes || null] + ); + } +} + +// --------------------------------------------------------------------------- +// Products +// --------------------------------------------------------------------------- + +export async function searchProducts(query, limit = 30) { + const d = await getDb(); + return d.select( + `SELECT id, name, brand, category, kcal_per_100g, protein_per_100g, + fat_per_100g, carbs_per_100g, fiber_per_100g + FROM products + WHERE name LIKE $1 OR brand LIKE $1 + ORDER BY name COLLATE NOCASE + LIMIT $2`, + [`%${query}%`, limit] + ); +} + +export async function getProduct(id) { + const d = await getDb(); + const rows = await d.select("SELECT * FROM products WHERE id = $1", [id]); + return rows.length ? rows[0] : null; +} + +export async function addProduct(p) { + const d = await getDb(); + const r = await d.execute( + `INSERT INTO products + (name, brand, category, weight_g, kcal_per_100g, protein_per_100g, + fat_per_100g, carbs_per_100g, fiber_per_100g, + vit_a_mcg, vit_c_mg, vit_d_mcg, vit_e_mg, vit_k_mcg, + vit_b1_mg, vit_b2_mg, vit_b3_mg, vit_b6_mg, vit_b12_mcg, folate_mcg, + calcium_mg, iron_mg, magnesium_mg, zinc_mg, copper_mg, + selenium_mcg, potassium_mg, sodium_mg, source) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26,$27,$28,$29)`, + [ + p.name, p.brand ?? null, p.category ?? null, p.weight_g ?? null, + p.kcal_per_100g ?? null, p.protein_per_100g ?? null, + p.fat_per_100g ?? null, p.carbs_per_100g ?? null, p.fiber_per_100g ?? null, + p.vit_a_mcg ?? null, p.vit_c_mg ?? null, p.vit_d_mcg ?? null, + p.vit_e_mg ?? null, p.vit_k_mcg ?? null, + p.vit_b1_mg ?? null, p.vit_b2_mg ?? null, p.vit_b3_mg ?? null, + p.vit_b6_mg ?? null, p.vit_b12_mcg ?? null, p.folate_mcg ?? null, + p.calcium_mg ?? null, p.iron_mg ?? null, p.magnesium_mg ?? null, + p.zinc_mg ?? null, p.copper_mg ?? null, + p.selenium_mcg ?? null, p.potassium_mg ?? null, p.sodium_mg ?? null, + p.source ?? "manual", + ] + ); + return r.lastInsertId; +} + +// --------------------------------------------------------------------------- +// Food log +// --------------------------------------------------------------------------- + +export async function getFoodForDate(date) { + const d = await getDb(); + return d.select( + `SELECT fl.*, p.name as product_name, p.brand as product_brand, + r.name as recipe_name + FROM food_log fl + LEFT JOIN products p ON fl.product_id = p.id + LEFT JOIN recipes r ON fl.recipe_id = r.id + WHERE fl.date = $1 + ORDER BY fl.time ASC`, + [date] + ); +} + +// Add a food log entry. Calculates macros from product if product_id given. +export async function addFoodEntry(date, time, productId, recipeId, amountG, note) { + const d = await getDb(); + let kcal = null, protein = null, fat = null, carbs = null, fiber = null; + + if (productId) { + const p = await getProduct(productId); + if (p && p.kcal_per_100g != null) { + const f = amountG / 100; + kcal = Math.round((p.kcal_per_100g ?? 0) * f); + protein = Math.round((p.protein_per_100g ?? 0) * f * 10) / 10; + fat = Math.round((p.fat_per_100g ?? 0) * f * 10) / 10; + carbs = Math.round((p.carbs_per_100g ?? 0) * f * 10) / 10; + fiber = Math.round((p.fiber_per_100g ?? 0) * f * 10) / 10; + } + } + + await d.execute( + `INSERT INTO food_log + (date, time, product_id, recipe_id, amount_g, kcal, protein_g, fat_g, carbs_g, fiber_g, note) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11)`, + [date, time, productId ?? null, recipeId ?? null, amountG, + kcal, protein, fat, carbs, fiber, note ?? null] + ); +} + +// Add food by manual kcal only (for backward compat with old text entries) +export async function addFoodManual(date, time, desc, kcal) { + const d = await getDb(); + await d.execute( + `INSERT INTO food_log (date, time, note, amount_g, kcal) + VALUES ($1, $2, $3, 0, $4)`, + [date, time, desc, kcal ?? null] + ); +} + +export async function deleteFoodEntry(id) { + const d = await getDb(); + await d.execute("DELETE FROM food_log WHERE id = $1", [id]); +} + +// --------------------------------------------------------------------------- +// Activity log +// --------------------------------------------------------------------------- + +export async function getActivitiesForDate(date) { + const d = await getDb(); + return d.select( + "SELECT * FROM activity_log WHERE date = $1 ORDER BY time ASC", + [date] + ); +} + +export async function addActivity(date, time, type, desc, minutes, kcal) { + const d = await getDb(); + await d.execute( + `INSERT INTO activity_log (date, time, type, desc, minutes, kcal) + VALUES ($1,$2,$3,$4,$5,$6)`, + [date, time, type ?? "manual", desc, minutes ?? null, kcal ?? null] + ); +} + +export async function deleteActivity(id) { + const d = await getDb(); + await d.execute("DELETE FROM activity_log WHERE id = $1", [id]); +} + +// --------------------------------------------------------------------------- +// Supplements +// --------------------------------------------------------------------------- + +export async function getSupplements() { + const d = await getDb(); + return d.select("SELECT * FROM supplements ORDER BY name COLLATE NOCASE"); +} + +export async function addSupplement(s) { + const d = await getDb(); + const r = await d.execute( + `INSERT INTO supplements + (name, daily_dose_g, vit_a_mcg, vit_c_mg, vit_d_mcg, vit_e_mg, vit_k_mcg, + vit_b1_mg, vit_b2_mg, vit_b3_mg, vit_b6_mg, vit_b12_mcg, folate_mcg, + calcium_mg, iron_mg, magnesium_mg, zinc_mg, copper_mg, selenium_mcg, + potassium_mg, sodium_mg) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21)`, + [ + s.name, s.daily_dose_g ?? null, + s.vit_a_mcg ?? null, s.vit_c_mg ?? null, s.vit_d_mcg ?? null, + s.vit_e_mg ?? null, s.vit_k_mcg ?? null, + s.vit_b1_mg ?? null, s.vit_b2_mg ?? null, s.vit_b3_mg ?? null, + s.vit_b6_mg ?? null, s.vit_b12_mcg ?? null, s.folate_mcg ?? null, + s.calcium_mg ?? null, s.iron_mg ?? null, s.magnesium_mg ?? null, + s.zinc_mg ?? null, s.copper_mg ?? null, s.selenium_mcg ?? null, + s.potassium_mg ?? null, s.sodium_mg ?? null, + ] + ); + return r.lastInsertId; +} + +export async function logSupplement(date, time, supplementId, doseG) { + const d = await getDb(); + await d.execute( + "INSERT INTO supplement_log (date, time, supplement_id, dose_g) VALUES ($1,$2,$3,$4)", + [date, time, supplementId, doseG ?? null] + ); +} + +export async function getSupplementLogForDate(date) { + const d = await getDb(); + return d.select( + `SELECT sl.*, s.name FROM supplement_log sl + JOIN supplements s ON sl.supplement_id = s.id + WHERE sl.date = $1 ORDER BY sl.time ASC`, + [date] + ); +} + +// --------------------------------------------------------------------------- +// Analytics helpers +// --------------------------------------------------------------------------- + +// Returns {kcal, protein_g, fat_g, carbs_g, fiber_g} totals for a date +export async function getFoodTotalsForDate(date) { + const d = await getDb(); + const rows = await d.select( + `SELECT + SUM(kcal) as kcal, + SUM(protein_g) as protein_g, + SUM(fat_g) as fat_g, + SUM(carbs_g) as carbs_g, + SUM(fiber_g) as fiber_g + FROM food_log WHERE date = $1`, + [date] + ); + return rows[0] || { kcal: 0, protein_g: 0, fat_g: 0, carbs_g: 0, fiber_g: 0 }; +} + +export async function getActivityTotalsForDate(date) { + const d = await getDb(); + const rows = await d.select( + "SELECT SUM(kcal) as kcal, SUM(minutes) as minutes FROM activity_log WHERE date = $1", + [date] + ); + return rows[0] || { kcal: 0, minutes: 0 }; +} + +// Weight data for charts: last N days +export async function getRecentWeights(days = 30) { + const d = await getDb(); + return d.select( + `SELECT date, MIN(weight_kg) as weight_kg + FROM weight_log + WHERE date >= date('now', $1) + GROUP BY date + ORDER BY date ASC`, + [`-${days} days`] + ); +} diff --git a/src/food-table.js b/src/food-table.js new file mode 100644 index 0000000..d10493b --- /dev/null +++ b/src/food-table.js @@ -0,0 +1,91 @@ +// Calorie database for common Russian products +// kcal_per_100g: calories per 100 grams +// default_portion_g: typical serving size in grams +// aliases: alternative names/spellings for parser matching + +export const FOODS = [ + // --- Grains & starchy --- + { key: "rice", name: "рис", aliases: ["рис", "рисов"], kcal: 130, portion: 200 }, + { key: "buckwheat", name: "гречка", aliases: ["гречк", "гречнев"], kcal: 110, portion: 200 }, + { key: "pasta", name: "макароны", aliases: ["макарон", "паст", "спагетти", "лапш"], kcal: 130, portion: 200 }, + { key: "potato", name: "картошка", aliases: ["картош", "картофел", "пюре"], kcal: 80, portion: 250 }, + { key: "potato_fried", name: "жареная картошка", aliases: [], kcal: 190, portion: 200 }, + { key: "bread_white", name: "хлеб белый", aliases: ["хлеб", "батон"], kcal: 250, portion: 30 }, + { key: "bread_black", name: "хлеб чёрный", aliases: ["чёрный хлеб", "бородинск"], kcal: 200, portion: 30 }, + { key: "corn_canned", name: "кукуруза конс.", aliases: ["кукуруз"], kcal: 80, portion: 100 }, + { key: "peas_canned", name: "горошек конс.", aliases: ["горош", "горох"], kcal: 70, portion: 100 }, + + // --- Dairy --- + { key: "tvorog", name: "творог", aliases: ["творог", "творож"], kcal: 120, portion: 150 }, + { key: "smetana", name: "сметана 20%", aliases: ["сметан"], kcal: 200, portion: 20 }, + { key: "milk", name: "молоко", aliases: ["молок", "молоч"], kcal: 55, portion: 250 }, + { key: "cheese_45", name: "сыр 45%", aliases: ["сыр"], kcal: 350, portion: 30 }, + { key: "cheese_proc", name: "плавленный сыр", aliases: ["плавлен"], kcal: 300, portion: 30 }, + { key: "butter", name: "масло сливочное", aliases: ["сливочн", "масло слив"], kcal: 720, portion: 10 }, + { key: "kefir", name: "кефир", aliases: ["кефир"], kcal: 45, portion: 250 }, + + // --- Meat & sausage --- + { key: "kolbasa", name: "колбаса", aliases: ["колбас", "докторск"], kcal: 300, portion: 50 }, + { key: "sausage_pork", name: "колбаса свиная", aliases: ["свин колбас"], kcal: 330, portion: 50 }, + { key: "sausage_beef", name: "колбаса говяжья", aliases: ["говяж колбас"], kcal: 250, portion: 50 }, + { key: "chicken", name: "курица", aliases: ["курин", "куриц", "курочк"], kcal: 165, portion: 150 }, + { key: "chicken_breast", name: "куриная грудка", aliases: ["грудк"], kcal: 110, portion: 150 }, + { key: "beef", name: "говядина", aliases: ["говядин"], kcal: 190, portion: 150 }, + { key: "pork", name: "свинина", aliases: ["свинин"], kcal: 260, portion: 150 }, + { key: "egg", name: "яйцо", aliases: ["яйц", "яичниц"], kcal: 155, portion: 60 }, + { key: "sosiska", name: "сосиска", aliases: ["сосиск"], kcal: 280, portion: 50 }, + + // --- Vegetables --- + { key: "cabbage", name: "капуста", aliases: ["капуст"], kcal: 25, portion: 150 }, + { key: "carrot", name: "морковь", aliases: ["морков"], kcal: 35, portion: 100 }, + { key: "radish", name: "редис", aliases: ["редис", "редьк"], kcal: 16, portion: 100 }, + { key: "cucumber", name: "огурец", aliases: ["огурц", "огурец"], kcal: 15, portion: 100 }, + { key: "tomato", name: "помидор", aliases: ["помидор", "томат"], kcal: 20, portion: 120 }, + { key: "onion", name: "лук", aliases: ["лук", "луков"], kcal: 40, portion: 50 }, + + // --- Berries & fruit --- + { key: "strawberry", name: "клубника", aliases: ["клубник"], kcal: 33, portion: 150 }, + { key: "raspberry", name: "малина", aliases: ["малин"], kcal: 40, portion: 150 }, + { key: "apple", name: "яблоко", aliases: ["яблок", "яблоч"], kcal: 47, portion: 150 }, + { key: "banana", name: "банан", aliases: ["банан"], kcal: 89, portion: 120 }, + + // --- Nuts & oils --- + { key: "nuts_mixed", name: "орехи", aliases: ["орех", "орешк"], kcal: 600, portion: 30 }, + { key: "sunflower_oil", name: "подсолн. масло", aliases: ["подсолн", "раст масл", "растительн"], kcal: 900, portion: 15 }, + + // --- Sweets & drinks --- + { key: "sugar", name: "сахар", aliases: ["сахар"], kcal: 400, portion: 5 }, + { key: "tea", name: "чай", aliases: ["чай", "чаю"], kcal: 0, portion: 300 }, + { key: "coffee", name: "кофе", aliases: ["кофе"], kcal: 2, portion: 200 }, + { key: "cola", name: "газировка", aliases: ["газировк", "кола", "пепси", "спрайт"], kcal: 42, portion: 330 }, + { key: "chips", name: "чипсы", aliases: ["чипс"], kcal: 530, portion: 50 }, + { key: "chocolate", name: "шоколад", aliases: ["шоколад"], kcal: 540, portion: 25 }, + { key: "cookie", name: "печенье", aliases: ["печень"], kcal: 430, portion: 30 }, + + // --- Soups & prepared --- + { key: "soup", name: "суп", aliases: ["суп", "борщ", "щи", "рассольник"], kcal: 50, portion: 300 }, + { key: "porridge", name: "каша", aliases: ["каш"], kcal: 100, portion: 250 }, +]; + +// Build a lookup map for fast access by key +export const FOOD_MAP = Object.fromEntries(FOODS.map((f) => [f.key, f])); + +// Find matching food by description text +// Returns array of {food, matchLen} sorted by match length descending +export function matchFoods(text) { + const lower = text.toLowerCase(); + const matches = []; + + for (const food of FOODS) { + for (const alias of food.aliases) { + if (lower.includes(alias.toLowerCase())) { + matches.push({ food, matchLen: alias.length }); + break; + } + } + } + + // Longer matches first (more specific) + matches.sort((a, b) => b.matchLen - a.matchLen); + return matches.map((m) => m.food); +} diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..d6f645c --- /dev/null +++ b/src/main.js @@ -0,0 +1,526 @@ +import { initDb, getFullProfile, setProfileValue, getLatestWeight, + getWeightsForDate, addWeight, deleteWeight, + getFoodForDate, addFoodManual, deleteFoodEntry, + getActivitiesForDate, addActivity, deleteActivity, + getFoodTotalsForDate, getActivityTotalsForDate, + getSleepForDate, saveSleep, + searchProducts } from "./db.js"; +import { calculateBmr, getAge } from "./bmr.js"; +import { today, currentTime } from "./date-utils.js"; + +const app = document.getElementById("app"); + +let state = { + screen: "today", + date: today(), + profile: {}, + latestWeight: null, +}; + +// --------------------------------------------------------------------------- +// Boot +// --------------------------------------------------------------------------- + +async function init() { + await initDb(); + state.profile = await getFullProfile(); + state.latestWeight = await getLatestWeight(); + + if (!state.profile.height_cm) { + renderScreen("settings"); + } else { + renderScreen("today"); + } +} + +// --------------------------------------------------------------------------- +// Router +// --------------------------------------------------------------------------- + +function renderScreen(name) { + state.screen = name; + const screens = { + today: renderToday, + products: renderProducts, + charts: renderCharts, + settings: renderSettings, + }; + app.innerHTML = ""; + const nav = buildNav(name); + app.appendChild(nav); + const content = document.createElement("div"); + content.className = "screen-content"; + app.appendChild(content); + screens[name](content); +} + +function buildNav(active) { + const nav = document.createElement("nav"); + nav.className = "bottom-nav"; + const items = [ + { id: "today", label: "Сегодня" }, + { id: "products", label: "Продукты" }, + { id: "charts", label: "Графики" }, + { id: "settings", label: "Профиль" }, + ]; + for (const item of items) { + const btn = document.createElement("button"); + btn.className = "nav-btn" + (item.id === active ? " active" : ""); + btn.textContent = item.label; + btn.onclick = () => renderScreen(item.id); + nav.appendChild(btn); + } + return nav; +} + +// --------------------------------------------------------------------------- +// TODAY screen +// --------------------------------------------------------------------------- + +async function renderToday(container) { + container.innerHTML = `
Загрузка...
`; + + const [weights, food, activities, sleep, foodTotals, actTotals] = + await Promise.all([ + getWeightsForDate(state.date), + getFoodForDate(state.date), + getActivitiesForDate(state.date), + getSleepForDate(state.date), + getFoodTotalsForDate(state.date), + getActivityTotalsForDate(state.date), + ]); + + const bmr = calcBmrForDay(weights); + + container.innerHTML = ` +
+ +

${formatDate(state.date)}

+ +
+ +
+
+ ${Math.round(foodTotals.kcal ?? 0)} + ккал приход +
+
+ ${bmr != null ? bmr + Math.round(actTotals.kcal ?? 0) : "—"} + ккал расход +
+
+ ${balanceStr(foodTotals.kcal, bmr, actTotals.kcal)} + баланс +
+
+ +
+
+

Вес

+ +
+
    + ${weights.map(w => ` +
  • + ${w.time ?? ""} + ${w.weight_kg} кг + +
  • `).join("") || `
  • нет данных
  • `} +
+
+ +
+
+

Еда

+ +
+
    + ${food.map(f => ` +
  • + ${f.time ?? ""} + ${escHtml(f.product_name ?? f.recipe_name ?? f.note ?? "—")} + ${f.amount_g ? `${f.amount_g}г` : ""} + + ${f.kcal != null ? Math.round(f.kcal) + " кк" : ""} + +
  • `).join("") || `
  • нет данных
  • `} +
+
+ +
+
+

Активность

+ +
+
    + ${activities.map(a => ` +
  • + ${a.time ?? ""} + ${escHtml(a.desc ?? "—")} + ${a.minutes ? `${a.minutes} мин` : ""} + + ${a.kcal != null ? "−" + Math.round(a.kcal) + " кк" : ""} + +
  • `).join("") || `
  • нет данных
  • `} +
+
+ +
+
+

Сон

+ +
+
+ ${sleep ? formatSleepInfo(sleep) : 'нет данных'} +
+
+ `; + + container.querySelector("#prev-day").onclick = () => { + state.date = shiftDate(state.date, -1); + renderScreen("today"); + }; + container.querySelector("#next-day").onclick = () => { + state.date = shiftDate(state.date, 1); + renderScreen("today"); + }; + + container.querySelectorAll(".del-btn").forEach(btn => { + btn.onclick = async () => { + const { table, id } = btn.dataset; + if (table === "weight") await deleteWeight(parseInt(id)); + if (table === "food") await deleteFoodEntry(parseInt(id)); + if (table === "activity") await deleteActivity(parseInt(id)); + renderScreen("today"); + }; + }); + + container.querySelector("#add-weight-btn").onclick = () => showWeightModal(); + container.querySelector("#add-food-btn").onclick = () => showFoodModal(); + container.querySelector("#add-act-btn").onclick = () => showActivityModal(); + container.querySelector("#add-sleep-btn").onclick = () => showSleepModal(sleep); +} + +// --------------------------------------------------------------------------- +// Modals +// --------------------------------------------------------------------------- + +function showModal(html, onSubmit) { + const overlay = document.createElement("div"); + overlay.className = "modal-overlay"; + overlay.innerHTML = ``; + document.body.appendChild(overlay); + overlay.querySelector("#modal-cancel").onclick = () => overlay.remove(); + overlay.querySelector("#modal-ok").onclick = async () => { + const ok = await onSubmit(overlay); + if (ok !== false) { overlay.remove(); renderScreen("today"); } + }; + overlay.onclick = (e) => { if (e.target === overlay) overlay.remove(); }; +} + +function showWeightModal() { + showModal(` +

Вес

+
+ + +
+
+ + +
+ `, async (overlay) => { + const kg = parseFloat(overlay.querySelector("#m-kg").value); + const time = overlay.querySelector("#m-time").value; + if (isNaN(kg)) return false; + await addWeight(state.date, time, kg); + }); +} + +function showFoodModal() { + showModal(` +

Еда

+
+ + +
+
+ + +
+
+ + +
+ `, async (overlay) => { + const desc = overlay.querySelector("#m-desc").value.trim(); + const kcal = parseFloat(overlay.querySelector("#m-kcal").value) || null; + const time = overlay.querySelector("#m-time").value; + if (!desc) return false; + await addFoodManual(state.date, time, desc, kcal); + }); +} + +function showActivityModal() { + showModal(` +

Активность

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ `, async (overlay) => { + const desc = overlay.querySelector("#m-desc").value.trim(); + const min = parseInt(overlay.querySelector("#m-min").value) || null; + const kcal = parseFloat(overlay.querySelector("#m-kcal").value) || null; + const time = overlay.querySelector("#m-time").value; + if (!desc) return false; + await addActivity(state.date, time, "manual", desc, min, kcal); + }); +} + +function showSleepModal(existing) { + showModal(` +

Сон

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ `, async (overlay) => { + const bed = overlay.querySelector("#m-bed").value || null; + const asleep = overlay.querySelector("#m-asleep").value || null; + const wake = overlay.querySelector("#m-wake").value || null; + const nap = parseInt(overlay.querySelector("#m-nap").value) || 0; + const notes = overlay.querySelector("#m-notes").value.trim() || null; + await saveSleep(state.date, bed, asleep, wake, nap, notes); + }); +} + +// --------------------------------------------------------------------------- +// PRODUCTS screen (Phase 2 stub) +// --------------------------------------------------------------------------- + +async function renderProducts(container) { + container.innerHTML = ` +
+

Продукты

+ +
    +
    + `; + + const input = container.querySelector("#prod-search"); + const list = container.querySelector("#prod-results"); + + input.oninput = async () => { + const q = input.value.trim(); + if (q.length < 2) { list.innerHTML = ""; return; } + const results = await searchProducts(q); + if (!results.length) { + list.innerHTML = `
  • Не найдено. Каталог ещё пуст — скоро добавим!
  • `; + return; + } + list.innerHTML = results.map(p => ` +
  • + ${escHtml(p.name)}${p.brand ? " " + escHtml(p.brand) + "" : ""} + ${p.kcal_per_100g != null ? Math.round(p.kcal_per_100g) + " кк/100г" : "?"} +
  • + `).join(""); + }; +} + +// --------------------------------------------------------------------------- +// CHARTS screen (Phase 4 stub) +// --------------------------------------------------------------------------- + +async function renderCharts(container) { + container.innerHTML = ` +
    +

    Графики

    +

    Будет готово в Phase 4. Данные уже пишутся в БД.

    +
    + `; +} + +// --------------------------------------------------------------------------- +// SETTINGS screen +// --------------------------------------------------------------------------- + +async function renderSettings(container) { + const p = state.profile; + + container.innerHTML = ` +
    +

    Профиль

    +
    + + +
    +
    + + +
    +
    + +
    + + +
    +
    +
    + + +
    +
    + + +
    + +
    + `; + + let selectedSex = p.sex ?? "m"; + container.querySelectorAll(".type-chip").forEach(btn => { + btn.onclick = () => { + selectedSex = btn.dataset.sex; + container.querySelectorAll(".type-chip").forEach(b => b.classList.remove("selected")); + btn.classList.add("selected"); + }; + }); + + container.querySelector("#save-profile").onclick = async () => { + const height = parseInt(container.querySelector("#s-height").value); + const year = parseInt(container.querySelector("#s-year").value); + const bFormula = parseInt(container.querySelector("#s-bmr-formula").value) || null; + const bEst = parseInt(container.querySelector("#s-bmr-est").value) || null; + + if (isNaN(height) || isNaN(year)) { + alert("Введите рост и год рождения"); + return; + } + + await setProfileValue("height_cm", height); + await setProfileValue("birth_year", year); + await setProfileValue("sex", selectedSex); + if (bFormula) await setProfileValue("bmr_formula", bFormula); + if (bEst) await setProfileValue("bmr_estimated", bEst); + + state.profile = { ...state.profile, height_cm: String(height), + birth_year: String(year), sex: selectedSex, + bmr_formula: bFormula ? String(bFormula) : null, + bmr_estimated: bEst ? String(bEst) : null }; + renderScreen("today"); + }; +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function calcBmrForDay(weights) { + const p = state.profile; + if (!p.height_cm || !p.birth_year) return null; + if (p.bmr_estimated) return parseInt(p.bmr_estimated); + + const kg = weights.length ? weights[weights.length - 1].weight_kg + : state.latestWeight?.weight_kg; + if (!kg) return parseInt(p.bmr_formula) || null; + return calculateBmr(kg, parseInt(p.height_cm), getAge(parseInt(p.birth_year)), p.sex ?? "m"); +} + +function balanceStr(intake, bmr, actKcal) { + if (bmr == null) return "—"; + const b = Math.round((intake ?? 0) - bmr - (actKcal ?? 0)); + return (b >= 0 ? "+" : "") + b; +} + +function balanceClass(intake, bmr, actKcal) { + if (bmr == null) return ""; + const b = (intake ?? 0) - bmr - (actKcal ?? 0); + return b > 100 ? "surplus" : b < -100 ? "deficit" : "balanced"; +} + +function formatSleepInfo(s) { + const parts = []; + if (s.asleep_time && s.wake_time) { + const h = sleepHours(s.asleep_time, s.wake_time); + parts.push(`Сон: ${h.toFixed(1)} ч (${s.asleep_time} – ${s.wake_time})`); + } + if (s.nap_minutes) parts.push(`Дневной: ${s.nap_minutes} мин`); + if (s.notes) parts.push(escHtml(s.notes)); + return parts.map(p => `${p}`).join(""); +} + +function sleepHours(asleep, wake) { + const a = timeToMin(asleep); + const w = timeToMin(wake); + const diff = a >= 720 ? (1440 - a + w) : (w - a); + return diff / 60; +} + +function timeToMin(t) { + if (!t) return 0; + const [h, m] = t.split(":").map(Number); + return h * 60 + m; +} + +function shiftDate(dateStr, days) { + const d = new Date(dateStr); + d.setDate(d.getDate() + days); + return d.toISOString().slice(0, 10); +} + +function formatDate(dateStr) { + const d = new Date(dateStr + "T00:00:00"); + return d.toLocaleDateString("ru-RU", { weekday: "long", day: "numeric", month: "long" }); +} + +// Prevent XSS when inserting user text into innerHTML +function escHtml(str) { + return String(str ?? "") + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); +} + +// --------------------------------------------------------------------------- +// Start +// --------------------------------------------------------------------------- + +init().catch(console.error); diff --git a/src/measures.js b/src/measures.js new file mode 100644 index 0000000..363cd29 --- /dev/null +++ b/src/measures.js @@ -0,0 +1,44 @@ +// Household measures → grams/ml +// Used by parser.js to convert "ст.л." → 15g, etc. + +export const MEASURES = { + // Spoons + "ч.л.": 5, + "чайная ложка": 5, + "чайн ложк": 5, + "д.л.": 10, + "десертная ложка": 10, + "ст.л.": 15, + "столовая ложка": 15, + "стол ложк": 15, + + // Cups / glasses + "стакан": 250, + "полстакана": 125, + "кружка": 300, + + // Pieces (generic, product-specific overrides in food-table) + "кусок хлеба": 30, + "кусок": 30, + "ломтик": 20, + "долька": 10, + + // Pinches + "щепотка": 1, + "горсть": 30, +}; + +// Pattern aliases for parser regex matching +export const MEASURE_PATTERNS = [ + { pattern: /\bч\.?\s*л\.?\b/i, grams: 5 }, + { pattern: /\bчайн\w*\s+ложк\w*\b/i, grams: 5 }, + { pattern: /\bд\.?\s*л\.?\b/i, grams: 10 }, + { pattern: /\bдесертн\w*\s+ложк\w*\b/i, grams: 10 }, + { pattern: /\bст\.?\s*л\.?\b/i, grams: 15 }, + { pattern: /\bстолов\w*\s+ложк\w*\b/i, grams: 15 }, + { pattern: /\bстакан\w*\b/i, grams: 250 }, + { pattern: /\bполстакан\w*\b/i, grams: 125 }, + { pattern: /\bкружк\w*\b/i, grams: 300 }, + { pattern: /\bгорст\w*\b/i, grams: 30 }, + { pattern: /\bщепотк\w*\b/i, grams: 1 }, +]; diff --git a/src/parser.js b/src/parser.js new file mode 100644 index 0000000..cf37863 --- /dev/null +++ b/src/parser.js @@ -0,0 +1,112 @@ +// Parse free-text amount input into grams +// Handles: "200г", "половина пачки 400г", "2 ст.л.", "3 куска", etc. + +import { MEASURE_PATTERNS } from "./measures.js"; + +// Extract a number near a gram-like unit +const GRAMS_RE = /(\d+(?:[.,]\d+)?)\s*(г|гр|грамм|g)\b/i; + +// Extract fraction of a package +const FRACTION_PACK_RE = /(?:полов\w*|пол|1\/2)\s*(?:пачк\w*|упаков\w*|банк\w*)/i; +const THIRD_PACK_RE = /(?:треть|1\/3)\s*(?:пачк\w*|упаков\w*|банк\w*)/i; +const QUARTER_PACK_RE = /(?:четверт\w*|1\/4)\s*(?:пачк\w*|упаков\w*|банк\w*)/i; +const PACK_WEIGHT_RE = /(\d+)\s*(г|гр|грамм|g)\b/i; + +// Count of items +const COUNT_RE = /(\d+)\s*(кус\w*|штук\w*|шт\.?)\b/i; + +// Duration for activities (minutes/hours) +const DURATION_MIN_RE = /(\d+)\s*(мин\w*|m)\b/i; +const DURATION_HR_RE = /(\d+(?:[.,]\d+)?)\s*(час\w*|ч\.?|h)\b/i; + +// Just a bare number +const BARE_NUMBER_RE = /^(\d+(?:[.,]\d+)?)$/; + +export function parseAmount(amountStr, description) { + if (!amountStr || !amountStr.trim()) { + return { grams: null, minutes: null, count: null, source: "default" }; + } + + const text = amountStr.trim().toLowerCase(); + + // 1. Exact grams: "200г" + let m = text.match(GRAMS_RE); + if (m) { + return { grams: parseFloat(m[1].replace(",", ".")), minutes: null, count: null, source: "grams" }; + } + + // 2. Fraction of package: "половина пачки 400г" + const combined = (amountStr + " " + (description || "")).toLowerCase(); + + if (FRACTION_PACK_RE.test(combined)) { + const pw = combined.match(PACK_WEIGHT_RE); + if (pw) { + return { grams: parseFloat(pw[1]) / 2, minutes: null, count: null, source: "half-pack" }; + } + } + + if (THIRD_PACK_RE.test(combined)) { + const pw = combined.match(PACK_WEIGHT_RE); + if (pw) { + return { grams: Math.round(parseFloat(pw[1]) / 3), minutes: null, count: null, source: "third-pack" }; + } + } + + if (QUARTER_PACK_RE.test(combined)) { + const pw = combined.match(PACK_WEIGHT_RE); + if (pw) { + return { grams: parseFloat(pw[1]) / 4, minutes: null, count: null, source: "quarter-pack" }; + } + } + + // 3. Duration (for activities): "30 мин", "1.5 часа" + m = text.match(DURATION_HR_RE); + if (m) { + return { grams: null, minutes: Math.round(parseFloat(m[1].replace(",", ".")) * 60), count: null, source: "hours" }; + } + + m = text.match(DURATION_MIN_RE); + if (m) { + return { grams: null, minutes: parseInt(m[1]), count: null, source: "minutes" }; + } + + // 4. Household measure: "2 ст.л." or just "ст.л." + for (const mp of MEASURE_PATTERNS) { + if (mp.pattern.test(text)) { + // Check for a multiplier before the measure + const before = text.split(mp.pattern)[0].trim(); + const multiplier = before.match(/(\d+)/) ? parseInt(before.match(/(\d+)/)[1]) : 1; + return { grams: mp.grams * multiplier, minutes: null, count: null, source: "measure" }; + } + } + + // 5. Count: "3 куска", "2 шт" + m = text.match(COUNT_RE); + if (m) { + return { grams: null, minutes: null, count: parseInt(m[1]), source: "count" }; + } + + // 6. Bare number → assume grams + m = text.match(BARE_NUMBER_RE); + if (m) { + return { grams: parseFloat(m[1].replace(",", ".")), minutes: null, count: null, source: "bare-grams" }; + } + + return { grams: null, minutes: null, count: null, source: "unknown" }; +} + +// Estimate kcal from parsed amount + food data +export function estimateKcal(parsed, food) { + if (!food) return null; + + if (parsed.grams != null) { + return Math.round((food.kcal / 100) * parsed.grams); + } + + if (parsed.count != null) { + return Math.round((food.kcal / 100) * food.portion * parsed.count); + } + + // Use default portion + return Math.round((food.kcal / 100) * food.portion); +} diff --git a/src/styles.css b/src/styles.css new file mode 100644 index 0000000..82aef64 --- /dev/null +++ b/src/styles.css @@ -0,0 +1,664 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +:root { + --bg: #121212; + --surface: #1e1e1e; + --primary: #bb86fc; + --on-bg: #e0e0e0; + --on-surface: #ffffff; + --positive: #cf6679; + --negative: #03dac6; + --muted: #888; + --border: #333; + --radius: 8px; +} + +body { + font-family: system-ui, -apple-system, sans-serif; + background: var(--bg); + color: var(--on-bg); + font-size: 16px; + line-height: 1.4; + overflow-x: hidden; + -webkit-tap-highlight-color: transparent; +} + +#app { + max-width: 480px; + margin: 0 auto; + min-height: 100vh; + display: flex; + flex-direction: column; +} + +/* Header / date navigation */ +.day-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + background: var(--surface); + border-bottom: 1px solid var(--border); + position: sticky; + top: 0; + z-index: 10; +} + +.day-header button { + background: none; + border: none; + color: var(--primary); + font-size: 24px; + padding: 8px; + cursor: pointer; + border-radius: var(--radius); +} + +.day-header button:active { + background: rgba(187, 134, 252, 0.15); +} + +.day-header .date-text { + font-size: 18px; + font-weight: 500; + color: var(--on-surface); +} + +/* Weight display */ +.weight-row { + padding: 8px 16px; + background: var(--surface); + border-bottom: 1px solid var(--border); + font-size: 14px; + color: var(--muted); +} + +.weight-row span { + color: var(--on-surface); + font-weight: 500; +} + +/* Entry list */ +.entries { + flex: 1; + overflow-y: auto; + padding: 8px 0; +} + +.entry-item { + display: grid; + grid-template-columns: 50px 1fr 70px; + align-items: center; + padding: 10px 16px; + border-bottom: 1px solid var(--border); +} + +.entry-item:active { + background: rgba(255, 255, 255, 0.05); +} + +.entry-time { + font-size: 13px; + color: var(--muted); +} + +.entry-desc { + font-size: 15px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.entry-kcal { + text-align: right; + font-size: 14px; + font-weight: 500; +} + +.entry-kcal.intake { + color: var(--positive); +} + +.entry-kcal.expenditure { + color: var(--negative); +} + +/* Balance summary */ +.balance-bar { + display: flex; + justify-content: space-around; + padding: 12px 16px; + background: var(--surface); + border-top: 1px solid var(--border); + font-size: 14px; +} + +.balance-bar .label { + color: var(--muted); + font-size: 12px; +} + +.balance-bar .value { + font-size: 18px; + font-weight: 600; +} + +.balance-bar .value.deficit { + color: var(--negative); +} + +.balance-bar .value.surplus { + color: var(--positive); +} + +/* FAB (add button) */ +.fab { + position: fixed; + bottom: 80px; + right: 24px; + width: 56px; + height: 56px; + border-radius: 50%; + background: var(--primary); + color: #000; + border: none; + font-size: 28px; + font-weight: 300; + cursor: pointer; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); + display: flex; + align-items: center; + justify-content: center; +} + +.fab:active { + transform: scale(0.95); +} + +/* Bottom nav */ +.bottom-nav { + display: flex; + justify-content: space-around; + padding: 10px 0; + background: var(--surface); + border-top: 1px solid var(--border); + position: sticky; + bottom: 0; +} + +.bottom-nav button { + background: none; + border: none; + color: var(--muted); + font-size: 12px; + padding: 4px 16px; + cursor: pointer; +} + +.bottom-nav button.active { + color: var(--primary); +} + +/* Modal / add entry overlay */ +.modal-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.7); + z-index: 100; + display: flex; + align-items: flex-end; + justify-content: center; +} + +.modal-sheet { + background: var(--surface); + width: 100%; + max-width: 480px; + border-radius: 16px 16px 0 0; + padding: 20px 16px; + max-height: 85vh; + overflow-y: auto; +} + +.modal-sheet h2 { + font-size: 18px; + margin-bottom: 16px; + color: var(--on-surface); +} + +/* Form elements */ +.form-group { + margin-bottom: 14px; +} + +.form-group label { + display: block; + font-size: 13px; + color: var(--muted); + margin-bottom: 4px; +} + +.form-group input, +.form-group select, +.form-group textarea { + width: 100%; + padding: 10px 12px; + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--radius); + color: var(--on-surface); + font-size: 16px; + font-family: inherit; +} + +.form-group input:focus, +.form-group select:focus, +.form-group textarea:focus { + outline: none; + border-color: var(--primary); +} + +.type-chips { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.type-chip { + padding: 6px 14px; + border-radius: 20px; + border: 1px solid var(--border); + background: none; + color: var(--on-bg); + font-size: 14px; + cursor: pointer; +} + +.type-chip.selected { + background: var(--primary); + color: #000; + border-color: var(--primary); +} + +.btn-row { + display: flex; + gap: 12px; + margin-top: 16px; +} + +.btn { + flex: 1; + padding: 12px; + border-radius: var(--radius); + border: none; + font-size: 16px; + font-family: inherit; + cursor: pointer; +} + +.btn-primary { + background: var(--primary); + color: #000; + font-weight: 600; +} + +.btn-secondary { + background: var(--border); + color: var(--on-bg); +} + +/* Setup screen */ +.setup-screen { + padding: 24px 16px; + display: flex; + flex-direction: column; + gap: 20px; + min-height: 100vh; + justify-content: center; +} + +.setup-screen h1 { + font-size: 24px; + color: var(--on-surface); + text-align: center; +} + +.setup-screen p { + color: var(--muted); + text-align: center; + font-size: 14px; +} + +/* Kcal preview */ +.kcal-preview { + padding: 10px 12px; + background: rgba(187, 134, 252, 0.1); + border-radius: var(--radius); + text-align: center; + font-size: 18px; + font-weight: 500; + color: var(--primary); +} + +/* Empty state */ +.empty-state { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + color: var(--muted); + font-size: 15px; + padding: 40px; + text-align: center; +} + +/* Screen layout */ +#app { + display: flex; + flex-direction: column; + min-height: 100vh; +} + +.bottom-nav { + order: 2; + position: sticky; + bottom: 0; +} + +.screen-content { + order: 1; + flex: 1; + overflow-y: auto; + padding-bottom: 8px; +} + +.nav-btn { + background: none; + border: none; + color: var(--muted); + font-size: 13px; + padding: 8px 12px; + cursor: pointer; + font-family: inherit; +} + +.nav-btn.active { + color: var(--primary); + font-weight: 600; +} + +/* Day navigation */ +.day-header h2 { + font-size: 15px; + font-weight: 500; + text-align: center; +} + +.day-nav { + background: none; + border: none; + color: var(--primary); + font-size: 20px; + padding: 4px 10px; + cursor: pointer; +} + +/* Summary cards */ +.summary-row { + display: flex; + gap: 8px; + padding: 10px 12px; + background: var(--surface); + border-bottom: 1px solid var(--border); +} + +.summary-card { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + padding: 6px 4px; + border-radius: var(--radius); + background: var(--bg); +} + +.summary-val { + font-size: 18px; + font-weight: 700; +} + +.summary-lbl { + font-size: 10px; + color: var(--muted); + text-align: center; +} + +.summary-card.surplus .summary-val { color: #cf6679; } +.summary-card.deficit .summary-val { color: #03dac6; } +.summary-card.balanced .summary-val { color: var(--on-bg); } + +/* Log sections */ +.log-section { + padding: 0 0 4px; + border-bottom: 1px solid var(--border); +} + +.section-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 14px 4px; +} + +.section-header h3 { + font-size: 13px; + color: var(--muted); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.add-btn { + background: none; + border: none; + color: var(--primary); + font-size: 22px; + line-height: 1; + cursor: pointer; + padding: 0 4px; +} + +.log-list { + list-style: none; + padding: 0; + margin: 0; +} + +.log-item { + display: flex; + align-items: center; + gap: 8px; + padding: 7px 14px; + border-top: 1px solid rgba(255,255,255,0.04); +} + +.log-time { + font-size: 12px; + color: var(--muted); + min-width: 38px; +} + +.log-desc { + flex: 1; + font-size: 14px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.log-desc small { + color: var(--muted); + margin-left: 4px; + font-size: 11px; +} + +.log-kcal { + font-size: 13px; + color: var(--muted); + min-width: 52px; + text-align: right; +} + +.log-empty { + padding: 6px 14px; + font-size: 13px; + color: var(--muted); + font-style: italic; +} + +.del-btn { + background: none; + border: none; + color: #555; + font-size: 14px; + cursor: pointer; + padding: 2px 4px; + flex-shrink: 0; +} + +.del-btn:active { color: #cf6679; } + +/* Sleep info */ +.sleep-info { + padding: 6px 14px 8px; + display: flex; + flex-direction: column; + gap: 2px; +} + +.sleep-line { + font-size: 13px; + color: var(--on-bg); +} + +/* Modal */ +.modal-overlay { + position: fixed; + inset: 0; + background: rgba(0,0,0,0.75); + z-index: 200; + display: flex; + align-items: flex-end; + justify-content: center; +} + +.modal { + background: var(--surface); + width: 100%; + max-width: 480px; + border-radius: 16px 16px 0 0; + padding: 20px 16px 16px; + max-height: 90vh; + overflow-y: auto; +} + +.modal h3 { + font-size: 17px; + margin-bottom: 16px; +} + +.modal-btns { + display: flex; + gap: 10px; + margin-top: 16px; +} + +.btn-primary, .btn-secondary { + flex: 1; + padding: 12px; + border-radius: var(--radius); + border: none; + font-size: 16px; + font-family: inherit; + cursor: pointer; +} + +.btn-primary { + background: var(--primary); + color: #000; + font-weight: 600; +} + +.btn-secondary { + background: var(--border); + color: var(--on-bg); +} + +.full-width { width: 100%; } + +/* Products screen */ +.stub-screen, .settings-screen { + padding: 16px; +} + +.stub-screen h2, .settings-screen h2 { + font-size: 20px; + margin-bottom: 16px; +} + +.stub-note { + color: var(--muted); + font-size: 14px; + margin-top: 8px; +} + +.search-input { + width: 100%; + padding: 10px 14px; + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--radius); + color: var(--on-surface); + font-size: 16px; + font-family: inherit; + margin-bottom: 12px; +} + +.search-input:focus { + outline: none; + border-color: var(--primary); +} + +.prod-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 9px 4px; + border-bottom: 1px solid var(--border); +} + +.prod-name { + font-size: 14px; + flex: 1; +} + +.prod-name small { + color: var(--muted); + font-size: 12px; + margin-left: 4px; +} + +.prod-kcal { + font-size: 13px; + color: var(--muted); +} + +/* Loading */ +.loading { + text-align: center; + padding: 40px; + color: var(--muted); +}