43 Commits

Author SHA1 Message Date
komkida91
1ab52cccdc fix(builder): stabilize restaurante free-drag, reset-to-template, and memory sync 2026-03-05 15:18:33 +01:00
komkida91
8f56e84e1f feat(customizer): add pro visual presets, social styles, and responsive map controls 2026-03-03 19:56:09 +01:00
komkida91
cdebdf9ddf fix(builder): disable cache and normalize restaurante default template 2026-03-03 10:48:28 +01:00
komkida91
9cccbab1c8 fix(builder): calcular alto free-drag con posicion real y evitar desorden visual 2026-03-01 14:48:23 +01:00
komkida91
864846da0f fix(builder): ordenar template restaurante en carga sin tocar drag libre 2026-03-01 14:44:39 +01:00
komkida91
02e32c9673 fix(builder): restaurante libre y ordenado; otros rubros sin cambios 2026-03-01 14:40:33 +01:00
komkida91
40738cc65b fix(builder): forzar layout template estable y desactivar free-drag auto 2026-03-01 14:35:58 +01:00
komkida91
d18a92d017 fix(builder): forzar auto-orden restaurante al detectar solape vertical 2026-03-01 14:32:45 +01:00
komkida91
10d8fb8cae fix(builder): detectar solape severo en free-drag y reordenar restaurante 2026-03-01 14:30:05 +01:00
komkida91
14eca53c91 fix(builder): ordenar layout default restaurante y corregir alto real en free-drag 2026-03-01 14:25:47 +01:00
komkida91
0787c7bc46 fix(builder): evitar solape en free-drag y auto-reparar posiciones duplicadas 2026-03-01 14:17:19 +01:00
komkida91
f7a1c2dffc fix(builder): evitar caos de layout cuando free-drag no tiene posiciones validas 2026-03-01 14:13:35 +01:00
komkida91
2cc845eb4d fix(builder): activar free drag persistente para template restaurante 2026-03-01 14:11:25 +01:00
komkida91
6c1ccc0a48 fix(builder): alinear preview-final con geometria visual del editor 2026-03-01 13:55:37 +01:00
komkida91
846ad5c7c6 docs(builder): integra STATE y baseline premium en memoria codex 2026-03-01 13:47:36 +01:00
komkida91
48742a2736 feat(builder): baseline premium campos misiones en elementor 2026-03-01 13:47:14 +01:00
komkida91
208dca9f05 feat(builder): add social design controls and per-block background/transitions 2026-02-24 16:48:49 +01:00
komkida91
93d046a24c fix(builder): improve menu anchor targeting and free-drop placement 2026-02-24 16:21:25 +01:00
komkida91
b37d2d4bec chore(versioning): add blocking checklist for execution workflow 2026-02-22 17:57:21 +01:00
komkida91
a15e3e17af chore(versioning): memorize working method and refresh session state 2026-02-22 17:52:37 +01:00
komkida91
d5f2b819bf fix(builder): use stable canvas container for drop/reorder positioning 2026-02-22 17:52:24 +01:00
komkida91
b3bb7d57aa docs(builder): memorize restaurant status and next QA pass for menu icons 2026-02-21 17:24:13 +01:00
komkida91
e83e915584 fix(builder): stabilize drawer reset flow and restore editable restaurant theme 2026-02-21 17:22:31 +01:00
komkida91
e3a1c9d17f chore(versioning): profesionalizar VERSIONADO_IA y limpiar protocolo 2026-02-19 23:54:18 +01:00
komkida91
8ae0017533 chore(versioning): registrar commit educacion v2 y regla token 2026-02-18 07:16:05 +01:00
komkida91
8ac360b69d feat(builder): educacion v2 + navegacion por anclas y regla token 2026-02-18 07:15:30 +01:00
komkida91
6f143089b4 fix(builder): unificar layout y estabilizar redimension en canvas 2026-02-16 20:33:44 +01:00
komkida91
685659c0f1 chore(versioning): registrar hash del fix final del dia 2026-02-14 20:40:49 +01:00
komkida91
f363eefdca fix(builder): reparar texto, habilitar modo libre y forzar full preview 2026-02-14 20:40:32 +01:00
komkida91
400a8230b5 chore(versioning): registrar hash de fix layout sin toggle global 2026-02-14 20:34:32 +01:00
komkida91
f9f7d23b8d fix(layout): quitar dos columnas global y usar solo drag por bloque 2026-02-14 20:34:16 +01:00
komkida91
f2fbc6eedd chore(versioning): registrar hash de drag inteligente y preview completo 2026-02-14 20:26:08 +01:00
komkida91
a6089ee341 feat(builder): decidir 1 o 2 columnas moviendo bloques y preview completo 2026-02-14 20:25:54 +01:00
komkida91
f8935e7c00 chore(versioning): registrar hash de full width y dos columnas 2026-02-14 20:18:07 +01:00
komkida91
e5df6de8fc feat(layout): ancho total y dos columnas con control por bloque 2026-02-14 20:17:54 +01:00
komkida91
df641372fa chore(versioning): registrar hash de fix preview full-page 2026-02-14 19:52:53 +01:00
komkida91
e20f0867fe fix(preview): pantalla completa y layout por secciones estable 2026-02-14 19:52:41 +01:00
komkida91
22fcd505f4 chore(versioning): registrar hashes de api y pulido de builder 2026-02-14 19:47:22 +01:00
komkida91
c2ee81d202 fix(builder): conservar bloques cargados y mejorar estado de publicar 2026-02-14 19:47:10 +01:00
komkida91
b6fb4dadff feat(elementor): agregar api propia de guardado y publicacion 2026-02-14 19:47:04 +01:00
komkida91
7dddbc4764 docs(ops): estandarizar arranque UB24 y registrar hashes 2026-02-14 19:37:53 +01:00
komkida91
f6d8ab13c0 fix(db): evitar conversiones SQL invalidas en SQLite 2026-02-14 19:37:42 +01:00
komkida91
1a5778be2e feat(app): unificar arranque y registrar blueprint de elementor 2026-02-14 19:37:36 +01:00
10 changed files with 3292 additions and 397 deletions

View File

@@ -7,7 +7,7 @@
Desde `c:\word`:
```powershell
python demo/_run_elementor_temp.py
python -m demo.app
```
## Verificar que quedo arriba
@@ -27,12 +27,12 @@ Si responde `StatusCode: 200`, esta listo.
Desde `c:\word`:
```powershell
Start-Process -FilePath python -ArgumentList @('demo/_run_elementor_temp.py') -WorkingDirectory 'c:\word' -RedirectStandardOutput 'c:\word\logs_demo_app.txt' -RedirectStandardError 'c:\word\logs_demo_app.err'
Start-Process -FilePath python -ArgumentList @('-m','demo.app') -WorkingDirectory 'c:\word'
```
## Parar servidor
```powershell
Get-CimInstance Win32_Process | Where-Object { $_.Name -eq 'python.exe' -and $_.CommandLine -match '_run_elementor_temp.py' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force }
Get-CimInstance Win32_Process | Where-Object { $_.Name -eq 'python.exe' -and $_.CommandLine -match 'demo.app' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force }
```
## Logs

View File

@@ -4,6 +4,19 @@
## 🔄 Historial de Versiones
### Hash: `gkachele-elementor-restaurante-20260221-v22`
**Fecha:** 21 Febrero 2026
**Estado:** OK funcional (mejorable)
#### Cambios:
- OK Drawer Pro estabilizado: apertura/cierre, overlay, z-index, lock scroll, Escape/foco y sync por viewport.
- OK Reset de builder corrige limpieza de bloques + settings.
- OK Se evita autoload de plantilla cuando ya hay estado guardado.
- OK Tema restaurante sin colores duros por bloque; vuelve a depender de variables editables.
- OK Se restaura comportamiento de ancho/contenedor para no romper bloques al 100%.
- Pendiente: QA manual de cada icono del menu en preview final (plan siguiente lote).
---
### Hash: `gkachele-elementor-apple-20260209-v14`
**Fecha:** 09 Febrero 2026
**Estado:** OK Fase 1
@@ -238,6 +251,8 @@
**© 2025 GKACHELE™. Todos los derechos reservados.**
## 🔄 Historial de Versiones
### Hash: `gkachele-builder-docs-20260208-v1`
**Fecha:** 08 Febrero 2026
**Estado:** ✅ Documentado
@@ -259,3 +274,6 @@
- `memoria/ESTADO_ACTUAL.md` marcado como historico para evitar decisiones con contexto 2025.
---

View File

