Skip to content

Commit 62c9e82

Browse files
authored
Merge pull request #476 from PlanExeOrg/split-frontend-multi-user-app
Split frontend_multi_user/src/app.py into Blueprint modules
2 parents c82fb84 + 42f31d5 commit 62c9e82

15 files changed

Lines changed: 2888 additions & 2523 deletions

docs/proposals/131-codebase-cleanliness-remediation-roadmap.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ Large modules make the code harder to reason about, harder to test in isolation,
6868

6969
### Fix steps
7070

71-
1. Split `frontend_multi_user/src/app.py` by concern into `auth`, `billing`, `admin`, `downloads`, `account`, and `plan_routes`.
71+
1. ~~Split `frontend_multi_user/src/app.py` by concern into `auth`, `billing`, `admin`, `downloads`, `account`, and `plan_routes`.~~ **Done** (PR #476): Split 3,857-line monolith into 6 Flask Blueprint modules + utils (app.py reduced to 1,441 lines). Follow-up fix: updated all `url_for()` calls in templates to use blueprint-prefixed endpoint names (`plan_routes.*`, `auth.*`, `downloads.*`).
7272
2. Split `mcp_cloud/http_server.py` into `middleware`, `route_registration`, `tool_http_bridge`, and `server_boot`.
7373
3. Convert `worker_plan/worker_plan_internal/plan/run_plan_pipeline.py` from a giant task registry file into a thin pipeline assembly module plus task-specific modules grouped by stage.
7474
4. Extract reusable orchestration helpers from `worker_plan_database/app.py` into focused worker, billing, and queue modules.
@@ -181,7 +181,7 @@ The multi-user frontend handles auth, admin flows, billing, downloads, and user
181181

182182
### Phase 2: Split the Worst Offenders
183183

184-
1. Refactor `frontend_multi_user/src/app.py` first because it mixes the most distinct business concerns.
184+
1. ~~Refactor `frontend_multi_user/src/app.py` first because it mixes the most distinct business concerns.~~ **Done** (PR #476). Template `url_for()` references fixed to match new blueprint endpoints.
185185
2. Refactor `mcp_cloud/http_server.py` second because it sits on a public protocol boundary.
186186
3. Refactor `worker_plan_database/app.py` and `run_plan_pipeline.py` in smaller slices to avoid destabilizing the execution engine.
187187

@@ -209,7 +209,7 @@ This proposal integrates with existing PlanExe boundaries rather than fighting t
209209
## Success Metrics
210210

211211
1. No production-facing Python module above 1,500 lines after the first cleanup wave.
212-
2. `frontend_multi_user/src/app.py` reduced by at least 50% through route and helper extraction.
212+
2. ~~`frontend_multi_user/src/app.py` reduced by at least 50% through route and helper extraction.~~ **Done** (PR #476): Reduced by 63% (3,857 → 1,441 lines).
213213
3. `mcp_cloud/http_server.py` reduced to a focused HTTP assembly module rather than a mixed implementation file.
214214
4. Zero uncategorized top-level `print()` statements in production service modules.
215215
5. Documented justification for all remaining `except Exception:` boundaries in service code.
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
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

Comments
 (0)