From 06d4781dc6b8ee3d246c450f0a270e4d1273afff Mon Sep 17 00:00:00 2001 From: komkida91 Date: Thu, 12 Feb 2026 19:03:21 +0100 Subject: [PATCH] fix(builder): robustecer carga local de elementor en windows --- GEMINI.md | 1 + demo/app.py | 4 +- demo/routes/customizer.py | 135 +- demo/templates/customizer.html | 2498 ++++++++++++-------------------- demo/utils/theme_engine.py | 24 +- 5 files changed, 978 insertions(+), 1684 deletions(-) create mode 100644 GEMINI.md diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000..43860ba --- /dev/null +++ b/GEMINI.md @@ -0,0 +1 @@ +## Gemini Added Memories- El flujo principal del proyecto GKACHELE™ es:1. **Visita a la Landing Page (`/`):** Un visitante llega.2. **Solicitud de Plan:** El cliente selecciona un plan, lo que abre un menú desplegable con un formulario. Lo rellena.3. **Redirección al Registro (`/register`):** Al enviar el formulario, es redirigido a `/register` para completar su registro.4. **Creación del Sitio en Borrador (`/customizer`):** El cliente diseña su página en modo borrador.5. **Envío de Solicitud:** El cliente envía su sitio para aprobación.6. **Tu Dashboard de Administrador (`/dashboard`):** La solicitud aparece en tu panel de control.7. **Aprobación o Rechazo Manual:** Tú revisas la solicitud y decides si la apruebas o la rechazas.8. **Panel de Cliente (`/admin`):** Si apruebas, el sitio se publica y se crea el panel de gestión para el cliente.9. **Visualización Pública:** El sitio aprobado se hace visible al público.- El proyecto del usuario está desplegado en una Raspberry Pi (IP local 192.168.1.134) como un servicio systemd (`gkachele-saas.service`). La actualización de código se realiza desde su PC (WSL) copiando archivos vía scp y reiniciando el servicio en la Raspberry Pi.- Para levantar el entorno de prueba del proyecto en el directorio 'demo/', los pasos son: 1. Navegar a `cd demo`. 2. Instalar dependencias con `pip install -r requirements.txt`. 3. Ejecutar la aplicación con `python3 app.py`. La aplicación correrá localmente, generalmente en `http://127.0.0.1:5000`.- El proyecto se llama GKACHELE™ y es un SaaS (Software as a Service) para crear sitios web, al estilo de un WordPress auto-alojado.- El backend del proyecto GKACHELE™ es una aplicación monolítica en Python/Flask, con el código principal ubicado en el directorio `demo/`.- El proyecto GKACHELE™ utiliza un motor de plantillas propio (`demo/utils/theme_engine.py`) que imita la funcionalidad de WordPress y actualmente usa una base de datos SQLite (`demo/database/main.db`).- El despliegue actual del proyecto GKACHELE™ es un proceso manual en una Raspberry Pi, utilizando scripts `scp` y gestionando el servicio con `systemd`. El control de versiones se realiza con Gitea.- El objetivo principal con el proyecto GKACHELE™ es modernizarlo de forma incremental. El plan es: 1. **Contenerizar la aplicación con Docker.** (LISTO - Dockerfile creado). 2. **Automatizar los despliegues (CI/CD) usando Gitea Actions.** (LISTO - Runner configurado y funcionando). 3. **Implementar Nuevo Customizer (Apple Style).** (EN PROGRESO - Estructura saas-demo replicada, falta pulir bugs). 4. **Configurar Secretos de Gitea.** (Pendiente por el usuario: Docker Hub y SSH keys). 5. **Migrar la base de datos a PostgreSQL.** (Pendiente). 6. **Migración final a VPS.** (Fácil después de validar Docker en la Pi).- **Estado Customizer (02/02/2026):** - Se ha reemplazado `customizer.html` usando `saas-demo.html` como base estricta (Estilo "Apple"). - Se han integrado los campos de Rubros (Menú, Contacto) en el sidebar nativo. - **BUG REPORTADO:** El desplazamiento/arrastre de bloques en el menú lateral falla ("no funciona desplazar el menu lateral como bloques"). POSIBLE CAUSA: Conflicto de scroll en sidebar o evento dragstart. - **FEATURE REQUEST:** Background de imagen debe estar disponible en TODOS los planes. Background de video SOLO en Premium.- **Hash de Seguridad (Customizer):** `96CA...B852` (Generado el 02/02/2026).- El archivo `.gitea/workflows/deploy.yml` define el workflow de CI/CD para la app GKACHELE™. Se activa con `push` a la rama `feature/docker-setup` o manualmente.- Los pasos del workflow de Gitea Actions incluyen: checkout de código, login a Docker Hub, build y push de la imagen Docker, y despliegue en Raspberry Pi vía SSH.- El usuario debe commitear y hacer push del archivo `deploy.yml` a Gitea, y luego configurar los secretos necesarios en la interfaz de Gitea.- Un paso futuro clave es la creación de un archivo `docker-compose.yml` específico para la Raspberry Pi.- El usuario de Gitea del proyecto es 'admin'.- La Raspberry Pi del usuario usa el hostname `komkida.duckdns.org` para acceso SSH (puerto 2222). La aplicación y Gitea usan `gk-saas.komkida.duckdns.org`.- Gitea actions activado en `app.ini`. Runner `rpi-runner-1` activo. \ No newline at end of file diff --git a/demo/app.py b/demo/app.py index a573613..e41abde 100644 --- a/demo/app.py +++ b/demo/app.py @@ -40,7 +40,7 @@ def handle_404(e): @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 @@ -51,5 +51,5 @@ def add_header(response): 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) diff --git a/demo/routes/customizer.py b/demo/routes/customizer.py index fcc0319..fedbd75 100644 --- a/demo/routes/customizer.py +++ b/demo/routes/customizer.py @@ -1,134 +1 @@ -from flask import Blueprint, render_template, request, jsonify, session, current_app -import sqlite3 -import json -import time -import os -from config import MAIN_DB, THEMES_DIR -from utils.theme_engine import scan_available_themes, get_theme_config, render_gkachele_template -from utils.auth_decorators import login_required - -customizer_bp = Blueprint('customizer', __name__) - -@customizer_bp.route('/api/themes') -def list_themes(): - """Listar todos los templates disponibles filtrados por plan""" - from utils.theme_engine import get_themes_by_rubro - rubro = request.args.get('rubro', None) - user_id = session.get('user_id') - - user_plan = 'base' - if user_id: - conn = sqlite3.connect(MAIN_DB) - c = conn.cursor() - c.execute('SELECT plan FROM users WHERE id = ?', (user_id,)) - res = c.fetchone() - conn.close() - if res: user_plan = res[0] - - themes = get_themes_by_rubro(rubro, user_plan) if rubro else scan_available_themes() - return jsonify({'success': True, 'themes': themes, 'total': len(themes)}) - -@customizer_bp.route('/customizer/') -def customizer_view(site_id): - """Customizer: Sidebar + Preview""" - conn = sqlite3.connect(MAIN_DB) - c = conn.cursor() - c.execute('SELECT user_id, slug, theme, content_json FROM sites WHERE id = ?', (site_id,)) - site = c.fetchone() - conn.close() - - if not site: - return "Sitio no encontrado", 404 - - if 'user_id' in session and site[0] != session['user_id']: - return "No autorizado", 403 - - content = json.loads(site[3]) if site[3] else {} - theme = site[2] - - theme_template = None - theme_path = os.path.join(THEMES_DIR, theme, 'template.html') - if os.path.exists(theme_path): - with open(theme_path, 'r', encoding='utf-8') as f: - theme_template = f.read() - - theme_config = get_theme_config(theme) - - # Obtener plan del usuario para filtrar templates - c = conn.cursor() - c.execute('SELECT plan, rubro FROM users WHERE id = ?', (site[0],)) - user_data = c.fetchone() - user_plan = user_data[0] if user_data else 'base' - user_rubro = user_data[1] if user_data else 'restaurante' - - from utils.theme_engine import get_themes_by_rubro - available_themes = get_themes_by_rubro(user_rubro, user_plan) - - return render_template('customizer.html', - site_id=site_id, - slug=site[1], - theme=theme, - content=content, - theme_template=theme_template, - theme_config=theme_config, - available_themes=available_themes, - user_plan=user_plan) - -@customizer_bp.route('/api/customizer/save', methods=['POST']) -def save_customizer(): - data = request.get_json() - site_id = data.get('site_id') - content = data.get('content') - new_theme = data.get('theme') - - conn = sqlite3.connect(MAIN_DB) - c = conn.cursor() - if new_theme: - c.execute('UPDATE sites SET theme = ? WHERE id = ?', (new_theme, site_id)) - - c.execute('UPDATE sites SET content_json = ? WHERE id = ?', (json.dumps(content), site_id)) - conn.commit() - conn.close() - return jsonify({'success': True}) - -@customizer_bp.route('/api/customizer/add-block', methods=['POST']) -def add_block(): - data = request.get_json() - site_id = data.get('site_id') - block_type = data.get('block_type') - - conn = sqlite3.connect(MAIN_DB) - c = conn.cursor() - c.execute('SELECT content_json FROM sites WHERE id = ?', (site_id,)) - result = c.fetchone() - - content = json.loads(result[0]) if result[0] else {} - if 'blocks' not in content: content['blocks'] = [] - - new_block = { - 'id': f'block_{int(time.time() * 1000)}', - 'type': block_type, - 'content': data.get('content', {}), - 'order': len(content['blocks']) - } - content['blocks'].append(new_block) - - c.execute('UPDATE sites SET content_json = ? WHERE id = ?', (json.dumps(content), site_id)) - conn.commit() - conn.close() - return jsonify({'success': True, 'block': new_block}) - -@customizer_bp.route('/api/customizer/preview-frame/') -def preview_frame(site_id): - conn = sqlite3.connect(MAIN_DB) - c = conn.cursor() - c.execute('SELECT theme, content_json, user_id FROM sites WHERE id = ?', (site_id,)) - site = c.fetchone() - conn.close() - - if not site: return "Sitio no encontrado", 404 - - try: - return render_gkachele_template(site[0], json.loads(site[1]), site_id, site[2]) - except Exception as e: - return f"Error renderizando preview: {e}", 500 +from flask import Blueprint, render_template, request, jsonify, session, current_appimport sqlite3import jsonimport timeimport osfrom config import MAIN_DB, THEMES_DIRfrom utils.theme_engine import scan_available_themes, get_theme_config, render_gkachele_templatefrom utils.auth_decorators import login_requiredcustomizer_bp = Blueprint('customizer', __name__)@customizer_bp.route('/customizer', strict_slashes=False)def customizer_demo(): """Ruta de conveniencia: si se pasa ?site_id=ID delega a customizer_view, si no muestra demo""" sid = request.args.get('site_id') if sid: try: return customizer_view(int(sid)) except Exception: pass # Render demo template with empty content return render_template('customizer.html', site_id='demo', slug='demo', theme=None, content={}, theme_template=None, theme_config={}, available_themes={}, user_plan='base', rubro='demo', site_name='Demo Site', security_hash='96CA...B852')@customizer_bp.route('/api/themes')def list_themes(): """Listar todos los templates disponibles filtrados por plan""" from utils.theme_engine import get_themes_by_rubro rubro = request.args.get('rubro', None) user_id = session.get('user_id') user_plan = 'base' if user_id: conn = sqlite3.connect(MAIN_DB) c = conn.cursor() c.execute('SELECT plan FROM users WHERE id = ?', (user_id,)) res = c.fetchone() conn.close() if res: user_plan = res[0] themes = get_themes_by_rubro(rubro, user_plan) if rubro else scan_available_themes() return jsonify({'success': True, 'themes': themes, 'total': len(themes)})@customizer_bp.route('/customizer/', strict_slashes=False)def customizer_view(site_id): """Customizer: Sidebar + Preview""" conn = sqlite3.connect(MAIN_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 {} theme = site[2] # Obtener plan del usuario para filtrar templates y settings 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('customizer.html', site_id=site_id, slug=site[1], site=site, # Pass full site object if needed for ID site_name=content.get('site_name', 'Mi Sitio'), rubro=user_rubro, content=content, user_plan=user_plan, security_hash='96CA...B852')@customizer_bp.route('/api/customizer/save', methods=['POST'])def save_customizer(): data = request.get_json() site_id = data.get('site_id') # Datos parciales enviados por frontend new_blocks = data.get('blocks') new_bg = data.get('bg') conn = sqlite3.connect(MAIN_DB) c = conn.cursor() # 1. Obtener contenido actual para no perder otros datos (colores, site_name...) c.execute('SELECT content_json FROM sites WHERE id = ?', (site_id,)) row = c.fetchone() if not row: conn.close() return jsonify({'success': False, 'error': 'Site not found'}), 404 current_content = json.loads(row[0]) if row[0] else {} # 2. Merge changes if new_blocks is not None: current_content['blocks'] = new_blocks if new_bg is not None: current_content['bg'] = new_bg # 3. Save back c.execute('UPDATE sites SET content_json = ? WHERE id = ?', (json.dumps(current_content), site_id)) conn.commit() conn.close() return jsonify({'success': True})@customizer_bp.route('/api/customizer/get-blocks/', methods=['GET'])def get_blocks(site_id): """Retorna los bloques de un sitio""" conn = sqlite3.connect(MAIN_DB) c = conn.cursor() c.execute('SELECT content_json FROM sites WHERE id = ?', (site_id,)) result = c.fetchone() conn.close() if not result or not result[0]: return jsonify([]) try: content = json.loads(result[0]) return jsonify(content.get('blocks', [])) except: return jsonify([])@customizer_bp.route('/api/customizer/get-content/', methods=['GET'])def get_content(site_id): """Retorna el contenido completo (blocks + settings) de un sitio""" conn = sqlite3.connect(MAIN_DB) c = conn.cursor() c.execute('SELECT content_json FROM sites WHERE id = ?', (site_id,)) result = c.fetchone() conn.close() if not result or not result[0]: return jsonify({'success': True, 'content': {}}) try: content = json.loads(result[0]) return jsonify({'success': True, 'content': content}) except Exception: return jsonify({'success': True, 'content': {}})@customizer_bp.route('/api/customizer/add-block', methods=['POST'])def add_block(): data = request.get_json() site_id = data.get('site_id') block_type = data.get('block_type') conn = sqlite3.connect(MAIN_DB) c = conn.cursor() c.execute('SELECT content_json FROM sites WHERE id = ?', (site_id,)) result = c.fetchone() if not result: conn.close() return jsonify({'success': False, 'error': 'Sitio no encontrado'}), 404 content = json.loads(result[0]) if result[0] else {} if 'blocks' not in content: content['blocks'] = [] new_block = { 'id': f'block_{int(time.time() * 1000)}', 'type': block_type, 'content': data.get('content', {}), 'order': len(content['blocks']) } content['blocks'].append(new_block) c.execute('UPDATE sites SET content_json = ? WHERE id = ?', (json.dumps(content), site_id)) conn.commit() conn.close() return jsonify({'success': True, 'block': new_block})@customizer_bp.route('/api/customizer/preview-frame/')def preview_frame(site_id): conn = sqlite3.connect(MAIN_DB) c = conn.cursor() c.execute('SELECT theme, content_json, user_id FROM sites WHERE id = ?', (site_id,)) site = c.fetchone() conn.close() if not site: return "Sitio no encontrado", 404 try: return render_gkachele_template(site[0], json.loads(site[1]), site_id, site[2]) except Exception as e: return f"Error renderizando preview: {e}", 500 \ No newline at end of file diff --git a/demo/templates/customizer.html b/demo/templates/customizer.html index 5a5a7da..484f3ad 100644 --- a/demo/templates/customizer.html +++ b/demo/templates/customizer.html @@ -1,1663 +1,1089 @@ - + + - Personalizar - {{ site_name or 'Sitio' }} - - + PageBuilder - {{ site_name }} - GKACHELEâ„¢ + + -
- -
-
- -
{{ user_plan|upper }}
- -
- -
- - -
-
- - Plantilla & Plan -
-
-
- Template Actual - - Estás en el plan {{ user_plan|upper - }}. -
-
-
- - - -
-
- Identidad del - Sitio -
-
-
- Nombre del Sitio - -
-
- Título Hero - -
-
- Descripción Corta - -
-
- Logo URL - -
-
-
- - - -
-
- Colores -
-
-
- Color Primario -
- - -
-
-
- Color Secundario -
- - -
-
-
- Color Texto -
- - -
-
-
-
- - - -
-
- Contacto & - Ubicación -
-
-
- Dirección - -
-
- Teléfono - -
- -
- WhatsApp (Solo números) - - Añadirá un botón flotante si se rellena. -
-
- Email - -
-
-
- - -
-
- Redes - Sociales -
-
-
- Instagram URL - -
-
- Facebook URL - -
-
- TikTok URL - -
-
- LinkedIn URL - -
-
- YouTube URL - -
-
-
- - -
-
- Horarios de - Atención -
-
-
- Lunes - Viernes - -
-
- Sábados - -
-
- Domingos - -
-
-
- - -
-
- Especialidad - Culinaria -
-
-
- Título - -
-
- Descripción - -
-
- URL de Imagen - -
-
-
- - -
-
- Capacidad -
-
-
- Capacidad del Restaurante - - Número de personas que puede albergar el - restaurante. -
-
-
- - -
-
- Bloques de - Contenido -
-
-
- -
-
- -
- - - -
-
- - -
-
- Menú - Restaurante -
-
-
- Enlace PDF Menú - -
-
- -
-
-
- -
- - - -
- - -
-
- -
- - -
- - - -
-
- + +
+
+
Guardando...
- -
+ +
+ - -