Commit graph

280 commits

Author SHA1 Message Date
fda820568e Compass map rotation, landscape layout, GPS placeholder
- leaflet-rotate plugin: map rotates by compass heading via deviceorientation
- Landscape detection: CSS viewport rotation when phone held sideways (gamma > 50)
- Bottom nav hidden in landscape mode
- GPS placeholder: pixel art overlay shown until GPS signal arrives
- invalidateSize() after map init to prevent black screen
2026-06-25 16:54:45 +03:00
ad33c6defe Update README with manual routes, export/import, OTA router 2026-06-18 22:55:32 +03:00
19282ded78 Sensors, walk limits, GPS fixes, water UI, manual routes, Gridnev products, OTA router
GPS altitude: accel-based glitch filter (accel=0 + speed = GPS drift), altitude
  jump >100m filter, timestamp sanity (>24h = remove). Per-point Crr from accel
  vibration (smooth/mixed/gravel). Gap interpolation on map with dashed lines
  when accel confirms movement. Walk/run separate speed limits (configurable).
  Adaptive GPS accuracy (auto x2 when signal bad). Dead reckoning points when
  GPS silent but accel shows movement. Locale.US fix for sensor JSON.

Sensors: accelerometer + gyroscope in GpsRecorderService (RMS, max, pitch, yaw).
  Columns in gps_points. Dynamic addGpsPoint INSERT (safe if columns missing).

Manual routes: draw on map by tapping points, elevation from past tracks +
  open-meteo API, realistic timestamps by activity speed. Manual sessions
  skip glitch filtering on recalculate. Speed from GPS.config not hardcoded.

Water tracking: water_pct field in product add/edit forms, searchProducts
  returns water_pct, "ml" label for liquids, recipe water from ingredients.

BMR: moving average weight (7 days), fixed charts using dailyBase not raw bmr.

BLE: ble_clear_hr on stop, recovery toast, serde alias for ts/timestamp.

Products: 217 Gridnev products (bakery, desserts, bread, ready meals).
  Magnit scraper fix (NUXT_DATA name/value pairs, not indices). Supplements
  addSupplement with all 41 columns. Nutrient fallback RDI includes omega/EPA/DHA.

Export/import: custom products + recipes + supplements in single JSON bundle.
  Export all or single product. Dedup on import by name_lower + source.

OTA: router upload (LAN, always), VPS upload (-u flag). Kotlin checks router
  first (2s timeout), then VPS. Update URL from update_info.json.

Session detail: renderToken prevents stale setTimeout callbacks after recalculate.
WAL disabled (PRAGMA journal_mode=DELETE).
2026-06-18 22:54:45 +03:00
926fd70c88 Update README and PROJECT_MAP with BLE HR, water tracking, OTA, nutrients, altitude filter 2026-06-07 01:41:33 +03:00
7c93bb75c3 OTA updates, BLE HR, water tracking, GPS altitude filter, BMR fix, nutrient units
OTA: VPS nginx endpoint, build.sh -u upload flag, Kotlin check + browser download
BLE HR: FitPro protocol (PAIR/CCCD/HR_SWITCH/REALTIME/MEASURE), GATT queue,
  ACK responses, auto-detect device type, auto-start with GPS, re-trigger 25s
  Kotlin BleHrService, btleplug for desktop, file IPC for Android
Water: products.water_pct column, getDailyWaterMl(), progress bar on today screen
GPS altitude: outlier spike filter (>4m), stuck run interpolation (5+ identical),
  configurable smoothing (altSmoothHalf setting), filtered raw for kcal calc
BMR: removed broken estimateRealBmr, added activity level dropdown (1.2-1.725),
  fixed 7-day trend using dailyBase instead of raw bmr
Nutrients: ginseng/resveratrol/coq10 columns, fixed folate/selenium units (removed
  mult:1000), iodine/chromium input in mcg, addSupplement now inserts all 41 columns
UI: parseAmount() for 4*30 expressions, recipe portion amount field, nutrient
  reserves strip, GPS sessions in today view, pan on charts, showToast(),
  HR norms in settings, HR on recording screen, HR chart in track details
