18 Commits

Author SHA1 Message Date
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
komkida91
53aa755c39 docs(builder): documentar arranque rapido UB24 en local 2026-02-14 19:30:37 +01:00
komkida91
075dad6f1a chore(versioning): unificar documentacion y protocolo de sincronizacion 2026-02-14 19:14:48 +01:00
9 changed files with 886 additions and 196 deletions

View 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`

261
codex/HISTORIAL_CAMBIOS.md Normal file
View File

@@ -0,0 +1,261 @@
# 📝 Historial de Cambios - GKACHELE™
**© 2025 GKACHELE™. Todos los derechos reservados.**
## 🔄 Historial de Versiones
### 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.**
### 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.
---

36
codex/MEMORIA_CODEX.md Normal file
View File

@@ -0,0 +1,36 @@
# Memoria Codex - GKACHELE
**Fecha de corte:** 14 Febrero 2026
## Fuente de verdad (estado IA)
- `codex/VERSIONADO_IA.md`
- `codex/BUILDER_ELEMENTOR_VERSION.md`
- `codex/HISTORIAL_CAMBIOS.md`
## Punto exacto
- 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`
## 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).
## 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>`).
## 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`.
## Comando de arranque
- `python -m demo.app` desde `c:\word`

View File

@@ -3,20 +3,18 @@
## Rama de trabajo ## Rama de trabajo
- `ai/ub24-builder-v1` - `ai/ub24-builder-v1`
## 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)
## Regla de trabajo ## Regla de trabajo
1. Cada cambio funcional se guarda en un commit separado. 1. Cada cambio funcional se guarda en un commit separado.
2. Cada commit se registra con su hash. 2. Cada commit se registra con su hash.
3. Cada commit debe incluir comando de reversión rápida. 3. Cada commit debe incluir comando de reversion rapida.
4. La rama debe quedar sincronizada con remoto al cerrar bloque de trabajo.
## Registro de hashes ## Convencion de mensaje
### Baseline
- Commit: `cb99f26`
- Objetivo: crear rama y política de versionado para trabajo IA.
- Revert:
- `git revert <hash>`
- o volver a commit previo: `git reset --hard <hash_anterior>` (solo si se aprueba explícitamente)
## Convención de mensaje
- `feat(builder): ...` - `feat(builder): ...`
- `fix(builder): ...` - `fix(builder): ...`
- `refactor(builder): ...` - `refactor(builder): ...`
@@ -27,6 +25,25 @@
2. Push continuo a `origin/ai/ub24-builder-v1`. 2. Push continuo a `origin/ai/ub24-builder-v1`.
3. Merge cuando validemos en local y Raspberry. 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:
- `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)
### Correccion historial ### Correccion historial
- Commit: `fe8657e` - Commit: `fe8657e`
- Objetivo: revertir commit no deseado y mantener separacion de cambios. - Objetivo: revertir commit no deseado y mantener separacion de cambios.
@@ -45,22 +62,6 @@
- Revert: - Revert:
- `git revert 1c04f04` - `git revert 1c04f04`
## 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.
## Control de rama (local/remoto)
- Rama local activa: `ai/ub24-builder-v1`
- Upstream remoto: `origin/ai/ub24-builder-v1`
- Estado al registrar: local `ahead 5`
- Politica: seguir commits atomicos y luego `git push origin ai/ub24-builder-v1` por lote validado.
## 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.
### Ajustes Builder (limpieza + preview + ancho) ### Ajustes Builder (limpieza + preview + ancho)
- Commit: `7c5f671` - 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. - 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.
@@ -72,3 +73,87 @@
- Objetivo: mejorar vista previa (forzar modo limpio y restaurar estado), eliminar precarga automatica de bloques, y agregar modo de menu (horizontal/acordeon/ambos). - Objetivo: mejorar vista previa (forzar modo limpio y restaurar estado), eliminar precarga automatica de bloques, y agregar modo de menu (horizontal/acordeon/ambos).
- Revert: - Revert:
- `git revert dd98e9d` - `git revert dd98e9d`
### Runtime unificado (app + elementor)
- Commit: `1a5778b`
- Objetivo: unificar arranque con `python -m demo.app` y registrar blueprint de Elementor en runtime principal.
- Revert:
- `git revert 1a5778b`
### Fix SQLite wrapper (arranque sin error SQL)
- Commit: `f6d8ab1`
- Objetivo: evitar conversiones SQL invalidas en SQLite que rompian inicializacion y generaban reintentos.
- Revert:
- `git revert f6d8ab1`
### API Elementor save/publish
- Commit: `b6fb4da`
- Objetivo: agregar endpoint dedicado `/api/elementor/save` para guardar builder con opcion de publicar.
- Revert:
- `git revert b6fb4da`
### Builder persistencia y feedback de publicacion
- Commit: `c2ee81d`
- Objetivo: mantener bloques cargados al entrar, normalizar bloques sin id y mostrar estado de guardado/publicacion en topbar.
- Revert:
- `git revert c2ee81d`
### Preview full-page + layout estable
- Commit: `e20f086`
- Objetivo: hacer que vista previa ocupe pagina completa y forzar layout por secciones (sin modo libre por defecto) para alinear bloques.
- Revert:
- `git revert e20f086`
### Full width + dos columnas por bloque
- Commit: `e5df6de`
- Objetivo: expandir ancho util del canvas y habilitar 2 columnas reales con toggle "Ancho completo" por bloque.
- Revert:
- `git revert e5df6de`
### Drag inteligente columnas + preview completo
- Commit: `a6089ee`
- Objetivo: permitir decidir 1 o 2 columnas moviendo bloques al soltar (centro=ancho completo, lados=media columna) y agregar opcion de preview completo en nueva pestana.
- Revert:
- `git revert a6089ee`
### Quitar layout global + preview completo real
- Commit: `f9f7d23`
- Objetivo: eliminar toggle global de 2 columnas; mantener decision 1/2 columnas solo por movimiento de bloques; y forzar preview completo sin margenes.
- Revert:
- `git revert f9f7d23`
### Fix final dia: texto, modo libre y full preview
- Commit: `f363eef`
- Objetivo: reparar caracteres mojibake en UI, habilitar modo libre real para mover bloques completos y forzar modo completo con `?full=1`.
- Revert:
- `git revert f363eef`
## 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 -m demo.app`
2. Abrir:
- `http://127.0.0.1:5001/elementor/1`
3. Verificacion rapida:
- `Invoke-WebRequest http://127.0.0.1:5001/elementor/1 -UseBasicParsing`
Notas:
- En el primer arranque puede tardar unos segundos adicionales por inicializacion de DB.
- Logs:
- `c:\word\logs_demo_app.txt`
- `c:\word\logs_demo_app.err`
## 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.
## 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.

