Add production rules discovered during real projects

- Correct hooks-and-api.md: file hooks vs module hooks param injection,
  FK naming not always _num, CmsApi.hook Promise pattern
- Add 9 new rules to hooks-and-api.md: reserved `tipo` var, ghost modules,
  cms_uploads schema, name vs title by menuType, menuOrder, localCache
  gotcha, slug generation, uploads from hooks, CocoEmail
- Add 5 rules to modular-system.md: minified/ dir, Docker workflow,
  debug tools (?compiletwig/?pruebas), general sections deploy, controlador
- Add 2 rules to css-js-conventions.md: Vue inline conflict, Vue mount delay
- Add testing section to quick-reference.md
- Create docs/deploy-and-sync.md for production deploy and sync rules
- Promote 3 critical rules to CLAUDE.md (rules 11-13)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-24 16:17:00 +00:00
parent 2c079088ef
commit f2021361ec
6 changed files with 283 additions and 9 deletions

View File

@@ -102,6 +102,9 @@ Do NOT modify web-base files — they are shared across all projects.
8. Tailwind CSS as primary styling, custom CSS scoped with BEM when needed
9. Twig concatenation uses `~` operator: `'value=' ~ variable`
10. `enlace` (link) fields already include slashes
11. **File hooks (`hooks/*.php`) do NOT inject variables.** Always read params manually: `$params = json_decode(file_get_contents('php://input'), true) ?: [];` — Only module hooks (`modulos/*/hook.php`) receive variables directly.
12. **`tipo` is a reserved variable.** The `.htaccess` injects `tipo=barra` on every request. Never use `tipo` as a hook parameter name — it gets overwritten, causing 404s. Use `account_type`, `user_tipo`, etc.
13. **Never pass `html` via `save_module` to modules with configured builder vars** (banners, carousels, modules with images/colors set from admin). It corrupts the visual configuration. Only pass `js` and/or `css`. See [docs/deploy-and-sync.md](docs/deploy-and-sync.md).
## Documentation
@@ -114,3 +117,4 @@ Do NOT modify web-base files — they are shared across all projects.
- [docs/production-patterns.md](docs/production-patterns.md) — Real production patterns (header, zigzag, FAQ, forms)
- [docs/vue-builder-rules.md](docs/vue-builder-rules.md) — CMS-VUE rules (tabs, colorpicker, components)
- [docs/vue-builder-examples.md](docs/vue-builder-examples.md) — Vue builder examples (Banner Slideshow, etc.)
- [docs/deploy-and-sync.md](docs/deploy-and-sync.md) — Deploy to production (save_module, sync, minified regeneration)

View File

