Commit Graph

970 Commits

Author SHA1 Message Date
dadaloop82 d13f744aea feat: v1.1.0 - Docker, i18n, setup wizard, rate limiting, OpenAPI
New features:
- Docker support (Dockerfile + docker-compose.yml)
- GitHub Actions CI pipeline (PHP lint, JS lint, Docker build, i18n validation)
- Internationalization system with 3 languages (it, en, de) and 347 translation keys
- First-run setup wizard (4-step configuration)
- File-based API rate limiting (120/15/5 req/min tiers)
- OpenAPI 3.1.0 specification for all 43 API endpoints
- CONTRIBUTING.md with translation and development guide
- Screenshots directory placeholder

Modified:
- README.md: Docker badges, install instructions, translations section
- api/index.php: rate limiting middleware
- assets/js/app.js: i18n system, setup wizard, t() function
- assets/css/style.css: setup wizard styles
- index.html: data-i18n attributes, setup wizard overlay, language settings
- .gitignore: rate_limits exclusion
2026-04-10 06:03:11 +00:00
dadaloop82 e0956c6043 Prepare for public distribution v1.0.0
- Remove all personal data from source code (HA IP, JWT tokens)
- Move secrets to .env configuration (gitignored)
- Create .env.example template for new installations
- Add centralized env() helper, eliminate code duplication (~120 lines removed)
- Add input validation on inventory operations (quantity bounds, location whitelist)
- Remove sensitive credential exposure in API responses
- Remove database and runtime files from Git tracking
- Disable database push-to-GitHub backup (local-only backup now)
- Update .gitignore for distribution
- Add comprehensive README with installation guide
- Add CHANGELOG.md for version tracking
- Add MIT LICENSE
- Add author/license headers to all source files
- TTS defaults now empty (configured per-installation via .env)
v1.0.0
2026-04-10 05:24:27 +00:00
dadaloop82 35cf133be4 Pre-distribution snapshot: current working state 2026-04-10 05:16:17 +00:00
dadaloop82 655ec63aaf 📦 Backup database automatico - 2026-04-10 03:00 2026-04-10 03:00:01 +00:00
dadaloop82 6531765921 Priorità verdura/frutta aperta in ricette + stime corrette
Stime opened (PHP+JS):
- Avocado -> 2d; fragole/banane/pesca/mango -> 2d
- Mela/pera/kiwi/ananas/melone/uva -> 3d
- Zucchina/melanzana/pomodoro/peperone -> 3d (era 7d)
- Broccolo/cavolfiore/sedano/finocchio -> 3d (era 7d)
- Cipolla/cipollotto/scalogno/porro -> 4d (era 7d)
- Carota -> 5d (era 7d); Aglio -> 10d
- Insalata/rucola/spinaci -> 2d (era 4d)

Recipe generator (index.php):
- SQL include i.opened_at
- getItemPriority: rileva opened_at su QUALSIASI unità (non solo conf)
- Elementi [APERTO] con scadenza <=5gg promossi in 'fortemente consigliati'
- Label '📦 PRODOTTI APERTI' aggiornata
- [APERTO] mostrato nel testo ingredienti del prompt AI
- Stesso fix nel contesto chat (opened_at + [APERTO] label)

DB migration: ricalcolate scadenze aperti con nuove stime
2026-04-09 12:56:10 +00:00
dadaloop82 d690ad826c Suggerisci confezione da usare per prima (scad. più vicina)
- Nel form USA, se il prodotto ha >=2 slot in inventario con scadenze
  diverse (es. 2 lotti dispensa, o frigo+dispensa), mostra un banner
  giallo: '⚠️ Usa prima quella in Frigo — scade il 12/04 (tra 3 giorni)!'
- Logica: filtra item con expiry_date, ordina per scadenza ASC,
  mostra hint solo se ci sono almeno 2 scadenze diverse O 2 location diverse
- Nessun hint se tutto ha la stessa scadenza (inutile)
2026-04-09 05:12:25 +00:00
dadaloop82 346f69426f Fix: 'Modifica scheda prodotto' duplicato
Ogni chiamata a showProductAction() creava un nuovo div e lo appendeva
con .after() senza rimuovere il precedente -> link moltiplicato.
Fix: il div ha id='catalog-edit-link', viene riutilizzato se esiste;
se il prodotto non è in inventario il link viene rimosso.
2026-04-09 05:08:47 +00:00
dadaloop82 f86059cdcd Auto-refresh: 5min pagina corrente + 2min lista spesa bg
- Intervallo pagina corrente: 5 minuti (era 10) -> dashboard, inventario,
  scadenze aperte, ricette, log si aggiornano automaticamente