@@ -1,36 +1,104 @@
# Memoria Codex - GKACHELE
**Fecha de corte:** 14 Febrero 2026
Entendido.
## Fuente de verdad (estado IA)
Queda memorizado el foco: mejorar sin retrocesos, calidad profesional y avance real por lotes verificables.
Cuando vuelvas, arranco leyendo memoria y ejecutando directo.
**Fecha de corte:** 04 Marzo 2026
## Fuente de verdad
- `codex/VERSIONADO_IA.md`
- `codex/BUILDER_ELEMENTOR_VERSION.md`
- `codex/HISTORIAL_CAMBIOS.md`
- `codex/MEMORIA_CODEX.md`
- `STATE.md` (raiz del workspace, memoria operativa Docker/SaaS desde 2026-02-28)
## Punto exacto
## Estado acordado (retomar desde aqui)
- Rama activa: `ai/ub24-builder-v1`
- Commit HEAD: `6d696c4`
- Version base builder: `gkachele-elementor-templates-20260210-v21`
- Estado de sync local/remoto al corte: `0/0`
- Checkpoint actual: `8f56e84`
- Tag local de checkpoint: `builder-customizer-pro-20260304-v1` (pendiente crear si se solicita)
- Base funcional estable previa: `e83e915` (tag local: `builder-stable-e83e915`)
- Estado de sync remoto verificado (`2026-03-04`): `0 0`
## Estado actual funcional
- Builder visual operativo en `/elementor/<site_id>`.
- Drag/drop y resize fluido en canvas.
- Inspector para estilos y contenido.
- Modo libre con snapping.
- Preview limpio y menu por modos (horizontal/acordeon/ambos).
## Estado funcional verificado
- Builder operativo en `/elementor/<site_id>`.
- Customizer unificado en una sola ruta:
- `/customizer/<id>` activo
- `/customizer2/<id>` y `/customizer3/<id>` fuera (`404`)
- Mejoras ya hechas en builder:
- menu manual con mejor mapeo a bloques reales
- drop en modo libre mas preciso
- bloque `Redes` con mas controles visuales
- controles de fondo/transicion por bloque (version inicial)
## Pendientes inmediatos
- Cargar bloques reales por plantilla de rubro (no solo look-and-feel).
- Definir persistencia de resenas al publicar.
- Integrar calendario real (Calendly/Google).
- Consolidar puente Builder -> SaaS (`/customizer/<id>`).
## Feedback del usuario memorizado (obligatorio)
- Hay cosas buenas en lo hecho.
- El resultado visual actual todavia se percibe "cutre".
- Las animaciones actuales no alcanzan nivel pro.
- Los fondos (sobre todo imagen de fondo) no se ven profesionales.
- Lo de `Redes` debe escalarse y aplicarse mejor a cada bloque.
- No perder este contexto ni volver a "no saber en que quedamos".
- Estandar visual confirmado por usuario: nivel premium como landing `campos-misiones`
- Hero con fondo potente y composicion pro.
- Bloques de contacto/redes con look comercial real.
- Mapas embebidos y galerias con acabado profesional.
- Botones con micro-movimiento y sombra consistente.
- Este nivel debe replicarse en plantillas y en el customizer de `word`.
## Flujo operativo memorizado
1. Construir y validar local en `http://127.0.0.1:5001`.
2. Guardar cambios en commits atomicos.
3. Push a `origin/ai/ub24-builder-v1` en cada lote validado.
4. Actualizar hash y estado en `codex/VERSIONADO_IA.md`.
## Siguiente lote acordado
1. Subir calidad visual global (no solo `Redes`) con sistema consistente por bloque.
2. Rehacer fondos animados a nivel pro (capas, movimiento elegante, control fino).
3. Rehacer transiciones/hover con calidad premium y coherencia global.
4. Mantener flujo: `1 archivo + 1 cambio + 1 prueba` por iteracion.
## Comando de arranque
- `python -m demo.app` desde `c:\word`
## Metodo operativo
- Misma rama + control por hash/revert.
- Sin push/tag sin orden explicita del usuario.
- Validacion inmediata por URL canonica:
- `http://127.0.0.1:5001/elementor/1`
- Arranque:
- `python -m demo.app` desde `c:\word`
## Memoria integrada desde STATE.md (raiz)
- Objetivo paralelo vigente: automatizacion Docker estable en `C:\word` con flujo reproducible.
- Regla activa: un solo flujo oficial de automatizacion, cambios pequenos y verificables, registro de fallo+fix.
- Flujo Docker objetivo:
1. `docker compose build`
2. `docker compose up -d`
3. `docker compose ps`
4. `docker compose logs --tail=200`
5. `docker compose restart <service>` cuando aplique
- Proximo paso operativo en esa linea: definir compose canonico unico y retirar variantes no usadas.
- Estandar visual SaaS validado (2026-02-28): referencia `Campos Misiones` como base premium replicable.
## Pendientes criticos memorizados (04 Marzo 2026)
1. Paridad real entre editor y preview final:
- lo que se ve mientras se edita debe verse igual en `/elementor/<id>/preview-final`.
2. Movimiento libre real de objetos:
- drag/reordenamiento debe funcionar estable y predecible.
3. Cache:
- revisar y corregir cache agresiva que oculta cambios recientes.
4. Ancho de bloques:
- no forzar 100% por defecto.
- permitir control real de ancho (mas angosto/mas ancho) sin romper layout.
5. Hero izquierda/derecha:
- swap de contenido debe reflejarse claramente en editor y preview final.
6. Modularizacion tecnica:
- dividir `elementor_builder.html` en archivos mas pequenos (CSS/JS por responsabilidades) para evitar regressiones y acelerar fixes.
## Control de version operativo (obligatorio desde 2026-03-04)
1. Antes de responder estado de version: ejecutar y registrar
- `git rev-parse --short HEAD`
- `git rev-list --left-right --count ai/ub24-builder-v1...origin/ai/ub24-builder-v1`
2. Si la memoria no coincide con `HEAD`, corregir memoria en la misma sesion.
3. No declarar "sincronizado" sin evidencia `0 0` en el momento.
## Actualizacion 2026-03-05 (builder restaurante)
- Rama objetivo reafirmada: `ai/ub24-builder-v1`.
- Lote tecnico aplicado en `elementor/templates/elementor_builder.html`:
1. `Reset` vuelve a plantilla base del rubro, no vacia bloques.
2. Se mantiene `free drag` como modo prioritario en restaurante.
3. Subir/bajar reordena visualmente en modo libre.
4. Menu reconoce targets por semantica y tipo de bloque (contacto/mapa/redes/resenas/carta).
5. Se agrega autosave de borrador y propagacion de `device` a preview-final.
- Nota de operacion:
- Evitar forzado automatico de posiciones cuando el usuario esta ubicando bloques manualmente.

47
codex/SESSION_STATE.md Normal file
View File

@@ -0,0 +1,47 @@
# SESSION STATE - Live Memory
Last update: 2026-03-05 (sync)
Owner: Codex + user
## Current objective
Stabilize the UB24 builder workflow with reproducible progress and no context loss between sessions.
## Current context snapshot
- Canonical workspace: `C:\word`
- Active branch target: `ai/ub24-builder-v1`
- Runtime command: `python -m demo.app`
- Canonical URL: `http://127.0.0.1:5001/elementor/1`
## Done recently
1. Defined memory/versioning strategy based on stable rules + live state + history.
2. Added startup/close hooks to enforce session continuity.
3. Updated startup protocol to always read `agent.md` in addition to core memory files.
4. Disabled legacy routes `/customizer2/<site_id>` and `/customizer3/<site_id>` in `demo/routes/customizer_ascii.py` (now return `404`).
5. Stabilized `elementor_builder` for restaurante in free-drag mode while preserving manual placement.
6. Reset action now restores base template per rubro (instead of wiping all blocks).
7. Menu mapping improved to resolve links by semantic intent + block type (contact/map/social/review/cards).
8. Added draft autosave and preview-final device propagation (`desktop/tablet/phone`).
## In progress
1. Final QA of restaurante flow end-to-end (order, menu links, responsive, publish persistence).
## Blockers
1. None active.
## Next 3 steps
1. QA complete on `site_id=1` for reset -> reorder -> preview-final -> publish.
2. Tune spacing/heights in free-drag for blocks with dynamic content (contact/social/map) without auto-restack.
3. Consolidate docs/cross references for single customizer flow and mark legacy as deprecated.
## Quick handoff template (copy and fill at close)
### What changed today
-
### What was validated
-
### What failed (if any)
-
### Next immediate action
-

View File

