Add ops scripts and offline transformers bootstrap.
Maintenance CLI for finished-product/shopping audit, Bring sync, and install-transformers-model.sh to fetch the Xenova classifier (model gitignored). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
node_modules/
|
||||||
|
.git/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
*.log
|
||||||
@@ -52,3 +52,4 @@ data/food_facts_cache.json
|
|||||||
data/category_ai_cache.json
|
data/category_ai_cache.json
|
||||||
assets/img/logo/*_backup.*
|
assets/img/logo/*_backup.*
|
||||||
logs/*.log
|
logs/*.log
|
||||||
|
assets/vendor/transformers/Xenova/
|
||||||
|
|||||||
+107
File diff suppressed because one or more lines are too long
@@ -0,0 +1,163 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Audit: products depleted in last N days vs shopping list / Bring / smart shopping.
|
||||||
|
* Usage: php scripts/audit-finished-shopping.php [days]
|
||||||
|
*/
|
||||||
|
define('CRON_MODE', true);
|
||||||
|
require_once __DIR__ . '/../api/bootstrap.php';
|
||||||
|
require_once __DIR__ . '/../api/index.php';
|
||||||
|
|
||||||
|
$days = max(1, (int)($argv[1] ?? 30));
|
||||||
|
$db = getDB();
|
||||||
|
|
||||||
|
// Recompute smart shopping fresh
|
||||||
|
ob_start();
|
||||||
|
smartShopping($db);
|
||||||
|
$smartJson = ob_get_clean();
|
||||||
|
$smartData = json_decode($smartJson, true);
|
||||||
|
$smartItems = $smartData['items'] ?? [];
|
||||||
|
$smartByPid = [];
|
||||||
|
$smartByName = [];
|
||||||
|
foreach ($smartItems as $si) {
|
||||||
|
foreach ($si['variants'] ?? [] as $v) {
|
||||||
|
$smartByPid[(int)$v['product_id']] = $si;
|
||||||
|
}
|
||||||
|
$smartByPid[(int)$si['product_id']] = $si;
|
||||||
|
$sn = strtolower(trim($si['shopping_name'] ?? $si['name'] ?? ''));
|
||||||
|
if ($sn !== '') $smartByName[$sn] = $si;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bring list
|
||||||
|
$bringNames = [];
|
||||||
|
$bringSpecs = [];
|
||||||
|
$auth = bringAuth();
|
||||||
|
if ($auth && !empty($auth['bringListUUID'])) {
|
||||||
|
$listData = bringRequest('GET', "https://api.getbring.com/rest/v2/bringlists/{$auth['bringListUUID']}");
|
||||||
|
if ($listData && isset($listData['purchase'])) {
|
||||||
|
foreach ($listData['purchase'] as $bi) {
|
||||||
|
$k = mb_strtolower($bi['name'] ?? '');
|
||||||
|
$bringNames[$k] = $bi['name'] ?? '';
|
||||||
|
$bringSpecs[$k] = $bi['specification'] ?? '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal shopping list
|
||||||
|
$shopNames = [];
|
||||||
|
$shopRows = $db->query("SELECT name, specification FROM shopping_list")->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
foreach ($shopRows as $r) {
|
||||||
|
$shopNames[mb_strtolower($r['name'])] = $r;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Products with zero stock, last activity in window
|
||||||
|
$rows = $db->query("
|
||||||
|
SELECT p.id, p.name, p.brand, p.shopping_name, p.unit,
|
||||||
|
COALESCE((SELECT SUM(i.quantity) FROM inventory i WHERE i.product_id = p.id), 0) AS stock_qty,
|
||||||
|
(SELECT MAX(t.created_at) FROM transactions t
|
||||||
|
WHERE t.product_id = p.id AND t.undone = 0
|
||||||
|
AND t.type IN ('out','waste','in')
|
||||||
|
AND t.created_at >= datetime('now', '-{$days} days')) AS last_activity,
|
||||||
|
(SELECT MAX(t.created_at) FROM transactions t
|
||||||
|
WHERE t.product_id = p.id AND t.undone = 0
|
||||||
|
AND t.type IN ('out','waste')
|
||||||
|
AND t.created_at >= datetime('now', '-{$days} days')) AS last_out,
|
||||||
|
(SELECT COUNT(*) FROM transactions t
|
||||||
|
WHERE t.product_id = p.id AND t.undone = 0 AND t.type IN ('out','waste')) AS use_count,
|
||||||
|
(SELECT COUNT(*) FROM transactions t
|
||||||
|
WHERE t.product_id = p.id AND t.undone = 0 AND t.type = 'in') AS buy_count
|
||||||
|
FROM products p
|
||||||
|
WHERE COALESCE((SELECT SUM(i.quantity) FROM inventory i WHERE i.product_id = p.id), 0) <= 0.001
|
||||||
|
AND (SELECT MAX(t.created_at) FROM transactions t
|
||||||
|
WHERE t.product_id = p.id AND t.undone = 0
|
||||||
|
AND t.type IN ('out','waste','in')
|
||||||
|
AND t.created_at >= datetime('now', '-{$days} days')) IS NOT NULL
|
||||||
|
ORDER BY last_activity DESC
|
||||||
|
")->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$missing = [];
|
||||||
|
$onList = [];
|
||||||
|
$suppressed = [];
|
||||||
|
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
$pid = (int)$r['id'];
|
||||||
|
$generic = trim($r['shopping_name'] ?? '') ?: computeShoppingName($r['name'], '', $r['brand'] ?? '');
|
||||||
|
$bringKey = mb_strtolower(italianToBring($generic));
|
||||||
|
$shopKey = mb_strtolower($generic);
|
||||||
|
|
||||||
|
$smart = $smartByPid[$pid] ?? $smartByName[mb_strtolower($generic)] ?? null;
|
||||||
|
$onBring = isset($bringNames[$bringKey]);
|
||||||
|
$onShop = isset($shopNames[$shopKey]);
|
||||||
|
$inSmart = $smart !== null && ($smart['urgency'] ?? 'none') !== 'none';
|
||||||
|
|
||||||
|
$entry = [
|
||||||
|
'id' => $pid,
|
||||||
|
'name' => $r['name'],
|
||||||
|
'brand' => $r['brand'],
|
||||||
|
'generic' => $generic,
|
||||||
|
'last_activity' => $r['last_activity'],
|
||||||
|
'last_out' => $r['last_out'],
|
||||||
|
'use_count' => (int)$r['use_count'],
|
||||||
|
'buy_count' => (int)$r['buy_count'],
|
||||||
|
'on_bring' => $onBring,
|
||||||
|
'on_shop' => $onShop,
|
||||||
|
'in_smart' => $inSmart,
|
||||||
|
'smart_urgency' => $smart['urgency'] ?? null,
|
||||||
|
'smart_reasons' => $smart['reasons'] ?? [],
|
||||||
|
'bring_spec' => $bringSpecs[$bringKey] ?? '',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!$onBring && !$onShop && !$inSmart) {
|
||||||
|
$missing[] = $entry;
|
||||||
|
} elseif ($onBring || $onShop) {
|
||||||
|
$onList[] = $entry;
|
||||||
|
} elseif ($inSmart) {
|
||||||
|
$suppressed[] = $entry; // in smart but not synced yet
|
||||||
|
} else {
|
||||||
|
$missing[] = $entry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "=== Audit prodotti esauriti (ultimi {$days} giorni) ===\n";
|
||||||
|
echo 'Totale esauriti con attività recente: ' . count($rows) . "\n";
|
||||||
|
echo 'Già in lista/Bring: ' . count($onList) . "\n";
|
||||||
|
echo 'In smart shopping ma non in lista: ' . count($suppressed) . "\n";
|
||||||
|
echo 'MANCANTI (né lista né Bring né smart): ' . count($missing) . "\n\n";
|
||||||
|
|
||||||
|
if ($missing) {
|
||||||
|
echo "--- MANCANTI ---\n";
|
||||||
|
foreach ($missing as $m) {
|
||||||
|
echo sprintf(
|
||||||
|
"- [%d] %s%s → generico: %s | usi:%d acquisti:%d | ultimo:%s\n",
|
||||||
|
$m['id'],
|
||||||
|
$m['name'],
|
||||||
|
$m['brand'] ? " ({$m['brand']})" : '',
|
||||||
|
$m['generic'],
|
||||||
|
$m['use_count'],
|
||||||
|
$m['buy_count'],
|
||||||
|
$m['last_activity']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
echo "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($suppressed) {
|
||||||
|
echo "--- IN SMART MA NON IN LISTA/BRING ---\n";
|
||||||
|
foreach ($suppressed as $m) {
|
||||||
|
echo sprintf(
|
||||||
|
"- [%d] %s → %s | urgenza:%s | %s\n",
|
||||||
|
$m['id'],
|
||||||
|
$m['name'],
|
||||||
|
$m['generic'],
|
||||||
|
$m['smart_urgency'] ?? '?',
|
||||||
|
implode(', ', $m['smart_reasons'] ?? [])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export JSON for fix script
|
||||||
|
file_put_contents(
|
||||||
|
__DIR__ . '/../data/audit_finished_missing.json',
|
||||||
|
json_encode(['days' => $days, 'missing' => $missing, 'suppressed' => $suppressed], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)
|
||||||
|
);
|
||||||
|
echo "\nReport salvato in data/audit_finished_missing.json\n";
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Backfill Bring!/shopping list for products depleted in the last N days.
|
||||||
|
* Usage: php scripts/backfill-finished-shopping.php [days]
|
||||||
|
*/
|
||||||
|
define('CRON_MODE', true);
|
||||||
|
require_once __DIR__ . '/../api/bootstrap.php';
|
||||||
|
require_once __DIR__ . '/../api/index.php';
|
||||||
|
|
||||||
|
$days = max(1, (int)($argv[1] ?? RECENTLY_EXHAUSTED_DAYS));
|
||||||
|
$db = getDB();
|
||||||
|
|
||||||
|
$rows = $db->query("
|
||||||
|
SELECT p.id, p.name, p.shopping_name
|
||||||
|
FROM products p
|
||||||
|
WHERE COALESCE((SELECT SUM(i.quantity) FROM inventory i WHERE i.product_id = p.id), 0) <= 0.001
|
||||||
|
AND (
|
||||||
|
SELECT MAX(t.created_at) FROM transactions t
|
||||||
|
WHERE t.product_id = p.id AND t.undone = 0 AND t.type IN ('out','waste')
|
||||||
|
) >= datetime('now', '-{$days} days')
|
||||||
|
ORDER BY (
|
||||||
|
SELECT MAX(t.created_at) FROM transactions t
|
||||||
|
WHERE t.product_id = p.id AND t.undone = 0 AND t.type IN ('out','waste')
|
||||||
|
) DESC
|
||||||
|
")->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
echo '[' . date('Y-m-d H:i:s') . "] Backfill {$days}d — " . count($rows) . " prodotti esauriti\n";
|
||||||
|
|
||||||
|
$added = 0;
|
||||||
|
$updated = 0;
|
||||||
|
$skipped = 0;
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
$res = bringAddDepletedProduct($db, (int)$r['id']);
|
||||||
|
if (!empty($res['added'])) {
|
||||||
|
$added++;
|
||||||
|
echo " + {$r['name']} → {$res['generic_name']}\n";
|
||||||
|
} elseif (!empty($res['updated'])) {
|
||||||
|
$updated++;
|
||||||
|
echo " ~ {$r['name']} → {$res['generic_name']}\n";
|
||||||
|
} else {
|
||||||
|
$skipped++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
smartShopping($db);
|
||||||
|
$json = ob_get_clean();
|
||||||
|
$decoded = json_decode($json, true);
|
||||||
|
if ($decoded && !empty($decoded['success'])) {
|
||||||
|
$decoded['cached_at'] = date('c');
|
||||||
|
$decoded['cached_ts'] = time();
|
||||||
|
file_put_contents(
|
||||||
|
__DIR__ . '/../data/smart_shopping_cache.json',
|
||||||
|
json_encode($decoded, JSON_UNESCAPED_UNICODE)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
bringSyncFull($db, false);
|
||||||
|
$sync = json_decode(ob_get_clean(), true);
|
||||||
|
$auto = $sync['auto_add'] ?? [];
|
||||||
|
|
||||||
|
echo '[' . date('Y-m-d H:i:s') . "] bringAddDepleted: added={$added} updated={$updated} skipped={$skipped}\n";
|
||||||
|
echo '[' . date('Y-m-d H:i:s') . '] bringSync auto_add: ' . json_encode($auto, JSON_UNESCAPED_UNICODE) . "\n";
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Download @xenova/transformers runtime + all-MiniLM-L6-v2 for offline category classification.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
VENDOR="$ROOT/assets/vendor/transformers"
|
||||||
|
MODEL="$VENDOR/Xenova/all-MiniLM-L6-v2"
|
||||||
|
ONNX="$MODEL/onnx"
|
||||||
|
BASE="https://huggingface.co/Xenova/all-MiniLM-L6-v2/resolve/main"
|
||||||
|
|
||||||
|
mkdir -p "$ONNX"
|
||||||
|
|
||||||
|
echo "→ transformers.min.js"
|
||||||
|
curl -fsSL "https://cdn.jsdelivr.net/npm/@xenova/transformers@2.17.2/dist/transformers.min.js" \
|
||||||
|
-o "$VENDOR/transformers.min.js"
|
||||||
|
|
||||||
|
for f in config.json tokenizer.json tokenizer_config.json; do
|
||||||
|
echo "→ $f"
|
||||||
|
curl -fsSL "$BASE/$f" -o "$MODEL/$f"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "→ onnx/model_quantized.onnx (~22 MB)"
|
||||||
|
curl -fsSL "$BASE/onnx/model_quantized.onnx" -o "$ONNX/model_quantized.onnx"
|
||||||
|
|
||||||
|
chown -R www-data:www-data "$VENDOR" 2>/dev/null || true
|
||||||
|
echo "Done. Model installed under assets/vendor/transformers/"
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Full Bring! sync: recompute smart shopping, migrate names, dedupe generics,
|
||||||
|
* fix specs, remove obsolete items, add missing critical/high.
|
||||||
|
*
|
||||||
|
* Usage: php scripts/sync-shopping-bring.php
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (PHP_SAPI !== 'cli') {
|
||||||
|
http_response_code(403);
|
||||||
|
exit('Forbidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
define('CRON_MODE', true);
|
||||||
|
require_once __DIR__ . '/../api/bootstrap.php';
|
||||||
|
require_once __DIR__ . '/../api/index.php';
|
||||||
|
|
||||||
|
$db = getDB();
|
||||||
|
|
||||||
|
echo '[' . date('Y-m-d H:i:s') . "] Starting full Bring! sync…\n";
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
bringSyncFull($db, true);
|
||||||
|
$json = ob_get_clean();
|
||||||
|
$result = json_decode($json, true);
|
||||||
|
|
||||||
|
if (!$result || empty($result['success'])) {
|
||||||
|
echo '[' . date('Y-m-d H:i:s') . '] ERROR: ' . ($result['error'] ?? $json) . "\n";
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '[' . date('Y-m-d H:i:s') . '] Smart items: ' . ($result['smart_items'] ?? '?') . "\n";
|
||||||
|
echo '[' . date('Y-m-d H:i:s') . '] Migrate: ' . json_encode($result['migrate'] ?? [], JSON_UNESCAPED_UNICODE) . "\n";
|
||||||
|
echo '[' . date('Y-m-d H:i:s') . '] Dedupe: ' . json_encode($result['dedupe'] ?? [], JSON_UNESCAPED_UNICODE) . "\n";
|
||||||
|
echo '[' . date('Y-m-d H:i:s') . '] Specs: ' . json_encode($result['specs'] ?? [], JSON_UNESCAPED_UNICODE) . "\n";
|
||||||
|
echo '[' . date('Y-m-d H:i:s') . '] Cleanup: ' . json_encode($result['cleanup'] ?? [], JSON_UNESCAPED_UNICODE) . "\n";
|
||||||
|
echo '[' . date('Y-m-d H:i:s') . '] Auto-add: ' . json_encode($result['auto_add'] ?? [], JSON_UNESCAPED_UNICODE) . "\n";
|
||||||
|
if (!empty($result['dedupe_final'])) {
|
||||||
|
echo '[' . date('Y-m-d H:i:s') . '] Dedupe (final): ' . json_encode($result['dedupe_final'], JSON_UNESCAPED_UNICODE) . "\n";
|
||||||
|
}
|
||||||
|
if (!empty($result['cache_restored'])) {
|
||||||
|
echo '[' . date('Y-m-d H:i:s') . '] Cache restored: ' . $result['cache_restored'] . " items\n";
|
||||||
|
}
|
||||||
|
echo '[' . date('Y-m-d H:i:s') . "] Done.\n";
|
||||||
Reference in New Issue
Block a user