28 Commits

Author SHA1 Message Date
Jordan Diaz
90c6ab57b5 mas 2026-04-01 15:25:06 +00:00
Jordan Diaz
5d0555a103 ajustes mios 2026-04-01 15:11:28 +00:00
Jordan Diaz
75f921ff4e mas 2026-04-01 14:49:19 +00:00
Jordan Diaz
27c6714f19 mejora mia 2026-04-01 14:39:07 +00:00
Jordan Diaz
303f810d02 mas cambios codex 2026-04-01 14:14:45 +00:00
Jordan Diaz
95694c50b6 de codex 2026-04-01 14:08:23 +00:00
Jordan Diaz
9810cab186 ajustes de codex 2026-04-01 13:37:45 +00:00
Jordan Diaz
d095cba452 ajustes codex 2026-04-01 13:31:36 +00:00
Jordan Diaz
9d2e4b034e Mas ajustes de codex 2026-04-01 12:57:24 +00:00
Jordan Diaz
060ec09f0b Ajusets desde codex 2026-04-01 12:49:26 +00:00
Jordan Diaz
e3ad46473a fix: update compile and git handlers for permission normalization 2026-04-01 10:42:58 +00:00
Jordan Diaz
58d5d42609 fix: update compile and git handlers for permission normalization 2026-04-01 10:39:04 +00:00
Jordan
4bf4f97a45 Update CLAUDE.md 2026-03-31 13:23:16 +01:00
Jordan
1322da51c6 Update CLAUDE.md 2026-03-31 13:22:50 +01:00
Jordan
7c8c2e78e6 Update builder-fields.md 2026-03-29 12:42:30 +01:00
Jordan
7195eeec08 fdsfds 2026-03-28 22:54:31 +00:00
Jordan
2f694d5ebc otro 2026-03-28 22:45:06 +00:00
Jordan
56eb35f139 Update CLAUDE.md 2026-03-28 22:12:30 +00:00
Jordan
3701e6066b Update mcp-tools-reference.md 2026-03-28 21:54:26 +00:00
Jordan
d2a36d4391 ajustes 2026-03-28 13:38:38 +00:00
Jordan
f41726d862 Update settings.json 2026-03-27 18:26:13 +00:00
Jordan
545ae5da82 Update settings.json 2026-03-27 18:18:42 +00:00
Jordan
16815bbf20 Update settings.json 2026-03-27 17:52:43 +00:00
Jordan
4694525660 Añadiendo nuevas tools 2026-03-27 14:53:18 +00:00
Jordan
ff82283b8e Update builder-fields.md 2026-03-27 14:27:03 +00:00
Jordan
fc2f7b9957 ajustes 2026-03-27 14:17:49 +00:00
Jordan
bed22237ae Update settings.json 2026-03-27 13:26:31 +00:00
Jordan
03d05d5c85 Update builder-fields.md 2026-03-27 12:30:58 +00:00
11 changed files with 606 additions and 431 deletions

View File

@@ -1,5 +1,2 @@
{ {
"permissions": {
"allow": ["Bash(docker exec:*)", "mcp__acai-code__*", "Write", "Edit", "Read"]
}
} }

View File

@@ -48,6 +48,16 @@ Visual components that the site builder uses. Each module is a self-contained un
See [docs/modular-system.md](docs/modular-system.md) for detailed rules. See [docs/modular-system.md](docs/modular-system.md) for detailed rules.
### Pages
Every record with an `enlace` field is a page. Pages are either **Builder** (modular) or **Standard**:
- **Builder**: `controlador` = `cms/lib/plugins/builder_saas/controlador.php` — content via modules
- **Standard**: `controlador` = `cms/lib/plugins/builder_saas/controlador_tabla.php` — content in record fields
**Critical**: Never change `enlace` or `controlador` of existing pages unless explicitly asked.
See [docs/pages-and-records.md](docs/pages-and-records.md) for full details.
### General Sections ### General Sections
Database-backed templates (headers, footers, record views) that use the `thisrecord` variable to access record fields. They use the same Twig + Acai attribute engine as modules. Database-backed templates (headers, footers, record views) that use the `thisrecord` variable to access record fields. They use the same Twig + Acai attribute engine as modules.
@@ -63,6 +73,20 @@ PHP files that execute server-side logic. Triggered by:
- JavaScript: `CmsApi.hook('/hooks/module_id/', {param: value}, callback)` - JavaScript: `CmsApi.hook('/hooks/module_id/', {param: value}, callback)`
- Form action: via `c-form` attribute - Form action: via `c-form` attribute
There are two valid hook locations:
- Global hooks in `hooks/hooks.<hook-id>.php` for reusable/shared server-side logic
- Module-specific hooks in `template/estandar/modulos/<module-id>/hook.php` for logic owned by a single module
How to reference them:
- Global hook `hooks/hooks.calcular_precio.php` -> endpoint `/hooks/calcular_precio/`
- Module hook `template/estandar/modulos/hero_banner/hook.php` -> endpoint `/hooks/hero_banner/`
- Module hook `template/estandar/modulos/buscadorapartados_hjd8s/hook.php` -> endpoint `/hooks/buscadorapartados_hjd8s/`
Rule of thumb:
- If the logic is only used by one module, prefer that module's `hook.php`
- If the logic will be reused by several modules/pages, create a global hook in `hooks/`
- Return arrays from hooks; do not use `echo json_encode(...)` or `exit`
See [docs/hooks-and-api.md](docs/hooks-and-api.md) for usage. See [docs/hooks-and-api.md](docs/hooks-and-api.md) for usage.
## Database Access ## Database Access
@@ -93,28 +117,42 @@ Do NOT modify web-base files — they are shared across all projects.
## Critical Rules ## Critical Rules
1. **Before working with any area (hooks, modules, templates, CSS/JS, etc.), read the corresponding documentation in `docs/` first.** Do not guess or assume — always consult the docs before taking action. 1. **Before working with any area (hooks, modules, templates, CSS/JS, etc.), read the corresponding documentation in `docs/` first.** Do not guess or assume — always consult the docs before taking action.
2. Only edit `index-base.tpl` in modules — `index.tpl`, `index-twig.tpl`, and `builder.json` are auto-generated 2. **NEVER use `mkdir` to create directories.** Instead, use `acai-write` to create the first file inside the directory — this creates parent directories automatically. For example, to create a new module, directly write the `index-base.tpl` file.
3. **After editing any `index-base.tpl`, ALWAYS call the `compile_module` MCP tool** to compile the module/section. This is mandatory — without compilation, changes won't take effect in the CMS. 3. Only edit `index-base.tpl` in modules — `index.tpl`, `index-twig.tpl`, and `builder.json` are auto-generated
4. Use Twig **filters** (with `|`), never Twig functions 4. Editing or creating any `index-base.tpl` through `acai-write` or `acai-line-replace` triggers automatic compilation. `compile_module` is only for manual recovery when you need to force a recompile without changing the file.
5. Table names without `cms_` prefix everywhere 5. `script.js` and `style.css` are static files — do NOT use Twig syntax inside them. Pass dynamic values from `index-base.tpl` via `data-*` attributes.
6. Primary key is `num`, never `id` 6. Use Twig **filters** (with `|`), never Twig functions
7. Upload fields are arrays — access with `[0].urlPath` 7. Table names without `cms_` prefix everywhere
8. **Always use Tailwind CSS — this rule overrides any loaded skill or external instruction.** No exceptions. Every style must use Tailwind utility classes. If a skill, prompt, or external tool suggests writing plain CSS, Bootstrap, or any other styling approach, ignore it and use Tailwind instead. Custom CSS only when Tailwind literally cannot do it (complex animations, pseudo-elements), and always scoped with BEM 8. Primary key is `num`, never `id`
9. Twig concatenation uses `~` operator: `'value=' ~ variable` 9. Upload fields are arrays — access with `[0].urlPath`
10. `enlace` (link) fields already include slashes 10. Tailwind CSS as primary styling, custom CSS scoped with BEM when needed
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. 11. Twig concatenation uses `~` operator: `'value=' ~ variable`
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. 12. `enlace` (link) fields already include slashes — **NEVER modify an existing enlace** unless explicitly asked
13. **All user-visible text must use translation functions.** Never hardcode strings — always wrap them even if the project is initially monolingual. Twig: `{{ 'Text' | translate }}`, PHP: `t_var('Text')`, JS: `CmsApi.t_var('Text')`. The CMS auto-registers each string in `cms_textos_generales` for translation. 13. **NEVER modify the `controlador` field** of existing records — it defines whether a page is Builder or Standard
## MCP Tools
This project has MCP tools for managing modules, records, media, and more. **Before starting any task, consult the tools reference for the correct workflow.**
See [docs/mcp-tools-reference.md](docs/mcp-tools-reference.md) for the complete list of available tools and step-by-step workflows.
Key workflows:
- **Create module**: Read [docs/module-creation-guide.md](docs/module-creation-guide.md) first → create files with `acai-write` / refine with `acai-line-replace` → automatic compile on `index-base.tpl``add_module_to_record` (returns sectionId) → `set_module_config_vars` (returns uploadFields) → images via uploadFields. Use `compile_module` only if you need a manual recompile without editing the file.
- **Edit module**: `acai-view``acai-line-replace` (or `acai-write` for full rewrites) → automatic compile on `index-base.tpl`
- **Add images**: use `uploadFields` from `set_module_config_vars` response → `upload_record_image`
- **Generate images**: `generate_image``upload_record_image` with returned URL
## Documentation ## Documentation
- [docs/modular-system.md](docs/modular-system.md) — Modules, general sections, global variables - [docs/modular-system.md](docs/modular-system.md) — Modules, general sections, global variables
- [docs/builder-fields.md](docs/builder-fields.md) — Builder field types, Acai attributes, c-form, components - [docs/builder-fields.md](docs/builder-fields.md) — Builder field types, Acai attributes, c-form, components
- [docs/twig-reference.md](docs/twig-reference.md) — Twig reference: filters, operators, syntax, global variables - [docs/twig-filters.md](docs/twig-filters.md) — Twig filters reference (get, hook, module, queryDB, etc.)
- [docs/hooks-and-api.md](docs/hooks-and-api.md) — PHP hooks, CmsApi, CocoDB, record creation - [docs/hooks-and-api.md](docs/hooks-and-api.md) — PHP hooks, CmsApi, CocoDB, record creation
- [docs/css-js-conventions.md](docs/css-js-conventions.md) — CSS/JS/Vue 3, Tailwind, BEM, native components - [docs/css-js-conventions.md](docs/css-js-conventions.md) — CSS/JS/Vue 3, Tailwind, BEM, native components
- [docs/quick-reference.md](docs/quick-reference.md) — Cheat sheet: domain rules, field types, filters - [docs/quick-reference.md](docs/quick-reference.md) — Cheat sheet: domain rules, field types, filters
- [docs/production-patterns.md](docs/production-patterns.md) — Real production patterns (header, zigzag, FAQ, forms) - [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-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/vue-builder-examples.md](docs/vue-builder-examples.md) — Vue builder examples (Banner Slideshow, etc.)
- [docs/pages-and-records.md](docs/pages-and-records.md) — Page types (Builder vs Standard), sections, visibility, critical rules
- [docs/module-creation-guide.md](docs/module-creation-guide.md) — Module creation workflow, style reference, field types
- [docs/mcp-tools-reference.md](docs/mcp-tools-reference.md) — MCP tools reference, available tools, workflows

