{"id":"cs-1782348976576-vv6j1a9z","status":"done","createdAt":1782348976576,"finishedAt":1782349331528,"meta":{"projectName":"ALimpiezas","projectPath":"C:\\xampp\\htdocs\\ALimpiezas","messagePreview":"Se corta por algun motivo otra vez: read: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Init.phpcompleted\nshellcom","phaseId":null},"events":[{"event":"status","data":{"text":"Modo IDE / sesión — completo (80 herramientas, uso selectivo)"},"at":1782348976580},{"event":"status","data":{"text":"Agente"},"at":1782348976583},{"event":"status","data":{"text":"Modo revisión: snapshot local por archivo"},"at":1782348976584},{"event":"status","data":{"text":"Contexto del IDE cargado"},"at":1782348976584},{"event":"agent","data":{"agentId":"agent-433f2df9-d91f-4f51-9d33-bbd01896105f","projectPath":"C:\\xampp\\htdocs\\ALimpiezas"},"at":1782348976584},{"event":"changes","data":{"sessionId":"cs-1782348976579-0tozus","pending":0},"at":1782348976584},{"event":"status","data":{"text":"Procesando…"},"at":1782348976647},{"event":"tool_activity","data":{"activity":{"id":"tool_e35a512b-21e6-4720-a345-fa1bc654704","name":"read","status":"running","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js","detail":"","at":"2026-06-25T00:57:27.372Z"}},"at":1782349047372},{"event":"tool_activity","data":{"activity":{"id":"tool_654277dc-5322-40d3-8dc3-5ca8c21ca2f","name":"read","status":"running","summary":"read: …ampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\View\\MobileExpenses.html.twig","detail":"","at":"2026-06-25T00:57:27.373Z"}},"at":1782349047373},{"event":"tool_activity","data":{"activity":{"id":"tool_2063c9d1-e2bb-41a5-8dd2-d2c54c2f7d8","name":"read","status":"running","summary":"read: …ampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Controller\\MobileExpenses.php","detail":"","at":"2026-06-25T00:57:27.374Z"}},"at":1782349047374},{"event":"tool_activity","data":{"activity":{"id":"tool_a935cdcf-c8f9-4216-8c37-24f112fe85f","name":"read","status":"running","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Lib\\AiExpenseProcessor.php","detail":"","at":"2026-06-25T00:57:27.374Z"}},"at":1782349047374},{"event":"tool_activity","data":{"activity":{"id":"tool_e35a512b-21e6-4720-a345-fa1bc654704","name":"read","status":"completed","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js","detail":"","at":"2026-06-25T00:57:27.527Z"}},"at":1782349047527},{"event":"tool_activity","data":{"activity":{"id":"tool_2063c9d1-e2bb-41a5-8dd2-d2c54c2f7d8","name":"read","status":"completed","summary":"read: …ampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Controller\\MobileExpenses.php","detail":"","at":"2026-06-25T00:57:27.531Z"}},"at":1782349047531},{"event":"tool_activity","data":{"activity":{"id":"tool_654277dc-5322-40d3-8dc3-5ca8c21ca2f","name":"read","status":"completed","summary":"read: …ampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\View\\MobileExpenses.html.twig","detail":"","at":"2026-06-25T00:57:27.538Z"}},"at":1782349047538},{"event":"text","data":{"chunk":"Continúo el hilo: reviso los archivos del OCR móvil ya identificados y el estado de la conexión con el servicio IA.\n"},"at":1782349331528},{"event":"tool_activity","data":{"activity":{"id":"tool_a935cdcf-c8f9-4216-8c37-24f112fe85f","name":"read","status":"completed","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Lib\\AiExpenseProcessor.php","detail":"","at":"2026-06-25T00:57:27.653Z"}},"at":1782349047653},{"event":"tool_activity","data":{"activity":{"id":"tool_76c031b6-68e5-4423-8b50-205357b999d","name":"grep","status":"running","summary":"grep: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile","detail":"","at":"2026-06-25T00:58:29.021Z"}},"at":1782349109021},{"event":"tool_activity","data":{"activity":{"id":"tool_974af1fc-9327-4c43-8e96-804ac95f080","name":"read","status":"running","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Lib\\MobileConfig.php","detail":"","at":"2026-06-25T00:58:29.024Z"}},"at":1782349109024},{"event":"tool_activity","data":{"activity":{"id":"tool_c69ed173-0757-4c90-8f03-3620ebf82ce","name":"read","status":"running","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\ai-service\\main.py","detail":"","at":"2026-06-25T00:58:29.024Z"}},"at":1782349109024},{"event":"tool_activity","data":{"activity":{"id":"tool_ce2bade5-ded8-4766-8110-d46ec4523a1","name":"read","status":"running","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\CSS\\mobile.css","detail":"","at":"2026-06-25T00:58:29.025Z"}},"at":1782349109025},{"event":"tool_activity","data":{"activity":{"id":"tool_974af1fc-9327-4c43-8e96-804ac95f080","name":"read","status":"completed","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Lib\\MobileConfig.php","detail":"","at":"2026-06-25T00:58:29.129Z"}},"at":1782349109129},{"event":"tool_activity","data":{"activity":{"id":"tool_ce2bade5-ded8-4766-8110-d46ec4523a1","name":"read","status":"completed","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\CSS\\mobile.css","detail":"","at":"2026-06-25T00:58:29.133Z"}},"at":1782349109133},{"event":"tool_activity","data":{"activity":{"id":"tool_c69ed173-0757-4c90-8f03-3620ebf82ce","name":"read","status":"completed","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\ai-service\\main.py","detail":"","at":"2026-06-25T00:58:29.136Z"}},"at":1782349109136},{"event":"tool_activity","data":{"activity":{"id":"tool_76c031b6-68e5-4423-8b50-205357b999d","name":"grep","status":"completed","summary":"grep: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile","detail":"","at":"2026-06-25T00:58:29.198Z"}},"at":1782349109198},{"event":"text","data":{"chunk":"Encontré un bug crítico: `fileInput` no está definido en la validación del upload. Reviso el backend y la conexión OCR.\n"},"at":1782349331528},{"event":"tool_activity","data":{"activity":{"id":"tool_2d6633ff-1745-4cc2-95fd-e8a91c1a138","name":"read","status":"running","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\CSS\\mobile.css","detail":"","at":"2026-06-25T00:59:11.245Z"}},"at":1782349151245},{"event":"tool_activity","data":{"activity":{"id":"tool_309aa639-4608-4ae0-97fe-bd5c3f414cb","name":"grep","status":"running","summary":"grep: C:\\xampp\\htdocs\\ALimpiezas","detail":"","at":"2026-06-25T00:59:11.260Z"}},"at":1782349151260},{"event":"tool_activity","data":{"activity":{"id":"tool_149b6dd5-20b3-4014-ad13-d615d529963","name":"read","status":"running","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Init.php","detail":"","at":"2026-06-25T00:59:11.261Z"}},"at":1782349151261},{"event":"tool_activity","data":{"activity":{"id":"tool_53ad9f67-1dd2-4672-bb74-2233890c8c2","name":"read","status":"running","summary":"read: …htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\View\\Master\\MobileLayout.html.twig","detail":"","at":"2026-06-25T00:59:11.261Z"}},"at":1782349151261},{"event":"tool_activity","data":{"activity":{"id":"tool_2d6633ff-1745-4cc2-95fd-e8a91c1a138","name":"read","status":"completed","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\CSS\\mobile.css","detail":"","at":"2026-06-25T00:59:11.337Z"}},"at":1782349151337},{"event":"tool_activity","data":{"activity":{"id":"tool_149b6dd5-20b3-4014-ad13-d615d529963","name":"read","status":"completed","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Init.php","detail":"","at":"2026-06-25T00:59:11.368Z"}},"at":1782349151368},{"event":"tool_activity","data":{"activity":{"id":"tool_53ad9f67-1dd2-4672-bb74-2233890c8c2","name":"read","status":"completed","summary":"read: …htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\View\\Master\\MobileLayout.html.twig","detail":"","at":"2026-06-25T00:59:11.370Z"}},"at":1782349151370},{"event":"tool_activity","data":{"activity":{"id":"tool_309aa639-4608-4ae0-97fe-bd5c3f414cb","name":"grep","status":"completed","summary":"grep: C:\\xampp\\htdocs\\ALimpiezas","detail":"","at":"2026-06-25T00:59:11.494Z"}},"at":1782349151494},{"event":"tool_activity","data":{"activity":{"id":"tool_3ea705c2-40be-4fd4-9929-058ee41f98a","name":"read","status":"running","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\Core\\Internal\\RequestFiles.php","detail":"","at":"2026-06-25T00:59:59.240Z"}},"at":1782349199240},{"event":"tool_activity","data":{"activity":{"id":"tool_f2520e30-31d1-4d7f-9894-63ad64b59d0","name":"read","status":"running","summary":"read: …ampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Controller\\MobileSettings.php","detail":"","at":"2026-06-25T00:59:59.241Z"}},"at":1782349199241},{"event":"tool_activity","data":{"activity":{"id":"tool_3ad3170c-3624-4cfc-a1b4-a98e41f5a01","name":"grep","status":"running","summary":"grep: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile","detail":"","at":"2026-06-25T00:59:59.242Z"}},"at":1782349199242},{"event":"tool_activity","data":{"activity":{"id":"tool_3ea705c2-40be-4fd4-9929-058ee41f98a","name":"read","status":"completed","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\Core\\Internal\\RequestFiles.php","detail":"","at":"2026-06-25T00:59:59.349Z"}},"at":1782349199349},{"event":"tool_activity","data":{"activity":{"id":"tool_f2520e30-31d1-4d7f-9894-63ad64b59d0","name":"read","status":"completed","summary":"read: …ampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Controller\\MobileSettings.php","detail":"","at":"2026-06-25T00:59:59.354Z"}},"at":1782349199354},{"event":"tool_activity","data":{"activity":{"id":"tool_3ad3170c-3624-4cfc-a1b4-a98e41f5a01","name":"grep","status":"completed","summary":"grep: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile","detail":"","at":"2026-06-25T00:59:59.440Z"}},"at":1782349199440},{"event":"tool_activity","data":{"activity":{"id":"tool_5e1b7f65-f7b4-4119-b38d-e7e38148f2c","name":"read","status":"running","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Lib\\VoiceCommandBridge.php","detail":"","at":"2026-06-25T01:00:32.292Z"}},"at":1782349232292},{"event":"tool_activity","data":{"activity":{"id":"tool_15c76447-ae87-454b-b86c-34af933319d","name":"read","status":"running","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\docs\\MANUAL-AGENTE-IA.md","detail":"","at":"2026-06-25T01:00:32.292Z"}},"at":1782349232292},{"event":"tool_activity","data":{"activity":{"id":"tool_b9184639-accf-4f99-b1b9-f94897df041","name":"shell","status":"running","summary":"shell","detail":"curl -s -m 3 \"http://127.0.0.1:8090/health\" 2>&1; echo \"---\"; curl -s -m 3 \"http://192.168.1.16:8090/health\" 2>&1","at":"2026-06-25T01:00:32.294Z"}},"at":1782349232294},{"event":"tool_activity","data":{"activity":{"id":"tool_5e1b7f65-f7b4-4119-b38d-e7e38148f2c","name":"read","status":"completed","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Lib\\VoiceCommandBridge.php","detail":"","at":"2026-06-25T01:00:32.405Z"}},"at":1782349232405},{"event":"tool_activity","data":{"activity":{"id":"tool_15c76447-ae87-454b-b86c-34af933319d","name":"read","status":"completed","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\docs\\MANUAL-AGENTE-IA.md","detail":"","at":"2026-06-25T01:00:32.408Z"}},"at":1782349232408},{"event":"tool_activity","data":{"activity":{"id":"tool_b9184639-accf-4f99-b1b9-f94897df041","name":"shell","status":"completed","summary":"shell","detail":"curl -s -m 3 \"http://127.0.0.1:8090/health\" 2>&1; echo \"---\"; curl -s -m 3 \"http://192.168.1.16:8090/health\" 2>&1","at":"2026-06-25T01:00:33.228Z"}},"at":1782349233228},{"event":"tool_activity","data":{"activity":{"id":"tool_11571b48-0d7d-4146-9157-f346080c851","name":"read","status":"running","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\docs\\MANUAL-AGENTE-IA.md","detail":"","at":"2026-06-25T01:01:11.549Z"}},"at":1782349271549},{"event":"tool_activity","data":{"activity":{"id":"tool_3ed577a6-7a46-40ba-955d-1000f10f6cf","name":"grep","status":"running","summary":"grep: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile","detail":"","at":"2026-06-25T01:01:11.550Z"}},"at":1782349271550},{"event":"tool_activity","data":{"activity":{"id":"tool_11571b48-0d7d-4146-9157-f346080c851","name":"read","status":"completed","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\docs\\MANUAL-AGENTE-IA.md","detail":"","at":"2026-06-25T01:01:11.656Z"}},"at":1782349271656},{"event":"tool_activity","data":{"activity":{"id":"tool_3ed577a6-7a46-40ba-955d-1000f10f6cf","name":"grep","status":"completed","summary":"grep: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile","detail":"","at":"2026-06-25T01:01:11.735Z"}},"at":1782349271735},{"event":"text","data":{"chunk":"Aplicando correcciones: bug de `fileInput`, método `resolveUploadedFile` faltante, selectores móviles con `<label>` y panel de debug.\n"},"at":1782349331528},{"event":"tool_activity","data":{"activity":{"id":"tool_eb97dec0-6bcf-4d18-8c05-29fc4f233dc","name":"edit","status":"completed","summary":"edit: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js","detail":"","at":"2026-06-25T01:01:30.852Z"}},"at":1782349290852},{"event":"file_change","data":{"sessionId":"cs-1782348976579-0tozus","change":{"id":"ch-1","path":"Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js","type":"edit","diffString":"--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js\n+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js\n@@ -37,6 +37,46 @@\n         }\n     }\n \n+    function debugLog(message, detail) {\n+        var panel = document.getElementById('debug-panel');\n+        if (!panel) {\n+            return;\n+        }\n+        var line = document.createElement('div');\n+        line.className = 'debug-line';\n+        var text = new Date().toLocaleTimeString('es-ES') + ' — ' + message;\n+        if (detail) {\n+            text += ' · ' + (typeof detail === 'string' ? detail : JSON.stringify(detail));\n+        }\n+        line.textContent = text;\n+        panel.appendChild(line);\n+        panel.scrollTop = panel.scrollHeight;\n+    }\n+\n+    function runDiagnose() {\n+        var data = new FormData();\n+        data.append('action', 'diagnose');\n+        debugLog('Comprobando servicio OCR/IA…');\n+        postForm(data).then(function (response) {\n+            if (!response.ok) {\n+                debugLog('Diagnóstico falló', response.message || 'error');\n+                return;\n+            }\n+            debugLog('Modo', response.mode);\n+            debugLog('URL servicio', response.service_url);\n+            if (response.service && response.service.ok) {\n+                debugLog('Servicio OCR', 'OK');\n+            } else if (response.service) {\n+                debugLog('Servicio OCR', response.service.message || 'no responde');\n+            }\n+            if (response.ollama) {\n+                debugLog('Ollama LAN', response.ollama.ok ? 'OK' : (response.ollama.message || 'no responde'));\n+            }\n+        }).catch(function () {\n+            debugLog('Diagnóstico', 'error de red con FacturaScripts');\n+        });\n+    }\n+\n     function formatErrorMessage(data) {\n         var html = '<div class=\"alert alert-danger mb-0\"><strong>' + (data.message || 'Error') + '</strong>';\n         if (data.reasons && data.reasons.length) {","diffHtml":"<div class=\"diff-colored\"><div class=\"diff-line diff-meta\">--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js</div><div class=\"diff-line diff-meta\">+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js</div><div class=\"diff-line diff-hunk\">@@ -37,6 +37,46 @@</div><div class=\"diff-line diff-ctx\">         }</div><div class=\"diff-line diff-ctx\">     }</div><div class=\"diff-line diff-ctx\"> </div><div class=\"diff-line diff-add\">+    function debugLog(message, detail) {</div><div class=\"diff-line diff-add\">+        var panel = document.getElementById('debug-panel');</div><div class=\"diff-line diff-add\">+        if (!panel) {</div><div class=\"diff-line diff-add\">+            return;</div><div class=\"diff-line diff-add\">+        }</div><div class=\"diff-line diff-add\">+        var line = document.createElement('div');</div><div class=\"diff-line diff-add\">+        line.className = 'debug-line';</div><div class=\"diff-line diff-add\">+        var text = new Date().toLocaleTimeString('es-ES') + ' — ' + message;</div><div class=\"diff-line diff-add\">+        if (detail) {</div><div class=\"diff-line diff-add\">+            text += ' · ' + (typeof detail === 'string' ? detail : JSON.stringify(detail));</div><div class=\"diff-line diff-add\">+        }</div><div class=\"diff-line diff-add\">+        line.textContent = text;</div><div class=\"diff-line diff-add\">+        panel.appendChild(line);</div><div class=\"diff-line diff-add\">+        panel.scrollTop = panel.scrollHeight;</div><div class=\"diff-line diff-add\">+    }</div><div class=\"diff-line diff-add\">+</div><div class=\"diff-line diff-add\">+    function runDiagnose() {</div><div class=\"diff-line diff-add\">+        var data = new FormData();</div><div class=\"diff-line diff-add\">+        data.append('action', 'diagnose');</div><div class=\"diff-line diff-add\">+        debugLog('Comprobando servicio OCR/IA…');</div><div class=\"diff-line diff-add\">+        postForm(data).then(function (response) {</div><div class=\"diff-line diff-add\">+            if (!response.ok) {</div><div class=\"diff-line diff-add\">+                debugLog('Diagnóstico falló', response.message || 'error');</div><div class=\"diff-line diff-add\">+                return;</div><div class=\"diff-line diff-add\">+            }</div><div class=\"diff-line diff-add\">+            debugLog('Modo', response.mode);</div><div class=\"diff-line diff-add\">+            debugLog('URL servicio', response.service_url);</div><div class=\"diff-line diff-add\">+            if (response.service &amp;&amp; response.service.ok) {</div><div class=\"diff-line diff-add\">+                debugLog('Servicio OCR', 'OK');</div><div class=\"diff-line diff-add\">+            } else if (response.service) {</div><div class=\"diff-line diff-add\">+                debugLog('Servicio OCR', response.service.message || 'no responde');</div><div class=\"diff-line diff-add\">+            }</div><div class=\"diff-line diff-add\">+            if (response.ollama) {</div><div class=\"diff-line diff-add\">+                debugLog('Ollama LAN', response.ollama.ok ? 'OK' : (response.ollama.message || 'no responde'));</div><div class=\"diff-line diff-add\">+            }</div><div class=\"diff-line diff-add\">+        }).catch(function () {</div><div class=\"diff-line diff-add\">+            debugLog('Diagnóstico', 'error de red con FacturaScripts');</div><div class=\"diff-line diff-add\">+        });</div><div class=\"diff-line diff-add\">+    }</div><div class=\"diff-line diff-add\">+</div><div class=\"diff-line diff-ctx\">     function formatErrorMessage(data) {</div><div class=\"diff-line diff-ctx\">         var html = '&lt;div class=\"alert alert-danger mb-0\"&gt;&lt;strong&gt;' + (data.message || 'Error') + '&lt;/strong&gt;';</div><div class=\"diff-line diff-ctx\">         if (data.reasons &amp;&amp; data.reasons.length) {</div></div>","linesAdded":40,"linesRemoved":0,"status":"pending"},"pending":1},"at":1782349290852},{"event":"tool_activity","data":{"activity":{"id":"tool_51520127-1294-4037-9b59-0ae82c4b47d","name":"edit","status":"completed","summary":"edit: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js","detail":"","at":"2026-06-25T01:01:31.923Z"}},"at":1782349291923},{"event":"file_change","data":{"sessionId":"cs-1782348976579-0tozus","change":{"id":"ch-2","path":"Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js","type":"edit","diffString":"--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js\n+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js\n@@ -172,6 +172,7 @@\n \n         if (galleryInput) {\n             galleryInput.addEventListener('change', function () {\n+                debugLog('Galería/archivo seleccionado', galleryInput.files.length ? galleryInput.files[0].name : 'vacío');\n                 if (!assignSelectedFile(galleryInput)) {\n                     showResult(formatErrorMessage({\n                         message: 'No se pudo leer el archivo seleccionado.',\n@@ -183,6 +184,7 @@\n \n         if (cameraInput) {\n             cameraInput.addEventListener('change', function () {\n+                debugLog('Foto cámara', cameraInput.files.length ? cameraInput.files[0].name : 'vacío');\n                 if (!assignSelectedFile(cameraInput)) {\n                     showResult(formatErrorMessage({\n                         message: 'No se pudo leer la foto.',","diffHtml":"<div class=\"diff-colored\"><div class=\"diff-line diff-meta\">--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js</div><div class=\"diff-line diff-meta\">+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js</div><div class=\"diff-line diff-hunk\">@@ -172,6 +172,7 @@</div><div class=\"diff-line diff-ctx\"> </div><div class=\"diff-line diff-ctx\">         if (galleryInput) {</div><div class=\"diff-line diff-ctx\">             galleryInput.addEventListener('change', function () {</div><div class=\"diff-line diff-add\">+                debugLog('Galería/archivo seleccionado', galleryInput.files.length ? galleryInput.files[0].name : 'vacío');</div><div class=\"diff-line diff-ctx\">                 if (!assignSelectedFile(galleryInput)) {</div><div class=\"diff-line diff-ctx\">                     showResult(formatErrorMessage({</div><div class=\"diff-line diff-ctx\">                         message: 'No se pudo leer el archivo seleccionado.',</div><div class=\"diff-line diff-hunk\">@@ -183,6 +184,7 @@</div><div class=\"diff-line diff-ctx\"> </div><div class=\"diff-line diff-ctx\">         if (cameraInput) {</div><div class=\"diff-line diff-ctx\">             cameraInput.addEventListener('change', function () {</div><div class=\"diff-line diff-add\">+                debugLog('Foto cámara', cameraInput.files.length ? cameraInput.files[0].name : 'vacío');</div><div class=\"diff-line diff-ctx\">                 if (!assignSelectedFile(cameraInput)) {</div><div class=\"diff-line diff-ctx\">                     showResult(formatErrorMessage({</div><div class=\"diff-line diff-ctx\">                         message: 'No se pudo leer la foto.',</div></div>","linesAdded":2,"linesRemoved":0,"status":"pending"},"pending":2},"at":1782349291923},{"event":"tool_activity","data":{"activity":{"id":"tool_8c678267-dd6b-458b-ba45-6aedc20c316","name":"edit","status":"completed","summary":"edit: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js","detail":"","at":"2026-06-25T01:01:33.274Z"}},"at":1782349293274},{"event":"file_change","data":{"sessionId":"cs-1782348976579-0tozus","change":{"id":"ch-3","path":"Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js","type":"edit","diffString":"--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js\n+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js\n@@ -385,7 +385,7 @@\n                 return;\n             }\n \n-            if (expenseMode !== 'manual' && (!fileInput || !fileInput.files.length)) {\n+            if (expenseMode !== 'manual' && !uploadFile) {\n                 showResult(formatErrorMessage({\n                     message: 'Debes elegir un archivo, una foto de galería o hacer una foto.',\n                     reasons: ['En modo OCR/IA el documento es obligatorio.', 'Usa «Elegir archivo o galería» o «Hacer foto».'],\n@@ -396,13 +396,19 @@\n \n             btn.disabled = true;\n             btn.innerHTML = '<span class=\"spinner-border spinner-border-sm me-2\"></span>Procesando...';\n+            debugLog('Enviando documento', uploadFile ? uploadFile.name + ' (' + uploadFile.size + ' B)' : 'sin archivo');\n \n             postForm(formData)\n                 .then(function (data) {\n+                    if (data.debug) {\n+                        debugLog('Respuesta servidor', data.debug);\n+                    }\n                     if (!data.ok) {\n+                        debugLog('Error upload', data.message);\n                         showResult(formatErrorMessage(data));\n                         return;\n                     }\n+                    debugLog('Upload OK', data.message || 'analizado');\n \n                     if (data.warning) {\n                         showResult('<div class=\"alert alert-warning\">' + data.warning + '</div>');","diffHtml":"<div class=\"diff-colored\"><div class=\"diff-line diff-meta\">--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js</div><div class=\"diff-line diff-meta\">+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js</div><div class=\"diff-line diff-hunk\">@@ -385,7 +385,7 @@</div><div class=\"diff-line diff-ctx\">                 return;</div><div class=\"diff-line diff-ctx\">             }</div><div class=\"diff-line diff-ctx\"> </div><div class=\"diff-line diff-del\">-            if (expenseMode !== 'manual' &amp;&amp; (!fileInput || !fileInput.files.length)) {</div><div class=\"diff-line diff-add\">+            if (expenseMode !== 'manual' &amp;&amp; !uploadFile) {</div><div class=\"diff-line diff-ctx\">                 showResult(formatErrorMessage({</div><div class=\"diff-line diff-ctx\">                     message: 'Debes elegir un archivo, una foto de galería o hacer una foto.',</div><div class=\"diff-line diff-ctx\">                     reasons: ['En modo OCR/IA el documento es obligatorio.', 'Usa «Elegir archivo o galería» o «Hacer foto».'],</div><div class=\"diff-line diff-hunk\">@@ -396,13 +396,19 @@</div><div class=\"diff-line diff-ctx\"> </div><div class=\"diff-line diff-ctx\">             btn.disabled = true;</div><div class=\"diff-line diff-ctx\">             btn.innerHTML = '&lt;span class=\"spinner-border spinner-border-sm me-2\"&gt;&lt;/span&gt;Procesando...';</div><div class=\"diff-line diff-add\">+            debugLog('Enviando documento', uploadFile ? uploadFile.name + ' (' + uploadFile.size + ' B)' : 'sin archivo');</div><div class=\"diff-line diff-ctx\"> </div><div class=\"diff-line diff-ctx\">             postForm(formData)</div><div class=\"diff-line diff-ctx\">                 .then(function (data) {</div><div class=\"diff-line diff-add\">+                    if (data.debug) {</div><div class=\"diff-line diff-add\">+                        debugLog('Respuesta servidor', data.debug);</div><div class=\"diff-line diff-add\">+                    }</div><div class=\"diff-line diff-ctx\">                     if (!data.ok) {</div><div class=\"diff-line diff-add\">+                        debugLog('Error upload', data.message);</div><div class=\"diff-line diff-ctx\">                         showResult(formatErrorMessage(data));</div><div class=\"diff-line diff-ctx\">                         return;</div><div class=\"diff-line diff-ctx\">                     }</div><div class=\"diff-line diff-add\">+                    debugLog('Upload OK', data.message || 'analizado');</div><div class=\"diff-line diff-ctx\"> </div><div class=\"diff-line diff-ctx\">                     if (data.warning) {</div><div class=\"diff-line diff-ctx\">                         showResult('&lt;div class=\"alert alert-warning\"&gt;' + data.warning + '&lt;/div&gt;');</div></div>","linesAdded":7,"linesRemoved":1,"status":"pending"},"pending":3},"at":1782349293274},{"event":"tool_activity","data":{"activity":{"id":"tool_f41dfb9d-06c6-4f1c-b032-cf3b55ec29d","name":"edit","status":"completed","summary":"edit: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js","detail":"","at":"2026-06-25T01:01:33.863Z"}},"at":1782349293863},{"event":"file_change","data":{"sessionId":"cs-1782348976579-0tozus","change":{"id":"ch-4","path":"Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js","type":"edit","diffString":"--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js\n+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js\n@@ -420,7 +420,8 @@\n                         openConfirmModal(data.extracted);\n                     }\n                 })\n-                .catch(function () {\n+                .catch(function (err) {\n+                    debugLog('Fetch error', err && err.message ? err.message : 'desconocido');\n                     showResult(formatErrorMessage({\n                         message: 'Error de conexión con el servidor.',\n                         reasons: ['Comprueba que Apache (XAMPP) esté activo.']","diffHtml":"<div class=\"diff-colored\"><div class=\"diff-line diff-meta\">--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js</div><div class=\"diff-line diff-meta\">+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js</div><div class=\"diff-line diff-hunk\">@@ -420,7 +420,8 @@</div><div class=\"diff-line diff-ctx\">                         openConfirmModal(data.extracted);</div><div class=\"diff-line diff-ctx\">                     }</div><div class=\"diff-line diff-ctx\">                 })</div><div class=\"diff-line diff-del\">-                .catch(function () {</div><div class=\"diff-line diff-add\">+                .catch(function (err) {</div><div class=\"diff-line diff-add\">+                    debugLog('Fetch error', err &amp;&amp; err.message ? err.message : 'desconocido');</div><div class=\"diff-line diff-ctx\">                     showResult(formatErrorMessage({</div><div class=\"diff-line diff-ctx\">                         message: 'Error de conexión con el servidor.',</div><div class=\"diff-line diff-ctx\">                         reasons: ['Comprueba que Apache (XAMPP) esté activo.']</div></div>","linesAdded":2,"linesRemoved":1,"status":"pending"},"pending":4},"at":1782349293863},{"event":"tool_activity","data":{"activity":{"id":"tool_351a3d48-83c3-4e59-a8a5-eff2654c9db","name":"edit","status":"completed","summary":"edit: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js","detail":"","at":"2026-06-25T01:01:35.349Z"}},"at":1782349295349},{"event":"file_change","data":{"sessionId":"cs-1782348976579-0tozus","change":{"id":"ch-5","path":"Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js","type":"edit","diffString":"--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js\n+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js\n@@ -487,5 +487,7 @@\n         bindUploadForm();\n         bindReviewButtons();\n         bindConfirmActions();\n+        debugLog('Página lista', 'modo=' + expenseMode);\n+        runDiagnose();\n     });\n })();","diffHtml":"<div class=\"diff-colored\"><div class=\"diff-line diff-meta\">--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js</div><div class=\"diff-line diff-meta\">+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js</div><div class=\"diff-line diff-hunk\">@@ -487,5 +487,7 @@</div><div class=\"diff-line diff-ctx\">         bindUploadForm();</div><div class=\"diff-line diff-ctx\">         bindReviewButtons();</div><div class=\"diff-line diff-ctx\">         bindConfirmActions();</div><div class=\"diff-line diff-add\">+        debugLog('Página lista', 'modo=' + expenseMode);</div><div class=\"diff-line diff-add\">+        runDiagnose();</div><div class=\"diff-line diff-ctx\">     });</div><div class=\"diff-line diff-ctx\"> })();</div></div>","linesAdded":2,"linesRemoved":0,"status":"pending"},"pending":5},"at":1782349295349},{"event":"tool_activity","data":{"activity":{"id":"tool_6e967491-d6ec-4ba6-ade0-aa2cddf44b6","name":"edit","status":"completed","summary":"edit: …ampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Controller\\MobileExpenses.php","detail":"","at":"2026-06-25T01:01:54.008Z"}},"at":1782349314008},{"event":"file_change","data":{"sessionId":"cs-1782348976579-0tozus","change":{"id":"ch-6","path":"Plugins\\ALimpiezasMobile\\Controller\\MobileExpenses.php","type":"edit","diffString":"--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Controller\\MobileExpenses.php\n+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Controller\\MobileExpenses.php\n@@ -6,6 +6,7 @@\n use FacturaScripts\\Core\\Lib\\ControllerPermissions;\n use FacturaScripts\\Core\\Response;\n use FacturaScripts\\Core\\Tools;\n+use FacturaScripts\\Core\\UploadedFile;\n use FacturaScripts\\Core\\Where;\n use FacturaScripts\\Dinamic\\Model\\User;\n use FacturaScripts\\Plugins\\ALimpiezasMobile\\Lib\\AiExpenseProcessor;","diffHtml":"<div class=\"diff-colored\"><div class=\"diff-line diff-meta\">--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Controller\\MobileExpenses.php</div><div class=\"diff-line diff-meta\">+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Controller\\MobileExpenses.php</div><div class=\"diff-line diff-hunk\">@@ -6,6 +6,7 @@</div><div class=\"diff-line diff-ctx\"> use FacturaScripts\\Core\\Lib\\ControllerPermissions;</div><div class=\"diff-line diff-ctx\"> use FacturaScripts\\Core\\Response;</div><div class=\"diff-line diff-ctx\"> use FacturaScripts\\Core\\Tools;</div><div class=\"diff-line diff-add\">+use FacturaScripts\\Core\\UploadedFile;</div><div class=\"diff-line diff-ctx\"> use FacturaScripts\\Core\\Where;</div><div class=\"diff-line diff-ctx\"> use FacturaScripts\\Dinamic\\Model\\User;</div><div class=\"diff-line diff-ctx\"> use FacturaScripts\\Plugins\\ALimpiezasMobile\\Lib\\AiExpenseProcessor;</div></div>","linesAdded":1,"linesRemoved":0,"status":"pending"},"pending":6},"at":1782349314008},{"event":"tool_activity","data":{"activity":{"id":"tool_376de112-2182-46e1-a8b8-08d5c71b611","name":"edit","status":"completed","summary":"edit: …ampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Controller\\MobileExpenses.php","detail":"","at":"2026-06-25T01:01:54.518Z"}},"at":1782349314518},{"event":"file_change","data":{"sessionId":"cs-1782348976579-0tozus","change":{"id":"ch-7","path":"Plugins\\ALimpiezasMobile\\Controller\\MobileExpenses.php","type":"edit","diffString":"--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Controller\\MobileExpenses.php\n+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Controller\\MobileExpenses.php\n@@ -124,4 +124,20 @@\n         $this->setTemplate(false);\n         $response->json($payload);\n     }\n+\n+    private function resolveUploadedFile(string $field): ?UploadedFile\n+    {\n+        $file = $this->request->files->get($field);\n+        if ($file instanceof UploadedFile) {\n+            return $file;\n+        }\n+\n+        foreach ($this->request->files->getArray($field) as $item) {\n+            if ($item instanceof UploadedFile) {\n+                return $item;\n+            }\n+        }\n+\n+        return null;\n+    }\n }","diffHtml":"<div class=\"diff-colored\"><div class=\"diff-line diff-meta\">--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Controller\\MobileExpenses.php</div><div class=\"diff-line diff-meta\">+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Controller\\MobileExpenses.php</div><div class=\"diff-line diff-hunk\">@@ -124,4 +124,20 @@</div><div class=\"diff-line diff-ctx\">         $this-&gt;setTemplate(false);</div><div class=\"diff-line diff-ctx\">         $response-&gt;json($payload);</div><div class=\"diff-line diff-ctx\">     }</div><div class=\"diff-line diff-add\">+</div><div class=\"diff-line diff-add\">+    private function resolveUploadedFile(string $field): ?UploadedFile</div><div class=\"diff-line diff-add\">+    {</div><div class=\"diff-line diff-add\">+        $file = $this-&gt;request-&gt;files-&gt;get($field);</div><div class=\"diff-line diff-add\">+        if ($file instanceof UploadedFile) {</div><div class=\"diff-line diff-add\">+            return $file;</div><div class=\"diff-line diff-add\">+        }</div><div class=\"diff-line diff-add\">+</div><div class=\"diff-line diff-add\">+        foreach ($this-&gt;request-&gt;files-&gt;getArray($field) as $item) {</div><div class=\"diff-line diff-add\">+            if ($item instanceof UploadedFile) {</div><div class=\"diff-line diff-add\">+                return $item;</div><div class=\"diff-line diff-add\">+            }</div><div class=\"diff-line diff-add\">+        }</div><div class=\"diff-line diff-add\">+</div><div class=\"diff-line diff-add\">+        return null;</div><div class=\"diff-line diff-add\">+    }</div><div class=\"diff-line diff-ctx\"> }</div></div>","linesAdded":16,"linesRemoved":0,"status":"pending"},"pending":7},"at":1782349314518},{"event":"tool_activity","data":{"activity":{"id":"tool_4c884b49-1b6a-4701-98f0-f8eeeaa041c","name":"edit","status":"completed","summary":"edit: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Lib\\MobileConfig.php","detail":"","at":"2026-06-25T01:01:55.209Z"}},"at":1782349315209},{"event":"file_change","data":{"sessionId":"cs-1782348976579-0tozus","change":{"id":"ch-8","path":"Plugins\\ALimpiezasMobile\\Lib\\MobileConfig.php","type":"edit","diffString":"--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Lib\\MobileConfig.php\n+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Lib\\MobileConfig.php\n@@ -32,4 +32,9 @@\n     {\n         return rtrim(Tools::settings('alimpiezas', 'ai_service_url', 'http://127.0.0.1:8090'), '/');\n     }\n+\n+    public static function ollamaUrl(): string\n+    {\n+        return rtrim(Tools::settings('alimpiezas', 'ollama_url', 'http://192.168.1.16:11434'), '/');\n+    }\n }","diffHtml":"<div class=\"diff-colored\"><div class=\"diff-line diff-meta\">--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Lib\\MobileConfig.php</div><div class=\"diff-line diff-meta\">+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Lib\\MobileConfig.php</div><div class=\"diff-line diff-hunk\">@@ -32,4 +32,9 @@</div><div class=\"diff-line diff-ctx\">     {</div><div class=\"diff-line diff-ctx\">         return rtrim(Tools::settings('alimpiezas', 'ai_service_url', 'http://127.0.0.1:8090'), '/');</div><div class=\"diff-line diff-ctx\">     }</div><div class=\"diff-line diff-add\">+</div><div class=\"diff-line diff-add\">+    public static function ollamaUrl(): string</div><div class=\"diff-line diff-add\">+    {</div><div class=\"diff-line diff-add\">+        return rtrim(Tools::settings('alimpiezas', 'ollama_url', 'http://192.168.1.16:11434'), '/');</div><div class=\"diff-line diff-add\">+    }</div><div class=\"diff-line diff-ctx\"> }</div></div>","linesAdded":5,"linesRemoved":0,"status":"pending"},"pending":8},"at":1782349315209},{"event":"tool_activity","data":{"activity":{"id":"tool_2175feed-0fdc-40a7-b1c5-964cde2ab39","name":"edit","status":"completed","summary":"edit: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Lib\\AiExpenseProcessor.php","detail":"","at":"2026-06-25T01:01:55.735Z"}},"at":1782349315735},{"event":"file_change","data":{"sessionId":"cs-1782348976579-0tozus","change":{"id":"ch-9","path":"Plugins\\ALimpiezasMobile\\Lib\\AiExpenseProcessor.php","type":"edit","diffString":"--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Lib\\AiExpenseProcessor.php\n+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Lib\\AiExpenseProcessor.php\n@@ -24,7 +24,7 @@\n         }\n \n         $extracted = MobileConfig::isAiServiceEnabled()\n-            ? self::callAiService(FS_FOLDER . '/MyFiles/' . $attached->path, $docType)\n+            ? self::callAiService(FS_FOLDER . '/MyFiles/' . $attached->path, $docType, MobileConfig::expenseMode())\n             : self::manualExtract($docType);\n \n         if (!empty($extracted['_error'])) {","diffHtml":"<div class=\"diff-colored\"><div class=\"diff-line diff-meta\">--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Lib\\AiExpenseProcessor.php</div><div class=\"diff-line diff-meta\">+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Lib\\AiExpenseProcessor.php</div><div class=\"diff-line diff-hunk\">@@ -24,7 +24,7 @@</div><div class=\"diff-line diff-ctx\">         }</div><div class=\"diff-line diff-ctx\"> </div><div class=\"diff-line diff-ctx\">         $extracted = MobileConfig::isAiServiceEnabled()</div><div class=\"diff-line diff-del\">-            ? self::callAiService(FS_FOLDER . '/MyFiles/' . $attached-&gt;path, $docType)</div><div class=\"diff-line diff-add\">+            ? self::callAiService(FS_FOLDER . '/MyFiles/' . $attached-&gt;path, $docType, MobileConfig::expenseMode())</div><div class=\"diff-line diff-ctx\">             : self::manualExtract($docType);</div><div class=\"diff-line diff-ctx\"> </div><div class=\"diff-line diff-ctx\">         if (!empty($extracted['_error'])) {</div></div>","linesAdded":1,"linesRemoved":1,"status":"pending"},"pending":9},"at":1782349315735},{"event":"tool_activity","data":{"activity":{"id":"tool_264934ab-3bc5-4e36-8dcd-28fc95d89fe","name":"edit","status":"completed","summary":"edit: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Lib\\AiExpenseProcessor.php","detail":"","at":"2026-06-25T01:01:57.741Z"}},"at":1782349317741},{"event":"file_change","data":{"sessionId":"cs-1782348976579-0tozus","change":{"id":"ch-10","path":"Plugins\\ALimpiezasMobile\\Lib\\AiExpenseProcessor.php","type":"edit","diffString":"--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Lib\\AiExpenseProcessor.php\n+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Lib\\AiExpenseProcessor.php\n@@ -117,6 +117,69 @@\n         return ['ok' => true, 'health' => $data];\n     }\n \n+    public static function diagnose(): array\n+    {\n+        $mode = MobileConfig::expenseMode();\n+        $serviceUrl = MobileConfig::aiServiceUrl();\n+        $service = MobileConfig::isAiServiceEnabled() ? self::checkServiceHealth() : ['ok' => true, 'skipped' => true];\n+        $ollama = $mode === MobileConfig::MODE_AI ? self::checkOllamaHealth() : ['ok' => null, 'skipped' => true];\n+\n+        return [\n+            'ok' => true,\n+            'mode' => $mode,\n+            'service_url' => $serviceUrl,\n+            'ollama_url' => MobileConfig::ollamaUrl(),\n+            'service' => $service,\n+            'ollama' => $ollama,\n+            'php_curl' => function_exists('curl_init'),\n+            'upload_max' => ini_get('upload_max_filesize'),\n+            'post_max' => ini_get('post_max_size'),\n+        ];\n+    }\n+\n+    public static function checkOllamaHealth(): array\n+    {\n+        if (!function_exists('curl_init')) {\n+            return ['ok' => false, 'message' => 'cURL no disponible'];\n+        }\n+\n+        $url = MobileConfig::ollamaUrl() . '/api/tags';\n+        $curl = curl_init();\n+        curl_setopt_array($curl, [\n+            CURLOPT_URL => $url,\n+            CURLOPT_RETURNTRANSFER => true,\n+            CURLOPT_TIMEOUT => 8,\n+            CURLOPT_CONNECTTIMEOUT => 4,\n+        ]);\n+        $response = curl_exec($curl);\n+        $httpCode = (int) curl_getinfo($curl, CURLINFO_HTTP_CODE);\n+        $error = curl_error($curl);\n+        curl_close($curl);\n+\n+        if ($response === false || $httpCode >= 400) {\n+            return [\n+                'ok' => false,\n+                'message' => 'Ollama no responde en ' . MobileConfig::ollamaUrl(),\n+                'reasons' => array_filter([$error ?: null, 'HTTP ' . $httpCode]),\n+            ];\n+        }\n+\n+        $data = json_decode($response, true);\n+        $models = [];\n+        if (is_array($data['models'] ?? null)) {\n+            foreach ($data['models'] as $model) {\n+                if (!empty($model['name'])) {\n+                    $models[] = $model['name'];\n+                }\n+            }\n+        }\n+\n+        return [\n+            'ok' => true,\n+            'models' => array_slice($models, 0, 5),\n+        ];\n+    }\n+\n     private static function saveUploadedFile(UploadedFile $uploadFile): ?AttachedFile\n     {\n         if (false === $uploadFile->isValid()) {","diffHtml":"<div class=\"diff-colored\"><div class=\"diff-line diff-meta\">--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Lib\\AiExpenseProcessor.php</div><div class=\"diff-line diff-meta\">+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Lib\\AiExpenseProcessor.php</div><div class=\"diff-line diff-hunk\">@@ -117,6 +117,69 @@</div><div class=\"diff-line diff-ctx\">         return ['ok' =&gt; true, 'health' =&gt; $data];</div><div class=\"diff-line diff-ctx\">     }</div><div class=\"diff-line diff-ctx\"> </div><div class=\"diff-line diff-add\">+    public static function diagnose(): array</div><div class=\"diff-line diff-add\">+    {</div><div class=\"diff-line diff-add\">+        $mode = MobileConfig::expenseMode();</div><div class=\"diff-line diff-add\">+        $serviceUrl = MobileConfig::aiServiceUrl();</div><div class=\"diff-line diff-add\">+        $service = MobileConfig::isAiServiceEnabled() ? self::checkServiceHealth() : ['ok' =&gt; true, 'skipped' =&gt; true];</div><div class=\"diff-line diff-add\">+        $ollama = $mode === MobileConfig::MODE_AI ? self::checkOllamaHealth() : ['ok' =&gt; null, 'skipped' =&gt; true];</div><div class=\"diff-line diff-add\">+</div><div class=\"diff-line diff-add\">+        return [</div><div class=\"diff-line diff-add\">+            'ok' =&gt; true,</div><div class=\"diff-line diff-add\">+            'mode' =&gt; $mode,</div><div class=\"diff-line diff-add\">+            'service_url' =&gt; $serviceUrl,</div><div class=\"diff-line diff-add\">+            'ollama_url' =&gt; MobileConfig::ollamaUrl(),</div><div class=\"diff-line diff-add\">+            'service' =&gt; $service,</div><div class=\"diff-line diff-add\">+            'ollama' =&gt; $ollama,</div><div class=\"diff-line diff-add\">+            'php_curl' =&gt; function_exists('curl_init'),</div><div class=\"diff-line diff-add\">+            'upload_max' =&gt; ini_get('upload_max_filesize'),</div><div class=\"diff-line diff-add\">+            'post_max' =&gt; ini_get('post_max_size'),</div><div class=\"diff-line diff-add\">+        ];</div><div class=\"diff-line diff-add\">+    }</div><div class=\"diff-line diff-add\">+</div><div class=\"diff-line diff-add\">+    public static function checkOllamaHealth(): array</div><div class=\"diff-line diff-add\">+    {</div><div class=\"diff-line diff-add\">+        if (!function_exists('curl_init')) {</div><div class=\"diff-line diff-add\">+            return ['ok' =&gt; false, 'message' =&gt; 'cURL no disponible'];</div><div class=\"diff-line diff-add\">+        }</div><div class=\"diff-line diff-add\">+</div><div class=\"diff-line diff-add\">+        $url = MobileConfig::ollamaUrl() . '/api/tags';</div><div class=\"diff-line diff-add\">+        $curl = curl_init();</div><div class=\"diff-line diff-add\">+        curl_setopt_array($curl, [</div><div class=\"diff-line diff-add\">+            CURLOPT_URL =&gt; $url,</div><div class=\"diff-line diff-add\">+            CURLOPT_RETURNTRANSFER =&gt; true,</div><div class=\"diff-line diff-add\">+            CURLOPT_TIMEOUT =&gt; 8,</div><div class=\"diff-line diff-add\">+            CURLOPT_CONNECTTIMEOUT =&gt; 4,</div><div class=\"diff-line diff-add\">+        ]);</div><div class=\"diff-line diff-add\">+        $response = curl_exec($curl);</div><div class=\"diff-line diff-add\">+        $httpCode = (int) curl_getinfo($curl, CURLINFO_HTTP_CODE);</div><div class=\"diff-line diff-add\">+        $error = curl_error($curl);</div><div class=\"diff-line diff-add\">+        curl_close($curl);</div><div class=\"diff-line diff-add\">+</div><div class=\"diff-line diff-add\">+        if ($response === false || $httpCode &gt;= 400) {</div><div class=\"diff-line diff-add\">+            return [</div><div class=\"diff-line diff-add\">+                'ok' =&gt; false,</div><div class=\"diff-line diff-add\">+                'message' =&gt; 'Ollama no responde en ' . MobileConfig::ollamaUrl(),</div><div class=\"diff-line diff-add\">+                'reasons' =&gt; array_filter([$error ?: null, 'HTTP ' . $httpCode]),</div><div class=\"diff-line diff-add\">+            ];</div><div class=\"diff-line diff-add\">+        }</div><div class=\"diff-line diff-add\">+</div><div class=\"diff-line diff-add\">+        $data = json_decode($response, true);</div><div class=\"diff-line diff-add\">+        $models = [];</div><div class=\"diff-line diff-add\">+        if (is_array($data['models'] ?? null)) {</div><div class=\"diff-line diff-add\">+            foreach ($data['models'] as $model) {</div><div class=\"diff-line diff-add\">+                if (!empty($model['name'])) {</div><div class=\"diff-line diff-add\">+                    $models[] = $model['name'];</div><div class=\"diff-line diff-add\">+                }</div><div class=\"diff-line diff-add\">+            }</div><div class=\"diff-line diff-add\">+        }</div><div class=\"diff-line diff-add\">+</div><div class=\"diff-line diff-add\">+        return [</div><div class=\"diff-line diff-add\">+            'ok' =&gt; true,</div><div class=\"diff-line diff-add\">+            'models' =&gt; array_slice($models, 0, 5),</div><div class=\"diff-line diff-add\">+        ];</div><div class=\"diff-line diff-add\">+    }</div><div class=\"diff-line diff-add\">+</div><div class=\"diff-line diff-ctx\">     private static function saveUploadedFile(UploadedFile $uploadFile): ?AttachedFile</div><div class=\"diff-line diff-ctx\">     {</div><div class=\"diff-line diff-ctx\">         if (false === $uploadFile-&gt;isValid()) {</div></div>","linesAdded":63,"linesRemoved":0,"status":"pending"},"pending":10},"at":1782349317742},{"event":"tool_activity","data":{"activity":{"id":"tool_2e56aaf1-0bce-4ca4-a588-ffa39193b52","name":"edit","status":"completed","summary":"edit: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Lib\\AiExpenseProcessor.php","detail":"","at":"2026-06-25T01:01:59.071Z"}},"at":1782349319071},{"event":"file_change","data":{"sessionId":"cs-1782348976579-0tozus","change":{"id":"ch-11","path":"Plugins\\ALimpiezasMobile\\Lib\\AiExpenseProcessor.php","type":"edit","diffString":"--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Lib\\AiExpenseProcessor.php\n+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Lib\\AiExpenseProcessor.php\n@@ -357,7 +357,7 @@\n         ];\n     }\n \n-    private static function callAiService(string $filePath, string $docType): array\n+    private static function callAiService(string $filePath, string $docType, string $mode = MobileConfig::MODE_OCR): array\n     {\n         $serviceUrl = MobileConfig::aiServiceUrl();\n         $endpoint = $serviceUrl . '/api/v1/extract-invoice';\n@@ -371,6 +371,8 @@\n             CURLOPT_CONNECTTIMEOUT => 10,\n             CURLOPT_POSTFIELDS => [\n                 'doc_type' => $docType,\n+                'mode' => $mode,\n+                'ollama_url' => MobileConfig::ollamaUrl(),\n                 'file' => new \\CURLFile(\n                     $filePath,\n                     self::guessMimeType($filePath),","diffHtml":"<div class=\"diff-colored\"><div class=\"diff-line diff-meta\">--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Lib\\AiExpenseProcessor.php</div><div class=\"diff-line diff-meta\">+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Lib\\AiExpenseProcessor.php</div><div class=\"diff-line diff-hunk\">@@ -357,7 +357,7 @@</div><div class=\"diff-line diff-ctx\">         ];</div><div class=\"diff-line diff-ctx\">     }</div><div class=\"diff-line diff-ctx\"> </div><div class=\"diff-line diff-del\">-    private static function callAiService(string $filePath, string $docType): array</div><div class=\"diff-line diff-add\">+    private static function callAiService(string $filePath, string $docType, string $mode = MobileConfig::MODE_OCR): array</div><div class=\"diff-line diff-ctx\">     {</div><div class=\"diff-line diff-ctx\">         $serviceUrl = MobileConfig::aiServiceUrl();</div><div class=\"diff-line diff-ctx\">         $endpoint = $serviceUrl . '/api/v1/extract-invoice';</div><div class=\"diff-line diff-hunk\">@@ -371,6 +371,8 @@</div><div class=\"diff-line diff-ctx\">             CURLOPT_CONNECTTIMEOUT =&gt; 10,</div><div class=\"diff-line diff-ctx\">             CURLOPT_POSTFIELDS =&gt; [</div><div class=\"diff-line diff-ctx\">                 'doc_type' =&gt; $docType,</div><div class=\"diff-line diff-add\">+                'mode' =&gt; $mode,</div><div class=\"diff-line diff-add\">+                'ollama_url' =&gt; MobileConfig::ollamaUrl(),</div><div class=\"diff-line diff-ctx\">                 'file' =&gt; new \\CURLFile(</div><div class=\"diff-line diff-ctx\">                     $filePath,</div><div class=\"diff-line diff-ctx\">                     self::guessMimeType($filePath),</div></div>","linesAdded":3,"linesRemoved":1,"status":"pending"},"pending":11},"at":1782349319073},{"event":"tool_activity","data":{"activity":{"id":"tool_19588df5-8562-46a3-ab1d-08a9f3fa484","name":"edit","status":"completed","summary":"edit: …ampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\View\\MobileExpenses.html.twig","detail":"","at":"2026-06-25T01:02:09.093Z"}},"at":1782349329093},{"event":"file_change","data":{"sessionId":"cs-1782348976579-0tozus","change":{"id":"ch-12","path":"Plugins\\ALimpiezasMobile\\View\\MobileExpenses.html.twig","type":"edit","diffString":"--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\View\\MobileExpenses.html.twig\n+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\View\\MobileExpenses.html.twig\n@@ -32,22 +32,22 @@\n                     <label class=\"form-label\">Documento <span class=\"text-muted\" id=\"file-label-hint\">(obligatorio para OCR)</span></label>\n                     <input type=\"file\" name=\"image\" id=\"file-input-active\" class=\"visually-hidden\" tabindex=\"-1\" aria-hidden=\"true\" accept=\"image/*,.pdf,application/pdf,.heic,.heif\">\n                     <div class=\"d-grid gap-2 mb-2\">\n-                        <div class=\"file-picker-wrap\" id=\"pick-file-wrap\">\n-                            <span class=\"btn btn-outline-secondary btn-lg w-100\" id=\"pick-file-btn\">\n-                                <i class=\"fa-solid fa-folder-open me-2\"></i>Elegir archivo o galería\n-                            </span>\n-                            <input type=\"file\" id=\"file-input-gallery\" class=\"file-picker-overlay\"\n+                        <label class=\"file-picker-wrap mb-0\" id=\"pick-file-wrap\">\n+                            <input type=\"file\" id=\"file-input-gallery\" class=\"visually-hidden\"\n                                    accept=\"image/*,.pdf,application/pdf,.heic,.heif\"\n                                    aria-label=\"Elegir archivo o galería\">\n-                        </div>\n-                        <div class=\"file-picker-wrap\" id=\"pick-camera-wrap\">\n-                            <span class=\"btn btn-outline-secondary btn-lg w-100\" id=\"pick-camera-btn\">\n-                                <i class=\"fa-solid fa-camera me-2\"></i>Hacer foto\n+                            <span class=\"btn btn-outline-secondary btn-lg w-100\">\n+                                <i class=\"fa-solid fa-folder-open me-2\"></i>Elegir archivo o galería\n                             </span>\n-                            <input type=\"file\" id=\"file-input-camera\" class=\"file-picker-overlay\"\n+                        </label>\n+                        <label class=\"file-picker-wrap mb-0\" id=\"pick-camera-wrap\">\n+                            <input type=\"file\" id=\"file-input-camera\" class=\"visually-hidden\"\n                                    accept=\"image/*\" capture=\"environment\"\n                                    aria-label=\"Hacer foto con cámara\">\n-                        </div>\n+                            <span class=\"btn btn-outline-secondary btn-lg w-100\">\n+                                <i class=\"fa-solid fa-camera me-2\"></i>Hacer foto\n+                            </span>\n+                        </label>\n                     </div>\n                     <div id=\"file-selected-name\" class=\"small text-muted\">Ningún documento seleccionado</div>\n                 </div>","diffHtml":"<div class=\"diff-colored\"><div class=\"diff-line diff-meta\">--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\View\\MobileExpenses.html.twig</div><div class=\"diff-line diff-meta\">+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\View\\MobileExpenses.html.twig</div><div class=\"diff-line diff-hunk\">@@ -32,22 +32,22 @@</div><div class=\"diff-line diff-ctx\">                     &lt;label class=\"form-label\"&gt;Documento &lt;span class=\"text-muted\" id=\"file-label-hint\"&gt;(obligatorio para OCR)&lt;/span&gt;&lt;/label&gt;</div><div class=\"diff-line diff-ctx\">                     &lt;input type=\"file\" name=\"image\" id=\"file-input-active\" class=\"visually-hidden\" tabindex=\"-1\" aria-hidden=\"true\" accept=\"image/*,.pdf,application/pdf,.heic,.heif\"&gt;</div><div class=\"diff-line diff-ctx\">                     &lt;div class=\"d-grid gap-2 mb-2\"&gt;</div><div class=\"diff-line diff-del\">-                        &lt;div class=\"file-picker-wrap\" id=\"pick-file-wrap\"&gt;</div><div class=\"diff-line diff-del\">-                            &lt;span class=\"btn btn-outline-secondary btn-lg w-100\" id=\"pick-file-btn\"&gt;</div><div class=\"diff-line diff-del\">-                                &lt;i class=\"fa-solid fa-folder-open me-2\"&gt;&lt;/i&gt;Elegir archivo o galería</div><div class=\"diff-line diff-del\">-                            &lt;/span&gt;</div><div class=\"diff-line diff-del\">-                            &lt;input type=\"file\" id=\"file-input-gallery\" class=\"file-picker-overlay\"</div><div class=\"diff-line diff-add\">+                        &lt;label class=\"file-picker-wrap mb-0\" id=\"pick-file-wrap\"&gt;</div><div class=\"diff-line diff-add\">+                            &lt;input type=\"file\" id=\"file-input-gallery\" class=\"visually-hidden\"</div><div class=\"diff-line diff-ctx\">                                    accept=\"image/*,.pdf,application/pdf,.heic,.heif\"</div><div class=\"diff-line diff-ctx\">                                    aria-label=\"Elegir archivo o galería\"&gt;</div><div class=\"diff-line diff-del\">-                        &lt;/div&gt;</div><div class=\"diff-line diff-del\">-                        &lt;div class=\"file-picker-wrap\" id=\"pick-camera-wrap\"&gt;</div><div class=\"diff-line diff-del\">-                            &lt;span class=\"btn btn-outline-secondary btn-lg w-100\" id=\"pick-camera-btn\"&gt;</div><div class=\"diff-line diff-del\">-                                &lt;i class=\"fa-solid fa-camera me-2\"&gt;&lt;/i&gt;Hacer foto</div><div class=\"diff-line diff-add\">+                            &lt;span class=\"btn btn-outline-secondary btn-lg w-100\"&gt;</div><div class=\"diff-line diff-add\">+                                &lt;i class=\"fa-solid fa-folder-open me-2\"&gt;&lt;/i&gt;Elegir archivo o galería</div><div class=\"diff-line diff-ctx\">                             &lt;/span&gt;</div><div class=\"diff-line diff-del\">-                            &lt;input type=\"file\" id=\"file-input-camera\" class=\"file-picker-overlay\"</div><div class=\"diff-line diff-add\">+                        &lt;/label&gt;</div><div class=\"diff-line diff-add\">+                        &lt;label class=\"file-picker-wrap mb-0\" id=\"pick-camera-wrap\"&gt;</div><div class=\"diff-line diff-add\">+                            &lt;input type=\"file\" id=\"file-input-camera\" class=\"visually-hidden\"</div><div class=\"diff-line diff-ctx\">                                    accept=\"image/*\" capture=\"environment\"</div><div class=\"diff-line diff-ctx\">                                    aria-label=\"Hacer foto con cámara\"&gt;</div><div class=\"diff-line diff-del\">-                        &lt;/div&gt;</div><div class=\"diff-line diff-add\">+                            &lt;span class=\"btn btn-outline-secondary btn-lg w-100\"&gt;</div><div class=\"diff-line diff-add\">+                                &lt;i class=\"fa-solid fa-camera me-2\"&gt;&lt;/i&gt;Hacer foto</div><div class=\"diff-line diff-add\">+                            &lt;/span&gt;</div><div class=\"diff-line diff-add\">+                        &lt;/label&gt;</div><div class=\"diff-line diff-ctx\">                     &lt;/div&gt;</div><div class=\"diff-line diff-ctx\">                     &lt;div id=\"file-selected-name\" class=\"small text-muted\"&gt;Ningún documento seleccionado&lt;/div&gt;</div><div class=\"diff-line diff-ctx\">                 &lt;/div&gt;</div></div>","linesAdded":11,"linesRemoved":11,"status":"pending"},"pending":12},"at":1782349329093},{"event":"tool_activity","data":{"activity":{"id":"tool_1a47d84a-cae6-4a15-a1ee-ddee9a0ebee","name":"edit","status":"completed","summary":"edit: …ampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\View\\MobileExpenses.html.twig","detail":"","at":"2026-06-25T01:02:10.024Z"}},"at":1782349330024},{"event":"file_change","data":{"sessionId":"cs-1782348976579-0tozus","change":{"id":"ch-13","path":"Plugins\\ALimpiezasMobile\\View\\MobileExpenses.html.twig","type":"edit","diffString":"--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\View\\MobileExpenses.html.twig\n+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\View\\MobileExpenses.html.twig\n@@ -61,6 +61,10 @@\n                 </div>\n             </form>\n             <div id=\"upload-result\" class=\"mt-3\"></div>\n+            <details class=\"mt-3 debug-panel-wrap\">\n+                <summary class=\"small text-muted\">Debug conexión OCR/IA</summary>\n+                <div id=\"debug-panel\" class=\"debug-panel small mt-2\"></div>\n+            </details>\n         </div>\n     </div>\n ","diffHtml":"<div class=\"diff-colored\"><div class=\"diff-line diff-meta\">--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\View\\MobileExpenses.html.twig</div><div class=\"diff-line diff-meta\">+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\View\\MobileExpenses.html.twig</div><div class=\"diff-line diff-hunk\">@@ -61,6 +61,10 @@</div><div class=\"diff-line diff-ctx\">                 &lt;/div&gt;</div><div class=\"diff-line diff-ctx\">             &lt;/form&gt;</div><div class=\"diff-line diff-ctx\">             &lt;div id=\"upload-result\" class=\"mt-3\"&gt;&lt;/div&gt;</div><div class=\"diff-line diff-add\">+            &lt;details class=\"mt-3 debug-panel-wrap\"&gt;</div><div class=\"diff-line diff-add\">+                &lt;summary class=\"small text-muted\"&gt;Debug conexión OCR/IA&lt;/summary&gt;</div><div class=\"diff-line diff-add\">+                &lt;div id=\"debug-panel\" class=\"debug-panel small mt-2\"&gt;&lt;/div&gt;</div><div class=\"diff-line diff-add\">+            &lt;/details&gt;</div><div class=\"diff-line diff-ctx\">         &lt;/div&gt;</div><div class=\"diff-line diff-ctx\">     &lt;/div&gt;</div><div class=\"diff-line diff-ctx\"> </div></div>","linesAdded":4,"linesRemoved":0,"status":"pending"},"pending":13},"at":1782349330026},{"event":"tool_activity","data":{"activity":{"id":"tool_4fdeeb96-2fd5-4439-afe9-dc58e60d818","name":"edit","status":"running","summary":"edit: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\CSS\\mobile.css","detail":"","at":"2026-06-25T01:02:11.515Z"}},"at":1782349331515},{"event":"status","data":{"text":"Límite de 80 herramientas alcanzado — finalizando con lo obtenido."},"at":1782349331515},{"event":"status","data":{"text":"Turno añadido al transcript del IDE (reabrir chat para verlo)"},"at":1782349331525},{"event":"done","data":{"ok":true,"bridgeSessionId":"bs-1782349331522-ozbgts","agentId":"agent-433f2df9-d91f-4f51-9d33-bbd01896105f","runId":"run-24db9593-de93-49dc-9e60-a9ff98a2855c","status":"cancelled","result":"","changeSessionId":"cs-1782348976579-0tozus","pendingChanges":13,"changes":[{"id":"ch-1","callId":"tool_eb97dec0-6bcf-4d18-8c05-29fc4f233dc","path":"Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js","type":"edit","diffString":"--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js\n+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js\n@@ -37,6 +37,46 @@\n         }\n     }\n \n+    function debugLog(message, detail) {\n+        var panel = document.getElementById('debug-panel');\n+        if (!panel) {\n+            return;\n+        }\n+        var line = document.createElement('div');\n+        line.className = 'debug-line';\n+        var text = new Date().toLocaleTimeString('es-ES') + ' — ' + message;\n+        if (detail) {\n+            text += ' · ' + (typeof detail === 'string' ? detail : JSON.stringify(detail));\n+        }\n+        line.textContent = text;\n+        panel.appendChild(line);\n+        panel.scrollTop = panel.scrollHeight;\n+    }\n+\n+    function runDiagnose() {\n+        var data = new FormData();\n+        data.append('action', 'diagnose');\n+        debugLog('Comprobando servicio OCR/IA…');\n+        postForm(data).then(function (response) {\n+            if (!response.ok) {\n+                debugLog('Diagnóstico falló', response.message || 'error');\n+                return;\n+            }\n+            debugLog('Modo', response.mode);\n+            debugLog('URL servicio', response.service_url);\n+            if (response.service && response.service.ok) {\n+                debugLog('Servicio OCR', 'OK');\n+            } else if (response.service) {\n+                debugLog('Servicio OCR', response.service.message || 'no responde');\n+            }\n+            if (response.ollama) {\n+                debugLog('Ollama LAN', response.ollama.ok ? 'OK' : (response.ollama.message || 'no responde'));\n+            }\n+        }).catch(function () {\n+            debugLog('Diagnóstico', 'error de red con FacturaScripts');\n+        });\n+    }\n+\n     function formatErrorMessage(data) {\n         var html = '<div class=\"alert alert-danger mb-0\"><strong>' + (data.message || 'Error') + '</strong>';\n         if (data.reasons && data.reasons.length) {","linesAdded":40,"linesRemoved":0,"status":"pending"},{"id":"ch-2","callId":"tool_51520127-1294-4037-9b59-0ae82c4b47d","path":"Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js","type":"edit","diffString":"--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js\n+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js\n@@ -172,6 +172,7 @@\n \n         if (galleryInput) {\n             galleryInput.addEventListener('change', function () {\n+                debugLog('Galería/archivo seleccionado', galleryInput.files.length ? galleryInput.files[0].name : 'vacío');\n                 if (!assignSelectedFile(galleryInput)) {\n                     showResult(formatErrorMessage({\n                         message: 'No se pudo leer el archivo seleccionado.',\n@@ -183,6 +184,7 @@\n \n         if (cameraInput) {\n             cameraInput.addEventListener('change', function () {\n+                debugLog('Foto cámara', cameraInput.files.length ? cameraInput.files[0].name : 'vacío');\n                 if (!assignSelectedFile(cameraInput)) {\n                     showResult(formatErrorMessage({\n                         message: 'No se pudo leer la foto.',","linesAdded":2,"linesRemoved":0,"status":"pending"},{"id":"ch-3","callId":"tool_8c678267-dd6b-458b-ba45-6aedc20c316","path":"Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js","type":"edit","diffString":"--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js\n+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js\n@@ -385,7 +385,7 @@\n                 return;\n             }\n \n-            if (expenseMode !== 'manual' && (!fileInput || !fileInput.files.length)) {\n+            if (expenseMode !== 'manual' && !uploadFile) {\n                 showResult(formatErrorMessage({\n                     message: 'Debes elegir un archivo, una foto de galería o hacer una foto.',\n                     reasons: ['En modo OCR/IA el documento es obligatorio.', 'Usa «Elegir archivo o galería» o «Hacer foto».'],\n@@ -396,13 +396,19 @@\n \n             btn.disabled = true;\n             btn.innerHTML = '<span class=\"spinner-border spinner-border-sm me-2\"></span>Procesando...';\n+            debugLog('Enviando documento', uploadFile ? uploadFile.name + ' (' + uploadFile.size + ' B)' : 'sin archivo');\n \n             postForm(formData)\n                 .then(function (data) {\n+                    if (data.debug) {\n+                        debugLog('Respuesta servidor', data.debug);\n+                    }\n                     if (!data.ok) {\n+                        debugLog('Error upload', data.message);\n                         showResult(formatErrorMessage(data));\n                         return;\n                     }\n+                    debugLog('Upload OK', data.message || 'analizado');\n \n                     if (data.warning) {\n                         showResult('<div class=\"alert alert-warning\">' + data.warning + '</div>');","linesAdded":7,"linesRemoved":1,"status":"pending"},{"id":"ch-4","callId":"tool_f41dfb9d-06c6-4f1c-b032-cf3b55ec29d","path":"Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js","type":"edit","diffString":"--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js\n+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js\n@@ -420,7 +420,8 @@\n                         openConfirmModal(data.extracted);\n                     }\n                 })\n-                .catch(function () {\n+                .catch(function (err) {\n+                    debugLog('Fetch error', err && err.message ? err.message : 'desconocido');\n                     showResult(formatErrorMessage({\n                         message: 'Error de conexión con el servidor.',\n                         reasons: ['Comprueba que Apache (XAMPP) esté activo.']","linesAdded":2,"linesRemoved":1,"status":"pending"},{"id":"ch-5","callId":"tool_351a3d48-83c3-4e59-a8a5-eff2654c9db","path":"Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js","type":"edit","diffString":"--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js\n+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Assets\\JS\\mobile.js\n@@ -487,5 +487,7 @@\n         bindUploadForm();\n         bindReviewButtons();\n         bindConfirmActions();\n+        debugLog('Página lista', 'modo=' + expenseMode);\n+        runDiagnose();\n     });\n })();","linesAdded":2,"linesRemoved":0,"status":"pending"},{"id":"ch-6","callId":"tool_6e967491-d6ec-4ba6-ade0-aa2cddf44b6","path":"Plugins\\ALimpiezasMobile\\Controller\\MobileExpenses.php","type":"edit","diffString":"--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Controller\\MobileExpenses.php\n+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Controller\\MobileExpenses.php\n@@ -6,6 +6,7 @@\n use FacturaScripts\\Core\\Lib\\ControllerPermissions;\n use FacturaScripts\\Core\\Response;\n use FacturaScripts\\Core\\Tools;\n+use FacturaScripts\\Core\\UploadedFile;\n use FacturaScripts\\Core\\Where;\n use FacturaScripts\\Dinamic\\Model\\User;\n use FacturaScripts\\Plugins\\ALimpiezasMobile\\Lib\\AiExpenseProcessor;","linesAdded":1,"linesRemoved":0,"status":"pending"},{"id":"ch-7","callId":"tool_376de112-2182-46e1-a8b8-08d5c71b611","path":"Plugins\\ALimpiezasMobile\\Controller\\MobileExpenses.php","type":"edit","diffString":"--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Controller\\MobileExpenses.php\n+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Controller\\MobileExpenses.php\n@@ -124,4 +124,20 @@\n         $this->setTemplate(false);\n         $response->json($payload);\n     }\n+\n+    private function resolveUploadedFile(string $field): ?UploadedFile\n+    {\n+        $file = $this->request->files->get($field);\n+        if ($file instanceof UploadedFile) {\n+            return $file;\n+        }\n+\n+        foreach ($this->request->files->getArray($field) as $item) {\n+            if ($item instanceof UploadedFile) {\n+                return $item;\n+            }\n+        }\n+\n+        return null;\n+    }\n }","linesAdded":16,"linesRemoved":0,"status":"pending"},{"id":"ch-8","callId":"tool_4c884b49-1b6a-4701-98f0-f8eeeaa041c","path":"Plugins\\ALimpiezasMobile\\Lib\\MobileConfig.php","type":"edit","diffString":"--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Lib\\MobileConfig.php\n+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Lib\\MobileConfig.php\n@@ -32,4 +32,9 @@\n     {\n         return rtrim(Tools::settings('alimpiezas', 'ai_service_url', 'http://127.0.0.1:8090'), '/');\n     }\n+\n+    public static function ollamaUrl(): string\n+    {\n+        return rtrim(Tools::settings('alimpiezas', 'ollama_url', 'http://192.168.1.16:11434'), '/');\n+    }\n }","linesAdded":5,"linesRemoved":0,"status":"pending"},{"id":"ch-9","callId":"tool_2175feed-0fdc-40a7-b1c5-964cde2ab39","path":"Plugins\\ALimpiezasMobile\\Lib\\AiExpenseProcessor.php","type":"edit","diffString":"--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Lib\\AiExpenseProcessor.php\n+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Lib\\AiExpenseProcessor.php\n@@ -24,7 +24,7 @@\n         }\n \n         $extracted = MobileConfig::isAiServiceEnabled()\n-            ? self::callAiService(FS_FOLDER . '/MyFiles/' . $attached->path, $docType)\n+            ? self::callAiService(FS_FOLDER . '/MyFiles/' . $attached->path, $docType, MobileConfig::expenseMode())\n             : self::manualExtract($docType);\n \n         if (!empty($extracted['_error'])) {","linesAdded":1,"linesRemoved":1,"status":"pending"},{"id":"ch-10","callId":"tool_264934ab-3bc5-4e36-8dcd-28fc95d89fe","path":"Plugins\\ALimpiezasMobile\\Lib\\AiExpenseProcessor.php","type":"edit","diffString":"--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Lib\\AiExpenseProcessor.php\n+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Lib\\AiExpenseProcessor.php\n@@ -117,6 +117,69 @@\n         return ['ok' => true, 'health' => $data];\n     }\n \n+    public static function diagnose(): array\n+    {\n+        $mode = MobileConfig::expenseMode();\n+        $serviceUrl = MobileConfig::aiServiceUrl();\n+        $service = MobileConfig::isAiServiceEnabled() ? self::checkServiceHealth() : ['ok' => true, 'skipped' => true];\n+        $ollama = $mode === MobileConfig::MODE_AI ? self::checkOllamaHealth() : ['ok' => null, 'skipped' => true];\n+\n+        return [\n+            'ok' => true,\n+            'mode' => $mode,\n+            'service_url' => $serviceUrl,\n+            'ollama_url' => MobileConfig::ollamaUrl(),\n+            'service' => $service,\n+            'ollama' => $ollama,\n+            'php_curl' => function_exists('curl_init'),\n+            'upload_max' => ini_get('upload_max_filesize'),\n+            'post_max' => ini_get('post_max_size'),\n+        ];\n+    }\n+\n+    public static function checkOllamaHealth(): array\n+    {\n+        if (!function_exists('curl_init')) {\n+            return ['ok' => false, 'message' => 'cURL no disponible'];\n+        }\n+\n+        $url = MobileConfig::ollamaUrl() . '/api/tags';\n+        $curl = curl_init();\n+        curl_setopt_array($curl, [\n+            CURLOPT_URL => $url,\n+            CURLOPT_RETURNTRANSFER => true,\n+            CURLOPT_TIMEOUT => 8,\n+            CURLOPT_CONNECTTIMEOUT => 4,\n+        ]);\n+        $response = curl_exec($curl);\n+        $httpCode = (int) curl_getinfo($curl, CURLINFO_HTTP_CODE);\n+        $error = curl_error($curl);\n+        curl_close($curl);\n+\n+        if ($response === false || $httpCode >= 400) {\n+            return [\n+                'ok' => false,\n+                'message' => 'Ollama no responde en ' . MobileConfig::ollamaUrl(),\n+                'reasons' => array_filter([$error ?: null, 'HTTP ' . $httpCode]),\n+            ];\n+        }\n+\n+        $data = json_decode($response, true);\n+        $models = [];\n+        if (is_array($data['models'] ?? null)) {\n+            foreach ($data['models'] as $model) {\n+                if (!empty($model['name'])) {\n+                    $models[] = $model['name'];\n+                }\n+            }\n+        }\n+\n+        return [\n+            'ok' => true,\n+            'models' => array_slice($models, 0, 5),\n+        ];\n+    }\n+\n     private static function saveUploadedFile(UploadedFile $uploadFile): ?AttachedFile\n     {\n         if (false === $uploadFile->isValid()) {","linesAdded":63,"linesRemoved":0,"status":"pending"},{"id":"ch-11","callId":"tool_2e56aaf1-0bce-4ca4-a588-ffa39193b52","path":"Plugins\\ALimpiezasMobile\\Lib\\AiExpenseProcessor.php","type":"edit","diffString":"--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Lib\\AiExpenseProcessor.php\n+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Lib\\AiExpenseProcessor.php\n@@ -357,7 +357,7 @@\n         ];\n     }\n \n-    private static function callAiService(string $filePath, string $docType): array\n+    private static function callAiService(string $filePath, string $docType, string $mode = MobileConfig::MODE_OCR): array\n     {\n         $serviceUrl = MobileConfig::aiServiceUrl();\n         $endpoint = $serviceUrl . '/api/v1/extract-invoice';\n@@ -371,6 +371,8 @@\n             CURLOPT_CONNECTTIMEOUT => 10,\n             CURLOPT_POSTFIELDS => [\n                 'doc_type' => $docType,\n+                'mode' => $mode,\n+                'ollama_url' => MobileConfig::ollamaUrl(),\n                 'file' => new \\CURLFile(\n                     $filePath,\n                     self::guessMimeType($filePath),","linesAdded":3,"linesRemoved":1,"status":"pending"},{"id":"ch-12","callId":"tool_19588df5-8562-46a3-ab1d-08a9f3fa484","path":"Plugins\\ALimpiezasMobile\\View\\MobileExpenses.html.twig","type":"edit","diffString":"--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\View\\MobileExpenses.html.twig\n+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\View\\MobileExpenses.html.twig\n@@ -32,22 +32,22 @@\n                     <label class=\"form-label\">Documento <span class=\"text-muted\" id=\"file-label-hint\">(obligatorio para OCR)</span></label>\n                     <input type=\"file\" name=\"image\" id=\"file-input-active\" class=\"visually-hidden\" tabindex=\"-1\" aria-hidden=\"true\" accept=\"image/*,.pdf,application/pdf,.heic,.heif\">\n                     <div class=\"d-grid gap-2 mb-2\">\n-                        <div class=\"file-picker-wrap\" id=\"pick-file-wrap\">\n-                            <span class=\"btn btn-outline-secondary btn-lg w-100\" id=\"pick-file-btn\">\n-                                <i class=\"fa-solid fa-folder-open me-2\"></i>Elegir archivo o galería\n-                            </span>\n-                            <input type=\"file\" id=\"file-input-gallery\" class=\"file-picker-overlay\"\n+                        <label class=\"file-picker-wrap mb-0\" id=\"pick-file-wrap\">\n+                            <input type=\"file\" id=\"file-input-gallery\" class=\"visually-hidden\"\n                                    accept=\"image/*,.pdf,application/pdf,.heic,.heif\"\n                                    aria-label=\"Elegir archivo o galería\">\n-                        </div>\n-                        <div class=\"file-picker-wrap\" id=\"pick-camera-wrap\">\n-                            <span class=\"btn btn-outline-secondary btn-lg w-100\" id=\"pick-camera-btn\">\n-                                <i class=\"fa-solid fa-camera me-2\"></i>Hacer foto\n+                            <span class=\"btn btn-outline-secondary btn-lg w-100\">\n+                                <i class=\"fa-solid fa-folder-open me-2\"></i>Elegir archivo o galería\n                             </span>\n-                            <input type=\"file\" id=\"file-input-camera\" class=\"file-picker-overlay\"\n+                        </label>\n+                        <label class=\"file-picker-wrap mb-0\" id=\"pick-camera-wrap\">\n+                            <input type=\"file\" id=\"file-input-camera\" class=\"visually-hidden\"\n                                    accept=\"image/*\" capture=\"environment\"\n                                    aria-label=\"Hacer foto con cámara\">\n-                        </div>\n+                            <span class=\"btn btn-outline-secondary btn-lg w-100\">\n+                                <i class=\"fa-solid fa-camera me-2\"></i>Hacer foto\n+                            </span>\n+                        </label>\n                     </div>\n                     <div id=\"file-selected-name\" class=\"small text-muted\">Ningún documento seleccionado</div>\n                 </div>","linesAdded":11,"linesRemoved":11,"status":"pending"},{"id":"ch-13","callId":"tool_1a47d84a-cae6-4a15-a1ee-ddee9a0ebee","path":"Plugins\\ALimpiezasMobile\\View\\MobileExpenses.html.twig","type":"edit","diffString":"--- a/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\View\\MobileExpenses.html.twig\n+++ b/C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\View\\MobileExpenses.html.twig\n@@ -61,6 +61,10 @@\n                 </div>\n             </form>\n             <div id=\"upload-result\" class=\"mt-3\"></div>\n+            <details class=\"mt-3 debug-panel-wrap\">\n+                <summary class=\"small text-muted\">Debug conexión OCR/IA</summary>\n+                <div id=\"debug-panel\" class=\"debug-panel small mt-2\"></div>\n+            </details>\n         </div>\n     </div>\n ","linesAdded":4,"linesRemoved":0,"status":"pending"}],"transcriptPath":"C:\\Users\\Paterna\\.cursor\\projects\\c-xampp-htdocs-ALimpiezas\\agent-transcripts\\736ac9a4-90f0-4491-8940-56ec149befda\\736ac9a4-90f0-4491-8940-56ec149befda.jsonl","notifyTitle":"Agente terminado","notifyBody":"Continúo el hilo: reviso los archivos del OCR móvil ya identificados y el estado de la conexión con el servicio IA.\nEnco"},"at":1782349331526}]}
