From 2c5627354e429dff327e5a0acc0e52b700b12951 Mon Sep 17 00:00:00 2001 From: NguyenND Date: Wed, 16 Sep 2026 13:38:10 -0400 Subject: [PATCH] Sep 16 - Optimize code, part 1, fix Internal Server error --- Claude.md | 14 ++++++++++++ extensions.py | 59 ++++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 65 insertions(+), 8 deletions(-) diff --git a/Claude.md b/Claude.md index 6456df6..78a612a 100644 --- a/Claude.md +++ b/Claude.md @@ -858,6 +858,14 @@ logger_handler.logger.info("...") logger_handler.logger.error("...", exc_info=True) # always pass exc_info=True in except blocks ``` +**`logger_handler` is a proxy object, not the `AppLogger`** (`extensions.py`, Set 24). Modules +imported before `create_app()` runs `init_logger()` — `utils/helpers.py`, `utils/template_helpers.py`, +`utils/geocoding.py` — copy this reference at import time. It used to be a plain `None`, so any +logging call from those modules raised `AttributeError: 'NoneType' object has no attribute 'logger'`. +The proxy forwards to the real `AppLogger` once `init_logger()` builds it, and to a stdlib logger +before that. **Do not restore `logger_handler = None`**, and do not test it for `None`/truthiness — +it is always truthy. + ### Log Destinations - `logs/application.log` — rotating 10MB/5 backups - `logs/errors.log` — rotating 5MB/10 backups @@ -1319,6 +1327,12 @@ it (the workers share no pub/sub). | — | Verified offline (67 checks): formula guard incl. openpyxl save/reload, role gates per role and for JSON callers, limiter behind ProxyFix (spoofed XFF, shared IP, per-username cap, reset on login), debug endpoint, check-in ID rule, config limits, POST-only routes, migration against a fake cursor (create / covered / re-run / fallback / missing column). Not run against MySQL, Nginx or a browser | | — | Known, not changed: `templates/confirm_delete_qr.html` links (GET) to the POST-only `deactivate_qr_code`, so that button returns 405 | +### Set 24 — Access-Denied Path Raised 500 (logger_handler was None) (Sept 16, 2026) +| File | Fix | +|---|---| +| `extensions.py` | `logger_handler` is now a proxy (see §14). `utils/helpers.py` imports it at module import time, which happens BEFORE `init_logger()`, so it was permanently `None`: the Set 23 role check logged a warning when denying access and raised `AttributeError: 'NoneType' object has no attribute 'logger'` → 500 for a project manager opening `/time-attendance` or editing a QR code. The same latent bug affected `generate_qr_code()` logging in `utils/helpers.py` and every call in `utils/template_helpers.py` / `utils/geocoding.py` | +| — | The step-1 tests had stubbed a working logger into `extensions`, which hid it. They now import the real module and run the role checks BEFORE `init_logger()`, exactly like a gunicorn worker | + --- ## 21. Infrastructure & Deployment diff --git a/extensions.py b/extensions.py index 5ac542d..ea8a490 100644 --- a/extensions.py +++ b/extensions.py @@ -23,17 +23,60 @@ from logger_handler import AppLogger db = SQLAlchemy() # --------------------------------------------------------------------------- -# Application-level logger — initialized via init_logger() below +# Application-level logger # --------------------------------------------------------------------------- -logger_handler: "AppLogger | None" = None +# `logger_handler` is a PROXY, not the AppLogger itself. +# +# Modules imported before create_app() calls init_logger() — utils/helpers.py, +# utils/template_helpers.py, utils/geocoding.py — bind whatever object this name +# holds at import time, and `from extensions import logger_handler` copies the +# reference. While that was a plain None, any later call such as +# `logger_handler.logger.warning(...)` raised +# AttributeError: 'NoneType' object has no attribute 'logger' +# (Sept 16 2026: a blocked project manager got a 500 instead of a redirect, +# because the access-denied path logs a warning.) +# +# The proxy forwards to the real AppLogger once init_logger() has built it, and +# to a plain stdlib logger before that, so logging can never break a request. +import logging as _logging + +_logger_instance = None # the real AppLogger, set by init_logger() + + +class _FallbackLogger: + """Stand-in used before init_logger() has run: logs, never raises.""" + + def __init__(self): + self.logger = _logging.getLogger('qr_attendance_app') + + def __getattr__(self, name): + def _noop(*args, **kwargs): + self.logger.debug(f"{name}() called before init_logger()") + return _noop + + +_fallback_logger = _FallbackLogger() + + +class _LoggerHandlerProxy: + """Forwards every attribute to the real AppLogger, or to _FallbackLogger.""" + + def __getattr__(self, name): + return getattr(_logger_instance or _fallback_logger, name) + + def __repr__(self): + return f" {_logger_instance or _fallback_logger!r}>" + + +logger_handler = _LoggerHandlerProxy() def init_logger(app, database) -> AppLogger: """ - Instantiate AppLogger and bind it to the module-level ``logger_handler`` - variable so every Blueprint that does ``from extensions import logger_handler`` - receives the same fully-initialized instance. + Instantiate AppLogger and bind it behind the module-level ``logger_handler`` + proxy, so every module that did ``from extensions import logger_handler`` — + whenever it was imported — ends up using this fully-initialized instance. """ - global logger_handler - logger_handler = AppLogger(app, database) - return logger_handler + global _logger_instance + _logger_instance = AppLogger(app, database) + return _logger_instance