import logging import os from flask import Flask from app.config import config from app.extensions import db, login_manager, migrate, csrf from app.utils.formatters import format_currency, format_percent, format_large_number def _setup_logging(app): """ Configure the 'app' namespace logger to write to a rotating file AND stderr. Uses a pipe-delimited format so the log viewer can parse each field easily: 2026-06-01 12:00:00|INFO|app.services.teller_service|message text The file path comes from LOG_FILE_PATH config (defaults to logs/app.log next to the project root). The directory is created automatically. """ import sys from logging.handlers import RotatingFileHandler app_log = logging.getLogger('app') if app_log.handlers: return # already configured (avoids duplicate handlers on reload) app_log.setLevel(logging.INFO) app_log.propagate = False # don't double-emit through the root logger fmt = logging.Formatter( '%(asctime)s|%(levelname)s|%(name)s|%(message)s', datefmt='%Y-%m-%d %H:%M:%S', ) # --- rotating file handler (primary — always on) --- log_file = app.config.get('LOG_FILE_PATH', '') if log_file: try: os.makedirs(os.path.dirname(log_file), exist_ok=True) fh = RotatingFileHandler( log_file, maxBytes=app.config.get('LOG_MAX_BYTES', 10 * 1024 * 1024), backupCount=app.config.get('LOG_BACKUP_COUNT', 5), encoding='utf-8', ) fh.setLevel(logging.INFO) fh.setFormatter(fmt) app_log.addHandler(fh) except Exception as exc: # Can't open file (permissions, bad path) — fall through to stderr only print(f'[pfm] WARNING: could not open log file {log_file!r}: {exc}', file=sys.stderr) # --- stderr handler (secondary — also always on so Gunicorn captures it) --- sh = logging.StreamHandler(sys.stderr) sh.setLevel(logging.INFO) sh.setFormatter(fmt) app_log.addHandler(sh) app_log.info('Logging initialised — file=%s', log_file or '(none)') def create_app(config_name=None): if config_name is None: config_name = os.environ.get('FLASK_ENV', 'development') config_name = 'production' if config_name == 'production' else 'development' app = Flask(__name__) app.config.from_object(config[config_name]) # Wire app.* loggers into Gunicorn's error log (or stderr in dev). # basicConfig is a no-op under Gunicorn because Gunicorn already # installed root handlers before our app is imported — so we must # attach handlers explicitly. _setup_logging(app) db.init_app(app) login_manager.init_app(app) migrate.init_app(app, db) csrf.init_app(app) from app.routes.auth import auth_bp from app.routes.dashboard import dashboard_bp from app.routes.accounts import accounts_bp from app.routes.categories import categories_bp from app.routes.transactions import transactions_bp from app.routes.budgets import budgets_bp from app.routes.goals import goals_bp from app.routes.investments import investments_bp from app.routes.ai import ai_bp from app.routes.reports import reports_bp from app.routes.settings import settings_bp from app.routes.teller import teller_bp from app.routes.logs import logs_bp from app.routes.bank_import import bank_import_bp app.register_blueprint(auth_bp) app.register_blueprint(dashboard_bp) app.register_blueprint(accounts_bp) app.register_blueprint(categories_bp) app.register_blueprint(transactions_bp) app.register_blueprint(budgets_bp) app.register_blueprint(goals_bp) app.register_blueprint(investments_bp) app.register_blueprint(ai_bp) app.register_blueprint(reports_bp) app.register_blueprint(settings_bp) app.register_blueprint(teller_bp) app.register_blueprint(logs_bp) app.register_blueprint(bank_import_bp) with app.app_context(): from app.models import ( User, Account, Category, Receipt, RecurringRule, Transaction, Budget, Goal, GoalContribution, Investment, InvestmentTransaction, NetWorthSnapshot, AiInsight, FxRate ) from app.models.teller_enrollment import TellerEnrollment, TellerAccount app.jinja_env.globals['format_currency'] = format_currency app.jinja_env.globals['format_percent'] = format_percent app.jinja_env.globals['format_large_number'] = format_large_number @app.template_filter('currency') def currency_filter(value, symbol=None): return format_currency(value, symbol) @app.template_filter('percent') def percent_filter(value): return format_percent(value) @app.template_filter('shares') def shares_filter(value): """Format share quantity — strips trailing zeros, keeps up to 8 decimal places.""" try: f = float(value) except (TypeError, ValueError): return '0' # Format to 8 decimal places then strip trailing zeros s = '{:.8f}'.format(f).rstrip('0').rstrip('.') return s if s else '0' return app