- Intervallo lista spesa: 2 minuti in background anche se non sei sulla
  pagina Shopping -> badge/contatore aggiornato, e se sei sulla pagina
  Shopping vedi i nuovi prodotti aggiunti da Bring! da un altro device
- refreshCurrentPage() ora copre anche 'recipe' e 'log'
- visibilitychange: refresh immediato su qualsiasi pagina (già presente)
2026-04-09 05:07:16 +00:00
dadaloop82 7f0d4d817b Auto-refresh dati ogni 10 min + visibilitychange
- setInterval ogni 10 minuti ricarica la pagina corrente (dashboard,
  inventario, spesa) silenziosamente, senza reload — i contatori di
  scadenza restano sempre aggiornati anche se la scheda è aperta da ore
- visibilitychange ora aggiorna SEMPRE la pagina corrente, non solo
  la lista spesa, quando la tab torna in foreground
2026-04-09 05:03:47 +00:00
dadaloop82 978c298cc3 📦 Backup database automatico - 2026-04-09 03:00 2026-04-09 03:00:01 +00:00
dadaloop82 48543ee8c4 Fix opened expiry: estimation rewrite + 0ml display + HTTPS info
- Restructure estimateOpenedExpiryDays (PHP+JS): check 'eternal' items FIRST
  before any location check, so they never get 5/30d regardless of shelf:
  - Salt/sugar/honey/vinegar/bicarbonato -> 9999d (never expires)
  - Spirits -> 730d, aroma/extracts/tea -> 730d, olio/coffee -> 365d
  - Pasta/rice/dry legumes (non-frigo) -> 365d; Polenta/flour -> 180d
  - Maionese/ketchup/senape -> 90d, soy sauce -> 90d anywhere
  - Dispensa fallback: 60d (was 30d); pantry salsa di pomodoro -> 5d
  - Fruit/veg in fridge: specific rules (banana 10d, citrus 14d, etc.)
  - burro 30d, panna 4d, mortadella/wurstel 5d (improved from old values)
- Fix dashboard (getStats): remove min() with stored expiry_date (was from old
  wrong estimation); use opened_at + new estimate directly
- Filter opened list: skip items with days_to_expiry > 365 (non-perishables)
- Fix useFromInventory (both paths): min(opened_estimate, sealed_expiry)
  so original sealed expiry is respected if it expires sooner
- DB migration: re-compute expiry_date for all 22 opened inventory rows
  (maionese: +90d, salt/sugar/aceto: +9999d, muesli/biscotti: +60d, etc.)
- Clear opened_at for 'inchusa' birra (user confirmed: not actually opened)
- Fix '4 conf + 0ml' display: only show remainder if remainderAmt >= 1
- Add ' Stabile' expiry badge for days > 365 (JS)
- Add dispensa-ca.crt to /data/ for browser import (HTTPS trust)
2026-04-08 14:29:44 +00:00
dadaloop82 19489a0265 Smart opened-product expiry: days countdown, edibility, correct sort
PHP getStats() opened section:
- Primary detection: opened_at IS NOT NULL (reliable, set by useFromInventory)
  Fallback: fractional-qty pattern (legacy items)
- Per-item compute opened_expiry = min(opened_at + estimateOpenedExpiryDaysPHP, original_expiry)
  → vacuum_sealed items get 1.5× multiplier
  → always take sooner of 'opened shelf life' vs 'original sealed expiry'
- Add days_to_expiry, opened_expiry, is_edible, has_opened_at to each item
- Filter legacy items (no opened_at) with expiry > 14 days (too much noise)
- Sort by days_to_expiry ASC (soonest/spoiled first) instead of updated_at DESC

JS dashboard opened render:
- Expiry badge:  Scaduto / ⚠️ Scade oggi /  Xgg (urgent≤2, soon≤5, ok>5)
- 🔒 icon added when vacuum_sealed=1
- Spoiled items shown with strikethrough name + muted styling (.alert-item-spoiled)
- Cap display at 10 items; 'e altri N prodotti aperti...' note if more
- Sort comes from server (removed JS openedFraction sort)

