Experimental dynamic manifest

This commit is contained in:
Jack Kingsman
2026-02-20 22:49:39 -08:00
parent 2321411ef0
commit c90a30787a
3 changed files with 86 additions and 23 deletions

View File

@@ -1,13 +1,27 @@
import logging
from pathlib import Path
from fastapi import FastAPI, HTTPException
from fastapi.responses import FileResponse
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import FileResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
logger = logging.getLogger(__name__)
def _resolve_request_origin(request: Request) -> str:
"""Resolve the external origin, honoring common reverse-proxy headers."""
forwarded_proto = request.headers.get("x-forwarded-proto")
forwarded_host = request.headers.get("x-forwarded-host")
if forwarded_proto and forwarded_host:
proto = forwarded_proto.split(",")[0].strip()
host = forwarded_host.split(",")[0].strip()
if proto and host:
return f"{proto}://{host}"
return str(request.base_url).rstrip("/")
def register_frontend_static_routes(app: FastAPI, frontend_dir: Path) -> bool:
"""Register frontend static file routes if a built frontend is available.
@@ -55,6 +69,41 @@ def register_frontend_static_routes(app: FastAPI, frontend_dir: Path) -> bool:
"""Serve the frontend index.html."""
return FileResponse(index_file)
@app.get("/site.webmanifest")
async def serve_webmanifest(request: Request):
"""Serve a dynamic web manifest using the active request origin."""
origin = _resolve_request_origin(request)
manifest = {
"name": "RemoteTerm for MeshCore",
"short_name": "RemoteTerm",
"id": f"{origin}/",
"start_url": f"{origin}/",
"scope": f"{origin}/",
"display": "standalone",
"display_override": ["window-controls-overlay", "standalone", "fullscreen"],
"theme_color": "#111419",
"background_color": "#111419",
"icons": [
{
"src": f"{origin}/web-app-manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable",
},
{
"src": f"{origin}/web-app-manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable",
},
],
}
return JSONResponse(
manifest,
media_type="application/manifest+json",
headers={"Cache-Control": "no-store"},
)
@app.get("/{path:path}")
async def serve_frontend(path: str):
"""Serve frontend files, falling back to index.html for SPA routing."""

View File

@@ -1,21 +0,0 @@
{
"name": "RemoteTerm for MeshCore",
"short_name": "RemoteTerm",
"icons": [
{
"src": "/web-app-manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/web-app-manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

View File

@@ -53,6 +53,16 @@ def test_valid_dist_serves_static_and_spa_fallback(tmp_path):
assert root_response.status_code == 200
assert "index page" in root_response.text
manifest_response = client.get("/site.webmanifest")
assert manifest_response.status_code == 200
assert manifest_response.headers["content-type"].startswith("application/manifest+json")
manifest = manifest_response.json()
assert manifest["start_url"] == "http://testserver/"
assert manifest["scope"] == "http://testserver/"
assert manifest["id"] == "http://testserver/"
assert manifest["display"] == "standalone"
assert manifest["icons"][0]["src"] == "http://testserver/web-app-manifest-192x192.png"
file_response = client.get("/robots.txt")
assert file_response.status_code == 200
assert file_response.text == "User-agent: *"
@@ -64,3 +74,28 @@ def test_valid_dist_serves_static_and_spa_fallback(tmp_path):
asset_response = client.get("/assets/app.js")
assert asset_response.status_code == 200
assert "console.log('ok');" in asset_response.text
def test_webmanifest_uses_forwarded_origin_headers(tmp_path):
app = FastAPI()
dist_dir = tmp_path / "frontend" / "dist"
dist_dir.mkdir(parents=True)
(dist_dir / "index.html").write_text("<html><body>index page</body></html>")
registered = register_frontend_static_routes(app, dist_dir)
assert registered is True
with TestClient(app) as client:
response = client.get(
"/site.webmanifest",
headers={
"x-forwarded-proto": "https",
"x-forwarded-host": "mesh.example.com:8443",
},
)
assert response.status_code == 200
data = response.json()
assert data["start_url"] == "https://mesh.example.com:8443/"
assert data["scope"] == "https://mesh.example.com:8443/"
assert data["id"] == "https://mesh.example.com:8443/"