@@ -83,9 +83,17 @@ if (section) {
### CmsApi (Client-Side)
```js
// Callback
CmsApi.hook('/hooks/module_id/', { action: 'getData', id: 123 }, function(response) {
console.log(response);
});
// Promise
CmsApi.hook('/hooks/module_id/', { action: 'getData', id: 123 }).then(function(res) {
console.log(res);
}).catch(function(err) {
console.error(err);
});
```
### Cuándo usar Vue 3
@@ -123,6 +131,8 @@ createApp({
Siempre usar `'${'` y `'}'` como delimitadores Vue para evitar conflicto con Twig.
**IMPORTANTE: JS Vue SIEMPRE en `script.js`, nunca inline.** Twig también interpreta `${ }`. Si el JS con Vue está inline en `<script>` dentro de `index-base.tpl`, Twig lo corrompe al renderizar. Todo el código Vue debe ir en el archivo `script.js` del módulo.
---
## Variables Globales Disponibles
@@ -216,6 +226,12 @@ Después de cambios dinámicos: `AOS.refresh()` en JavaScript.
---
## Testing con Vue
Vue necesita 3-5 segundos para montar después de navegar a una página. El `display:none` inicial se quita cuando `checkAuth()` o el `mounted()` completan. Al testear con Playwright, esperar antes de verificar contenido Vue.
---
## Buenas prácticas
- HTML/Twig semántico

56
docs/deploy-and-sync.md Normal file
View File

@@ -0,0 +1,56 @@
# Deploy & Sync
Reglas para desplegar módulos y secciones generales a producción via MCP, y para sincronizar con el servidor.
---
## Protocolo para `save_module`
1. `save_module` con `html`, `js`, `css`**LOS 3 SIEMPRE**. Si omites uno, se borra de prod (queda 0 bytes).
2. Ejecutar `check_module` → fuerza recompilación de `index-twig.tpl`
3. Visitar `?compiletwig` en una página que use el módulo
4. Verificar que renderiza
---
## NUNCA pasar `html` a módulos con builder vars configurados
Módulos con sample data en el builder (banners, carouseles, módulos con imágenes/colores configurados desde el admin) se ROMPEN al recompilar el HTML. El `save_module` puede perder/corromper la configuración visual.
**Para estos módulos:**
- Solo pasar `js` y/o `css`, nunca `html`
- Si se rompe: `recover_git` con `list_git_log` para revertir
- Editar el HTML desde el panel admin de Acai, no por MCP
---
## Forzar regeneración de minified
Si subes solo `js`, el `minified/script-{hash}.js` NO se regenera automáticamente.
**Solución:**
1. Leer el HTML actual de prod con `get_module` (sections: `["html"]`)
2. Añadir un espacio al final
3. `save_module` con los 3 (html+espacio, js, css)
**IMPORTANTE:** Usar SIEMPRE el HTML de prod (`get_module`), NUNCA el local.
---
## General Sections (`custom-*`)
Se despliegan con `save_general_section`, no `save_module`:
- El módulo se llama `custom-{nombre_tabla}` (ej: `custom-productos`)
- Se sube con `table: "productos"`, `content`, `javascript`, `css`
---
## Sync y Versionado
### Sync from server puede borrar módulos locales
El "Sync from server" puede eliminar módulos que existen en local pero no en el servidor. Si borra `custom-header` o `custom-footer`, TODAS las páginas crashean.
**Regla:** Hacer commit ANTES de cualquier sync.
### Sync solo sube archivos modificados
Para forzar un sync de un archivo sin cambios reales, añadir un espacio al final.

View File

@@ -4,16 +4,17 @@
Hooks son archivos PHP en `hooks/` que ejecutan lógica server-side. También pueden estar dentro de un módulo en `template/estandar/modulos/<module-id>/hook.php`.
### Estructura de un Hook
### Hooks de módulo vs hooks de archivo
**IMPORTANTE:** Los hooks de **módulo** (`modulos/*/hook.php`) y los hooks de **archivo** (`hooks/*.php`) reciben parámetros de forma DIFERENTE.
#### Hooks de módulo (`modulos/MODULE_ID/hook.php`)
Los parámetros se inyectan como variables directamente:
```php
<?php
// Los parámetros se reciben como variables directamente
// Ejemplo: Si llamas hook con {param1: 100}, tendrás $param1 = 100
// Si llamas hook con {param1: 100}, tendrás $param1 = 100
$resultado = $param1 * 2;
// Retornar un array (se convierte a JSON)
return [
"success" => true,
"message" => "Valor procesado: " . $resultado,
@@ -22,6 +23,22 @@ return [
?>
```
#### Hooks de archivo (`hooks/*.php`)
Se ejecutan dentro de `function returnData() {}` via `eval()`. **NINGUNA variable del request se inyecta.** Hay que leer manualmente:
```php
<?php
// SIEMPRE hacer esto al inicio de cada hook de archivo:
$params = json_decode(file_get_contents('php://input'), true) ?: [];
$nombre = trim(@$params['nombre']);
$email = trim(@$params['email']);
return [
"success" => true,
"nombre" => $nombre
];
?>
```
### Testing Hooks
El Docker debe estar corriendo. Hacer curl al endpoint del hook:
@@ -32,6 +49,18 @@ curl http://localhost:8080/hooks/example_hook/
No usar X-Hooks-Token en desarrollo local.
### Gotchas de Hooks
#### `tipo` es variable reservada
El `.htaccess` de Acai añade `tipo=barra` a cada request. Si envías `tipo` como parámetro en un hook, se sobrescribe → 404 o comportamiento inesperado.
**Usar nombres alternativos:** `account_type`, `user_tipo`, `record_tipo`, etc.
#### Módulos fantasma interceptan hooks
Si `save_module` crea un directorio `modulos/auth_signup/hook.php`, este intercepta la URL `/hooks/auth_signup/` porque `addModulesHooksToLayout()` se ejecuta ANTES que `addFilesHooksToLayout()`.
**Solución:** Los hooks generales van en `hooks/hooks.{nombre}.php`. Nunca crear hooks como módulos. Si aparecen fantasmas, borrar el directorio.
### Cómo Llamar Hooks
**Desde HTML (recomendado para módulos):**
@@ -161,7 +190,7 @@ CmsApi::delete('productos',
- Nombres de tabla **sin** prefijo `cms_`
- Primary key siempre es `num`, nunca `id`
- Foreign keys: `categoria_num`, no `categoria_id`
- Foreign keys: el nombre es **exactamente** el definido en el schema (`.ini.php`). A veces es `categoria_num`, a veces `categoria`. Siempre consultar el schema antes de asumir
- Upload fields: no se manejan via insert/update
- Operadores: `=`, `!=`, `>`, `>=`, `<`, `<=`, `LIKE`, `IN`
@@ -170,11 +199,18 @@ CmsApi::delete('productos',
## CmsApi (JavaScript — Client-Side)
```js
// Llamar hook
// Llamar hook (callback)
CmsApi.hook('/hooks/module_id/', { param: 'value' }, function(response) {
// response es la salida del hook
});
// Llamar hook (Promise)
CmsApi.hook('/hooks/module_id/', { param: 'value' }).then(function(res) {
// res = lo que devuelve el PHP del hook
}).catch(function(err) {
// error de red
});
// Leer registros (si está expuesto via hooks)
CmsApi.get('tableName', { where: conditions }, function(records) {
// records array
@@ -413,3 +449,111 @@ if (!empty($stock)) {
return ["success" => true, "total" => $total];
?>
```
---
## Gotchas de Base de Datos
### Nombres de columnas = nombres en el schema
Los campos foreign key NO añaden `_num` automáticamente. El nombre es exactamente el definido en el `.ini.php`. Siempre consultar `DESCRIBE cms_<tabla>` o el schema antes de escribir INSERT/UPDATE.
### Campos `name` vs `title` según `menuType`
El campo principal depende del `menuType` de la tabla:
| menuType | Campo principal | Ejemplos |
|----------|----------------|----------|
| `category` | `name` | apartados, categorias_productos, productos |
| `multi` | `title` | users, localizaciones, correos, favoritos |
| `single` | no aplica | configuracion, configuracion_tienda |
Las tablas `category` generan `name` automáticamente como campo de navegación. Las `multi` generan `title` como campo scaffold por defecto.
### Orden de tablas y campos
Al crear tablas o campos, usar `menuOrder` / `order` alto (99+) para que aparezcan al final del listado admin sin alterar el orden existente.
### `cms_uploads` tiene esquema diferente
La tabla usa `createdTime` (no `createdDate`). No tiene `createdByUserNum`, `updatedDate`, ni `updatedByUserNum`. Para insertar un upload manualmente:
```php
global $TABLE_PREFIX;
$sql = "INSERT INTO ".$TABLE_PREFIX."uploads (createdTime, tableName, recordNum, fieldName, urlPath, `order`)
VALUES (NOW(), 'productos', $num, 'foto', '$urlPath', $nextOrder)";
```
### Upload fields en PHP
Incluso con una sola imagen, los uploads son arrays:
```php
$image = @$p['foto'][0]['urlPath'] ?: '';
```
---
## CocoDB Cache en Memoria
`CocoDB::localCache()` se llama en cada request. Todos los `CmsApi::get()` se cachean en memoria por hash de SQL+options. Si insertas un registro y luego haces get con la misma query exacta, devuelve el resultado cacheado (sin el nuevo registro).
**Solución:** Usar opciones diferentes en la segunda query (ej: `ignoreSchema: true` en una, sin en la otra) para que el hash sea distinto.
---
## Generación de Slug (enlace)
`CmsApi::insert()` desde hooks NO genera el slug automáticamente (el panel admin sí lo hace). Generar manualmente:
```php
$slug = strtolower(trim($name));
$slug = preg_replace('/[áàâäã]/u', 'a', $slug);
$slug = preg_replace('/[éèêë]/u', 'e', $slug);
$slug = preg_replace('/[íìîï]/u', 'i', $slug);
$slug = preg_replace('/[óòôöõ]/u', 'o', $slug);
$slug = preg_replace('/[úùûü]/u', 'u', $slug);
$slug = preg_replace('/[ñ]/u', 'n', $slug);
$slug = preg_replace('/[^a-z0-9\-]/', '-', $slug);
$slug = preg_replace('/-+/', '-', $slug);
$slug = trim($slug, '-');
$enlace = '/' . $slug . '-' . substr(uniqid(), -6) . '/';
```
---
## Uploads desde Hooks
CmsApi no gestiona uploads directamente. Para subir archivos desde un hook:
1. Recibir base64 via `php://input`
2. Decodificar y guardar en `$_SERVER['DOCUMENT_ROOT'] . '/cms/uploads/'`
3. Insertar manualmente en `cms_uploads`
```php
$params = json_decode(file_get_contents('php://input'), true) ?: [];
$data = base64_decode($params['file_b64']);
$filename = $num . '_' . time() . '.' . $extension;
$uploadsDir = $_SERVER['DOCUMENT_ROOT'] . '/cms/uploads';
file_put_contents($uploadsDir . '/' . $filename, $data);
global $TABLE_PREFIX;
$sql = "INSERT INTO ".$TABLE_PREFIX."uploads (createdTime, tableName, recordNum, fieldName, urlPath, `order`)
VALUES (NOW(), 'productos', $num, 'foto', '/cms/uploads/$filename', 1)";
mysql_query($sql);
```
---
## Email (CocoEmail)
### Requiere template en BD
`CocoEmail::send('IDENTIFICADOR', $params, $to)` busca el template en `cms_correos` por `identificador`. Si no existe → excepción "Correo no encontrado". Si `$to` tiene emails inválidos/vacíos → "No hay destinatarios válidos".
**Siempre envolver en try/catch:**
```php
try {
hook("/hooks/customEmailHeader/", ["remote" => @$_SESSION["REMOTE_URL"]]);
CocoEmail::send("ACTIVAR_CUENTA", $user, [$user["email"]]);
} catch (Exception $e) {
// Email falló pero la operación principal sigue
}
```
### Variables en templates de email
Las variables se reemplazan con `{nombre_campo}`:
```
Hola {nombre}, tu código es {codigo}
```
El segundo parámetro de `send()` es el array de datos con las variables.

View File

@@ -13,7 +13,10 @@ Modules are the visual building blocks of Acai websites. Each module lives in `t
├── index-twig.tpl # Compiled Twig output (auto-generated, do NOT edit)
├── builder.json # Compiled builder vars (auto-generated, do NOT edit)
├── style.css # Module-scoped styles
── script.js # Module JavaScript
── script.js # Module JavaScript
└── minified/
├── script-{hash}.js # JS minificado (servido al browser, do NOT edit)
└── style-{hash}.css # CSS minificado (servido al browser, do NOT edit)
```
### Template Syntax
@@ -63,7 +66,7 @@ General sections are database-backed templates used for record views, headers, f
- Access record data via the `thisrecord` variable
- Upload fields return **arrays**: `thisrecord.image[0].urlPath`
- Additional upload metadata: `info1` (alt text), `info2`, `info3`, `info4`
- Foreign key fields use `_num` suffix: `thisrecord.category_num`
- Foreign key field names match the schema exactly (may or may not have `_num` suffix — always check the `.ini.php`)
- Saved via `save_general_section()` (not `save_module()`)
- Parser type 2 = Twig (recommended), 0 = Acai legacy syntax
@@ -103,3 +106,46 @@ The `multiv2` builder field type creates repeatable groups of fields:
```
Access individual items: `record.items[0].title`, `record.items[1].image`, etc.
---
## Workflow Local (Docker)
Al editar módulos en desarrollo local con Docker, los archivos compilados no se regeneran automáticamente. Hay que copiar manualmente:
```bash
# 1. Editar HTML y JS por separado
vim modulos/MODULE_ID/index-base.tpl # Solo HTML
vim modulos/MODULE_ID/script.js # Todo el JS
# 2. Copiar para que Docker los sirva
cp modulos/MODULE_ID/index-base.tpl modulos/MODULE_ID/index.tpl
cp modulos/MODULE_ID/script.js modulos/MODULE_ID/minified/script.js
```
### Herramientas de debug
- **`?compiletwig`** — Añadir a cualquier URL. Regenera los `index-twig.tpl` pero con un pipeline diferente al auto-compile. Útil para forzar recompilación, pero puede romper en ciertos contextos. Usar con precaución.
- **`?pruebas`** — Bypass de modo mantenimiento. Añadir a cualquier URL establece `$_SESSION["pruebas"]=true`. Solo hay que hacerlo una vez por sesión.
---
## General Sections — Deploy
Las general sections se identifican como `custom-{nombre_tabla}` (ej: `custom-productos`). Se despliegan con `save_general_section`, NO con `save_module`:
- `table`: nombre de la tabla (ej: `"productos"`)
- `content`: HTML del template
- `javascript`: JS
- `css`: CSS
---
## Páginas CMS (Apartados)
### Campo `controlador` obligatorio para builder
Si una página debe renderizar módulos del builder (drag-and-drop), necesita estos campos configurados:
```
controlador = "cms/lib/plugins/builder_saas/controlador.php"
precontrolador = "cms/lib/plugins/builder_saas/controlador_tabla.php"
```
Sin estos campos, la página muestra solo la general section (`custom-apartados`) en vez de los módulos asignados.

View File

@@ -83,3 +83,11 @@
| `loop.index is odd/even` | Para layouts alternados |
| `interno` | True dentro del editor CMS |
| `thisrecord` | Registro actual (en secciones generales) |
## Testing
| Regla | Detalle |
|-------|---------|
| Hooks devuelven JSON en network | Al testear con Playwright, usar `browser_network_requests` o `fetch()` en `browser_evaluate`. El snapshot visual no muestra respuestas de hooks |
| Vue tarda en montar | Después de navegar a una página con Vue, esperar 3-5s antes de verificar contenido |
| `?pruebas` | Añadir a URL para bypass de modo mantenimiento (una vez por sesión) |