sustitude flet by flask
Browse files- app/main.py +89 -11
- app/requirements.txt +2 -0
- app/static/manifest.json +27 -0
- app/static/sw.js +22 -0
- app/templates/base.html +69 -10
- app/templates/login.html +28 -0
app/main.py
CHANGED
|
@@ -8,8 +8,10 @@ import time
|
|
| 8 |
import base64
|
| 9 |
import requests
|
| 10 |
import datetime
|
| 11 |
-
from flask import Flask, render_template, request, jsonify, redirect, url_for
|
| 12 |
from flask_socketio import SocketIO, emit
|
|
|
|
|
|
|
| 13 |
import gitlab
|
| 14 |
import telebot
|
| 15 |
from telebot import types
|
|
@@ -18,7 +20,50 @@ from dotenv import load_dotenv
|
|
| 18 |
|
| 19 |
load_dotenv()
|
| 20 |
|
| 21 |
-
# ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
class LoanManager:
|
| 23 |
def __init__(self, filename="prestamos.json"):
|
| 24 |
self.filename = filename
|
|
@@ -54,12 +99,7 @@ class LoanManager:
|
|
| 54 |
|
| 55 |
loan_mgr = LoanManager()
|
| 56 |
|
| 57 |
-
# ---
|
| 58 |
-
app = Flask(__name__)
|
| 59 |
-
app.config['SECRET_KEY'] = 'maker-secret-key'
|
| 60 |
-
socketio = SocketIO(app, cors_allowed_origins="*")
|
| 61 |
-
|
| 62 |
-
# --- CONFIGURACIÓN DE VARIABLES GLOBALES ---
|
| 63 |
TG_TOKEN = os.getenv("TELEGRAM_TOKEN")
|
| 64 |
TG_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID")
|
| 65 |
try:
|
|
@@ -69,14 +109,12 @@ except:
|
|
| 69 |
|
| 70 |
GOOGLE_PROXY_URL = os.getenv("GOOGLE_PROXY_URL") or "https://script.google.com/macros/s/AKfycbz7z1Jb0vsur42GmmqrL3PVXeRkN2WxSojFDIleEDoLOg6MnrmJjb_uuPcQ15CTwyzD/exec"
|
| 71 |
|
| 72 |
-
# --- CONFIGURACIÓN DE PROXY PARA EL BOT ---
|
| 73 |
if TG_TOKEN and GOOGLE_PROXY_URL:
|
| 74 |
base_url = GOOGLE_PROXY_URL.split('?')[0]
|
| 75 |
telebot.apihelper.API_URL = base_url + "?path={1}&token={0}"
|
| 76 |
telebot.apihelper.CONNECT_TIMEOUT = 60
|
| 77 |
telebot.apihelper.READ_TIMEOUT = 60
|
| 78 |
|
| 79 |
-
# Inicializar bot
|
| 80 |
bot = telebot.TeleBot(TG_TOKEN) if TG_TOKEN else None
|
| 81 |
|
| 82 |
def escape_md(text):
|
|
@@ -154,6 +192,43 @@ if bot:
|
|
| 154 |
def index():
|
| 155 |
return render_template('index.html', title="MAKER STATION")
|
| 156 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
@app.route('/prestamos')
|
| 158 |
def prestamos():
|
| 159 |
loans = loan_mgr.get_all()
|
|
@@ -207,6 +282,7 @@ def api_prestamo():
|
|
| 207 |
return jsonify({"status": "success", "id": loan_id})
|
| 208 |
|
| 209 |
@app.route('/repos')
|
|
|
|
| 210 |
def repos():
|
| 211 |
GITLAB_URL = "https://gitlab.com"
|
| 212 |
GIT_TOKEN = os.getenv("GITLAB_TOKEN")
|
|
@@ -217,10 +293,12 @@ def repos():
|
|
| 217 |
try:
|
| 218 |
gl = gitlab.Gitlab(GITLAB_URL, private_token=GIT_TOKEN)
|
| 219 |
projects = gl.groups.get(GIT_GROUP).projects.list(all=True)
|
| 220 |
-
except:
|
|
|
|
| 221 |
return render_template('repos.html', title="Proyectos", projects=projects)
|
| 222 |
|
| 223 |
@app.route('/ver/<pid>/<pname>')
|
|
|
|
| 224 |
def ver_repo(pid, pname):
|
| 225 |
GIT_TOKEN = os.getenv("GITLAB_TOKEN")
|
| 226 |
GITLAB_URL = "https://gitlab.com"
|
|
|
|
| 8 |
import base64
|
| 9 |
import requests
|
| 10 |
import datetime
|
| 11 |
+
from flask import Flask, render_template, request, jsonify, redirect, url_for, flash
|
| 12 |
from flask_socketio import SocketIO, emit
|
| 13 |
+
from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user
|
| 14 |
+
import mysql.connector
|
| 15 |
import gitlab
|
| 16 |
import telebot
|
| 17 |
from telebot import types
|
|
|
|
| 20 |
|
| 21 |
load_dotenv()
|
| 22 |
|
| 23 |
+
# --- APP CONFIG ---
|
| 24 |
+
app = Flask(__name__)
|
| 25 |
+
app.config['SECRET_KEY'] = os.getenv("SECRET_KEY", "maker-secret-key")
|
| 26 |
+
socketio = SocketIO(app, cors_allowed_origins="*")
|
| 27 |
+
|
| 28 |
+
# --- LOGIN MANAGER ---
|
| 29 |
+
login_manager = LoginManager()
|
| 30 |
+
login_manager.init_app(app)
|
| 31 |
+
login_manager.login_view = 'login'
|
| 32 |
+
login_manager.login_message = "Por favor, inicia sesión para acceder."
|
| 33 |
+
login_manager.login_message_category = "red"
|
| 34 |
+
|
| 35 |
+
class User(UserMixin):
|
| 36 |
+
def __init__(self, id, username):
|
| 37 |
+
self.id = id
|
| 38 |
+
self.username = username
|
| 39 |
+
|
| 40 |
+
# --- MYSQL CONFIG ---
|
| 41 |
+
def get_db_connection():
|
| 42 |
+
try:
|
| 43 |
+
conn = mysql.connector.connect(
|
| 44 |
+
host=os.getenv("MYSQL_HOST", "localhost"),
|
| 45 |
+
user=os.getenv("MYSQL_USER", "root"),
|
| 46 |
+
password=os.getenv("MYSQL_PASSWORD", ""),
|
| 47 |
+
database=os.getenv("MYSQL_DB", "makerpage")
|
| 48 |
+
)
|
| 49 |
+
return conn
|
| 50 |
+
except Exception as e:
|
| 51 |
+
print(f"Error MySQL: {e}")
|
| 52 |
+
return None
|
| 53 |
+
|
| 54 |
+
@login_manager.user_loader
|
| 55 |
+
def load_user(user_id):
|
| 56 |
+
conn = get_db_connection()
|
| 57 |
+
if not conn: return None
|
| 58 |
+
cursor = conn.cursor(dictionary=True)
|
| 59 |
+
cursor.execute("SELECT id, username FROM users WHERE id = %s", (user_id,))
|
| 60 |
+
user_data = cursor.fetchone()
|
| 61 |
+
conn.close()
|
| 62 |
+
if user_data:
|
| 63 |
+
return User(str(user_data['id']), user_data['username'])
|
| 64 |
+
return None
|
| 65 |
+
|
| 66 |
+
# --- CLASE PARA PERSISTENCIA DE PRÉSTAMOS ---
|
| 67 |
class LoanManager:
|
| 68 |
def __init__(self, filename="prestamos.json"):
|
| 69 |
self.filename = filename
|
|
|
|
| 99 |
|
| 100 |
loan_mgr = LoanManager()
|
| 101 |
|
| 102 |
+
# --- CONFIGURACIÓN DE TELEGRAM ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
TG_TOKEN = os.getenv("TELEGRAM_TOKEN")
|
| 104 |
TG_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID")
|
| 105 |
try:
|
|
|
|
| 109 |
|
| 110 |
GOOGLE_PROXY_URL = os.getenv("GOOGLE_PROXY_URL") or "https://script.google.com/macros/s/AKfycbz7z1Jb0vsur42GmmqrL3PVXeRkN2WxSojFDIleEDoLOg6MnrmJjb_uuPcQ15CTwyzD/exec"
|
| 111 |
|
|
|
|
| 112 |
if TG_TOKEN and GOOGLE_PROXY_URL:
|
| 113 |
base_url = GOOGLE_PROXY_URL.split('?')[0]
|
| 114 |
telebot.apihelper.API_URL = base_url + "?path={1}&token={0}"
|
| 115 |
telebot.apihelper.CONNECT_TIMEOUT = 60
|
| 116 |
telebot.apihelper.READ_TIMEOUT = 60
|
| 117 |
|
|
|
|
| 118 |
bot = telebot.TeleBot(TG_TOKEN) if TG_TOKEN else None
|
| 119 |
|
| 120 |
def escape_md(text):
|
|
|
|
| 192 |
def index():
|
| 193 |
return render_template('index.html', title="MAKER STATION")
|
| 194 |
|
| 195 |
+
@app.route('/login', methods=['GET', 'POST'])
|
| 196 |
+
def login():
|
| 197 |
+
if current_user.is_authenticated:
|
| 198 |
+
return redirect(url_for('repos'))
|
| 199 |
+
|
| 200 |
+
if request.method == 'POST':
|
| 201 |
+
username = request.form.get('username')
|
| 202 |
+
password = request.form.get('password')
|
| 203 |
+
|
| 204 |
+
conn = get_db_connection()
|
| 205 |
+
if not conn:
|
| 206 |
+
flash("Error de conexión a la base de datos", "red")
|
| 207 |
+
return render_template('login.html')
|
| 208 |
+
|
| 209 |
+
cursor = conn.cursor(dictionary=True)
|
| 210 |
+
# NOTA: En producción usar hashing (e.g. werkzeug.security)
|
| 211 |
+
cursor.execute("SELECT id, username FROM users WHERE username = %s AND password = %s", (username, password))
|
| 212 |
+
user_data = cursor.fetchone()
|
| 213 |
+
conn.close()
|
| 214 |
+
|
| 215 |
+
if user_data:
|
| 216 |
+
user = User(str(user_data['id']), user_data['username'])
|
| 217 |
+
login_user(user)
|
| 218 |
+
flash(f"¡Bienvenido, {username}!", "green")
|
| 219 |
+
return redirect(url_for('repos'))
|
| 220 |
+
else:
|
| 221 |
+
flash("Usuario o contraseña incorrectos", "red")
|
| 222 |
+
|
| 223 |
+
return render_template('login.html', title="Login")
|
| 224 |
+
|
| 225 |
+
@app.route('/logout')
|
| 226 |
+
@login_required
|
| 227 |
+
def logout():
|
| 228 |
+
logout_user()
|
| 229 |
+
flash("Has cerrado sesión", "blue")
|
| 230 |
+
return redirect(url_for('index'))
|
| 231 |
+
|
| 232 |
@app.route('/prestamos')
|
| 233 |
def prestamos():
|
| 234 |
loans = loan_mgr.get_all()
|
|
|
|
| 282 |
return jsonify({"status": "success", "id": loan_id})
|
| 283 |
|
| 284 |
@app.route('/repos')
|
| 285 |
+
@login_required
|
| 286 |
def repos():
|
| 287 |
GITLAB_URL = "https://gitlab.com"
|
| 288 |
GIT_TOKEN = os.getenv("GITLAB_TOKEN")
|
|
|
|
| 293 |
try:
|
| 294 |
gl = gitlab.Gitlab(GITLAB_URL, private_token=GIT_TOKEN)
|
| 295 |
projects = gl.groups.get(GIT_GROUP).projects.list(all=True)
|
| 296 |
+
except Exception as e:
|
| 297 |
+
print(f"GitLab Error: {e}")
|
| 298 |
return render_template('repos.html', title="Proyectos", projects=projects)
|
| 299 |
|
| 300 |
@app.route('/ver/<pid>/<pname>')
|
| 301 |
+
@login_required
|
| 302 |
def ver_repo(pid, pname):
|
| 303 |
GIT_TOKEN = os.getenv("GITLAB_TOKEN")
|
| 304 |
GITLAB_URL = "https://gitlab.com"
|
app/requirements.txt
CHANGED
|
@@ -7,3 +7,5 @@ python-dotenv
|
|
| 7 |
gunicorn
|
| 8 |
eventlet
|
| 9 |
markdown
|
|
|
|
|
|
|
|
|
| 7 |
gunicorn
|
| 8 |
eventlet
|
| 9 |
markdown
|
| 10 |
+
flask-login
|
| 11 |
+
mysql-connector-python
|
app/static/manifest.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "Maker Station",
|
| 3 |
+
"short_name": "MakerStation",
|
| 4 |
+
"description": "Panel de control para Maker Station",
|
| 5 |
+
"start_url": "/",
|
| 6 |
+
"display": "standalone",
|
| 7 |
+
"background_color": "#0f172a",
|
| 8 |
+
"theme_color": "#38bdf8",
|
| 9 |
+
"icons": [
|
| 10 |
+
{
|
| 11 |
+
"src": "/static/assets/icon192x192.png",
|
| 12 |
+
"sizes": "192x192",
|
| 13 |
+
"type": "image/png"
|
| 14 |
+
},
|
| 15 |
+
{
|
| 16 |
+
"src": "/static/assets/icon-512x512.png",
|
| 17 |
+
"sizes": "512x512",
|
| 18 |
+
"type": "image/png"
|
| 19 |
+
},
|
| 20 |
+
{
|
| 21 |
+
"src": "/static/assets/apple-touch-icon.png",
|
| 22 |
+
"sizes": "180x180",
|
| 23 |
+
"type": "image/png",
|
| 24 |
+
"purpose": "maskable"
|
| 25 |
+
}
|
| 26 |
+
]
|
| 27 |
+
}
|
app/static/sw.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const CACHE_NAME = 'maker-station-v1';
|
| 2 |
+
const ASSETS = [
|
| 3 |
+
'/',
|
| 4 |
+
'/static/css/style.css',
|
| 5 |
+
'/static/js/script.js',
|
| 6 |
+
'/static/assets/favicon.png',
|
| 7 |
+
'/static/assets/icon192x192.png'
|
| 8 |
+
];
|
| 9 |
+
|
| 10 |
+
self.addEventListener('install', (event) => {
|
| 11 |
+
event.waitUntil(
|
| 12 |
+
caches.open(CACHE_NAME).then((cache) => cache.addAll(ASSETS))
|
| 13 |
+
);
|
| 14 |
+
});
|
| 15 |
+
|
| 16 |
+
self.addEventListener('fetch', (event) => {
|
| 17 |
+
event.respondWith(
|
| 18 |
+
caches.match(event.request).then((response) => {
|
| 19 |
+
return response || fetch(event.request);
|
| 20 |
+
})
|
| 21 |
+
);
|
| 22 |
+
});
|
app/templates/base.html
CHANGED
|
@@ -5,6 +5,15 @@
|
|
| 5 |
<meta charset="UTF-8">
|
| 6 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
<title>{{ title or "MAKERSTATION" }}</title>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
<link rel="icon" href="{{ url_for('static', filename='assets/favicon.png') }}">
|
| 9 |
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
| 10 |
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=Outfit:wght@500;700&display=swap"
|
|
@@ -21,16 +30,34 @@
|
|
| 21 |
<span>MAKER STATION</span>
|
| 22 |
</a>
|
| 23 |
<div class="nav-links">
|
| 24 |
-
<a href="/repos" class="nav-item">PROYECTOS</a>
|
| 25 |
<a href="/prestamos" class="nav-item">PRÉSTAMOS</a>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
</div>
|
| 27 |
-
<button id="notif-btn" class="btn-icon">
|
| 28 |
-
<i class="fas fa-bell"></i>
|
| 29 |
-
</button>
|
| 30 |
</div>
|
| 31 |
</nav>
|
| 32 |
|
| 33 |
<main class="content-wrapper">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
{% block content %}{% endblock %}
|
| 35 |
</main>
|
| 36 |
|
|
@@ -38,12 +65,8 @@
|
|
| 38 |
|
| 39 |
<script src="{{ url_for('static', filename='js/script.js') }}"></script>
|
| 40 |
<script>
|
|
|
|
| 41 |
const socket = io();
|
| 42 |
-
|
| 43 |
-
socket.on('connect', () => {
|
| 44 |
-
console.log('Static Connection established');
|
| 45 |
-
});
|
| 46 |
-
|
| 47 |
socket.on('notification', (data) => {
|
| 48 |
showNotification(data.text, data.color || 'blue');
|
| 49 |
});
|
|
@@ -56,7 +79,10 @@
|
|
| 56 |
container.appendChild(notif);
|
| 57 |
|
| 58 |
if ("Notification" in window && Notification.permission === "granted") {
|
| 59 |
-
new Notification("MAKER STATION", {
|
|
|
|
|
|
|
|
|
|
| 60 |
}
|
| 61 |
|
| 62 |
setTimeout(() => {
|
|
@@ -65,11 +91,44 @@
|
|
| 65 |
}, 5000);
|
| 66 |
}
|
| 67 |
|
|
|
|
| 68 |
document.getElementById('notif-btn').addEventListener('click', () => {
|
| 69 |
if ("Notification" in window) {
|
| 70 |
Notification.requestPermission();
|
| 71 |
}
|
| 72 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
</script>
|
| 74 |
</body>
|
| 75 |
|
|
|
|
| 5 |
<meta charset="UTF-8">
|
| 6 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
<title>{{ title or "MAKERSTATION" }}</title>
|
| 8 |
+
|
| 9 |
+
<!-- PWA Meta Tags -->
|
| 10 |
+
<link rel="manifest" href="{{ url_for('static', filename='manifest.json') }}">
|
| 11 |
+
<meta name="theme-color" content="#38bdf8">
|
| 12 |
+
<link rel="apple-touch-icon" href="{{ url_for('static', filename='assets/apple-touch-icon.png') }}">
|
| 13 |
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
| 14 |
+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
| 15 |
+
<meta name="apple-mobile-web-app-title" content="Maker Station">
|
| 16 |
+
|
| 17 |
<link rel="icon" href="{{ url_for('static', filename='assets/favicon.png') }}">
|
| 18 |
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
| 19 |
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=Outfit:wght@500;700&display=swap"
|
|
|
|
| 30 |
<span>MAKER STATION</span>
|
| 31 |
</a>
|
| 32 |
<div class="nav-links">
|
|
|
|
| 33 |
<a href="/prestamos" class="nav-item">PRÉSTAMOS</a>
|
| 34 |
+
<a href="/repos" class="nav-item">PROYECTOS</a>
|
| 35 |
+
{% if current_user.is_authenticated %}
|
| 36 |
+
<a href="/logout" class="nav-item" style="color: #ef4444;"><i class="fas fa-sign-out-alt"></i></a>
|
| 37 |
+
{% endif %}
|
| 38 |
+
</div>
|
| 39 |
+
<div style="display: flex; gap: 1rem; align-items: center;">
|
| 40 |
+
<button id="install-btn" class="btn-icon" style="display: none;">
|
| 41 |
+
<i class="fas fa-download"></i>
|
| 42 |
+
</button>
|
| 43 |
+
<button id="notif-btn" class="btn-icon">
|
| 44 |
+
<i class="fas fa-bell"></i>
|
| 45 |
+
</button>
|
| 46 |
</div>
|
|
|
|
|
|
|
|
|
|
| 47 |
</div>
|
| 48 |
</nav>
|
| 49 |
|
| 50 |
<main class="content-wrapper">
|
| 51 |
+
{% with messages = get_flashed_messages(with_categories=true) %}
|
| 52 |
+
{% if messages %}
|
| 53 |
+
{% for category, message in messages %}
|
| 54 |
+
<div class="notification glass {{ category }}" style="margin-bottom: 2rem;">
|
| 55 |
+
<i class="fas fa-exclamation-circle"></i> <span>{{ message }}</span>
|
| 56 |
+
</div>
|
| 57 |
+
{% endfor %}
|
| 58 |
+
{% endif %}
|
| 59 |
+
{% endwith %}
|
| 60 |
+
|
| 61 |
{% block content %}{% endblock %}
|
| 62 |
</main>
|
| 63 |
|
|
|
|
| 65 |
|
| 66 |
<script src="{{ url_for('static', filename='js/script.js') }}"></script>
|
| 67 |
<script>
|
| 68 |
+
// Socket.IO
|
| 69 |
const socket = io();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
socket.on('notification', (data) => {
|
| 71 |
showNotification(data.text, data.color || 'blue');
|
| 72 |
});
|
|
|
|
| 79 |
container.appendChild(notif);
|
| 80 |
|
| 81 |
if ("Notification" in window && Notification.permission === "granted") {
|
| 82 |
+
new Notification("MAKER STATION", {
|
| 83 |
+
body: text,
|
| 84 |
+
icon: "/static/assets/icon192x192.png"
|
| 85 |
+
});
|
| 86 |
}
|
| 87 |
|
| 88 |
setTimeout(() => {
|
|
|
|
| 91 |
}, 5000);
|
| 92 |
}
|
| 93 |
|
| 94 |
+
// Notification Permission
|
| 95 |
document.getElementById('notif-btn').addEventListener('click', () => {
|
| 96 |
if ("Notification" in window) {
|
| 97 |
Notification.requestPermission();
|
| 98 |
}
|
| 99 |
});
|
| 100 |
+
|
| 101 |
+
// PWA Service Worker Registration
|
| 102 |
+
if ('serviceWorker' in navigator) {
|
| 103 |
+
window.addEventListener('load', () => {
|
| 104 |
+
navigator.serviceWorker.register('/static/sw.js').then((reg) => {
|
| 105 |
+
console.log('SW Registered', reg);
|
| 106 |
+
}).catch((err) => {
|
| 107 |
+
console.log('SW Registration failed', err);
|
| 108 |
+
});
|
| 109 |
+
});
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
// PWA Install Prompt
|
| 113 |
+
let deferredPrompt;
|
| 114 |
+
const installBtn = document.getElementById('install-btn');
|
| 115 |
+
|
| 116 |
+
window.addEventListener('beforeinstallprompt', (e) => {
|
| 117 |
+
e.preventDefault();
|
| 118 |
+
deferredPrompt = e;
|
| 119 |
+
installBtn.style.display = 'block';
|
| 120 |
+
});
|
| 121 |
+
|
| 122 |
+
installBtn.addEventListener('click', async () => {
|
| 123 |
+
if (deferredPrompt) {
|
| 124 |
+
deferredPrompt.prompt();
|
| 125 |
+
const { outcome } = await deferredPrompt.userChoice;
|
| 126 |
+
if (outcome === 'accepted') {
|
| 127 |
+
installBtn.style.display = 'none';
|
| 128 |
+
}
|
| 129 |
+
deferredPrompt = null;
|
| 130 |
+
}
|
| 131 |
+
});
|
| 132 |
</script>
|
| 133 |
</body>
|
| 134 |
|
app/templates/login.html
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block content %}
|
| 4 |
+
<div class="hero section" style="text-align: center; margin-bottom: 3rem;">
|
| 5 |
+
<h1>INICIO DE SESIÓN</h1>
|
| 6 |
+
<p class="text-dim">Accede a los proyectos de Maker Station.</p>
|
| 7 |
+
</div>
|
| 8 |
+
|
| 9 |
+
<div class="glass" style="max-width: 400px; margin: 0 auto; padding: 2.5rem; border-radius: 24px;">
|
| 10 |
+
<form method="POST">
|
| 11 |
+
<div class="form-group">
|
| 12 |
+
<label for="username">Usuario</label>
|
| 13 |
+
<input type="text" id="username" name="username" placeholder="Tu usuario" required autofocus>
|
| 14 |
+
</div>
|
| 15 |
+
<div class="form-group">
|
| 16 |
+
<label for="password">Contraseña</label>
|
| 17 |
+
<input type="password" id="password" name="password" placeholder="••••••••" required>
|
| 18 |
+
</div>
|
| 19 |
+
<button type="submit" class="btn btn-primary" style="width: 100%; margin-top: 1rem;">
|
| 20 |
+
ENTRAR <i class="fas fa-sign-in-alt" style="margin-left: 0.5rem;"></i>
|
| 21 |
+
</button>
|
| 22 |
+
</form>
|
| 23 |
+
|
| 24 |
+
<div style="margin-top: 2rem; text-align: center; font-size: 0.85rem;" class="text-dim">
|
| 25 |
+
<p>Esta sección está protegida para miembros de Maker Station.</p>
|
| 26 |
+
</div>
|
| 27 |
+
</div>
|
| 28 |
+
{% endblock %}
|