from gevent import monkey monkey.patch_all() # MUST BE LINE 1 & 2 import os import io import base64 import qrcode import uuid from datetime import datetime from flask import Flask, render_template, request, redirect, url_for, flash from flask_login import LoginManager, login_user, logout_user, current_user, login_required, UserMixin from flask_sqlalchemy import SQLAlchemy from flask_socketio import SocketIO from sqlalchemy import text from werkzeug.middleware.proxy_fix import ProxyFix app = Flask(__name__) # --- INFRASTRUCTURE CONFIG --- app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1) app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'forge-protocol-alpha-772') app.config['PROPAGATE_EXCEPTIONS'] = True # --- DATABASE ENGINE --- database_url = os.environ.get('DATABASE_URL', 'sqlite:///forge.db') if database_url.startswith("postgres://"): database_url = database_url.replace("postgres://", "postgresql+psycopg://", 1) elif database_url.startswith("postgresql://"): database_url = database_url.replace("postgresql://", "postgresql+psycopg://", 1) app.config['SQLALCHEMY_DATABASE_URI'] = database_url app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False db = SQLAlchemy(app) # --- MODELS --- class Organization(db.Model): __tablename__ = 'organization' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(100), unique=True, nullable=False) org_uuid = db.Column(db.String(100), unique=True, default=lambda: str(uuid.uuid4())) created_at = db.Column(db.DateTime, default=datetime.utcnow) users = db.relationship('User', backref='organization', lazy=True) class User(db.Model, UserMixin): __tablename__ = 'user' id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(80), unique=True, nullable=False) password = db.Column(db.String(120), nullable=False) role = db.Column(db.String(20), default='user') first_name = db.Column(db.String(80)) last_name = db.Column(db.String(80)) org_id = db.Column(db.Integer, db.ForeignKey('organization.id'), nullable=True) class TimeCard(db.Model): __tablename__ = 'time_card' id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey('user.id')) org_id = db.Column(db.Integer, db.ForeignKey('organization.id'), nullable=True) date = db.Column(db.String(20)) clock_in = db.Column(db.DateTime) break1_start = db.Column(db.DateTime) break1_end = db.Column(db.DateTime) break2_start = db.Column(db.DateTime) break2_end = db.Column(db.DateTime) clock_out = db.Column(db.DateTime) status = db.Column(db.String(20)) total_hours = db.Column(db.Float, default=0.0) user = db.relationship('User', backref='timecards') # --- INITIALIZATION --- with app.app_context(): try: db.create_all() if not Organization.query.first(): default_org = Organization(name="Forge HQ") db.session.add(default_org) db.session.commit() if not User.query.filter_by(username='Admin').first(): admin_org = Organization.query.first() admin = User(username='Admin', password='AdminPassword772', role='admin', org_id=admin_org.id) db.session.add(admin) db.session.commit() except Exception as e: print(f"DB Startup Error: {e}") # --- UTILS & SOCKETS --- login_manager = LoginManager(app) login_manager.login_view = 'login' socketio = SocketIO(app, async_mode='gevent', cors_allowed_origins="*") @login_manager.user_loader def load_user(user_id): return User.query.get(int(user_id)) # --- ROUTES --- @app.route('/') def index(): return render_template('index.html', title="Home") @app.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'POST': user = User.query.filter_by(username=request.form.get('username')).first() if user and user.password == request.form.get('password'): login_user(user) return redirect(url_for('admin_dashboard' if user.role == 'admin' else 'forge_clock_info')) flash("Invalid Credentials", "danger") return render_template('login.html', title="Operator Login") @app.route('/signup', methods=['GET', 'POST']) def signup(): if request.method == 'POST': if User.query.filter_by(username=request.form.get('username')).first(): flash("Operative ID already registered.", "danger") return redirect(url_for('signup')) org = Organization.query.first() new_user = User( username=request.form.get('username'), password=request.form.get('password'), first_name=request.form.get('first_name'), last_name=request.form.get('last_name'), role='user', org_id=org.id if org else None ) db.session.add(new_user) db.session.commit() login_user(new_user) return redirect(url_for('forge_clock_info')) return render_template('signup.html', title="System Enrollment") @app.route('/forge-clock', methods=['GET', 'POST']) @login_required def forge_clock_info(): # Renamed to match base.html BuildError requirements active = TimeCard.query.filter_by(user_id=current_user.id).filter(TimeCard.status != 'Completed').first() if request.method == 'POST': action = request.form.get('action') now = datetime.now() if action == 'CLOCK_IN' and not active: active = TimeCard(user_id=current_user.id, org_id=current_user.org_id, date=now.strftime("%Y-%m-%d"), clock_in=now, status='Active') db.session.add(active) elif active: if action == 'B1_START': active.break1_start = now elif action == 'B1_END': active.break1_end = now elif action == 'CLOCK_OUT': active.clock_out = now active.status = 'Completed' duration = active.clock_out - active.clock_in active.total_hours = round(duration.total_seconds() / 3600, 2) db.session.commit() return redirect(url_for('forge_clock_info')) history = TimeCard.query.filter_by(user_id=current_user.id).order_by(TimeCard.id.desc()).limit(10).all() return render_template('forge_clock.html', session=active, history=history, title="Forge Clock") @app.route('/qr-gen', methods=['GET', 'POST']) def qr_gen(): qr_img = None if request.method == 'POST': data = request.form.get('qr_data') img = qrcode.make(data) buf = io.BytesIO() img.save(buf, format='PNG') qr_img = base64.b64encode(buf.getvalue()).decode('utf-8') return render_template('qr_generator.html', qr_img=qr_img, title="QR Architect") @app.route('/runway-calc') def runway_calc(): return render_template('index.html', title="Runway Calc") @app.route('/stats') @login_required def stats(): history = TimeCard.query.filter_by(org_id=current_user.org_id).order_by(TimeCard.id.desc()).limit(20).all() total_h = db.session.query(db.func.sum(TimeCard.total_hours)).filter(TimeCard.org_id==current_user.org_id).scalar() or 0.0 active_count = User.query.filter_by(org_id=current_user.org_id).count() return render_template('stats.html', history=history, total_hours=round(total_h, 1), active_users=active_count, title="Metrics") @app.route('/admin-dashboard') @login_required def admin_dashboard(): if current_user.role != 'admin': return "Access Denied", 403 return render_template('admin_dashboard.html', users=User.query.filter_by(org_id=current_user.org_id).all(), timecards=TimeCard.query.filter_by(org_id=current_user.org_id).order_by(TimeCard.id.desc()).all(), title="Admin Console") @app.route('/logout') def logout(): logout_user() return redirect(url_for('index')) if __name__ == "__main__": port = int(os.environ.get("PORT", 8080)) socketio.run(app, host='0.0.0.0', port=port)