Scrapers: 5ka Load JSON button, magnit_scraper.user.js (NUXT_DATA parsing)
2026-06-07 01:36:38 +03:00
0bafb2b317 АУП norms with star, nutrient half-life table, recipe nutrients in daily strip
RDI/АУП:
- rdi_values.norm_type column: 'rdi' or 'aup'
- АУП values seeded: omega3 1000mg, EPA/DHA 250mg, lutein 5mg,
  lycopene 5mg, silicon 30mg, vanadium 0.04mg (all 3 standards)
- Nutrient chips show * after label for АУП norms
- Supplement form placeholders show RDI/АУП values (with * for АУП)

Half-life table:
- nutrient_halflife: nutrient_key, min/max/avg days, source
- 32 nutrients seeded with whole-body biological half-life data
- Sources: PubMed, ICRP, Performance Lab, clinical studies

Recipe nutrients in daily strip:
- recipes.cached_nutrients TEXT column (JSON)
- cacheRecipeNutrients() computes and stores per-serving nutrients
- Called on recipe save in editor
- calcDailyNutrients() now reads cached_nutrients for recipe food entries
- getFoodForDate() JOINs r.cached_nutrients

BMR.loadRDI() now loads norm_type alongside values.
BMR.RDI_TYPES exposed for star rendering.
2026-06-02 18:39:09 +03:00
c72a22a965 Fix dedup FK: delete product_units refs before products, remove PRAGMA hack
FOREIGN KEY constraint failed because product_units table has FK to
products(id) and wasn't cleaned before DELETE. Now: delete orphaned
product_units rows first, then delete dupe products. No PRAGMA needed.

Verified on desktop: 16675 → 8330 with FK enforcement ON.
2026-06-01 22:43:02 +03:00
46ad9022c2 Fix dedup: PRAGMA foreign_keys=OFF before delete, alert on seed error
FOREIGN KEY constraint failed on desktop (rusqlite enables FK by default).
Android was silently failing too. Fix: disable FK during dedup, re-enable after.

Tested: desktop 16675 → 8330 products, idx_products_dedup created.
Also: alert() instead of boot-label for seed errors (visible to user).
2026-06-01 21:25:23 +03:00
ed20d4a9c8 Fix DB path: 3 levels up from app_data_dir, not 2 (files/dev.baldav.app/ → root) 2026-06-01 17:33:59 +03:00
9a4d62dd03 Fix dedup not running: Rust opened wrong DB path on Android
Root cause: app_data_dir() = files/dev.baldav.app/ but tauri-plugin-sql
stores baldav.db in app root /data/user/0/pkg/baldav.db (two levels up).
Rust created empty DB at wrong path, dedup ran on empty DB = no-op.

Fix: added p_root = app_data_dir/../../../baldav.db check.
Also: show seed error on boot label instead of swallowing silently.
2026-06-01 17:21:36 +03:00
a0c7dfb5e2 Fix dedup: run BEFORE version check, RDI standards from DB
Dedup fix:
- Dedup SQL now runs BEFORE seed version check (was inside "if new version" block,
  never executed when version matched). 16675→8330 products.
- UNIQUE INDEX on (name_lower, source, COALESCE(store_id,'')) prevents future dupes
- Errors propagated via map_err (no more silent failures)

RDI standards from DB (not hardcoded):
- rdi_standards table: RU, EU, US
- rdi_values table: nutrient_key + value per standard
- 26 nutrients seeded for all 3 standards
- BMR.loadRDI(code) reads from DB, replaces hardcoded RDI object
- Settings: "Стандарт РСП" dropdown (РФ/ЕС/США)
- Profile stores selected standard, loaded at init
- Fallback: hardcoded RU values until DB ready
2026-06-01 16:45:30 +03:00
01427941d4 Fix dedup: use execute() not execute_batch(), propagate errors
execute_batch() silently failed on multi-statement dedup SQL.
Split into 4 separate execute() calls with map_err — errors
now propagate to JS and show to user instead of being swallowed.
2026-06-01 14:58:15 +03:00
b2b6b6a7ef Dedup products, fix labels, fix track comparison, seed v9
Dedup products in Rust import (not JS — JS was too slow/hung):
- Key: (name_lower, source, COALESCE(store_id,''))
- Same product from same store = duplicate, deleted
- Different sources (magnit vs ion vs manual) = separate products
- Reassigns food_log + recipe_ingredients refs before deleting
- UNIQUE INDEX prevents future dupes (INSERT OR IGNORE now works)
- Tested: 16675 -> 8330 in 0.15s on device DB copy

