bober-bbq-bot/run_web.py
byrsapty 375022e739 Add backup_remote.py and wire up the rest of the rclone rework
Continuation of the previous commit (gdrive_backup.py removal landed
separately by accident) — adds the new module itself, the admin
routes/template using it, generalized Setting keys, updated docs/
provisioning notes, and tests.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-05 22:57:59 +03:00

115 lines
4.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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)