feat(web): serve the interface at / — /v3 kept as a legacy alias

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://<device>/ 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ
This commit is contained in:
ChuckBuilds
2026-07-16 10:43:40 -04:00
co-authored by Claude Sonnet 5
parent d49cb5851d
commit c548cfeabe
2 changed files with 14 additions and 8 deletions
+13 -7
View File
@@ -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 "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') app.register_blueprint(api_v3, url_prefix='/api/v3')
# Route to serve plugin asset files (registered on main app, not blueprint, for /assets/... path) # 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) # List of paths that should NOT be redirected (allow normal operation)
allowed_paths = [ 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 '/api/v3/', # All API endpoints
'/static/', # Static files (CSS, JS, images) '/static/', # Static files (CSS, JS, images)
'/hotspot-detect.html', # iOS/macOS detection '/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_display)
limiter.limit("200 per minute")(stream_logs) limiter.limit("200 per minute")(stream_logs)
# Main route - redirect to v3 interface as default # The pages blueprint's index now serves '/' directly (see the un-prefixed
@app.route('/') # blueprint registration above), so no redirect route is needed here.
def index():
"""Redirect to v3 interface"""
return redirect(url_for('pages_v3.index'))
@app.route('/favicon.ico') @app.route('/favicon.ico')
def favicon(): def favicon():
@@ -87,7 +87,7 @@ select:focus,input:focus{outline:none;border-color:#3b82f6;box-shadow:0 0 0 3px
</div> </div>
<div class="footer"> <div class="footer">
<a href="/v3">Open Full Interface</a> <a href="/">Open Full Interface</a>
</div> </div>
<script> <script>