# Adjuntos y dictado

## Capturar

En el modal de creación o edición:

- Pegar una captura con Ctrl+V en Windows/Linux o ⌘V en Mac mientras el formulario tiene el foco.
- Elegir archivos, arrastrarlos a la zona de adjuntos o usar la cámara en un dispositivo táctil.
- Revisar las miniaturas antes de guardar. Quitar un archivo pendiente no lo sube a ningún sitio.
- Formatos: PNG, JPEG, WebP, GIF y PDF. Hasta 5 adjuntos por pendiente, 10 MB por archivo y 30 MB por envío.
- Los adjuntos guardados se pueden abrir en otra pestaña y eliminar mediante el botón de papelera y una segunda pulsación en «Eliminar». Las altas y bajas quedan en el historial.

Los errores de validación y de conexión conservan el texto y los archivos seleccionados mientras se mantenga abierta la página. No existe un borrador persistente de los archivos: cerrar o recargar la página descarta las selecciones que aún no se guardaron.

## Cloudflare R2

Dominio de la aplicación en producción: `https://task.tecwi.co`.
Necesitamos el nombre exacto del bucket privado, su endpoint S3, Access Key ID y Secret Access Key. Crear credenciales R2 con permiso **Object Read & Write**, limitado a ese bucket. El endpoint se copia de Cloudflare, incluyendo el sufijo de jurisdicción si corresponde; no es el dominio de la aplicación.

La integración está instalada con `league/flysystem-aws-s3-v3`. No está activada ni conectada a una cuenta real: faltan las credenciales y el bucket.

Configurar en `.env` del servidor:

```dotenv
R2_ENABLED=true
R2_ACCESS_KEY_ID=
R2_SECRET_ACCESS_KEY=
R2_BUCKET=
R2_ENDPOINT=https://ACCOUNT_ID.r2.cloudflarestorage.com
```

Usar un bucket privado y claves con permisos de lectura/escritura para ese bucket. No incluirlas en Git ni en JavaScript. Después de configurar, ejecutar `php artisan config:cache` y probar subir, visualizar y eliminar un archivo real.

El navegador envía el archivo a Laravel, que lo transmite a R2 mediante un flujo. PHP crea su archivo temporal de subida y lo elimina al terminar la petición; la aplicación **no guarda copias permanentes en el servidor ni utiliza un disco local alternativo**. La base MySQL conserva solamente nombre, ruta del objeto, tipo, tamaño y referencia a la tarjeta. Si R2 no está configurado, el formulario permite previsualizar archivos, pero bloquea el guardado con adjuntos; se puede guardar el texto retirando las selecciones.

La visualización pasa por una ruta autenticada que genera una URL R2 de lectura válida durante diez minutos. El archivo se descarga desde R2. No se publica el bucket ni se exponen las claves al cliente. Al emplear subidas desde PHP, esta implementación no necesita CORS para PUT desde el navegador.

Configurar en PHP `upload_max_filesize` de al menos 10M y `post_max_size` superior a 30M (40M recomendado para este límite), y el límite equivalente del proxy/web server. XAMPP tiene actualmente ambos valores en 40M. Comprobar los valores efectivos al publicar.

Si hay fallo de base de datos después de subir, se intenta eliminar los objetos nuevos. Si falla también esa eliminación, se registra su ruta para limpieza en el log. Conservar copias independientes de la base de datos y una política de retención de R2 acorde a las necesidades del proyecto.

## Dictado

El micrófono del teclado del teléfono funciona sobre el campo de descripción, como sobre un campo de texto normal. La web no puede pulsar ese micrófono del sistema por el usuario.

El botón «Dictar» utiliza `SpeechRecognition`/`webkitSpeechRecognition` cuando existe soporte y el contexto es seguro (HTTPS o localhost). Solicita acceso al micrófono al pulsarlo, muestra el texto provisional y añade los resultados finales a la descripción. Al guardar, espera el último resultado del reconocimiento. Se detiene al cerrar el modal o cambiar de pestaña.

La disponibilidad depende del navegador/dispositivo y puede requerir conexión a los servicios de reconocimiento del navegador. No se garantiza transcripción local u offline. La aplicación no graba ni almacena un archivo de audio. Si no hay soporte o se deniega el permiso, se mantiene la alternativa del micrófono del teclado. No se usa una API de IA de pago ni se necesita una clave para este dictado.

Si una captura nueva contiene descripción pero no título, el inicio de la descripción se utiliza como título (hasta 120 caracteres). La descripción completa se conserva; esto no realiza clasificación ni resumen mediante IA.

## Verificación

Las pruebas de Laravel usan un disco R2 simulado, comprueban validación por contenido, límites, metadatos, historial, URLs firmadas, acceso privado y limpieza ante errores. Los recorridos de navegador verifican portapapeles sintético, selección de archivos, persistencia tras rechazos y eventos de voz simulados. Falta verificar voz con un micrófono físico y R2 con credenciales reales.

Referencias: [Cloudflare R2: URLs firmadas](https://developers.cloudflare.com/r2/api/s3/presigned-urls/), [Laravel: almacenamiento](https://laravel.com/docs/12.x/filesystem), [SpeechRecognition y compatibilidad](https://developer.mozilla.org/en-US/docs/Web/API/SpeechRecognition).

### Diagnóstico en PC

El botón muestra «Iniciando…» mientras se espera el servicio y se puede volver a pulsar para cancelar. Si no comienza en 12 segundos, se recupera el botón y aparece una explicación. Los errores de permisos o conexión permanecen visibles bajo la descripción.

En la prueba automatizada del Opera instalado (135), en localhost y con un dispositivo de audio simulado, `SpeechRecognition.start()` no produjo eventos de inicio, error o fin. Esto reproduce un servicio que no responde, aunque no constituye una prueba de audio físico. Se mantiene la alternativa de dictado del sistema; para independizar el dictado del motor del navegador haría falta integrar otro servicio de transcripción.
