feat: AI price estimation for shopping list with per-item real-time display

- Add get_shopping_price / get_all_shopping_prices API endpoints
- AI (Gemini) estimates retail price per natural unit (pack, piece, bunch)
  instead of always per-kg — avoids absurd totals like €1609
- _calcEstimatedTotal: proper g/ml→package conversion using defQty + regex
  on unit_label; only 'kg'/'l' labels trigger weight/volume math
- Cache key bumped to v2 to invalidate old per-kg cached entries
- Suggested quantity cap lowered from 20 to 10 conf/pz
- Unit mismatch guard: if totalUsed >> buyCount*5 for unit=conf, use
  purchase frequency instead of raw consumption rate
- JS _buildPricePayload: use smartShoppingItems for qty/unit (not Bring! spec)
- JS _cachedPrices: persist in sessionStorage (survives navigation);
  validated by _qty/_unit metadata so stale totals auto-invalidate
- Price display redesigned: right-side column per row (price-col-main +
  price-col-unit) instead of small inline badge
- fetchAllPrices: buttons disabled immediately before guard check;
  running total uses only current shoppingItems (not Object.values cache)
- Background refresh: always silent (removed 90s interaction condition)
- visibilitychange: sets _bgCall=true for shopping before refreshCurrentPage
- .gitignore: add runtime data files (bring_migrate_ts, shopping_price_cache,
  anomaly_dismissed, opened_shelf_cache, shopping_name_cache)
- Remove bring_catalog.json and bring_migrate_ts.json from tracking
This commit is contained in:
dadaloop82
2026-05-07 17:31:23 +00:00
parent 4196130835
commit 5f510c0451
11 changed files with 1249 additions and 743 deletions
+133
View File
@@ -516,6 +516,24 @@ body {
color: var(--primary);
}
/* Skeleton shimmer while stat card data is loading */
@keyframes stat-shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
.stat-value.stat-loading {
color: transparent !important;
background: linear-gradient(90deg, var(--border) 25%, color-mix(in srgb, var(--border) 40%, white) 50%, var(--border) 75%);
background-size: 200% 100%;
animation: stat-shimmer 1.2s ease-in-out infinite;
border-radius: 8px;
min-width: 40px;
min-height: 2.4rem;
display: inline-block;
vertical-align: middle;
user-select: none;
}
.stat-label {
font-size: 0.85rem;
color: var(--text-light);
@@ -1861,6 +1879,121 @@ body {
.badge-local-tag { background: #e0f2fe; color: #0369a1; cursor: pointer; }
.badge-local-tag:hover { background: #bae6fd; }
/* ─── Shopping price badge ─── */
.shopping-item-price-badge {
margin-top: 4px;
min-height: 1.2rem;
}
.price-badge-value {
display: inline-block;
font-size: 0.7rem;
font-weight: 600;
color: #15803d;
background: #dcfce7;
border-radius: 8px;
padding: 1px 7px;
white-space: nowrap;
}
.price-badge-loading {
display: inline-block;
font-size: 0.7rem;
color: #94a3b8;
font-style: italic;
}
.price-badge-error {
display: inline-block;
font-size: 0.65rem;
color: #94a3b8;
}
/* ─── Price column — right-side per-item price display ─── */
.shopping-item-price-col {
display: flex;
flex-direction: column;
align-items: flex-end;
justify-content: center;
flex-shrink: 0;
min-width: 58px;
text-align: right;
padding: 0 4px;
}
.price-col-main {
font-size: 1.05rem;
font-weight: 700;
color: #15803d;
white-space: nowrap;
line-height: 1.25;
}
.price-col-unit {
font-size: 0.62rem;
color: #94a3b8;
white-space: nowrap;
line-height: 1.2;
margin-top: 1px;
}
.price-col-loading {
font-size: 0.7rem;
color: #cbd5e1;
font-style: italic;
}
.price-col-error {
font-size: 0.75rem;
color: #cbd5e1;
}
/* ─── Price summary bar (top of shopping tab) ─── */
.shopping-price-total-row {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px 4px;
background: linear-gradient(135deg, #f0fdf4, #dcfce7);
border-radius: 10px;
margin: 8px 0 4px;
font-size: 0.88rem;
font-weight: 600;
color: #166534;
box-shadow: 0 1px 4px rgba(21,128,61,0.08);
}
.price-total-label {
flex: 1;
}
.price-total-value {
font-size: 1rem;
font-weight: 700;
}
.btn-price-refresh {
background: none;
border: none;
cursor: pointer;
font-size: 0.85rem;
padding: 2px 6px;
border-radius: 6px;
transition: background 0.15s;
}
.btn-price-refresh:hover { background: rgba(0,0,0,0.07); }
.btn-price-refresh:disabled { opacity: 0.5; cursor: default; }
/* Loading progress bar */
.price-loading-bar {
height: 3px;
background: #dcfce7;
border-radius: 2px;
overflow: hidden;
margin: 0 12px 6px;
}
.price-loading-inner {
height: 100%;
width: 0%;
background: #16a34a;
border-radius: 2px;
transition: width 0.3s ease;
}
@keyframes price-sweep {
0% { transform: translateX(-100%); }
100% { transform: translateX(350%); }
}
/* Tag add button */
.shopping-item-tag-btn {
background: none;