CSS:
- .opened-expiry-{ok,soon,urgent,today,spoiled} badge classes
- .alert-item-spoiled strikethrough styling
- .alert-more-note
2026-04-08 12:30:36 +00:00
dadaloop82 e8649a87fc 📦 Backup database automatico - 2026-04-08 03:00 2026-04-08 03:00:01 +00:00
dadaloop82 dccda8ebc9 Fix: any-token product family grouping + auto timer reset on cache change
Root cause: autoAddCriticalItems used stale in-memory cache (old critical items)
and re-added items to Bring right after manual removal, because on_bring was
now false but urgency was still 'critical' in the old cache.

PHP smartShopping():
- Rename stockByFirstToken → stockByAnyToken (indexes ALL significant tokens)
- 'Passata di pomodoro' depleted + 'Polpa di pomodoro' in stock → share token
  'pomodoro' → passata no longer flagged as critical (COVERS: passata/polpa/pelato
  and any future tomato product variant)
- Same logic: 'aglio'/'aglio rosso', 'latte'/'latte di montagna', etc.

JS loadSmartShopping():
- When critical item set changes (items added OR removed), immediately reset
  _autoAddedCriticalTs and _bringCleanupTs so next shopping load uses fresh data
  instead of debounced old data

JS cleanupObsoleteBringItems():
- Use any-token matching (like PHP) for both stockByAnyToken and urgentSmartByToken
  → 'Passata di pomodoro' in Bring, 'polpa' in stock → share 'pomodoro' → removed
2026-04-07 15:26:35 +00:00
dadaloop82 dcc7e9de42 Fix smart shopping: skip depleted products with equivalent in-stock substitutes
PHP smartShopping():
- Add nameTokens() helper (mirrors JS _nameTokens)
- Build stockByFirstToken map before product loop
- Skip depleted (qty=0) products whose first token has stock elsewhere
  → 'Aglio rosso' depleted but 'Aglio' qty=3 → skip
  → 'Latte Parzialmente Scremato' depleted but 'Latte di Montagna' 4.8 conf → skip
  → 'Muesli Frutta Secca' depleted but 'Muesli multifrutta' 930g → skip
- Result: 13→9 items, no more false critical flagging for covered products

JS cleanupObsoleteBringItems():
- Rewrite with stockByFirstToken approach (aggregate by first token, not product_id)
- urgentMatch logic: if smart item is completely depleted (qty=0) but equivalent
  stock exists via first token → still remove from Bring (need is covered)
- Only keep Bring item if: smart flags it with current_qty>0 (genuinely running low)

Also: removed Milch/Knoblauch/Fruechte/Passata from Bring directly (immediate fix)
2026-04-07 15:20:33 +00:00
dadaloop82 0bca79b8a2 Fix 3 bugs: banana use blocked, cleanup never ran, stale Bring items
1. Use form pz step bug (banana non scalabile):
   - min=0.25 + step=0.5 → solo 0.75, 1.25, 1.75... validi per browser
   - 1 intero (default) era INVALIDO → form bloccato silenziosamente
   - Fix: step='any', min=0.01 (il passo logico resta in adjustUseQty)

2. cleanupObsoleteBringItems mai eseguita:
   - Usava products_list che non ha campo quantity → (qty||0)<=0 sempre vero → skip sempre
   - Fix: usa inventory_list che ha le qty reali per location

3. cleanupObsoleteBringItems troppo rara:
   - sessionStorage → una sola volta per sessione
   - Fix: localStorage con TTL 30 minuti
   - Ora rimuove da Bring qualsiasi item che ha scorte in inventario
     e NON è flaggato come critical/high dalla spesa intelligente
2026-04-07 15:08:03 +00:00
dadaloop82 9b51bb606d Fix smart shopping false positives (prodotti appena comprati/sufficienti)
PHP smartShopping():
- Absolute minimum fallback: requires isRegular + buyCount>=2 + pctLeft<80
  (before: ANY product with buyCount>=1 → triggered for newly bought items)
- Add justRestocked guard: skip item if bought within 3 days AND pctLeft>=50%
  and not expiring (prevents items bought yesterday showing as urgent)
- Add daysSinceLastBuy calculation

