From c548cfeabe1cc400dd7c360ea5dcc6b0eaf2077a Mon Sep 17 00:00:00 2001 From: ChuckBuilds Date: Thu, 16 Jul 2026 10:43:40 -0400 Subject: [PATCH] =?UTF-8?q?feat(web):=20serve=20the=20interface=20at=20/?= =?UTF-8?q?=20=E2=80=94=20/v3=20kept=20as=20a=20legacy=20alias?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The user-visible URL no longer carries the interface version: the pages blueprint is now registered un-prefixed (primary) AND at /v3 (second registration, name='pages_v3_legacy'), so: - http:/// serves the interface directly (the old @app.route('/') redirect is removed — the blueprint's own index takes its place) - every existing /v3/... bookmark and all the hardcoded /v3/partials/... fetches in templates/JS keep working verbatim through the alias mount — zero template/JS churn, zero broken links - url_for('pages_v3.*') resolves against the primary registration, so all server-side redirects (captive portal detection endpoints) now emit un-prefixed URLs - the AP-mode captive-portal allowlist learned the un-prefixed page paths (/setup, /partials/, /settings/, /plugin-ui/) so setup-mode requests don't redirect-loop - /api/v3 and the templates/v3, static/v3 directories are deliberately untouched (internal, invisible to users; owner-confirmed scope) Validation: dual registration mechanics tested against real Flask (test client): /, /v3, /v3/ redirect, partials and /setup reachable on both mounts, url_for yields un-prefixed paths; py_compile passes. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ --- web_interface/app.py | 20 ++++++++++++------- web_interface/templates/v3/captive_setup.html | 2 +- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/web_interface/app.py b/web_interface/app.py index 5e857cd5..d3121856 100644 --- a/web_interface/app.py +++ b/web_interface/app.py @@ -176,7 +176,12 @@ except Exception as _hm_err: # pragma: no cover - defensive startup guard "Could not enable plugin health/metrics for web UI: %s", _hm_err ) -app.register_blueprint(pages_v3, url_prefix='/v3') +# Pages are served un-prefixed (the interface lives at /); the /v3 mount is a +# legacy alias kept so existing bookmarks and the hardcoded /v3/partials/... +# fetches in templates/JS keep working unchanged. url_for('pages_v3.*') +# resolves against the primary (un-prefixed) registration. +app.register_blueprint(pages_v3, url_prefix='') +app.register_blueprint(pages_v3, url_prefix='/v3', name='pages_v3_legacy') app.register_blueprint(api_v3, url_prefix='/api/v3') # Route to serve plugin asset files (registered on main app, not blueprint, for /assets/... path) @@ -407,7 +412,11 @@ def captive_portal_redirect(): # List of paths that should NOT be redirected (allow normal operation) allowed_paths = [ - '/v3', # Main interface and all sub-paths (includes /v3/setup) + '/v3', # Legacy-prefixed interface and all sub-paths + '/setup', # Captive setup page itself (un-prefixed mount) + '/partials/', # HTMX partials (un-prefixed mount) + '/settings/', # Settings search index (un-prefixed mount) + '/plugin-ui/', # Plugin-provided web UI assets (un-prefixed mount) '/api/v3/', # All API endpoints '/static/', # Static files (CSS, JS, images) '/hotspot-detect.html', # iOS/macOS detection @@ -799,11 +808,8 @@ if limiter: limiter.limit("200 per minute")(stream_display) limiter.limit("200 per minute")(stream_logs) -# Main route - redirect to v3 interface as default -@app.route('/') -def index(): - """Redirect to v3 interface""" - return redirect(url_for('pages_v3.index')) +# The pages blueprint's index now serves '/' directly (see the un-prefixed +# blueprint registration above), so no redirect route is needed here. @app.route('/favicon.ico') def favicon(): diff --git a/web_interface/templates/v3/captive_setup.html b/web_interface/templates/v3/captive_setup.html index 5490d845..bce4cd5e 100644 --- a/web_interface/templates/v3/captive_setup.html +++ b/web_interface/templates/v3/captive_setup.html @@ -87,7 +87,7 @@ select:focus,input:focus{outline:none;border-color:#3b82f6;box-shadow:0 0 0 3px