diff --git a/.gitignore b/.gitignore index 9161f64..466ae7f 100644 --- a/.gitignore +++ b/.gitignore @@ -207,4 +207,5 @@ cython_debug/ marimo/_static/ marimo/_lsp/ __marimo__/ -README.md \ No newline at end of file +README.md +Claude.md \ No newline at end of file diff --git a/app.py b/app.py index b543b17..f2bd3e5 100644 --- a/app.py +++ b/app.py @@ -175,7 +175,10 @@ def create_app() -> Flask: @app.before_request def log_request_info(): - """Log request information for security monitoring""" + """Record request start time and scan for suspicious user agents""" + # Always record start time for slow-query detection in after_request + g.start_time = _time.time() + if (request.endpoint and (request.endpoint.startswith('static') or request.path.startswith('/api/logs'))): @@ -198,14 +201,24 @@ def create_app() -> Flask: @app.after_request def log_response_info(response): - """Log response information for performance monitoring""" + """Log slow requests and error responses for performance and health monitoring""" from extensions import logger_handler as lh if request.endpoint and request.endpoint.startswith('static'): return response - if hasattr(request, 'start_time'): - duration = _time.time() - request.start_time - if duration > 5.0: - lh.logger.warning(f"Slow request: {request.path} took {duration:.2f} seconds") + if hasattr(g, 'start_time'): + duration = _time.time() - g.start_time + if duration > 2.0: + lh.log_system_event( + event_type="slow_query_detected", + description=f"Slow request: {request.endpoint} took {duration:.2f}s", + severity="WARNING", + additional_data={ + 'duration': duration, + 'endpoint': request.endpoint, + 'method': request.method, + 'user': session.get('username', 'anonymous') + } + ) if response.status_code >= 400: lh.logger.warning( f"Error response: {response.status_code} for {request.path} " @@ -217,37 +230,22 @@ def create_app() -> Flask: # Error handlers # ------------------------------------------------------------------ + @app.errorhandler(403) + def forbidden(error): + """Handle forbidden access errors""" + return render_template('errors/403.html'), 403 + + @app.errorhandler(404) + def not_found(error): + """Handle page not found errors""" + return render_template('errors/404.html'), 404 + @app.errorhandler(500) def internal_error(error): """Handle internal server errors with user-friendly page""" if app.debug: return None - return ''' - - - Server Error - -

🔧 Something went wrong

-

We're working to fix this issue. Please try again later.

- ← Back to Home - - - ''', 500 - - @app.errorhandler(404) - def not_found(error): - """Handle page not found errors""" - return ''' - - - Page Not Found - -

🔍 Page Not Found

-

The page you're looking for doesn't exist.

