4 Commits

877 changed files with 137732 additions and 137500 deletions

View File

@@ -0,0 +1,7 @@
---
description: Gestión de funcionalidades del Plan Base
---
1. Los temas permitidos son únicamente los marcados como `plan: base` en `config.json`.
2. El Customizer permite cambios básicos de colores y texto.
3. El rubro por defecto es `restaurante`.

View File

@@ -0,0 +1,7 @@
---
description: Gestión de funcionalidades del Plan Premium
---
1. Acceso total a todas las plantillas (`base`, `pro`, `premium`).
2. Funcionalidades exclusivas como Custom Domains y soporte prioritario.
3. Capacidad de usar el Customizer Premium completo sin restricciones.

View File

@@ -0,0 +1,7 @@
---
description: Gestión de funcionalidades del Plan Pro
---
1. Incluye acceso a plantillas marcadas como `plan: pro` y `plan: base`.
2. Habilita bloques avanzados de video e imágenes.
3. El soporte para personalización de tipografía está activado.

28
codex/VERSIONADO_IA.md Normal file
View File

@@ -0,0 +1,28 @@
# Versionado IA - UB24/Elementor
## Rama de trabajo
- `ai/ub24-builder-v1`
## Regla de trabajo
1. Cada cambio funcional se guarda en un commit separado.
2. Cada commit se registra con su hash.
3. Cada commit debe incluir comando de reversión rápida.
## Registro de hashes
### 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): ...`
- `fix(builder): ...`
- `refactor(builder): ...`
- `chore(versioning): ...`
## Flujo con Gitea
1. Trabajo local en `ai/ub24-builder-v1`.
2. Push continuo a `origin/ai/ub24-builder-v1`.
3. Merge cuando validemos en local y Raspberry.

21
demo/Dockerfile Normal file
View File

