feat: BarcodeDetector nativo + camera selector + recipe dedup + remote debug logging
- Scanner: usa BarcodeDetector API nativa (Chrome Android) come primario, Quagga come fallback - Settings: aggiunta tab Fotocamera per scegliere posteriore/anteriore/specifica - Scanner feedback: barra verde (scansione attiva), gialla (barcode rilevato) - Ricette: invio titoli ricette del giorno per evitare duplicati nello stesso giorno - Debug: sistema di logging remoto (client_debug.log) per diagnostica da dispositivi chioscati - Fix: permessi .env per scrittura da Apache
This commit is contained in:
+153
-32
@@ -121,6 +121,14 @@ try {
|
||||
getServerSettings();
|
||||
break;
|
||||
|
||||
case 'client_log':
|
||||
clientLog();
|
||||
break;
|
||||
|
||||
case 'get_client_log':
|
||||
getClientLog();
|
||||
break;
|
||||
|
||||
// ===== SPESA ONLINE =====
|
||||
case 'dupliclick_login':
|
||||
dupliclickLogin();
|
||||
@@ -149,6 +157,48 @@ try {
|
||||
echo json_encode(['error' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
// ===== CLIENT LOG =====
|
||||
|
||||
function clientLog(): void {
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$logFile = __DIR__ . '/../data/client_debug.log';
|
||||
$ua = $_SERVER['HTTP_USER_AGENT'] ?? 'unknown';
|
||||
// Identify device from UA
|
||||
$device = 'unknown';
|
||||
if (preg_match('/tablet|ipad|playbook|silk/i', $ua)) $device = 'tablet';
|
||||
elseif (preg_match('/mobile|android|iphone/i', $ua)) $device = 'phone';
|
||||
else $device = 'desktop';
|
||||
$ts = date('Y-m-d H:i:s');
|
||||
$msgs = $input['messages'] ?? [];
|
||||
$lines = [];
|
||||
foreach ($msgs as $m) {
|
||||
$lines[] = "[$ts] [$device] $m";
|
||||
}
|
||||
if ($lines) {
|
||||
// Keep log under 100KB — truncate oldest if needed
|
||||
if (file_exists($logFile) && filesize($logFile) > 100000) {
|
||||
$existing = file($logFile);
|
||||
$existing = array_slice($existing, -200);
|
||||
file_put_contents($logFile, implode('', $existing));
|
||||
}
|
||||
file_put_contents($logFile, implode("\n", $lines) . "\n", FILE_APPEND | LOCK_EX);
|
||||
}
|
||||
echo json_encode(['ok' => true]);
|
||||
}
|
||||
|
||||
function getClientLog(): void {
|
||||
$logFile = __DIR__ . '/../data/client_debug.log';
|
||||
$lines = 100;
|
||||
if (isset($_GET['lines'])) $lines = min(500, max(1, (int)$_GET['lines']));
|
||||
if (!file_exists($logFile)) {
|
||||
echo json_encode(['log' => '(empty)', 'lines' => 0]);
|
||||
return;
|
||||
}
|
||||
$all = file($logFile);
|
||||
$tail = array_slice($all, -$lines);
|
||||
echo json_encode(['log' => implode('', $tail), 'lines' => count($tail), 'total' => count($all)]);
|
||||
}
|
||||
|
||||
// ===== PRODUCT FUNCTIONS =====
|
||||
|
||||
function searchBarcode(PDO $db): void {
|
||||
@@ -1055,6 +1105,7 @@ function generateRecipe(PDO $db): void {
|
||||
$options = $input['options'] ?? [];
|
||||
$appliances = $input['appliances'] ?? [];
|
||||
$dietaryRestrictions = $input['dietary_restrictions'] ?? '';
|
||||
$todayRecipes = $input['today_recipes'] ?? [];
|
||||
|
||||
// Fetch all inventory items with expiry info
|
||||
$stmt = $db->query("
|
||||
@@ -1146,9 +1197,16 @@ function generateRecipe(PDO $db): void {
|
||||
$dietaryText = "\n\nRESTRIZIONI ALIMENTARI:\n{$dietaryRestrictions}\nRispetta SEMPRE queste restrizioni.";
|
||||
}
|
||||
|
||||
// Today's previous recipes - avoid repetition
|
||||
$todayText = '';
|
||||
if (!empty($todayRecipes)) {
|
||||
$todayList = implode(', ', array_map(function($t) { return '"' . $t . '"'; }, $todayRecipes));
|
||||
$todayText = "\n\nRICETTE GIÀ PREPARATE OGGI:\n{$todayList}\nNON proporre una ricetta simile o con lo stesso concetto di quelle già fatte oggi. Varia il tipo di piatto, gli ingredienti principali e lo stile di cucina. Ad esempio se a pranzo c'era una piadina, a cena proponi pasta, riso, zuppa o altro — MAI un'altra piadina o wrap o piatto concettualmente simile.";
|
||||
}
|
||||
|
||||
$prompt = <<<PROMPT
|
||||
Sei un nutrizionista e chef italiano esperto. Genera UNA ricetta per $mealLabel per $persons persona/e usando PRINCIPALMENTE gli ingredienti disponibili nella dispensa dell'utente.
|
||||
{$extraRulesText}{$appliancesText}{$dietaryText}
|
||||
{$extraRulesText}{$appliancesText}{$dietaryText}{$todayText}
|
||||
|
||||
REGOLE IMPORTANTI:
|
||||
1. PRIORITÀ ASSOLUTA: usa prima gli ingredienti in scadenza o già scaduti (se ancora utilizzabili)
|
||||
@@ -2198,45 +2256,35 @@ function dupliclickSearch(): void {
|
||||
'x-ebsn-account: ' . $token,
|
||||
];
|
||||
|
||||
// Search catalog by item name only (spec confuses the search engine)
|
||||
$url = 'https://www.dupliclick.it/ebsn/api/products?' . http_build_query([
|
||||
'q' => $query,
|
||||
'page' => 1,
|
||||
'order_by' => 'search_score desc'
|
||||
]);
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 15,
|
||||
CURLOPT_HTTPHEADER => $baseHeaders,
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
if (curl_errno($ch)) {
|
||||
echo json_encode(['error' => 'Errore connessione DupliClick: ' . curl_error($ch)]);
|
||||
curl_close($ch);
|
||||
return;
|
||||
}
|
||||
curl_close($ch);
|
||||
|
||||
$data = json_decode(trim($response), true);
|
||||
if (!$data || ($data['response']['status'] ?? -1) !== 0) {
|
||||
echo json_encode(['error' => 'Errore nella ricerca', 'details' => $data['response'] ?? null]);
|
||||
// Search catalog by item name only first
|
||||
$searchResults = dupliclickCatalogSearch($query, $baseHeaders);
|
||||
if ($searchResults === null) {
|
||||
echo json_encode(['error' => 'Errore nella ricerca']);
|
||||
return;
|
||||
}
|
||||
|
||||
$products = $data['data']['products'] ?? [];
|
||||
$products = $searchResults['products'];
|
||||
$total = $searchResults['total'];
|
||||
|
||||
if (empty($products)) {
|
||||
// Fallback: try searching with spec keywords appended
|
||||
$specKeywords = dupliclickExtractSpecKeywords($spec);
|
||||
if ($specKeywords) {
|
||||
$searchResults = dupliclickCatalogSearch($query . ' ' . $specKeywords, $baseHeaders);
|
||||
if ($searchResults && !empty($searchResults['products'])) {
|
||||
$products = $searchResults['products'];
|
||||
$total = $searchResults['total'];
|
||||
}
|
||||
}
|
||||
if (empty($products)) {
|
||||
echo json_encode(['success' => true, 'query' => $query, 'product' => null, 'total' => 0]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Format top 10 products
|
||||
$topProducts = array_slice($products, 0, 10);
|
||||
$formatted = array_map('formatDupliclickProduct', $topProducts);
|
||||
$total = $data['data']['page']['totItems'] ?? 0;
|
||||
|
||||
// If multiple results, use AI to pick the best match
|
||||
$bestProduct = $formatted[0];
|
||||
@@ -2246,6 +2294,22 @@ function dupliclickSearch(): void {
|
||||
if ($aiResult !== null) {
|
||||
$bestProduct = $aiResult;
|
||||
$aiUsed = true;
|
||||
} elseif ($aiResult === null && !empty($spec)) {
|
||||
// AI said no match — try refined search with spec keywords
|
||||
$specKeywords = dupliclickExtractSpecKeywords($spec);
|
||||
if ($specKeywords) {
|
||||
$refined = dupliclickCatalogSearch($query . ' ' . $specKeywords, $baseHeaders);
|
||||
if ($refined && !empty($refined['products'])) {
|
||||
$refinedFormatted = array_map('formatDupliclickProduct', array_slice($refined['products'], 0, 10));
|
||||
$aiResult2 = aiSelectBestProduct($query, $spec, $refinedFormatted, $aiPrompt);
|
||||
if ($aiResult2 !== null) {
|
||||
$bestProduct = $aiResult2;
|
||||
$aiUsed = true;
|
||||
} else {
|
||||
$bestProduct = $refinedFormatted[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2258,6 +2322,59 @@ function dupliclickSearch(): void {
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search DupliClick catalog and return raw products array
|
||||
*/
|
||||
function dupliclickCatalogSearch(string $query, array $headers): ?array {
|
||||
$url = 'https://www.dupliclick.it/ebsn/api/products?' . http_build_query([
|
||||
'q' => $query,
|
||||
'page' => 1,
|
||||
'order_by' => 'search_score desc'
|
||||
]);
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 15,
|
||||
CURLOPT_HTTPHEADER => $headers,
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
if (curl_errno($ch)) { curl_close($ch); return null; }
|
||||
curl_close($ch);
|
||||
|
||||
$data = json_decode(trim($response), true);
|
||||
if (!$data || ($data['response']['status'] ?? -1) !== 0) return null;
|
||||
|
||||
return [
|
||||
'products' => $data['data']['products'] ?? [],
|
||||
'total' => $data['data']['page']['totItems'] ?? 0,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract meaningful product keywords from a Bring specification string,
|
||||
* stripping quantities, emojis, and noise words.
|
||||
*/
|
||||
function dupliclickExtractSpecKeywords(string $spec): string {
|
||||
if (empty($spec)) return '';
|
||||
// Remove priority emojis
|
||||
$clean = preg_replace('/[\x{1F534}\x{1F7E1}\x{1F7E2}]/u', '', $spec);
|
||||
// Remove quantities (150g, 500ml, 2x, 1 flacone, etc.)
|
||||
$clean = preg_replace('/\d+\s*(g|kg|ml|l|pz|pezzi|conf|flacon[ei]|x)\b/i', '', $clean);
|
||||
$clean = preg_replace('/\d+x\d*/i', '', $clean);
|
||||
// Remove standalone numbers
|
||||
$clean = preg_replace('/\b\d+\b/', '', $clean);
|
||||
// Remove noise words
|
||||
$noise = ['senza', 'con', 'più', 'meno', 'circa', 'tipo', 'lidl', 'coop', 'conad', 'esselunga'];
|
||||
$clean = preg_replace('/\b(' . implode('|', $noise) . ')\b/i', '', $clean);
|
||||
// Remove commas and extra spaces
|
||||
$clean = preg_replace('/[,+]/', ' ', $clean);
|
||||
$clean = preg_replace('/\s+/', ' ', trim($clean));
|
||||
return $clean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use Gemini AI to pick the best product from search results
|
||||
*/
|
||||
@@ -2266,17 +2383,21 @@ function aiSelectBestProduct(string $itemName, string $spec, array $products, st
|
||||
$apiKey = $env['GEMINI_API_KEY'] ?? '';
|
||||
if (empty($apiKey)) return null;
|
||||
|
||||
$defaultPrompt = "Sei un assistente per la spesa online. Ti viene dato il nome di un prodotto che l'utente vuole comprare e una lista di prodotti trovati nel catalogo del supermercato.
|
||||
$defaultPrompt = "Sei un assistente per la spesa online. Ti viene dato il nome di un prodotto che l'utente vuole comprare (con eventuale descrizione tra parentesi) e una lista di prodotti trovati nel catalogo del supermercato.
|
||||
|
||||
Regole di selezione:
|
||||
- Scegli il prodotto che corrisponde ESATTAMENTE a quello richiesto (stessa categoria merceologica)
|
||||
- La DESCRIZIONE tra parentesi è FONDAMENTALE: se l'utente cerca \"Pancetta (a cubetti)\", DEVI trovare pancetta A CUBETTI, non pancetta generica
|
||||
- Se la descrizione include un tipo specifico (\"a cubetti\", \"a fette\", \"biologico\", \"cotto\", \"a pasta dura\"), il prodotto DEVE contenere quella caratteristica nel nome
|
||||
- Preferisci prodotti freschi/sfusi rispetto a trasformati (es. \"Arance\" = arance frutta, NON aranciata bevanda)
|
||||
- Se c'è una descrizione (es. \"a cubetti\", \"biologico\"), trova il prodotto che include quella caratteristica
|
||||
- Se ci sono più varianti valide, scegli quella con il miglior rapporto qualità/prezzo
|
||||
- Preferisci formati standard per una famiglia
|
||||
- NON scegliere mai un prodotto di categoria diversa (bevanda vs frutta, surgelato vs fresco, condimento vs ortaggio, ecc.)
|
||||
- NON scegliere mai un prodotto di categoria diversa (bevanda vs frutta, surgelato vs fresco, condimento vs ortaggio, pasta ripiena vs formaggio, ecc.)
|
||||
- \"Finocchio\" = ortaggio fresco, NON semi di finocchio o tisana
|
||||
- \"Arance\" = frutta fresca, NON aranciata o succo
|
||||
- \"Formaggio\" = formaggio intero/pezzo, NON prodotti che contengono formaggio come ingrediente (ravioli, sfogliavelo, ecc.)
|
||||
- \"Detergente intimo\" = detergente per igiene intima, NON detersivo generico
|
||||
- Rispondi -1 se NESSUN prodotto corrisponde ragionevolmente alla richiesta
|
||||
|
||||
Rispondi SOLO con il numero (indice 0-based) del prodotto migliore, oppure -1 se nessun prodotto è appropriato.";
|
||||
|
||||
|
||||
@@ -863,6 +863,22 @@ body {
|
||||
background: var(--danger);
|
||||
box-shadow: 0 0 10px var(--danger);
|
||||
animation: scanLine 2s ease-in-out infinite;
|
||||
transition: background 0.2s, box-shadow 0.2s, height 0.2s;
|
||||
}
|
||||
|
||||
/* While Quagga is actively scanning frames */
|
||||
.scanner-line.scanning {
|
||||
background: #00c853;
|
||||
box-shadow: 0 0 12px #00c853;
|
||||
animation: scanLineActive 0.8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Barcode partially detected — strong pulse */
|
||||
.scanner-line.detecting {
|
||||
background: #ffd600;
|
||||
box-shadow: 0 0 20px #ffd600, 0 0 40px rgba(255,214,0,0.4);
|
||||
height: 5px;
|
||||
animation: scanLineDetect 0.3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes scanLine {
|
||||
@@ -870,6 +886,16 @@ body {
|
||||
50% { opacity: 0.3; }
|
||||
}
|
||||
|
||||
@keyframes scanLineActive {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
@keyframes scanLineDetect {
|
||||
0%, 100% { opacity: 1; transform: scaleY(1); }
|
||||
50% { opacity: 0.7; transform: scaleY(2); }
|
||||
}
|
||||
|
||||
.scan-result {
|
||||
background: var(--bg-card);
|
||||
border-radius: var(--radius);
|
||||
|
||||
+335
-40
@@ -306,6 +306,43 @@ let scannerStream = null;
|
||||
let quaggaRunning = false;
|
||||
let aiStream = null;
|
||||
|
||||
// ===== CAMERA HELPER =====
|
||||
function getCameraConstraints(extraVideo = {}) {
|
||||
const s = getSettings();
|
||||
const mode = s.camera_facing || 'environment';
|
||||
// Front cameras on older devices often have lower resolution — don't over-request
|
||||
const isFront = (mode === 'user');
|
||||
const videoConstraints = {
|
||||
width: { ideal: isFront ? 640 : 1280 },
|
||||
height: { ideal: isFront ? 480 : 720 },
|
||||
...extraVideo
|
||||
};
|
||||
if (mode === 'environment' || mode === 'user') {
|
||||
videoConstraints.facingMode = mode;
|
||||
} else {
|
||||
// Specific deviceId selected
|
||||
videoConstraints.deviceId = { exact: mode };
|
||||
}
|
||||
return { video: videoConstraints };
|
||||
}
|
||||
|
||||
function isFrontCamera() {
|
||||
const s = getSettings();
|
||||
return (s.camera_facing || 'environment') === 'user';
|
||||
}
|
||||
|
||||
async function enumerateCameras() {
|
||||
try {
|
||||
// Need a temporary stream to get device labels
|
||||
const tempStream = await navigator.mediaDevices.getUserMedia({ video: true });
|
||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||
tempStream.getTracks().forEach(t => t.stop());
|
||||
return devices.filter(d => d.kind === 'videoinput');
|
||||
} catch(e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// ===== SETTINGS / CONFIG =====
|
||||
function getSettings() {
|
||||
try {
|
||||
@@ -340,6 +377,10 @@ async function loadSettingsUI() {
|
||||
document.getElementById('setting-pref-comfort').checked = !!s.pref_comfort;
|
||||
document.getElementById('setting-pref-zerowaste').checked = !!s.pref_zerowaste;
|
||||
document.getElementById('setting-dietary').value = s.dietary || '';
|
||||
// Camera
|
||||
const cameraSelect = document.getElementById('setting-camera-facing');
|
||||
if (cameraSelect) cameraSelect.value = s.camera_facing || 'environment';
|
||||
loadCameraDevices();
|
||||
renderAppliances(s.appliances || []);
|
||||
loadSpesaSettings();
|
||||
|
||||
@@ -369,6 +410,23 @@ function renderAppliances(appliances) {
|
||||
`).join('');
|
||||
}
|
||||
|
||||
async function loadCameraDevices() {
|
||||
const select = document.getElementById('setting-camera-facing');
|
||||
if (!select) return;
|
||||
const s = getSettings();
|
||||
const current = s.camera_facing || 'environment';
|
||||
// Remove old device-specific options (keep first 2: environment, user)
|
||||
while (select.options.length > 2) select.remove(2);
|
||||
const cameras = await enumerateCameras();
|
||||
cameras.forEach(cam => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = cam.deviceId;
|
||||
opt.textContent = cam.label || `Camera ${cam.deviceId.slice(0, 8)}…`;
|
||||
select.appendChild(opt);
|
||||
});
|
||||
select.value = current;
|
||||
}
|
||||
|
||||
function addAppliance() {
|
||||
const input = document.getElementById('new-appliance-input');
|
||||
const name = (input.value || '').trim();
|
||||
@@ -420,6 +478,8 @@ async function saveSettings() {
|
||||
s.pref_comfort = document.getElementById('setting-pref-comfort').checked;
|
||||
s.pref_zerowaste = document.getElementById('setting-pref-zerowaste').checked;
|
||||
s.dietary = document.getElementById('setting-dietary').value.trim();
|
||||
// Camera
|
||||
s.camera_facing = document.getElementById('setting-camera-facing').value;
|
||||
// Save spesa AI prompt if the field exists
|
||||
const spesaPromptEl = document.getElementById('setting-spesa-ai-prompt');
|
||||
if (spesaPromptEl) s.spesa_ai_prompt = spesaPromptEl.value.trim();
|
||||
@@ -1162,31 +1222,80 @@ async function submitEditInventory(e, id, productId) {
|
||||
refreshCurrentPage();
|
||||
}
|
||||
|
||||
// ===== SCAN DEBUG LOG =====
|
||||
let _scanDebugVisible = false;
|
||||
let _scanLogBuffer = [];
|
||||
let _scanLogTimer = null;
|
||||
|
||||
function scanLog(msg) {
|
||||
const el = document.getElementById('scan-debug-log');
|
||||
if (el) {
|
||||
const ts = new Date().toLocaleTimeString('it-IT', {hour:'2-digit',minute:'2-digit',second:'2-digit',fractionalSecondDigits:1});
|
||||
el.textContent += `[${ts}] ${msg}\n`;
|
||||
el.scrollTop = el.scrollHeight;
|
||||
}
|
||||
console.log('[ScanDebug]', msg);
|
||||
// Buffer for remote send
|
||||
_scanLogBuffer.push(msg);
|
||||
if (!_scanLogTimer) {
|
||||
_scanLogTimer = setTimeout(flushScanLog, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
function flushScanLog() {
|
||||
_scanLogTimer = null;
|
||||
if (_scanLogBuffer.length === 0) return;
|
||||
const msgs = _scanLogBuffer.splice(0);
|
||||
fetch(`${API_BASE}?action=client_log`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ messages: msgs })
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
function toggleScanDebug() {
|
||||
const el = document.getElementById('scan-debug-log');
|
||||
if (!el) return;
|
||||
_scanDebugVisible = !_scanDebugVisible;
|
||||
el.style.display = _scanDebugVisible ? 'block' : 'none';
|
||||
}
|
||||
|
||||
// ===== BARCODE SCANNER =====
|
||||
let _useBarcodeDetector = ('BarcodeDetector' in window);
|
||||
|
||||
async function initScanner() {
|
||||
const video = document.getElementById('scanner-video');
|
||||
const viewport = document.getElementById('scanner-viewport');
|
||||
const logEl = document.getElementById('scan-debug-log');
|
||||
if (logEl) logEl.textContent = '';
|
||||
|
||||
const constraints = getCameraConstraints();
|
||||
scanLog(`Camera mode: ${getSettings().camera_facing || 'environment'}`);
|
||||
scanLog(`BarcodeDetector: ${_useBarcodeDetector ? 'YES (native)' : 'NO (Quagga fallback)'}`);
|
||||
scanLog(`Constraints: ${JSON.stringify(constraints.video)}`);
|
||||
|
||||
try {
|
||||
// Stop any existing stream
|
||||
stopScanner();
|
||||
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
video: {
|
||||
facingMode: 'environment',
|
||||
width: { ideal: 1280 },
|
||||
height: { ideal: 720 }
|
||||
}
|
||||
});
|
||||
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||
const track = stream.getVideoTracks()[0];
|
||||
const caps = track.getSettings ? track.getSettings() : {};
|
||||
scanLog(`Stream OK — track: ${track.label}`);
|
||||
scanLog(`Resolution: ${caps.width||'?'}x${caps.height||'?'}, facing: ${caps.facingMode||'N/A'}`);
|
||||
|
||||
scannerStream = stream;
|
||||
video.srcObject = stream;
|
||||
await video.play();
|
||||
scanLog(`Video playing — videoWidth: ${video.videoWidth}, videoHeight: ${video.videoHeight}`);
|
||||
|
||||
// Start Quagga for barcode detection
|
||||
startQuagga(video);
|
||||
if (_useBarcodeDetector) {
|
||||
startNativeScanner(video);
|
||||
} else {
|
||||
startQuaggaScanner(video);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
scanLog(`CAMERA ERROR: ${err.name}: ${err.message}`);
|
||||
console.error('Camera error:', err);
|
||||
document.getElementById('scan-result').style.display = 'block';
|
||||
document.getElementById('scan-result').innerHTML = `
|
||||
@@ -1199,29 +1308,165 @@ async function initScanner() {
|
||||
}
|
||||
}
|
||||
|
||||
function startQuagga(videoEl) {
|
||||
// ===== NATIVE BarcodeDetector SCANNER =====
|
||||
async function startNativeScanner(videoEl) {
|
||||
if (quaggaRunning) return;
|
||||
|
||||
const scannerLine = document.querySelector('.scanner-line');
|
||||
const detector = new BarcodeDetector({
|
||||
formats: ['ean_13', 'ean_8', 'code_128', 'code_39', 'upc_a', 'upc_e']
|
||||
});
|
||||
|
||||
let scanning = true;
|
||||
quaggaRunning = true;
|
||||
let frameCount = 0;
|
||||
let partialCount = 0;
|
||||
let lastDetected = '';
|
||||
let detectCount = 0;
|
||||
let detectionHistory = {};
|
||||
|
||||
scanLog('Native BarcodeDetector started');
|
||||
|
||||
function updateFeedback(state) {
|
||||
if (!scannerLine) return;
|
||||
scannerLine.classList.remove('scanning', 'detecting');
|
||||
if (state) scannerLine.classList.add(state);
|
||||
}
|
||||
|
||||
async function scanFrame() {
|
||||
if (!scanning || !scannerStream) return;
|
||||
frameCount++;
|
||||
|
||||
if (frameCount === 1) updateFeedback('scanning');
|
||||
|
||||
try {
|
||||
const barcodes = await detector.detect(videoEl);
|
||||
|
||||
if (barcodes.length > 0) {
|
||||
const code = barcodes[0].rawValue;
|
||||
const format = barcodes[0].format;
|
||||
partialCount++;
|
||||
scanLog(`Native detect #${partialCount} [f${frameCount}]: ${code} (${format})`);
|
||||
updateFeedback('detecting');
|
||||
|
||||
if (!detectionHistory[code]) detectionHistory[code] = { count: 0 };
|
||||
detectionHistory[code].count++;
|
||||
|
||||
if (code === lastDetected) {
|
||||
detectCount++;
|
||||
} else {
|
||||
lastDetected = code;
|
||||
detectCount = 1;
|
||||
}
|
||||
|
||||
if (detectCount >= 2 || detectionHistory[code].count >= 2) {
|
||||
scanning = false;
|
||||
quaggaRunning = false;
|
||||
updateFeedback(null);
|
||||
scanLog(`CONFIRMED: ${code} after ${frameCount} frames`);
|
||||
onBarcodeDetected(code);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
updateFeedback('scanning');
|
||||
}
|
||||
} catch (e) {
|
||||
scanLog(`Native detect error: ${e.message}`);
|
||||
}
|
||||
|
||||
if (scanning) {
|
||||
if (frameCount % 30 === 0) {
|
||||
scanLog(`Native scanning... f${frameCount}, partials: ${partialCount}`);
|
||||
}
|
||||
requestAnimationFrame(scanFrame);
|
||||
}
|
||||
}
|
||||
|
||||
requestAnimationFrame(scanFrame);
|
||||
}
|
||||
|
||||
// ===== QUAGGA FALLBACK SCANNER =====
|
||||
function startQuaggaScanner(videoEl) {
|
||||
if (quaggaRunning) return;
|
||||
|
||||
const canvas = document.getElementById('scanner-canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const frontCam = isFrontCamera();
|
||||
const scannerLine = document.querySelector('.scanner-line');
|
||||
let frameCount = 0;
|
||||
let partialCount = 0;
|
||||
|
||||
scanLog(`Quagga starting — frontCam: ${frontCam}`);
|
||||
|
||||
let scanning = true;
|
||||
quaggaRunning = true;
|
||||
let lastDetected = '';
|
||||
let detectCount = 0;
|
||||
let detectionHistory = {};
|
||||
|
||||
// Alternate between full frame and center-cropped for better detection
|
||||
let scanPass = 0; // 0=full, 1=center-crop, 2=full-enhanced, 3=center-enhanced
|
||||
|
||||
function updateScannerFeedback(state) {
|
||||
if (!scannerLine) return;
|
||||
scannerLine.classList.remove('scanning', 'detecting');
|
||||
if (state) scannerLine.classList.add(state);
|
||||
}
|
||||
|
||||
function getFrameDataUrl(pass) {
|
||||
const vw = videoEl.videoWidth;
|
||||
const vh = videoEl.videoHeight;
|
||||
|
||||
if (pass % 2 === 0) {
|
||||
// Full frame
|
||||
canvas.width = vw;
|
||||
canvas.height = vh;
|
||||
ctx.drawImage(videoEl, 0, 0);
|
||||
} else {
|
||||
// Center crop: 60% of frame, focused on barcode area
|
||||
const cropW = Math.round(vw * 0.7);
|
||||
const cropH = Math.round(vh * 0.4);
|
||||
const sx = Math.round((vw - cropW) / 2);
|
||||
const sy = Math.round((vh - cropH) / 2);
|
||||
canvas.width = cropW;
|
||||
canvas.height = cropH;
|
||||
ctx.drawImage(videoEl, sx, sy, cropW, cropH, 0, 0, cropW, cropH);
|
||||
}
|
||||
|
||||
// Apply enhancement on passes 2,3 or always for front cam
|
||||
if (frontCam || pass >= 2) {
|
||||
enhanceCanvasForBarcode(ctx, canvas.width, canvas.height);
|
||||
}
|
||||
|
||||
return canvas.toDataURL('image/jpeg', 0.95);
|
||||
}
|
||||
|
||||
function scanFrame() {
|
||||
if (!scanning || !scannerStream) return;
|
||||
frameCount++;
|
||||
scanPass = (scanPass + 1) % 4;
|
||||
|
||||
canvas.width = videoEl.videoWidth;
|
||||
canvas.height = videoEl.videoHeight;
|
||||
ctx.drawImage(videoEl, 0, 0);
|
||||
const dataUrl = getFrameDataUrl(scanPass);
|
||||
|
||||
if (frameCount === 1) {
|
||||
scanLog(`Frame #1 — video: ${videoEl.videoWidth}x${videoEl.videoHeight}`);
|
||||
updateScannerFeedback('scanning');
|
||||
}
|
||||
|
||||
let callbackCalled = false;
|
||||
const safetyTimer = setTimeout(() => {
|
||||
if (!callbackCalled && scanning) {
|
||||
scanLog(`Quagga timeout on f${frameCount}, retrying...`);
|
||||
setTimeout(scanFrame, 100);
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
try {
|
||||
const imgSize = Math.max(canvas.width, canvas.height);
|
||||
Quagga.decodeSingle({
|
||||
src: canvas.toDataURL('image/jpeg', 0.8),
|
||||
src: dataUrl,
|
||||
numOfWorkers: 0,
|
||||
inputStream: { size: 800 },
|
||||
inputStream: { size: Math.min(imgSize, 800) },
|
||||
decoder: {
|
||||
readers: [
|
||||
'ean_reader',
|
||||
@@ -1230,39 +1475,81 @@ function startQuagga(videoEl) {
|
||||
'code_39_reader',
|
||||
'upc_reader',
|
||||
'upc_e_reader'
|
||||
]
|
||||
],
|
||||
multiple: false
|
||||
},
|
||||
locate: true
|
||||
locate: true,
|
||||
locator: { patchSize: 'large', halfSample: false }
|
||||
}, function(result) {
|
||||
callbackCalled = true;
|
||||
clearTimeout(safetyTimer);
|
||||
if (result && result.codeResult) {
|
||||
const code = result.codeResult.code;
|
||||
const format = result.codeResult.format;
|
||||
partialCount++;
|
||||
const passName = ['full','crop','full+enh','crop+enh'][scanPass];
|
||||
scanLog(`Partial #${partialCount} [f${frameCount} ${passName}]: ${code} (${format})`);
|
||||
updateScannerFeedback('detecting');
|
||||
|
||||
if (!detectionHistory[code]) detectionHistory[code] = { count: 0, lastFrame: 0 };
|
||||
detectionHistory[code].count++;
|
||||
detectionHistory[code].lastFrame = frameCount;
|
||||
|
||||
if (code === lastDetected) {
|
||||
detectCount++;
|
||||
} else {
|
||||
lastDetected = code;
|
||||
detectCount = 1;
|
||||
}
|
||||
// Require 2 consecutive reads for reliability
|
||||
if (detectCount >= 2) {
|
||||
|
||||
const dominated = detectionHistory[code];
|
||||
if (detectCount >= 2 || dominated.count >= 2) {
|
||||
scanning = false;
|
||||
quaggaRunning = false;
|
||||
updateScannerFeedback(null);
|
||||
scanLog(`CONFIRMED: ${code} after ${frameCount} frames (consec:${detectCount}, total:${dominated.count})`);
|
||||
onBarcodeDetected(code);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
updateScannerFeedback('scanning');
|
||||
}
|
||||
if (scanning) {
|
||||
setTimeout(scanFrame, 300);
|
||||
if (frameCount % 20 === 0) {
|
||||
scanLog(`Scanning... f${frameCount}, partials: ${partialCount}, pass: ${scanPass}`);
|
||||
}
|
||||
setTimeout(scanFrame, 150);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
callbackCalled = true;
|
||||
clearTimeout(safetyTimer);
|
||||
scanLog(`Quagga error: ${e.message}`);
|
||||
if (scanning) setTimeout(scanFrame, 500);
|
||||
}
|
||||
}
|
||||
|
||||
// Start scanning after a small delay
|
||||
setTimeout(scanFrame, 500);
|
||||
}
|
||||
|
||||
// Enhance low-quality camera frames for better barcode recognition
|
||||
function enhanceCanvasForBarcode(ctx, w, h) {
|
||||
const imageData = ctx.getImageData(0, 0, w, h);
|
||||
const d = imageData.data;
|
||||
// Convert to high-contrast grayscale
|
||||
for (let i = 0; i < d.length; i += 4) {
|
||||
// Luminance
|
||||
let gray = 0.299 * d[i] + 0.587 * d[i+1] + 0.114 * d[i+2];
|
||||
// Increase contrast
|
||||
gray = ((gray - 128) * 1.5) + 128;
|
||||
gray = gray < 0 ? 0 : gray > 255 ? 255 : gray;
|
||||
// Threshold to make bars more distinct
|
||||
gray = gray < 140 ? 0 : 255;
|
||||
d[i] = d[i+1] = d[i+2] = gray;
|
||||
}
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
}
|
||||
|
||||
function stopScanner() {
|
||||
quaggaRunning = false;
|
||||
if (scannerStream) {
|
||||
@@ -2560,9 +2847,7 @@ async function initAICamera() {
|
||||
if (aiStream) {
|
||||
aiStream.getTracks().forEach(t => t.stop());
|
||||
}
|
||||
aiStream = await navigator.mediaDevices.getUserMedia({
|
||||
video: { facingMode: 'environment', width: { ideal: 1280 }, height: { ideal: 720 } }
|
||||
});
|
||||
aiStream = await navigator.mediaDevices.getUserMedia(getCameraConstraints());
|
||||
video.srcObject = aiStream;
|
||||
await video.play();
|
||||
} catch (err) {
|
||||
@@ -3156,12 +3441,9 @@ async function searchItemPrice(idx, force = false) {
|
||||
renderShoppingItems();
|
||||
|
||||
try {
|
||||
// Include specification in the search query for better catalog results
|
||||
let searchQ = item.name;
|
||||
// Send item name as query, spec separately for AI selection
|
||||
const searchQ = item.name;
|
||||
const spec = item.specification || '';
|
||||
// Strip priority emojis from spec before appending
|
||||
const cleanSpec = spec.replace(/[🔴🟡🟢]/g, '').trim();
|
||||
if (cleanSpec) searchQ += ' ' + cleanSpec;
|
||||
|
||||
const s2 = getSettings();
|
||||
const aiPrompt = s2.spesa_ai_prompt || '';
|
||||
@@ -3451,9 +3733,7 @@ async function scanExpiryWithAI() {
|
||||
|
||||
// Start camera
|
||||
try {
|
||||
expiryStream = await navigator.mediaDevices.getUserMedia({
|
||||
video: { facingMode: 'environment', width: { ideal: 1280 }, height: { ideal: 720 } }
|
||||
});
|
||||
expiryStream = await navigator.mediaDevices.getUserMedia(getCameraConstraints());
|
||||
const video = document.getElementById('expiry-video');
|
||||
video.srcObject = expiryStream;
|
||||
await video.play();
|
||||
@@ -3515,9 +3795,7 @@ function retakeExpiry() {
|
||||
document.getElementById('expiry-scan-status').style.display = 'none';
|
||||
|
||||
// Restart camera
|
||||
navigator.mediaDevices.getUserMedia({
|
||||
video: { facingMode: 'environment', width: { ideal: 1280 }, height: { ideal: 720 } }
|
||||
}).then(stream => {
|
||||
navigator.mediaDevices.getUserMedia(getCameraConstraints()).then(stream => {
|
||||
expiryStream = stream;
|
||||
const video = document.getElementById('expiry-video');
|
||||
video.srcObject = stream;
|
||||
@@ -3711,10 +3989,21 @@ function saveRecipeToArchive(recipe) {
|
||||
localStorage.setItem('dispensa_recipe_archive', JSON.stringify(archive));
|
||||
}
|
||||
|
||||
function getTodayRecipeTitles() {
|
||||
const archive = getRecipeArchive();
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
return archive
|
||||
.filter(e => e.date === today && e.recipe && e.recipe.title)
|
||||
.map(e => e.recipe.title);
|
||||
}
|
||||
|
||||
let _recipeArchiveEntries = [];
|
||||
|
||||
function loadRecipeArchive() {
|
||||
const container = document.getElementById('recipe-archive');
|
||||
if (!container) return;
|
||||
const archive = getRecipeArchive();
|
||||
_recipeArchiveEntries = archive;
|
||||
|
||||
if (archive.length === 0) {
|
||||
container.innerHTML = '<div class="empty-state" style="padding:20px"><div class="empty-state-icon">🍳</div><p>Nessuna ricetta salvata.<br>Genera la tua prima ricetta!</p></div>';
|
||||
@@ -3729,6 +4018,7 @@ function loadRecipeArchive() {
|
||||
}
|
||||
|
||||
let html = '';
|
||||
let flatIdx = 0;
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const yesterday = new Date(Date.now() - 86400000).toISOString().slice(0, 10);
|
||||
|
||||
@@ -3744,7 +4034,9 @@ function loadRecipeArchive() {
|
||||
const r = entry.recipe;
|
||||
const mealIcon = MEAL_LABELS[r.meal] || r.meal;
|
||||
const tags = (r.tags || []).slice(0, 3).join(', ');
|
||||
html += `<div class="recipe-archive-card" onclick='viewArchivedRecipe(${JSON.stringify(JSON.stringify(entry))})'>`;
|
||||
// Find this entry's index in the flat archive array
|
||||
const archiveIdx = archive.indexOf(entry);
|
||||
html += `<div class="recipe-archive-card" onclick="viewArchivedRecipe(${archiveIdx})">`;
|
||||
html += `<div class="recipe-archive-card-header">`;
|
||||
html += `<span class="recipe-archive-meal">${mealIcon}</span>`;
|
||||
html += `<span class="recipe-archive-title">${escapeHtml(r.title)}</span>`;
|
||||
@@ -3755,6 +4047,7 @@ function loadRecipeArchive() {
|
||||
html += `<span>👥 ${r.persons}</span>`;
|
||||
if (tags) html += `<span>${tags}</span>`;
|
||||
html += `</div></div>`;
|
||||
flatIdx++;
|
||||
}
|
||||
html += `</div>`;
|
||||
}
|
||||
@@ -3762,8 +4055,9 @@ function loadRecipeArchive() {
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
function viewArchivedRecipe(entryJson) {
|
||||
const entry = JSON.parse(entryJson);
|
||||
function viewArchivedRecipe(idx) {
|
||||
const entry = _recipeArchiveEntries[idx];
|
||||
if (!entry) return;
|
||||
renderRecipe(entry.recipe);
|
||||
document.getElementById('recipe-overlay').style.display = 'flex';
|
||||
document.getElementById('recipe-ask').style.display = 'none';
|
||||
@@ -3985,7 +4279,8 @@ async function generateRecipe() {
|
||||
persons,
|
||||
options,
|
||||
appliances: settings.appliances || [],
|
||||
dietary_restrictions: settings.dietary_restrictions || ''
|
||||
dietary_restrictions: settings.dietary_restrictions || '',
|
||||
today_recipes: getTodayRecipeTitles()
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
[2026-03-12 17:30:05] [desktop] Camera mode: environment
|
||||
[2026-03-12 17:30:05] [desktop] BarcodeDetector: NO (Quagga fallback)
|
||||
[2026-03-12 17:30:05] [desktop] Constraints: {"width":{"ideal":1280},"height":{"ideal":720},"facingMode":"environment"}
|
||||
[2026-03-12 17:30:05] [desktop] Stream OK — track: HP TrueVision HD Camera (30c9:002c)
|
||||
[2026-03-12 17:30:05] [desktop] Resolution: 1280x720, facing: user
|
||||
[2026-03-12 17:30:05] [desktop] Video playing — videoWidth: 1280, videoHeight: 720
|
||||
[2026-03-12 17:30:05] [desktop] Quagga starting — frontCam: false
|
||||
[2026-03-12 17:30:05] [desktop] Frame #1 — video: 1280x720
|
||||
[2026-03-12 17:30:11] [desktop] Scanning... f20, partials: 0, pass: 0
|
||||
[2026-03-12 17:30:15] [desktop] Scanning... f40, partials: 0, pass: 0
|
||||
[2026-03-12 17:30:20] [desktop] Scanning... f60, partials: 0, pass: 0
|
||||
[2026-03-12 17:30:21] [phone] Camera mode: user
|
||||
[2026-03-12 17:30:21] [phone] BarcodeDetector: YES (native)
|
||||
[2026-03-12 17:30:21] [phone] Constraints: {"width":{"ideal":640},"height":{"ideal":480},"facingMode":"user"}
|
||||
[2026-03-12 17:30:21] [phone] Stream OK — track: camera 1, facing front
|
||||
[2026-03-12 17:30:21] [phone] Resolution: 480x640, facing: user
|
||||
[2026-03-12 17:30:21] [phone] Video playing — videoWidth: 480, videoHeight: 640
|
||||
[2026-03-12 17:30:21] [phone] Native BarcodeDetector started
|
||||
[2026-03-12 17:30:23] [phone] Native detect #1 [f3]: 4003608024917 (ean_13)
|
||||
[2026-03-12 17:30:23] [phone] Native detect #2 [f6]: 8003000024917 (ean_13)
|
||||
[2026-03-12 17:30:23] [phone] Native detect #3 [f16]: 8003000024917 (ean_13)
|
||||
[2026-03-12 17:30:23] [phone] CONFIRMED: 8003000024917 after 16 frames
|
||||
[2026-03-12 17:30:25] [desktop] Scanning... f80, partials: 0, pass: 0
|
||||
[2026-03-12 17:30:30] [desktop] Scanning... f100, partials: 0, pass: 0
|
||||
[2026-03-12 17:30:35] [desktop] Scanning... f120, partials: 0, pass: 0
|
||||
[2026-03-12 17:30:40] [desktop] Scanning... f140, partials: 0, pass: 0
|
||||
[2026-03-12 17:30:44] [desktop] Scanning... f160, partials: 0, pass: 0
|
||||
[2026-03-12 17:30:49] [desktop] Scanning... f180, partials: 0, pass: 0
|
||||
[2026-03-12 17:30:54] [desktop] Scanning... f200, partials: 0, pass: 0
|
||||
[2026-03-12 17:30:59] [desktop] Scanning... f220, partials: 0, pass: 0
|
||||
[2026-03-12 17:31:04] [desktop] Scanning... f240, partials: 0, pass: 0
|
||||
[2026-03-12 17:31:07] [desktop] Camera mode: user
|
||||
[2026-03-12 17:31:07] [desktop] BarcodeDetector: YES (native)
|
||||
[2026-03-12 17:31:07] [desktop] Constraints: {"width":{"ideal":640},"height":{"ideal":480},"facingMode":"user"}
|
||||
[2026-03-12 17:31:07] [desktop] Stream OK — track: camera 1, facing front
|
||||
[2026-03-12 17:31:07] [desktop] Resolution: 480x640, facing: user
|
||||
[2026-03-12 17:31:07] [desktop] Video playing — videoWidth: 480, videoHeight: 640
|
||||
[2026-03-12 17:31:07] [desktop] Native BarcodeDetector started
|
||||
[2026-03-12 17:31:09] [desktop] Scanning... f260, partials: 0, pass: 0
|
||||
[2026-03-12 17:31:14] [desktop] Native scanning... f30, partials: 0
|
||||
[2026-03-12 17:31:14] [desktop] Native detect #1 [f31]: 8003000024917 (ean_13)
|
||||
[2026-03-12 17:31:14] [desktop] Native detect #2 [f33]: 8003000024917 (ean_13)
|
||||
[2026-03-12 17:31:14] [desktop] CONFIRMED: 8003000024917 after 33 frames
|
||||
[2026-03-12 17:31:14] [desktop] Scanning... f280, partials: 0, pass: 0
|
||||
[2026-03-12 17:31:19] [desktop] Scanning... f300, partials: 0, pass: 0
|
||||
[2026-03-12 17:31:22] [desktop] Camera mode: user
|
||||
[2026-03-12 17:31:22] [desktop] BarcodeDetector: YES (native)
|
||||
[2026-03-12 17:31:22] [desktop] Constraints: {"width":{"ideal":640},"height":{"ideal":480},"facingMode":"user"}
|
||||
[2026-03-12 17:31:22] [desktop] Stream OK — track: camera 1, facing front
|
||||
[2026-03-12 17:31:22] [desktop] Resolution: 480x640, facing: user
|
||||
[2026-03-12 17:31:22] [desktop] Video playing — videoWidth: 480, videoHeight: 640
|
||||
[2026-03-12 17:31:22] [desktop] Native BarcodeDetector started
|
||||
[2026-03-12 17:31:24] [desktop] Scanning... f320, partials: 0, pass: 0
|
||||
[2026-03-12 17:31:26] [desktop] Native scanning... f30, partials: 0
|
||||
[2026-03-12 17:31:28] [desktop] Scanning... f340, partials: 0, pass: 0
|
||||
[2026-03-12 17:31:30] [desktop] Native detect #1 [f49]: 016418230816 (upc_a)
|
||||
[2026-03-12 17:31:30] [desktop] Native detect #2 [f54]: 003418230816 (upc_a)
|
||||
[2026-03-12 17:31:33] [desktop] Scanning... f360, partials: 0, pass: 0
|
||||
[2026-03-12 17:31:33] [desktop] Native scanning... f60, partials: 2
|
||||
[2026-03-12 17:31:33] [desktop] Native detect #3 [f63]: 9019489230816 (ean_13)
|
||||
[2026-03-12 17:31:38] [desktop] Scanning... f380, partials: 0, pass: 0
|
||||
[2026-03-12 17:31:42] [desktop] Native scanning... f90, partials: 3
|
||||
[2026-03-12 17:31:43] [desktop] Scanning... f400, partials: 0, pass: 0
|
||||
[2026-03-12 17:31:48] [desktop] Scanning... f420, partials: 0, pass: 0
|
||||
[2026-03-12 17:31:49] [desktop] Native scanning... f120, partials: 3
|
||||
[2026-03-12 17:31:53] [desktop] Scanning... f440, partials: 0, pass: 0
|
||||
[2026-03-12 17:31:58] [desktop] Native scanning... f150, partials: 3
|
||||
[2026-03-12 17:31:58] [desktop] Scanning... f460, partials: 0, pass: 0
|
||||
[2026-03-12 17:32:03] [desktop] Native scanning... f180, partials: 3
|
||||
[2026-03-12 17:32:03] [desktop] Native detect #4 [f188]: 8000300337396 (ean_13)
|
||||
[2026-03-12 17:32:03] [desktop] Native detect #5 [f189]: 8000300337396 (ean_13)
|
||||
[2026-03-12 17:32:03] [desktop] CONFIRMED: 8000300337396 after 189 frames
|
||||
[2026-03-12 17:32:03] [desktop] Scanning... f480, partials: 0, pass: 0
|
||||
[2026-03-12 17:32:08] [desktop] Scanning... f500, partials: 0, pass: 0
|
||||
[2026-03-12 17:32:11] [desktop] Camera mode: user
|
||||
[2026-03-12 17:32:11] [desktop] BarcodeDetector: YES (native)
|
||||
[2026-03-12 17:32:11] [desktop] Constraints: {"width":{"ideal":640},"height":{"ideal":480},"facingMode":"user"}
|
||||
[2026-03-12 17:32:11] [desktop] Stream OK — track: camera 1, facing front
|
||||
[2026-03-12 17:32:11] [desktop] Resolution: 480x640, facing: user
|
||||
[2026-03-12 17:32:11] [desktop] Video playing — videoWidth: 480, videoHeight: 640
|
||||
[2026-03-12 17:32:11] [desktop] Native BarcodeDetector started
|
||||
[2026-03-12 17:32:13] [desktop] Scanning... f520, partials: 0, pass: 0
|
||||
Binary file not shown.
@@ -0,0 +1,118 @@
|
||||
{
|
||||
"token": "eyJraWQiOiJzdGQiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJkYWRhbG9vcDgyQGdtYWlsLmNvbSIsImV4cCI6MTc3NDU5NjkzMywianRpIjoiMjM0NzIxNDUzVkJLR0hBSFlDUUxITjdIWFVNREJWIn0.97ZRFe3i0-0sCoVe_lG5Li2YVXkEyyPTfTr6tck984HKQryUf_12BidPe23aL_2eUeUiuMbd8HJfkyumlfOvmA",
|
||||
"email": "dadaloop82@gmail.com",
|
||||
"logged_at": "2026-03-12T06:35:33+00:00",
|
||||
"user": {
|
||||
"userId": 234721,
|
||||
"firstName": "Daniel",
|
||||
"lastName": "Stimpfl",
|
||||
"fidelityCard": "0401011000247",
|
||||
"email": "dadaloop82@gmail.com",
|
||||
"login": "dadaloop82@gmail.com",
|
||||
"phone": "+393490726254",
|
||||
"userType": {
|
||||
"userTypeId": "1"
|
||||
},
|
||||
"companyId": 0,
|
||||
"hasFavorites": true,
|
||||
"defaultStoreAddress": {
|
||||
"addressId": null,
|
||||
"addressName": null
|
||||
},
|
||||
"codInt": "2167695",
|
||||
"profile": {
|
||||
"level": 2,
|
||||
"confirmed": true
|
||||
},
|
||||
"hrAgent": {
|
||||
"hrId": null,
|
||||
"name": "",
|
||||
"internalCode": ""
|
||||
},
|
||||
"giftCertificatesReceived": [],
|
||||
"legals": [
|
||||
{
|
||||
"legalId": 1
|
||||
}
|
||||
],
|
||||
"userPointsCounters": [
|
||||
{
|
||||
"codInt": "0",
|
||||
"name": "Fidelity",
|
||||
"value": 0,
|
||||
"pointsUsed": 0
|
||||
}
|
||||
],
|
||||
"userPoints": [
|
||||
{
|
||||
"codInt": "0",
|
||||
"name": "Fidelity",
|
||||
"value": 0,
|
||||
"pointsUsed": 0
|
||||
}
|
||||
],
|
||||
"person": {
|
||||
"personId": "8116672",
|
||||
"firstName": "Daniel",
|
||||
"lastName": "Stimpfl",
|
||||
"gender": "",
|
||||
"birthDate": "1982-06-19",
|
||||
"birthPlace": "",
|
||||
"company": "",
|
||||
"fiscalCode": "",
|
||||
"vatCode": "",
|
||||
"vatFiscalCode": "",
|
||||
"codInt": "",
|
||||
"active": "1",
|
||||
"industryId": null,
|
||||
"industryName": null,
|
||||
"emailCertified": "",
|
||||
"vatSdiCode": "",
|
||||
"personTypeId": null,
|
||||
"personInfos": []
|
||||
},
|
||||
"billingAddress": {
|
||||
"addressId": 605113,
|
||||
"addressTypeId": 1,
|
||||
"addressType": "billing",
|
||||
"zoneId": -1,
|
||||
"active": 1,
|
||||
"deliveryAddressId": 605113,
|
||||
"shippingAddressId": 605113,
|
||||
"addressName": "residenza",
|
||||
"address1": "VIA PETER ROSEGGER",
|
||||
"address2": "",
|
||||
"addressNumber": "20\/C",
|
||||
"city": "LAIVES",
|
||||
"postalcode": "",
|
||||
"floor": "",
|
||||
"doorbellName": "",
|
||||
"province": "BZ",
|
||||
"apartmentNumber": "",
|
||||
"maxDistance": 0.4,
|
||||
"country": {
|
||||
"countryId": 1,
|
||||
"name": "Italia",
|
||||
"iso": "IT",
|
||||
"codInt": ""
|
||||
},
|
||||
"doorbellNumber": "",
|
||||
"referencePhoneNumber": "",
|
||||
"referenceMobileNumber": "",
|
||||
"referenceEMail": "",
|
||||
"addressNote": "",
|
||||
"ztl": null,
|
||||
"elevator": null,
|
||||
"countryId": "1"
|
||||
},
|
||||
"contact": {
|
||||
"contactId": "56730212",
|
||||
"email1": "dadaloop82@gmail.com",
|
||||
"email2": "",
|
||||
"homePhone": "",
|
||||
"workPhone": ""
|
||||
},
|
||||
"crmUserSegments": []
|
||||
},
|
||||
"cart_id": 1476245
|
||||
}
|
||||
+21
-2
@@ -9,7 +9,7 @@
|
||||
<title>Dispensa Manager</title>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🏠</text></svg>">
|
||||
<link rel="stylesheet" href="assets/css/style.css?v=20260312h">
|
||||
<link rel="stylesheet" href="assets/css/style.css?v=20260312t">
|
||||
<!-- QuaggaJS for barcode scanning -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2@1.8.4/dist/quagga.min.js"></script>
|
||||
</head>
|
||||
@@ -134,6 +134,8 @@
|
||||
</button>
|
||||
</div>
|
||||
<p class="scan-hint">Scansiona il barcode, scrivi il nome del prodotto, oppure usa l'AI per identificarlo</p>
|
||||
<div id="scan-debug-log" style="display:none;margin-top:12px;padding:10px;background:#1a1a2e;color:#0f0;font-family:monospace;font-size:0.7rem;max-height:200px;overflow-y:auto;border-radius:8px;white-space:pre-wrap"></div>
|
||||
<button class="btn btn-small btn-secondary" style="margin-top:8px;opacity:0.5" onclick="toggleScanDebug()">🐛 Debug Log</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -558,6 +560,7 @@
|
||||
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-recipe')" data-tab="tab-recipe" title="Ricette">🍳</button>
|
||||
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-appliances')" data-tab="tab-appliances" title="Elettrodomestici">🔌</button>
|
||||
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-spesa')" data-tab="tab-spesa" title="Spesa Online">🛍️</button>
|
||||
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-camera')" data-tab="tab-camera" title="Fotocamera">📷</button>
|
||||
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-security')" data-tab="tab-security" title="Sicurezza">🔒</button>
|
||||
</div>
|
||||
<div class="settings-panels">
|
||||
@@ -686,6 +689,22 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Camera Tab -->
|
||||
<div class="settings-panel" id="tab-camera">
|
||||
<div class="settings-card">
|
||||
<h4>📷 Fotocamera</h4>
|
||||
<p class="settings-hint">Scegli quale fotocamera utilizzare per la scansione barcode e l'identificazione AI.</p>
|
||||
<div class="form-group">
|
||||
<label>📸 Fotocamera predefinita</label>
|
||||
<select id="setting-camera-facing" class="form-input">
|
||||
<option value="environment">📱 Posteriore (default)</option>
|
||||
<option value="user">🤳 Anteriore</option>
|
||||
</select>
|
||||
<p class="settings-hint mt-2">Se hai più fotocamere, puoi selezionarne una specifica dall'elenco sopra dopo aver concesso i permessi.</p>
|
||||
<button class="btn btn-small btn-secondary mt-2" onclick="loadCameraDevices()">🔄 Rileva fotocamere</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Security Tab -->
|
||||
<div class="settings-panel" id="tab-security">
|
||||
<div class="settings-card">
|
||||
@@ -839,6 +858,6 @@
|
||||
<div class="modal-content" id="modal-content" onclick="event.stopPropagation()"></div>
|
||||
</div>
|
||||
|
||||
<script src="assets/js/app.js?v=20260312n"></script>
|
||||
<script src="assets/js/app.js?v=20260312w"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user