Serve the panel from a sub-path, and an Apache conf for /heos

The app assumed it lived at the server root: its links were /static/...
and its fetches /api/..., which behind a proxy at /heos resolve to the
wrong place, so the page would load and nothing on it would work.

It now takes the prefix from X-Forwarded-Prefix, and everything it
generates -- stylesheet, icons, the manifest's start_url, every fetch --
follows. Nothing changes when it is served from its own port.

deploy/heos.conf is the Apache side, restricted to the local network by
default, since this controls the speakers and the vhost it hangs off has
a public certificate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-14 21:57:16 +02:00
co-authored by Claude Opus 5
parent 6213bcf23f
commit 6d132f53ff
6 changed files with 79 additions and 7 deletions
+12 -2
View File
@@ -13,6 +13,7 @@ import argparse
from functools import wraps
from flask import Flask, jsonify, render_template, request
from werkzeug.middleware.proxy_fix import ProxyFix
import config
from avr import AvrError
@@ -20,6 +21,13 @@ from controller import Controller, TargetError
from heos import HeosError
app = Flask(__name__)
# Served straight from port 5005 this changes nothing. Behind a reverse
# proxy that mounts us on a sub-path (Apache at /heos, say) it reads the
# X-Forwarded-Prefix that proxy sets, so every URL the app generates is
# /heos/... instead of /..., and the page works either way.
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1)
controller: Controller = None
@@ -320,6 +328,9 @@ def main():
global controller
parser = argparse.ArgumentParser(description="HEOS panel")
parser.add_argument("--port", type=int, default=config.WEB_PORT)
parser.add_argument("--host", default="0.0.0.0",
help="0.0.0.0 so your phone can reach it directly; "
"127.0.0.1 to allow only a local reverse proxy")
parser.add_argument("--demo", action="store_true",
help="run with fake speakers, for working on the UI away from the kit")
args = parser.parse_args()
@@ -330,8 +341,7 @@ def main():
else:
controller = Controller(config)
# 0.0.0.0 so your phone can reach it over the LAN.
app.run(host="0.0.0.0", port=args.port, threaded=True)
app.run(host=args.host, port=args.port, threaded=True)
if __name__ == "__main__":