- ← Back to Home - - - ''', 404 + return render_template('errors/500.html'), 500 return app @@ -293,13 +291,30 @@ def create_tables(): def update_existing_qr_codes(): - """Update existing QR codes with URLs and regenerate QR images with logging""" + """Update existing QR codes with missing URLs or images at startup. + + Regenerates qr_url slugs without needing a request context. + For qr_code_image, constructs the base URL from FLASK_HOST/FLASK_PORT + config so this can run safely outside any HTTP request. + """ from extensions import db as _db, logger_handler as lh from utils.helpers import generate_qr_code, get_qr_styling, generate_qr_url - from flask import current_app, request + from config import Config as _Cfg try: from models.qrcode import QRCode qr_codes = QRCode.query.filter_by(active_status=True).all() + if not qr_codes: + return + + # Build a base URL that does not require an active request context. + host = os.environ.get('FLASK_HOST', '0.0.0.0') + # 0.0.0.0 is a bind address, not a reachable hostname — default to localhost + if host in ('0.0.0.0', ''): + host = 'localhost' + port = os.environ.get('FLASK_PORT', '5000') + scheme = 'https' if _Cfg.SESSION_COOKIE_SECURE else 'http' + base_url = f"{scheme}://{host}:{port}/" + updated_count = 0 for qr_code in qr_codes: if not qr_code.qr_url or not qr_code.qr_code_image: @@ -307,7 +322,7 @@ def update_existing_qr_codes(): if not qr_code.qr_url: qr_code.qr_url = generate_qr_url(qr_code.name, qr_code.id) if not qr_code.qr_code_image: - qr_data = f"{request.url_root}qr/{qr_code.qr_url}" + qr_data = f"{base_url}qr/{qr_code.qr_url}" styling = get_qr_styling(qr_code) qr_code.qr_code_image = generate_qr_code( data=qr_data, @@ -323,37 +338,10 @@ def update_existing_qr_codes(): continue if updated_count > 0: _db.session.commit() - lh.logger.info(f"Updated {updated_count} existing QR codes with missing URLs/images") + lh.logger.info(f"Startup: updated {updated_count} QR codes with missing URLs/images") except Exception as e: lh.log_database_error('update_existing_qr_codes', e) - - -def log_slow_query_performance(app_instance): - """Register slow-query monitoring hooks on the given app instance""" - from extensions import logger_handler as lh - - @app_instance.before_request - def before_request(): - g.start_time = _time.time() - - @app_instance.after_request - def after_request(response): - if hasattr(g, 'start_time'): - duration = _time.time() - g.start_time - if duration > 2.0: - lh.log_system_event( - event_type="slow_query_detected", - description=f"Slow request: {request.endpoint} took {duration:.2f}s", - severity="WARNING", - additional_data={ - 'duration': duration, - 'endpoint': request.endpoint, - 'method': request.method, - 'user': session.get('username', 'anonymous') - } - ) - return response - + # --------------------------------------------------------------------------- # Entry point @@ -365,7 +353,7 @@ if __name__ == '__main__': with app.app_context(): try: create_tables() - log_slow_query_performance(app) + update_existing_qr_codes() from extensions import logger_handler logger_handler.logger.info("Initializing performance optimizations") @@ -389,4 +377,4 @@ if __name__ == '__main__': host=_Cfg.FLASK_HOST, port=_Cfg.FLASK_PORT, threaded=_Cfg.THREADED - ) + ) \ No newline at end of file diff --git a/config.py b/config.py index 2347c5d..5b77e79 100644 --- a/config.py +++ b/config.py @@ -33,6 +33,16 @@ class Config: ) DEBUG = os.environ.get('DEBUG', 'False').lower() == 'true' + # ------------------------------------------------------------------ # + # SQLAlchemy connection pool (read from .env; safe defaults) + # ------------------------------------------------------------------ # + SQLALCHEMY_ENGINE_OPTIONS = { + 'pool_size': int(os.environ.get('SQLALCHEMY_ENGINE_OPTIONS_POOL_SIZE', '10')), + 'pool_timeout': int(os.environ.get('SQLALCHEMY_ENGINE_OPTIONS_POOL_TIMEOUT', '20')), + 'pool_recycle': int(os.environ.get('SQLALCHEMY_ENGINE_OPTIONS_POOL_RECYCLE', '3600')), + 'max_overflow': int(os.environ.get('SQLALCHEMY_ENGINE_OPTIONS_MAX_OVERFLOW', '20')), + } + # ------------------------------------------------------------------ # # Session / cookies # ------------------------------------------------------------------ # @@ -107,4 +117,4 @@ _config_map = { def get_config(): """Return the active Config class based on FLASK_ENV.""" env = os.environ.get('FLASK_ENV', 'default').lower() - return _config_map.get(env, Config) + return _config_map.get(env, Config) \ No newline at end of file diff --git a/models/attendance.py b/models/attendance.py index 24d0694..420303a 100644 --- a/models/attendance.py +++ b/models/attendance.py @@ -18,7 +18,7 @@ class AttendanceData(base.db.Model): qr_code_id = base.db.Column(base.db.Integer, base.db.ForeignKey('qr_codes.id', ondelete='CASCADE'), nullable=False) employee_id = base.db.Column(base.db.String(50), nullable=False) check_in_date = base.db.Column(base.db.Date, nullable=False, default=datetime.today) - check_in_time = base.db.Column(base.db.Time, nullable=False, default=datetime.now().time) + check_in_time = base.db.Column(base.db.Time, nullable=False, default=lambda: datetime.now().time()) device_info = base.db.Column(base.db.String(200)) user_agent = base.db.Column(base.db.Text) ip_address = base.db.Column(base.db.String(45)) diff --git a/models/user.py b/models/user.py index a88bf47..e56e09c 100644 --- a/models/user.py +++ b/models/user.py @@ -53,6 +53,7 @@ class User(base.db.Model): """Check if user has staff-level permissions (includes new roles)""" return self.role in STAFF_LEVEL_ROLES + @staticmethod def has_export_permissions(user_role): """Check if user role has export permissions""" return user_role in ['admin', 'payroll'] diff --git a/routes/payroll.py b/routes/payroll.py index ccbf6db..3b96e0e 100644 --- a/routes/payroll.py +++ b/routes/payroll.py @@ -650,43 +650,4 @@ def get_miss_punch_details(employee_id): 'message': 'Internal server error. Please check the server logs.' }), 500 -def get_employee_name(employee_id): - """Helper function to get employee full name by ID""" - try: - result = db.session.execute(text(""" - SELECT CONCAT(firstName, ' ', lastName) as full_name - FROM employee - WHERE id = :employee_id - """), {'employee_id': employee_id}) - row = result.fetchone() - return row[0] if row else f"Employee {employee_id}" - - except Exception as e: - logger_handler.logger.warning(f"Error getting employee name for ID {employee_id}: {e}") - return f"Employee {employee_id}" - -def get_qr_code_checkin_count(qr_code_id): - """Helper function to get total check-ins count for a QR code""" - try: - count = AttendanceData.query.filter_by(qr_code_id=qr_code_id).count() - logger_handler.logger.info(f"QR Code {qr_code_id} total check-ins: {count}") - return count - except Exception as e: - logger_handler.logger.error(f"Error getting check-ins count for QR {qr_code_id}: {e}") - return 0 - -@bp.context_processor -def inject_payroll_utils(): - """Inject payroll utility functions into templates""" - return { - 'get_employee_name': get_employee_name, - 'format_hours': lambda hours: f"{hours:.2f}" if hours else "0.00" - } - -@bp.context_processor -def inject_dashboard_utils(): - """Inject dashboard utility functions into templates""" - return { - 'get_qr_code_checkin_count': get_qr_code_checkin_count - } diff --git a/routes/time_attendance_export.py b/routes/time_attendance_export.py index ed85fd9..69916ff 100644 --- a/routes/time_attendance_export.py +++ b/routes/time_attendance_export.py @@ -11,17 +11,6 @@ Contains: - export_time_attendance_excel() (single-employee / all-employees) - export_time_attendance_by_building_excel() """ -""" -routes/time_attendance.py -========================= -Time attendance dashboard, import pipeline, export (Excel / by-building), -and records management routes. - -Routes: /time-attendance, /time-attendance/import/*, - /time-attendance/export*, /time-attendance/records, - /time-attendance/record/, /time-attendance/delete/, - /api/time-attendance/* -""" from flask import Blueprint, render_template, request, redirect, flash, session, jsonify, send_file, Response, g, current_app, url_for from datetime import datetime, date, timedelta, time import io, os, json, re, uuid, traceback diff --git a/templates/errors/403.html b/templates/errors/403.html index b10a1da..d5b9bc5 100644 --- a/templates/errors/403.html +++ b/templates/errors/403.html @@ -10,12 +10,12 @@ Management{% endblock %} {% block content %}

This action requires administrator privileges.

- + Go to Dashboard {% if session.role == 'staff' %} - + View Profile diff --git a/templates/errors/404.html b/templates/errors/404.html index a7eb5d9..cbb3aa8 100644 --- a/templates/errors/404.html +++ b/templates/errors/404.html @@ -9,7 +9,7 @@ endblock %} {% block content %}

The page you're looking for doesn't exist or has been moved.

- + Go to Dashboard diff --git a/templates/errors/500.html b/templates/errors/500.html index c01c33c..d0b119b 100644 --- a/templates/errors/500.html +++ b/templates/errors/500.html @@ -10,7 +10,7 @@ endblock %} {% block content %}

Please try again in a few moments.

- + Go to Dashboard diff --git a/utils/geocoding.py b/utils/geocoding.py index b88a4dd..5f57692 100644 --- a/utils/geocoding.py +++ b/utils/geocoding.py @@ -34,6 +34,16 @@ except Exception as e: gmaps_client = None print(f"❌ Error initializing Google Maps client: {e}") + +def is_gmaps_available(): + """Return True if the Google Maps client is initialized and usable. + + Always call this (or check ``if gmaps_client:``) before calling any + method on ``gmaps_client`` to prevent AttributeError when the API key + is absent. + """ + return gmaps_client is not None + # --------------------------------------------------------------------------- # Geocoding cache # --------------------------------------------------------------------------- @@ -811,4 +821,4 @@ def get_all_locations_from_qr_codes(): return [row[0] for row in result.fetchall()] except Exception as e: logger_handler.logger.error(f"Error loading locations: {e}") - return [] + return [] \ No newline at end of file