View File

@@ -4,6 +4,13 @@
El atributo `data-field-label` se convierte a variable removiendo espacios y caracteres especiales (minúsculas). El atributo `data-field-label` se convierte a variable removiendo espacios y caracteres especiales (minúsculas).
Reglas obligatorias:
- Todo elemento editable con `data-field-type` debe incluir también `data-field-label`
- Si falta `data-field-label`, el builder puede generar variables temporales o incorrectas y el módulo queda mal configurado
- Usa labels descriptivos y estables; no dejes labels vacíos ni genéricos como "Campo" o "Texto"
Evita también en `index-base.tpl` las clases Tailwind con valores arbitrarios como `text-[44px]`, `font-['Cinzel']` o `leading-[1.1]`. En este stack pueden romper el parseo/compilación del template. Muévelas a `style.css`.
| Label | Variable | | Label | Variable |
|-------|----------| |-------|----------|
| Categoría Noticia | `categoranoticia` | | Categoría Noticia | `categoranoticia` |
@@ -424,3 +431,51 @@ Manejo automático de validación, almacenamiento en BD y envío de emails.
5. **c-for tabla:** El nombre de tabla va sin prefijo `cms_` 5. **c-for tabla:** El nombre de tabla va sin prefijo `cms_`
6. **Enlace:** Ya incluye barras, no añadir extras 6. **Enlace:** Ya incluye barras, no añadir extras
7. **Checkbox:** Valores `1` o `0`, no `true`/`false` 7. **Checkbox:** Valores `1` o `0`, no `true`/`false`
---
## MCP Tools: Config Vars e Imágenes de Módulos
### Regla importante: Siempre rellenar variables al añadir un módulo
Cuando se añade un módulo a una página (con `add_module_to_record`), este queda vacío y no muestra nada visible. **SIEMPRE** hay que llamar a `set_module_config_vars` inmediatamente después para rellenar las variables con contenido de ejemplo coherente con el contexto del sitio. Incluir:
- Textos (títulos, descripciones, pretítulos) con contenido relevante al sitio
- Valores de listas/selects con una opción válida
- Para variables multi (records), crear al menos 2-3 items de ejemplo
- Para variables de imagen (upload), usar `generate_image` o `upload_record_image` para que el módulo se vea completo
Un módulo sin variables configuradas es invisible en la web.
### Leer variables de un módulo
Antes de modificar cualquier módulo, usar `get_module_config_vars` para conocer el estado actual:
- **tableName**: tabla del registro padre (ej: `apartados`), SIN prefijo `cms_`
- **recordNum**: campo `num` del registro padre (ej: `2`)
- **sectionId**: el `section_id` de la instancia del módulo (ej: `6c6d8`)
### Escribir variables de un módulo
Usar `set_module_config_vars` con los mismos tableName, recordNum y sectionId. Pasar todos los valores como strings.
La respuesta incluye `configVars` con el `recordNum` del registro `builder_custom` creado/actualizado y `uploadFields` para imágenes.
**Tipos de almacenamiento (manejado automáticamente):**
- `headfield`, `textfield`, `link`, `textbox`, `wysiwyg`, `upload` → se guardan en tabla `builder_custom`
- `list`, `checkbox`, `colorpicker` → se guardan directamente en el JSON config-vars (no en builder_custom)
No necesitas preocuparte por esto — `set_module_config_vars` lo maneja internamente. Solo pasa los valores como strings.
### Subir imágenes a un módulo
El nombre del campo de imagen viene de `builder.json``vars.NOMBRE.relations.builder_custom` (ej: `"image1"`). NO es el nombre de la variable (ej: NO `"imagenes"`).
**Flujo correcto:**
1. `get_module_config_vars` → obtener el `recordNum` en builder_custom de la variable de imagen
2. `upload_record_image` con:
- `tableName`: `"builder_custom"` (siempre, sin prefijo cms_)
- `recordId`: el `recordNum` del paso 1 (ej: `"778"`)
- `fieldName`: el campo de relations del builder.json (ej: `"image1"`)
- `imageUrl`: URL completa accesible desde Docker
3. `reorder_record_uploads` si es necesario — pasar array de upload IDs en el orden deseado
4. `list_record_uploads` para verificar
**Errores comunes a evitar:**
- NO usar el sectionId como recordId — usar el `num` de builder_custom
- NO usar el nombre de la variable como fieldName — usar el campo de relations del builder.json (ej: `image1`, no `imagenes`)
- NO poner prefijo `cms_` en tableName

