04/08 Refactored codes, fixed issues

This commit is contained in:
2026-04-08 16:12:33 -04:00
parent c5e6566b6d
commit 6e1c9bbc7d
11 changed files with 84 additions and 124 deletions
+2 -1
View File
@@ -207,4 +207,5 @@ cython_debug/
marimo/_static/ marimo/_static/
marimo/_lsp/ marimo/_lsp/
__marimo__/ __marimo__/
README.md README.md
Claude.md
+54 -66
View File
@@ -175,7 +175,10 @@ def create_app() -> Flask:
@app.before_request @app.before_request
def log_request_info(): 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 if (request.endpoint and
(request.endpoint.startswith('static') or (request.endpoint.startswith('static') or
request.path.startswith('/api/logs'))): request.path.startswith('/api/logs'))):
@@ -198,14 +201,24 @@ def create_app() -> Flask:
@app.after_request @app.after_request
def log_response_info(response): 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 from extensions import logger_handler as lh
if request.endpoint and request.endpoint.startswith('static'): if request.endpoint and request.endpoint.startswith('static'):
return response return response
if hasattr(request, 'start_time'): if hasattr(g, 'start_time'):
duration = _time.time() - request.start_time duration = _time.time() - g.start_time
if duration > 5.0: if duration > 2.0:
lh.logger.warning(f"Slow request: {request.path} took {duration:.2f} seconds") 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: if response.status_code >= 400:
lh.logger.warning( lh.logger.warning(
f"Error response: {response.status_code} for {request.path} " f"Error response: {response.status_code} for {request.path} "
@@ -217,37 +230,22 @@ def create_app() -> Flask:
# Error handlers # 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) @app.errorhandler(500)
def internal_error(error): def internal_error(error):
"""Handle internal server errors with user-friendly page""" """Handle internal server errors with user-friendly page"""
if app.debug: if app.debug:
return None return None
return ''' return render_template('errors/500.html'), 500
<!DOCTYPE html>
<html>
<head><title>Server Error</title></head>
<body style="font-family: Arial; text-align: center; margin-top: 100px;">
<h1>🔧 Something went wrong</h1>
<p>We're working to fix this issue. Please try again later.</p>
<a href="/" style="color: #2563eb;">← Back to Home</a>
</body>
</html>
''', 500
@app.errorhandler(404)
def not_found(error):
"""Handle page not found errors"""
return '''
<!DOCTYPE html>
<html>
<head><title>Page Not Found</title></head>
<body style="font-family: Arial; text-align: center; margin-top: 100px;">
<h1>🔍 Page Not Found</h1>
<p>The page you're looking for doesn't exist.</p>
<a href="/" style="color: #2563eb;">← Back to Home</a>
</body>
</html>
''', 404
return app return app
@@ -293,13 +291,30 @@ def create_tables():
def update_existing_qr_codes(): 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 extensions import db as _db, logger_handler as lh
from utils.helpers import generate_qr_code, get_qr_styling, generate_qr_url 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: try:
from models.qrcode import QRCode from models.qrcode import QRCode
qr_codes = QRCode.query.filter_by(active_status=True).all() 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 updated_count = 0
for qr_code in qr_codes: for qr_code in qr_codes:
if not qr_code.qr_url or not qr_code.qr_code_image: 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: if not qr_code.qr_url:
qr_code.qr_url = generate_qr_url(qr_code.name, qr_code.id) qr_code.qr_url = generate_qr_url(qr_code.name, qr_code.id)
if not qr_code.qr_code_image: 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) styling = get_qr_styling(qr_code)
qr_code.qr_code_image = generate_qr_code( qr_code.qr_code_image = generate_qr_code(
data=qr_data, data=qr_data,
@@ -323,37 +338,10 @@ def update_existing_qr_codes():
continue continue
if updated_count > 0: if updated_count > 0:
_db.session.commit() _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: except Exception as e:
lh.log_database_error('update_existing_qr_codes', 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 # Entry point
@@ -365,7 +353,7 @@ if __name__ == '__main__':
with app.app_context(): with app.app_context():
try: try:
create_tables() create_tables()
log_slow_query_performance(app) update_existing_qr_codes()
from extensions import logger_handler from extensions import logger_handler
logger_handler.logger.info("Initializing performance optimizations") logger_handler.logger.info("Initializing performance optimizations")
@@ -389,4 +377,4 @@ if __name__ == '__main__':
host=_Cfg.FLASK_HOST, host=_Cfg.FLASK_HOST,
port=_Cfg.FLASK_PORT, port=_Cfg.FLASK_PORT,
threaded=_Cfg.THREADED threaded=_Cfg.THREADED
) )
+11 -1
View File
@@ -33,6 +33,16 @@ class Config:
) )
DEBUG = os.environ.get('DEBUG', 'False').lower() == 'true' 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 # Session / cookies
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
@@ -107,4 +117,4 @@ _config_map = {
def get_config(): def get_config():
"""Return the active Config class based on FLASK_ENV.""" """Return the active Config class based on FLASK_ENV."""
env = os.environ.get('FLASK_ENV', 'default').lower() env = os.environ.get('FLASK_ENV', 'default').lower()
return _config_map.get(env, Config) return _config_map.get(env, Config)
+1 -1
View File
@@ -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) 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) 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_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)) device_info = base.db.Column(base.db.String(200))
user_agent = base.db.Column(base.db.Text) user_agent = base.db.Column(base.db.Text)
ip_address = base.db.Column(base.db.String(45)) ip_address = base.db.Column(base.db.String(45))
+1
View File
@@ -53,6 +53,7 @@ class User(base.db.Model):
"""Check if user has staff-level permissions (includes new roles)""" """Check if user has staff-level permissions (includes new roles)"""
return self.role in STAFF_LEVEL_ROLES return self.role in STAFF_LEVEL_ROLES
@staticmethod
def has_export_permissions(user_role): def has_export_permissions(user_role):
"""Check if user role has export permissions""" """Check if user role has export permissions"""
return user_role in ['admin', 'payroll'] return user_role in ['admin', 'payroll']
-39
View File
@@ -650,43 +650,4 @@ def get_miss_punch_details(employee_id):
'message': 'Internal server error. Please check the server logs.' 'message': 'Internal server error. Please check the server logs.'
}), 500 }), 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
}
-11
View File
@@ -11,17 +11,6 @@ Contains:
- export_time_attendance_excel() (single-employee / all-employees) - export_time_attendance_excel() (single-employee / all-employees)
- export_time_attendance_by_building_excel() - 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/<id>, /time-attendance/delete/<id>,
/api/time-attendance/*
"""
from flask import Blueprint, render_template, request, redirect, flash, session, jsonify, send_file, Response, g, current_app, url_for 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 from datetime import datetime, date, timedelta, time
import io, os, json, re, uuid, traceback import io, os, json, re, uuid, traceback
+2 -2
View File
@@ -10,12 +10,12 @@ Management{% endblock %} {% block content %}
<p class="error-detail">This action requires administrator privileges.</p> <p class="error-detail">This action requires administrator privileges.</p>
</div> </div>
<div class="error-actions"> <div class="error-actions">
<a href="{{ url_for('dashboard') }}" class="btn btn-primary"> <a href="{{ url_for('dashboard.dashboard') }}" class="btn btn-primary">
<i class="fas fa-home"></i> <i class="fas fa-home"></i>
Go to Dashboard Go to Dashboard
</a> </a>
{% if session.role == 'staff' %} {% if session.role == 'staff' %}
<a href="{{ url_for('profile') }}" class="btn btn-secondary"> <a href="{{ url_for('auth.profile') }}" class="btn btn-secondary">
<i class="fas fa-user"></i> <i class="fas fa-user"></i>
View Profile View Profile
</a> </a>
+1 -1
View File
@@ -9,7 +9,7 @@ endblock %} {% block content %}
<p>The page you're looking for doesn't exist or has been moved.</p> <p>The page you're looking for doesn't exist or has been moved.</p>
</div> </div>
<div class="error-actions"> <div class="error-actions">
<a href="{{ url_for('dashboard') }}" class="btn btn-primary"> <a href="{{ url_for('dashboard.dashboard') }}" class="btn btn-primary">
<i class="fas fa-home"></i> <i class="fas fa-home"></i>
Go to Dashboard Go to Dashboard
</a> </a>
+1 -1
View File
@@ -10,7 +10,7 @@ endblock %} {% block content %}
<p class="error-detail">Please try again in a few moments.</p> <p class="error-detail">Please try again in a few moments.</p>
</div> </div>
<div class="error-actions"> <div class="error-actions">
<a href="{{ url_for('dashboard') }}" class="btn btn-primary"> <a href="{{ url_for('dashboard.dashboard') }}" class="btn btn-primary">
<i class="fas fa-home"></i> <i class="fas fa-home"></i>
Go to Dashboard Go to Dashboard
</a> </a>
+11 -1
View File
@@ -34,6 +34,16 @@ except Exception as e:
gmaps_client = None gmaps_client = None
print(f"❌ Error initializing Google Maps client: {e}") 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 # Geocoding cache
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -811,4 +821,4 @@ def get_all_locations_from_qr_codes():
return [row[0] for row in result.fetchall()] return [row[0] for row in result.fetchall()]
except Exception as e: except Exception as e:
logger_handler.logger.error(f"Error loading locations: {e}") logger_handler.logger.error(f"Error loading locations: {e}")
return [] return []