JS isLowStock():
- pz threshold: <=1 (was <=2) — 2 pezzi rimasti non è già urgente
2026-04-07 15:02:15 +00:00
dadaloop82 72535ce41c UX: fraction buttons for pz unit in use form + fix qty display
- _pzFractionLabel(): formats 2.5 → '2½', 2.25 → '2¼', etc.
- formatQuantity/formatQuantityParts: use fraction label for pz (was rounding to int)
- loadUseInventoryInfo normal mode: show ¼/½/¾/1 intero quick-select buttons for pz
- setPzFraction(): sets quantity input + syncs active button highlight
- adjustUseQty pz: step = 0.5 (half-piece) instead of 1; syncs frac button on ±
- CSS: .pz-fraction-btns, .fraction-btn-row, .frac-btn, .frac-btn.active
2026-04-07 13:03:32 +00:00
dadaloop82 dc36ce2ae4 fix: MODIFICA button now edits inventory instance (expiry/location/qty/vacuum)
The MODIFICA button on the action page was opening the catalog editor
(name/brand/category). Users expect it to edit the physical item in hand.

Changes:
- MODIFICA button → openInventoryEdit(): edits the inventory row directly.
  If product is in one location → opens editActionInventoryItem directly.
  If multiple locations → shows a location picker modal first.
- editActionInventoryItem modal already has: qty ±, unit, conf size, location
  buttons, expiry date, vacuum toggle — all fields for the instance.
- Catalog editing (name/brand/category) moved to a small secondary link
  '⚙️ Modifica scheda prodotto' shown discreetly below the action buttons.
- Removed redundant 'Tocca una riga per modificare' hint from status bar.
- Added .btn-link-small CSS class for the secondary catalog-edit link.
2026-04-07 12:52:31 +00:00
dadaloop82 4e576559a9 feat: barcode scan button + reminder in manual product form
- Add 📷 scan button next to the barcode field in product form
  Opens a camera modal (BarcodeDetector if available, manual fallback)
  Detects barcode after 2 consistent frames, fills field and closes modal
- Show ⚠️ hint below the barcode field when it's empty (new products only):
  'Aggiungi il barcode così al prossimo acquisto basta scansionarlo!'
  Hint hides automatically when a code is entered or scanned
- Hint is hidden in edit-product mode (barcode already saved)
- scanBarcodeForForm() reuses the modal overlay; handles camera permission
  errors gracefully (shows manual input only)
2026-04-07 12:17:18 +00:00
dadaloop82 b7ed9899fa feat+fix: Bring removal, multi-expiry batches, FIFO in cooking steps
BRING! REMOVAL FIX (latte/aglio not removed after shopping):
- PHP addToInventory: replace exact strcasecmp with token-based fuzzy
  matching (same logic as _productOnBring) so custom Bring item names
  and translated catalog keys both match correctly
- JS submitAdd: add client-side fallback — if PHP removal missed the item,
  use _findSimilarItem against the loaded shoppingItems and call bring_remove

MULTI-EXPIRY BATCHES (when buying conf with different expiry dates):
- Add form (unit=conf): shows '+ Lotto con scadenza diversa' button
- Each extra batch has its own qty + expiry date input with +/- controls
- On submitAdd, extra batches are submitted as additional inventory_add calls
  (separate DB rows, separate expiry dates)
- Multi-batch section hidden in 'Ce l'avevo già' mode and for non-conf units
- Re-shown/hidden when switching unit via onAddUnitChange

RECIPE COOKING STEPS - FIFO ingredient display:
- renderCookingStep: each ingredient row now shows brand chip, location chip,
  and expiry date chip (color-coded: red ≤3d, yellow ≤7d)
- PHP already selected earliest-expiry inventory entry (ORDER BY days_left ASC
  with > not >= ensures first/earliest match wins)
- CSS: .cooking-ing-meta, .cooking-ing-chip, .exp-soon, .exp-close,
  .multi-batch-row, .multi-batch-qty, .multi-batch-date, .btn-icon-sm
2026-04-07 12:10:14 +00:00
dadaloop82 22ae3abf47 📦 Backup database automatico - 2026-04-07 03:00 2026-04-07 03:00:01 +00:00
dadaloop82 5be62cfbfd fix: low stock detection for rarely-used items
- PHP smart_shopping: add absolute stock fallback that flags conf/pz items
  with <=2 units (medium) or <=1 unit (high) and g/ml at <=20% of default,
  regardless of usage frequency. Fixes products like Panna da cucina that
  are rarely used but running low and were invisible to the frequency-based
  urgency logic (pctLeft was 66% since last purchase was 3 at once).