@@ -1,105 +1,180 @@
# Versionado IA - UB24/Elementor
# Versionado IA - UB24 / Elementor
## Rama de trabajo
- `ai/ub24-builder-v1`
## 0) Protocolo Bloqueante (siempre)
1. Definir objetivo del lote en 1 linea.
2. Tocar un solo archivo por fix (salvo excepcion aprobada).
3. Hacer un solo cambio por intento.
4. Probar inmediatamente con flujo fijo acordado.
5. Si falla, revert inmediato por hash/commit (sin encadenar parches).
6. Si pasa, commit atomico con mensaje semantico.
7. Push solo con orden explicita del usuario.
8. Antes de declarar estado, verificar sync real (`fetch` + `rev-list`).
## Estado de sincronizacion (14 Febrero 2026)
- Local: `ai/ub24-builder-v1`
- Remoto: `origin/ai/ub24-builder-v1`
- Divergencia verificada: `0/0` (sin commits pendientes entre local y remoto)
## 1) Objetivo
Definir un proceso de versionado auditable, reproducible y estable para el desarrollo del builder UB24.
## Regla de trabajo
1. Cada cambio funcional se guarda en un commit separado.
2. Cada commit se registra con su hash.
3. Cada commit debe incluir comando de reversion rapida.
4. La rama debe quedar sincronizada con remoto al cerrar bloque de trabajo.
## 2) Estado actual verificado
- Fecha de verificacion: `2026-03-04`
- Rama activa: `ai/ub24-builder-v1`
- Upstream: `origin/ai/ub24-builder-v1`
- Divergencia local/remoto: `0 0`
- HEAD actual: `8f56e84`
- Ultimo commit: `8f56e84 feat(customizer): add pro visual presets, social styles, and responsive map controls`
- Comando usado:
- `git rev-list --left-right --count ai/ub24-builder-v1...origin/ai/ub24-builder-v1`
## Convencion de mensaje
## 3) Politica de versionado
1. Commits atomicos por cambio funcional.
2. Mensajes bajo convencion semantica.
3. Reversion siempre definida por commit.
4. Cada bloque validado se empuja a remoto.
5. No mezclar cambios de infraestructura con cambios de UX en el mismo commit.
## 4) Convencion de commits
- `feat(builder): ...`
- `fix(builder): ...`
- `refactor(builder): ...`
- `chore(versioning): ...`
- `docs(builder): ...`
## Flujo con Gitea
1. Trabajo local en `ai/ub24-builder-v1`.
2. Push continuo a `origin/ai/ub24-builder-v1`.
3. Merge cuando validemos en local y Raspberry.
## Protocolo fijo de sincronizacion (siempre)
1. Verificar rama activa: `git branch --show-current`
2. Actualizar referencias remotas: `git fetch origin --prune`
3. Medir divergencia: `git rev-list --left-right --count ai/ub24-builder-v1...origin/ai/ub24-builder-v1`
4. Si el resultado no es `0 0`, sincronizar antes de continuar.
5. Despues de cada lote validado:
## 5) Flujo obligatorio por sesion
1. Verificar rama activa:
- `git branch --show-current`
2. Actualizar referencias remotas:
- `git fetch origin --prune`
3. Verificar divergencia:
- `git rev-list --left-right --count ai/ub24-builder-v1...origin/ai/ub24-builder-v1`
4. Si no es `0 0`, sincronizar antes de editar.
5. Al cerrar lote validado:
- `git add <archivos>`
- `git commit -m "tipo(scope): mensaje"`
- `git push origin ai/ub24-builder-v1`
6. Registrar hash y objetivo en este archivo y en `codex/HISTORIAL_CAMBIOS.md`.
## Registro de hashes
### Baseline
- Commit: `cb99f26`
- Objetivo: crear rama y politica de versionado para trabajo IA.
- Revert:
- `git revert <hash>`
- `git reset --hard <hash_anterior>` (solo con aprobacion explicita)
## 5.1) Metodo de ejecucion memorizado (obligatorio)
1. Misma rama activa (`ai/ub24-builder-v1`), sin ramas paralelas para fixes rapidos.
2. Control por hash/revert: si un intento falla, revert inmediato por commit.
3. Un archivo por fix cuando sea posible (evitar mezclar cambios laterales).
4. Validacion funcional inmediata despues de cada microcambio.
5. No commit/push sin orden explicita del usuario.
### Correccion historial
- Commit: `fe8657e`
- Objetivo: revertir commit no deseado y mantener separacion de cambios.
- Revert:
- `git revert fe8657e`
### Fix local Elementor
- Commit: `22e564e`
- Objetivo: robustecer arranque local y carga de themes en Windows (BOM + logs seguros).
- Revert:
- `git revert 22e564e`
### Fase 1 Builder (visual pro)
- Commit: `1c04f04`
- Objetivo: consolidar estilos reutilizables y subir calidad visual en hero, features, cards y contact del preview.
- Revert:
- `git revert 1c04f04`
### Ajustes Builder (limpieza + preview + ancho)
- Commit: `7c5f671`
- Objetivo: quitar texto en barra Apple, limpiar menu vacio, preview local funcional sin salir de builder, ancho desktop al 100%, control de ancho por bloque y descripcion en bloque video.
- Revert:
- `git revert 7c5f671`
### Ajustes Builder (preview limpio + menu modos)
- Commit: `dd98e9d`
- Objetivo: mejorar vista previa (forzar modo limpio y restaurar estado), eliminar precarga automatica de bloques, y agregar modo de menu (horizontal/acordeon/ambos).
- Revert:
- `git revert dd98e9d`
## URL local canonica (unificada)
- Base local: `http://127.0.0.1:5001`
- Builder local: `http://127.0.0.1:5001/elementor/1`
- Regla: usar siempre `127.0.0.1` (no `localhost`) en scripts, pruebas y documentacion local.
## Arranque rapido local (Windows)
1. Desde `c:\word`, ejecutar:
- `python demo/_run_elementor_temp.py`
2. Abrir:
## 6) Protocolo de arranque local
- Comando canonico:
- `python -m demo.app`
- URL canonica:
- `http://127.0.0.1:5001`
- Builder:
- `http://127.0.0.1:5001/elementor/1`
3. Verificacion rapida:
- Verificacion rapida:
- `Invoke-WebRequest http://127.0.0.1:5001/elementor/1 -UseBasicParsing`
Notas:
- En el primer arranque puede tardar ~40-50 segundos antes de quedar escuchando en `5001`.
- Logs:
- `c:\word\logs_demo_app.txt`
- `c:\word\logs_demo_app.err`
## 7) Criterios de estabilidad (no negociables)
1. No tocar funcionalidades estables sin requerimiento explicito.
2. Mantener una sola estrategia de layout y DnD por flujo.
3. Mantener una sola ruta de guardado activa:
- `/api/elementor/save`
4. Validar guardado/publicacion antes de cerrar lote.
5. Evitar cambios amplios si un fix local resuelve el problema.
## Control de rama (local/remoto)
- Rama local activa: `ai/ub24-builder-v1`
- Upstream remoto: `origin/ai/ub24-builder-v1`
- Estado al registrar: `en sync (0/0)` al 14 Febrero 2026
- Politica: commits atomicos + push por lote validado + verificacion de divergencia al inicio y al cierre.
## 8) Registro de commits relevantes
| Fecha | Commit | Tipo | Objetivo | Revert recomendado |
|---|---|---|---|---|
| 2026-02-21 | `e83e915` | fix | Estabilizar Drawer Pro, reset real de builder y restaurar tema restaurante editable + ancho | `git revert e83e915` |
| 2026-02-14 | `cb99f26` | chore | Crear rama y politica inicial de versionado IA | `git revert cb99f26` |
| 2026-02-14 | `fe8657e` | fix | Revertir commit no deseado y limpiar historial | `git revert fe8657e` |
| 2026-02-14 | `22e564e` | fix | Robustecer arranque local/theme load en Windows | `git revert 22e564e` |
| 2026-02-14 | `1c04f04` | feat | Mejora visual base (hero/features/cards/contact) | `git revert 1c04f04` |
| 2026-02-14 | `7c5f671` | fix | Limpieza preview/menu y ajustes de ancho | `git revert 7c5f671` |
| 2026-02-14 | `dd98e9d` | fix | Preview limpio y menu por modos | `git revert dd98e9d` |
| 2026-02-15 | `1a5778b` | refactor | Runtime unificado `python -m demo.app` + blueprint Elementor | `git revert 1a5778b` |
| 2026-02-15 | `8ac360b` | feat | Educacion V2 + correccion de anclas | `git revert 8ac360b` |
| 2026-02-15 | `f6d8ab1` | fix | Corregir wrapper SQLite para evitar fallo de arranque | `git revert f6d8ab1` |
| 2026-02-15 | `b6fb4da` | feat | API dedicada `/api/elementor/save` con opcion publicar | `git revert b6fb4da` |
| 2026-02-15 | `c2ee81d` | fix | Persistencia de bloques + feedback de publish | `git revert c2ee81d` |
| 2026-02-15 | `e20f086` | fix | Preview full-page y layout estable por secciones | `git revert e20f086` |
| 2026-02-15 | `e5df6de` | feat | Full width + soporte real de 2 columnas | `git revert e5df6de` |
| 2026-02-15 | `a6089ee` | feat | Drag inteligente por drop + preview en nueva pestana | `git revert a6089ee` |
| 2026-02-15 | `f9f7d23` | fix | Quitar layout global y forzar preview completo | `git revert f9f7d23` |
| 2026-02-15 | `f363eef` | fix | Correccion de texto/UI + modo libre + `?full=1` | `git revert f363eef` |
| 2026-02-16 | `6f14308` | fix | Unificar layout y estabilizar redimension en canvas | `git revert 6f14308` |
## Fases memorizadas (builder)
1. Fase 1 (UI Pro base): navbar premium, hero premium, sistema de espaciado/grid, pulido visual consistente.
2. Fase 2 (estructura): separar renderers por bloque y reducir inline styles para automatizacion.
3. Fase 3 (presets): presets por rubro + reglas responsive + variantes exportables.
## 9) Decisiones funcionales vigentes
- Rubros oficiales permitidos:
- `restaurante`
- `danza`
- `cosmeticos`
- `despachos`
- `gimnasios`
- `educacion`
- `base_otro`
- Preview final separada habilitada:
- `GET /elementor/<site_id>/preview-final`
- `GET /ub24/<site_id>/preview-final`
- Customizer activo unificado:
- `GET /customizer/<site_id>`
- Rutas legacy retiradas:
- `GET /customizer2/<site_id>` -> `404`
- `GET /customizer3/<site_id>` -> `404`
- Motor de reordenamiento seleccionado para DnD:
- `SortableJS` (estrategia unica)
## 10) Pendientes priorizados
1. QA manual de cada icono/accion del menu superior en preview final (siguiente lote).
2. Cerrar y validar flujo unico del customizer en docs cruzadas (sin rutas legacy).
3. Footer global obligatorio con autoria del proyecto.
4. Watermark de autoria en codigo bajo convencion unica.
5. Flujo dual estable de preview (editor/pagina real) + mejora touch/capacitiva.
## 11) Referencias cruzadas
- Historial funcional detallado: `codex/HISTORIAL_CAMBIOS.md`
- Arranque rapido local: `codex/ARRANQUE_RAPIDO_UB24.md`
- Flujo general del proyecto: `codex/FLUJO_PROYECTO.md`
## 12) Nota operativa
Este archivo define el estandar de trabajo. Cualquier cambio de proceso debe registrarse en un commit `chore(versioning)` y quedar reflejado aqui.
Memorizado.
En la próxima sesión haré esto como primer paso:
1. Revisar en internet referencias de diseño (layouts, bloques, redes, animaciones, efectos pro).
2. Extraer patrones aplicables al builder.
3. Convertirlos en mejoras concretas y ordenadas en tu proyecto.
Entendido.
Queda memorizado el foco: mejorar sin retrocesos, calidad profesional y avance real por lotes verificables.
Cuando vuelvas, arranco leyendo memoria y ejecutando directo.
## 13) Continuidad de sesion (agregado 2026-03-04)
- Se adopta esquema de memoria en 3 capas:
1. `AGENTS.md` (reglas estables)
2. `codex/SESSION_STATE.md` (estado vivo de trabajo)
3. `codex/VERSIONADO_IA.md` (historial de decisiones/versionado)
- Hook de arranque oficial:
- `powershell -ExecutionPolicy Bypass -File .\codex\start-session.ps1`
- Hook de cierre oficial:
- `powershell -ExecutionPolicy Bypass -File .\codex\end-session.ps1`
## 14) Ajuste de operacion (2026-03-04)
- A partir de esta fecha, la lectura de contexto de inicio es automatica por politica del asistente.
- No se requiere ejecutar scripts manuales para que el asistente cargue memoria.
- `start-session.ps1` y `end-session.ps1` quedan como herramientas opcionales de soporte.
## 15) Regla de sincronizacion estricta (2026-03-04)
1. Toda respuesta sobre "version actual" debe salir de git en tiempo real, no de memoria previa.
2. Al detectar desfase entre memoria y `HEAD`, actualizar `codex/VERSIONADO_IA.md` y `codex/MEMORIA_CODEX.md` en la misma sesion.
3. Toda afirmacion de push/sync debe incluir evidencia de `rev-list`:
- `0 0` = sincronizado
- distinto de `0 0` = no sincronizado
## 16) Lote aplicado (2026-03-05)
- Rama de trabajo confirmada: `ai/ub24-builder-v1`
- Base de partida: `8f56e84`
- Archivo principal intervenido:
- `elementor/templates/elementor_builder.html`
- Cambios del lote:
1. `Reset` restaura plantilla base por rubro (ya no borra todo).
2. Flujo restaurante en `free_drag` preserva posicion manual (sin auto-restack agresivo por render).
3. Botones subir/bajar aplican reordenamiento visible.
4. Mapeo de menu a bloques por semantica + tipo (`contact`, `map`, `social`, `review`, `cards/gallery/hero`).
5. Autosave borrador en cambios de inspector/settings.
6. Preview final recibe `device` (`desktop/tablet/phone`) desde el editor.

