Compare commits
65 Commits
main
...
ai/ub24-bu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8fdcd5ed48 | ||
|
|
32ea7849eb | ||
|
|
9ff6d2ee34 | ||
|
|
442acb1f8d | ||
|
|
1ab52cccdc | ||
|
|
8f56e84e1f | ||
|
|
cdebdf9ddf | ||
|
|
9cccbab1c8 | ||
|
|
864846da0f | ||
|
|
02e32c9673 | ||
|
|
40738cc65b | ||
|
|
d18a92d017 | ||
|
|
10d8fb8cae | ||
|
|
14eca53c91 | ||
|
|
0787c7bc46 | ||
|
|
f7a1c2dffc | ||
|
|
2cc845eb4d | ||
|
|
6c1ccc0a48 | ||
|
|
846ad5c7c6 | ||
|
|
48742a2736 | ||
|
|
208dca9f05 | ||
|
|
93d046a24c | ||
|
|
b37d2d4bec | ||
|
|
a15e3e17af | ||
|
|
d5f2b819bf | ||
|
|
b3bb7d57aa | ||
|
|
e83e915584 | ||
|
|
e3a1c9d17f | ||
|
|
8ae0017533 | ||
|
|
8ac360b69d | ||
|
|
6f143089b4 | ||
|
|
685659c0f1 | ||
|
|
f363eefdca | ||
|
|
400a8230b5 | ||
|
|
f9f7d23b8d | ||
|
|
f2fbc6eedd | ||
|
|
a6089ee341 | ||
|
|
f8935e7c00 | ||
|
|
e5df6de8fc | ||
|
|
df641372fa | ||
|
|
e20f0867fe | ||
|
|
22fcd505f4 | ||
|
|
c2ee81d202 | ||
|
|
b6fb4dadff | ||
|
|
7dddbc4764 | ||
|
|
f6d8ab13c0 | ||
|
|
1a5778be2e | ||
|
|
53aa755c39 | ||
|
|
075dad6f1a | ||
|
|
6d696c4c5d | ||
|
|
e409f25ab4 | ||
|
|
1fa4b166e5 | ||
|
|
dd98e9d9f0 | ||
|
|
b1602f7067 | ||
|
|
7c5f67150c | ||
|
|
78e47929d9 | ||
|
|
429602c1e2 | ||
|
|
5b9c9fb42b | ||
|
|
c20c9a71f7 | ||
|
|
1c04f04170 | ||
|
|
22e564e524 | ||
|
|
fe8657e495 | ||
|
|
06d4781dc6 | ||
|
|
6aad246fbc | ||
|
|
cb99f261c5 |
23
.gitignore
vendored
23
.gitignore
vendored
@@ -14,3 +14,26 @@ wp-content/cache/supercache/
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Python / caches
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Local data / logs
|
||||
*.db
|
||||
*.err
|
||||
*.out
|
||||
logs_demo_app.*
|
||||
backups/
|
||||
|
||||
# Tooling snapshots
|
||||
codex/snapshots/
|
||||
free-llm-api-resources/
|
||||
llm
|
||||
gitea_data*/
|
||||
|
||||
# Demo temp artifacts
|
||||
demo/_run_elementor_temp.py
|
||||
demo/_run_ub24_temp.py
|
||||
demo/elementor_1_snapshot.html
|
||||
demo/preview_final_snapshot.html
|
||||
|
||||
40
codex/ARRANQUE_RAPIDO_UB24.md
Normal file
40
codex/ARRANQUE_RAPIDO_UB24.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# Arranque Rapido UB24 (Local Windows)
|
||||
|
||||
## URL canonica
|
||||
- `http://127.0.0.1:5001/elementor/1`
|
||||
|
||||
## Inicio en 1 comando
|
||||
Desde `c:\word`:
|
||||
|
||||
```powershell
|
||||
python -m demo.app
|
||||
```
|
||||
|
||||
## Verificar que quedo arriba
|
||||
En otra terminal:
|
||||
|
||||
```powershell
|
||||
Invoke-WebRequest http://127.0.0.1:5001/elementor/1 -UseBasicParsing
|
||||
```
|
||||
|
||||
Si responde `StatusCode: 200`, esta listo.
|
||||
|
||||
## Tiempo de arranque esperado
|
||||
- Primer arranque: puede tardar ~40-50 segundos por inicializacion de DB.
|
||||
- Arranques siguientes: normalmente mas rapido.
|
||||
|
||||
## Levantar en background
|
||||
Desde `c:\word`:
|
||||
|
||||
```powershell
|
||||
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 'demo.app' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force }
|
||||
```
|
||||
|
||||
## Logs
|
||||
- `c:\word\logs_demo_app.txt`
|
||||
- `c:\word\logs_demo_app.err`
|
||||
279
codex/HISTORIAL_CAMBIOS.md
Normal file
279
codex/HISTORIAL_CAMBIOS.md
Normal file
@@ -0,0 +1,279 @@
|
||||
# 📝 Historial de Cambios - GKACHELE™
|
||||
|
||||
**© 2025 GKACHELE™. Todos los derechos reservados.**
|
||||
|
||||
## 🔄 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
|
||||
|
||||
#### Cambios:
|
||||
- OK Modo libre unico activo
|
||||
- OK Drag/resize con handles 8 puntos + Shift
|
||||
- OK Guías de snap
|
||||
- OK Touch (capacitivos)
|
||||
- OK Auto-height del canvas
|
||||
- OK Menu superior movible y redimensionable
|
||||
- OK Boton Alinear
|
||||
|
||||
---
|
||||
|
||||
### Hash: `gkachele-elementor-apple-20260209-v13`
|
||||
**Fecha:** 09 Febrero 2026
|
||||
**Estado:** OK UX Builder
|
||||
|
||||
#### Cambios:
|
||||
- OK Sidebar: solo drag (sin agregar por click)
|
||||
- OK Menu: acordeon responsive por tamanos de preview
|
||||
- OK Media: captions + fit (cover/contain) en imagen/galeria
|
||||
- OK Video: drag & drop (dataURL) o URL
|
||||
- OK Delete inline en bloques (icono en preview)
|
||||
- OK Free drag: links no bloquean movimiento
|
||||
- OK Boton atras (history.back)
|
||||
|
||||
---
|
||||
|
||||
### Hash: `gkachele-elementor-apple-20260209-v12`
|
||||
**Fecha:** 09 Febrero 2026
|
||||
**Estado:** OK Responsive + fondos
|
||||
|
||||
#### Cambios:
|
||||
- OK Menu responsive con acordeon en movil
|
||||
- OK Fondo: animacion de gradiente + GIF de fondo
|
||||
- OK Boton: estilos (primario/outline/ghost) y tamanos
|
||||
- OK Boton flotante WhatsApp (si hay numero)
|
||||
- OK Transiciones premium (botones/iconos/hover)
|
||||
- OK Modo libre: sin etiqueta ON, estado visual
|
||||
|
||||
---
|
||||
|
||||
### Hash: `gkachele-elementor-apple-20260209-v11`
|
||||
**Fecha:** 09 Febrero 2026
|
||||
**Estado:** OK Mejoras UX/UI
|
||||
|
||||
#### Cambios:
|
||||
- OK Mas tipografias en el inspector
|
||||
- OK Redes: estilos premium (pill/circulo/outline/minimal/solid) + iconos extra
|
||||
- OK Drag & drop de imagenes desde el preview (imagen y galeria)
|
||||
- OK Mapa: embed corregido (google.com/maps)
|
||||
- OK Contacto: mailto/tel
|
||||
- OK Calendario: vista tipo calendario (si no hay embed)
|
||||
- OK Resenas: estilos (card/quote/compacto)
|
||||
- OK Resize activo en ambos modos
|
||||
- OK Modo libre con tamanos mas controlados
|
||||
- OK Transiciones y hover premium en iconos
|
||||
|
||||
---
|
||||
|
||||
### Hash: `gkachele-elementor-apple-20260209-v10`
|
||||
**Fecha:** 09 Febrero 2026
|
||||
**Estado:** OK Pendientes anotados
|
||||
|
||||
#### Cambios:
|
||||
- OK Memoria actualizada para adaptacion a SaaS
|
||||
- OK Pendientes: color de texto, transiciones, colores de iconos
|
||||
- OK Pendientes: resize en modo libre, mapa direccion
|
||||
- OK Pendientes: tarjetas con 3 opciones y menu movil acordeon
|
||||
|
||||
---
|
||||
|
||||
|
||||
### Hash: `gkachele-elementor-apple-20260209-v9`
|
||||
**Fecha:** 09 Febrero 2026
|
||||
**Estado:** OK Ajustado
|
||||
|
||||
#### Cambios:
|
||||
- OK Iconos de preview (movil/tablet/web)
|
||||
- OK Redes con iconos y tamano ajustable
|
||||
- OK Mapa sin API y boton scroll
|
||||
- OK Carga de logo/fondo por arrastre
|
||||
|
||||
---
|
||||
|
||||
|
||||
### Hash: `gkachele-elementor-apple-20260209-v8`
|
||||
**Fecha:** 09 Febrero 2026
|
||||
**Estado:** OK Ajustado
|
||||
|
||||
#### Cambios:
|
||||
- OK Menu superior detecta secciones
|
||||
- OK Layout 2 columnas basico
|
||||
- OK Contador ilimitado en topbar
|
||||
- OK Botones de tamano preview
|
||||
|
||||
---
|
||||
|
||||
|
||||
### Hash: `gkachele-elementor-apple-20260209-v7`
|
||||
**Fecha:** 09 Febrero 2026
|
||||
**Estado:** OK Ajustado
|
||||
|
||||
#### Cambios:
|
||||
- OK Fondos con gradiente y controles visibles
|
||||
- OK Tama?o de bloques por porcentaje
|
||||
- OK Topbar con contador y salto a bloques
|
||||
- OK Animaciones configurables
|
||||
|
||||
---
|
||||
|
||||
|
||||
### Hash: `gkachele-elementor-apple-20260209-v6`
|
||||
**Fecha:** 09 Febrero 2026
|
||||
**Estado:** OK Ajustado
|
||||
|
||||
#### Cambios:
|
||||
- OK Modo libre (drag X/Y) con snapping
|
||||
- OK Boton de activacion en topbar
|
||||
- OK Posiciones guardadas por bloque
|
||||
|
||||
---
|
||||
|
||||
|
||||
### Hash: `gkachele-elementor-apple-20260209-v5`
|
||||
**Fecha:** 09 Febrero 2026
|
||||
**Estado:** OK Ajustado
|
||||
|
||||
#### Cambios:
|
||||
- OK Componentes nuevos: tarjetas e iconos
|
||||
- OK Sidebar e inspector con configuracion de items
|
||||
- OK Render en preview y tema apple-pro
|
||||
|
||||
---
|
||||
|
||||
|
||||
### Hash: `gkachele-elementor-apple-20260209-v4`
|
||||
**Fecha:** 09 Febrero 2026
|
||||
**Estado:** OK Ajustado
|
||||
|
||||
#### Cambios:
|
||||
- OK Editor de tipografias (texto y titulos) en el inspector
|
||||
- OK Paleta de colores (fondo, texto, secundario)
|
||||
- OK Preview con variables de estilo aplicadas
|
||||
- OK Animaciones sutiles en bloques (hover + entrada)
|
||||
|
||||
---
|
||||
|
||||
|
||||
### Hash: `gkachele-builder-apple-20260208-v2`
|
||||
**Fecha:** 08 Febrero 2026
|
||||
**Estado:** ✅ Ajustado
|
||||
|
||||
#### Cambios:
|
||||
- ✅ Restaurado template del builder en `elementor/templates/elementor_builder.html`
|
||||
- ✅ Drag & drop funcional (agregar y reordenar en preview)
|
||||
- ✅ Inspector estable para editar bloques y ajustes del sitio
|
||||
|
||||
---
|
||||
|
||||
### Hash: `gkachele-elementor-apple-20260208-v3`
|
||||
**Fecha:** 08 Febrero 2026
|
||||
**Estado:** ✅ Base de trabajo (congelada)
|
||||
|
||||
#### Cambios:
|
||||
- ✅ Version base del builder para continuar mañana
|
||||
- ✅ Drag & drop estable en preview
|
||||
- ✅ Inspector con edicion de bloques
|
||||
- ✅ Fondo y video por URL en preview
|
||||
|
||||
---
|
||||
|
||||
### Hash: `gkachele-builder-apple-20260208-v1`
|
||||
**Fecha:** 08 Febrero 2026
|
||||
**Estado:** ✅ En progreso
|
||||
|
||||
#### Cambios:
|
||||
- ✅ Builder tipo drag & drop en ruta `/elementor/{site_id}`
|
||||
- ✅ Vista lienzo con bloques sobre la pagina (no lista)
|
||||
- ✅ Preview en vivo con marco estilo navegador
|
||||
- ✅ Tema `apple-pro` para paginas profesionales
|
||||
- ✅ Soporte de logo, fondo con imagen y video en ajustes
|
||||
- ✅ Redes sociales ampliadas (TikTok, YouTube, LinkedIn, X, Telegram)
|
||||
|
||||
---
|
||||
|
||||
### Hash: `gkachele-login-customizer-20250114-v3`
|
||||
**Fecha:** 14 Enero 2025 - 11:34
|
||||
**Estado:** ✅ Funcionando
|
||||
|
||||
#### Cambios:
|
||||
- ✅ **Login redirige correctamente a customizer**
|
||||
- ✅ Añadido debugging completo (backend + frontend)
|
||||
- ✅ Content-Type JSON explícito en respuesta
|
||||
- ✅ Flush=True en todos los prints
|
||||
- ✅ Console.log en frontend para debugging
|
||||
- ✅ Verificación de sitios del usuario antes de redirigir
|
||||
|
||||
#### Funcionalidad:
|
||||
- Login → Busca primer sitio del usuario
|
||||
- Si tiene sitio → `/customizer/{site_id}`
|
||||
- Si no tiene sitio → `/dashboard`
|
||||
- Frontend recibe y procesa correctamente el redirect
|
||||
|
||||
---
|
||||
|
||||
### Hash: `gkachele-argentina-20250114-v2`
|
||||
**Fecha:** 14 Enero 2025 - 11:15
|
||||
|
||||
#### Cambios:
|
||||
- Documentación objetivos Argentina
|
||||
- Gitea y workflows documentados
|
||||
- Subdominios y pagos documentados
|
||||
|
||||
---
|
||||
|
||||
### Hash: `gkachele-template-system-20250114-v1`
|
||||
**Fecha:** 14 Enero 2025 - 08:50
|
||||
|
||||
#### Cambios:
|
||||
- Sistema modular de templates
|
||||
- Menús y widgets dinámicos
|
||||
- Sistema de roles basado en DB
|
||||
|
||||
---
|
||||
|
||||
**Ultima actualizacion:** 09 Febrero 2026
|
||||
|
||||
---
|
||||
|
||||
**© 2025 GKACHELE™. Todos los derechos reservados.**
|
||||
|
||||
## 🔄 Historial de Versiones
|
||||
|
||||
### Hash: `gkachele-builder-docs-20260208-v1`
|
||||
**Fecha:** 08 Febrero 2026
|
||||
**Estado:** ✅ Documentado
|
||||
|
||||
#### Cambios:
|
||||
- ✅ Guía rápida del builder en `memoria/BUILDER_ELEMENTOR.md`
|
||||
- ✅ Entrada en índice de documentación
|
||||
|
||||
---
|
||||
|
||||
### Hash: `gkachele-docs-sync-20260214-v1`
|
||||
**Fecha:** 14 Febrero 2026
|
||||
**Estado:** OK Documentacion unificada
|
||||
|
||||
#### Cambios:
|
||||
- Fuente vigente consolidada en `codex/*`.
|
||||
- `codex/VERSIONADO_IA.md` actualizado con protocolo fijo de sync local/remoto.
|
||||
- `codex/MEMORIA_CODEX.md` actualizado con punto exacto (rama, commit, version base, estado sync).
|
||||
- `memoria/ESTADO_ACTUAL.md` marcado como historico para evitar decisiones con contexto 2025.
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
104
codex/MEMORIA_CODEX.md
Normal file
104
codex/MEMORIA_CODEX.md
Normal file
@@ -0,0 +1,104 @@
|
||||
# Memoria Codex - GKACHELE
|
||||
|
||||
Entendido.
|
||||
|
||||
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/MEMORIA_CODEX.md`
|
||||
- `STATE.md` (raiz del workspace, memoria operativa Docker/SaaS desde 2026-02-28)
|
||||
|
||||
## Estado acordado (retomar desde aqui)
|
||||
- Rama activa: `ai/ub24-builder-v1`
|
||||
- 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 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)
|
||||
|
||||
## 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`.
|
||||
|
||||
## 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.
|
||||
|
||||
## 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.
|
||||
77
codex/SESSION_STATE.md
Normal file
77
codex/SESSION_STATE.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# SESSION STATE - Live Memory
|
||||
|
||||
Last update: 2026-03-10 (builder glass preset + ignore cleanup)
|
||||
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`).
|
||||
9. Added universal LLM router toolkit for multi-project reuse at `tools/llm_universal` with provider fallback, cooldown, and SQLite usage tracking.
|
||||
10. Stored cross-project LLM execution plan in root file `llm`.
|
||||
11. Configured and verified `GITHUB_TOKEN` and `CLOUDFLARE_API_TOKEN`/`CLOUDFLARE_ACCOUNT_ID` in user env.
|
||||
12. Improved universal router behavior: missing-key fallback handling + round-robin balancing by route.
|
||||
13. Validated unified multi-provider flow with real calls (fallback to GitHub Models when other candidates fail).
|
||||
14. Added unified chat interface `tools/llm_universal/chat_cli.py` with persistent context via `chat_history.json`.
|
||||
15. Documented full LLM implementation log in `codex/LLM_UNIVERSAL_LOG.md`.
|
||||
16. Enabled OpenRouter in user env from local Cline secrets and validated live calls.
|
||||
17. Added unified HTTP API server for LLM (`tools/llm_universal/api_server.py`) with persistent sessions and context continuity.
|
||||
18. Added run/test scripts for immediate usage (`run_server.ps1`, `test_api.ps1`) and validated E2E flow.
|
||||
19. Finalized universal assistant as standalone service in `tools/llm_universal` (detached from `demo.app`), with browser chat on `:5055`.
|
||||
20. Installed Task Scheduler autostart (`GKACHELE-LLM-Chat-Autostart`) to launch at user logon.
|
||||
21. Locked assistant web to fixed agent mode and route (`agent`) to avoid manual model switching.
|
||||
22. Added startup memory preload in agent sessions (`AGENTS.md`, `agent.md`, `codex/SESSION_STATE.md`, `codex/VERSIONADO_IA.md`).
|
||||
23. Verified LLM GUI server health on `127.0.0.1:5055` and clarified `304` responses are normal cache behavior for `GET /assistant`.
|
||||
24. Verified Groq API key is valid (direct `GET /models` and `POST /chat/completions` return `200`).
|
||||
25. Started and validated both local GUIs:
|
||||
- LLM assistant: `http://127.0.0.1:5055/assistant` (health: `GET /api/llm/health`)
|
||||
- UB24 builder: `http://127.0.0.1:5001/elementor/1`
|
||||
26. Added xAI (Grok) provider routing to `tools/llm_universal/provider_config.example.json` and set `XAI_API_KEY` in user env.
|
||||
27. xAI endpoints returned `403` for both `GET /v1/models` and `POST /v1/chat/completions` with the provided key (base URL `https://api.x.ai/v1`).
|
||||
28. Added OpenAI provider using Responses API with `codex-mini-latest` in routes; implemented Responses payload parsing in router and updated key setup/checklist and autostart env propagation.
|
||||
29. Added menu glass/dark/default presets and hover/blur styling in `elementor/templates/elementor_builder.html`; logging of incoming presets to `logs/elementor_save.log`.
|
||||
30. Expanded `.gitignore` to drop local artifacts (db/logs/snapshots/__pycache__/gitea_data/free-llm resources, etc.).
|
||||
|
||||
## In progress
|
||||
1. Final QA of restaurante flow end-to-end (order, menu links, responsive, publish persistence).
|
||||
2. Enable additional provider keys (beyond OpenRouter) for universal LLM fallback.
|
||||
3. Stabilize free-tier provider mix (monitor Groq/auth errors if they reappear and keep fallback stable).
|
||||
4. Prepare first project integration using `/api/llm/chat`.
|
||||
5. Standalone service smoke validated (`/assistant` + `/api/llm/health` + chat POST).
|
||||
|
||||
## Blockers
|
||||
1. xAI endpoints returned `403` with the provided key; need a valid xAI API key and/or the correct base URL from the xAI console.
|
||||
2. OpenAI provider added but not validated yet (requires `OPENAI_API_KEY`).
|
||||
3. Agent UI occasionally shows raw tool-call JSON (e.g. `{"type":"tool_call",...}`) instead of executing tool and returning final natural-language response.
|
||||
|
||||
## Next 3 steps
|
||||
1. If `:5055` goes down again, inspect `tools/llm_universal/_api_err.log` for crash details and adjust autostart to restart on failure.
|
||||
2. Persist and auto-restore last `session_id` in web UI so context resumes automatically after PC restart.
|
||||
3. Fix agent tool-call parser/loop so raw JSON is never rendered to user and every valid tool call is executed before final answer.
|
||||
|
||||
## Quick handoff template (copy and fill at close)
|
||||
### What changed today
|
||||
-
|
||||
|
||||
### What was validated
|
||||
-
|
||||
|
||||
### What failed (if any)
|
||||
-
|
||||
|
||||
### Next immediate action
|
||||
-
|
||||
192
codex/VERSIONADO_IA.md
Normal file
192
codex/VERSIONADO_IA.md
Normal file
@@ -0,0 +1,192 @@
|
||||
# Versionado IA - UB24 / Elementor
|
||||
|
||||
## 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`).
|
||||
|
||||
## 1) Objetivo
|
||||
Definir un proceso de versionado auditable, reproducible y estable para el desarrollo del builder UB24.
|
||||
|
||||
## 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`
|
||||
|
||||
## 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): ...`
|
||||
|
||||
## 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`
|
||||
|
||||
## 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.
|
||||
|
||||
## 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`
|
||||
- Verificacion rapida:
|
||||
- `Invoke-WebRequest http://127.0.0.1:5001/elementor/1 -UseBasicParsing`
|
||||
|
||||
## 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.
|
||||
|
||||
## 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` |
|
||||
|
||||
## 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.
|
||||
|
||||
## 17) Lote aplicado (2026-03-10)
|
||||
- Rama de trabajo: `ai/ub24-builder-v1`
|
||||
- Commits:
|
||||
- `442acb1 fix(menu): add glass preset and save logging`
|
||||
- `9ff6d2e chore: ignore local artifacts`
|
||||
- `32ea784 chore(session): log glass preset changes`
|
||||
- Cambios:
|
||||
1. Menú ahora soporta presets `default/glass/dark` con blur/sombra/hover; selector en inspector y aplicación en render (`elementor/templates/elementor_builder.html`).
|
||||
2. Log de guardados de `/api/elementor/save` para capturar presets recibidos en `logs/elementor_save.log` (`elementor/routes.py`).
|
||||
3. `.gitignore` ampliado para excluir artefactos locales (db, logs, snapshots, __pycache__, gitea_data, free-llm resources).
|
||||
4. `codex/SESSION_STATE.md` actualizado con el estado de estos cambios y bloqueos vigentes.
|
||||
52
demo/app.py
52
demo/app.py
@@ -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"❌ EXCEPCIÓN: {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}")
|
||||
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
104
demo/db.py
Normal 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
|
||||
877
demo/templates/customizer_webflow.html
Normal file
877
demo/templates/customizer_webflow.html
Normal 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>
|
||||
@@ -22,7 +22,7 @@ def scan_available_themes():
|
||||
continue
|
||||
|
||||
try:
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
with open(config_path, 'r', encoding='utf-8-sig') as f:
|
||||
config = json.load(f)
|
||||
|
||||
themes[theme_dir] = {
|
||||
@@ -38,7 +38,7 @@ def scan_available_themes():
|
||||
'preview': f'/themes/{theme_dir}/preview.jpg' if os.path.exists(os.path.join(theme_path, 'preview.jpg')) else None
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error cargando template {theme_dir}: {e}")
|
||||
print(f"WARNING: Error cargando template {theme_dir}: {e}")
|
||||
continue
|
||||
|
||||
return themes
|
||||
@@ -50,10 +50,10 @@ def get_theme_config(theme_id):
|
||||
return None
|
||||
|
||||
try:
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
with open(config_path, 'r', encoding='utf-8-sig') as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error cargando config de {theme_id}: {e}")
|
||||
print(f"WARNING: Error cargando config de {theme_id}: {e}")
|
||||
return None
|
||||
|
||||
def get_themes_by_rubro(rubro, user_plan='base'):
|
||||
@@ -127,7 +127,7 @@ def render_gkachele_template(theme, content, site_id=None, user_id=None):
|
||||
menus = get_site_menus(site_id, user_id)
|
||||
widgets = get_site_widgets(site_id, user_id)
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error obteniendo menús/widgets: {e}")
|
||||
print(f"WARNING: Error obteniendo menus/widgets: {e}")
|
||||
|
||||
theme_template = ''
|
||||
theme_path = os.path.join(THEMES_DIR, theme, 'template.html')
|
||||
|
||||
149
elementor/routes.py
Normal file
149
elementor/routes.py
Normal file
@@ -0,0 +1,149 @@
|
||||
from flask import Blueprint, render_template, session, request, jsonify, make_response
|
||||
import json
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
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)
|
||||
|
||||
# Debug trace para capturar presets/menus que llegan del front
|
||||
try:
|
||||
log_dir = Path('logs')
|
||||
log_dir.mkdir(exist_ok=True)
|
||||
log_path = log_dir / 'elementor_save.log'
|
||||
snapshot = {
|
||||
"ts": datetime.utcnow().isoformat() + "Z",
|
||||
"site_id": site_id,
|
||||
"keys": list(content.keys()) if isinstance(content, dict) else [],
|
||||
"menu_preset": content.get('menu_preset') if isinstance(content, dict) else None,
|
||||
"menu_icon_style": content.get('menu_icon_style') if isinstance(content, dict) else None,
|
||||
"menu_card_bg": content.get('menu_card_bg') if isinstance(content, dict) else None,
|
||||
"social_preset": content.get('social_preset') if isinstance(content, dict) else None,
|
||||
}
|
||||
with log_path.open('a', encoding='utf-8') as f:
|
||||
f.write(json.dumps(snapshot, ensure_ascii=False) + "\n")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
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
|
||||
|
||||
3321
elementor/templates/elementor_builder.html
Normal file
3321
elementor/templates/elementor_builder.html
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,133 +1,20 @@
|
||||
# 📊 Estado Actual del Proyecto - GKACHELE™
|
||||
# Estado Actual del Proyecto - GKACHELE
|
||||
|
||||
**© 2025 GKACHELE™. Todos los derechos reservados.**
|
||||
**Fecha de actualizacion:** 14 Febrero 2026
|
||||
|
||||
## 🎯 Hash Actual
|
||||
## Aviso
|
||||
Este archivo queda como referencia historica del ciclo 2025 (customizer inicial).
|
||||
|
||||
**Hash:** `gkachele-customizer-wordpress-adaptado-20250115-002`
|
||||
**Fecha:** 15 Enero 2025
|
||||
**Estado:** ✅ **Demo WordPress completo creado - Listo para copiar y adaptar**
|
||||
## Fuente vigente de estado IA/UB24
|
||||
- `codex/VERSIONADO_IA.md`
|
||||
- `codex/MEMORIA_CODEX.md`
|
||||
- `codex/BUILDER_ELEMENTOR_VERSION.md`
|
||||
- `codex/HISTORIAL_CAMBIOS.md`
|
||||
|
||||
---
|
||||
## Estado vigente resumido
|
||||
- Rama activa: `ai/ub24-builder-v1`
|
||||
- Builder UB24 en evolucion activa (ciclo 2026).
|
||||
- Estado local/remoto verificado en sincronizacion al corte.
|
||||
|
||||
## ✅ FUNCIONANDO
|
||||
|
||||
### Flujo Completo:
|
||||
1. ✅ **Landing** - Funciona correctamente
|
||||
2. ✅ **Registro** - Crea usuario + sitio automático
|
||||
3. ✅ **Login** - Redirige DIRECTAMENTE a `/customizer/{site_id}`
|
||||
4. ✅ **Customizer** - Personalización funcional
|
||||
- Cambiar contenido
|
||||
- Cambiar colores
|
||||
- Cambiar tipografía
|
||||
- **✅ Añadir bloques (video, imagen, texto, redes sociales, mapa)**
|
||||
- **✅ Eliminar bloques**
|
||||
- **✅ Ver lista de bloques añadidos**
|
||||
- Guardar cambios (draft)
|
||||
- Enviar solicitud (pending)
|
||||
5. ✅ **Dashboard Cliente** - `/dashboard` - Ve sus sitios
|
||||
6. ✅ **Dashboard Admin** - `/admin` - Gestión completa
|
||||
- Ver usuarios
|
||||
- Eliminar usuarios
|
||||
- Ver solicitudes
|
||||
- Aprobar/rechazar sitios
|
||||
|
||||
### Sistema de Base de Datos:
|
||||
- ✅ Multi-tenant en `main.db`
|
||||
- ✅ Tablas: users, sites, menus, widgets, content, settings, requests, media
|
||||
- ✅ Sistema de roles basado en DB (administrator, editor, author, subscriber)
|
||||
- ✅ Filtrado por `user_id` (cada cliente solo ve sus datos)
|
||||
|
||||
### Templates:
|
||||
- ✅ Sistema modular (_gkachele/header.php, footer.php, sidebar.php)
|
||||
- ✅ Menús dinámicos (header, footer, sidebar)
|
||||
- ✅ Widgets dinámicos
|
||||
- ✅ Temas: restaurante-moderno, restaurante-elegante, gimnasio-claro
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ PENDIENTE
|
||||
|
||||
### Funcionalidades Core:
|
||||
- [x] **Añadir bloques (COMPLETADO)**
|
||||
- [x] **Eliminar bloques (COMPLETADO)**
|
||||
- [x] **Lista de bloques en sidebar (COMPLETADO)**
|
||||
- [ ] Editar contenido de bloques existentes
|
||||
- [ ] Mover bloques (drag & drop) en customizer
|
||||
- [ ] Editar inline (contenteditable)
|
||||
- [ ] UI completa para gestionar menús desde customizer
|
||||
|
||||
### Infraestructura:
|
||||
- [ ] Subdominios automáticos
|
||||
- [ ] Integración pagos Nominalia
|
||||
- [ ] Gitea y workflows
|
||||
- [ ] Control de versiones Git
|
||||
|
||||
### Mejoras:
|
||||
- [ ] Landing optimizada para Argentina
|
||||
- [ ] Limitaciones por plan
|
||||
- [ ] Sistema de actividad/logs
|
||||
- [ ] Mejorar UI/UX
|
||||
|
||||
---
|
||||
|
||||
## 🚀 PRÓXIMOS PASOS
|
||||
|
||||
1. **Probar con 1 rubro completo** (restaurante)
|
||||
2. **Mejorar customizer** (mover bloques, editar inline)
|
||||
3. **Gitea y workflows** (despliegues automáticos)
|
||||
4. **Subdominios** (con tu dominio)
|
||||
5. **Pagos Nominalia** (comodines)
|
||||
|
||||
---
|
||||
|
||||
## 📍 DEPLOYMENT
|
||||
|
||||
### Actual: Raspberry Pi 3
|
||||
- ✅ Funcionando
|
||||
- ✅ Login → Customizer funciona
|
||||
- ✅ Todo operativo
|
||||
|
||||
### Futuro: VPS
|
||||
- Si funciona 1 rubro bien → Migrar a VPS
|
||||
- Mismo código
|
||||
- Más recursos
|
||||
- Dominio propio
|
||||
|
||||
---
|
||||
|
||||
## 🔄 ÚLTIMA ACTUALIZACIÓN
|
||||
|
||||
**Hash:** `gkachele-customizer-wordpress-adaptado-20250115-002`
|
||||
**Fecha:** 15 Enero 2025
|
||||
**Logro:** ✅ **Demo WordPress completo creado - Listo para copiar y adaptar**
|
||||
|
||||
### Cambios en este Hash:
|
||||
- ✅ Creado demo completo WordPress: `customizer-wordpress-COMPLETO.html`
|
||||
- ✅ Todas las funcionalidades de WordPress implementadas (paneles, toggles, sliders, repeaters, etc.)
|
||||
- ✅ Documentación completa: `CUSTOMIZER_WORDPRESS_COMPLETO.md`
|
||||
- ✅ Objetivo claro documentado: `OBJETIVO_CUSTOMIZER_ADAPTADO.md`
|
||||
- ✅ Identificados problemas críticos: `PROBLEMAS_CRITICOS_CUSTOMIZER.md`
|
||||
|
||||
### Plan para Próxima Sesión:
|
||||
1. **COPIAR WordPress** (demo completo)
|
||||
2. **AUTOMATIZAR** procesos (guardado, preview, etc.)
|
||||
3. **CAMBIAR branding** a GKACHELE™ (logo, nombres, colores)
|
||||
4. **ADAPTAR** funcionalidades con lo que ya sabemos
|
||||
5. **INTEGRAR** con backend Flask existente
|
||||
|
||||
### Archivos Creados:
|
||||
- `customizer-wordpress-COMPLETO.html` - Demo completo independiente
|
||||
- `CUSTOMIZER_WORDPRESS_COMPLETO.md` - Documentación completa
|
||||
- `OBJETIVO_CUSTOMIZER_ADAPTADO.md` - Objetivo claro
|
||||
- `PROBLEMAS_CRITICOS_CUSTOMIZER.md` - Problemas identificados
|
||||
|
||||
### Estado:
|
||||
- ✅ Demo WordPress completo (funcional, independiente)
|
||||
- ✅ Documentación completa
|
||||
- ✅ Objetivo claro definido
|
||||
- ⏳ Pendiente: Copiar y adaptar al customizer real
|
||||
|
||||
---
|
||||
|
||||
**© 2025 GKACHELE™. Todos los derechos reservados.**
|
||||
## Nota operativa
|
||||
No usar este archivo como fuente principal para decisiones de desarrollo actuales.
|
||||
|
||||
Reference in New Issue
Block a user