- JS isLowStock(): return true (not false) when totalRemaining <= 0.
  A fully depleted item is definitely low-stock; the Bring! add prompt
  should fire when you use the very last unit.
2026-04-06 10:53:15 +00:00
dadaloop82 6424f381af fix: TTS only on Rileggi; use-all deducts all locations; fix DB permissions 2026-04-06 10:28:24 +00:00
dadaloop82 b47dcb4fac fix: TTS only on Rileggi btn; use-all deducts from all locations
Cooking mode TTS:
- Removed auto-speak from renderCookingStep() entirely
- TTS now fires ONLY when user presses 'Rileggi' (replayCookingTTS)
- Timer-expiry TTS unchanged (still speaks when a cooking timer expires)

submitUseAll fix:
- Changed location from selected-location to '__all__'
- 'Usato TUTTO / Finito' means the product is completely consumed;
  using a specific location could fail with a 404 if the async
  loadUseInventoryInfo() hadn't yet updated the selector (race condition)
- The __all__ path in PHP removes inventory across every location
2026-04-06 10:23:03 +00:00
dadaloop82 4e8b586201 feat: AI photo identification from product form
When creating a new product (manual entry), a '📷 Scatta foto e identifica con AI'
button appears at the top of the form. Tapping it:
1. Opens a camera modal (same pattern as expiry scanner)
2. User takes photo of product/label
3. Sends to gemini_identify — returns name, brand, category + OpenFoodFacts matches
4. User can pick a specific OFF match (fills barcode + full details via lookup_barcode)
   or tap 'Usa dati AI' to fill just name/brand/category from Gemini
5. All matching fields are auto-filled: name, brand, category, barcode, image, unit/qty
6. Button hidden when editing an existing product (not needed)
2026-04-06 09:23:41 +00:00
dadaloop82 a6bc05cd2d feat: spesa mode stats banner + scan zoom x1/x2 toggle
Spesa mode banner:
- Tracks each added product in _spesaSession[]
- Shows a rotating stat/phrase below the title: count, top category,
  duplicates, fun milestone messages (primo prodotto, ottimo ritmo, spesa epica…)
- Banner gains two-line layout (title + stat)

Scan zoom:
- Small pill button 'x1'/'x2' overlaid top-right of the camera viewport
- On hardware-zoom capable devices (Android Chrome) uses track.applyConstraints zoom
- Falls back to CSS scale(2) on video element for all other browsers
- Zoom resets to x1 on stopScanner()
2026-04-06 09:16:50 +00:00
dadaloop82 7782eb1519 fix: pre-fill conf size from product's weight/volume unit when switching to 'confezioni'
If a product was created with unit='g' (or ml/kg/l) and a default_quantity,
that value already IS the package size — no need to ask again.
Applied in both showAddForm() initial render and onAddUnitChange() toggle.
2026-04-06 09:10:29 +00:00
dadaloop82 50da545c72 feat: predict expiry date from product history when adding items
- PHP: new 'expiry_history' action computes avg shelf life (expiry_date - added_at)
  from inventory table for the same product_id (last 730 days, valid entries only)
- JS: _fetchExpiryHistoryAndUpdate() fires async after showAddForm() renders
  and replaces the rule-based estimate with the historical average if available