Removed JS dedup migration (replaced by Rust).

Order in import_seed_products:
1. Index name_lower
2. Dedup (reassign refs + delete)
3. UNIQUE INDEX
4. INSERT OR IGNORE from seed
5. UPDATE store products
6. seed_version + COMMIT
2026-06-01 14:51:38 +03:00
45d3736a99 Chart zoom 2x faster, chart padding, clearer food cost format
Charts:
- Zoom speed: 0.02 -> 0.04 (all 5 chart instances)
- stats-chart-block: added padding 0 14px to match content margins

Food cost of workout:
- Was: "1.8 x Хлеб (102г, 273 кк)" — ambiguous "1.8 x"
- Now: "1.8 порц. Хлеб (по 102г, 273 кк/порц.)" — clear units
- Header: "485 ккал — это как:" instead of "485 ккал ="
- Math: 485/273 = 1.78 portions of 102g each

Duplicates: seed DB has none (verified). Device dupes from pre-dedup
imports — JS migration handles them on next launch.
2026-06-01 13:51:47 +03:00
8ad8fc2da8 Source badges in all search dropdowns (food, ingredient, edit food) 2026-05-31 01:30:25 +03:00
b4255ef2d5 Block product deletion if used in food log or recipes 2026-05-31 00:48:23 +03:00
bcb4124be0 Delete product button on expanded card, deleteProduct DB function
- "Уд." button (red) next to "Ред." on product card
- deleteProduct: nullifies food_log refs, zeros recipe_ingredients refs, deletes product
- List refreshes after deletion
2026-05-30 21:08:32 +03:00
d365554a7b Fix dedup migration: 3 bulk SQL queries instead of per-dupe loop
Was: for loop with 3 IPC calls per duplicate group = thousands of
round-trips = 10+ minutes on phone. Now: 3 bulk UPDATE/DELETE
statements that run in SQLite directly. One-time migration,
subsequent launches skip (COUNT=0).
2026-05-30 20:55:20 +03:00
0136b8ae0e Dedup products, fix labels, fix track comparison, seed v9
1. Deduplicate products: 13482 -> 8325 in seed DB. JS migration
   deduplicates app DB on launch: reassigns food_log and
   recipe_ingredients to kept product before deleting dupes.

2. MICRO_FIELDS: full chemical names + alternatives for all nutrients.
   "B3 / PP / ниацин", "B9 / фолиевая", "Кальций / Ca" etc.

3. Track comparison: newer track always compared against older.
   Green = newer faster (progress), Red = newer slower (regression).
   Removed separate cyan dashed track — only colored segments.
   Order of selection doesn't matter — date determines which is newer.

4. Source badges in search: source column was already in SELECT,
   badges render via sourceBadge(p.source) in prod card.

5. Gap threshold already cosmetic-only (confirmed, no changes needed).

Seed DB v9, VACUUM'd, indexed.
2026-05-30 18:09:16 +03:00
81125c03fa Remove unused Emitter import and mut inserted warning 2026-05-30 16:32:26 +03:00
44dfa0b88a Add name_lower index: seed import 56s -> 0.09s (600x faster)
ATTACH UPDATE with correlated subqueries on name_lower was doing
full table scans on 13000 rows. 56 seconds on desktop, 2-5 min
on phone. User saw "загрузка..." for minutes on every launch.