View File

@@ -1,17 +1,37 @@
"""
GKACHELE SaaS PageBuilder - Sistema Modular
© 2025 GKACHELE™. Todos los derechos reservados.
"""
GKACHELE SaaS PageBuilder - Sistema Modular
"""
import os
from flask import Flask, jsonify, request
from config import SECRET_KEY, PORT
from database import init_db
from routes.auth import auth_bp
from routes.dashboard import dashboard_bp
from routes.customizer import customizer_bp
from routes.admin import admin_bp
from routes.public import public_bp
import sys
from flask import Flask, jsonify
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
PROJECT_ROOT = os.path.dirname(BASE_DIR)
if PROJECT_ROOT not in sys.path:
sys.path.insert(0, PROJECT_ROOT)
try:
from .config import SECRET_KEY, PORT
from .database import init_db
from .routes.auth import auth_bp
from .routes.dashboard import dashboard_bp
from .routes.customizer import customizer_bp
from .routes.admin import admin_bp
from .routes.public import public_bp
except ImportError:
from config import SECRET_KEY, PORT
from database import init_db
from routes.auth import auth_bp
from routes.dashboard import dashboard_bp
from routes.customizer import customizer_bp
from routes.admin import admin_bp
from routes.public import public_bp
try:
from elementor.routes import elementor_bp
except ImportError:
elementor_bp = None
app = Flask(__name__, template_folder='templates', static_folder='static')
app.secret_key = SECRET_KEY
@@ -26,6 +46,8 @@ app.register_blueprint(dashboard_bp)
app.register_blueprint(customizer_bp)
app.register_blueprint(admin_bp)
app.register_blueprint(public_bp)
if elementor_bp is not None and 'elementor' not in app.blueprints:
app.register_blueprint(elementor_bp)
# Manejadores de Errores Globales
@app.errorhandler(500)
@@ -34,15 +56,18 @@ def handle_500(e):
response.status_code = 500
return response
@app.errorhandler(404)
def handle_404(e):
return jsonify({'success': False, 'error': 'No encontrado'}), 404
@app.errorhandler(Exception)
def handle_exception(e):
print(f"ERROR: EXCEPCION: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
# Middleware
@app.after_request
def add_header(response):
@@ -50,6 +75,7 @@ def add_header(response):
response.headers['Content-Security-Policy'] = "frame-ancestors *;"
return response
if __name__ == '__main__':
print(f"GKACHELE SaaS Modular iniciado en puerto {PORT}")
app.run(debug=True, host='0.0.0.0', port=PORT)

104
demo/db.py Normal file
View File

@@ -0,0 +1,104 @@
import os
import sqlite3
import json
# DB Settings (Postgres)
DB_HOST = os.environ.get('DB_HOST', None)
DB_PORT = int(os.environ.get('DB_PORT', '5432'))
DB_NAME = os.environ.get('DB_NAME', 'gkachele')
DB_USER = os.environ.get('DB_USER', 'gkachele')
DB_PASSWORD = os.environ.get('DB_PASSWORD', 'gkachele_pass')
# SQLite Settings
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
SQLITE_PATH = os.path.join(BASE_DIR, 'database', 'main.db')
class CursorWrapper:
def __init__(self, cursor, is_sqlite=False):
self._cursor = cursor
self._is_sqlite = is_sqlite
def execute(self, query, params=None):
try:
if self._is_sqlite:
# Skip postgres-specific maintenance commands
if 'setval' in query or 'pg_get_serial_sequence' in query:
return None
# Adapt psycopg2-style placeholders (%s) to sqlite (?)
if isinstance(query, str) and '%s' in query:
query = query.replace('%s', '?')
# SQLite doesn't support IF NOT EXISTS in ALTER TABLE
if 'ALTER TABLE' in query and 'ADD COLUMN' in query and 'IF NOT EXISTS' in query:
query = query.replace('IF NOT EXISTS', '')
# Handle SERIAL PRIMARY KEY and other PG types
query = query.replace('SERIAL PRIMARY KEY', 'INTEGER PRIMARY KEY AUTOINCREMENT')
if 'IF NOT EXISTS' not in query and 'CREATE TABLE' in query:
query = query.replace('CREATE TABLE', 'CREATE TABLE IF NOT EXISTS')
else:
# Adapt sqlite-style placeholders (?) to psycopg2 (%s)
if isinstance(query, str) and '?' in query:
query = query.replace('?', '%s')
if params is None:
return self._cursor.execute(query)
return self._cursor.execute(query, params)
except (sqlite3.OperationalError, sqlite3.IntegrityError) as e:
msg = str(e).lower()
if "duplicate column name" in msg or "already exists" in msg:
return None
raise e
def fetchone(self):
return self._cursor.fetchone()
def fetchall(self):
return self._cursor.fetchall()
def __getattr__(self, name):
return getattr(self._cursor, name)
class ConnectionWrapper:
def __init__(self, conn, is_sqlite=False):
self._conn = conn
self._is_sqlite = is_sqlite
def cursor(self):
return CursorWrapper(self._conn.cursor(), is_sqlite=self._is_sqlite)
def commit(self):
return self._conn.commit()
def close(self):
return self._conn.close()
def __getattr__(self, name):
return getattr(self._conn, name)
def get_db():
if DB_HOST:
try:
import psycopg2
conn = psycopg2.connect(
host=DB_HOST,
port=DB_PORT,
dbname=DB_NAME,
user=DB_USER,
password=DB_PASSWORD,
)
return ConnectionWrapper(conn, is_sqlite=False)
except Exception as e:
print(f"Postgres connection failed: {e}. Falling back to SQLite.")
# Fallback to SQLite
os.makedirs(os.path.dirname(SQLITE_PATH), exist_ok=True)
conn = sqlite3.connect(SQLITE_PATH)
return ConnectionWrapper(conn, is_sqlite=True)
try:
import psycopg2
IntegrityError = psycopg2.IntegrityError
except ImportError:
IntegrityError = sqlite3.IntegrityError

View File

@@ -0,0 +1,877 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Customizer New - {{ site_name }}</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Oswald:wght@400;600;700&family=Source+Serif+4:wght@400;500;700&display=swap" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<style>
:root {
--bg: #e4dccb;
--bg-2: #f3ecde;
--panel: #fff8eb;
--panel-2: #f2e7cd;
--canvas: #ece3d1;
--text: #1f1b17;
--muted: #6e6458;
--accent: #9b2f16;
--accent-2: #2d5e4c;
--border: rgba(31, 27, 23, 0.14);
--shadow-soft: 0 12px 28px rgba(31, 27, 23, 0.14);
--shadow-panel: 0 20px 42px rgba(31, 27, 23, 0.16);
--fx-blur: 8px;
--fx-radius: 12px;
--topbar-a: rgba(45, 94, 76, 0.96);
--topbar-b: rgba(31, 27, 23, 0.88);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: "Source Serif 4", serif;
color: var(--text);
background: radial-gradient(circle at 10% 0%, #f7f1e2 0%, var(--bg) 46%, #d6c6a5 100%);
}
.topbar {
height: 52px; display: flex; align-items: center; justify-content: space-between;
padding: 0 16px;
border-bottom: 1px solid var(--border);
background: linear-gradient(120deg, var(--topbar-a), var(--topbar-b));
box-shadow: var(--shadow-soft);
}
.brand {
font-family: "Oswald", sans-serif;
letter-spacing: 0.04em;
font-size: 1.02rem;
font-weight: 700;
color: #f7f1e2;
display: flex;
align-items: center;
gap: 8px;
}
.btn {
padding: 8px 12px;
border: 1px solid rgba(255, 248, 235, 0.7);
background: rgba(255, 248, 235, 0.15);
color: #f7f1e2;
border-radius: 999px;
font-size: 12px;
cursor: pointer;
transition: transform 140ms ease, box-shadow 140ms ease;
}
.btn:hover { transform: translateY(-2px); box-shadow: 0 8px 16px rgba(31, 27, 23, 0.26); }
.btn-primary {
background: var(--accent);
border-color: var(--accent);
color: #fff;
box-shadow: 0 8px 16px rgba(155, 47, 22, 0.3);
}
.layout { display: grid; grid-template-columns: 260px 1fr 260px; height: calc(100vh - 52px); }
.side {
background: linear-gradient(160deg, var(--panel), #f5ebd4);
border-right: 1px solid var(--border);
overflow: auto;
box-shadow: inset -1px 0 0 rgba(255, 255, 255, 0.4);
backdrop-filter: blur(var(--fx-blur));
-webkit-backdrop-filter: blur(var(--fx-blur));
}
.side.right { border-right: 0; border-left: 1px solid var(--border); }
.section { padding: 12px; }
.section-title {
font-family: "Oswald", sans-serif;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 1px;
color: var(--muted);
margin-bottom: 8px;
}
.list-item {
padding: 8px 10px;
background: rgba(255, 255, 255, 0.58);
border: 1px solid var(--border);
border-radius: var(--fx-radius);
margin-bottom: 6px;
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
font-size: 12px;
box-shadow: 0 6px 14px rgba(31, 27, 23, 0.08);
backdrop-filter: blur(var(--fx-blur));
-webkit-backdrop-filter: blur(var(--fx-blur));
}
.list-item.active {
border-color: rgba(45, 94, 76, 0.45);
box-shadow: 0 10px 16px rgba(45, 94, 76, 0.12);
}
.list-actions button {
width: 22px;
height: 22px;
border-radius: 6px;
border: 1px solid var(--border);
background: rgba(255, 255, 255, 0.6);
color: var(--text);
cursor: pointer;
}
.canvas {
background: radial-gradient(circle at 30% 0%, #f4ead6 0%, var(--canvas) 60%, #ddcfb1 100%);
overflow: auto;
padding: 16px;
}
.preview {
background: #fff;
border-radius: calc(var(--fx-radius) + 4px);
border: 1px solid var(--border);
overflow: hidden;
min-height: 80vh;
box-shadow: var(--shadow-panel);
backdrop-filter: blur(var(--fx-blur));
-webkit-backdrop-filter: blur(var(--fx-blur));
}
.preview iframe { width: 100%; height: 80vh; border: 0; display: block; }
.field { margin-bottom: 10px; }
.field label { display: block; font-size: 11px; color: var(--muted); margin-bottom: 4px; }
.field input, .field textarea, .field select {
width: 100%;
padding: 8px 10px;
border-radius: 8px;
border: 1px solid var(--border);
background: rgba(255, 255, 255, 0.75);
color: var(--text);
font-size: 12px;
}
.hint { font-size: 11px; color: var(--muted); }
.template-box {
padding: 10px;
border-radius: var(--fx-radius);
border: 1px solid var(--border);
background: rgba(255, 255, 255, 0.56);
margin-bottom: 12px;
box-shadow: 0 8px 18px rgba(31, 27, 23, 0.1);
backdrop-filter: blur(var(--fx-blur));
-webkit-backdrop-filter: blur(var(--fx-blur));
}
.preset-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 6px;
margin-bottom: 8px;
}
.preset-chip {
border: 1px solid var(--border);
border-radius: 999px;
background: rgba(255, 255, 255, 0.72);
color: var(--text);
font-size: 11px;
padding: 6px 8px;
cursor: pointer;
}
.preset-chip.active {
background: var(--accent);
color: #fff;
border-color: var(--accent);
}
.field input[type="range"] {
padding: 0;
accent-color: var(--accent);
}
body.compact .side { display: none; }
body.compact .layout { grid-template-columns: 1fr; }
body.compact .canvas { padding: 0; }
body.compact .preview { min-height: calc(100vh - 52px); border-radius: 0; border: 0; }
.toggle-panels {
padding: 6px 10px;
border: 1px solid rgba(255, 248, 235, 0.7);
background: rgba(255, 248, 235, 0.15);
color: #f7f1e2;
border-radius: 999px;
font-size: 12px;
cursor: pointer;
transition: transform 140ms ease, box-shadow 140ms ease;
}
.toggle-panels:hover { transform: translateY(-2px); box-shadow: 0 8px 16px rgba(31, 27, 23, 0.26); }
</style>
</head>
<body>
<div class="topbar">
<div class="brand"><i class="fas fa-layer-group"></i> Customizer New</div>
<div style="display:flex;gap:8px;align-items:center;">
<button class="toggle-panels" onclick="togglePanels()">Paneles</button>
<button class="btn" onclick="reloadPreview()">Refrescar preview</button>
<button class="btn btn-primary" onclick="saveState()">Guardar</button>
</div>
</div>
<div class="layout">
<aside class="side">
<div class="section">
<div class="section-title">Páginas</div>
<div id="pagesList"></div>
<button class="btn btn-primary" style="width:100%;margin-top:6px;" onclick="loadDemo()">Cargar demo</button>
</div>
<div class="section">
<div class="section-title">Capas (Secciones)</div>
<div id="sectionsList"></div>
<button class="btn" style="width:100%;margin-top:6px;" onclick="addSection()">Agregar sección</button>
</div>
</aside>
<main class="canvas">
<div class="preview">
<iframe id="previewFrame" src="/api/customizer/preview-frame/{{ site_id }}?page=home&edit=1&t={{ security_hash }}"></iframe>
</div>
</main>
<aside class="side right">
<div class="section">
<div class="section-title">Sistema Visual Pro</div>
<div id="visualPanel"></div>
</div>
<div class="section">
<div class="section-title">Propiedades</div>
<div class="hint">Edicion simple para plantillas</div>
<div id="propsPanel"></div>
</div>
</aside>
</div>
<script>
const SITE_ID = {{ site_id }};
const THEME = "{{ theme }}";
const VISUAL_PRESETS = {
tierra_editorial: {
name: 'Tierra Editorial',
bg: '#e4dccb',
bg2: '#f3ecde',
panel: '#fff8eb',
text: '#1f1b17',
muted: '#6e6458',
accent: '#9b2f16',
accent2: '#2d5e4c',
blur: 8,
radius: 12,
depth: 38,
glow: 16,
map_ratio: '16 / 9',
map_h_desktop: 360,
map_h_tablet: 280,
map_h_mobile: 220,
social_style: 'pill'
},
glass_aurora: {
name: 'Glass Aurora',
bg: '#dde6ff',
bg2: '#eafcff',
panel: '#f7fbff',
text: '#1c2433',
muted: '#5a6a86',
accent: '#6b5bff',
accent2: '#3ecfc2',
blur: 18,
radius: 18,
depth: 56,
glow: 42,
map_ratio: '16 / 9',
map_h_desktop: 380,
map_h_tablet: 300,
map_h_mobile: 240,
social_style: 'glass'
},
ice_minimal: {
name: 'Ice Minimal',
bg: '#e9eef5',
bg2: '#f5f8fc',
panel: '#ffffff',
text: '#172133',
muted: '#667389',
accent: '#3a7bf7',
accent2: '#2fba99',
blur: 10,
radius: 14,
depth: 34,
glow: 10,
map_ratio: '4 / 3',
map_h_desktop: 340,
map_h_tablet: 280,
map_h_mobile: 220,
social_style: 'minimal'
},
noir_pro: {
name: 'Noir Pro',
bg: '#131620',
bg2: '#1b2030',
panel: '#1f2535',
text: '#eff3ff',
muted: '#9eabc7',
accent: '#5f8dff',
accent2: '#52d7be',
blur: 12,
radius: 12,
depth: 62,
glow: 26,
map_ratio: '16 / 10',
map_h_desktop: 360,
map_h_tablet: 290,
map_h_mobile: 230,
social_style: 'card'
}
};
let state = {
content: {{ content | tojson | safe }},
blocks: {{ content.get('blocks', []) | tojson | safe }},
settings: {{ content.get('settings', {}) | tojson | safe }},
pages: [
{ id: 'home', title: 'Inicio' },
{ id: 'servicios', title: 'Servicios' },
{ id: 'proyectos', title: 'Proyectos' },
{ id: 'contacto', title: 'Contacto' }
],
currentPage: 'home',
selectedIndex: null
};
let visualSaveTimer = null;
function applyCompactMode() {
if (THEME === 'streaming-vix') {
document.body.classList.add('compact');
}
}
function togglePanels() {
document.body.classList.toggle('compact');
}
function normalizeBlocks() {
state.blocks = (state.blocks || []).map(b => {
if (!b || typeof b !== 'object') return b;
if (!b.page) b.page = 'home';
if (!b.data) b.data = {};
return b;
});
}
function normalizeVisualSettings() {
if (!state.settings) state.settings = {};
const basePreset = VISUAL_PRESETS.tierra_editorial;
const current = state.settings.visual || {};
state.settings.visual = {
preset: current.preset || 'tierra_editorial',
...basePreset,
...current
};
if (!VISUAL_PRESETS[state.settings.visual.preset]) {
state.settings.visual.preset = 'tierra_editorial';
}
}
function renderVisualPanel() {
const panel = document.getElementById('visualPanel');
if (!panel) return;
const v = state.settings.visual || VISUAL_PRESETS.tierra_editorial;
const chips = Object.entries(VISUAL_PRESETS).map(([key, preset]) => `
<button class="preset-chip ${v.preset === key ? 'active' : ''}" onclick="applyVisualPreset('${key}')">${preset.name}</button>
`).join('');
panel.innerHTML = `
<div class="template-box">
<div class="preset-grid">${chips}</div>
${colorVisualTpl('accent', 'Acento', v.accent)}
${colorVisualTpl('accent2', 'Acento 2', v.accent2)}
${rangeVisualTpl('blur', 'Glass Blur', v.blur, 0, 28, 1)}
${rangeVisualTpl('radius', 'Radio', v.radius, 6, 28, 1)}
${rangeVisualTpl('depth', 'Profundidad', v.depth, 0, 100, 1)}
${rangeVisualTpl('glow', 'Glow', v.glow, 0, 80, 1)}
${selectVisualTpl('social_style', 'Estilo Redes', v.social_style, [
{ value: 'pill', label: 'Pill' },
{ value: 'glass', label: 'Glass' },
{ value: 'card', label: 'Card' },
{ value: 'minimal', label: 'Minimal' }
])}
${selectVisualTpl('map_ratio', 'Ratio Mapa', v.map_ratio, [
{ value: '16 / 9', label: '16:9' },
{ value: '4 / 3', label: '4:3' },
{ value: '1 / 1', label: '1:1' },
{ value: '21 / 9', label: '21:9' },
{ value: 'auto', label: 'Auto' }
])}
${rangeVisualTpl('map_h_desktop', 'Mapa Desktop', v.map_h_desktop, 220, 700, 10)}
${rangeVisualTpl('map_h_tablet', 'Mapa Tablet', v.map_h_tablet, 180, 540, 10)}
${rangeVisualTpl('map_h_mobile', 'Mapa Móvil', v.map_h_mobile, 140, 420, 10)}
</div>
`;
}
function colorVisualTpl(id, label, value) {
return `<div class="field"><label>${label}</label><input type="color" value="${value}" oninput="updateVisual('${id}', this.value, true)" onchange="updateVisual('${id}', this.value, false)"></div>`;
}
function rangeVisualTpl(id, label, value, min, max, step) {
return `<div class="field"><label>${label}: <strong>${value}</strong></label><input type="range" min="${min}" max="${max}" step="${step}" value="${value}" oninput="updateVisual('${id}', this.value, true)" onchange="updateVisual('${id}', this.value, false)"></div>`;
}
function selectVisualTpl(id, label, selected, options) {
const opts = options.map(opt => `<option value="${opt.value}" ${selected === opt.value ? 'selected' : ''}>${opt.label}</option>`).join('');
return `<div class="field"><label>${label}</label><select oninput="updateVisual('${id}', this.value, true)" onchange="updateVisual('${id}', this.value, false)">${opts}</select></div>`;
}
function applyVisualPreset(presetKey) {
const preset = VISUAL_PRESETS[presetKey];
if (!preset) return;
state.settings.visual = { preset: presetKey, ...preset };
applyEditorVisual();
applyPreviewVisual();
renderVisualPanel();
saveState();
}
function updateVisual(key, value, live) {
const v = state.settings.visual || {};
if (['blur', 'radius', 'depth', 'glow', 'map_h_desktop', 'map_h_tablet', 'map_h_mobile'].includes(key)) {
v[key] = Number(value);
} else {
v[key] = value;
}
state.settings.visual = v;
applyEditorVisual();
applyPreviewVisual();
if (!live) {
renderVisualPanel();
saveState();
return;
}
if (visualSaveTimer) clearTimeout(visualSaveTimer);
visualSaveTimer = setTimeout(() => saveState(), 700);
}
function hexToRgba(hex, alpha) {
if (!hex || typeof hex !== 'string') return `rgba(0,0,0,${alpha})`;
const clean = hex.replace('#', '');
const full = clean.length === 3 ? clean.split('').map(ch => ch + ch).join('') : clean;
const num = parseInt(full, 16);
if (Number.isNaN(num)) return `rgba(0,0,0,${alpha})`;
const r = (num >> 16) & 255;
const g = (num >> 8) & 255;
const b = num & 255;
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
}
function applyEditorVisual() {
const v = state.settings.visual || VISUAL_PRESETS.tierra_editorial;
const root = document.documentElement;
const depth = Number(v.depth || 40);
const blur = Number(v.blur || 8);
const radius = Number(v.radius || 12);
const glow = Number(v.glow || 0);
root.style.setProperty('--bg', v.bg);
root.style.setProperty('--bg-2', v.bg2);
root.style.setProperty('--panel', v.panel);
root.style.setProperty('--panel-2', v.panel);
root.style.setProperty('--canvas', v.bg2);
root.style.setProperty('--text', v.text);
root.style.setProperty('--muted', v.muted);
root.style.setProperty('--accent', v.accent);
root.style.setProperty('--accent-2', v.accent2);
root.style.setProperty('--border', hexToRgba(v.text, 0.14));
root.style.setProperty('--fx-blur', `${blur}px`);
root.style.setProperty('--fx-radius', `${radius}px`);
root.style.setProperty('--shadow-soft', `0 12px 28px ${hexToRgba(v.text, 0.08 + depth / 600)}`);
root.style.setProperty('--shadow-panel', `0 22px 46px ${hexToRgba(v.text, 0.12 + depth / 500)}`);
root.style.setProperty('--topbar-a', hexToRgba(v.accent2, 0.94));
root.style.setProperty('--topbar-b', hexToRgba(v.text, 0.9));
if (glow > 0) {
root.style.setProperty('--shadow-soft', `0 12px 28px ${hexToRgba(v.text, 0.08 + depth / 600)}, 0 0 ${Math.round(glow / 3)}px ${hexToRgba(v.accent, 0.18)}`);
}
}
function applyPreviewVisual() {
const frame = document.getElementById('previewFrame');
if (!frame || !frame.contentDocument) return;
const doc = frame.contentDocument;
const v = state.settings.visual || VISUAL_PRESETS.tierra_editorial;
const styleId = 'gk-customizer-visual';
let styleEl = doc.getElementById(styleId);
if (!styleEl) {
styleEl = doc.createElement('style');
styleEl.id = styleId;
doc.head.appendChild(styleEl);
}
const blur = Number(v.blur || 8);
const radius = Number(v.radius || 12);
const glow = Number(v.glow || 0);
const font = (state.settings && state.settings.font_family) ? state.settings.font_family : 'Source Serif 4';
const mapRatio = v.map_ratio || '16 / 9';
const mapDesktop = Number(v.map_h_desktop || 360);
const mapTablet = Number(v.map_h_tablet || 280);
const mapMobile = Number(v.map_h_mobile || 220);
const socialStyle = v.social_style || 'pill';
const socialBg = socialStyle === 'minimal'
? 'transparent'
: socialStyle === 'card'
? hexToRgba(v.panel, 0.92)
: hexToRgba(v.panel, 0.72);
const socialBorder = socialStyle === 'minimal'
? hexToRgba(v.text, 0.26)
: hexToRgba(v.text, 0.14);
const socialShadow = socialStyle === 'card'
? `0 14px 26px ${hexToRgba(v.text, 0.18)}`
: socialStyle === 'glass'
? `0 10px 18px ${hexToRgba(v.text, 0.15)}, 0 0 ${Math.max(6, Math.round(glow / 2))}px ${hexToRgba(v.accent, 0.2)}`
: `0 8px 14px ${hexToRgba(v.text, 0.12)}`;
styleEl.textContent = `
:root {
--gk-accent: ${v.accent};
--gk-accent-2: ${v.accent2};
--gk-text: ${v.text};
--gk-muted: ${v.muted};
}
body {
background: radial-gradient(circle at 15% 0%, ${v.bg2} 0%, ${v.bg} 56%, ${v.panel} 100%) !important;
color: ${v.text} !important;
font-family: '${font}', system-ui, sans-serif !important;
}
:where(section, article, .card, .panel, .tile, .feature, .service-item, .pricing-card, .contact-card, .box) {
background: ${hexToRgba(v.panel, 0.72)} !important;
border: 1px solid ${hexToRgba(v.text, 0.14)} !important;
border-radius: ${radius}px !important;
box-shadow: 0 16px 28px ${hexToRgba(v.text, 0.14)}${glow > 0 ? `, 0 0 ${Math.round(glow / 2)}px ${hexToRgba(v.accent, 0.22)}` : ''} !important;
backdrop-filter: blur(${blur}px) !important;
-webkit-backdrop-filter: blur(${blur}px) !important;
}
:where(button, .btn, a.btn, input, textarea, select) {
border-radius: ${Math.max(8, radius - 2)}px !important;
}
:where(a, .link-accent, h1, h2, h3) {
color: inherit;
}
:where(.btn-primary, button.primary, a.primary) {
background: linear-gradient(120deg, ${v.accent}, ${v.accent2}) !important;
color: #fff !important;
border-color: transparent !important;
}
:where(.redes-sociales a, .social-icon, .social-btn, [data-social], .social-link) {
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
gap: 8px !important;
background: ${socialBg} !important;
border: 1px solid ${socialBorder} !important;
color: ${v.text} !important;
border-radius: ${Math.max(10, radius)}px !important;
padding: 8px 12px !important;
box-shadow: ${socialShadow} !important;
text-decoration: none !important;
backdrop-filter: blur(${Math.max(0, blur - 2)}px) !important;
-webkit-backdrop-filter: blur(${Math.max(0, blur - 2)}px) !important;
transition: transform 120ms ease, box-shadow 120ms ease, border-color 120ms ease !important;
}
:where(.redes-sociales a, .social-icon, .social-btn, [data-social], .social-link):hover {
transform: translateY(-2px) !important;
border-color: ${hexToRgba(v.accent, 0.5)} !important;
box-shadow: 0 14px 24px ${hexToRgba(v.text, 0.2)}, 0 0 ${Math.max(8, Math.round(glow / 2))}px ${hexToRgba(v.accent, 0.24)} !important;
}
:where(.mapa-container iframe, iframe.map-frame, [class*="map"] iframe, [data-block-type="map"] iframe) {
width: 100% !important;
max-width: 100% !important;
border: 0 !important;
border-radius: ${Math.max(10, radius)}px !important;
box-shadow: 0 12px 22px ${hexToRgba(v.text, 0.16)} !important;
display: block !important;
${mapRatio === 'auto' ? `height: ${mapDesktop}px !important;` : `height: auto !important; aspect-ratio: ${mapRatio} !important;`}
}
@media (max-width: 1024px) {
:where(.mapa-container iframe, iframe.map-frame, [class*="map"] iframe, [data-block-type="map"] iframe) {
${mapRatio === 'auto' ? `height: ${mapTablet}px !important;` : `min-height: ${mapTablet}px !important;`}
}
}
@media (max-width: 768px) {
:where(.mapa-container iframe, iframe.map-frame, [class*="map"] iframe, [data-block-type="map"] iframe) {
${mapRatio === 'auto' ? `height: ${mapMobile}px !important;` : `min-height: ${mapMobile}px !important;`}
}
}
`;
applyPerBlockMapSizing(doc, mapRatio, mapDesktop);
}
function applyPerBlockMapSizing(doc, fallbackRatio, fallbackHeight) {
const mapBlocks = (state.blocks || []).filter(b => (b.page || 'home') === state.currentPage && (b.type === 'map' || b.type === 'mapa'));
if (!mapBlocks.length) return;
const mapIframes = doc.querySelectorAll('.mapa-container iframe, iframe.map-frame, [class*="map"] iframe, [data-block-type="map"] iframe');
if (!mapIframes.length) return;
mapIframes.forEach((iframe, i) => {
const data = (mapBlocks[i] && mapBlocks[i].data) ? mapBlocks[i].data : {};
const ratio = data.ratio || fallbackRatio || '16 / 9';
const height = Number(data.height || fallbackHeight || 360);
if (ratio && ratio !== 'auto') {
iframe.style.aspectRatio = ratio;
iframe.style.height = 'auto';
iframe.style.minHeight = `${Math.max(160, Math.min(height, 800))}px`;
} else {
iframe.style.height = `${Math.max(160, Math.min(height, 800))}px`;
iframe.style.aspectRatio = '';
iframe.style.minHeight = '';
}
});
}
function getSections() {
const sections = [];
let current = null;
state.blocks.forEach((block, idx) => {
if ((block.page || 'home') !== state.currentPage) return;
if (block.type === 'heading') {
if (current) current.endIdx = idx - 1;
current = { title: block.data.text || 'Sección', startIdx: idx, endIdx: idx, idx };
sections.push(current);
} else if (!current) {
current = { title: 'Sección', startIdx: idx, endIdx: idx, idx };
sections.push(current);
} else {
current.endIdx = idx;
}
});
return sections;
}
function renderPages() {
const list = document.getElementById('pagesList');
list.innerHTML = state.pages.map(p => `
<div class="list-item ${p.id === state.currentPage ? 'active' : ''}" onclick="setPage('${p.id}')">
<span>${p.title}</span>
</div>
`).join('');
}
function renderSections() {
const list = document.getElementById('sectionsList');
const sections = getSections();
if (sections.length === 0) {
list.innerHTML = '<div class="list-item">Sin secciones</div>';
return;
}
list.innerHTML = sections.map((s, i) => `
<div class="list-item ${state.selectedIndex === s.idx ? 'active' : ''}" onclick="selectBlock(${s.idx})">
<span>${s.title}</span>
<div class="list-actions">
<button onclick="moveSection(${i}, -1); event.stopPropagation()">↑</button>
<button onclick="moveSection(${i}, 1); event.stopPropagation()">↓</button>
</div>
</div>
`).join('');
}
function renderProps() {
const panel = document.getElementById('propsPanel');
let html = '';
if (THEME === 'streaming-vix') {
const c = state.content || {};
html += `<div class="template-box">
<div class="section-title">Template Streaming</div>
${fieldTpl('hero_title', 'Titulo principal', c.hero_title || 'Bienvenido')}
${textareaTpl('hero_description', 'Descripcion', c.hero_description || '')}
${fieldTpl('cta_primary_text', 'CTA principal', c.cta_primary_text || 'Empezar ahora')}
${fieldTpl('cta_primary_url', 'URL CTA principal', c.cta_primary_url || '#cta')}
${fieldTpl('cta_secondary_text', 'CTA secundario', c.cta_secondary_text || 'Ver catalogo')}
${fieldTpl('cta_secondary_url', 'URL CTA secundario', c.cta_secondary_url || '#showcase')}
${textareaTpl('benefits', 'Beneficios (uno por linea)', (c.benefits || []).join('\n'))}
${textareaTpl('showcase', 'Contenido destacado (uno por linea)', (c.showcase || []).join('\n'))}
${fieldTpl('footer_text', 'Footer', c.footer_text || '')}
</div>`;
}
const idx = state.selectedIndex;
if (idx !== null && state.blocks[idx]) {
const b = state.blocks[idx];
html += `<div class="template-box"><div class="section-title">Bloque</div>`;
if (b.type === 'heading') {
html += field('text', 'Titulo', b.data.text || '');
html += field('color', 'Color', b.data.color || '#0f172a');
} else if (b.type === 'paragraph') {
html += textarea('text', 'Contenido', b.data.text || '');
} else if (b.type === 'image') {
html += field('url', 'URL Imagen', b.data.url || '');
} else if (b.type === 'map' || b.type === 'mapa') {
html += field('url', 'URL Embed', b.data.url || b.data.embed || '');
html += field('address', 'Dirección', b.data.address || '');
html += selectField('ratio', 'Ratio', b.data.ratio || '16 / 9', [
{ value: '16 / 9', label: '16:9' },
{ value: '4 / 3', label: '4:3' },
{ value: '1 / 1', label: '1:1' },
{ value: '21 / 9', label: '21:9' },
{ value: 'auto', label: 'Auto' }
]);
html += field('height', 'Altura (px)', b.data.height || '');
} else if (b.type === 'list') {
html += field('title', 'Titulo', b.data.title || '');
html += textarea('items', 'Items (uno por linea)', (b.data.items || []).join('\n'));
} else {
html += textarea('json', 'JSON', JSON.stringify(b.data || {}, null, 2));
}
html += `</div>`;
}
panel.innerHTML = html || '<div class="hint">Selecciona un bloque o edita el template.</div>';
}
function field(id, label, value) {
return `<div class="field"><label>${label}</label><input id="field_${id}" value="${value}" oninput="updateField('${id}')"></div>`;
}
function textarea(id, label, value) {
return `<div class="field"><label>${label}</label><textarea id="field_${id}" rows="5" oninput="updateField('${id}')">${value}</textarea></div>`;
}
function selectField(id, label, selected, options) {
const opts = options.map(opt => `<option value="${opt.value}" ${selected === opt.value ? 'selected' : ''}>${opt.label}</option>`).join('');
return `<div class="field"><label>${label}</label><select id="field_${id}" oninput="updateField('${id}')" onchange="updateField('${id}')">${opts}</select></div>`;
}
function fieldTpl(id, label, value) {
return `<div class="field"><label>${label}</label><input id="tpl_${id}" value="${value}" oninput="updateTemplate('${id}')"></div>`;
}
function textareaTpl(id, label, value) {
return `<div class="field"><label>${label}</label><textarea id="tpl_${id}" rows="4" oninput="updateTemplate('${id}')">${value}</textarea></div>`;
}
function updateField(id) {
const b = state.blocks[state.selectedIndex];
if (!b) return;
if (id === 'items') {
b.data.items = document.getElementById('field_items').value.split('\\n').filter(Boolean);
} else if (id === 'json') {
try { b.data = JSON.parse(document.getElementById('field_json').value); } catch (e) { return; }
} else if (id === 'url' && (b.type === 'map' || b.type === 'mapa')) {
const val = document.getElementById('field_url').value;
b.data.url = val;
b.data.embed = val;
} else if (id === 'height' && (b.type === 'map' || b.type === 'mapa')) {
const raw = document.getElementById('field_height').value;
const n = Number(raw);
b.data.height = Number.isFinite(n) && n > 0 ? n : '';
} else {
b.data[id] = document.getElementById('field_' + id).value;
}
saveState();
}
function updateTemplate(id) {
if (!state.content) state.content = {};
if (id === 'benefits' || id === 'showcase') {
const raw = document.getElementById('tpl_' + id).value;
state.content[id] = raw.split('\n').filter(Boolean);
} else {
state.content[id] = document.getElementById('tpl_' + id).value;
}
saveState();
}
function selectBlock(idx) {
state.selectedIndex = idx;
renderSections();
renderProps();
}
function setPage(id) {
state.currentPage = id;
state.selectedIndex = null;
renderPages();
renderSections();
renderProps();
reloadPreview();
}
function moveSection(idx, dir) {
const sections = getSections();
const newIdx = idx + dir;
if (newIdx < 0 || newIdx >= sections.length) return;
const order = sections.map((_, i) => i);
const temp = order[idx];
order[idx] = order[newIdx];
order[newIdx] = temp;
reorderSections(order);
}
function reorderSections(order) {
const sections = getSections();
if (sections.length === 0) return;
const removed = new Set();
const reordered = [];
order.forEach(i => {
const sec = sections[i];
for (let x = sec.startIdx; x <= sec.endIdx; x++) {
reordered.push(state.blocks[x]);
removed.add(x);
}
});
const firstStart = sections[0].startIdx;
const before = [];
const after = [];
state.blocks.forEach((b, i) => {
if (removed.has(i)) return;
if (i < firstStart) before.push(b);
else after.push(b);
});
state.blocks = before.concat(reordered, after);
renderSections();
saveState();
}
function addSection() {
const title = prompt('Título de la sección', 'Nueva sección');
if (!title) return;
state.blocks.push({ id: Math.random(), type: 'heading', colSpan: 4, page: state.currentPage, data: { text: title, color: '#0f172a' } });
state.blocks.push({ id: Math.random(), type: 'paragraph', colSpan: 4, page: state.currentPage, data: { text: 'Contenido de la sección...' } });
renderSections();
saveState();
}
function loadDemo() {
const demo = [
{ id: 1, type: 'heading', colSpan: 4, page: 'home', data: { text: 'Construcción de Viviendas', color: '#0f172a' } },
{ id: 2, type: 'paragraph', colSpan: 4, page: 'home', data: { text: 'Diseño, planificación y obra llave en mano.' } },
{ id: 3, type: 'heading', colSpan: 4, page: 'home', data: { text: 'Servicios', color: '#0f172a' } },
{ id: 4, type: 'list', colSpan: 4, page: 'home', data: { title: 'Servicios', items: ['Proyecto', 'Obra nueva', 'Reformas', 'Permisos'] } },
{ id: 5, type: 'heading', colSpan: 4, page: 'home', data: { text: 'Testimonios', color: '#0f172a' } },
{ id: 6, type: 'list', colSpan: 4, page: 'home', data: { title: 'Opiniones', items: ['Excelente calidad', 'Buen trato', 'Recomendado'] } }
];
state.blocks = demo;
renderSections();
saveState();
}
async function saveState() {
if (!state.content) state.content = {};
state.content.settings = state.settings || {};
state.content.blocks = state.blocks || [];
await fetch('/api/customizer/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
site_id: SITE_ID,
content: state.content
})
});
reloadPreview();
}
function reloadPreview() {
const frame = document.getElementById('previewFrame');
frame.src = `/api/customizer/preview-frame/${SITE_ID}?page=${state.currentPage}&edit=1&t=${Date.now()}`;
}
applyCompactMode();
if (!state.content) state.content = {};
normalizeVisualSettings();
normalizeBlocks();
applyEditorVisual();
const previewFrame = document.getElementById('previewFrame');
if (previewFrame) {
previewFrame.addEventListener('load', applyPreviewVisual);
}
renderPages();
renderSections();
renderVisualPanel();
renderProps();
applyPreviewVisual();
</script>
</body>
</html>

128
elementor/routes.py Normal file
View File

@@ -0,0 +1,128 @@
from flask import Blueprint, render_template, session, request, jsonify, make_response
import json
from db import get_db
from utils.theme_engine import get_theme_config
elementor_bp = Blueprint(
'elementor',
__name__,
template_folder='templates',
static_folder='static',
static_url_path='/elementor/static'
)
def _render_builder(site_id, builder_mode='default', preview_only=False, **_kwargs):
conn = get_db()
c = conn.cursor()
c.execute('SELECT user_id, slug, theme, content_json FROM sites WHERE id = ?', (site_id,))
site = c.fetchone()
if not site:
conn.close()
return 'Sitio no encontrado', 404
if 'user_id' in session and site[0] != session['user_id']:
conn.close()
return 'No autorizado', 403
content = json.loads(site[3]) if site[3] else {}
if not isinstance(content, dict):
content = {}
theme = site[2]
theme_config = get_theme_config(theme)
c.execute('SELECT plan, rubro FROM users WHERE id = ?', (site[0],))
user_data = c.fetchone()
conn.close()
user_plan = user_data[0] if user_data else 'base'
user_rubro = user_data[1] if user_data else 'restaurante'
html = render_template(
'elementor_builder.html',
site_id=site_id,
slug=site[1],
theme=theme,
content=content,
theme_config=theme_config,
user_plan=user_plan,
rubro=user_rubro,
builder_mode=builder_mode,
preview_only=bool(preview_only)
)
response = make_response(html)
# Dynamic builder output should never be cached.
response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0'
response.headers['Pragma'] = 'no-cache'
response.headers['Expires'] = '0'
return response
@elementor_bp.route('/elementor/<int:site_id>')
def elementor_view(site_id):
return _render_builder(site_id, builder_mode='default')
@elementor_bp.route('/elementor/<int:site_id>/preview-final')
def elementor_preview_final(site_id):
return _render_builder(site_id, builder_mode='default', preview_only=True)
@elementor_bp.route('/ub24/<int:site_id>')
def ub24_view(site_id):
return _render_builder(site_id, builder_mode='ub24')
@elementor_bp.route('/ub24/<int:site_id>/preview-final')
def ub24_preview_final(site_id):
return _render_builder(site_id, builder_mode='ub24', preview_only=True)
@elementor_bp.route('/api/elementor/save', methods=['POST'])
def save_elementor():
data = request.get_json(silent=True) or {}
site_id = data.get('site_id')
content = data.get('content')
publish = bool(data.get('publish'))
if not site_id or not isinstance(content, dict):
return jsonify({'success': False, 'error': 'Payload invalido'}), 400
conn = get_db()
c = conn.cursor()
c.execute('SELECT user_id, content_json FROM sites WHERE id = ?', (site_id,))
row = c.fetchone()
if not row:
conn.close()
return jsonify({'success': False, 'error': 'Sitio no encontrado'}), 404
owner_id = row[0]
if 'user_id' in session and session['user_id'] != owner_id:
conn.close()
return jsonify({'success': False, 'error': 'No autorizado'}), 403
current_content = {}
try:
if row[1]:
current_content = json.loads(row[1]) or {}
except Exception:
current_content = {}
merged = dict(current_content)
merged.update(content)
if publish:
c.execute('UPDATE sites SET content_json = ?, status = ? WHERE id = ?', (json.dumps(merged), 'published', site_id))
else:
c.execute('UPDATE sites SET content_json = ? WHERE id = ?', (json.dumps(merged), site_id))
conn.commit()
conn.close()
response = jsonify({'success': True, 'published': publish})
response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0'
response.headers['Pragma'] = 'no-cache'
response.headers['Expires'] = '0'
return response

File diff suppressed because it is too large Load Diff