# Comandos artisan del proyecto

Mapa de **todos** los comandos custom en `app/Console/Commands/`, agrupados por qué los mantiene
vivos (programado, deploy, migración, tarea activa, o diagnóstico manual). Sirve para no tener que
releer el código cada vez que hace falta correr uno. Criterio de baja: `CLAUDE.md` § "Gotchas" /
comandos de un solo uso.

## Programados (`routes/console.php`)

| Comando | Cuándo | Para qué |
|---|---|---|
| `sitemap:generate` | Diario 03:00 | Regenera `sitemap.xml` por división — el contenido cambia desde el panel. |
| `notifications:prune {--read-days=} {--unread-days=} {--chunk=} {--dry-run}` | Diario 03:10 | Borra notificaciones de campanita viejas, según `config/notifications.php`. |
| `slugs:prune-history {--months=} {--chunk=} {--dry-run}` | Diario 03:20 | Borra entradas viejas y sin uso del historial de slugs (301), según `config/slugs.php`. |

## Invocados por el deploy (`deploy/deploy.sh`)

| Comando | Para qué |
|---|---|
| `slugs:sync {--model=} {--no-prune} {--dry-run}` | Reconstruye el catálogo de slugs desde pages/blogs/machines/categorías/sectores. |
| `sitemap:generate` | También se corre a mano en el deploy, no solo por cron. |
| `permissions:snapshot {--path=}` | Guarda el mapa de roles/permisos ANTES de correr el seeder, para poder diffear después. |
| `permissions:diff {--path=} {--fail-on-change} {--keep}` | Reporta qué permiso cambió el `PermissionSeeder` en este deploy (por rol). |

`db:seed --class=PermissionSeeder --force` también corre en cada deploy — no es un comando propio,
es el seeder que sí está pensado para re-correrse (ver `CLAUDE.md` § "Permisos y roles").

## Invocado por una migración

| Comando | Para qué |
|---|---|
| `media:brand-images-to-storage {--dry-run}` | Mueve `logos.url_logo*` y `languages.flag_image` de `public/images` a storage. Lo llama `Artisan::call()` desde la migración `2026_08_28_000001_move_brand_images_to_storage` — **no se puede borrar**: un `migrate:fresh` desde cero rompe si el comando no existe, aunque ya haya terminado su trabajo en producción (verificado 2026-09-18). |

## Tarea activa: importar contenido a producción en caliente

Ver `docs/IMPORTAR-CONTENIDO-EN-PROD-EN-CALIENTE.md`. Se van a volver a usar mientras esa tarea no
cierre del todo.

| Comando | Para qué |
|---|---|
| `content:export {--database=} {--out=} {--without-machinery} {--pretty}` | Exporta el contenido del editor (builder, sectores, maquinaria) de una base a un JSON. |
| `content:import {--file=} {--dry-run} {--force} {--without-machinery}` | Importa ese JSON en la base actual sin tocar los datos vivos (tickets, usuarios, etc.). `--dry-run` hace todo el trabajo y lo deshace, para ver qué pasaría. |
| `content:storage-manifest {--out=} {--missing-out=}` | Lista los archivos de storage que usa ese contenido, para copiarlos con `rsync --files-from`; también dice cuáles referencia la base y no están en disco. |
| `site:snapshot {--base=} {--out=} {--timeout=} {--division=} {--also=}` | Guarda el estado de cada URL pública (status, título, texto) — la foto de "antes" o "después" de un deploy. |
| `site:compare <antes> <despues> {--caida=50} {--todo}` | Compara dos fotos de `site:snapshot` y avisa qué se rompería al desplegar (caída de texto sospechosa, URLs perdidas). |

## Diagnóstico / reparación manual (correr cuando el problema puede volver)

No están programados ni en el deploy — se corren a mano cuando hace falta investigar o reparar algo
puntual. Todos son seguros para tirar primero con `--dry-run`.

| Comando | Para qué |
|---|---|
| `links:audit {--all} {--unpublished} {--strict}` | Reporta links internos del builder (menús, bloques, footers, headers, shared sections, rich text) que no apuntan a nada. |
| `links:repair {--dry-run} {--strict}` | Reescribe esos links rotos cuando la ruta guardada ya no sirve nada. |
| `media:audit-public-images {--prune} {--include-legacy} {--list} {--why=} {--views} {--include-backfills}` | Reporta qué archivos de `public/images` siguen en uso y por quién; con `--prune` borra los que nadie referencia. |
| `page-blocks:repair-wrappers {--dry-run}` | Repara wrappers de botón opcional guardados como array crudo del repeater. |
| `nav:sync-division-menus {--dry-run} {--division=*}` | Reconstruye el navbar de una o todas las divisiones a la estructura que sirve producción — correr si cambia el catálogo (sectores, maquinaria). |
| `storage:clean-unused {--dry-run}` | Borra archivos de storage sin referencia en la DB, en 8 carpetas: `blogs`, `machinery`, `product-sectors`, `pages`, `header`, `footer-media`, `sale-ticket`, `promo-flyers`. |

### `storage:clean-unused` — bug de tablas renombradas (fix 2026-09-28)

`$standardSources` seguía apuntando a `sale_ticket_media` (tabla dropeada,
`2026_07_06_000000_migrate_and_drop_sale_ticket_media`) y `used_machine_sale_media` (renombrada a
`used_machine_media`, `2026_06_29_000000_rename_used_machine_sale_media_to_used_machine_media`).
Con esas tablas inexistentes el comando tiraba `QueryException` **antes** de llegar al loop de
borrado — ni siquiera `--dry-run` corría. Fix: una sola entrada `'used_machine_media' =>
['media_path']`.