Fix: CREATE INDEX on name_lower in both main and seed DBs before
the UPDATE. Also pre-built index in seed DB file.

Benchmarked: 56.5s -> 0.09s on desktop. Phone should be <1s.
2026-05-30 16:24:47 +03:00
8d9b5aaf7e Source badges on products: Магнит (red), 5ка (green), KDV (orange), ИОН (gray), Свой (yellow)
- sourceBadge() helper renders colored badge by source field
- searchProducts SELECT now includes source column
- getFoodForDate SELECT includes p.source as product_source
- Badge shown on product cards in search and food log entries
- CSS: .src-badge with per-source colors (pastel, dark-theme friendly)
2026-05-30 15:59:50 +03:00
2df5908fca Fix product save: add name_lower, product-only micro fields, strawberry
Root cause: addProduct INSERT didn't include name_lower column.
Search uses WHERE name_lower LIKE — products with NULL name_lower
never found. User entered 20 min of strawberry data for nothing.

Fixes:
- addProduct: includes name_lower = name.toLowerCase()
- Added phosphorus_mg and carotene_mcg to addProduct INSERT
- PRODUCT_MICRO_FIELDS: subset of MICRO_FIELDS for product forms
  (excludes supplement-only fields: omega, EPA/DHA, fish oil etc.)
- showAddProductModal and showEditProductModal use PRODUCT_MICRO_FIELDS
- Rust: removed stale inserted+=1 line
- Seed DB v8: added "Клубника свежая" (alias for "Земляника садовая")
2026-05-30 15:33:59 +03:00
f20d45aa18 Bulk seed import: INSERT SELECT instead of per-row loop
Was: 13000 individual INSERT OR IGNORE in a loop = 2+ minutes.
Now: one INSERT ... SELECT FROM seed.products = seconds.

Flow:
1. ATTACH seed DB
2. INSERT OR IGNORE ... SELECT (bulk, new products)
3. UPDATE store products with KBJU + micronutrients (user_edited=0 only)
4. UPDATE nutrients_guessed flag
5. INSERT OR REPLACE seed_version into profile
6. COMMIT (all or nothing — crash-safe)
7. DETACH

seed_version written INSIDE transaction — if app crashes before COMMIT,
next launch re-does the import. No partial state.
2026-05-29 22:33:12 +03:00
ca45a5acc0 Protect user-edited products from seed overwrites
New column: user_edited INTEGER DEFAULT 0 on products table.
- JS migration adds column
- showEditProductModal sets user_edited=1 on save
- Rust seed import: UPDATE store products (source IN magnit/5ka/kdv/ion)
  WHERE user_edited IS NULL OR user_edited = 0
- Now updates KBJU + all micronutrients from seed (not just INSERT OR IGNORE)
- User edits preserved: user_edited=1 products never touched by seed import
- Manual/recipe_import products also never touched (source check)

Flow: seed update fixes wrong KBJU (e.g. bread 268→243) automatically,
but if user already fixed it manually — their fix stays.
2026-05-29 22:12:37 +03:00
3789747729 Edit product from search card, fix Ayutinskiy bread kcal
Product editing:
- "Ред." button on expanded product card
- showEditProductModal: edit name, KBJU, all micronutrients
- Updates product in DB directly, refreshes search

Bread fix:
- "Хлеб Тостовый пшеничный Аютинский 280г" was 268 in Magnit data
- Package says 243 kcal/100g — corrected in seed DB
- Seed version 7

Users can now fix any wrong KBJU directly from the product card
without re-importing or editing the seed DB manually.
2026-05-29 22:01:52 +03:00
25849b8537 Fix export: use Tauri save_file instead of blob, text button labels
Export was broken on Android — blob download (URL.createObjectURL)
doesn't work in WebView. Now uses Tauri save_file command which
triggers SAF file picker (same as GPX export).

Replaced cryptic arrow icons with readable text: "Импорт" / "Экспорт"
on both recipes and supplements screens.
2026-05-29 21:27:31 +03:00
5ecce90744 Export/import recipes and supplements via JSON files
Recipes:
- Export all: downloads baldav-recipes.json with ingredients + product KBJU
- Export one: button in recipe detail downloads {name}.json
- Import: reads JSON file, creates recipes + missing products

