rogarces85 commited on
Commit
8e6725a
·
verified ·
1 Parent(s): 4d5cf1a

Upload 5 files

Browse files
Files changed (5) hide show
  1. .gitignore +11 -0
  2. README.md +197 -11
  3. app.py +231 -0
  4. index.html +190 -0
  5. requirements.txt +1 -0
.gitignore ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ *.so
5
+ .Python
6
+ *.db-journal
7
+ .env
8
+ .venv
9
+ env/
10
+ venv/
11
+ .DS_Store
README.md CHANGED
@@ -1,13 +1,199 @@
1
- ---
2
- title: OR Training
3
- emoji: 🐠
4
- colorFrom: green
5
- colorTo: indigo
6
- sdk: gradio
7
- sdk_version: 5.49.1
8
- app_file: app.py
9
- pinned: false
10
- short_description: aplicacion para entrenamientos
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
+ # 🏃‍♂️ Osorno Runners - Sistema de Planes de Entrenamiento
2
+
3
+ Sistema profesional para crear y gestionar planes de entrenamiento personalizados para corredores.
4
+
5
+ ## 🚀 Características
6
+
7
+ - ✅ Generación automática de planes de entrenamiento
8
+ - ✅ Base de datos SQLite persistente
9
+ - ✅ Sistema de autenticación (Usuario/Admin)
10
+ - Exportación a PDF
11
+ - ✅ Cálculo de zonas de frecuencia cardíaca
12
+ - ✅ IMC y datos antropométricos
13
+ - ✅ Cross training integrado
14
+ - ✅ Carreras preparatorias
15
+ - ✅ Mensajes motivacionales personalizados
16
+
17
+ ## 📦 Tecnologías
18
+
19
+ - **Frontend**: HTML, CSS, JavaScript
20
+ - **Backend**: Python + Gradio
21
+ - **Base de Datos**: SQLite
22
+ - **Hosting**: HuggingFace Spaces
23
+
24
+ ## 🔐 Credenciales por Defecto
25
+
26
+ ### Usuario Regular
27
+ - **Usuario**: USER
28
+ - **Contraseña**: 123
29
+ - **Permisos**: Ver planes propios
30
+
31
+ ### Administrador
32
+ - **Usuario**: ADMIN
33
+ - **Contraseña**: 123
34
+ - **Permisos**: Crear, ver y eliminar todos los planes
35
+
36
+ ## 🏗️ Estructura del Proyecto
37
+
38
+ ```
39
+ osorno-runners-hf/
40
+ ├── app.py # Aplicación principal Gradio
41
+ ├── index.html # Frontend
42
+ ├── requirements.txt # Dependencias Python
43
+ ├── README.md # Este archivo
44
+ └── osorno_runners.db # Base de datos SQLite (se crea automáticamente)
45
+ ```
46
+
47
+ ## 🚀 Despliegue en HuggingFace Spaces
48
+
49
+ ### Paso 1: Crear Space
50
+
51
+ 1. Ve a [HuggingFace Spaces](https://huggingface.co/spaces)
52
+ 2. Click en "Create new Space"
53
+ 3. Nombre: `osorno-runners` (o el que prefieras)
54
+ 4. SDK: **Gradio**
55
+ 5. Visibility: Public o Private
56
+ 6. Click "Create Space"
57
+
58
+ ### Paso 2: Subir Archivos
59
+
60
+ **Opción A - Mediante la interfaz web:**
61
+ 1. Click en "Files" en tu Space
62
+ 2. Click "Add file" → "Upload files"
63
+ 3. Arrastra todos los archivos del proyecto
64
+
65
+ **Opción B - Mediante Git:**
66
+ ```bash
67
+ git clone https://huggingface.co/spaces/TU_USUARIO/osorno-runners
68
+ cd osorno-runners
69
+ # Copia los archivos del proyecto aquí
70
+ git add .
71
+ git commit -m "Initial commit"
72
+ git push
73
+ ```
74
+
75
+ ### Paso 3: Configuración
76
+
77
+ El Space se desplegará automáticamente. La base de datos SQLite se creará en el primer inicio.
78
+
79
+ ## 💻 Desarrollo Local
80
+
81
+ ```bash
82
+ # Instalar dependencias
83
+ pip install -r requirements.txt
84
+
85
+ # Ejecutar aplicación
86
+ python app.py
87
+
88
+ # Abrir navegador en http://localhost:7860
89
+ ```
90
+
91
+ ## 📊 Base de Datos
92
+
93
+ ### Tabla: planes
94
+ ```sql
95
+ CREATE TABLE planes (
96
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
97
+ plan_data TEXT NOT NULL,
98
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
99
+ created_by TEXT NOT NULL,
100
+ athlete_name TEXT NOT NULL,
101
+ distance TEXT NOT NULL,
102
+ race_date TEXT NOT NULL
103
+ )
104
+ ```
105
+
106
+ ### Tabla: users
107
+ ```sql
108
+ CREATE TABLE users (
109
+ username TEXT PRIMARY KEY,
110
+ password TEXT NOT NULL,
111
+ role TEXT NOT NULL,
112
+ name TEXT NOT NULL
113
+ )
114
+ ```
115
+
116
+ ## 🎯 Funcionalidades Principales
117
+
118
+ ### Generación de Planes
119
+ - Cálculo automático de duración según distancia y experiencia
120
+ - Periodización (Base, Específico, Competencia, Transición)
121
+ - Distribución inteligente de volumen
122
+ - Zonas de FC personalizadas (Karvonen)
123
+
124
+ ### Tipos de Sesiones
125
+ - Trote continuo base
126
+ - Intervalos de velocidad
127
+ - Tempo run
128
+ - Long run (carrera larga)
129
+ - Recuperación activa
130
+ - Cross training
131
+
132
+ ### Exportación PDF
133
+ - Diseño profesional
134
+ - Toda la información del atleta
135
+ - Desglose semanal completo
136
+ - Zonas de frecuencia cardíaca
137
+
138
+ ## 🔧 Personalización
139
+
140
+ ### Agregar Nuevos Usuarios
141
+ Edita `app.py` en la función `init_db()`:
142
+
143
+ ```python
144
+ cursor.execute("INSERT INTO users VALUES ('NUEVO_USER', 'pass123', 'user', 'Nombre')")
145
+ ```
146
+
147
+ ### Modificar Algoritmos
148
+ Los algoritmos de generación están en `index.html` en las funciones JavaScript:
149
+ - `calculateWeeks()`: Duración del plan
150
+ - `getBaseKm()`: Kilometraje base
151
+ - `generateWeekSessions()`: Sesiones semanales
152
+
153
+ ## 📈 Mejoras Futuras
154
+
155
+ - [ ] Integración con IA (HuggingFace Inference API)
156
+ - [ ] Análisis de progreso
157
+ - [ ] Gráficos de volumen
158
+ - [ ] Exportar a Google Calendar
159
+ - [ ] Notificaciones por email
160
+ - [ ] Versión móvil nativa
161
+
162
+ ## 🤝 Contribuciones
163
+
164
+ Las contribuciones son bienvenidas. Por favor:
165
+ 1. Fork el repositorio
166
+ 2. Crea una rama para tu feature
167
+ 3. Commit tus cambios
168
+ 4. Push a la rama
169
+ 5. Abre un Pull Request
170
+
171
+ ## 📄 Licencia
172
+
173
+ MIT License
174
+
175
+ ## 👨‍💻 Autor
176
+
177
+ Desarrollado para Osorno Runners
178
+
179
+ ## 🐛 Reporte de Bugs
180
+
181
+ Si encuentras algún bug, por favor:
182
+ 1. Ve a la pestaña "Community" del Space
183
+ 2. Abre una nueva discusión
184
+ 3. Describe el problema detalladamente
185
+
186
+ ## ⚡ Notas Importantes
187
+
188
+ - La base de datos SQLite es persistente en HuggingFace Spaces
189
+ - Los datos se mantienen entre reinicios del Space
190
+ - Para backup, descarga `osorno_runners.db` desde Files
191
+ - Máximo 50MB de almacenamiento en plan gratuito
192
+
193
+ ## 🌟 Showcase
194
+
195
+ Ejemplo de uso: [Demo en vivo](https://huggingface.co/spaces/TU_USUARIO/osorno-runners)
196
+
197
  ---
198
 
199
+ **¡Dale una si te gusta el proyecto!**
app.py ADDED
@@ -0,0 +1,231 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import json
3
+ import sqlite3
4
+ from datetime import datetime
5
+ from pathlib import Path
6
+ import os
7
+
8
+ # Configuración de la base de datos
9
+ DB_FILE = 'osorno_runners.db'
10
+
11
+ def init_db():
12
+ """Inicializar base de datos SQLite"""
13
+ conn = sqlite3.connect(DB_FILE)
14
+ cursor = conn.cursor()
15
+
16
+ # Tabla de planes
17
+ cursor.execute('''
18
+ CREATE TABLE IF NOT EXISTS planes (
19
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
20
+ plan_data TEXT NOT NULL,
21
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
22
+ created_by TEXT NOT NULL,
23
+ athlete_name TEXT NOT NULL,
24
+ distance TEXT NOT NULL,
25
+ race_date TEXT NOT NULL
26
+ )
27
+ ''')
28
+
29
+ # Tabla de usuarios
30
+ cursor.execute('''
31
+ CREATE TABLE IF NOT EXISTS users (
32
+ username TEXT PRIMARY KEY,
33
+ password TEXT NOT NULL,
34
+ role TEXT NOT NULL,
35
+ name TEXT NOT NULL
36
+ )
37
+ ''')
38
+
39
+ # Insertar usuarios por defecto si no existen
40
+ cursor.execute('SELECT COUNT(*) FROM users')
41
+ if cursor.fetchone()[0] == 0:
42
+ cursor.execute("INSERT INTO users VALUES ('USER', '123', 'user', 'Usuario')")
43
+ cursor.execute("INSERT INTO users VALUES ('ADMIN', '123', 'admin', 'Administrador')")
44
+ print("✅ Usuarios por defecto creados: USER/123 y ADMIN/123")
45
+
46
+ conn.commit()
47
+ conn.close()
48
+ print("✅ Base de datos inicializada correctamente")
49
+
50
+ def save_plan_to_db(plan_data, username):
51
+ """Guardar plan en la base de datos"""
52
+ try:
53
+ conn = sqlite3.connect(DB_FILE)
54
+ cursor = conn.cursor()
55
+
56
+ cursor.execute('''
57
+ INSERT INTO planes (plan_data, created_by, athlete_name, distance, race_date)
58
+ VALUES (?, ?, ?, ?, ?)
59
+ ''', (
60
+ json.dumps(plan_data),
61
+ username,
62
+ plan_data['userData']['name'],
63
+ plan_data['userData']['distance'],
64
+ plan_data['userData']['raceDate']
65
+ ))
66
+
67
+ conn.commit()
68
+ plan_id = cursor.lastrowid
69
+ conn.close()
70
+
71
+ print(f"✅ Plan guardado con ID: {plan_id}")
72
+ return plan_id
73
+ except Exception as e:
74
+ print(f"❌ Error al guardar plan: {e}")
75
+ return None
76
+
77
+ def get_all_plans(username=None, role='user'):
78
+ """Obtener todos los planes según permisos"""
79
+ try:
80
+ conn = sqlite3.connect(DB_FILE)
81
+ cursor = conn.cursor()
82
+
83
+ if role == 'admin':
84
+ cursor.execute('SELECT id, plan_data, created_at, created_by FROM planes ORDER BY created_at DESC')
85
+ else:
86
+ cursor.execute('SELECT id, plan_data, created_at, created_by FROM planes WHERE created_by = ? ORDER BY created_at DESC', (username,))
87
+
88
+ plans = []
89
+ for row in cursor.fetchall():
90
+ plan_data = json.loads(row[1])
91
+ plan_data['id'] = row[0]
92
+ plan_data['createdAt'] = row[2]
93
+ plan_data['createdBy'] = row[3]
94
+ plans.append(plan_data)
95
+
96
+ conn.close()
97
+ print(f"✅ Cargados {len(plans)} planes para {username} ({role})")
98
+ return plans
99
+ except Exception as e:
100
+ print(f"❌ Error al cargar planes: {e}")
101
+ return []
102
+
103
+ def delete_plan_from_db(plan_id):
104
+ """Eliminar plan de la base de datos"""
105
+ try:
106
+ conn = sqlite3.connect(DB_FILE)
107
+ cursor = conn.cursor()
108
+ cursor.execute('DELETE FROM planes WHERE id = ?', (plan_id,))
109
+ conn.commit()
110
+ conn.close()
111
+ print(f"✅ Plan {plan_id} eliminado")
112
+ return True
113
+ except Exception as e:
114
+ print(f"❌ Error al eliminar plan: {e}")
115
+ return False
116
+
117
+ def authenticate_user(username, password):
118
+ """Autenticar usuario"""
119
+ try:
120
+ conn = sqlite3.connect(DB_FILE)
121
+ cursor = conn.cursor()
122
+ cursor.execute('SELECT role, name FROM users WHERE username = ? AND password = ?',
123
+ (username.upper(), password))
124
+ result = cursor.fetchone()
125
+ conn.close()
126
+
127
+ if result:
128
+ user_data = {
129
+ 'username': username.upper(),
130
+ 'role': result[0],
131
+ 'name': result[1]
132
+ }
133
+ print(f"✅ Login exitoso: {username} ({result[0]})")
134
+ return user_data
135
+ else:
136
+ print(f"❌ Login fallido: {username}")
137
+ return None
138
+ except Exception as e:
139
+ print(f"❌ Error en autenticación: {e}")
140
+ return None
141
+
142
+ # APIs para el frontend
143
+ def api_login(request: gr.Request):
144
+ """API de login"""
145
+ try:
146
+ data = json.loads(request.query_params.get('data', '{}'))
147
+ username = data.get('username', '')
148
+ password = data.get('password', '')
149
+
150
+ user = authenticate_user(username, password)
151
+ if user:
152
+ return json.dumps({'success': True, 'user': user})
153
+ return json.dumps({'success': False, 'error': 'Credenciales inválidas'})
154
+ except Exception as e:
155
+ return json.dumps({'success': False, 'error': str(e)})
156
+
157
+ def api_save_plan(request: gr.Request):
158
+ """API para guardar plan"""
159
+ try:
160
+ data = json.loads(request.query_params.get('data', '{}'))
161
+ plan_json = data.get('plan', '{}')
162
+ username = data.get('username', '')
163
+
164
+ plan_data = json.loads(plan_json)
165
+ plan_id = save_plan_to_db(plan_data, username)
166
+
167
+ if plan_id:
168
+ return json.dumps({'success': True, 'id': plan_id})
169
+ return json.dumps({'success': False, 'error': 'Error al guardar'})
170
+ except Exception as e:
171
+ return json.dumps({'success': False, 'error': str(e)})
172
+
173
+ def api_get_plans(request: gr.Request):
174
+ """API para obtener planes"""
175
+ try:
176
+ data = json.loads(request.query_params.get('data', '{}'))
177
+ username = data.get('username', '')
178
+ role = data.get('role', 'user')
179
+
180
+ plans = get_all_plans(username, role)
181
+ return json.dumps({'success': True, 'plans': plans})
182
+ except Exception as e:
183
+ return json.dumps({'success': False, 'error': str(e)})
184
+
185
+ def api_delete_plan(request: gr.Request):
186
+ """API para eliminar plan"""
187
+ try:
188
+ data = json.loads(request.query_params.get('data', '{}'))
189
+ plan_id = data.get('plan_id', 0)
190
+
191
+ success = delete_plan_from_db(plan_id)
192
+ if success:
193
+ return json.dumps({'success': True})
194
+ return json.dumps({'success': False, 'error': 'Error al eliminar'})
195
+ except Exception as e:
196
+ return json.dumps({'success': False, 'error': str(e)})
197
+
198
+ # Inicializar BD al arrancar
199
+ print("🚀 Iniciando Osorno Runners - Sistema de Entrenamiento")
200
+ init_db()
201
+
202
+ # Leer HTML
203
+ html_path = Path("index.html")
204
+ if html_path.exists():
205
+ html_content = html_path.read_text(encoding='utf-8')
206
+ print("✅ HTML cargado correctamente")
207
+ else:
208
+ html_content = "<h1>Error: index.html no encontrado</h1>"
209
+ print("❌ Error: index.html no encontrado")
210
+
211
+ # Crear interfaz Gradio
212
+ with gr.Blocks(
213
+ title="Osorno Runners",
214
+ theme=gr.themes.Soft(),
215
+ css="""
216
+ .gradio-container {padding: 0 !important;}
217
+ .contain {max-width: 100% !important;}
218
+ """
219
+ ) as app:
220
+ gr.HTML(html_content)
221
+
222
+ # Configuración del servidor
223
+ if __name__ == "__main__":
224
+ print("🌐 Iniciando servidor en puerto 7860")
225
+ app.launch(
226
+ server_name="0.0.0.0",
227
+ server_port=7860,
228
+ share=False,
229
+ show_error=True
230
+ )
231
+
index.html ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="es">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Osorno Runners</title>
7
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
8
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
9
+ <style>
10
+ *{margin:0;padding:0;box-sizing:border-box}:root{--primary:#dc143c;--primary-dark:#8b0000;--secondary:#00a65a;--danger:#dd4b39;--warning:#f39c12;--info:#00c0ef;--dark:#222d32;--sidebar:#222d32;--content:#ecf0f5;--white:#fff;--text-dark:#333;--text-light:#666;--border:#ddd}body{font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif;background:var(--content);color:var(--text-dark)}.login-container{display:flex;justify-content:center;align-items:center;min-height:100vh;background:linear-gradient(135deg,var(--primary),var(--primary-dark))}.login-box{background:var(--white);padding:40px;border-radius:10px;box-shadow:0 10px 40px rgba(0,0,0,.3);max-width:400px;width:100%}.login-header{text-align:center;margin-bottom:30px}.login-header i{font-size:60px;color:var(--primary);margin-bottom:15px}.header{background:var(--primary);color:var(--white);padding:15px 20px;display:flex;justify-content:space-between;align-items:center;position:fixed;top:0;left:0;right:0;z-index:1000}.menu-toggle{background:0 0;border:none;color:var(--white);font-size:24px;cursor:pointer}.logo{font-size:24px;font-weight:700;display:flex;align-items:center;gap:10px}.sidebar{position:fixed;top:60px;left:0;width:250px;height:calc(100vh - 60px);background:var(--sidebar);color:var(--white);overflow-y:auto;transition:transform .3s;z-index:999}.sidebar.hidden{transform:translateX(-100%)}.sidebar-menu{list-style:none;padding:20px 0}.sidebar-menu a{display:flex;align-items:center;gap:15px;padding:12px 20px;color:var(--white);text-decoration:none;cursor:pointer}.sidebar-menu a:hover,.sidebar-menu a.active{background:rgba(255,255,255,.1);border-left:3px solid var(--primary)}.main-content{margin-left:250px;margin-top:60px;padding:30px;transition:margin-left .3s}.main-content.expanded{margin-left:0}.stats-container{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:20px;margin-bottom:30px}.stat-card{background:var(--white);border-radius:8px;padding:20px;box-shadow:0 2px 10px rgba(0,0,0,.1);display:flex;justify-content:space-between;align-items:center}.stat-icon{width:70px;height:70px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:30px;color:var(--white)}.stat-card.primary .stat-icon{background:var(--primary)}.stat-card.success .stat-icon{background:var(--secondary)}.stat-card.warning .stat-icon{background:var(--warning)}.stat-card.info .stat-icon{background:var(--info)}.box{background:var(--white);border-radius:8px;box-shadow:0 2px 10px rgba(0,0,0,.1);margin-bottom:30px}.box-header{background:#f7f7f7;padding:15px 20px;border-bottom:1px solid var(--border)}.box-body{padding:20px}.form-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:20px;margin-bottom:20px}.form-group{display:flex;flex-direction:column}.form-group label{font-weight:600;margin-bottom:8px;font-size:14px}.form-group input,.form-group select,.form-group textarea{padding:10px 15px;border:1px solid var(--border);border-radius:5px;font-size:14px}.form-group input:focus,.form-group select:focus{outline:0;border-color:var(--primary)}.checkbox-group{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:10px}.checkbox-item{display:flex;align-items:center;padding:8px;background:#f8f9fa;border-radius:5px}.section-divider{margin:30px 0 20px;padding-bottom:12px;border-bottom:3px solid var(--primary);color:var(--primary);font-size:20px;font-weight:700}.btn{padding:12px 30px;border:none;border-radius:5px;font-size:16px;font-weight:600;cursor:pointer;display:inline-flex;align-items:center;gap:10px}.btn-primary{background:var(--primary);color:var(--white)}.btn-success{background:var(--secondary);color:var(--white)}.btn-danger{background:var(--danger);color:var(--white)}.btn-sm{padding:6px 15px;font-size:14px}.btn-secondary{background:#6c757d;color:var(--white)}.plan-list{display:grid;gap:20px}.plan-card{background:var(--white);border-radius:8px;box-shadow:0 2px 10px rgba(0,0,0,.1);padding:20px}.alert{padding:15px 20px;border-radius:5px;margin-bottom:20px;display:flex;align-items:center;gap:10px}.alert-info{background:#d1ecf1;color:#0c5460;border-left:4px solid var(--info)}.hidden{display:none!important}.prep-race-item{background:#f8f9fa;padding:15px;border-radius:8px;margin-bottom:15px;border-left:4px solid var(--primary)}@media(max-width:768px){.sidebar{transform:translateX(-100%)}.main-content{margin-left:0}}
11
+ </style>
12
+ </head>
13
+ <body>
14
+ <div id="loginScreen" class="login-container">
15
+ <div class="login-box">
16
+ <div class="login-header">
17
+ <i class="fas fa-running"></i>
18
+ <h2>Osorno Runners</h2>
19
+ <p>Sistema de Planes de Entrenamiento</p>
20
+ </div>
21
+ <form id="loginForm">
22
+ <div class="form-group">
23
+ <label><i class="fas fa-user"></i> Usuario</label>
24
+ <input type="text" id="loginUsername" required placeholder="USER o ADMIN">
25
+ </div>
26
+ <div class="form-group" style="margin-top:15px">
27
+ <label><i class="fas fa-lock"></i> Contraseña</label>
28
+ <input type="password" id="loginPassword" required>
29
+ </div>
30
+ <button type="submit" class="btn btn-primary" style="width:100%;margin-top:20px;justify-content:center">
31
+ <i class="fas fa-sign-in-alt"></i> Iniciar Sesión
32
+ </button>
33
+ </form>
34
+ <div style="margin-top:20px;text-align:center;color:#666;font-size:13px">
35
+ <p><strong>Usuario:</strong> USER / <strong>Pass:</strong> 123</p>
36
+ <p><strong>Admin:</strong> ADMIN / <strong>Pass:</strong> 123</p>
37
+ </div>
38
+ </div>
39
+ </div>
40
+
41
+ <div id="appScreen" class="hidden">
42
+ <header class="header">
43
+ <div style="display:flex;align-items:center;gap:15px">
44
+ <button class="menu-toggle" onclick="toggleSidebar()"><i class="fas fa-bars"></i></button>
45
+ <div class="logo"><i class="fas fa-running"></i><span>Osorno Runners</span></div>
46
+ </div>
47
+ <div style="display:flex;align-items:center;gap:20px">
48
+ <span id="currentUserName">Usuario</span>
49
+ <button class="btn-danger" style="padding:8px 15px;border:none;border-radius:5px;cursor:pointer" onclick="logout()">
50
+ <i class="fas fa-sign-out-alt"></i> Salir
51
+ </button>
52
+ </div>
53
+ </header>
54
+
55
+ <nav class="sidebar" id="sidebar">
56
+ <ul class="sidebar-menu">
57
+ <li><a href="#" class="active" onclick="showSection('dashboard')"><i class="fas fa-tachometer-alt"></i><span>Dashboard</span></a></li>
58
+ <li id="menu-new-plan"><a href="#" onclick="showSection('new-plan')"><i class="fas fa-plus-circle"></i><span>Nuevo Plan</span></a></li>
59
+ <li><a href="#" onclick="showSection('plan-list')"><i class="fas fa-list-ul"></i><span>Ver Planes</span></a></li>
60
+ </ul>
61
+ </nav>
62
+
63
+ <main class="main-content" id="mainContent">
64
+ <section id="dashboard-section">
65
+ <h1 style="margin-bottom:30px"><i class="fas fa-tachometer-alt"></i> Dashboard</h1>
66
+ <div class="stats-container">
67
+ <div class="stat-card primary">
68
+ <div><h3 style="font-size:14px;color:#666;margin-bottom:10px">PLANES CREADOS</h3><p id="stat-plans" style="font-size:28px;font-weight:700">0</p></div>
69
+ <div class="stat-icon"><i class="fas fa-list-alt"></i></div>
70
+ </div>
71
+ <div class="stat-card success">
72
+ <div><h3 style="font-size:14px;color:#666;margin-bottom:10px">TOTAL ATLETAS</h3><p id="stat-users" style="font-size:28px;font-weight:700">0</p></div>
73
+ <div class="stat-icon"><i class="fas fa-users"></i></div>
74
+ </div>
75
+ </div>
76
+ <div class="alert alert-info"><i class="fas fa-info-circle"></i><span>Sistema conectado a base de datos SQLite en HuggingFace Space</span></div>
77
+ </section>
78
+
79
+ <section id="new-plan-section" class="hidden">
80
+ <h1 style="margin-bottom:30px"><i class="fas fa-plus-circle"></i> Crear Nuevo Plan</h1>
81
+ <div class="box">
82
+ <div class="box-header"><h3 style="font-size:18px;font-weight:700"><i class="fas fa-user-circle"></i> Información del Atleta</h3></div>
83
+ <div class="box-body">
84
+ <form id="trainingForm">
85
+ <div class="form-grid">
86
+ <div class="form-group">
87
+ <label><i class="fas fa-user"></i> Nombre *</label>
88
+ <input type="text" id="athleteName" required>
89
+ </div>
90
+ <div class="form-group">
91
+ <label><i class="fas fa-birthday-cake"></i> Edad *</label>
92
+ <input type="number" id="athleteAge" required min="10" max="100">
93
+ </div>
94
+ <div class="form-group">
95
+ <label><i class="fas fa-layer-group"></i> Experiencia *</label>
96
+ <select id="experience" required>
97
+ <option value="">Selecciona...</option>
98
+ <option value="principiante">Principiante</option>
99
+ <option value="intermedio">Intermedio</option>
100
+ <option value="avanzado">Avanzado</option>
101
+ </select>
102
+ </div>
103
+ </div>
104
+ <div class="section-divider"><i class="fas fa-weight"></i> Antropometría</div>
105
+ <div class="form-grid">
106
+ <div class="form-group">
107
+ <label>Peso (kg) *</label>
108
+ <input type="number" id="weight" required min="30" max="200" step="0.1">
109
+ </div>
110
+ <div class="form-group">
111
+ <label>Talla (cm) *</label>
112
+ <input type="number" id="height" required min="100" max="250">
113
+ </div>
114
+ <div class="form-group">
115
+ <label>IMC</label>
116
+ <input type="text" id="imc" readonly style="background:#e9ecef">
117
+ </div>
118
+ </div>
119
+ <div class="section-divider"><i class="fas fa-heartbeat"></i> Datos Fisiológicos</div>
120
+ <div class="form-grid">
121
+ <div class="form-group">
122
+ <label>FC Máxima (bpm) *</label>
123
+ <input type="number" id="hrMax" required min="100" max="220">
124
+ </div>
125
+ <div class="form-group">
126
+ <label>FC Reposo (bpm) *</label>
127
+ <input type="number" id="hrRest" required min="30" max="100">
128
+ </div>
129
+ <div class="form-group">
130
+ <label>VO2Max (opcional)</label>
131
+ <input type="number" id="vo2max" min="20" max="90" step="0.1">
132
+ </div>
133
+ </div>
134
+ <div class="section-divider"><i class="fas fa-bullseye"></i> Objetivo</div>
135
+ <div class="form-grid">
136
+ <div class="form-group">
137
+ <label>Distancia *</label>
138
+ <select id="distance" required>
139
+ <option value="">Selecciona...</option>
140
+ <option value="5K">5K</option>
141
+ <option value="10K">10K</option>
142
+ <option value="21K">21K</option>
143
+ <option value="42K">42K</option>
144
+ </select>
145
+ </div>
146
+ <div class="form-group">
147
+ <label>Ritmo (min/km) *</label>
148
+ <input type="text" id="targetPace" required placeholder="4:30">
149
+ </div>
150
+ <div class="form-group">
151
+ <label>Fecha Carrera *</label>
152
+ <input type="date" id="raceDate" required>
153
+ </div>
154
+ </div>
155
+ <div class="section-divider"><i class="fas fa-calendar-day"></i> Disponibilidad</div>
156
+ <div class="form-group">
157
+ <label>Días de Entrenamiento *</label>
158
+ <div class="checkbox-group">
159
+ <label class="checkbox-item"><input type="checkbox" name="trainingDays" value="lunes">Lunes</label>
160
+ <label class="checkbox-item"><input type="checkbox" name="trainingDays" value="martes">Martes</label>
161
+ <label class="checkbox-item"><input type="checkbox" name="trainingDays" value="miércoles">Miércoles</label>
162
+ <label class="checkbox-item"><input type="checkbox" name="trainingDays" value="jueves">Jueves</label>
163
+ <label class="checkbox-item"><input type="checkbox" name="trainingDays" value="viernes">Viernes</label>
164
+ <label class="checkbox-item"><input type="checkbox" name="trainingDays" value="sábado">Sábado</label>
165
+ <label class="checkbox-item"><input type="checkbox" name="trainingDays" value="domingo">Domingo</label>
166
+ </div>
167
+ </div>
168
+ <div style="margin-top:30px">
169
+ <button type="submit" class="btn btn-success"><i class="fas fa-check"></i> Generar Plan</button>
170
+ </div>
171
+ </form>
172
+ </div>
173
+ </div>
174
+ </section>
175
+
176
+ <section id="plan-list-section" class="hidden">
177
+ <h1 style="margin-bottom:30px"><i class="fas fa-list-ul"></i> Planes de Entrenamiento</h1>
178
+ <div id="planListContent" class="plan-list"></div>
179
+ <div id="noPlanPlaceholder" class="hidden">
180
+ <div class="alert alert-info"><i class="fas fa-info-circle"></i><span>No hay planes disponibles</span></div>
181
+ </div>
182
+ </section>
183
+ </main>
184
+ </div>
185
+
186
+ <script>
187
+ let currentUser=null;const API_URL='/api';async function apiCall(endpoint,data){try{const response=await fetch(API_URL+endpoint,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)});return await response.json()}catch(e){console.error('API Error:',e);return{success:false,error:e.message}}}document.getElementById('loginForm').addEventListener('submit',async function(e){e.preventDefault();const username=document.getElementById('loginUsername').value.toUpperCase(),password=document.getElementById('loginPassword').value,result=await apiCall('/login',{username:username,password:password});if(result.success){currentUser=result.user;document.getElementById('loginScreen').classList.add('hidden');document.getElementById('appScreen').classList.remove('hidden');document.getElementById('currentUserName').textContent=currentUser.name;currentUser.role==='user'&&(document.getElementById('menu-new-plan').style.display='none');loadPlans()}else alert('Usuario o contraseña incorrectos')});function logout(){currentUser=null;document.getElementById('appScreen').classList.add('hidden');document.getElementById('loginScreen').classList.remove('hidden');document.getElementById('loginForm').reset()}function toggleSidebar(){document.getElementById('sidebar').classList.toggle('hidden');document.getElementById('mainContent').classList.toggle('expanded')}function showSection(section){document.querySelectorAll('main > section').forEach(s=>s.classList.add('hidden'));document.querySelectorAll('.sidebar-menu a').forEach(a=>a.classList.remove('active'));if('dashboard'===section)document.getElementById('dashboard-section').classList.remove('hidden');else if('new-plan'===section){if(currentUser&&'admin'===currentUser.role)document.getElementById('new-plan-section').classList.remove('hidden');else{alert('Solo administradores pueden crear planes');return}}else'plan-list'===section&&(document.getElementById('plan-list-section').classList.remove('hidden'),loadPlans());window.event&&window.event.target&&window.event.target.closest('a').classList.add('active')}async function loadPlans(){const result=await apiCall('/get_plans',{username:currentUser.username,role:currentUser.role});if(result.success){const plans=result.plans,listContent=document.getElementById('planListContent'),placeholder=document.getElementById('noPlanPlaceholder');if(0===plans.length){placeholder.classList.remove('hidden');listContent.innerHTML='';return}placeholder.classList.add('hidden');let html='';plans.forEach(plan=>{html+=`<div class="plan-card"><div><h3><i class="fas fa-user"></i> ${plan.userData.name}</h3><p><strong>Objetivo:</strong> ${plan.userData.distance} - ${plan.totalWeeks} sem</p></div><div style="display:flex;gap:10px"><button class="btn btn-success btn-sm" onclick="generatePDF(${plan.id})"><i class="fas fa-file-pdf"></i> PDF</button>${currentUser&&'admin'===currentUser.role?`<button class="btn btn-danger btn-sm" onclick="deletePlan(${plan.id})"><i class="fas fa-trash"></i></button>`:''}</div></div>`});listContent.innerHTML=html;document.getElementById('stat-plans').textContent=plans.length}}document.getElementById('trainingForm').addEventListener('submit',async function(e){e.preventDefault();const trainingDays=Array.from(document.querySelectorAll('input[name="trainingDays"]:checked')).map(cb=>cb.value);if(trainingDays.length<3)return void alert('Selecciona al menos 3 días');const plan={userData:{name:document.getElementById('athleteName').value,age:parseInt(document.getElementById('athleteAge').value),experience:document.getElementById('experience').value,weight:parseFloat(document.getElementById('weight').value),height:parseFloat(document.getElementById('height').value),imc:document.getElementById('imc').value,hrMax:parseInt(document.getElementById('hrMax').value),hrRest:parseInt(document.getElementById('hrRest').value),vo2max:document.getElementById('vo2max').value||null,distance:document.getElementById('distance').value,targetPace:document.getElementById('targetPace').value,raceDate:document.getElementById('raceDate').value,trainingDays:trainingDays},totalWeeks:12,weeklyPlans:[]};const result=await apiCall('/save_plan',{plan:JSON.stringify(plan),username:currentUser.username});result.success?(alert('Plan creado exitosamente'),document.getElementById('trainingForm').reset(),showSection('plan-list')):alert('Error al crear plan: '+result.error)});async function deletePlan(planId){if(confirm('¿Eliminar este plan?')){const result=await apiCall('/delete_plan',{plan_id:planId});result.success?(alert('Plan eliminado'),loadPlans()):alert('Error al eliminar')}}function generatePDF(planId){alert('Generación de PDF disponible en versión completa')}
188
+ </script>
189
+ </body>
190
+ </html>
requirements.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ gradio==4.44.0