Product catalog cleanup (no-badge unused) + order-independent search
- searchProducts: order-independent token AND (every whitespace word must appear in name, any order), ranked full-substring > prefix > length. Fixes needing to type words in the name's order. - Cleanup of unlabeled junk (health-diet/ashan/fast-food, null source): keep-list = magnit,5ka,ion,kdv,manual,recipe_import,gridnev. DB.cleanupUnlabeledProducts deletes non-keep AND unused (food_log/recipe_ingredients protected); Settings button 'Очистить продукты без магазина' with a counted appConfirm. - Seed products_seed.db cleaned: 24443 -> 13008 (non-keep removed), seed_meta version 10 -> 11, VACUUM (13.5MB -> 8.9MB). So future imports don't re-add junk. On-device 11207 unused non-keep will be removed via the button (24 eaten stay).
This commit is contained in:
parent
d11402f116
commit
8f57c482a8
5 changed files with 71 additions and 16 deletions
|
|
@ -30,7 +30,7 @@
|
|||
| `css/styles.css` | ~2100 | Темная тема, мобильный дизайн, HR зоны, ландшафтный режим |
|
||||
| `index.html` | 28 | Точка входа, порядок загрузки скриптов |
|
||||
| `img/gps-placeholder.png` | 63KB | Пиксельарт заглушка при поиске GPS |
|
||||
| `data/products_seed.db` | ~12MB | 24443 продукта (HD 8090, magnit 7690, 5ka 3354, ashan 2000, ion 1079, kdv 667, mcdonalds 619, kfc 361, dodo 258, gridnev 217) |
|
||||
| `data/products_seed.db` | ~9MB | 13008 продуктов с меткой источника (magnit 7690, 5ka 3354, ion 1079, kdv 667, gridnev 217, manual 1). Импорт без плашки (HD/ashan/фастфуд) вычищен, seed_meta.version=11. Поиск — `searchProducts` порядко-независимый (AND по токенам). Чистка каталога: `cleanupUnlabeledProducts` (неиспользованные non-keep) |
|
||||
|
||||
**Библиотеки** (`lib/`): leaflet.js (1.9.4), leaflet-rotate.js (компас), chart.min.js, chartjs-plugin-zoom.min.js, hammer.min.js, jszip.min.js
|
||||
|
||||
|
|
|
|||
14
README.md
14
README.md
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
## Возможности
|
||||
|
||||
- **Дневник питания** — 24400+ продуктов (Магнит, 5ка, Health-Diet.ru, Ашан, Макдоналдс, KFC, Додо Пицца, KDV, ИОН, Гриднев). 37 нутриентов (витамины, минералы, omega-3/6, вода, клетчатка). Рецепты с термообработкой. Формулы порций (4*30, 300+50).
|
||||
- **Дневник питания** — ~13000 продуктов с меткой источника (Магнит, 5ка, ИОН, KDV, Гриднев, свои). Поиск порядко-независимый (слова в любом порядке). Каталог чистится от неиспользованного импорта без метки (health-diet и пр.) кнопкой в настройках — съеденные и в рецептах сохраняются. 37 нутриентов (витамины, минералы, omega-3/6, вода, клетчатка). Рецепты с термообработкой. Формулы порций (4*30, 300+50).
|
||||
- **GPS трекинг** — запись маршрутов на велосипеде/пешком в фоне (foreground service, точки пишутся при выключенном экране; один файл на сессию с id сессии в имени `gps_points_<ms>_s<id>.jsonl` для однозначной привязки, переживает START_STICKY-перезапуск ОС; полный до-импорт нативных точек по timestamp при финализации — без потерь, даже если экран был выключен всю запись, а при пробуждении экрана точки видимо нарастают на карте по мере импорта). При живой записи нативный файл — источник истины для БД, watchPosition только рисует маркер (без дублей одного фикса с разными ts). Восстановление точек из файлов на устройстве — матч файла к сессии по id/дате, дозаполнение разрывов с дедупом по близости ±2.5с, безопасный пересчёт без удаления точек, чистка уже импортированных файлов. Оффлайн карты (Leaflet + IndexedDB кэш). Импорт GPX/FTB (FitoTrack). Экспорт GPX. Фильтр altitude outlier'ов и GPS-залипаний. Отбор точек по точности (жёсткий порог по настройке «Мин. точность» — рубит грубые сотовые фиксы ~800м, оставляет слабый но точный GPS; где сигнала нет — честный разрыв). Направление (bearing) каждой точки пишется в БД. Вращение карты по компасу (leaflet-rotate, EMA-сглаживание, gimbal lock компенсация) и course-up по GPS-курсу (в ландшафте карта доворачивается вместе с экраном — курс к физическому верху). Ландшафтный режим (CSS rotation + панель справа, 3 режима: off/auto/lock). Dead reckoning при потере GPS (гироскоп + акселерометр). Placeholder до GPS (пиксельарт). Карта проезженных дорог (дедупликация 2м, вело/пешие). Настраиваемый цвет линии трека (чтобы отличать от цветных дорог). Навигационный маркер позиции с треугольником-направленцем (по компасу или GPS-курсу); порядок слоёв тайлы<дороги<маршрут<трек<маркер. Импорт тайлов по регионам (SQLite, роутер LAN).
|
||||
- **BLE пульсометр** — подключение к FitPro браслетам и стандартным HR датчикам (0x180D). ЧСС, SpO2, давление в реальном времени при записи трека.
|
||||
- **Расчёт ккал** — велосипед: НЕТТО-модель (суточный баланс уже учитывает BMR, поэтому активность считается сверх покоя). Кручение = max(физика мощности из gradient+Crr+CdA / КПД, эмпирический MET-нетто по скорости); накат (freewheel, `power≤0` — «велик катился бы сам») = только базовый обмен. MET-таблица для ходьбы/бега и тренировок (приседания 6.0, скакалка 12.3, растяжки 2.3, бассейн 8.0). Набор высоты — кумулятивный гистерезис (устойчив на плотных треках, где per-point разница <1м). Пункт меню «Пересчитать высоту/калории» пересчитывает статистику всех треков без изменения точек. Профили велосипедов. BMR по Mifflin-St Jeor с коэффициентом активности.
|
||||
|
|
@ -80,16 +80,16 @@ smart/
|
|||
|
||||
## Данные продуктов
|
||||
|
||||
Каталог (seed) очищен от импорта без метки источника (health-diet, ашан, фастфуд) — остаются источники с плашкой:
|
||||
|
||||
| Источник | Продуктов | Витамины/минералы |
|
||||
|----------|-----------|-------------------|
|
||||
| Health-Diet.ru | 8090 | да (37 нутриентов, omega-3/6, вода) |
|
||||
| Магнит (SSR парсинг) | 7690 | нет |
|
||||
| 5ка (API скрейпер) | 3354 | нет |
|
||||
| Ашан (HD) | 2000 | да |
|
||||
| ИОН (web.ion.ru) | 1079 | да |
|
||||
| KDV (SSR парсинг) | 667 | нет |
|
||||
| Макдоналдс (HD) | 619 | да |
|
||||
| KFC (HD) | 361 | да |
|
||||
| Додо Пицца | 258 | КБЖУ |
|
||||
| Гриднев (PDF) | 217 | да |
|
||||
| **Всего** | **~24400** | |
|
||||
| Свои (manual) | — | по вводу |
|
||||
| **Всего (seed)** | **~13000** | |
|
||||
|
||||
На устройстве такая же чистка — кнопка «Очистить продукты без магазина» в настройках (удаляет только неиспользованные без метки; съеденные/в рецептах остаются).
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -106,24 +106,36 @@ async function saveSleep(date, bedTime, asleepTime, wakeTime, napMinutes, notes)
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function searchProducts(query, limit) {
|
||||
const q = query.toLowerCase();
|
||||
return select(
|
||||
`SELECT id, name, brand, category, weight_g, kcal_per_100g,
|
||||
const q = (query || "").toLowerCase().trim();
|
||||
// Order-independent: every whitespace-separated word must appear in the name
|
||||
// (in any order). Fixes "надо вводить слова в порядке названия".
|
||||
const tokens = q.split(/\s+/).filter(Boolean);
|
||||
if (tokens.length === 0) return [];
|
||||
const cols = `id, name, brand, category, weight_g, kcal_per_100g,
|
||||
protein_per_100g, fat_per_100g, carbs_per_100g, fiber_per_100g,
|
||||
composition, nutrients_guessed, source, water_pct,
|
||||
vit_a_mcg, vit_c_mg, vit_b1_mg, vit_b2_mg, vit_b3_mg, vit_e_mg,
|
||||
calcium_mg, iron_mg, magnesium_mg, potassium_mg, sodium_mg, phosphorus_mg,
|
||||
carotene_mcg, selenium_mcg, zinc_mg, copper_mg, folate_mcg,
|
||||
vit_d_mcg, vit_k_mcg, vit_b6_mg, vit_b12_mcg
|
||||
vit_d_mcg, vit_k_mcg, vit_b6_mg, vit_b12_mcg`;
|
||||
const where = tokens.map((_, i) => `name_lower LIKE ?${i + 1}`).join(" AND ");
|
||||
const vals = tokens.map(t => `%${t}%`);
|
||||
const nFull = vals.length + 1, nPrefix = vals.length + 2, nLimit = vals.length + 3;
|
||||
vals.push(`%${q}%`); // full contiguous string present → rank first
|
||||
vals.push(`${q}%`); // name starts with the query → rank next
|
||||
vals.push(limit ?? 200);
|
||||
return select(
|
||||
`SELECT ${cols}
|
||||
FROM products
|
||||
WHERE name_lower LIKE ?1
|
||||
WHERE ${where}
|
||||
GROUP BY COALESCE(store_id, id)
|
||||
ORDER BY
|
||||
CASE WHEN name_lower LIKE ?2 THEN 0 ELSE 1 END,
|
||||
CASE WHEN name_lower LIKE ?${nFull} THEN 0 ELSE 1 END,
|
||||
CASE WHEN name_lower LIKE ?${nPrefix} THEN 0 ELSE 1 END,
|
||||
length(name),
|
||||
name COLLATE NOCASE
|
||||
LIMIT ?3`,
|
||||
[`%${q}%`, `${q}%`, limit ?? 200]
|
||||
LIMIT ?${nLimit}`,
|
||||
vals
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -172,6 +184,27 @@ async function deleteProduct(id) {
|
|||
await execute("DELETE FROM products WHERE id = ?1", [id]);
|
||||
}
|
||||
|
||||
// Catalog cleanup: sources that carry a "плашка" (badge) and are worth keeping even
|
||||
// if never eaten. Everything else (health-diet / ashan / fast food / null source) is
|
||||
// removed ONLY when unused — i.e. not referenced by food_log or recipe_ingredients,
|
||||
// so eaten products and recipe ingredients always survive.
|
||||
const PRODUCT_KEEP_SOURCES = "'magnit','5ka','ion','kdv','manual','recipe_import','gridnev'";
|
||||
const UNLABELED_UNUSED_WHERE =
|
||||
`(source IS NULL OR source NOT IN (${PRODUCT_KEEP_SOURCES}))
|
||||
AND id NOT IN (SELECT product_id FROM food_log WHERE product_id IS NOT NULL)
|
||||
AND id NOT IN (SELECT product_id FROM recipe_ingredients WHERE product_id > 0)`;
|
||||
|
||||
async function countUnlabeledUnusedProducts() {
|
||||
const rows = await select(`SELECT COUNT(*) AS cnt FROM products WHERE ${UNLABELED_UNUSED_WHERE}`);
|
||||
return rows[0]?.cnt || 0;
|
||||
}
|
||||
|
||||
async function cleanupUnlabeledProducts() {
|
||||
const n = await countUnlabeledUnusedProducts();
|
||||
await execute(`DELETE FROM products WHERE ${UNLABELED_UNUSED_WHERE}`);
|
||||
return n;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Cooking methods & Recipes
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -1106,6 +1139,8 @@ window.DB.getManualProducts = getManualProducts;
|
|||
window.DB.addProduct = addProduct;
|
||||
window.DB.deleteProduct = deleteProduct;
|
||||
window.DB.getProductRefs = getProductRefs;
|
||||
window.DB.countUnlabeledUnusedProducts = countUnlabeledUnusedProducts;
|
||||
window.DB.cleanupUnlabeledProducts = cleanupUnlabeledProducts;
|
||||
window.DB.getFoodForDate = getFoodForDate;
|
||||
window.DB.addFoodManual = addFoodManual;
|
||||
window.DB.addFoodEntry = addFoodEntry;
|
||||
|
|
|
|||
|
|
@ -5722,6 +5722,13 @@ async function renderSettings(container) {
|
|||
</div>
|
||||
<button class="btn-secondary" id="s-import-tiles" style="padding:6px 12px;font-size:13px;">Файл</button>
|
||||
</div>
|
||||
<div class="settings-item">
|
||||
<div class="settings-item-left">
|
||||
<div class="settings-item-title">Очистить продукты без магазина</div>
|
||||
<div class="settings-item-desc">Удалить неиспользованные продукты без метки источника (health-diet, фастфуд). Съеденные и в рецептах остаются.</div>
|
||||
</div>
|
||||
<button class="btn-secondary" id="s-clean-products" style="padding:6px 12px;font-size:13px;">Очистить</button>
|
||||
</div>
|
||||
<div class="settings-item">
|
||||
<div class="settings-item-left">
|
||||
<div class="settings-item-title">Скачать с роутера</div>
|
||||
|
|
@ -5999,6 +6006,19 @@ async function renderSettings(container) {
|
|||
if (el) el.textContent = `${n} тайлов в кэше`;
|
||||
});
|
||||
|
||||
// Catalog cleanup: drop unused products that carry no source badge (health-diet,
|
||||
// fast food, null source). Eaten / recipe-used products are protected.
|
||||
let cleanBtn = container.querySelector("#s-clean-products");
|
||||
if (cleanBtn) cleanBtn.onclick = async () => {
|
||||
let n = await DB.countUnlabeledUnusedProducts();
|
||||
if (!n) { showToast("Нечего чистить"); return; }
|
||||
if (!(await appConfirm(`Удалить ${n} неиспользованных продуктов без метки источника (health-diet, фастфуд)? Съеденные и в рецептах останутся.`, "Удалить"))) return;
|
||||
cleanBtn.disabled = true; cleanBtn.textContent = "Чистка...";
|
||||
let removed = await DB.cleanupUnlabeledProducts();
|
||||
cleanBtn.disabled = false; cleanBtn.textContent = "Очистить";
|
||||
showToast(`Удалено ${removed} продуктов`);
|
||||
};
|
||||
|
||||
// Import tiles from a file the user picks on the device (SAF picker on Android).
|
||||
container.querySelector("#s-import-tiles").onclick = async () => {
|
||||
let btn = container.querySelector("#s-import-tiles");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue