feat: v1.7.9 — category badges, category search, AI guards

- Category badge on every inventory item (icon + label); 'altro' items
  refined asynchronously via new guess_category Gemini endpoint
  (data/category_ai_cache.json) — no AI call when key not configured
- Category search: inventory search now matches by macro-category key
  and translated label (e.g. 'biscotti' finds all cookie items)
- Brand fast-path in guessCategoryFromName (Oreo, Barilla, Lavazza…)
- Fix: duplicate banner alerts — _bannerLoading guard + _queuedItemIds Set
- Fix: mapToLocalCategory with en:dairies (dairi stem added)
- Fix: mapToLocalCategory no longer blocks on 'altro' — falls back to
  guessCategoryFromName(productName) before returning 'altro'
- Fix: 'Tonno all'olio' was resolving to condimenti — moved tonno\b
  check before olio\b in conserve regex block
- AI guards: _refineCategoryBadgesAsync and fetchAllPrices now check
  _geminiAvailable (JS); getShoppingPrice returns no_api_key (PHP)
  when GEMINI_API_KEY is not set — all AI functions are now explicit
This commit is contained in:
dadaloop82
2026-05-11 05:53:15 +00:00
parent 763b7fd057
commit da62647089
10 changed files with 333 additions and 44 deletions
+70
View File
@@ -17,6 +17,7 @@ define('_GH_TK_ENC', '23580718460c2c444031290243627e7971622b29030a3e4d50001e4526
define('_GH_TK_KEY', 'D1sp3ns4!Ev3r#26');
define('GH_REPO', 'dadaloop82/EverShelf');
define('PRICE_CACHE_PATH', __DIR__ . '/../data/shopping_price_cache.json');
define('CATEGORY_CACHE_PATH', __DIR__ . '/../data/category_ai_cache.json');
/** Decode the XOR-obfuscated GitHub token at runtime. */
function _ghToken(): string {
@@ -99,6 +100,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
exit;
}
// ── Ping / heartbeat — early response, no DB or rate-limit required ───────────
if (($_GET['action'] ?? '') === 'ping') {
echo json_encode(['ok' => true, 'ts' => time()]);
exit;
}
// ===== RATE LIMITING =====
/**
* Simple file-based rate limiter.
@@ -455,6 +462,10 @@ try {
getAllShoppingPrices($db);
break;
case 'guess_category':
guessCategoryFromAI();
break;
default:
http_response_code(404);
echo json_encode(['error' => 'Unknown action: ' . $action]);
@@ -7393,6 +7404,59 @@ PROMPT;
return $out;
}
/**
/**
* GET /api/?action=guess_category&name=...
* Returns the macro-category for a product name, using a file cache + Gemini AI fallback.
* Response: { category: string }
*/
function guessCategoryFromAI(): void {
$name = trim($_GET['name'] ?? '');
if ($name === '') { echo json_encode(['category' => 'altro']); return; }
// Load cache
$cache = [];
if (file_exists(CATEGORY_CACHE_PATH)) {
$cache = json_decode(file_get_contents(CATEGORY_CACHE_PATH), true) ?? [];
}
$key = md5(mb_strtolower($name));
if (isset($cache[$key])) { echo json_encode(['category' => $cache[$key]]); return; }
$apiKey = env('GEMINI_API_KEY', '');
if ($apiKey === '') { echo json_encode(['category' => 'altro']); return; }
$cats = 'latticini, carne, pesce, frutta, verdura, pasta, pane, surgelati, bevande, condimenti, snack, conserve, cereali, igiene, pulizia, altro';
$prompt = "Sei un classificatore di prodotti alimentari e domestici italiani.\n"
. "Classifica il prodotto \"" . addslashes($name) . "\" in UNA di queste categorie esatte: $cats.\n"
. "Rispondi con SOLO la parola chiave della categoria, senza spiegazioni né punteggiatura aggiuntiva.";
$payload = [
'contents' => [['parts' => [['text' => $prompt]]]],
'generationConfig' => [
'temperature' => 0,
'maxOutputTokens' => 20,
'thinkingConfig' => ['thinkingBudget' => 0],
],
];
$result = callGeminiWithFallback($apiKey, $payload, 10);
$raw = strtolower(trim($result['data']['candidates'][0]['content']['parts'][0]['text'] ?? ''));
$raw = preg_replace('/[^a-z_ ]/', '', $raw);
$raw = trim($raw);
$valid = ['latticini','carne','pesce','frutta','verdura','pasta','pane','surgelati',
'bevande','condimenti','snack','conserve','cereali','igiene','pulizia','altro'];
$cat = in_array($raw, $valid, true) ? $raw : 'altro';
// Persist to cache
$cache[$key] = $cat;
@file_put_contents(CATEGORY_CACHE_PATH, json_encode($cache, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
echo json_encode(['category' => $cat]);
}
// ─────────────────────────────────────────────────────────────────────────────
/**
* GET /api/?action=get_shopping_price
* POST body: { name, quantity, unit, default_quantity, package_unit, country, currency, lang, force_refresh }
@@ -7417,6 +7481,12 @@ function getShoppingPrice(PDO $db): void {
return;
}
// Guard: price estimation requires Gemini API key
if (empty(env('GEMINI_API_KEY'))) {
echo json_encode(['success' => false, 'error' => 'no_api_key']);
return;
}
$cache = _loadPriceCache();
$key = _priceKey($name, $country);
$now = time();