Supplements:
- Export all: downloads baldav-supplements.json
- Import: reads JSON, creates supplements (skips id, gets new one)

Buttons: arrow-down (import) and arrow-up (export) next to + on both screens.
Uses downloadFile() helper for browser download, pickFile() for file selection.
Import matches products by name, creates missing ones from exported KBJU.
2026-05-29 21:13:44 +03:00
7b41382ea4 Add-btn border: gray (--border) instead of primary 2026-05-29 20:30:36 +03:00
f871e5bb28 Add custom product with full nutrients, bigger + buttons with border
Custom product:
- "+" button next to search on Products tab
- showAddProductModal: name, KBJU, + all MICRO_FIELDS (34 nutrient fields)
- All optional except name
- Creates product with source="manual"
- Refreshes search after save

Button style:
- .add-btn: 0.5px border with primary color, rounded corners (6px)
- Min 40x36px for finger-friendly tapping on retina
- Flexbox centered content
2026-05-29 18:21:49 +03:00
b4b2914d00 Convert beta-carotene to vitamin A, remove from nutrient strip
12 mcg beta-carotene = 1 mcg retinol equivalent (vitamin A).
Carotene from food and supplements now adds to vit_a_mcg total.
Removed carotene_mcg from RDI table and NUTRIENT_LABELS —
no longer shows as separate "b-car 0%" chip misleading users.

Carrot 150g: 18000 mcg carotene / 12 = 1500 mcg vit A = 188% RDI.
Supplement field "Бета-каротин" kept in form for data entry.
2026-05-29 18:09:29 +03:00
cbac8d5d7b Fix carotene missing from food query, add fish oil field, full nutrient SELECT
- getFoodForDate: added p.phosphorus_mg, p.carotene_mcg (were missing)
- searchProducts: added carotene, selenium, zinc, copper, folate, D, K, B6, B12
- Supplement form: "Рыбий жир (мг)" field for total capsule weight
- Omega fields relabeled: "в т.ч." prefix to show they're part of fish oil

Carrot (12000 mcg carotene) was showing 0% because carotene_mcg
was not in the getFoodForDate SELECT.
2026-05-29 18:00:02 +03:00
e6674e2014 Fix B9 and Se showing 0%: unit mismatch mg vs mcg
B9 (folate) label said "мкг" but packaging shows "мг" — user entered
0.4 mg, DB stored 0.4 mcg, 0.4/200 = 0%. Fixed: label says "мг",
mult:1000 converts to mcg on save, back to mg on display.
Same for selenium: packaging uses mg, DB uses mcg.

Beta-carotene showing 0% is correct — Daily Men supplement contains
retinol acetate (vit A), not beta-carotene. They are different forms.
2026-05-29 17:53:57 +03:00
22a2f6296d Update PROJECT_MAP, README, session logs with current stats 2026-05-29 17:47:58 +03:00
4d97ba607b Always copy seed DB from assets, VACUUM seed to reclaim space
Size comparison was unreliable — SQLite reuses free pages internally,
so adding products didn't change file size. New seed never copied.
Now: always overwrite on every launch (~8MB, <1 sec).
VACUUM reduced seed from 8.5MB to 8.2MB.
2026-05-29 15:38:10 +03:00
636de9bc25 Add EPA and DHA fields to supplements (omega-3 breakdown) 2026-05-29 15:21:59 +03:00
c986b6af9a Russian names for minerals in supplement form: Кальций, Магний, Железо etc. 2026-05-29 14:59:15 +03:00
818276d60c Merge two nutrient strips into one: scroll collapsed, wrap expanded
Was: upper micro-strip (supplements only, scroll) + lower nutrient-strip
(food+supps, expand). Now: ONE strip at top that does both.

Collapsed: horizontal scroll, single row, no wrap.
Tap: expands to full wrap view showing all nutrients.
Tap again: collapses back to scroll row.

