04/08 Refactored codes, fixed issues
This commit is contained in:
@@ -208,3 +208,4 @@ marimo/_static/
|
|||||||
marimo/_lsp/
|
marimo/_lsp/
|
||||||
__marimo__/
|
__marimo__/
|
||||||
README.md
|
README.md
|
||||||
|
Claude.md
|
||||||
@@ -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,38 +338,11 @@ 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")
|
||||||
|
|||||||
@@ -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
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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']
|
||||||
|
|||||||
@@ -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,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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user