|
| 1 | +# Design: Split frontend_multi_user/src/app.py into Blueprint modules |
| 2 | + |
| 3 | +**Date:** 2026-03-31 |
| 4 | +**Status:** Proposed |
| 5 | +**Implements:** Proposal 131, Phase 2, Step 1 |
| 6 | + |
| 7 | +## Goal |
| 8 | + |
| 9 | +Split the 3,857-line monolithic `MyFlaskApp` class in `frontend_multi_user/src/app.py` into focused Flask Blueprint modules. Preserve all behavior, routes, and the AGENTS.md constraints. |
| 10 | + |
| 11 | +## Module layout |
| 12 | + |
| 13 | +``` |
| 14 | +frontend_multi_user/src/ |
| 15 | + app.py # Assembly: Flask app creation, config, db init, schema migrations, middleware, blueprints |
| 16 | + auth.py # Blueprint "auth": OAuth, login/logout, session |
| 17 | + billing.py # Blueprint "billing": Stripe & Telegram payments, credit helpers |
| 18 | + admin_routes.py # Blueprint "admin_routes": admin panel, database utils, reconciliation, demo_run |
| 19 | + plan_routes.py # Blueprint "plan_routes": /run, /plan/*, progress, telemetry, stop/retry/resume |
| 20 | + downloads.py # Blueprint "downloads": /plan/download/*, /admin/task/<id>/* file serving |
| 21 | + utils.py # Pure helpers shared across blueprints |
| 22 | +``` |
| 23 | + |
| 24 | +## Per-module contents |
| 25 | + |
| 26 | +### app.py (assembly, ~700 lines target) |
| 27 | + |
| 28 | +Keeps: |
| 29 | +- All imports, module-level constants (`RUN_DIR`, `SHOW_DEMO_PLAN`, `CREDIT_SCALE`, `DEMO_FORM_RUN_PROMPT_UUIDS`, `AUTH_PROVIDER_LABELS`) |
| 30 | +- `MyFlaskApp` class with `__init__` (Flask creation, config, dotenv, db init, schema migrations, Flask-Admin registration, OAuth setup) |
| 31 | +- Middleware: `_auto_login_open_access`, `_admin_full_width`, `inject_current_user_name` |
| 32 | +- App-level routes: `/` (dashboard), `/models`, `/healthcheck`, `/llms.txt`, `/llm.txt`, `/ping` |
| 33 | +- Startup helpers: `_track_flask_app_started`, `_start_check`, `_fetch_worker_plan_llm_info`, `_looks_like_production_url`, `_register_oauth_providers`, `_determine_open_access` |
| 34 | +- Schema migration helpers (`_ensure_*`, `_create_tables_with_retry`, `_seed_initial_records`) |
| 35 | +- Flask-Admin setup and `MyAdminIndexView` |
| 36 | +- `User` class (Flask-Login), `login_manager.user_loader` |
| 37 | +- `_profile_model_rows_map()`, `_model_profile_options()` (used by dashboard and models route) |
| 38 | +- Blueprint registration: imports and registers all blueprints |
| 39 | +- `__main__` block |
| 40 | +- `nocache` decorator, `admin_required` decorator |
| 41 | +- `_new_model`, `build_postgres_uri_from_env` |
| 42 | + |
| 43 | +Stashes into `app.config` during init: |
| 44 | +- `PLANEXE_RUN_DIR` (path) |
| 45 | +- `WORKER_PLAN_URL` (string) |
| 46 | +- `PLANEXE_PROJECT_ROOT` (path) |
| 47 | +- `PATH_TO_PYTHON` (path) |
| 48 | +- `PROMPT_CATALOG` (PromptCatalog instance) |
| 49 | +- `PLANEXE_CONFIG` (PlanExeConfig instance) |
| 50 | +- `PLANEXE_DOTENV` (PlanExeDotEnv instance) |
| 51 | +- `OPEN_ACCESS` (bool) |
| 52 | +- `API_KEY_SHOW_ONCE` (bool) |
| 53 | +- `PLAN_TELEMETRY_CACHE` (dict reference) |
| 54 | + |
| 55 | +### utils.py (~120 lines) |
| 56 | + |
| 57 | +Pure functions with no Flask or db dependency: |
| 58 | +- `_safe_float(value)` |
| 59 | +- `_safe_int(value)` |
| 60 | +- `_clean_text(value)` |
| 61 | +- `_extract_exception_type(message)` |
| 62 | +- `_extract_nested_value(payload, key_names)` |
| 63 | +- `_extract_provider_model_from_activity_key(model_key)` |
| 64 | +- `_to_credit_decimal(value)` (uses CREDIT_SCALE constant, passed or imported) |
| 65 | +- `_format_credit_display(value)` |
| 66 | +- `_format_relative_time(value)` |
| 67 | +- `_normalize_plan_view_mode(value)` |
| 68 | +- `_coerce_json_dict(value)` |
| 69 | + |
| 70 | +Also exports `CREDIT_SCALE` constant. |
| 71 | + |
| 72 | +### auth.py (~200 lines) |
| 73 | + |
| 74 | +Blueprint name: `auth`, no url_prefix. |
| 75 | + |
| 76 | +Routes: |
| 77 | +- `/login` (GET, POST) |
| 78 | +- `/api/oauth-redirect-uri` (GET) |
| 79 | +- `/login/<provider>` (GET) |
| 80 | +- `/auth/<provider>/callback` (GET) |
| 81 | +- `/logout` (GET) |
| 82 | + |
| 83 | +Helpers (moved from MyFlaskApp): |
| 84 | +- `_oauth_redirect_url(provider)` — reads `current_app.config['PUBLIC_BASE_URL']` |
| 85 | +- `_auth_provider_label(provider)` |
| 86 | +- `_get_user_from_provider(provider, token)` |
| 87 | +- `_avatar_url_from_profile(provider, profile)` |
| 88 | +- `_upsert_user_from_oauth(provider, profile)` |
| 89 | +- `_update_user_from_profile(user, provider, profile)` |
| 90 | +- `_get_or_create_api_key(user, name)` |
| 91 | + |
| 92 | +Accesses: `current_app.config`, `current_app.extensions['authlib.integrations.flask_client']` (OAuth), `database_api` db singleton, Flask-Login `login_user`/`logout_user`. |
| 93 | + |
| 94 | +### billing.py (~250 lines) |
| 95 | + |
| 96 | +Blueprint name: `billing`, url_prefix `/billing`. |
| 97 | + |
| 98 | +Routes: |
| 99 | +- `/stripe/checkout` (POST) |
| 100 | +- `/stripe/webhook` (POST) |
| 101 | +- `/telegram/invoice` (POST) |
| 102 | +- `/telegram/webhook` (POST) |
| 103 | + |
| 104 | +Helpers: |
| 105 | +- `_apply_credit_delta(user, delta, reason, source, external_id)` |
| 106 | +- `_apply_payment_credits(user_id, provider, provider_payment_id, credits, amount, currency, raw_payload)` |
| 107 | +- `_record_event(event_type, message, context)` |
| 108 | +- `_finalize_stripe_checkout_session(user, checkout_session_id)` |
| 109 | + |
| 110 | +Accesses: `current_app.config`, `stripe` library, db singleton, CreditHistory/PaymentRecord/EventItem models. |
| 111 | + |
| 112 | +### admin_routes.py (~300 lines) |
| 113 | + |
| 114 | +Blueprint name: `admin_routes`, no url_prefix. |
| 115 | + |
| 116 | +Routes: |
| 117 | +- `/admin/reconciliation` (GET) |
| 118 | +- `/admin/database` (GET, POST) |
| 119 | +- `/admin/database/backup` (GET) |
| 120 | +- `/ping/stream` (GET) |
| 121 | +- `/demo_run` (GET) |
| 122 | + |
| 123 | +Helpers: |
| 124 | +- `_get_database_size_info()` |
| 125 | +- `_get_purge_activity_info()` |
| 126 | +- `_purge_activity_data(keep_n)` |
| 127 | +- `_vacuum_task_item()` |
| 128 | +- `_proxy_backup_response()` |
| 129 | +- `_build_reconciliation_report(max_tasks, tolerance_usd)` |
| 130 | + |
| 131 | +Uses `admin_required` decorator imported from `app.py`. |
| 132 | + |
| 133 | +### plan_routes.py (~800 lines) |
| 134 | + |
| 135 | +Blueprint name: `plan_routes`, no url_prefix. |
| 136 | + |
| 137 | +Routes: |
| 138 | +- `/run` (GET, POST) |
| 139 | +- `/create_plan` (POST) |
| 140 | +- `/run_status` (GET) |
| 141 | +- `/progress` (GET) |
| 142 | +- `/viewplan` (GET) |
| 143 | +- `/plan` (GET) |
| 144 | +- `/plan/stop` (POST) |
| 145 | +- `/plan/retry` (POST) |
| 146 | +- `/plan/resume` (POST) |
| 147 | +- `/plan/meta` (GET) |
| 148 | +- `/plan/view-mode` (POST) |
| 149 | +- `/plan/telemetry` (GET) |
| 150 | + |
| 151 | +Helpers: |
| 152 | +- `_get_current_user_account()` — shared with account, but primary user is plan_routes; import from here or utils |
| 153 | +- `_get_plan_view_mode_preference()`, `_set_plan_view_mode_preference(mode)` |
| 154 | +- `_admin_user_ids()` — used by plan list filtering; shared with admin |
| 155 | +- `_load_prompt_preview_safe(task_id, max_chars)` |
| 156 | +- `_build_plan_failure_trace(task)` |
| 157 | +- `_build_plan_telemetry_cache_key(task, include_raw)` |
| 158 | +- `_build_plan_telemetry(task, include_raw, expose_raw_usage_data)` |
| 159 | +- `_read_activity_overview_from_task(task)` |
| 160 | +- `_read_inference_cost_from_task(task)` |
| 161 | +- `_find_latest_task_event(task_id, event_type, max_events_to_scan)` |
| 162 | +- `_read_activity_overview_from_run_zip(run_zip_snapshot)` |
| 163 | +- `_read_inference_cost_from_run_zip(run_zip_snapshot)` |
| 164 | + |
| 165 | +`_get_current_user_account()` and `_admin_user_ids()` are also needed by `app.py` (dashboard) and `account.py`. These go in `utils.py` or a small `user_helpers.py` — but to keep module count low, put them in `plan_routes.py` and import from there where needed. |
| 166 | + |
| 167 | +### downloads.py (~150 lines) |
| 168 | + |
| 169 | +Blueprint name: `downloads`, no url_prefix. |
| 170 | + |
| 171 | +Routes: |
| 172 | +- `/plan/download/report` (GET) |
| 173 | +- `/plan/download/zip` (GET) |
| 174 | +- `/admin/task/<uuid:task_id>/report` (GET) |
| 175 | +- `/admin/task/<uuid:task_id>/run_zip` (GET) |
| 176 | +- `/admin/task/<uuid:task_id>/track_activity` (GET) |
| 177 | + |
| 178 | +Helpers: |
| 179 | +- `_sanitize_legacy_run_zip_for_download(run_zip_snapshot)` |
| 180 | + |
| 181 | +Uses `admin_required` from `app.py`, `nocache` from `app.py`. |
| 182 | + |
| 183 | +## Shared state strategy |
| 184 | + |
| 185 | +The `MyFlaskApp.__init__` stashes all instance state that blueprints need into `app.config`: |
| 186 | + |
| 187 | +```python |
| 188 | +self.app.config['WORKER_PLAN_URL'] = self.worker_plan_url |
| 189 | +self.app.config['PLANEXE_RUN_DIR'] = self.planexe_run_dir |
| 190 | +# etc. |
| 191 | +``` |
| 192 | + |
| 193 | +Blueprint code accesses via `current_app.config['WORKER_PLAN_URL']`. |
| 194 | + |
| 195 | +The telemetry cache dict is stashed the same way: `self.app.config['PLAN_TELEMETRY_CACHE'] = self._plan_telemetry_cache`. |
| 196 | + |
| 197 | +## Blueprint registration |
| 198 | + |
| 199 | +In `MyFlaskApp.__init__`, after all setup: |
| 200 | + |
| 201 | +```python |
| 202 | +from frontend_multi_user.src.auth import auth_bp |
| 203 | +from frontend_multi_user.src.billing import billing_bp |
| 204 | +from frontend_multi_user.src.admin_routes import admin_routes_bp |
| 205 | +from frontend_multi_user.src.plan_routes import plan_routes_bp |
| 206 | +from frontend_multi_user.src.downloads import downloads_bp |
| 207 | + |
| 208 | +self.app.register_blueprint(auth_bp) |
| 209 | +self.app.register_blueprint(billing_bp) |
| 210 | +self.app.register_blueprint(admin_routes_bp) |
| 211 | +self.app.register_blueprint(plan_routes_bp) |
| 212 | +self.app.register_blueprint(downloads_bp) |
| 213 | +``` |
| 214 | + |
| 215 | +## Constraints preserved |
| 216 | + |
| 217 | +- Single `db` singleton from `database_api.planexe_db_singleton` (AGENTS.md rule) |
| 218 | +- No imports from `worker_plan_internal`, `worker_plan.app`, or `frontend_single_user` |
| 219 | +- Admin identity via Flask-Login username string + deterministic UUID |
| 220 | +- Schema migration helpers stay in `app.py` (run once at startup) |
| 221 | +- Flask-Admin registration stays in `app.py` |
| 222 | +- No new dependencies |
| 223 | + |
| 224 | +## Testing strategy |
| 225 | + |
| 226 | +- No automated tests exist currently for this frontend |
| 227 | +- Verification: start the app, confirm all routes respond (manual smoke test) |
| 228 | +- Run `python test.py` from repo root to confirm no regressions in other packages |
| 229 | + |
| 230 | +## Success criteria |
| 231 | + |
| 232 | +- `app.py` reduced from ~3,857 lines to ~700 lines |
| 233 | +- Each new module under ~800 lines |
| 234 | +- All existing routes return identical responses |
| 235 | +- No new files beyond the 6 listed above |
0 commit comments