Edoruin commited on
Commit
e1815c8
·
1 Parent(s): 69d0633

pc responsive issues

Browse files
Files changed (2) hide show
  1. app/main.py +80 -154
  2. app/requirements.txt +0 -1
app/main.py CHANGED
@@ -13,7 +13,6 @@ from flask import Flask, render_template, request, jsonify, redirect, url_for, f
13
  from flask_socketio import SocketIO, emit
14
  from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user
15
  from werkzeug.security import generate_password_hash, check_password_hash
16
- import mysql.connector
17
  import gitlab
18
  import telebot
19
  from telebot import types
@@ -41,61 +40,66 @@ class User(UserMixin):
41
  self.id = id
42
  self.username = username
43
 
44
- # --- BASE DE DATOS MYSQL ---
45
- def get_db_connection():
46
- """Establece y retorna una conexión a la base de datos MySQL."""
47
- try:
48
- conn = mysql.connector.connect(
49
- host=os.getenv("MYSQL_HOST", "localhost"),
50
- user=os.getenv("MYSQL_USER", "root"),
51
- password=os.getenv("MYSQL_PASSWORD", ""),
52
- database=os.getenv("MYSQL_DB", "makerpage")
53
- )
54
- return conn
55
- except mysql.connector.Error as err:
56
- print(f"ERROR CRÍTICO MYSQL: {err}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  return None
58
 
59
- def init_db():
60
- """Inicializa la base de datos y crea las tablas necesarias si no existen."""
61
- conn = get_db_connection()
62
- if conn:
63
- try:
64
- cursor = conn.cursor()
65
- # Crear tabla de usuarios con soporte para hashing de contraseñas
66
- cursor.execute("""
67
- CREATE TABLE IF NOT EXISTS users (
68
- id INT AUTO_INCREMENT PRIMARY KEY,
69
- username VARCHAR(50) UNIQUE NOT NULL,
70
- password VARCHAR(255) NOT NULL
71
- )
72
- """)
73
- conn.commit()
74
- print("INFO: Base de datos verificada/inicializada correctamente.")
75
- except mysql.connector.Error as err:
76
- print(f"ERROR al inicializar tablas: {err}")
77
- finally:
78
- conn.close()
79
- else:
80
- print("ADVERTENCIA: No se pudo conectar a MySQL para inicializar. Verifica las credenciales.")
81
-
82
- # Llamar a la inicialización al arrancar
83
- init_db()
84
 
85
  @login_manager.user_loader
86
  def load_user(user_id):
87
- """Recupera un usuario de la base de datos por su ID para Flask-Login."""
88
- conn = get_db_connection()
89
- if not conn: return None
90
- try:
91
- cursor = conn.cursor(dictionary=True)
92
- cursor.execute("SELECT id, username FROM users WHERE id = %s", (user_id,))
93
- user_data = cursor.fetchone()
94
- return User(str(user_data['id']), user_data['username']) if user_data else None
95
- except:
96
- return None
97
- finally:
98
- conn.close()
99
 
100
  # --- GESTOR DE PRÉSTAMOS (Persistencia JSON) ---
101
  class LoanManager:
@@ -147,11 +151,10 @@ try:
147
  except:
148
  pass
149
 
150
- # URL del proxy de Google Script para evadir restricciones de red si es necesario
151
  GOOGLE_PROXY_URL = os.getenv("GOOGLE_PROXY_URL") or "https://script.google.com/macros/s/AKfycbz7z1Jb0vsur42GmmqrL3PVXeRkN2WxSojFDIleEDoLOg6MnrmJjb_uuPcQ15CTwyzD/exec"
152
 
153
  if TG_TOKEN and GOOGLE_PROXY_URL:
154
- # Configurar telebot para usar el proxy
155
  base_url = GOOGLE_PROXY_URL.split('?')[0]
156
  telebot.apihelper.API_URL = base_url + "?path={1}&token={0}"
157
  telebot.apihelper.CONNECT_TIMEOUT = 60
@@ -160,17 +163,15 @@ if TG_TOKEN and GOOGLE_PROXY_URL:
160
  bot = telebot.TeleBot(TG_TOKEN) if TG_TOKEN else None
161
 
162
  def escape_md(text):
163
- """Escapa caracteres especiales para el modo Markdown de Telegram."""
164
  if not text: return ""
165
  for char in ['_', '*', '[', '`']:
166
  text = text.replace(char, f"\\{char}")
167
  return text
168
 
169
- # --- MANEJADORES DEL BOT DE TELEGRAM ---
170
  if bot:
171
  @bot.callback_query_handler(func=lambda call: True)
172
  def handle_query(call):
173
- """Maneja las pulsaciones de botones Inline (Aceptar/Declinar)."""
174
  try:
175
  if call.data.startswith("accept_"):
176
  loan_id = call.data.replace("accept_", "")
@@ -178,7 +179,6 @@ if bot:
178
  bot.answer_callback_query(call.id, "Préstamo Aceptado")
179
  nuevo_texto = f"✅ *ACEPTADO*\n{escape_md(call.message.text)}"
180
  bot.edit_message_text(nuevo_texto, call.message.chat.id, call.message.message_id, parse_mode="Markdown")
181
- # Emitir evento Socket.IO para actualizar la UI en tiempo real
182
  socketio.emit('notification', {"text": f"Préstamo {loan_id[:8]} ACEPTADO", "color": "green"})
183
  elif call.data.startswith("decline_"):
184
  loan_id = call.data.replace("decline_", "")
@@ -192,7 +192,6 @@ if bot:
192
 
193
  @bot.message_handler(commands=['aceptar', 'declinar', 'status'])
194
  def handle_text_commands(message):
195
- """Maneja comandos directos por texto."""
196
  try:
197
  text = message.text.split()
198
  if len(text) < 2:
@@ -204,47 +203,31 @@ if bot:
204
  emoji = "✅" if status == "ACCEPTED" else "❌"
205
  bot.reply_to(message, f"{emoji} Préstamo {loan_id} actualizado a {status}")
206
  socketio.emit('notification', {"text": f"Préstamo {loan_id[:8]} {status}", "color": "green" if status == "ACCEPTED" else "red"})
207
- else:
208
- bot.reply_to(message, "ID de préstamo no encontrado.")
209
  except Exception as e:
210
  print(f"Command Error: {e}")
211
 
212
- @bot.message_handler(commands=['getid'])
213
- def handle_getid(message):
214
- """Comando de ayuda para obtener el Chat ID actual."""
215
- try:
216
- bot.reply_to(message, f"Chat ID: `{message.chat.id}`", parse_mode="Markdown")
217
- except Exception as e:
218
- print(f"GetID Error: {e}")
219
-
220
  def start_bot_thread():
221
- """Hilo de ejecución para el polling infinito del bot de Telegram."""
222
  if bot:
223
- print("INFO: Esperando 5s para estabilizar conexión con Telegram...")
224
  time.sleep(5)
225
  try: bot.delete_webhook()
226
  except: pass
227
  while True:
228
  try:
229
- print("DEBUG: Iniciando polling de Telegram...")
230
  bot.infinity_polling(timeout=20, long_polling_timeout=10)
231
  except Exception as e:
232
- print(f"Error Polling: {e}. Reintento en 30s...")
233
  time.sleep(30)
234
 
235
  if bot:
236
  threading.Thread(target=start_bot_thread, daemon=True).start()
237
 
238
- # --- RUTAS WEB (Flask) ---
239
 
240
  @app.route('/')
241
  def index():
242
- """Página principal del panel."""
243
  return render_template('index.html', title="MAKER SPACE")
244
 
245
  @app.route('/login', methods=['GET', 'POST'])
246
  def login():
247
- """Ruta para el inicio de sesión de usuarios."""
248
  if current_user.is_authenticated:
249
  return redirect(url_for('repos'))
250
 
@@ -252,19 +235,10 @@ def login():
252
  username = request.form.get('username')
253
  password = request.form.get('password')
254
 
255
- conn = get_db_connection()
256
- if not conn:
257
- flash("Error de conexión a la base de datos", "red")
258
- return render_template('login.html')
259
-
260
- cursor = conn.cursor(dictionary=True)
261
- cursor.execute("SELECT id, username, password FROM users WHERE username = %s", (username,))
262
- user_data = cursor.fetchone()
263
- conn.close()
264
 
265
- # Verificación segura de la contraseña usando hashes
266
  if user_data and check_password_hash(user_data['password'], password):
267
- user = User(str(user_data['id']), user_data['username'])
268
  login_user(user)
269
  flash(f"¡Bienvenido de nuevo, {username}!", "green")
270
  return redirect(url_for('repos'))
@@ -275,7 +249,6 @@ def login():
275
 
276
  @app.route('/register', methods=['GET', 'POST'])
277
  def register():
278
- """Ruta para el registro de nuevos usuarios."""
279
  if current_user.is_authenticated:
280
  return redirect(url_for('repos'))
281
 
@@ -288,47 +261,28 @@ def register():
288
  flash("Las contraseñas no coinciden", "red")
289
  return render_template('register.html')
290
 
291
- # Generar hash seguro de la contraseña
292
- hashed_password = generate_password_hash(password)
293
-
294
- conn = get_db_connection()
295
- if not conn:
296
- flash("Error de conexión a la base de datos", "red")
297
- return render_template('register.html')
298
-
299
- try:
300
- cursor = conn.cursor()
301
- cursor.execute("INSERT INTO users (username, password) VALUES (%s, %s)", (username, hashed_password))
302
- conn.commit()
303
  flash("Cuenta creada exitosamente. Ahora puedes iniciar sesión.", "green")
304
  return redirect(url_for('login'))
305
- except mysql.connector.Error as err:
306
- if err.errno == 1062:
307
- flash("El nombre de usuario ya existe.", "red")
308
- else:
309
- flash("Error al crear la cuenta. Inténtalo de nuevo.", "red")
310
- finally:
311
- conn.close()
312
 
313
  return render_template('register.html', title="Registro")
314
 
315
  @app.route('/logout')
316
  @login_required
317
  def logout():
318
- """Cierra la sesión del usuario actual."""
319
  logout_user()
320
  flash("Has cerrado sesión", "blue")
321
  return redirect(url_for('index'))
322
 
323
  @app.route('/prestamos')
324
  def prestamos():
325
- """Vista del historial y formulario de préstamos."""
326
  loans = loan_mgr.get_all()
327
  return render_template('prestamos.html', title="Préstamos", loans=loans)
328
 
329
  @app.route('/api/prestamo', methods=['POST'])
330
  def api_prestamo():
331
- """API para registrar una nueva solicitud de préstamo."""
332
  data = request.json
333
  solicitante = data.get('solicitante')
334
  h_salida = data.get('hora_salida')
@@ -341,7 +295,7 @@ def api_prestamo():
341
  loan_id = str(uuid.uuid4())
342
  lista_items_str = "\n".join([f"• [{i['categoria']}] {i['descripcion']} (x{i['cantidad']})" for i in items])
343
 
344
- # Preparar mensaje para Telegram
345
  mensaje_tg = (
346
  "🛠 *NUEVA SOLICITUD DE PRÉSTAMO*\n"
347
  f"👤 *Solicitante:* {escape_md(solicitante)}\n"
@@ -352,7 +306,6 @@ def api_prestamo():
352
  res_tg = "OK"
353
  if bot and TG_CHAT_ID:
354
  try:
355
- # Crear botones de acción rápida en el mensaje de Telegram
356
  markup = types.InlineKeyboardMarkup()
357
  markup.add(
358
  types.InlineKeyboardButton("✅ Aceptar", callback_data=f"accept_{loan_id}"),
@@ -362,8 +315,8 @@ def api_prestamo():
362
  except Exception as e:
363
  res_tg = str(e)
364
 
365
- # Registrar en el historial JSON
366
- nuevo = {
367
  "id": loan_id,
368
  "Solicitante": solicitante,
369
  "item": lista_items_str,
@@ -372,34 +325,28 @@ def api_prestamo():
372
  "devolucion": h_retorno,
373
  "status_loan": "PENDING",
374
  "status_tg": res_tg
375
- }
376
 
377
- loan_mgr.add_loan(nuevo)
378
  return jsonify({"status": "success", "id": loan_id})
379
 
380
  @app.route('/api/entregar/<loan_id>', methods=['POST'])
381
  def api_entregar(loan_id):
382
- """API para notificar la entrega física de los materiales."""
383
  loan = None
384
  for l in loan_mgr.get_all():
385
  if l['id'] == loan_id:
386
  loan = l
387
  break
388
 
389
- if not loan:
390
- return jsonify({"status": "error", "message": "Préstamo no encontrado"}), 404
391
-
392
- if loan['status_loan'] != "ACCEPTED":
393
- return jsonify({"status": "error", "message": "Solo se pueden entregar préstamos aceptados"}), 400
394
 
395
- # Ajuste de zona horaria a República Dominicana (UTC-4)
396
  utc_now = datetime.datetime.now(datetime.timezone.utc)
397
  rd_now = utc_now - datetime.timedelta(hours=4)
398
  ahora = rd_now.strftime("%H:%M")
399
 
400
  loan_mgr.update_status(loan_id, "DELIVERED")
401
 
402
- # Notificar a Telegram sobre la entrega
403
  mensaje_tg = (
404
  "📦 *ENTREGA DE MATERIALES*\n"
405
  f"👤 *Solicitante:* {escape_md(loan['Solicitante'])}\n"
@@ -408,10 +355,8 @@ def api_entregar(loan_id):
408
  )
409
 
410
  if bot and TG_CHAT_ID:
411
- try:
412
- bot.send_message(TG_CHAT_ID, mensaje_tg, parse_mode="Markdown")
413
- except Exception as e:
414
- print(f"Error Telegram Entregar: {e}")
415
 
416
  socketio.emit('notification', {"text": f"Entrega confirmada para {loan['Solicitante']}", "color": "blue"})
417
  return jsonify({"status": "success"})
@@ -419,60 +364,41 @@ def api_entregar(loan_id):
419
  @app.route('/repos')
420
  @login_required
421
  def repos():
422
- """Lista de proyectos desde GitLab (requiere autenticación)."""
423
- GITLAB_URL = "https://gitlab.com"
424
  GIT_TOKEN = os.getenv("GITLAB_TOKEN")
425
  GIT_GROUP = os.getenv("GITLAB_GROUP_ID")
426
-
427
  projects = []
428
  if GIT_TOKEN and GIT_GROUP:
429
  try:
430
- gl = gitlab.Gitlab(GITLAB_URL, private_token=GIT_TOKEN)
431
  projects = gl.groups.get(GIT_GROUP).projects.list(all=True)
432
- except Exception as e:
433
- print(f"GitLab Error: {e}")
434
  return render_template('repos.html', title="Proyectos", projects=projects)
435
 
436
  @app.route('/ver/<pid>/<pname>')
437
  @login_required
438
  def ver_repo(pid, pname):
439
- """Vista detallada de un proyecto, muestra el README renderizado."""
440
  GIT_TOKEN = os.getenv("GITLAB_TOKEN")
441
- GITLAB_URL = "https://gitlab.com"
442
-
443
- readme_html = "<p>README.md no encontrado.</p>"
444
- web_url = "#"
445
- download_url = "#"
446
 
447
  if GIT_TOKEN:
448
  try:
449
- gl = gitlab.Gitlab(GITLAB_URL, private_token=GIT_TOKEN)
450
  project = gl.projects.get(pid)
451
  web_url = project.web_url
452
- download_url = f"{GITLAB_URL}/api/v4/projects/{pid}/repository/archive.zip?private_token={GIT_TOKEN}"
453
 
454
- # Buscar el archivo README.md en las ramas principales
455
- readme_text = ""
456
  for branch in ["main", "master"]:
457
  try:
458
  f = project.files.get(file_path='README.md', ref=branch)
459
  readme_text = base64.b64decode(f.content).decode("utf-8")
 
460
  break
461
  except: continue
462
-
463
- if readme_text:
464
- # Renderizar Markdown a HTML
465
- readme_html = markdown.markdown(readme_text, extensions=['fenced_code', 'tables'])
466
  except: pass
467
 
468
- return render_template('ver_repo.html',
469
- title=pname,
470
- project_name=pname,
471
- readme_html=readme_html,
472
- web_url=web_url,
473
- download_url=download_url)
474
 
475
  if __name__ == '__main__':
476
- # Ejecución del servidor con Socket.IO y soporte para Eventlet
477
  port = int(os.getenv("PORT", 7860))
478
  socketio.run(app, host="0.0.0.0", port=port, debug=True)
 
13
  from flask_socketio import SocketIO, emit
14
  from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user
15
  from werkzeug.security import generate_password_hash, check_password_hash
 
16
  import gitlab
17
  import telebot
18
  from telebot import types
 
40
  self.id = id
41
  self.username = username
42
 
43
+ # --- GESTOR DE USUARIOS (Persistencia JSON) ---
44
+ class UserManager:
45
+ """Clase para manejar los usuarios en un archivo JSON."""
46
+ def __init__(self, filename="users.json"):
47
+ self.filename = filename
48
+ self.users = self._load()
49
+
50
+ def _load(self):
51
+ """Carga los usuarios desde el archivo JSON."""
52
+ if os.path.exists(self.filename):
53
+ try:
54
+ with open(self.filename, "r") as f:
55
+ return json.load(f)
56
+ except:
57
+ return []
58
+ return []
59
+
60
+ def save(self):
61
+ """Guarda la lista de usuarios en el archivo JSON."""
62
+ with open(self.filename, "w") as f:
63
+ json.dump(self.users, f, indent=4)
64
+
65
+ def add_user(self, username, password):
66
+ """Registra un nuevo usuario con contraseña hasheada."""
67
+ if self.get_by_username(username):
68
+ return False
69
+
70
+ user_id = str(len(self.users) + 1)
71
+ hashed_pw = generate_password_hash(password)
72
+ self.users.append({
73
+ "id": user_id,
74
+ "username": username,
75
+ "password": hashed_pw
76
+ })
77
+ self.save()
78
+ return True
79
+
80
+ def get_by_username(self, username):
81
+ """Busca un usuario por su nombre."""
82
+ for user in self.users:
83
+ if user["username"] == username:
84
+ return user
85
  return None
86
 
87
+ def get_by_id(self, user_id):
88
+ """Busca un usuario por su ID."""
89
+ for user in self.users:
90
+ if user["id"] == user_id:
91
+ return user
92
+ return None
93
+
94
+ user_mgr = UserManager()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
 
96
  @login_manager.user_loader
97
  def load_user(user_id):
98
+ """Cargador de usuario para Flask-Login desde el JSON."""
99
+ user_data = user_mgr.get_by_id(user_id)
100
+ if user_data:
101
+ return User(user_data['id'], user_data['username'])
102
+ return None
 
 
 
 
 
 
 
103
 
104
  # --- GESTOR DE PRÉSTAMOS (Persistencia JSON) ---
105
  class LoanManager:
 
151
  except:
152
  pass
153
 
154
+ # URL del proxy de Google Script
155
  GOOGLE_PROXY_URL = os.getenv("GOOGLE_PROXY_URL") or "https://script.google.com/macros/s/AKfycbz7z1Jb0vsur42GmmqrL3PVXeRkN2WxSojFDIleEDoLOg6MnrmJjb_uuPcQ15CTwyzD/exec"
156
 
157
  if TG_TOKEN and GOOGLE_PROXY_URL:
 
158
  base_url = GOOGLE_PROXY_URL.split('?')[0]
159
  telebot.apihelper.API_URL = base_url + "?path={1}&token={0}"
160
  telebot.apihelper.CONNECT_TIMEOUT = 60
 
163
  bot = telebot.TeleBot(TG_TOKEN) if TG_TOKEN else None
164
 
165
  def escape_md(text):
166
+ """Escapa caracteres para Markdown de Telegram."""
167
  if not text: return ""
168
  for char in ['_', '*', '[', '`']:
169
  text = text.replace(char, f"\\{char}")
170
  return text
171
 
 
172
  if bot:
173
  @bot.callback_query_handler(func=lambda call: True)
174
  def handle_query(call):
 
175
  try:
176
  if call.data.startswith("accept_"):
177
  loan_id = call.data.replace("accept_", "")
 
179
  bot.answer_callback_query(call.id, "Préstamo Aceptado")
180
  nuevo_texto = f"✅ *ACEPTADO*\n{escape_md(call.message.text)}"
181
  bot.edit_message_text(nuevo_texto, call.message.chat.id, call.message.message_id, parse_mode="Markdown")
 
182
  socketio.emit('notification', {"text": f"Préstamo {loan_id[:8]} ACEPTADO", "color": "green"})
183
  elif call.data.startswith("decline_"):
184
  loan_id = call.data.replace("decline_", "")
 
192
 
193
  @bot.message_handler(commands=['aceptar', 'declinar', 'status'])
194
  def handle_text_commands(message):
 
195
  try:
196
  text = message.text.split()
197
  if len(text) < 2:
 
203
  emoji = "✅" if status == "ACCEPTED" else "❌"
204
  bot.reply_to(message, f"{emoji} Préstamo {loan_id} actualizado a {status}")
205
  socketio.emit('notification', {"text": f"Préstamo {loan_id[:8]} {status}", "color": "green" if status == "ACCEPTED" else "red"})
 
 
206
  except Exception as e:
207
  print(f"Command Error: {e}")
208
 
 
 
 
 
 
 
 
 
209
  def start_bot_thread():
 
210
  if bot:
 
211
  time.sleep(5)
212
  try: bot.delete_webhook()
213
  except: pass
214
  while True:
215
  try:
 
216
  bot.infinity_polling(timeout=20, long_polling_timeout=10)
217
  except Exception as e:
 
218
  time.sleep(30)
219
 
220
  if bot:
221
  threading.Thread(target=start_bot_thread, daemon=True).start()
222
 
223
+ # --- RUTAS WEB ---
224
 
225
  @app.route('/')
226
  def index():
 
227
  return render_template('index.html', title="MAKER SPACE")
228
 
229
  @app.route('/login', methods=['GET', 'POST'])
230
  def login():
 
231
  if current_user.is_authenticated:
232
  return redirect(url_for('repos'))
233
 
 
235
  username = request.form.get('username')
236
  password = request.form.get('password')
237
 
238
+ user_data = user_mgr.get_by_username(username)
 
 
 
 
 
 
 
 
239
 
 
240
  if user_data and check_password_hash(user_data['password'], password):
241
+ user = User(user_data['id'], user_data['username'])
242
  login_user(user)
243
  flash(f"¡Bienvenido de nuevo, {username}!", "green")
244
  return redirect(url_for('repos'))
 
249
 
250
  @app.route('/register', methods=['GET', 'POST'])
251
  def register():
 
252
  if current_user.is_authenticated:
253
  return redirect(url_for('repos'))
254
 
 
261
  flash("Las contraseñas no coinciden", "red")
262
  return render_template('register.html')
263
 
264
+ if user_mgr.add_user(username, password):
 
 
 
 
 
 
 
 
 
 
 
265
  flash("Cuenta creada exitosamente. Ahora puedes iniciar sesión.", "green")
266
  return redirect(url_for('login'))
267
+ else:
268
+ flash("El nombre de usuario ya existe.", "red")
 
 
 
 
 
269
 
270
  return render_template('register.html', title="Registro")
271
 
272
  @app.route('/logout')
273
  @login_required
274
  def logout():
 
275
  logout_user()
276
  flash("Has cerrado sesión", "blue")
277
  return redirect(url_for('index'))
278
 
279
  @app.route('/prestamos')
280
  def prestamos():
 
281
  loans = loan_mgr.get_all()
282
  return render_template('prestamos.html', title="Préstamos", loans=loans)
283
 
284
  @app.route('/api/prestamo', methods=['POST'])
285
  def api_prestamo():
 
286
  data = request.json
287
  solicitante = data.get('solicitante')
288
  h_salida = data.get('hora_salida')
 
295
  loan_id = str(uuid.uuid4())
296
  lista_items_str = "\n".join([f"• [{i['categoria']}] {i['descripcion']} (x{i['cantidad']})" for i in items])
297
 
298
+ # Notificar a Telegram
299
  mensaje_tg = (
300
  "🛠 *NUEVA SOLICITUD DE PRÉSTAMO*\n"
301
  f"👤 *Solicitante:* {escape_md(solicitante)}\n"
 
306
  res_tg = "OK"
307
  if bot and TG_CHAT_ID:
308
  try:
 
309
  markup = types.InlineKeyboardMarkup()
310
  markup.add(
311
  types.InlineKeyboardButton("✅ Aceptar", callback_data=f"accept_{loan_id}"),
 
315
  except Exception as e:
316
  res_tg = str(e)
317
 
318
+ # Guardar en JSON
319
+ loan_mgr.add_loan({
320
  "id": loan_id,
321
  "Solicitante": solicitante,
322
  "item": lista_items_str,
 
325
  "devolucion": h_retorno,
326
  "status_loan": "PENDING",
327
  "status_tg": res_tg
328
+ })
329
 
 
330
  return jsonify({"status": "success", "id": loan_id})
331
 
332
  @app.route('/api/entregar/<loan_id>', methods=['POST'])
333
  def api_entregar(loan_id):
 
334
  loan = None
335
  for l in loan_mgr.get_all():
336
  if l['id'] == loan_id:
337
  loan = l
338
  break
339
 
340
+ if not loan or loan['status_loan'] != "ACCEPTED":
341
+ return jsonify({"status": "error", "message": "No se puede entregar"}), 400
 
 
 
342
 
343
+ # Hora AST (Rep. Dom.)
344
  utc_now = datetime.datetime.now(datetime.timezone.utc)
345
  rd_now = utc_now - datetime.timedelta(hours=4)
346
  ahora = rd_now.strftime("%H:%M")
347
 
348
  loan_mgr.update_status(loan_id, "DELIVERED")
349
 
 
350
  mensaje_tg = (
351
  "📦 *ENTREGA DE MATERIALES*\n"
352
  f"👤 *Solicitante:* {escape_md(loan['Solicitante'])}\n"
 
355
  )
356
 
357
  if bot and TG_CHAT_ID:
358
+ try: bot.send_message(TG_CHAT_ID, mensaje_tg, parse_mode="Markdown")
359
+ except: pass
 
 
360
 
361
  socketio.emit('notification', {"text": f"Entrega confirmada para {loan['Solicitante']}", "color": "blue"})
362
  return jsonify({"status": "success"})
 
364
  @app.route('/repos')
365
  @login_required
366
  def repos():
 
 
367
  GIT_TOKEN = os.getenv("GITLAB_TOKEN")
368
  GIT_GROUP = os.getenv("GITLAB_GROUP_ID")
 
369
  projects = []
370
  if GIT_TOKEN and GIT_GROUP:
371
  try:
372
+ gl = gitlab.Gitlab("https://gitlab.com", private_token=GIT_TOKEN)
373
  projects = gl.groups.get(GIT_GROUP).projects.list(all=True)
374
+ except: pass
 
375
  return render_template('repos.html', title="Proyectos", projects=projects)
376
 
377
  @app.route('/ver/<pid>/<pname>')
378
  @login_required
379
  def ver_repo(pid, pname):
 
380
  GIT_TOKEN = os.getenv("GITLAB_TOKEN")
381
+ readme_html = "<p>README no disponible</p>"
382
+ web_url = "#"; download_url = "#"
 
 
 
383
 
384
  if GIT_TOKEN:
385
  try:
386
+ gl = gitlab.Gitlab("https://gitlab.com", private_token=GIT_TOKEN)
387
  project = gl.projects.get(pid)
388
  web_url = project.web_url
389
+ download_url = f"https://gitlab.com/api/v4/projects/{pid}/repository/archive.zip?private_token={GIT_TOKEN}"
390
 
 
 
391
  for branch in ["main", "master"]:
392
  try:
393
  f = project.files.get(file_path='README.md', ref=branch)
394
  readme_text = base64.b64decode(f.content).decode("utf-8")
395
+ readme_html = markdown.markdown(readme_text, extensions=['fenced_code', 'tables'])
396
  break
397
  except: continue
 
 
 
 
398
  except: pass
399
 
400
+ return render_template('ver_repo.html', title=pname, project_name=pname, readme_html=readme_html, web_url=web_url, download_url=download_url)
 
 
 
 
 
401
 
402
  if __name__ == '__main__':
 
403
  port = int(os.getenv("PORT", 7860))
404
  socketio.run(app, host="0.0.0.0", port=port, debug=True)
app/requirements.txt CHANGED
@@ -8,4 +8,3 @@ gunicorn
8
  eventlet
9
  markdown
10
  flask-login
11
- mysql-connector-python
 
8
  eventlet
9
  markdown
10
  flask-login