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
)
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():
@@ -87,7 +87,7 @@ select:focus,input:focus{outline:none;border-color:#3b82f6;box-shadow:0 0 0 3px
</div>
<div class="footer">
<a href="/v3">Open Full Interface</a>
<a href="/">Open Full Interface</a>
</div>
<script>