View File

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

106
elementor/routes.py Normal file
View File

@@ -0,0 +1,106 @@
from flask import Blueprint, render_template, session, request, jsonify
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', **_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'
return 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
)
@elementor_bp.route('/elementor/<int:site_id>')
def elementor_view(site_id):
return _render_builder(site_id, builder_mode='default')
@elementor_bp.route('/ub24/<int:site_id>')
def ub24_view(site_id):
return _render_builder(site_id, builder_mode='ub24')
@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()
return jsonify({'success': True, 'published': publish})

View File

@@ -17,14 +17,18 @@
.dot{width:10px;height:10px;border-radius:50%;background:linear-gradient(135deg,#59d9c8,#7aa7ff)} .dot{width:10px;height:10px;border-radius:50%;background:linear-gradient(135deg,#59d9c8,#7aa7ff)}
.section-title{font-size:11px;text-transform:uppercase;letter-spacing:1.2px;color:var(--muted);margin:12px 0 8px} .section-title{font-size:11px;text-transform:uppercase;letter-spacing:1.2px;color:var(--muted);margin:12px 0 8px}
.block-item{background:var(--panel2);border:1px solid var(--border);padding:8px 10px;border-radius:10px;margin-bottom:8px;cursor:grab} .block-item{background:var(--panel2);border:1px solid var(--border);padding:8px 10px;border-radius:10px;margin-bottom:8px;cursor:grab}
.main{padding:18px} .main{padding:10px}
.topbar{display:flex;align-items:center;gap:10px;margin-bottom:12px;flex-wrap:wrap} .topbar{display:flex;align-items:center;gap:10px;margin-bottom:12px;flex-wrap:wrap}
.save-status{font-size:12px;color:var(--muted);min-width:150px;text-align:right}
.save-status.ok{color:#34d399}
.save-status.error{color:#f87171}
.save-status.busy{color:#fbbf24}
.btn{background:var(--accent);color:#09121a;border:0;padding:8px 12px;border-radius:999px;font-weight:700;cursor:pointer;transition:transform .2s ease,box-shadow .2s ease} .btn{background:var(--accent);color:#09121a;border:0;padding:8px 12px;border-radius:999px;font-weight:700;cursor:pointer;transition:transform .2s ease,box-shadow .2s ease}
.btn:hover{transform:translateY(-1px);box-shadow:0 10px 22px rgba(15,23,42,.15)} .btn:hover{transform:translateY(-1px);box-shadow:0 10px 22px rgba(15,23,42,.15)}
.btn.secondary{background:transparent;color:var(--text);border:1px solid var(--border)} .btn.secondary{background:transparent;color:var(--text);border:1px solid var(--border)}
.btn.icon{width:34px;height:34px;padding:0;display:flex;align-items:center;justify-content:center} .btn.icon{width:34px;height:34px;padding:0;display:flex;align-items:center;justify-content:center}
.btn.active{border-color:var(--accent);box-shadow:0 0 0 2px rgba(89,217,200,.15)} .btn.active{border-color:var(--accent);box-shadow:0 0 0 2px rgba(89,217,200,.15)}
.preview-shell{background:#0f1520;border:1px solid #222b3a;border-radius:18px;padding:14px;width:100%;max-width:none;margin:0 auto} .preview-shell{background:#0f1520;border:1px solid #222b3a;border-radius:18px;padding:0;width:100%;max-width:none;margin:0 auto}
body.ub24 .preview-shell{background:#eef1f6;border-color:#e5e7eb} body.ub24 .preview-shell{background:#eef1f6;border-color:#e5e7eb}
body.ub24 .apple-bar{display:none} body.ub24 .apple-bar{display:none}
body.ub24 .preview-shell{padding:0;border-radius:12px} body.ub24 .preview-shell{padding:0;border-radius:12px}
@@ -45,7 +49,7 @@
.apple-bar{background:#f8fafc;border-bottom:1px solid #e2e8f0;padding:8px 12px;font-size:12px;color:#334155;display:flex;align-items:center;gap:6px} .apple-bar{background:#f8fafc;border-bottom:1px solid #e2e8f0;padding:8px 12px;font-size:12px;color:#334155;display:flex;align-items:center;gap:6px}
.apple-dot{width:9px;height:9px;border-radius:50%} .apple-dot{width:9px;height:9px;border-radius:50%}
.red{background:#f87171}.yellow{background:#fbbf24}.green{background:#4ade80} .red{background:#f87171}.yellow{background:#fbbf24}.green{background:#4ade80}
.canvas{min-height:700px;padding:34px;background:#f6f7fb;transition:background .6s ease,color .4s ease; color:var(--site-text,#0b0c10)} .canvas{min-height:700px;padding:18px;background:#f6f7fb;transition:background .6s ease,color .4s ease; color:var(--site-text,#0b0c10)}
.block{background:var(--site-card,#fff);border-radius:var(--radius-md);padding:var(--space-3);margin-bottom:var(--space-4);border:1px solid transparent;box-shadow:0 12px 30px rgba(15,23,42,.08);transition:transform .25s ease,box-shadow .25s ease,border-color .25s ease;animation:fadeUp .25s ease;color:var(--site-text,#0b0c10);position:relative;will-change:transform;cursor:grab} .block{background:var(--site-card,#fff);border-radius:var(--radius-md);padding:var(--space-3);margin-bottom:var(--space-4);border:1px solid transparent;box-shadow:0 12px 30px rgba(15,23,42,.08);transition:transform .25s ease,box-shadow .25s ease,border-color .25s ease;animation:fadeUp .25s ease;color:var(--site-text,#0b0c10);position:relative;will-change:transform;cursor:grab}
.block.dragging,.block.resizing{transition:none;cursor:grabbing} .block.dragging,.block.resizing{transition:none;cursor:grabbing}
body.dragging{user-select:none} body.dragging{user-select:none}
@@ -159,6 +163,28 @@
.preview-mode .block-actions, .preview-mode .block-actions,
.preview-mode .resize-handle, .preview-mode .resize-handle,
.preview-mode .scroll-btn{display:none !important} .preview-mode .scroll-btn{display:none !important}
body.preview-mode .app{grid-template-columns:1fr !important}
body.preview-mode .sidebar,
body.preview-mode .inspector,
body.preview-mode .topbar{display:none !important}
body.preview-mode .main{padding:0 !important}
body.preview-mode .preview-shell{
max-width:100% !important;
border:0 !important;
border-radius:0 !important;
padding:0 !important;
margin:0 !important;
background:transparent !important;
}
body.preview-mode .apple{
border:0 !important;
border-radius:0 !important;
min-height:100vh;
}
body.preview-mode .canvas{
min-height:100vh !important;
padding:0 !important;
}
.resize-handle{position:absolute;right:8px;bottom:8px;width:14px;height:14px;border-right:2px solid #94a3b8;border-bottom:2px solid #94a3b8;cursor:se-resize;opacity:.75} .resize-handle{position:absolute;right:8px;bottom:8px;width:14px;height:14px;border-right:2px solid #94a3b8;border-bottom:2px solid #94a3b8;cursor:se-resize;opacity:.75}
.resize-handle.edge{width:10px;height:10px;border:none;background:rgba(148,163,184,.35);border-radius:4px;opacity:.9} .resize-handle.edge{width:10px;height:10px;border:none;background:rgba(148,163,184,.35);border-radius:4px;opacity:.9}
.resize-handle.e{top:50%;right:-4px;transform:translateY(-50%);cursor:e-resize} .resize-handle.e{top:50%;right:-4px;transform:translateY(-50%);cursor:e-resize}
@@ -246,13 +272,15 @@
</select> </select>
<button class="btn secondary" id="btnBack">Atras</button> <button class="btn secondary" id="btnBack">Atras</button>
<button class="btn secondary" id="btnPreview">Vista previa</button> <button class="btn secondary" id="btnPreview">Vista previa</button>
<button class="btn secondary" id="btnFullPage">Completo</button>
<button class="btn secondary" id="btnTheme">Claro</button> <button class="btn secondary" id="btnTheme">Claro</button>
<button class="btn secondary icon" id="btnSizePhone" title="Movil"><i class="fa-solid fa-mobile-screen-button"></i></button> <button class="btn secondary icon" id="btnSizePhone" title="Movil"><i class="fa-solid fa-mobile-screen-button"></i></button>
<button class="btn secondary icon" id="btnSizeTablet" title="Tablet"><i class="fa-solid fa-tablet-screen-button"></i></button> <button class="btn secondary icon" id="btnSizeTablet" title="Tablet"><i class="fa-solid fa-tablet-screen-button"></i></button>
<button class="btn secondary icon active" id="btnSizeDesktop" title="Web"><i class="fa-solid fa-display"></i></button> <button class="btn secondary icon active" id="btnSizeDesktop" title="Web"><i class="fa-solid fa-display"></i></button>
<button class="btn secondary" id="btnFreeDrag" style="display:none">Modo libre</button> <button class="btn secondary" id="btnFreeDrag">Modo libre</button>
<button class="btn secondary" id="btnAlign">Alinear</button> <button class="btn secondary" id="btnAlign">Alinear</button>
<button class="btn" id="btnSave">Publicar</button> <button class="btn" id="btnSave">Publicar</button>
<div class="save-status" id="saveStatus">Listo</div>
</div> </div>
</div> </div>
<div class="preview-shell"> <div class="preview-shell">
@@ -349,7 +377,6 @@
<details class="acc"> <details class="acc">
<summary>Layout</summary> <summary>Layout</summary>
<div class="acc-body"> <div class="acc-body">
<div class="row"><label>Layout 2 columnas</label><input id="twoColToggle" type="checkbox"></div>
<div class="row"><label>Animaciones</label><input id="animToggle" type="checkbox"></div> <div class="row"><label>Animaciones</label><input id="animToggle" type="checkbox"></div>
</div> </div>
</details> </details>
@@ -361,6 +388,7 @@
const SITE_SLUG = "{{ slug }}"; const SITE_SLUG = "{{ slug }}";
const SERVER_CONTENT = {{ content|tojson }}; const SERVER_CONTENT = {{ content|tojson }};
const BUILDER_MODE = "{{ builder_mode or 'default' }}"; const BUILDER_MODE = "{{ builder_mode or 'default' }}";
const FULL_PAGE_MODE = new URLSearchParams(window.location.search).get("full") === "1";
const defaultSettings = { const defaultSettings = {
site_name: "{{ slug }}", site_name: "{{ slug }}",
primary_color: "#59d9c8", primary_color: "#59d9c8",
@@ -369,10 +397,9 @@
muted_color: "#6b7280", muted_color: "#6b7280",
font_body: "Manrope", font_body: "Manrope",
font_heading: "Manrope", font_heading: "Manrope",
free_drag: true, free_drag: false,
bg_color2: "#e9eef5", bg_color2: "#e9eef5",
bg_gradient: false, bg_gradient: false,
two_col: true,
animations: true, animations: true,
theme: "light", theme: "light",
bg_motion: "none", bg_motion: "none",
@@ -447,6 +474,7 @@ const state = {
let pendingMove = null; let pendingMove = null;
let pendingResize = null; let pendingResize = null;
let previewStateBefore = null; let previewStateBefore = null;
let isSaving = false;
function makeId(){ return "block_" + Date.now() + "_" + Math.floor(Math.random()*1000); } function makeId(){ return "block_" + Date.now() + "_" + Math.floor(Math.random()*1000); }
function getDefaultPos(){ function getDefaultPos(){
@@ -457,7 +485,7 @@ const state = {
} }
function defaultData(type){ function defaultData(type){
switch(type){ switch(type){
case "menu": return { title:"Menu", items:[], menu_mode:"both" }; case "menu": return { title:"Menu", items:[], menu_mode:"both", full_width:true };
case "hero": return { title:"Tu propuesta de valor", subtitle:"Explica en una frase por que elegirte.", button_text:"Contactar", button_url:"#contacto", image_url:"" }; case "hero": return { title:"Tu propuesta de valor", subtitle:"Explica en una frase por que elegirte.", button_text:"Contactar", button_url:"#contacto", image_url:"" };
case "text": return { text:"Describe tu negocio aqui." }; case "text": return { text:"Describe tu negocio aqui." };
case "image": return { url:"", alt:"", caption:"", fit:"cover", overlay_text:"" }; case "image": return { url:"", alt:"", caption:"", fit:"cover", overlay_text:"" };
@@ -475,7 +503,15 @@ const state = {
default: return {}; default: return {};
} }
} }
function escapeHtml(str){return String(str||"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/\"/g,"&quot;").replace(/'/g,"&#039;");} function repairMojibake(text){
const raw = String(text || "");
if (!/[ÃÂ]/.test(raw)) return raw;
try { return decodeURIComponent(escape(raw)); } catch(_e) { return raw; }
}
function escapeHtml(str){
const safe = repairMojibake(str);
return String(safe).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/\"/g,"&quot;").replace(/'/g,"&#039;");
}
function editable(tag, field, text, placeholder, multiline, style){ function editable(tag, field, text, placeholder, multiline, style){
const ph = placeholder ? ` data-placeholder="${escapeHtml(placeholder)}"` : ` data-placeholder=""`; const ph = placeholder ? ` data-placeholder="${escapeHtml(placeholder)}"` : ` data-placeholder=""`;
const ml = multiline ? ` data-multiline="true"` : ""; const ml = multiline ? ` data-multiline="true"` : "";
@@ -922,10 +958,11 @@ const state = {
const inner = document.createElement("div"); const inner = document.createElement("div");
inner.style.position = "relative"; inner.style.position = "relative";
inner.style.zIndex = "1"; inner.style.zIndex = "1";
if (!state.settings.free_drag && state.settings.two_col){ if (!state.settings.free_drag){
inner.style.display = "grid"; inner.style.display = "flex";
inner.style.gridTemplateColumns = "1fr 1fr"; inner.style.flexWrap = "wrap";
inner.style.gap = "16px"; inner.style.gap = "16px";
inner.style.width = "100%";
} }
canvas.appendChild(inner); canvas.appendChild(inner);
if (!state.blocks.length){ inner.innerHTML = '<div class="empty">Arrastra bloques para empezar.</div>'; return; } if (!state.blocks.length){ inner.innerHTML = '<div class="empty">Arrastra bloques para empezar.</div>'; return; }
@@ -945,7 +982,7 @@ const state = {
el.style.left = "0px"; el.style.left = "0px";
el.style.top = "0px"; el.style.top = "0px";
el.style.transform = `translate3d(${pos.x}px, ${pos.y}px, 0)`; el.style.transform = `translate3d(${pos.x}px, ${pos.y}px, 0)`;
const defaultWidth = block.type === "menu" ? 90 : (state.settings.two_col ? 48 : 70); const defaultWidth = block.type === "menu" ? 90 : 70;
const w = Math.max(30, Math.min(100, Number(block.data?.width || defaultWidth))); const w = Math.max(30, Math.min(100, Number(block.data?.width || defaultWidth)));
el.style.width = `${w}%`; el.style.width = `${w}%`;
const h = Number(block.data?.height || 0); const h = Number(block.data?.height || 0);
@@ -953,15 +990,14 @@ const state = {
el.style.cursor = "grab"; el.style.cursor = "grab";
} else { } else {
el.style.transform = ""; el.style.transform = "";
if (state.settings.two_col){ const isFullWidth = block.type === "menu" || !!block.data?.full_width;
const fullTypes = ["menu","hero","gallery","contact","video","map","calendar"]; if (isFullWidth){
if (fullTypes.includes(block.type)){ el.style.gridColumn = "1 / -1"; }
const w = Math.max(30, Math.min(100, Number(block.data?.width || 100))); const w = Math.max(30, Math.min(100, Number(block.data?.width || 100)));
el.style.width = `${w}%`; el.style.width = `${w}%`;
el.style.justifySelf = "start"; el.style.flex = "0 0 100%";
} else { } else {
const w = Math.max(30, Math.min(100, Number(block.data?.width || 100))); el.style.width = "calc(50% - 8px)";
el.style.width = `${w}%`; el.style.flex = "0 0 calc(50% - 8px)";
} }
} }
el.innerHTML = renderBlockHtml(block); el.innerHTML = renderBlockHtml(block);
@@ -1053,6 +1089,13 @@ const state = {
} }
function removeDrop(){ if (dropIndicator && dropIndicator.parentNode) dropIndicator.parentNode.removeChild(dropIndicator); dropIndicator=null; } function removeDrop(){ if (dropIndicator && dropIndicator.parentNode) dropIndicator.parentNode.removeChild(dropIndicator); dropIndicator=null; }
function resolveFullWidthByDropX(container, clientX){
if (state.settings.free_drag) return null;
const rect = container.getBoundingClientRect();
if (!rect || !rect.width) return null;
const ratio = (clientX - rect.left) / rect.width;
return ratio > 0.33 && ratio < 0.67;
}
function getDropIndex(container,y){ function getDropIndex(container,y){
const blocks=[...container.querySelectorAll(".block")]; const blocks=[...container.querySelectorAll(".block")];
for (let i=0;i<blocks.length;i++){ const r=blocks[i].getBoundingClientRect(); if (y < r.top + r.height/2) return i; } for (let i=0;i<blocks.length;i++){ const r=blocks[i].getBoundingClientRect(); if (y < r.top + r.height/2) return i; }
@@ -1067,15 +1110,22 @@ const state = {
} }
function addBlock(type,index=state.blocks.length){ function addBlock(type,index=state.blocks.length){
const b={ id: makeId(), type, data: defaultData(type) }; const b={ id: makeId(), type, data: defaultData(type) };
if (!state.settings.free_drag && type !== "menu"){
b.data.full_width = false;
}
if (BUILDER_MODE === "ub24"){ b.page = currentPage; } if (BUILDER_MODE === "ub24"){ b.page = currentPage; }
if (state.settings.free_drag){ b.pos = getDefaultPos(); } if (state.settings.free_drag){ b.pos = getDefaultPos(); }
state.blocks.splice(index,0,b); selectedBlockId=b.id; renderInspector(); renderPreview(); state.blocks.splice(index,0,b); selectedBlockId=b.id; renderInspector(); renderPreview();
} }
function moveBlock(id,toIndex){ function moveBlock(id,toIndex,opts={}){
const from=state.blocks.findIndex(b=>b.id===id); const from=state.blocks.findIndex(b=>b.id===id);
if (from<0) return; if (from<0) return;
const [b]=state.blocks.splice(from,1); const [b]=state.blocks.splice(from,1);
state.blocks.splice(toIndex,0,b); state.blocks.splice(toIndex,0,b);
if (opts && typeof opts.fullWidth === "boolean" && b.type !== "menu"){
b.data = b.data || {};
b.data.full_width = opts.fullWidth;
}
renderPreview(); renderPreview();
} }
@@ -1088,6 +1138,9 @@ const state = {
const input=(label,id,val)=>`<div class="row"><label>${label}</label><input id="${id}" type="text" value="${escapeHtml(val||"")}"></div>`; const input=(label,id,val)=>`<div class="row"><label>${label}</label><input id="${id}" type="text" value="${escapeHtml(val||"")}"></div>`;
const widthVal = Math.max(30, Math.min(100, Number(data.width || 100))); const widthVal = Math.max(30, Math.min(100, Number(data.width || 100)));
html += `<div class="row"><label>Ancho bloque (%)</label><input id="blockWidth" type="range" min="30" max="100" value="${widthVal}"><div id="blockWidthValue" style="margin-top:4px;color:var(--muted);font-size:12px">${widthVal}%</div></div>`; html += `<div class="row"><label>Ancho bloque (%)</label><input id="blockWidth" type="range" min="30" max="100" value="${widthVal}"><div id="blockWidthValue" style="margin-top:4px;color:var(--muted);font-size:12px">${widthVal}%</div></div>`;
if (!state.settings.free_drag && block.type !== "menu"){
html += `<div class="row"><label>Ancho completo</label><input id="blockFullWidth" type="checkbox" ${data.full_width ? "checked" : ""}></div>`;
}
if (block.type==="menu"){ if (block.type==="menu"){
html+=input("Titulo","menuTitle",data.title); html+=input("Titulo","menuTitle",data.title);
const mm = escapeHtml(data.menu_mode || "both"); const mm = escapeHtml(data.menu_mode || "both");
@@ -1212,6 +1265,10 @@ const state = {
if (widthEl){ if (widthEl){
block.data.width = Math.max(30, Math.min(100, Number(widthEl.value || 100))); block.data.width = Math.max(30, Math.min(100, Number(widthEl.value || 100)));
} }
const fullWidthEl = document.getElementById("blockFullWidth");
if (fullWidthEl && block.type !== "menu"){
block.data.full_width = !!fullWidthEl.checked;
}
if (block.type==="menu"){ if (block.type==="menu"){
block.data.title=document.getElementById("menuTitle").value; block.data.title=document.getElementById("menuTitle").value;
const mm = document.getElementById("menuMode"); const mm = document.getElementById("menuMode");
@@ -1311,12 +1368,19 @@ const state = {
if (id && !state.settings.free_drag){ if (id && !state.settings.free_drag){
const container=canvas.querySelector("div")||canvas; const container=canvas.querySelector("div")||canvas;
const index=getDropIndex(container,e.clientY); const index=getDropIndex(container,e.clientY);
moveBlock(id,index); const fullWidth = resolveFullWidthByDropX(container, e.clientX);
moveBlock(id,index,{ fullWidth });
} else if (type){ } else if (type){
const b={ id: makeId(), type, data: defaultData(type) }; const b={ id: makeId(), type, data: defaultData(type) };
if (state.settings.free_drag){ if (state.settings.free_drag){
const rect = canvas.getBoundingClientRect(); const rect = canvas.getBoundingClientRect();
b.pos = { x: Math.max(0, e.clientX - rect.left - 20), y: Math.max(0, e.clientY - rect.top - 20) }; b.pos = { x: Math.max(0, e.clientX - rect.left - 20), y: Math.max(0, e.clientY - rect.top - 20) };
} else {
const container=canvas.querySelector("div")||canvas;
const fullWidth = resolveFullWidthByDropX(container, e.clientX);
if (typeof fullWidth === "boolean" && type !== "menu"){
b.data.full_width = fullWidth;
}
} }
state.blocks.push(b); state.blocks.push(b);
selectedBlockId=b.id; selectedBlockId=b.id;
@@ -1530,7 +1594,6 @@ const state = {
const bgColor=document.getElementById("bgColorInput"); const bgColor=document.getElementById("bgColorInput");
const bgColor2=document.getElementById("bgColor2Input"); const bgColor2=document.getElementById("bgColor2Input");
const bgGradient=document.getElementById("bgGradientToggle"); const bgGradient=document.getElementById("bgGradientToggle");
const twoCol=document.getElementById("twoColToggle");
const textColor=document.getElementById("textColorInput"); const textColor=document.getElementById("textColorInput");
const mutedColor=document.getElementById("mutedColorInput"); const mutedColor=document.getElementById("mutedColorInput");
const fontBody=document.getElementById("fontBodySelect"); const fontBody=document.getElementById("fontBodySelect");
@@ -1546,7 +1609,6 @@ const state = {
siteName.value=s.site_name||""; primary.value=s.primary_color||"#59d9c8"; siteName.value=s.site_name||""; primary.value=s.primary_color||"#59d9c8";
bgColor.value=s.bg_color||"#f6f7fb"; bgColor2.value=s.bg_color2||"#e9eef5"; bgGradient.checked=!!s.bg_gradient; bgColor.value=s.bg_color||"#f6f7fb"; bgColor2.value=s.bg_color2||"#e9eef5"; bgGradient.checked=!!s.bg_gradient;
bgMotion.value=s.bg_motion||"none"; bgMotion.value=s.bg_motion||"none";
twoCol.checked=!!s.two_col;
textColor.value=s.text_color||"#0b0c10"; mutedColor.value=s.muted_color||"#6b7280"; textColor.value=s.text_color||"#0b0c10"; mutedColor.value=s.muted_color||"#6b7280";
fontBody.value=s.font_body||"Manrope"; fontHeading.value=s.font_heading||"Manrope"; fontBody.value=s.font_body||"Manrope"; fontHeading.value=s.font_heading||"Manrope";
logoDrop.textContent = s.logo_url ? "Logo cargado" : "Suelta imagen o click"; logoDrop.textContent = s.logo_url ? "Logo cargado" : "Suelta imagen o click";
@@ -1560,7 +1622,6 @@ const state = {
bgColor2.addEventListener("input",()=>{ s.bg_color2=bgColor2.value; renderPreview(); }); bgColor2.addEventListener("input",()=>{ s.bg_color2=bgColor2.value; renderPreview(); });
bgGradient.addEventListener("change",()=>{ s.bg_gradient=bgGradient.checked; renderPreview(); }); bgGradient.addEventListener("change",()=>{ s.bg_gradient=bgGradient.checked; renderPreview(); });
bgMotion.addEventListener("change",()=>{ s.bg_motion=bgMotion.value; renderPreview(); }); bgMotion.addEventListener("change",()=>{ s.bg_motion=bgMotion.value; renderPreview(); });
twoCol.addEventListener("change",()=>{ s.two_col=twoCol.checked; renderPreview(); });
textColor.addEventListener("input",()=>{ s.text_color=textColor.value; renderPreview(); }); textColor.addEventListener("input",()=>{ s.text_color=textColor.value; renderPreview(); });
mutedColor.addEventListener("input",()=>{ s.muted_color=mutedColor.value; renderPreview(); }); mutedColor.addEventListener("input",()=>{ s.muted_color=mutedColor.value; renderPreview(); });
fontBody.addEventListener("change",()=>{ s.font_body=fontBody.value; renderPreview(); }); fontBody.addEventListener("change",()=>{ s.font_body=fontBody.value; renderPreview(); });
@@ -1572,7 +1633,25 @@ const state = {
bindDrop(logoDrop, logoFile, (data)=>{ s.logo_url = data; }, "Logo cargado"); bindDrop(logoDrop, logoFile, (data)=>{ s.logo_url = data; }, "Logo cargado");
bindDrop(bgDrop, bgFile, (data)=>{ s.bg_image_url = data; }, "Fondo cargado"); bindDrop(bgDrop, bgFile, (data)=>{ s.bg_image_url = data; }, "Fondo cargado");
} }
function wireFreeDragToggle(){ return; } function wireFreeDragToggle(){
const btn = document.getElementById("btnFreeDrag");
if (!btn) return;
const applyLabel = ()=>{
btn.textContent = state.settings.free_drag ? "Modo libre: ON" : "Modo libre";
btn.classList.toggle("active", !!state.settings.free_drag);
};
btn.addEventListener("click", ()=>{
state.settings.free_drag = !state.settings.free_drag;
if (state.settings.free_drag){
state.blocks.forEach((b)=>{
if (!b.pos) b.pos = getDefaultPos();
});
}
applyLabel();
renderPreview();
});
applyLabel();
}
function wirePreviewSize(){ function wirePreviewSize(){
const shell=document.querySelector(".preview-shell"); const shell=document.querySelector(".preview-shell");
if (!shell) return; if (!shell) return;
@@ -1657,19 +1736,66 @@ const state = {
}); });
renderPreview(); renderPreview();
} }
function setSaveStatus(msg, kind=""){
const status = document.getElementById("saveStatus");
if (!status) return;
status.textContent = msg;
status.className = `save-status${kind ? ` ${kind}` : ""}`;
}
function normalizeLoadedBlocks(blocks){
if (!Array.isArray(blocks)) return [];
return blocks
.filter((b)=>b && typeof b === "object")
.map((b)=>({
...b,
id: b.id || makeId(),
data: (b.data && typeof b.data === "object") ? b.data : {}
}));
}
async function saveContent(){ async function saveContent(){
const payload={ site_id: SITE_ID, content: { ...SERVER_CONTENT, settings: state.settings, blocks: state.blocks } }; if (isSaving) return;
try{ const res=await fetch("/api/customizer/save",{ method:"POST", headers:{ "Content-Type":"application/json" }, body: JSON.stringify(payload) }); const data=await res.json(); if (!data.success) throw new Error("save failed"); alert("Cambios guardados"); } isSaving = true;
catch(err){ console.error(err); alert("No se pudo guardar"); } const btn = document.getElementById("btnSave");
if (btn){
btn.disabled = true;
btn.textContent = "Publicando...";
}
setSaveStatus("Guardando cambios...", "busy");
const payload = {
site_id: SITE_ID,
publish: true,
content: { ...SERVER_CONTENT, settings: state.settings, blocks: state.blocks }
};
try{
const res = await fetch("/api/elementor/save",{
method:"POST",
headers:{ "Content-Type":"application/json" },
body: JSON.stringify(payload)
});
const data = await res.json();
if (!res.ok || !data.success) throw new Error(data.error || "save failed");
setSaveStatus("Publicado", "ok");
} catch(err){
console.error(err);
setSaveStatus("Error al publicar", "error");
} finally {
isSaving = false;
if (btn){
btn.disabled = false;
btn.textContent = "Publicar";
}
}
} }
function init(){ function init(){
// By default we keep section flow layout for stable full-page composition.
state.settings.free_drag = false;
state.blocks = normalizeLoadedBlocks(state.blocks);
if (BUILDER_MODE === "ub24"){ if (BUILDER_MODE === "ub24"){
state.blocks.forEach(b=>{ if (!b.page) b.page = "home"; }); state.blocks.forEach(b=>{ if (!b.page) b.page = "home"; });
} }
if (BUILDER_MODE === "ub24"){ if (BUILDER_MODE === "ub24"){
state.settings.free_drag = false; state.settings.free_drag = false;
} }
state.blocks = [];
selectedBlockId = null; selectedBlockId = null;
wireSidebar(); wirePreviewDrop(); wireInlineEditing(); wireSettings(); wireFreeDragToggle(); wireJumpSelect(); wirePreviewSize(); wireThemeToggle(); wireSidebar(); wirePreviewDrop(); wireInlineEditing(); wireSettings(); wireFreeDragToggle(); wireJumpSelect(); wirePreviewSize(); wireThemeToggle();
const backBtn = document.getElementById("btnBack"); const backBtn = document.getElementById("btnBack");
@@ -1694,11 +1820,11 @@ const state = {
} }
if (sidebar) sidebar.style.display = isPreview ? "none" : ""; if (sidebar) sidebar.style.display = isPreview ? "none" : "";
if (inspector) inspector.style.display = isPreview ? "none" : ""; if (inspector) inspector.style.display = isPreview ? "none" : "";
if (main) main.style.padding = isPreview ? "8px" : ""; if (main) main.style.padding = isPreview ? "0" : "";
if (shell){ if (shell){
if (isPreview){ if (isPreview){
shell.style.maxWidth = "100%"; shell.style.maxWidth = "100%";
shell.style.margin = "8px auto"; shell.style.margin = "0";
shell.classList.remove("size-phone","size-tablet","size-desktop"); shell.classList.remove("size-phone","size-tablet","size-desktop");
shell.classList.add("size-desktop"); shell.classList.add("size-desktop");
["btnSizePhone","btnSizeTablet","btnSizeDesktop"].forEach(id=>{ ["btnSizePhone","btnSizeTablet","btnSizeDesktop"].forEach(id=>{
@@ -1721,6 +1847,25 @@ const state = {
return; return;
}); });
} }
const fullPageBtn = document.getElementById("btnFullPage");
if (fullPageBtn){
fullPageBtn.addEventListener("click",()=>{
const url = `${window.location.pathname}?full=1`;
window.location.href = url;
});
}
if (FULL_PAGE_MODE && !document.body.classList.contains("preview-mode")){
document.body.classList.add("preview-mode");
const shell = document.querySelector(".preview-shell");
const main = document.querySelector(".main");
if (main) main.style.padding = "0";
if (shell){
shell.style.maxWidth = "100%";
shell.style.margin = "0";
shell.classList.remove("size-phone","size-tablet","size-desktop");
shell.classList.add("size-desktop");
}
}
const pageSelect = document.getElementById("pageSelect"); const pageSelect = document.getElementById("pageSelect");
if (pageSelect){ if (pageSelect){
pageSelect.addEventListener("change",()=>{ pageSelect.addEventListener("change",()=>{

View File

@@ -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` ## Fuente vigente de estado IA/UB24
**Fecha:** 15 Enero 2025 - `codex/VERSIONADO_IA.md`
**Estado:****Demo WordPress completo creado - Listo para copiar y adaptar** - `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 ## Nota operativa
No usar este archivo como fuente principal para decisiones de desarrollo actuales.
### 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.**