Data source: food (product micronutrients on-the-fly) + supplements.
Sorted by % RDI ascending (most deficient first).

Removed: buildMicroStrip() call, .micro-strip element, old scroll handler.
2026-05-29 14:55:59 +03:00
de5cfd5c21 Manual KBJU input, omega-3/6/9, product micronutrients, nutrient strip expand
1. Manual food entry: added B/Zh/U fields. Creates custom product in DB
   with user-provided KBJU, then logs it. Fixed addProduct lastInsertId.

2. Omega 3-6-9: columns in supplements, in supplement totals query,
   in MICRO_FIELDS form (34 fields total now).

3. Product card micronutrients: searchProducts now fetches all micro
   columns. renderProdDetails shows nutrient chips (sorted by deficiency)
   when product is expanded.

4. Supplement form: added B5, B7, chromium, manganese, molybdenum,
   iodine, phosphorus, omega-3/6/9, silicon, vanadium, lutein, lycopene.

5. Nutrient strip: tap to expand/collapse. Collapsed = 1 row (36px).
   Tap = toggle full view showing all nutrients.

6. Seed DB v6: added "Тесто для чебуреков 500г" (261 kcal, 7.1/3.8/48.2).
2026-05-29 14:47:09 +03:00
885ef27f36 RDI table, nutrient % in food/recipes, daily deficiency strip
RDI (bmr.js):
- 27 nutrients per TR TS 022/2011 Russian daily intake standards
- Exposed as window.BMR.RDI

Supplement columns (db.js):
- 12 new columns: B5, B7(biotin), D, K, Cr, Mn, Mo, I, P, Si, V, lutein, lycopene, carotene
- getSupplementTotalsForDate expanded to sum all new columns

Food nutrient display (main.js):
- Removed asterisk (*) from daily kcal (guessed tint stays)
- getFoodForDate now JOINs all micronutrient columns from products
- foodNutrientLine() shows "C 12%, Ca 5%, Fe 8%..." under each food item
- calculateRecipeNutrition expanded to 19 micronutrients with cooking loss

Daily nutrients section (main.js):
- "Нутриенты" section on Today screen between Добавки and Сон
- calcDailyNutrients() sums food (on-the-fly from products) + supplements
- Sorted by % RDI ascending — most deficient first
- Color coded: red < 30%, orange 30-80%, green >= 80%

Recipe detail:
- "Микронутриенты (на порцию, % РСП)" chips section
2026-05-29 13:38:14 +03:00
03358964e2 Fix all querySelector('.modal-overlay') to grab last (topmost) overlay
When a sub-modal opens over a parent modal, querySelector returns the
FIRST (parent) overlay, not the new one. Compare items, recipe buttons,
goal markers — none of them got onclick bindings because they searched
in the wrong overlay.

All 11 occurrences replaced with [...querySelectorAll(...)].pop() to
always get the topmost (most recently added) modal overlay.
2026-05-29 10:41:10 +03:00
d5fa37a9fa Fix recipe save: lastInsertId was undefined, ingredients got null recipe_id
tauri-plugin-sql execute() returns { lastInsertId } but it's unreliable
(returns 0 or undefined for INSERT). New recipe got rid=undefined,
then addRecipeIngredient failed with NOT NULL constraint on recipe_id.

Fix: query actual id after insert with SELECT id ORDER BY id DESC LIMIT 1.
Edit worked because rid came from the existing recipeId parameter.
2026-05-29 09:25:30 +03:00
1f53b8fcc2 Add try/catch + alert on modal save button to surface hidden errors 2026-05-28 23:56:37 +03:00
45341ddba4 Real BMR from weight change history, not just formula
Added BMR.estimateRealBmr(weights, foodTotals):
- Uses actual weight delta + total food intake over 30 days
- Formula: realExpenditure = (totalIntake + dWeight_kg * 7700) / days
- 7700 kcal per kg of fat, negative dWeight = weight loss
- Requires 3+ days of data and 2+ weight entries
- Sanity check: rejects < 500 kcal/day as bad data
- Overrides formula BMR when enough data available

