feat: add smart scale BLE gateway integration

- Add evershelf-scale-gateway/ Android app (Kotlin):
  - BLE scanning and GATT connection to smart scales
  - Supports BT SIG Weight Scale (0x181D), Body Composition (0x181B), and generic heuristic parser
  - WebSocket server on port 8765 (local LAN)
  - Real-time weight broadcasting to EverShelf browser client
- Add scale status indicator in header (green/orange/grey dot)
- Add Settings tab for scale configuration (URL, enable toggle, test, APK download link)
- Add 'Read from scale' button in Add/Use forms when unit is g or ml
- Add scale WebSocket client logic in app.js with auto-reconnect
- Fix recipe suggestion: expiry-prioritized ingredients now only injected into
  AI prompt when user explicitly selects 'Priorità Scadenze' or 'Zero Sprechi'
- Update README with smart scale section and website link
- Update all translations (it, en, de) with scale strings
This commit is contained in:
dadaloop82
2026-04-14 15:59:40 +00:00
parent f2b518dd4b
commit 0893742f05
22 changed files with 1954 additions and 22 deletions
+30 -22
View File
@@ -1669,30 +1669,38 @@ function generateRecipe(PDO $db): void {
}
$ingredientsText = implode("\n\n", $ingredientSections);
// Build mandatory/recommended lists:
// - Truly mandatory: expired (group 1) OR expiring within 1 day (group 2 + daysLeft ≤ 1)
// These are genuinely at risk of being wasted RIGHT NOW.
// - Highly recommended: expiring in 2-3 days (group 2 + daysLeft > 1)
// Fresh products like milk naturally have short shelf lives and are often used anyway,
// so we suggest rather than force them.
// Build mandatory/recommended lists ONLY when user explicitly selected
// 'scadenze' (expiry priority) or 'zerowaste' (zero waste) options.
// Without these options, the recipe should use ALL available ingredients freely
// without being biased toward expiring items.
$mandatoryItems = [];
$recommendedItems = [];
foreach ($items as $item) {
$g = $getItemPriority($item);
$daysLeft = floatval($item['days_left']);
$isOpen = !empty($item['opened_at']) ||
(floatval($item['quantity']) > 0 && floatval($item['quantity']) < 1 && $item['unit'] === 'conf');
$expiryNote = !empty($item['expiry_date']) ? " — scade: {$item['expiry_date']}" : '';
$openNote = $isOpen ? ' [APERTO]' : '';
$label = $item['name'] . ($item['brand'] ? " ({$item['brand']})" : '') . $openNote . $expiryNote;
if ($g === 1 || ($g === 2 && $daysLeft <= 1)) {
$mandatoryItems[] = $label;
} elseif ($g === 2) {
$recommendedItems[] = $label;
} elseif ($isOpen && $daysLeft <= 5 && $daysLeft >= 0) {
// Opened items expiring within 5 days but not already in mandatory/recommended
// → bump to "strongly recommended" so the AI knows to use them
$recommendedItems[] = $label;
$wantsExpiryPriority = in_array('scadenze', $options) || in_array('zerowaste', $options);
$wantsOpenedPriority = in_array('opened', $options);
if ($wantsExpiryPriority || $wantsOpenedPriority) {
foreach ($items as $item) {
$g = $getItemPriority($item);
$daysLeft = floatval($item['days_left']);
$isOpen = !empty($item['opened_at']) ||
(floatval($item['quantity']) > 0 && floatval($item['quantity']) < 1 && $item['unit'] === 'conf');
$expiryNote = !empty($item['expiry_date']) ? " — scade: {$item['expiry_date']}" : '';
$openNote = $isOpen ? ' [APERTO]' : '';
$label = $item['name'] . ($item['brand'] ? " ({$item['brand']})" : '') . $openNote . $expiryNote;
if ($wantsExpiryPriority) {
if ($g === 1 || ($g === 2 && $daysLeft <= 1)) {
$mandatoryItems[] = $label;
} elseif ($g === 2) {
$recommendedItems[] = $label;
}
}
if (($wantsOpenedPriority || $wantsExpiryPriority) && $isOpen && $daysLeft <= 5 && $daysLeft >= 0) {
// Opened items expiring within 5 days but not already in mandatory/recommended
if (!in_array($label, $mandatoryItems) && !in_array($label, $recommendedItems)) {
$recommendedItems[] = $label;
}
}
}
}