"""Production entrypoint for the Flask backend (API + admin + webapp static). Dev: python bober_bbq/app.py (Flask debug server, auto-reload) Prod: python run_web.py (waitress, no reload, multi-threaded) """ import logging import os from apscheduler.schedulers.background import BackgroundScheduler from waitress import serve from bober_bbq.app import create_app from bober_bbq.config import config from bober_bbq.extensions import db from bober_bbq.migrate import migrate from bober_bbq.models import Setting from bober_bbq.seed import seed from bober_bbq.utils import error_alerts from bober_bbq.utils.backup_remote import run_scheduled_backup from bober_bbq.utils.checkbox_prro import maybe_close_shift from bober_bbq.utils.reconciliation import ( check_liqpay_token_health, check_monobank_token_health, reconcile_pending_payments, ) logger = logging.getLogger(__name__) app = create_app() with app.app_context(): db.create_all() migrate() seed() def _warn_on_insecure_defaults() -> None: # config.py silently falls back to these if .env is missing a line or a # client's provisioning skipped provision_client.sh (which generates a # real SECRET_KEY and asks for a real admin password). Left unnoticed, # SECRET_KEY="dev-secret-change-me" lets anyone forge session cookies, # and admin/admin is guessable in one try. Don't refuse to start (that # could brick a legitimate first boot before the owner sets a real # password) — just make it impossible to miss. problems = [] if config.SECRET_KEY == "dev-secret-change-me": problems.append("SECRET_KEY все ще стандартний (dev-secret-change-me)") if config.FLASK_ADMIN_PASSWORD == "admin": problems.append("пароль адміна все ще стандартний (admin/admin)") if not problems: return message = "; ".join(problems) logger.error("INSECURE DEFAULTS IN PRODUCTION: %s — виправте .env і перезапустіть сервіс", message) error_alerts.notify( "перевірка налаштувань безпеки при старті", RuntimeError(f"Використовуються стандартні небезпечні значення: {message}"), ) _warn_on_insecure_defaults() def _run_in_app_context(fn, job_name): # APScheduler's default executor only logs a job's exception itself — # nothing here previously caught one, so a crashing job (like # reconcile_payments hitting something other than MonobankError) failed # silently from the owner's perspective, visible only in server logs. with app.app_context(): try: fn() except Exception as e: logger.exception("Scheduled job %s crashed", job_name) error_alerts.notify(f"фонове завдання {job_name}", e) if __name__ == "__main__": scheduler = BackgroundScheduler(timezone=config.TIMEZONE) scheduler.add_job( lambda: _run_in_app_context(reconcile_pending_payments, "reconcile_payments"), "interval", minutes=10, id="reconcile_payments", ) scheduler.add_job( lambda: _run_in_app_context(check_monobank_token_health, "monobank_token_health"), "interval", hours=1, id="monobank_token_health", ) scheduler.add_job( lambda: _run_in_app_context(check_liqpay_token_health, "liqpay_token_health"), "interval", hours=1, id="liqpay_token_health", ) scheduler.add_job( lambda: _run_in_app_context(run_scheduled_backup, "backup_remote"), "interval", hours=1, id="backup_remote", ) scheduler.add_job( lambda: _run_in_app_context(maybe_close_shift, "checkbox_auto_close_shift"), "interval", minutes=10, id="checkbox_auto_close_shift", ) scheduler.start() port = int(os.getenv("PORT", "8000")) with app.app_context(): cafe_name = Setting.get("cafe_name", "Bober BBQ") print(f"{cafe_name} backend listening on http://0.0.0.0:{port}") serve(app, host="0.0.0.0", port=port)