DeltaZN commited on
Commit ·
6c64b04
1
Parent(s): 17f0444
fix(hf): serve lobby at root, ship built frontend, honor proxy headers
Browse files- Mount the Gradio lobby at / and remove the /connect redirect (the
https->http redirect downgrade behind HF's proxy caused a white screen).
- Add ProxyHeadersMiddleware so the app trusts X-Forwarded-Proto from HF's
TLS-terminating proxy instead of emitting http:// absolute redirects.
- Build the React SPA with base /world/ so its assets live under /world/assets
(served by the /world catch-all), freeing /assets for the lobby's own bundle.
- Commit dist/frontend so the Space (which does not run npm build) can serve
the /world observer; un-ignore dist/frontend accordingly.
- .gitignore +4 -1
- app.py +1 -1
- dist/frontend/assets/index-B3jtwnUR.js +0 -0
- dist/frontend/assets/index-Cbon7bZF.css +1 -0
- dist/frontend/index.html +13 -0
- src/world_simulator/api/gradio_app.py +15 -15
- vite.config.ts +3 -0
.gitignore
CHANGED
|
@@ -8,7 +8,10 @@ __pycache__/
|
|
| 8 |
htmlcov/
|
| 9 |
.venv/
|
| 10 |
node_modules/
|
| 11 |
-
dist/
|
|
|
|
|
|
|
|
|
|
| 12 |
build/
|
| 13 |
*.egg-info/
|
| 14 |
*.tsbuildinfo
|
|
|
|
| 8 |
htmlcov/
|
| 9 |
.venv/
|
| 10 |
node_modules/
|
| 11 |
+
dist/*
|
| 12 |
+
# The built frontend is committed so the Hugging Face Space (which does not run
|
| 13 |
+
# npm build) can serve it.
|
| 14 |
+
!dist/frontend/
|
| 15 |
build/
|
| 16 |
*.egg-info/
|
| 17 |
*.tsbuildinfo
|
app.py
CHANGED
|
@@ -11,7 +11,7 @@ from pathlib import Path
|
|
| 11 |
# installed in editable mode.
|
| 12 |
sys.path.insert(0, str(Path(__file__).parent / "src"))
|
| 13 |
|
| 14 |
-
from world_simulator.api.gradio_app import create_gradio_app
|
| 15 |
|
| 16 |
app = create_gradio_app(
|
| 17 |
config_path=Path(os.getenv("WORLD_SIMULATOR_CONFIG", "config/game.modal.local.json")),
|
|
|
|
| 11 |
# installed in editable mode.
|
| 12 |
sys.path.insert(0, str(Path(__file__).parent / "src"))
|
| 13 |
|
| 14 |
+
from world_simulator.api.gradio_app import create_gradio_app
|
| 15 |
|
| 16 |
app = create_gradio_app(
|
| 17 |
config_path=Path(os.getenv("WORLD_SIMULATOR_CONFIG", "config/game.modal.local.json")),
|
dist/frontend/assets/index-B3jtwnUR.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
dist/frontend/assets/index-Cbon7bZF.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
:root{color:#f5f7f0;font-synthesis:none;text-rendering:optimizelegibility;background:#0e1813;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif}*{box-sizing:border-box}html,body,#root{width:100%;height:100%;margin:0;overflow:hidden}button{font:inherit}.shell{width:100vw;height:100svh;position:relative;overflow:hidden}.worldCanvas{z-index:0;inset:0;width:100%!important;height:100%!important;position:absolute!important}.scoreboard,.eventFeed,.controls,.agentPanel,.errorToast,.busyToast{-webkit-backdrop-filter:blur(14px);backdrop-filter:blur(14px);background:#121c17cc;border:1px solid #ffffff2e;box-shadow:0 18px 60px #00000038}.scoreboard{z-index:60;border-radius:10px;gap:8px;width:min(460px,100vw - 28px);padding:10px 14px 12px;display:grid;position:absolute;top:14px;left:50%;transform:translate(-50%)}.scoreDuel{grid-template-columns:1fr auto 1fr;align-items:center;gap:10px;display:grid}.scoreSide{align-items:baseline;gap:10px;display:flex}.scoreName{letter-spacing:.06em;font-size:13px;font-weight:800}.scoreValue{font-size:24px;font-weight:800;line-height:1}.scoreVs{color:#f5f7f08c;text-transform:uppercase;font-size:12px;font-weight:700}.scoreMeta{color:#f5f7f0c7;flex-wrap:wrap;justify-content:space-between;gap:10px;font-size:12px;display:flex}.scoreActivity{text-overflow:ellipsis;white-space:nowrap;color:#c7e6bd;overflow:hidden}.famineBadge{color:#ffeebc;letter-spacing:.04em;background:#f0c34e24;border:1px solid #f0c34e80;border-radius:5px;padding:2px 8px;font-size:11px;font-weight:800;animation:1.6s ease-in-out infinite faminePulse}.countryGrid{grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;display:grid}.countryCard{background:#ffffff12;border:1px solid #ffffff38;border-radius:8px;gap:5px;min-width:0;padding:8px 9px;display:grid}.countryCardEmpty{color:#f5f7f099;grid-column:1/-1;font-size:12px}.countryHead{grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:6px;font-size:11px;display:grid}.countryHead strong{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.countryHead span:last-child{color:#f5f7f094;font-size:10px}.countryBadge,.npcCountryBadge{color:#101813;border-radius:5px;place-items:center;width:18px;height:18px;font-size:11px;font-weight:900;line-height:1;display:inline-grid}.countryPolicy{color:#f5f7f0d1;-webkit-line-clamp:2;-webkit-box-orient:vertical;min-height:2.5em;margin:0;font-size:11px;line-height:1.25;display:-webkit-box;overflow:hidden}.countryFacts{color:#f5f7f09e;grid-template-columns:repeat(2,minmax(0,1fr));gap:3px 8px;font-size:10px;display:grid}.countryFacts span{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}@keyframes faminePulse{0%,to{opacity:1}50%{opacity:.55}}.winProgress{gap:5px;display:grid}.winRow{grid-template-columns:158px 1fr;align-items:center;gap:8px;display:grid}.winLabel{color:#f5f7f0d1;white-space:nowrap;font-size:11px}.winBar{background:#ffffff1f;border-radius:5px;height:8px;overflow:hidden}.winFill{border-radius:5px;height:100%;transition:width .32s;display:block}.winFillPopulation{background:linear-gradient(90deg,#6fd96a,#b8f08a)}.winFillSurvival{background:linear-gradient(90deg,#5b96f0,#9fd1ff)}.modelPills{justify-content:center;gap:6px;display:flex}.modelPill{background:#ffffff17;border-radius:5px;padding:4px 8px;font-size:10px}.modelStatusReady{color:#d9f4cf}.modelStatusWarmup,.modelStatusUnknown{color:#fff3a1}.modelStatusError{color:#ffd3c8}.scoreboard .tooltipAnchor[data-tooltip]:before{border-top-color:#0000;border-bottom-color:#0a110ef5;top:calc(100% + 2px);bottom:auto}.scoreboard .tooltipAnchor[data-tooltip]:after{top:calc(100% + 14px);bottom:auto}.sidePanelHeader{border-bottom:1px solid #ffffff24;justify-content:space-between;align-items:center;gap:10px;padding:11px 12px 9px;display:flex}.sidePanelTitle{color:#fffce9;font-size:13px;font-weight:800}.sidePanelHint{color:#f5f7f08c;font-size:11px}.eventEmpty{color:#f5f7f08c;margin:0;padding:12px;font-size:12px}.eventFeed{z-index:30;border-radius:10px;flex-direction:column;width:min(316px,100vw - 28px);display:flex;position:absolute;top:14px;bottom:84px;right:14px;overflow:hidden}.eventList{scrollbar-width:thin;flex:1;align-content:start;gap:2px;min-height:0;padding:6px 8px 10px;display:grid;overflow-y:auto}.eventLine{border-left:3px solid #0000;border-radius:4px;grid-template-columns:34px 20px 1fr;align-items:baseline;gap:6px;padding:4px 6px;font-size:12px;line-height:1.3;display:grid}.eventTick{color:#f5f7f073;font-variant-numeric:tabular-nums;text-align:right;font-size:10px}.eventIcon{text-align:center}.eventText{color:#f5f7f0e0;overflow-wrap:anywhere}.severityGood{background:#6fd96a14;border-left-color:#6fd96a}.severityGood .eventText{color:#d6f5cf}.severityWarning{background:#f0c34e14;border-left-color:#f0c34e}.severityWarning .eventText{color:#ffeebc}.severityDanger{background:#f06a4e1a;border-left-color:#f06a4e}.severityDanger .eventText{color:#ffd3c8}.severityNeutral{border-left-color:#ffffff2e}.directiveTarget{color:#9fd1ff;flex-shrink:0;font-family:ui-monospace,monospace;font-size:10px;font-weight:700}.directiveText{color:#f5f7f0d1}.directiveBadge{color:#ffeebc;background:#f0c34e2e;border-radius:4px;flex-shrink:0;padding:1px 5px;font-size:9px;font-weight:700}.controls{z-index:30;border-radius:8px;gap:8px;padding:8px;display:flex;position:absolute;bottom:18px;right:14px}.controls button{color:#f7f5dd;cursor:pointer;background:#ffffff1f;border:0;border-radius:7px;place-items:center;width:42px;height:42px;display:grid}.controls button:disabled{cursor:wait;opacity:.88}.controls button:hover{background:#fff3}.controls button.isActive{color:#142018;background:#f8df6b}.controls button.isActive:hover{background:#ffe983}.controls button:focus-visible,.npcLabel button:focus-visible{outline-offset:2px;outline:2px solid #f8df6b}.agentPanel{z-index:30;border-radius:8px;width:min(316px,100vw - 108px);position:absolute;bottom:18px;left:14px;overflow:visible}.panelHeader{border-bottom:1px solid #ffffff24;justify-content:space-between;gap:12px;padding:13px 14px 10px;display:flex}.panelTitle{color:#fffce9;min-width:0;font-size:15px;font-weight:750}.panelRole{color:#c7e6bd;text-transform:uppercase;font-size:12px}.agentReadout{background:#ffffff1a;grid-template-columns:repeat(4,minmax(0,1fr));gap:1px;display:grid}.readoutCell{color:#f5f7f0d1;text-align:center;background:#121c17ad;min-width:0;padding:11px 8px;font-size:12px}.healthCell{color:#fff3a1;font-weight:750}.directiveBanner{background:#f8df6b17;border-top:1px solid #ffffff1a;gap:4px;padding:10px 12px;display:grid}.directiveBanner span{color:#fff3a1;text-transform:uppercase;font-size:11px;font-weight:750;line-height:1.1}.directiveBanner p{color:#f5f7f0db;margin:0;font-size:12px;line-height:1.35}.relationshipStrip{flex-wrap:wrap;gap:6px;padding:9px 10px 0;display:flex}.relationshipPill{color:#f5f7f0d1;background:#ffffff17;border-radius:6px;max-width:96px;padding:6px 8px;font-size:11px;line-height:1}.houseResidents{gap:2px;padding:9px 0 0;display:grid}.houseResidents:last-child{padding-bottom:10px}.houseResidentsLabel{color:#c7e6bd;letter-spacing:.07em;text-transform:uppercase;padding:0 10px;font-size:10px;font-weight:750}.houseResidentStrip{padding-top:4px}button.houseResidentPill{font:inherit;cursor:pointer;border:none;font-size:11px}button.houseResidentPill:hover{color:#fffce9;background:#ffffff2e}.memoryList{max-height:148px;padding:9px 10px 10px;overflow-y:auto}.memorySummary,.memoryItem{border-bottom:1px solid #ffffff1a;gap:4px;padding:9px 0;display:grid}.memorySummary{padding-top:0}.memoryItem:last-child{border-bottom:0}.memoryTick{color:#c7e6bd;font-size:11px;line-height:1.1}.memorySummary p,.memoryItem p{color:#f5f7f0d6;margin:0;font-size:12px;line-height:1.35;position:relative}.memorySummary p{color:#f5f7f0b8}.tooltipAnchor{min-width:0;position:relative;overflow:visible}.tooltipAnchor:focus-visible{outline-offset:2px;outline:2px solid #f8df6b}.truncateText{text-overflow:ellipsis;white-space:nowrap;min-width:0;display:block;overflow:hidden}.tooltipMeasure{pointer-events:none;visibility:hidden;white-space:nowrap;width:max-content;height:0;position:absolute;overflow:hidden}.tooltipAnchor[data-tooltip]:before,.tooltipAnchor[data-tooltip]:after,.memoryItem p[data-tooltip]:before,.memoryItem p[data-tooltip]:after{z-index:80;opacity:0;pointer-events:none;visibility:hidden;transition:opacity .12s,transform .12s,visibility .12s;position:absolute;left:50%}.tooltipAnchor[data-tooltip]:before,.memoryItem p[data-tooltip]:before{content:"";border:6px solid #0000}.tooltipAnchor[data-tooltip]:after,.memoryItem p[data-tooltip]:after{content:attr(data-tooltip);color:#fffce9;overflow-wrap:anywhere;text-align:left;text-transform:none;white-space:normal;background:#0a110ef5;border:1px solid #ffffff2e;border-radius:6px;width:max-content;max-width:min(320px,100vw - 28px);padding:8px 10px;font-size:12px;font-weight:600;line-height:1.3;box-shadow:0 14px 42px #00000047}.tooltipAnchor[data-tooltip]:hover:before,.tooltipAnchor[data-tooltip]:hover:after,.tooltipAnchor[data-tooltip]:focus-visible:before,.tooltipAnchor[data-tooltip]:focus-visible:after,.memoryItem p[data-tooltip]:hover:before,.memoryItem p[data-tooltip]:hover:after{opacity:1;visibility:visible}.tooltipAnchor[data-tooltip]:before,.memoryItem p[data-tooltip]:before{border-top-color:#0a110ef5;bottom:calc(100% + 2px);transform:translate(-50%,4px)}.tooltipAnchor[data-tooltip]:after,.memoryItem p[data-tooltip]:after{bottom:calc(100% + 14px);transform:translate(-50%,4px)}.tooltipAnchor[data-tooltip]:hover:before,.tooltipAnchor[data-tooltip]:hover:after,.tooltipAnchor[data-tooltip]:focus-visible:before,.tooltipAnchor[data-tooltip]:focus-visible:after,.memoryItem p[data-tooltip]:hover:before,.memoryItem p[data-tooltip]:hover:after{transform:translate(-50%)}.errorToast{z-index:70;color:#ffd3c8;border-color:#ff785e8a;border-radius:8px;max-width:min(560px,100vw - 28px);padding:11px 14px;font-size:13px;position:absolute;top:142px;left:50%;transform:translate(-50%)}.busyToast{z-index:65;color:#fffce9;border-radius:8px;align-items:center;gap:8px;max-width:min(560px,100vw - 28px);padding:11px 14px;font-size:13px;display:inline-flex;position:absolute;top:142px;left:50%;transform:translate(-50%)}.spinIcon{animation:.9s linear infinite spin}@keyframes spin{to{transform:rotate(360deg)}}.npcLabel{pointer-events:auto}.npcCard{color:#fffce9;cursor:pointer;text-align:left;background:#0f1814d1;border:1.5px solid #ffffff38;border-radius:8px;gap:3px;min-width:96px;max-width:136px;padding:6px 8px 7px;display:grid;box-shadow:0 10px 28px #0000003d}.npcCard:hover{background:#121e18eb;box-shadow:0 12px 30px #00000047}.npcCardDead{opacity:.62;filter:grayscale(.7)}.npcCardName{align-items:center;gap:5px;min-width:0;display:flex}.npcCardName strong{text-overflow:ellipsis;white-space:nowrap;font-size:12px;line-height:1.1;overflow:hidden}.npcCardIcon{font-size:11px;line-height:1}.npcCountryBadge{background:#f7d96c;width:16px;height:16px;font-size:10px}.npcCardRole{letter-spacing:.07em;text-transform:uppercase;font-size:9px;font-weight:700;line-height:1}.npcBar{background:#ffffff24;border-radius:3px;width:100%;height:5px;display:block;overflow:hidden}.npcBarFill{border-radius:3px;height:100%;transition:width .28s;display:block}.npcAgeBar{height:3px}.npcAgeFill{background:#d9b35a}.houseTag,.beastTag,.resourceTag{pointer-events:none;color:#fffce9;white-space:nowrap;background:#0f1814cc;border:1px solid #fff3;border-radius:7px;gap:3px;padding:5px 8px;display:grid}.houseTag{min-width:88px}button.houseTag{pointer-events:auto;font:inherit;text-align:left;cursor:pointer}button.houseTag:hover{background:#121e18eb;box-shadow:0 10px 26px #00000042}.houseTagBuilding{border-color:#ffd07a80}.houseTagDestroyed{opacity:.85;border-color:#f06a4e8c}.houseTagSelected{border-color:#9fd8ffd9;box-shadow:0 0 0 1px #9fd8ff73}.houseTagTitle,.beastTagTitle{font-size:11px;font-weight:700;line-height:1.1}.houseHpBar,.beastHpBar{height:4px}.beastTag{border-color:#f06a4e80;min-width:80px}.beastTagTitle{color:#ffd3c8}.resourceTag{padding:3px 7px;font-size:11px;font-weight:700}.treasuryTag,.cannonTag{text-align:center;background:#0f1814e0;border-color:#f4d36a73}.gameOverOverlay{z-index:100;-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);background:#080d0ac7;place-items:center;padding:20px;animation:.4s overlayIn;display:grid;position:absolute;inset:0}@keyframes overlayIn{0%{opacity:0}to{opacity:1}}.gameOverCard{text-align:center;background:#121c17f5;border:1px solid #ffffff38;border-radius:14px;gap:14px;width:min(620px,100vw - 40px);max-height:calc(100svh - 60px);padding:26px 28px;display:grid;overflow-y:auto;box-shadow:0 30px 90px #00000080}.gameOver-win .gameOverCard{border-color:#6fd96a73}.gameOver-lose .gameOverCard{border-color:#f06a4e73}.gameOverCard h1{letter-spacing:.04em;margin:0;font-size:26px}.gameOver-win .gameOverCard h1{color:#d6f5cf}.gameOver-lose .gameOverCard h1{color:#ffd3c8}.gameOverSubtitle{color:#f5f7f0c7;margin:0;font-size:14px}.gameOverScore{justify-content:center;gap:12px;font-size:17px;font-weight:800;display:flex}.scoreDivider{color:#f5f7f066}.gameOverStats{grid-template-columns:repeat(3,1fr);gap:8px;display:grid}.gameOverStat{background:#ffffff12;border-radius:8px;gap:3px;padding:10px 6px;display:grid}.gameOverStatValue{font-size:17px;font-weight:800}.gameOverStatLabel{color:#f5f7f09e;text-transform:uppercase;letter-spacing:.05em;font-size:11px}.gameOverCauses{color:#f5f7f0ad;margin:0;font-size:12px}.gameOverStory{text-align:left;gap:7px;display:grid}.gameOverStory h2{color:#f5f7f0cc;text-transform:uppercase;letter-spacing:.06em;margin:0;font-size:13px}.gameOverStoryList{scrollbar-width:thin;background:#00000038;border-radius:8px;gap:2px;max-height:220px;padding:6px;display:grid;overflow-y:auto}.gameOverDismiss{color:#fffce9;cursor:pointer;background:#ffffff1a;border:1px solid #ffffff42;border-radius:8px;justify-self:center;padding:10px 22px;font-size:13px;font-weight:700}.gameOverDismiss:hover{background:#ffffff2e}@media (width<=1180px){.eventFeed{width:min(262px,100vw - 28px)}}@media (width<=860px){.scoreboard{width:min(400px,100vw - 16px);top:8px}.eventFeed{max-height:32svh;top:auto;bottom:200px}.agentPanel{display:none}.controls{bottom:200px;right:8px}}
|
dist/frontend/index.html
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<title>World Simulator</title>
|
| 7 |
+
<script type="module" crossorigin src="/world/assets/index-B3jtwnUR.js"></script>
|
| 8 |
+
<link rel="stylesheet" crossorigin href="/world/assets/index-Cbon7bZF.css">
|
| 9 |
+
</head>
|
| 10 |
+
<body>
|
| 11 |
+
<div id="root"></div>
|
| 12 |
+
</body>
|
| 13 |
+
</html>
|
src/world_simulator/api/gradio_app.py
CHANGED
|
@@ -12,10 +12,9 @@ from fastapi.responses import (
|
|
| 12 |
FileResponse,
|
| 13 |
HTMLResponse,
|
| 14 |
JSONResponse,
|
| 15 |
-
RedirectResponse,
|
| 16 |
)
|
| 17 |
-
from fastapi.staticfiles import StaticFiles
|
| 18 |
from pydantic import BaseModel
|
|
|
|
| 19 |
|
| 20 |
from world_simulator.api.mcp_server import register_mcp_routes
|
| 21 |
from world_simulator.api.public_ui import mount_public_ui
|
|
@@ -43,6 +42,12 @@ def create_gradio_app(
|
|
| 43 |
runtime = create_game_runtime(world=create_world(config), config=config)
|
| 44 |
app = gr.Server(title="World Simulator")
|
| 45 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
app.add_middleware(
|
| 47 |
CORSMiddleware,
|
| 48 |
allow_origins=["*"],
|
|
@@ -56,10 +61,11 @@ def create_gradio_app(
|
|
| 56 |
register_player_routes(app, runtime)
|
| 57 |
register_admin_routes(app, runtime)
|
| 58 |
register_mcp_routes(app, runtime)
|
| 59 |
-
|
| 60 |
-
#
|
| 61 |
-
# MCP or mounted lobby routes above.
|
| 62 |
register_static_routes(app, static_dir)
|
|
|
|
|
|
|
| 63 |
return app
|
| 64 |
|
| 65 |
|
|
@@ -194,18 +200,12 @@ def register_gradio_api(app: gr.Server, runtime: GameRuntime) -> None:
|
|
| 194 |
|
| 195 |
def register_static_routes(app: gr.Server, static_dir: Path) -> None:
|
| 196 |
static_dir = static_dir.resolve()
|
| 197 |
-
assets_dir = static_dir / "assets"
|
| 198 |
index_path = static_dir / "index.html"
|
| 199 |
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
#
|
| 204 |
-
@app.get("/", response_model=None)
|
| 205 |
-
def root() -> RedirectResponse:
|
| 206 |
-
return RedirectResponse(url="/connect")
|
| 207 |
-
|
| 208 |
-
# The 3D world observer (React SPA) lives under /world.
|
| 209 |
@app.get("/world", response_class=HTMLResponse, response_model=None)
|
| 210 |
def world_root() -> FileResponse | HTMLResponse:
|
| 211 |
if index_path.exists():
|
|
|
|
| 12 |
FileResponse,
|
| 13 |
HTMLResponse,
|
| 14 |
JSONResponse,
|
|
|
|
| 15 |
)
|
|
|
|
| 16 |
from pydantic import BaseModel
|
| 17 |
+
from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware
|
| 18 |
|
| 19 |
from world_simulator.api.mcp_server import register_mcp_routes
|
| 20 |
from world_simulator.api.public_ui import mount_public_ui
|
|
|
|
| 42 |
runtime = create_game_runtime(world=create_world(config), config=config)
|
| 43 |
app = gr.Server(title="World Simulator")
|
| 44 |
|
| 45 |
+
# Honor X-Forwarded-Proto/-Host from the TLS-terminating proxy (e.g. Hugging
|
| 46 |
+
# Face Spaces). Without this, uvicorn only trusts forwarded headers from
|
| 47 |
+
# localhost, so it treats requests as plain http and emits http:// absolute
|
| 48 |
+
# redirects, which break under the HTTPS proxy.
|
| 49 |
+
app.add_middleware(ProxyHeadersMiddleware, trusted_hosts="*")
|
| 50 |
+
|
| 51 |
app.add_middleware(
|
| 52 |
CORSMiddleware,
|
| 53 |
allow_origins=["*"],
|
|
|
|
| 61 |
register_player_routes(app, runtime)
|
| 62 |
register_admin_routes(app, runtime)
|
| 63 |
register_mcp_routes(app, runtime)
|
| 64 |
+
# The SPA routes (/world) must be registered before the lobby, which is
|
| 65 |
+
# mounted at the root path and would otherwise shadow them.
|
|
|
|
| 66 |
register_static_routes(app, static_dir)
|
| 67 |
+
# The lobby is mounted at "/" and acts as a catch-all, so it must be last.
|
| 68 |
+
mount_public_ui(app, runtime, path="/")
|
| 69 |
return app
|
| 70 |
|
| 71 |
|
|
|
|
| 200 |
|
| 201 |
def register_static_routes(app: gr.Server, static_dir: Path) -> None:
|
| 202 |
static_dir = static_dir.resolve()
|
|
|
|
| 203 |
index_path = static_dir / "index.html"
|
| 204 |
|
| 205 |
+
# The 3D world observer (React SPA) lives under /world. Its assets are built
|
| 206 |
+
# with base "/world/" (see vite.config.ts), so they are served by the
|
| 207 |
+
# /world/{path} catch-all below rather than a separate /assets mount, which
|
| 208 |
+
# would otherwise collide with the Gradio lobby mounted at the root path.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
@app.get("/world", response_class=HTMLResponse, response_model=None)
|
| 210 |
def world_root() -> FileResponse | HTMLResponse:
|
| 211 |
if index_path.exists():
|
vite.config.ts
CHANGED
|
@@ -4,6 +4,9 @@ import { defineConfig } from "vite";
|
|
| 4 |
export default defineConfig({
|
| 5 |
plugins: [react()],
|
| 6 |
root: "frontend",
|
|
|
|
|
|
|
|
|
|
| 7 |
server: {
|
| 8 |
host: "127.0.0.1",
|
| 9 |
port: 5173,
|
|
|
|
| 4 |
export default defineConfig({
|
| 5 |
plugins: [react()],
|
| 6 |
root: "frontend",
|
| 7 |
+
// The SPA is served under /world (the lobby owns the root path), so assets
|
| 8 |
+
// must be referenced as /world/assets/... rather than /assets/...
|
| 9 |
+
base: "/world/",
|
| 10 |
server: {
|
| 11 |
host: "127.0.0.1",
|
| 12 |
port: 5173,
|