✅ **Verificado contra producción (2026-09-28) — la carpeta `sell-tickets/` legacy y las 5
carpetas "sin referencia" NO existen ahí.** `storage/app/public/` real en producción es
exactamente: `blogs`, `catalogue`, `divisions`, `footer-media`, `header`, `languages`,
`machinery`, `pages`, `product-sectors`, `promo-flyers`, `sale-ticket` (11 carpetas). Todo lo
demás que aparecía en el entorno local (`sell-tickets/` de 577 MB, `machine-catalogs`,
`page-media`, `company-overview-image`, `feature-icons`, `block-carousel-images`) era basura
acumulada solo en local (subidas de prueba, seeders, etc.) — **no hay tal riesgo en producción**,
el gap de `sell-tickets/` vs. `used_machine_media` documentado antes era una preocupación sobre
datos que no existen fuera de local. `catalogue/`, `divisions/` y `languages/` sí existen en
producción y siguen fuera de `$targetDirectories` (whitelist), así que `storage:clean-unused`
no las toca — ahí siguen a salvo las imágenes de logos/banderas migradas desde `public/`.

### ⚠️ Incidente 2026-09-28 — falso positivo real, comando corrido sin `--dry-run`

Con el fix de arriba ya aplicado, se corrió `storage:clean-unused` **sin** `--dry-run` en
producción y borró 45 archivos reales de `blogs/`, `pages/` y `sale-ticket/` (ULIDs de
`blogs/large/`/`blogs/thumbnails/`, imágenes sueltas legacy tipo `Blog-Industria.jpg`, y
`blogs/gallery/itg brazil/Articulo Existente_Kern.2.jpg`). Se restauraron desde el backup
automático de `spatie/laravel-backup` (`/var/backups/group-itg.com/ITG-GROUP/`, corre el 26 de
cada mes a las 02:00) vía `rsync -a --ignore-existing` (nunca pisa lo que ya está en destino) +
`chown -R www-data:www-data` después (el rsync corre como el usuario SSH, no `www-data`).

**Causa de fondo — CONFIRMADA y arreglada (2026-09-28)**: se importó el dump real de producción
del backup del 26/09 (2 días antes del incidente, `/var/backups/group-itg.com/ITG-GROUP/
2026-09-26-02-00-01.zip`) a una DB local (`itg_production_20260926`) y se corrió un escaneo
amplio de los 45 archivos contra **todas** las columnas varchar/text/json de **todas** las
tablas — no solo las que ya conocía `$standardSources`/`$jsonSources`. Resultado:

- Los 38 archivos de `blogs/` (sueltos, `gallery/`, `large/`, `thumbnails/`): **sin ninguna
  referencia en toda la BD**. Genuinamente huérfanos — el borrado ahí fue correcto. (La fila de
  `gallery_blog_images` con `Articulo Existente_Kern.2.jpg` que parecía un falso positivo solo
  existía en una BD local vieja, nunca en producción real — descartado.)
- Los 7 archivos de `pages/`: **todos** referenciados en `shared_sections.content` (JSON) —
  **falso positivo real y confirmado**. `shared_sections` (secciones reutilizables entre páginas,
  mismo esquema de bloques que `content_block_pages`) nunca se agregó a `$jsonSources` — el
  comando ni sabía que esa tabla existía.

**Fix**: agregada `'shared_sections' => ['content' => self::BUILDER_BLOCK_IMAGE_KEYS]` a
`$jsonSources`, reusando la misma lista de keys que ya usaba `content_block_pages` (ahora
extraída a la constante `BUILDER_BLOCK_IMAGE_KEYS` para no duplicarla) — mismo esquema de
bloques, misma lista de keys. Verificado con `ReflectionMethod` contra
`getReferencedFilesFromDatabase()` corriendo sobre el dump real: los 7 archivos ahora se detectan
como referenciados.

Sospecha adicional sin confirmar (no bloqueante, no es la causa de este incidente): `BlogResource/
Pages/EditBlog.php` no borra el archivo físico viejo cuando un editor reemplaza el thumbnail/imagen
grande de un blog (Filament no lo hace solo, no hay `deleteUploadedFileUsing` ni observer) — eso
explicaría por qué siguen apareciendo ULIDs huérfanos en `blogs/large/`/`blogs/thumbnails/` con el
tiempo.

**Auditoría completa post-fix (2026-09-28)**: para no depender de la memoria de a qué tablas hay
que acordarse de agregar, se corrió un barrido de **todas** las columnas varchar/text/json de
**todas** las tablas del dump real buscando cualquier mención a las 8 carpetas de
`$targetDirectories`. Resultado: todo lo que aparece ya está cubierto por
`$standardSources`/`$jsonSources` (con el fix de `shared_sections` puesto). Los únicos dos
resultados fuera de la lista conocida (`slugs.slug`, `slugs.path`) son rutas públicas
(`machinery/water-looms/detail`, `blogs/braiding-machines-growths/detail`) — confirmado con una
consulta real, no son rutas de archivo, no aplica el mismo riesgo. **No queda ningún hueco
conocido hoy.**

### Backfill manual de un solo uso, ya dado de baja

`machine-catalogues:move-to-canonical-path` movió `storage/app/public/catalogue/machinery` →
`machinery/catalogues-pdfs` (la ruta que ya usa el `FileUpload` del panel, `MachineResource.php:538`)
y reescribió `machine_catalogues.url`. Corrido en producción y borrado (2026-09-28) — su output
ya vive en la DB, no hacía falta para un `migrate:fresh`.

## Índice

Agregado a la tabla de `CLAUDE.md` § "Índice de documentación".