@@ -0,0 +1,21 @@
FROM python:3.11-slim
WORKDIR /app
# Instalar dependencias del sistema
RUN apt-get update && apt-get install -y \
git \
&& rm -rf /var/lib/apt/lists/*
# Copiar requirements e instalar
COPY demo/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copiar el resto del código
COPY . .
# Exponer el puerto
EXPOSE 5000
# Comando para arrancar
CMD ["python", "demo/app.py"]

View File

@@ -20,7 +20,7 @@ def init_db():
password TEXT NOT NULL, password TEXT NOT NULL,
role TEXT DEFAULT 'subscriber', role TEXT DEFAULT 'subscriber',
plan TEXT DEFAULT 'base', plan TEXT DEFAULT 'base',
rubro TEXT DEFAULT 'gimnasio', rubro TEXT DEFAULT 'restaurante',
status TEXT DEFAULT 'active', status TEXT DEFAULT 'active',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)''') )''')

View File

@@ -11,10 +11,21 @@ customizer_bp = Blueprint('customizer', __name__)
@customizer_bp.route('/api/themes') @customizer_bp.route('/api/themes')
def list_themes(): def list_themes():
"""Listar todos los templates disponibles""" """Listar todos los templates disponibles filtrados por plan"""
from utils.theme_engine import get_themes_by_rubro from utils.theme_engine import get_themes_by_rubro
rubro = request.args.get('rubro', None) rubro = request.args.get('rubro', None)
themes = get_themes_by_rubro(rubro) if rubro else scan_available_themes() 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)}) return jsonify({'success': True, 'themes': themes, 'total': len(themes)})
@customizer_bp.route('/customizer/<int:site_id>') @customizer_bp.route('/customizer/<int:site_id>')
@@ -42,7 +53,16 @@ def customizer_view(site_id):
theme_template = f.read() theme_template = f.read()
theme_config = get_theme_config(theme) theme_config = get_theme_config(theme)
available_themes = scan_available_themes()
# 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', return render_template('customizer.html',
site_id=site_id, site_id=site_id,
@@ -51,7 +71,8 @@ def customizer_view(site_id):
content=content, content=content,
theme_template=theme_template, theme_template=theme_template,
theme_config=theme_config, theme_config=theme_config,
available_themes=available_themes) available_themes=available_themes,
user_plan=user_plan)
@customizer_bp.route('/api/customizer/save', methods=['POST']) @customizer_bp.route('/api/customizer/save', methods=['POST'])
def save_customizer(): def save_customizer():

View File

@@ -1,4 +1,5 @@
from flask import Blueprint, render_template, send_from_directory, sqlite3 from flask import Blueprint, render_template, send_from_directory
import sqlite3
import json import json
import os import os
from config import MAIN_DB, UPLOADS_DIR from config import MAIN_DB, UPLOADS_DIR
@@ -14,7 +15,6 @@ def landing():
@public_bp.route('/site/<slug>') @public_bp.route('/site/<slug>')
def public_site(slug): def public_site(slug):
"""Sitio público del cliente""" """Sitio público del cliente"""
import sqlite3
conn = sqlite3.connect(MAIN_DB) conn = sqlite3.connect(MAIN_DB)
c = conn.cursor() c = conn.cursor()
c.execute('SELECT id, theme, content_json, status, user_id FROM sites WHERE slug = ?', (slug,)) c.execute('SELECT id, theme, content_json, status, user_id FROM sites WHERE slug = ?', (slug,))

View File

@@ -1,11 +1,15 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="es"> <html lang="es">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard - Administración del Sitio</title> <title>Dashboard - Administración del Sitio</title>
<style> <style>
* { box-sizing: border-box; } * {
box-sizing: border-box;
}
body { body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
margin: 0; margin: 0;
@@ -24,12 +28,14 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.sidebar-header { .sidebar-header {
padding: 10px 0 10px 20px; padding: 10px 0 10px 20px;
font-weight: 600; font-weight: 600;
font-size: 14px; font-size: 14px;
color: #fff; color: #fff;
} }
.menu-item { .menu-item {
display: block; display: block;
padding: 10px 20px; padding: 10px 20px;
@@ -39,12 +45,19 @@
border-left: 4px solid transparent; border-left: 4px solid transparent;
cursor: pointer; cursor: pointer;
} }
.menu-item:hover, .menu-item.active {
.menu-item:hover,
.menu-item.active {
background: #2c3338; background: #2c3338;
color: #72aee6; color: #72aee6;
border-left-color: #72aee6; border-left-color: #72aee6;
} }
.menu-item i { margin-right: 8px; width: 16px; text-align: center; }
.menu-item i {
margin-right: 8px;
width: 16px;
text-align: center;
}
/* Main Content */ /* Main Content */
.main-content { .main-content {
@@ -65,17 +78,30 @@
justify-content: space-between; justify-content: space-between;
font-size: 13px; font-size: 13px;
} }
.top-bar a { color: #fff; text-decoration: none; margin-left: 20px; }
.top-bar a:hover { color: #72aee6; } .top-bar a {
color: #fff;
text-decoration: none;
margin-left: 20px;
}
.top-bar a:hover {
color: #72aee6;
}
/* Content Area */ /* Content Area */
.wp-content { .gk-content {
padding: 20px; padding: 20px;
overflow-y: auto; overflow-y: auto;
flex-grow: 1; flex-grow: 1;
} }
h1 { font-size: 23px; font-weight: 400; margin: 0 0 20px 0; padding: 0; } h1 {
font-size: 23px;
font-weight: 400;
margin: 0 0 20px 0;
padding: 0;
}
.card { .card {
background: #fff; background: #fff;
@@ -83,7 +109,7 @@
padding: 20px; padding: 20px;
margin-bottom: 20px; margin-bottom: 20px;
max-width: 800px; max-width: 800px;
box-shadow: 0 1px 1px rgba(0,0,0,.04); box-shadow: 0 1px 1px rgba(0, 0, 0, .04);
} }
.welcome-panel { .welcome-panel {
@@ -107,11 +133,28 @@
cursor: pointer; cursor: pointer;
display: inline-block; display: inline-block;
} }
.btn-primary:hover { background: #135e96; }
table { width: 100%; border-collapse: collapse; margin-top: 10px; } .btn-primary:hover {
th, td { text-align: left; padding: 10px; border-bottom: 1px solid #f0f0f1; } background: #135e96;
th { font-weight: 600; } }
table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
}
th,
td {
text-align: left;
padding: 10px;
border-bottom: 1px solid #f0f0f1;
}
th {
font-weight: 600;
}
.status-badge { .status-badge {
background: #f0f0f1; background: #f0f0f1;
color: #646970; color: #646970;
@@ -120,18 +163,27 @@
font-size: 11px; font-size: 11px;
font-weight: 600; font-weight: 600;
} }
.status-badge.published { background: #edfaef; color: #008a20; }
.status-badge.pending { background: #fff8e5; color: #996800; } .status-badge.published {
background: #edfaef;
color: #008a20;
}
.status-badge.pending {
background: #fff8e5;
color: #996800;
}
</style> </style>
<!-- FontAwesome simplificado para iconos --> <!-- FontAwesome simplificado para iconos -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
</head> </head>
<body> <body>
<!-- Sidebar --> <!-- Sidebar -->
<div class="sidebar"> <div class="sidebar">
<div class="sidebar-header"> <div class="sidebar-header">
<i class="fab fa-wordpress-simple"></i> GKACHELE <i class="fas fa-cube"></i> GKACHELE
</div> </div>
<a href="#" class="menu-item active"><i class="fas fa-tachometer-alt"></i> Escritorio</a> <a href="#" class="menu-item active"><i class="fas fa-tachometer-alt"></i> Escritorio</a>
<a href="#" class="menu-item"><i class="fas fa-thumbtack"></i> Entradas</a> <a href="#" class="menu-item"><i class="fas fa-thumbtack"></i> Entradas</a>
@@ -156,45 +208,48 @@
</div> </div>
</div> </div>
<!-- WP Content --> <!-- GK Content -->
<div class="wp-content"> <div class="gk-content">
<h1>Escritorio</h1> <h1>Escritorio</h1>
<div class="welcome-panel"> <div class="welcome-panel">
<div> <div>
<h2 style="margin-top: 0;">¡Te damos la bienvenida a tu panel!</h2> <h2 style="margin-top: 0;">¡Te damos la bienvenida a tu panel!</h2>
<p style="color: #646970;">Aquí puedes gestionar todos tus sitios y contenidos de forma profesional.</p> <p style="color: #646970;">Aquí puedes gestionar todos tus sitios y contenidos de forma profesional.
</p>
</div> </div>
<button class="btn-primary" onclick="window.location.href='/dashboard/create'">+ Crear Nuevo Sitio</button> <button class="btn-primary" onclick="window.location.href='/dashboard/create'">+ Crear Nuevo
Sitio</button>
</div> </div>
<div class="card"> <div class="card">
<h3><i class="fas fa-globe"></i> Tus Sitios Web</h3> <h3><i class="fas fa-globe"></i> Tus Sitios Web</h3>
{% if not sites %} {% if not sites %}
<p>No tienes sitios creados. ¡Empieza ahora!</p> <p>No tienes sitios creados. ¡Empieza ahora!</p>
{% else %} {% else %}
<table> <table>
<thead> <thead>
<tr> <tr>
<th>Nombre (Slug)</th> <th>Nombre (Slug)</th>
<th>Tema</th> <th>Tema</th>
<th>Estado</th> <th>Estado</th>
<th>Acciones</th> <th>Acciones</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for site in sites %} {% for site in sites %}
<tr> <tr>
<td><strong>{{ site.slug }}</strong></td> <td><strong>{{ site.slug }}</strong></td>
<td>{{ site.theme }}</td> <td>{{ site.theme }}</td>
<td><span class="status-badge {{ site.status }}">{{ site.status|upper }}</span></td> <td><span class="status-badge {{ site.status }}">{{ site.status|upper }}</span></td>
<td> <td>
<a href="/customizer/{{ site.id }}" class="btn-primary" style="padding: 4px 8px; font-size: 11px;">Personalizar</a> <a href="/customizer/{{ site.id }}" class="btn-primary"
</td> style="padding: 4px 8px; font-size: 11px;">Personalizar</a>
</tr> </td>
{% endfor %} </tr>
</tbody> {% endfor %}
</table> </tbody>
</table>
{% endif %} {% endif %}
</div> </div>
@@ -216,4 +271,5 @@
</div> </div>
</body> </body>
</html> </html>

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
{ {
"name": "Restaurante Asiático", "name": "Restaurante Asiático",
"rubro": "restaurante", "rubro": "restaurante",
"plan": "premium",
"description": "Tema moderno y elegante para restaurantes asiáticos", "description": "Tema moderno y elegante para restaurantes asiáticos",
"sections": [ "sections": [
"hero", "hero",

View File

@@ -1,6 +1,7 @@
{ {
"name": "Restaurante Elegante", "name": "Restaurante Elegante",
"rubro": "restaurante", "rubro": "restaurante",
"plan": "pro",
"description": "Tema sofisticado y elegante para restaurantes de alta cocina", "description": "Tema sofisticado y elegante para restaurantes de alta cocina",
"sections": [ "sections": [
"hero", "hero",

View File

@@ -1,6 +1,7 @@
{ {
"name": "Restaurante Moderno", "name": "Restaurante Moderno",
"rubro": "restaurante", "rubro": "restaurante",
"plan": "base",
"description": "Tema elegante para restaurantes", "description": "Tema elegante para restaurantes",
"sections": [ "sections": [
"hero", "hero",

View File

@@ -30,6 +30,7 @@ def scan_available_themes():
'name': config.get('name', theme_dir), 'name': config.get('name', theme_dir),
'description': config.get('description', ''), 'description': config.get('description', ''),
'rubro': config.get('rubro', 'general'), 'rubro': config.get('rubro', 'general'),
'plan': config.get('plan', 'base'),
'sections': config.get('sections', []), 'sections': config.get('sections', []),
'colors': config.get('colors', {}), 'colors': config.get('colors', {}),
'typography': config.get('typography', {}), 'typography': config.get('typography', {}),
@@ -55,10 +56,19 @@ def get_theme_config(theme_id):
print(f"⚠️ Error cargando config de {theme_id}: {e}") print(f"⚠️ Error cargando config de {theme_id}: {e}")
return None return None
def get_themes_by_rubro(rubro): def get_themes_by_rubro(rubro, user_plan='base'):
"""Obtener templates filtrados por rubro""" """Obtener templates filtrados por rubro y plan del usuario"""
all_themes = scan_available_themes() all_themes = scan_available_themes()
return {k: v for k, v in all_themes.items() if v.get('rubro') == rubro or v.get('rubro') == 'general'} plan_priority = {'base': 1, 'pro': 2, 'premium': 3}
user_level = plan_priority.get(user_plan, 1)
filtered_themes = {}
for k, v in all_themes.items():
theme_level = plan_priority.get(v.get('plan', 'base'), 1)
# Solo mostrar temas del rubro (o general) que estén dentro del plan del usuario
if (v.get('rubro') == rubro or v.get('rubro') == 'general') and theme_level <= user_level:
filtered_themes[k] = v
return filtered_themes
def get_site_menus(site_id, user_id): def get_site_menus(site_id, user_id):
"""Obtener menús del sitio organizados por ubicación""" """Obtener menús del sitio organizados por ubicación"""
@@ -175,5 +185,20 @@ def render_gkachele_template(theme, content, site_id=None, user_id=None):
return template.render(**template_data) return template.render(**template_data)
full_template = header + theme_template + sidebar + footer full_template = header + theme_template + sidebar + footer
template = Template(full_template)
# Inject WhatsApp Button if configured
whatsapp = content.get('redes_sociales', {}).get('whatsapp')
if whatsapp:
wa_html = f'''
<a href="https://wa.me/{whatsapp}" class="whatsapp-float" target="_blank" style="position:fixed; width:60px; height:60px; bottom:40px; right:40px; background-color:#25d366; color:#FFF; border-radius:50px; text-align:center; font-size:30px; box-shadow: 2px 2px 3px #999; z-index:10000; display:flex; align-items:center; justify-content:center; text-decoration:none;">
<i class="fab fa-whatsapp"></i>
</a>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
'''
if is_full_page:
theme_template = theme_template.replace('</body>', wa_html + '</body>')
else:
footer = footer.replace('</body>', wa_html + '</body>')
template = Template(theme_template if is_full_page else header + theme_template + sidebar + footer)
return template.render(**template_data) return template.render(**template_data)

35
deploy_modular.sh Normal file
View File

@@ -0,0 +1,35 @@
#!/bin/bash
# GKACHELE™ SaaS - Modular Deployment Script
# Despliega la nueva estructura (routes, utils, config) a la Raspberry Pi
# Configuración
RASPBERRY_USER="pi"
RASPBERRY_HOST="192.168.1.134"
RASPBERRY_PORT="2222"
RASPBERRY_PASS="Gdk1983gdk45@"
RASPBERRY_PATH="/home/pi/gkachele-saas"
LOCAL_PATH="/mnt/c/word/demo"
echo "🚀 Iniciando despliegue modular de GKACHELE™..."
# 1. Asegurar directorios en la Raspberry
sshpass -p "$RASPBERRY_PASS" ssh -p $RASPBERRY_PORT -o StrictHostKeyChecking=no $RASPBERRY_USER@$RASPBERRY_HOST "mkdir -p $RASPBERRY_PATH/routes $RASPBERRY_PATH/utils $RASPBERRY_PATH/templates $RASPBERRY_PATH/themes"
# 2. Copiar archivos core
echo "📦 Copiando archivos base..."
sshpass -p "$RASPBERRY_PASS" scp -P $RASPBERRY_PORT -o StrictHostKeyChecking=no $LOCAL_PATH/app.py $LOCAL_PATH/config.py $LOCAL_PATH/database.py $RASPBERRY_USER@$RASPBERRY_HOST:$RASPBERRY_PATH/
# 3. Copiar rutas y utilidades (la nueva modularización)
echo "📦 Copiando módulos (routes & utils)..."
sshpass -p "$RASPBERRY_PASS" scp -P $RASPBERRY_PORT -o StrictHostKeyChecking=no -r $LOCAL_PATH/routes/* $RASPBERRY_USER@$RASPBERRY_HOST:$RASPBERRY_PATH/routes/
sshpass -p "$RASPBERRY_PASS" scp -P $RASPBERRY_PORT -o StrictHostKeyChecking=no -r $LOCAL_PATH/utils/* $RASPBERRY_USER@$RASPBERRY_HOST:$RASPBERRY_PATH/utils/
# 4. Copiar templates (por si hubo cambios)
echo "📦 Copiando templates..."
sshpass -p "$RASPBERRY_PASS" scp -P $RASPBERRY_PORT -o StrictHostKeyChecking=no -r $LOCAL_PATH/templates/* $RASPBERRY_USER@$RASPBERRY_HOST:$RASPBERRY_PATH/templates/
# 5. Reiniciar el servicio
echo "🔄 Reiniciando servicio GKACHELE™..."
sshpass -p "$RASPBERRY_PASS" ssh -p $RASPBERRY_PORT -o StrictHostKeyChecking=no $RASPBERRY_USER@$RASPBERRY_HOST "sudo systemctl restart gkachele-saas || (sudo pkill -f 'python3 app.py' && cd $RASPBERRY_PATH && nohup python3 app.py > /tmp/app_modular.log 2>&1 &)"
echo "✅ Despliegue completado con éxito."

View File

@@ -1,66 +1,22 @@
services: services:
# Base de datos MySQL para WordPress # GKACHELE™ SaaS Modular - Backend Flask
db: app:
image: mysql:8.0 build:
container_name: wordpress_db context: .
restart: unless-stopped dockerfile: ./demo/Dockerfile
environment: container_name: gkachele_app
MYSQL_DATABASE: wordpress
MYSQL_USER: wordpress
MYSQL_PASSWORD: wordpress_password
MYSQL_ROOT_PASSWORD: root_password
volumes:
- db_data:/var/lib/mysql
networks:
- wordpress_network
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
timeout: 20s
retries: 10
# WordPress
wordpress:
image: wordpress:latest
container_name: wordpress_app
restart: unless-stopped restart: unless-stopped
ports: ports:
- "8080:80" - "5000:5000"
environment: environment:
WORDPRESS_DB_HOST: db:3306 - FLASK_ENV=production
WORDPRESS_DB_USER: wordpress - SECRET_KEY=demo-secret-key-2025
WORDPRESS_DB_PASSWORD: wordpress_password
WORDPRESS_DB_NAME: wordpress
volumes: volumes:
- wordpress_data:/var/www/html - .:/app
- ./wp-content:/var/www/html/wp-content # Para temas y plugins personalizados - ./demo/database:/app/demo/database # Persistencia de la DB SQLite
depends_on:
db:
condition: service_healthy
networks: networks:
- wordpress_network - gkachele_network
# phpMyAdmin (opcional, para administrar la base de datos)
phpmyadmin:
image: phpmyadmin:latest
container_name: wordpress_phpmyadmin
restart: unless-stopped
ports:
- "8081:80"
environment:
PMA_HOST: db
PMA_USER: root
PMA_PASSWORD: root_password
depends_on:
- db
networks:
- wordpress_network
volumes:
db_data:
driver: local
wordpress_data:
driver: local
networks: networks:
wordpress_network: gkachele_network:
driver: bridge driver: bridge

Some files were not shown because too many files have changed in this diff Show More