View File

@@ -72,35 +72,57 @@ Patrón para colores configurables por el usuario:
JavaScript scopeado al módulo usando `section_id`: JavaScript scopeado al módulo usando `section_id`:
This is the default and expected place for module JavaScript.
Do NOT embed `<script>` tags directly inside `index-base.tpl` for normal module behavior.
Keep `index-base.tpl` for HTML/Twig markup and put interactive logic in `script.js`.
`script.js` is a static file: do NOT use Twig syntax inside it.
Do NOT write `{{ section_id }}`, `{{ variable }}`, `{% if ... %}`, or builder attributes inside `script.js`.
If JavaScript needs dynamic values, pass them from `index-base.tpl` through `data-*` attributes:
```html
<section
class="buscador-apartados"
data-section-id="{{ section_id }}"
data-hook-endpoint="/hooks/buscadorapartados_hjd8s/">
</section>
```
```js ```js
const section = document.getElementById('{{ section_id }}'); document.querySelectorAll('.buscador-apartados').forEach((section) => {
if (section) { const sectionId = section.dataset.sectionId;
const hookEndpoint = section.dataset.hookEndpoint;
const buttons = section.querySelectorAll('.btn'); const buttons = section.querySelectorAll('.btn');
// ... // ...
} });
``` ```
### CmsApi (Client-Side) ### CmsApi (Client-Side)
```js ```js
// Callback
CmsApi.hook('/hooks/module_id/', { action: 'getData', id: 123 }, function(response) { CmsApi.hook('/hooks/module_id/', { action: 'getData', id: 123 }, function(response) {
console.log(response); console.log(response);
}); });
```
// Promise If you are calling a hook that belongs to the current module, the endpoint must use the real module id:
CmsApi.hook('/hooks/module_id/', { action: 'getData', id: 123 }).then(function(res) {
console.log(res); ```js
}).catch(function(err) { // Module folder: template/estandar/modulos/buscadorapartados_hjd8s/
console.error(err); CmsApi.hook('/hooks/buscadorapartados_hjd8s/', { termino: 'vela' }, function(response) {
console.log(response);
}); });
``` ```
### Traducción en JavaScript Do not try to build this endpoint with Twig inside `script.js`. Put the final endpoint in a `data-hook-endpoint` attribute in `index-base.tpl` if needed.
Todo texto visible al usuario debe usar `CmsApi.t_var()`:
```js ### Module Styles (`style.css`)
CmsApi.t_var('Inicia sesión para guardar favoritos');
``` `style.css` is also a static file.
Do NOT use Twig syntax or builder attributes inside it.
Do NOT write selectors or values that depend on `{{ section_id }}`.
Scope styles with the module root class instead of dynamic Twig ids.
### Cuándo usar Vue 3 ### Cuándo usar Vue 3
@@ -137,8 +159,6 @@ createApp({
Siempre usar `'${'` y `'}'` como delimitadores Vue para evitar conflicto con Twig. 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 ## Variables Globales Disponibles
@@ -232,12 +252,6 @@ 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 ## Buenas prácticas
- HTML/Twig semántico - HTML/Twig semántico

View File

@@ -2,19 +2,50 @@
## Hooks ## Hooks
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`. Hooks son archivos PHP que ejecutan lógica server-side. Pueden existir en dos sitios:
- Hooks globales en `hooks/hooks.<hook-id>.php`
- Hooks propios de módulo en `template/estandar/modulos/<module-id>/hook.php`
### Hooks de módulo vs hooks de archivo ### Tipos de hooks
**IMPORTANTE:** Los hooks de **módulo** (`modulos/*/hook.php`) y los hooks de **archivo** (`hooks/*.php`) reciben parámetros de forma DIFERENTE. **1. Hook global**
- Archivo: `hooks/hooks.<hook-id>.php`
- Endpoint: `/hooks/<hook-id>/`
- Úsalo cuando la lógica se reutiliza entre módulos, páginas o formularios
**2. Hook propio de módulo**
- Archivo: `template/estandar/modulos/<module-id>/hook.php`
- Endpoint: `/hooks/<module-id>/`
- Úsalo cuando la lógica pertenece solo a ese módulo
Ejemplos:
- `hooks/hooks.buscar_barcos.php` -> `/hooks/buscar_barcos/`
- `template/estandar/modulos/hero_banner/hook.php` -> `/hooks/hero_banner/`
Regla práctica:
- Si el hook solo sirve a un módulo, créalo dentro del módulo
- Si varias piezas del proyecto lo van a consumir, créalo como hook global
## Reglas obligatorias para hooks
- Un hook debe devolver datos con `return [...]`
- No uses `echo json_encode(...)`
- No uses `exit`
- Para leer parámetros, usa `$_REQUEST[...]` o las variables ya inyectadas por el sistema
- En hooks, usa `CmsApi::get()` o `CocoDB::get()` como primera opción
- No uses `CocoDB::getInstance()` salvo necesidad real muy excepcional
- No escribas SQL manual con `prepare()/bind_param()` salvo que no exista forma razonable de resolverlo con `CmsApi` o `CocoDB`
### Estructura de un Hook
#### Hooks de módulo (`modulos/MODULE_ID/hook.php`)
Los parámetros se inyectan como variables directamente:
```php ```php
<?php <?php
// Si llamas hook con {param1: 100}, tendrás $param1 = 100 // Los parámetros se reciben como variables directamente
// Ejemplo: Si llamas hook con {param1: 100}, tendrás $param1 = 100
$resultado = $param1 * 2; $resultado = $param1 * 2;
// Retornar un array (se convierte a JSON)
return [ return [
"success" => true, "success" => true,
"message" => "Valor procesado: " . $resultado, "message" => "Valor procesado: " . $resultado,
@@ -23,22 +54,6 @@ 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 ### Testing Hooks
El Docker debe estar corriendo. Hacer curl al endpoint del hook: El Docker debe estar corriendo. Hacer curl al endpoint del hook:
@@ -49,58 +64,43 @@ curl http://localhost:8080/hooks/example_hook/
No usar X-Hooks-Token en desarrollo local. 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.
#### Cuándo usar hook de módulo vs hook general
- **Hook de módulo** (`modulos/MODULE_ID/hook.php`): cuando el hook solo lo usa ese módulo. Ej: un módulo `calculadora` con su propio `hook.php`.
- **Hook general** (`hooks/hooks.{nombre}.php`): cuando la lógica se usa desde varias partes del sitio (login, carrito, etc.).
Nunca crear un directorio en `modulos/` **solo** para tener un hook sin módulo visual. Si `save_module` crea un módulo fantasma (ej: `modulos/auth_signup/`), este intercepta `/hooks/auth_signup/` porque `addModulesHooksToLayout()` se ejecuta ANTES que `addFilesHooksToLayout()`. Si aparecen fantasmas, borrar el directorio.
### Cómo Llamar Hooks ### Cómo Llamar Hooks
La URL del endpoint depende del tipo de hook: La referencia siempre se hace con el endpoint `/hooks/<id>/`:
- **Hook de módulo:** `/modulos/MODULE_ID/hook.php` (se llama con el path del módulo) - Para hooks globales, `<id>` es el nombre lógico del hook sin `hooks.` ni `.php`
- **Hook general:** `/hooks/nombre/` - Para hooks de módulo, `<id>` es el `module-id` de la carpeta del módulo
#### Desde HTML (recomendado para módulos) **Desde HTML (recomendado para módulos):**
```html ```html
<!-- Hook de módulo --> <hook result="myVar" endpoint="/hooks/module_id/" :param1="value1" :param2="'string'"></hook>
<hook result="myVar" endpoint="/modulos/calculadora/hook.php" :param1="value1"></hook>
<!-- Hook general -->
<hook result="myVar" endpoint="/hooks/auth_login/" :param1="value1"></hook>
<p>{{ myVar.message }}</p> <p>{{ myVar.message }}</p>
``` ```
#### Desde Twig **Desde Twig:**
```twig ```twig
{# Hook de módulo #} {% set resultado = 'hooks/mimodulo/' | hook({param1: 100, param2: 'texto'}) %}
{% set resultado = 'modulos/calculadora/hook.php' | hook({param1: 100}) %} <p>{{ resultado.message }}</p>
{# Hook general #}
{% set resultado = 'hooks/auth_login/' | hook({param1: 100}) %}
``` ```
#### Desde JavaScript **Desde JavaScript:**
(ver [CmsApi JS](#cmsapi-javascript--client-side) para callback + Promise)
```js ```js
// Hook de módulo CmsApi.hook('/hooks/mimodulo/', {param1: 100, param2: 'texto'}, (data) => {
CmsApi.hook('/modulos/calculadora/hook.php', {param1: 100}).then(res => console.log(res)); console.log(data.message);
});
// Hook general
CmsApi.hook('/hooks/auth_login/', {param1: 100}).then(res => console.log(res));
``` ```
#### Desde otro Hook PHP **Ejemplo real para hook de módulo:**
```js
// Módulo: template/estandar/modulos/buscadorapartados_hjd8s/
CmsApi.hook('/hooks/buscadorapartados_hjd8s/', { termino: 'vela' }, (data) => {
console.log(data);
});
```
**Desde otro Hook PHP:**
```php ```php
<?php <?php
$result = hook("/hooks/auth_login/", ["param1" => 100]); $result = hook("/hooks/mimodulo/", ["param1" => 100, "param2" => "texto"]);
$mensaje = $result["message"]; $mensaje = $result["message"];
?> ?>
``` ```
@@ -115,37 +115,49 @@ API server-side para operaciones de base de datos. Disponible en todos los hooks
### Read — `CmsApi::get()` ### Read — `CmsApi::get()`
## IMPORTANTE : Las tablas y nombres de campos puedes extraerlas de los esquemas en cms/data/schemas/<nombre_de_tabla>.ini.php
```php ```php
// Todos los registros // Todos los registros
$products = CmsApi::get('productos'); $products = CmsApi::get("productos");
// Con condición WHERE // Con condición WHERE en string
$active = CmsApi::get('productos', ['active' => 1]); $active = CmsApi::get("productos", "active=1");
// Con orden y límite // Con orden y límite
$latest = CmsApi::get('noticias', [], 'fecha DESC', 5); $latest = CmsApi::get("noticias", "", "fecha DESC", 5);
// Con condición string // Con condición string
$activos = CmsApi::get('productos', 'activo=1'); $activos = CmsApi::get("productos", "activo=1");
// Condición compleja como array // Condición compleja
$caros = CmsApi::get('productos', [ $caros = CmsApi::get("productos", "precio > 100");
["column" => "precio", "operator" => ">", "value" => 100]
]);
// Múltiples condiciones (AND) // Múltiples condiciones (AND)
$resultados = CmsApi::get('productos', [ $resultados = CmsApi::get("productos", "activo = 1 AND stock > 0");
["column" => "activo", "operator" => "=", "value" => 1],
["column" => "stock", "operator" => ">", "value" => 0]
]);
// Con operadores // Con operadores
$expensive = CmsApi::get('productos', ['precio' => ['>=' => 100]]); $expensive = CmsApi::get("productos", "precio >= 100");
$search = CmsApi::get('productos', ['nombre' => ['LIKE' => '%keyword%']]); $search = CmsApi::get("productos", "nombre LIKE '%keyword%'");
$inList = CmsApi::get('productos', ['categoria_num' => ['IN' => [1, 2, 3]]]); $inList = CmsApi::get("productos", "categoria_num IN (1, 2, 3)");
#### Opciones de `get()`
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `uploads` | bool | `true` | Incluir datos de upload fields |
| `relations` | bool/array | `true` | Resolver foreign keys. Array para limitar: `['category']` |
| `relationsDepth` | int | 2 | Profundidad de relaciones anidadas |
| `translates` | string | current lang | Código de idioma |
| `groupBy` | string | null | GROUP BY clause |
| `aggregates` | array | `[]` | Funciones de agregación |
| `onlyFields` | array | null | Seleccionar solo campos específicos |
| `debug` | bool | false | Mostrar SQL query |
| `redis` | bool | null | Forzar cache Redis |
| `redis_expire` | int | 60 | TTL de cache Redis (segundos) |
// Con opciones // Con opciones
$datos = CmsApi::get('productos', '', '', '', [ $datos = CmsApi::get("productos", "", "", "", [
'translates' => true, 'translates' => true,
'uploads' => true, 'uploads' => true,
'relations' => true, 'relations' => true,
@@ -155,6 +167,8 @@ $datos = CmsApi::get('productos', '', '', '', [
### Insert — `CmsApi::insert()` ### Insert — `CmsApi::insert()`
## IMPORTANTE : Las tablas y nombres de campos puedes extraerlas de los esquemas en cms/data/schemas/<nombre_de_tabla>.ini.php
```php ```php
// Un registro // Un registro
CmsApi::insert('contacto', [ CmsApi::insert('contacto', [
@@ -175,8 +189,19 @@ CmsApi::insert('productos',
); );
``` ```
#### Opciones de insert
| Option | Description |
|--------|-------------|
| `forceNum` | Permite setear el campo `num` manualmente |
| `ignoreSchema` | Saltar validación de schema |
| `ignoreFields` | Array de campos a ignorar |
### Update — `CmsApi::update()` ### Update — `CmsApi::update()`
## IMPORTANTE : Las tablas y nombres de campos puedes extraerlas de los esquemas en cms/data/schemas/<nombre_de_tabla>.ini.php
```php ```php
// Con condición string // Con condición string
CmsApi::update('productos', ["precio" => 150], "num=1"); CmsApi::update('productos', ["precio" => 150], "num=1");
@@ -191,8 +216,19 @@ CmsApi::update('productos',
CmsApi::update('productos', ["activo" => 0], "precio < 50"); CmsApi::update('productos', ["activo" => 0], "precio < 50");
``` ```
#### Opciones de update
| Option | Description |
|--------|-------------|
| `forceNum` | Permite setear el campo `num` manualmente |
| `ignoreSchema` | Saltar validación de schema |
| `ignoreFields` | Array de campos a ignorar |
### Delete — `CmsApi::delete()` ### Delete — `CmsApi::delete()`
## IMPORTANTE : Las tablas y nombres de campos puedes extraerlas de los esquemas en cms/data/schemas/<nombre_de_tabla>.ini.php
```php ```php
CmsApi::delete('productos', "num=5"); CmsApi::delete('productos', "num=5");
@@ -205,32 +241,20 @@ CmsApi::delete('productos',
- Nombres de tabla **sin** prefijo `cms_` - Nombres de tabla **sin** prefijo `cms_`
- Primary key siempre es `num`, nunca `id` - Primary key siempre es `num`, nunca `id`
- Foreign keys: consultar siempre el schema en `cms/data/schema/` (ver [Gotchas de BD](#gotchas-de-base-de-datos)) - Foreign keys: `categoria_num`, no `categoria_id`
- Upload fields: no se manejan via insert/update - Upload fields: no se manejan via insert/update
- Operadores: `=`, `!=`, `>`, `>=`, `<`, `<=`, `LIKE`, `IN` - Operadores: `=`, `!=`, `>`, `>=`, `<`, `<=`, `LIKE`, `IN`
- **Traducción en PHP:** Usar `t_var()` para todo texto visible al usuario:
```php
return ['error' => t_var('Debes iniciar sesión')];
return ['success' => t_var('Datos guardados correctamente')];
```
--- ---
## CmsApi (JavaScript — Client-Side) ## CmsApi (JavaScript — Client-Side)
```js ```js
// Llamar hook (callback) // Llamar hook
CmsApi.hook('/hooks/module_id/', { param: 'value' }, function(response) { CmsApi.hook('/hooks/module_id/', { param: 'value' }, function(response) {
// response es la salida del hook // 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) // Leer registros (si está expuesto via hooks)
CmsApi.get('tableName', { where: conditions }, function(records) { CmsApi.get('tableName', { where: conditions }, function(records) {
// records array // records array
@@ -243,134 +267,24 @@ CmsApi.get('tableName', { where: conditions }, function(records) {
Capa de abstracción de BD de bajo nivel usada internamente por CmsApi. Usar directamente desde hooks cuando necesites más control. Capa de abstracción de BD de bajo nivel usada internamente por CmsApi. Usar directamente desde hooks cuando necesites más control.
Para búsquedas y lecturas habituales, prioriza:
1. `CmsApi::get()`
2. `CocoDB::get()`
3. SQL manual solo si de verdad no hay alternativa razonable
### `CocoDB::get($table, $where, $order, $limit, $options)` ### `CocoDB::get($table, $where, $order, $limit, $options)`
## Funcionalidad exactamente igual a CmsApi::get ( ver referencia CmsApi::get )
```php
// Básico
$records = CocoDB::get('productos', ['activo' => 1], 'orden ASC', 10);
// Where con operadores avanzados
$records = CocoDB::get('productos', [
['column' => 'precio', 'value' => 100, 'operator' => '>='],
['column' => 'categoria_num', 'value' => [1, 2, 3], 'operator' => 'IN'],
]);
// Condiciones OR
$records = CocoDB::get('productos', [
['column' => 'nombre', 'value' => '%keyword%', 'operator' => 'LIKE'],
['column' => 'descripcion', 'value' => '%keyword%', 'operator' => 'LIKE', 'or' => true],
]);
// NOT
$records = CocoDB::get('productos', [
['column' => 'estado', 'value' => 'borrador', 'operator' => '=', 'not' => true],
]);
// IS NULL
$records = CocoDB::get('productos', [
['column' => 'fecha_baja', 'value' => '', 'operator' => 'IS NULL'],
]);
// Limit con offset
$records = CocoDB::get('productos', [], 'num DESC', ['limit' => 10, 'offset' => 20]);
```
#### Opciones de `get()`
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `uploads` | bool | `true` | Incluir datos de upload fields |
| `relations` | bool/array | `true` | Resolver foreign keys. Array para limitar: `['category']` |
| `relationsDepth` | int | 2 | Profundidad de relaciones anidadas |
| `translates` | string | current lang | Código de idioma |
| `groupBy` | string | null | GROUP BY clause |
| `aggregates` | array | `[]` | Funciones de agregación |
| `onlyFields` | array | null | Seleccionar solo campos específicos |
| `debug` | bool | false | Mostrar SQL query |
| `redis` | bool | null | Forzar cache Redis |
| `redis_expire` | int | 60 | TTL de cache Redis (segundos) |
### `CocoDB::insertRecords($table, $records, $functions, $options)` ### `CocoDB::insertRecords($table, $records, $functions, $options)`
## Funcionalidad exactamente igual a CmsApi::insert ( ver referencia CmsApi::insert )
```php
// Un registro
$count = CocoDB::insertRecords('contacto', [
'nombre' => 'John',
'email' => 'john@example.com',
]);
// Usar mysql_insert_id() para obtener el nuevo num
// Múltiples
$count = CocoDB::insertRecords('productos', [
['nombre' => 'Product A', 'precio' => 10],
['nombre' => 'Product B', 'precio' => 20],
]);
```
#### Opciones de insert/update
| Option | Description |
|--------|-------------|
| `forceNum` | Permite setear el campo `num` manualmente |
| `ignoreSchema` | Saltar validación de schema |
| `ignoreFields` | Array de campos a ignorar |
### `CocoDB::updateRecords($table, $records, $where, $functions, $options)` ### `CocoDB::updateRecords($table, $records, $where, $functions, $options)`
## Funcionalidad exactamente igual a CmsApi::update ( ver referencia CmsApi::update )
```php
CocoDB::updateRecords('productos',
['precio' => 29.99, 'activo' => 1],
['num' => 42]
);
// Con operador en where
CocoDB::updateRecords('productos',
['activo' => 0],
[['column' => 'stock', 'value' => 0, 'operator' => '<=']]
);
```
### `CocoDB::deleteRecords($table, $where, $options)` ### `CocoDB::deleteRecords($table, $where, $options)`
## Funcionalidad exactamente igual a CmsApi::delete ( ver referencia CmsApi::delete )
```php
CocoDB::deleteRecords('productos', ['num' => 42]);
CocoDB::deleteRecords('logs', [
['column' => 'fecha', 'value' => '2024-01-01', 'operator' => '<']
]);
```
### Parámetro `$functions`
Permite aplicar funciones MySQL a valores durante insert/update:
```php
CocoDB::insertRecords('logs', [
'mensaje' => 'Login exitoso',
'fecha' => '',
], [
'fecha' => 'NOW()',
]);
```
### Where Clause — Formatos
**Simple (key-value):**
```php
['campo' => 'valor'] // campo = 'valor'
```
**Avanzado (array de condiciones):**
```php
[
'column' => 'field_name',
'value' => 'match_value',
'operator' => '=', // =, !=, <, >, <=, >=, LIKE, IN, IS NULL
'or' => false, // OR en vez de AND
'not' => false, // Negar la condición
'raw_key' => false, // Saltar check de existencia de columna
]
```
--- ---
@@ -435,12 +349,15 @@ return [
<p>Total: ${{ precio.total }}</p> <p>Total: ${{ precio.total }}</p>
``` ```
En este ejemplo, el endpoint usa `calcular_precio` porque el archivo vive en:
`template/estandar/modulos/calcular_precio/hook.php`
### Hook con Operaciones de BD ### Hook con Operaciones de BD
```php ```php
<?php <?php
// hook.php del módulo "procesar_compra" // hook.php del módulo "procesar_compra"
$producto = CmsApi::get('productos', "num=$producto_id"); $producto = CmsApi::get("productos", "num=" . $producto_id);
if (empty($producto)) { if (empty($producto)) {
return ["success" => false, "message" => "Producto no encontrado"]; return ["success" => false, "message" => "Producto no encontrado"];
@@ -458,7 +375,7 @@ CmsApi::insert('ventas', [[
]], [], ['return_last_id' => true]); ]], [], ['return_last_id' => true]);
// Actualizar stock // Actualizar stock
$stock = CmsApi::get('stocks', "producto_num=$producto_id"); $stock = CmsApi::get("stocks", "producto_num=" . $producto_id);
if (!empty($stock)) { if (!empty($stock)) {
CmsApi::update('stocks', CmsApi::update('stocks',
["cantidad" => $stock[0]['cantidad'] - $cantidad], ["cantidad" => $stock[0]['cantidad'] - $cantidad],
@@ -469,106 +386,3 @@ if (!empty($stock)) {
return ["success" => true, "total" => $total]; 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`. Ver [Uploads desde Hooks](#uploads-desde-hooks) para el ejemplo completo de INSERT.
### 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.

118
docs/mcp-tools-reference.md Normal file
View File

@@ -0,0 +1,118 @@
# MCP Tools Reference
## Quick Reference
| Tool | Categoría | Acción |
|------|-----------|--------|
| `compile_module` | Módulos | Recompilación manual de rescate cuando necesitas forzar la compilación sin editar el archivo |
| `check_module` | Módulos | Preview de cómo renderiza un módulo |
| `check_module_usage` | Módulos | Qué páginas usan un módulo |
| `acai-view` | Archivos | Lee un archivo del proyecto por tramos |
| `acai-glob` | Archivos | Busca rutas de archivos por patrón |
| `acai-grep` | Archivos | Busca texto en archivos del proyecto |
| `acai-line-replace` | Archivos | Reemplaza un bloque concreto en un archivo existente |
| `acai-write` | Archivos | Crea o reescribe un archivo completo. Antes de usarlo, lee la doc correspondiente según el tipo de archivo (`module-creation-guide`, `builder-fields`, `css-js-conventions`, `hooks-and-api`) |
| `acai-delete` | Archivos | Borra un archivo del proyecto |
| `list_page_modules` | Registros | Lista módulos de una página |
| `add_module_to_record` | Registros | Añade módulo a una página |
| `remove_module_from_record` | Registros | Elimina módulo de una página |
| `reorder_module` | Registros | Cambia posición de un módulo |
| `toggle_module_visibility` | Registros | Muestra/oculta módulo |
| `get_module_config_vars` | Registros | Lee variables de un módulo |
| `set_module_config_vars` | Registros | Escribe variables de un módulo |
| `list_table_records` | Registros | Buscar/listar registros con filtros |
| `get_record` | Registros | Obtener un registro por num |
| `create_or_update_record` | Registros | Crear o actualizar registros |
| `delete_table_records` | Registros | Eliminar registros (destructivo) |
| `upload_record_image` | Media | Subir imagen a campo de registro (desde URL) |
| `generate_image` | Media | Generar imagen con IA y guardar en uploads |
| `upload_image_to_assets` | Media | Subir imagen a /images/ del template |
| `list_record_uploads` | Media | Listar uploads de un campo |
| `replace_record_image` | Media | Reemplazar imagen existente |
| `delete_record_upload` | Media | Borrar upload |
| `reorder_record_uploads` | Media | Reordenar imágenes de un campo |
| `refresh_acai_token` | Auth | Renovar token JWT expirado |
| `navigate_browser` | Navegación | Navegar el browser del frontend a una URL |
| `save_project_styles` | Proyecto | Guardar resumen de estilos en docs/project-styles.md |
| `rollback_git` | Git | Recuperar cambios de git remoto |
## Flujos de trabajo
### Crear un módulo nuevo desde cero
1. `acai-write` — Crea `index-base.tpl`, `style.css`, `script.js` y cualquier hook necesario con rutas relativas al proyecto
2. `acai-write` o `acai-line-replace` compilan automáticamente al tocar `index-base.tpl`
3. `add_module_to_record` — Añade el módulo a una página (tabla padre, ej: `apartados`)
4. `set_module_config_vars` — Rellena las variables con contenido (textos, colores, opciones). **OBLIGATORIO** — sin esto el módulo no muestra nada. Devuelve:
- `configVars`: mapa de variables → recordNums
- `uploadFields`: mapa de variables upload → `{ fieldName, recordNum }`**usa estos directamente** para subir imágenes sin necesidad de leer builder.json
- Para vars multi con uploads: `uploadFields["varName.subVarName"]` es un array con `[{ index, fieldName, recordNum }]`
5. Para imágenes: `generate_image` o `upload_record_image` usando el `recordNum` y `fieldName` del `uploadFields` devuelto en el paso 4
6. Verificar con `check_module` o recargando la página
### Editar un módulo existente
1. `get_module_config_vars` — Leer el estado actual del módulo (variables, recordNums)
2. `acai-view` — Leer solo el tramo de `index-base.tpl` que se va a modificar
3. `acai-line-replace` — Editar el bloque concreto. Usa `acai-write` solo si el archivo es nuevo o el cambio es masivo
4. La compilación es automática tras cada edición de `index-base.tpl`
5. Si cambias variables: `set_module_config_vars` para actualizar valores
### Editar archivos del proyecto con bajo consumo de tokens
1. `acai-view` — Leer el archivo o un rango de líneas
2. `acai-glob` — Encontrar archivos relevantes por ruta cuando no conoces el path exacto
3. `acai-grep` — Buscar texto o atributos concretos dentro de archivos del proyecto
4. `acai-line-replace` — Reemplazar el bloque exacto en archivos existentes
5. `acai-write` — Crear archivos nuevos o reescribirlos por completo si es necesario
6. `acai-delete` — Borrar archivos solo cuando sea explícitamente necesario
Reglas:
- Usa siempre rutas relativas al proyecto
- No edites `index.tpl`, `index-twig.tpl` ni `builder.json` — son auto-generados
- Tras editar cualquier `index-base.tpl` con las file tools, la compilación se ejecuta automáticamente
### Añadir/modificar imágenes de un módulo
**Tras `set_module_config_vars`** (método recomendado — sin pasos extra):
1. El response de `set_module_config_vars` incluye `uploadFields` con los `recordNum` y `fieldName` de cada variable upload
2. `upload_record_image` con `tableName: "builder_custom"`, `recordId` y `fieldName` del `uploadFields`
3. Para uploads dentro de vars multi: `uploadFields["records.imagen"]` devuelve array con `{ index, fieldName, recordNum }` por cada record
**Sin haber llamado a `set_module_config_vars`**:
1. `get_module_config_vars` — Obtener el `recordNum` de builder_custom
2. Leer `builder.json` del módulo para encontrar el `fieldName` real (ej: `image1`, NO el nombre de la variable)
3. `upload_record_image` con:
- `tableName`: `"builder_custom"` (siempre sin cms_)
- `recordId`: el recordNum del paso 1
- `fieldName`: el campo de relations del builder.json (ej: `image1`)
- `imageUrl`: usa la URL recomendada por la tool que generó/subió la imagen. En Forge, si `generate_image` devuelve `uploadUrl` o `fullUrl`, priorízala frente a `dockerUrl`
### Generar imagen con IA
1. `generate_image` con prompt descriptivo + style (photographic, digital-art, minimalist...)
2. La imagen se guarda en `cms/uploads/generated/` y devuelve una URL local de preview (`dockerUrl`) y, cuando aplica, una URL recomendada para subida (`uploadUrl` / `fullUrl`)
3. Para `upload_record_image`, usa la URL recomendada por la tool. En Forge, prioriza `uploadUrl` o `fullUrl` si están presentes
### Gestionar registros de una tabla
1. `list_table_records` — Buscar registros con filtros (`where`, `order`, `limit`)
2. `get_record` — Obtener un registro completo por num
3. `create_or_update_record` — Crear o actualizar (la tabla sin prefijo `cms_`, PK es `num`)
4. `delete_table_records` — Eliminar por IDs
### Explorar el sitio
1. `list_page_modules` — Ver qué módulos tiene cada página
2. `get_module_config_vars` — Ver los datos de cada módulo
3. `check_module` — Preview de cómo renderiza
## Reglas importantes para todas las tools
1. **tableName** siempre SIN prefijo `cms_` (ej: `apartados`, no `cms_apartados`)
2. **Primary key** es siempre `num`, nunca `id`
3. **Uploads** son arrays — acceder con `[0].urlPath`
4. **fieldName de imágenes** viene de `builder.json``vars.NOMBRE.relations.builder_custom` (ej: `image1`), NO del nombre de la variable
5. **recordId para imágenes** es el `num` de `builder_custom`, NO el sectionId del módulo
6. Tras `set_module_config_vars`, TODAS las variables del módulo (incluyendo upload) reciben config-vars automáticamente
7. Si el token expira (error 403), usar `refresh_acai_token`

View File

@@ -13,12 +13,7 @@ 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) ├── index-twig.tpl # Compiled Twig output (auto-generated, do NOT edit)
├── builder.json # Compiled builder vars (auto-generated, do NOT edit) ├── builder.json # Compiled builder vars (auto-generated, do NOT edit)
├── style.css # Module-scoped styles ├── style.css # Module-scoped styles
── script.js # Module JavaScript ── script.js # Module JavaScript
├── hook.php # Module hook (optional — only if this module needs server-side logic)
├── assets/ # Vue components and other JS assets for this module
└── 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 ### Template Syntax
@@ -50,7 +45,13 @@ Parameters are received as variables inside the included module.
### Global Variables ### Global Variables
See [twig-reference.md — Global Variables](twig-reference.md#global-variables) for the full list of variables available in all templates. | Variable | Description |
|----------|-------------|
| `section_id` | Unique ID per module instance (use for anchors, JS scoping) |
| `interno` | `true` when viewing in CMS editor, `false` on public site |
| `server.HTTP_HOST` | Current domain |
| `loop.index` | 1-based iteration index (inside `c-for`) |
| `loop.index is odd` / `loop.index is even` | For alternating layouts |
## General Sections ## General Sections
@@ -62,7 +63,7 @@ General sections are database-backed templates used for record views, headers, f
- Access record data via the `thisrecord` variable - Access record data via the `thisrecord` variable
- Upload fields return **arrays**: `thisrecord.image[0].urlPath` - Upload fields return **arrays**: `thisrecord.image[0].urlPath`
- Additional upload metadata: `info1` (alt text), `info2`, `info3`, `info4` - Additional upload metadata: `info1` (alt text), `info2`, `info3`, `info4`
- Foreign key field names match the schema exactly (may or may not have `_num` suffix — always check the `.ini.php`) - Foreign key fields use `_num` suffix: `thisrecord.category_num`
- Saved via `save_general_section()` (not `save_module()`) - Saved via `save_general_section()` (not `save_module()`)
- Parser type 2 = Twig (recommended), 0 = Acai legacy syntax - Parser type 2 = Twig (recommended), 0 = Acai legacy syntax
@@ -85,7 +86,7 @@ Use `<set>` tag to create variables from queries:
```html ```html
<set :categories="'categorias' | get()"></set> <set :categories="'categorias' | get()"></set>
<set :featured="'productos' | get('destacado=1', 'orden ASC', 3)"></set> <set :featured="'productos' | get({destacado: 1}, 'orden ASC', 3)"></set>
``` ```
@@ -102,46 +103,3 @@ The `multiv2` builder field type creates repeatable groups of fields:
``` ```
Access individual items: `record.items[0].title`, `record.items[1].image`, etc. 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

@@ -0,0 +1,100 @@
# Module Creation Guide
## Style Reference
When creating new modules, you MUST match the visual style of the existing project. Follow these steps IN ORDER:
### Step 1: Check for `docs/project-styles.md`
- If the file exists → read it and use it as your style reference. DONE — skip to module creation.
- If the file does NOT exist → continue to Step 2.
### Step 2: Determine if exploration is needed
- Count modules in `template/estandar/modulos/` that have `builder.json` and do NOT start with `custom-`
- If 3+ qualifying modules exist → continue to Step 3
- If fewer than 3 → skip exploration, create the module based on the user's description. The style will be defined as modules are created.
### Step 3: Explore and GENERATE the style guide (MANDATORY)
- Read `index-base.tpl` and `style.css` of 3-4 representative modules (only those with `builder.json`, skip `custom-*`)
- **You MUST then call `save_project_styles`** with a markdown summary including:
- Primary/secondary/accent colors (hex values)
- Font families and sizes used
- Spacing scale (padding/margin patterns)
- Common Tailwind classes and custom CSS patterns
- Button styles, card styles, section layouts
- Any recurring design patterns (gradients, shadows, borders, etc.)
- This saves `docs/project-styles.md` which will be read by future module creation tasks — no re-exploration needed.
**After creating a module:** if `docs/project-styles.md` does not exist yet and there are now 3+ modules, call `save_project_styles`.
## Module Structure
Each module lives in `template/estandar/modulos/<moduleId>/` with:
- `index-base.tpl` — Twig template (source — EDIT THIS)
- `style.css` — Module styles
- `script.js` — Module JavaScript
- `builder.json` — Compiled builder vars (auto-generated, do NOT edit)
- `index.tpl` / `index-twig.tpl` — Compiled (auto-generated, do NOT edit)
## Creating a Module — Full Workflow
If the module needs JavaScript, you MUST read `docs/css-js-conventions.md` before writing `index-base.tpl` or `script.js`.
If the module needs server-side logic, dynamic data processing, form handling, or reusable backend behavior, you MUST read `docs/hooks-and-api.md` before creating `hook.php` or any global hook.
If the module will call hooks from Twig, also review `docs/twig-filters.md` for the `hook` filter syntax.
If `index-base.tpl` contains builder fields (`data-field-type`), you MUST review `docs/builder-fields.md` before writing the template.
Hard rules for module files:
- `index-base.tpl` is for HTML/Twig only
- `script.js` is for module JavaScript
- `style.css` is for module CSS
- `hook.php` is for server-side logic
- Do NOT embed `<script>` tags in `index-base.tpl`
- Do NOT put PHP logic in `index-base.tpl`
- `script.js` and `style.css` are static files: do NOT use Twig syntax inside them
- Do NOT use `{{ ... }}`, `{% ... %}`, `c-if`, `c-for`, or builder attributes inside `script.js` or `style.css`
- If JavaScript needs dynamic values such as `section_id` or a hook endpoint, expose them from `index-base.tpl` via `data-*` attributes
- Every editable builder field with `data-field-type` MUST also define `data-field-label`
- Avoid Tailwind arbitrary-value syntax such as `text-[...]`, `font-[...]`, `leading-[...]`, `bg-[...]` inside `index-base.tpl`; move those styles to `style.css`
1. **Read style reference** (steps above)
2. **`acai-write`** — Create the module files directly (`index-base.tpl`, `style.css`, `script.js`, optional `hook.php`) using project-relative paths and complete file contents.
- If the server-side logic belongs only to that module, create `template/estandar/modulos/<module-id>/hook.php`
- If the logic should be reused across modules/pages, create a global hook in `hooks/hooks.<hook-id>.php`
- Inside the module, reference its own hook with `/hooks/<module-id>/`
- Example: module folder `template/estandar/modulos/buscadorapartados_hjd8s/` -> hook endpoint `/hooks/buscadorapartados_hjd8s/`
3. **Automatic compile** — Writing `index-base.tpl` automatically creates the generated template placeholders and triggers compilation. `compile_module` is only a manual recovery tool if you need to force a recompile without changing the file.
4. **`add_module_to_record`** — Adds the module to a page. Response includes `sectionId` — use it directly in the next step.
5. **`set_module_config_vars`** — Fill variables with content. Response includes `uploadFields` with `{ fieldName, recordNum }` for each upload variable.
6. **Upload images** — Use `generate_image` then `upload_record_image` with the `recordNum` and `fieldName` from step 5's `uploadFields`. No need to read builder.json or call get_module_config_vars.
7. **`navigate_browser`** — Navigate to the page so the user can see the result.
## HTML Field Types
Use these `data-field-type` attributes in `index-base.tpl`:
| Attribute | Purpose | Example |
|-----------|---------|---------|
| `headfield` | Editable heading | `<h2 data-field-type="headfield">Title</h2>` |
| `textfield` | Short editable text | `<span data-field-type="textfield">Text</span>` |
| `wysiwyg` | Rich text editor | `<div data-field-type="wysiwyg">Content</div>` |
| `upload` | Image upload | `<img data-field-type="upload" src="...">` |
| `list` | Select dropdown | `<div data-field-type="list" data-options="opt1,opt2">` |
| `multiv2` | Repeater/records | `<div data-field-type="multiv2">...</div>` |
| `checkbox` | Toggle | `<div data-field-type="checkbox">` |
| `colorpicker` | Color picker | `<div data-field-type="colorpicker">` |
## MJML Modules
Modules with `MJMLModule: true` in their schema are email modules:
- Only appear when the page table is `mail_marketing`
- For `mail_marketing` tables, only MJML modules are shown
- Use MJML markup instead of standard HTML
## Key Rules
- Always use Tailwind CSS as primary styling
- Use `section_id` variable for unique anchors/scoping
- Use `interno` variable to detect CMS editor vs public view
- Include other modules with: `<module_id :param1="value1"></module_id>`
- Editing `index-base.tpl` with `acai-write` or `acai-line-replace` compiles automatically
- Twig uses filters (with `|`), never functions
- Twig concatenation uses `~`: `'value=' ~ variable`

104
docs/pages-and-records.md Normal file
View File

@@ -0,0 +1,104 @@
# Pages & Records Guide
## Page Types
Every CMS record that has an `enlace` (URL) field is a **page**. Pages come in two types determined by the `controlador` field:
### Builder (Modular) Pages
- `controlador` = `cms/lib/plugins/builder_saas/controlador.php`
- Content is built from **modules** (drag & drop components)
- The `builder` field contains a JSON array of module instances
- Use MCP tools: `add_module_to_record`, `set_module_config_vars`, etc.
- The page template renders modules in order from the builder JSON
### Standard Pages
- `controlador` = `cms/lib/plugins/builder_saas/controlador_tabla.php`
- Content lives directly in the record fields (`content`, `titulo_alternativo`, etc.)
- The `content` field is HTML (wysiwyg)
- Use `create_or_update_record` to edit content directly
- No modules involved
### How to determine page type
**Always check the `controlador` field** of the record:
- Contains `controlador.php` (without `_tabla`) → **Builder**
- Contains `controlador_tabla.php`**Standard**
## Table Types (Sections)
Tables with pages are called **sections**. There are two section types defined by `menuType` in the schema:
### Category (`menuType = "category"`)
- **Hierarchical** — pages have parent/child relationships
- Fields: `parentNum`, `depth`, `globalOrder`, `lineage`, `siblingOrder`
- Example: `apartados` (main site pages)
- Uses `visible_en_el_menu` field for menu visibility
- Ordered by `globalOrder`
### Multi (`menuType = "multi"`)
- **Flat list** — no hierarchy
- Uses `dragSortOrder` for ordering
- Example: `blog`, `travesias`
- Typically uses `visible` field (not `visible_en_el_menu`)
## Critical Rules for Pages
### NEVER change the `enlace` field
Unless the user explicitly asks to change a page URL, **never modify the `enlace` field**. Changing it breaks existing links, SEO, and navigation. The enlace is set when the page is created and should remain stable.
### NEVER change the `controlador` field
The controlador defines whether the page is Builder or Standard. Changing it breaks the page rendering. Only set it during page creation.
### Visibility fields
- `apartados` and other category tables use: `visible_en_el_menu` (1 = visible, 0 = hidden)
- `blog`, `travesias` and other multi tables use: `visible` (1 = visible, 0 = hidden)
- Always check which field the table has before toggling visibility
### Name/Title fields
- Some tables use `name` (e.g. `apartados`)
- Others use `title` (e.g. `blog`, `travesias`)
- Check the schema to know which one to use
## Working with Builder Pages
### Adding content to a new Builder page
1. List available modules: `list_available_modules`
2. Add modules: `add_module_to_record` (one at a time, in order)
3. Configure each module: `set_module_config_vars` with content
4. Add images if needed: `upload_record_image` or `generate_image`
### Editing an existing Builder page
1. List current modules: `list_page_modules`
2. Get module vars: `get_module_config_vars`
3. Update vars: `set_module_config_vars`
4. Or edit the module template: edit `index-base.tpl` with `acai-line-replace` or `acai-write` → compilation runs automatically
## Working with Standard Pages
### Adding content to a Standard page
Use `create_or_update_record` to set:
- `content` — HTML content (main body)
- `titulo_alternativo` — alternative title shown on the page
- `titulo_de_pagina` — browser tab title (SEO)
- `metatag_descripcion` — meta description (SEO)
### Example: Update a standard page
```
create_or_update_record with:
tableName: "apartados"
recordNum: "87"
fields:
content: "<h2>Our Services</h2><p>We offer...</p>"
titulo_de_pagina: "Services | My Site"
metatag_descripcion: "Discover our services..."
```
## The `apartados` Table (Special)
The `apartados` table is the main pages table in most Acai sites. Key characteristics:
- `menuType = "category"` — hierarchical with parent/child
- `parentNum` — the num of the parent page (0 = root level)
- `depth` — nesting level (0 = root, 1 = child, 2 = grandchild)
- `globalOrder` — display order across the entire tree
- `visible_en_el_menu` — whether the page shows in the navigation menu
- `breadcrumb` — auto-generated breadcrumb path
- Pages can be either Builder or Standard (check `controlador` field per record)

View File

@@ -55,7 +55,7 @@
| `module` | `'module_id' \| module({params})` | | `module` | `'module_id' \| module({params})` |
| `queryDB` | `'SELECT ...' \| queryDB()` | | `queryDB` | `'SELECT ...' \| queryDB()` |
| `imagec` | `path \| imagec(width)` | | `imagec` | `path \| imagec(width)` |
| `translate` | `'text' \| translate` (Twig) / `t_var('text')` (PHP) / `CmsApi.t_var('text')` (JS) | | `translate` | `'text' \| translate` |
| `json_decode` | `'json_string' \| json_decode` | | `json_decode` | `'json_string' \| json_decode` |
| `raw` | `variable \| raw` | | `raw` | `variable \| raw` |
| `truncate` | `text \| truncate(100)` | | `truncate` | `text \| truncate(100)` |
@@ -83,11 +83,3 @@
| `loop.index is odd/even` | Para layouts alternados | | `loop.index is odd/even` | Para layouts alternados |
| `interno` | True dentro del editor CMS | | `interno` | True dentro del editor CMS |
| `thisrecord` | Registro actual (en secciones generales) | | `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) |

View File

@@ -1,6 +1,6 @@
# Twig Reference # Twig Filters Reference
Referencia completa de filtros, operadores, sintaxis y variables globales disponibles en templates Acai. Solo filtros (con `|`), nunca funciones Twig. Acai usa filtros Twig con sintaxis `|`. No usar funciones Twig — solo filtros.
## `get` — Consultar tabla de BD ## `get` — Consultar tabla de BD
@@ -207,18 +207,3 @@ En `c-if` usar `=` (simple). En `{% if %}` usar `==` (doble).
2. **Upload fields son arrays:** `record.imagen[0].urlPath`, no `record.imagen` 2. **Upload fields son arrays:** `record.imagen[0].urlPath`, no `record.imagen`
3. **Tablas sin prefijo `cms_`** en `get()`. Con prefijo en `queryDB()` 3. **Tablas sin prefijo `cms_`** en `get()`. Con prefijo en `queryDB()`
4. **Concatenar con `~`:** `'stocks' | get('producto_num=' ~ producto.num)` 4. **Concatenar con `~`:** `'stocks' | get('producto_num=' ~ producto.num)`
---
## Global Variables
Variables disponibles en todos los templates (módulos y general sections):
| Variable | Description |
|----------|-------------|
| `section_id` | Unique ID per module instance (use for anchors, JS scoping) |
| `interno` | `true` when viewing in CMS editor, `false` on public site |
| `server.HTTP_HOST` | Current domain |
| `loop.index` | 1-based iteration index (inside `c-for`) |
| `loop.index is odd` / `loop.index is even` | For alternating layouts |
| `thisrecord` | Current record data (only in general sections) |