- Labeled with '📊 storico' badge on the estimate line (tooltip shows sample count)
- recalculateAddExpiry() and selectPurchaseType('new') both honour window._historyExpiryDays
- Vacuum-sealed multiplier still applied on top of historical base
- Falls back silently to rule-based estimateExpiryDays when no history exists
2026-04-06 09:09:04 +00:00
dadaloop82 568cc1e6fa fix: don't re-add items to Bring after user removes them (purchased blocklist, 4h TTL) 2026-04-06 08:53:14 +00:00
dadaloop82 854bc37709 📦 Backup database automatico - 2026-04-06 03:00 2026-04-06 03:00:01 +00:00
dadaloop82 fedb7c50e2 📦 Backup database automatico - 2026-04-05 03:00 2026-04-05 03:00:02 +00:00
dadaloop82 57677fa0d0 fix: keep previous settings (meal, persons, options) on regenerate 2026-04-04 15:35:07 +00:00
dadaloop82 e233dcef6d fix: remove duplicate const meal declaration in regenerateRecipe 2026-04-04 15:31:31 +00:00
dadaloop82 da5552e992 fix: hide meal-plan banner on chip uncheck; fix recipe variety (variation counter, temp scaling, client-side title tracking) 2026-04-04 15:29:07 +00:00
dadaloop82 bd6f92f2f3 fix: route TTS through PHP proxy to bypass mixed-content/CORS 2026-04-04 14:44:11 +00:00
dadaloop82 475d482184 feat: TTS generic API builder, remove HA refs, pre-fill credentials 2026-04-04 14:40:48 +00:00
dadaloop82 7bc1c87d5c feat: TTS via Home Assistant API, settings panel, remove browser speechSynthesis 2026-04-04 14:37:00 +00:00
dadaloop82 63db7cc114 feat: bring urgency sync, background auto-sync, recipe mealplan chip, screensaver fix 2026-04-04 14:32:25 +00:00
dadaloop82 6e3e451a39 📦 Backup database automatico - 2026-04-04 03:00 2026-04-04 03:00:01 +00:00
dadaloop82 3bbf093857 📦 Backup database automatico - 2026-04-03 03:00 2026-04-03 03:00:01 +00:00
dadaloop82 20e7d2cbfc 📦 Backup database automatico - 2026-04-02 03:00 2026-04-02 03:00:01 +00:00
dadaloop82 6f81846942 Smart shopping: timestamp ultimo aggiornamento, CSS progress dots e timer bar; fix layout modalità cucina 2026-04-01 05:52:46 +00:00
dadaloop82 e18fb5839a Smart shopping: cron ogni 5min pre-calcola cache server-side, API serve da cache (risposta istantanea) 2026-04-01 05:52:17 +00:00
dadaloop82 200ec145d9 📦 Backup database automatico - 2026-04-01 03:00 2026-04-01 03:00:01 +00:00
dadaloop82 fb7bb4d675 Modalità cucina: timer multipli persistenti con etichetta, riprendi dal passo salvato, progress dots, pulsante Ricomincia; priorità ricette basata su scadenze con ingredienti obbligatori 2026-03-31 15:55:35 +00:00
dadaloop82 2be6643104 📦 Backup database automatico - 2026-03-31 03:00 2026-03-31 03:00:01 +00:00
dadaloop82 bcddba46d4 Remove kg/l units everywhere — only g (grammi) and ml (millilitri)
- HTML: removed kg/l options from all unit selector dropdowns
- JS detectUnitAndQuantity(): auto-converts kg→g (*1000) and l→ml (*1000)
- JS unit labels: removed all kg/l entries from unitLabels maps
- JS category defaults: frutta 1000g, verdura 500g, bevande 1000ml
- JS step/min logic: simplified for g/ml only (no more 0.01 steps)
- JS getSubUnitStep(): removed kg/l cases
- JS isLowStock(): removed kg/l threshold
- JS spec parser: labels now show g/ml instead of kg/L
- PHP recipe parser: converts kg→g and l→ml immediately on parse
- PHP AI prompt: updated to specify only g/ml/pz/conf units
- PHP migration endpoint available at ?action=migrate_units (no-op if DB already clean)
2026-03-30 14:13:11 +00:00
dadaloop82 c4938457ac Fix min quantity for kg/l units in use forms
- Normal mode (non-conf) now sets min=0.01 for kg/l, min=1 for g/ml
- +/- buttons use unit-aware steps: 0.01 for small kg/l values, 0.1 for
  values <1, 0.5 for values >=1 (instead of fixed 0.5)
- Same fix applied to recipe use form
- Allows inputting e.g. 0.07kg (70g) when product is tracked in kg
2026-03-30 13:45:02 +00:00
dadaloop82 c63faf56e4 Conservative Bring! cleanup + operations log
- cleanupObsoleteBringItems() now much more conservative:
  * Only removes items matching a known DB product (preserves manual additions)
  * Only removes if the product has current_qty > 0 (has stock)
  * AND item is no longer flagged by smart shopping
- Added logOperation() — stores all Bring! operations in localStorage '_opLog'
  (bring_auto_add, bring_cleanup, bring_found, bring_manual_remove)
  Capped at 200 entries, each with timestamp + action + details
- All Bring! add/remove paths now log their operations
2026-03-30 13:36:51 +00:00