renderToday now queries recent weights + food and uses real BMR
if calculated, falling back to Mifflin-St Jeor formula.
2026-05-28 23:17:45 +03:00
2b147ef1dc Fix expenditure showing -- when BMR or profile incomplete
Two issues:
1. expend was null when BMR=null, even if GPS sessions had kcal.
   Now shows activity kcal alone if BMR unavailable.
2. calcBmr returned null if height/birth_year missing, but user
   had weight in profile. Now falls back: weight_log -> latestWeight
   -> profile.weight_kg. Also returns bmr_formula if height missing
   instead of null.

BMR calculation chain:
- bmr_estimated (manual override) takes priority
- Then formula: needs height + birth_year + weight (from any source)
- Then bmr_formula (pre-computed value in profile)
- Last resort: null → expenditure shows activity kcal only
2026-05-28 23:15:35 +03:00
5f0c21741c Fix remaining overlay.remove bypassing winStack
Found 5 places that did overlay.remove() + history.back() directly
instead of going through winStack:
- Session detail recalculate/refresh
- Delete supplement
- Delete bike
- Delete goal marker
- Goal marker add button
- Compare session selection

All converted to: winStack.pop().close() + _skipPops + history.back()
No more direct overlay.remove() outside of winStack close handlers.
2026-05-28 22:42:45 +03:00
4960f2a863 Rewrite fullscreen as history entries: each open=push, each back=pop
Single winStack[] array + single popstate handler replaces:
- Per-modal popHandlers (removed)
- _modalPopHandled flag (removed)
- overlay._pushFullscreen (removed)
- _suspended/resume mechanism (removed)

How it works:
- winPush(type, closeFn): push entry + history.pushState
- winPopTop(): pop top entry, call close(), return true
- closeModal(): pop all entries down to stackBase, history.go(-count)
- _skipPops counter: closeModal's history.go fires popstate — skip N events

Every open (modal, fullscreen map, fullscreen chart) = 1 winPush.
Every back = 1 winPopTop via popstate.
closeModal for buttons = pop N entries + history.go(-N) + skip N pops.

Sub-modals work because stackBase tracks where each modal started.
Ingredient modal pops only its own entry, recipe editor stays.
2026-05-28 22:24:57 +03:00
94ad61f5b9 Fix ingredient modal closing recipe editor — no setTimeout hacks
Root cause: closeModal of sub-modal called history.back(), parent
modal's popHandler was resumed and caught the popstate, closing itself.

Fix: popHandler checks _modalPopHandled flag first. When sub-modal's
closeModal sets the flag before history.back(), parent popHandler sees
it and skips. Global handler also sees it and skips.

Removed suspend/resume mechanism — replaced with simple flag check.
Each popHandler also checks overlay.parentNode for safety.
2026-05-28 14:31:20 +03:00
f395e8ec07 Fix sub-modal (ingredient) closing parent modal (recipe editor)
Problem: ingredient modal's closeModal called history.back(), which
triggered recipe editor's popHandler → closed recipe editor too.

Fix: modal suspend/resume system.
- When sub-modal opens, parent modal's popHandler is suspended
- When sub-modal closes, parent is resumed
- Parent popHandler ignores popstate events while suspended
- closeModal resumes parent before calling history.back()

Also: closeModal now pops exactly 1 history entry (not _historyDepth).
The _historyDepth counter tracks fullscreen entries within the modal,
which are handled by popHandler's decrements. closeModal only needs
to pop the modal's own base entry.
2026-05-27 22:55:43 +03:00
d59013cc62 Fix ingredient modal closing recipe editor
Ingredient modal's onSubmit returned undefined → showModal treated it
as success → closeModal + renderScreen → killed the parent recipe editor.

Fix: ingredient onSubmit returns "norender", showModal checks for it
and skips renderScreen. Modal still closes via closeModal/history.back,
returning to the recipe editor with the new ingredient added.
2026-05-27 20:53:00 +03:00