From e44e527796d3d243ae6003f24f1f80af183df491 Mon Sep 17 00:00:00 2001 From: alice Date: Wed, 20 May 2026 23:24:37 +0800 Subject: [PATCH 1/2] feat(web-studio): bundle web-studio into docker, mount at /studio + favicon to public MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Dockerfile: add node:20 build stage for web-studio, `vite build --base=/studio/` output to /app/web-studio/dist. New ENV OPENVIKING_WEB_STUDIO_DIR. - server/app.py: serve /studio and /studio/{path:path} from dist; SPA deep-link fallback to index.html. Favicon routes (/favicon.ico, /favicon.png, /apple-touch-icon.png, /mcp/favicon.ico+png+touch) read from the same dist — no more separate console/static dependency. - web-studio/public: ship favicon.ico, favicon-32.png, apple-touch-icon.png inside the SPA bundle so they end up in dist root. Old console (8020) untouched in this branch. --- Dockerfile | 26 +++++++++- openviking/server/app.py | 68 ++++++++++++++++++++----- web-studio/public/apple-touch-icon.png | Bin 0 -> 26166 bytes web-studio/public/favicon-32.png | Bin 0 -> 1823 bytes web-studio/public/favicon.ico | Bin 0 -> 704 bytes 5 files changed, 80 insertions(+), 14 deletions(-) create mode 100644 web-studio/public/apple-touch-icon.png create mode 100644 web-studio/public/favicon-32.png create mode 100644 web-studio/public/favicon.ico diff --git a/Dockerfile b/Dockerfile index 7c172ac47..d8cfeed27 100644 --- a/Dockerfile +++ b/Dockerfile @@ -82,7 +82,27 @@ RUN --mount=type=cache,target=/root/.cache/uv,id=uv-${TARGETPLATFORM} \ ;; \ esac -# Stage 3: runtime +# Stage 3: build web-studio static bundle (Vite SPA). +# Produces /web-studio/dist which the runtime stage mounts at /studio in the +# OpenViking server. Lockfile-first dependency install keeps the layer cached +# until package.json / lockfile actually change. +# +# WEB_STUDIO_BASE_PATH is passed to `vite build --base=...` and is also +# consumed at runtime by getRouterBasePath() through import.meta.env.BASE_URL, +# so the SPA's asset URLs and TanStack Router basepath stay in sync. +FROM node:20-bookworm-slim AS web-studio-builder +WORKDIR /web-studio +ARG TARGETPLATFORM +ARG WEB_STUDIO_BASE_PATH=/studio/ + +COPY web-studio/package.json web-studio/package-lock.json ./ +RUN --mount=type=cache,target=/root/.npm,id=npm-${TARGETPLATFORM} \ + npm ci + +COPY web-studio/ ./ +RUN npm run build -- --base="${WEB_STUDIO_BASE_PATH}" + +# Stage 4: runtime FROM python:3.13-slim-trixie RUN apt-get update && apt-get install -y --no-install-recommends \ @@ -94,6 +114,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ WORKDIR /app COPY --from=py-builder /app/.venv /app/.venv +COPY --from=web-studio-builder /web-studio/dist /app/web-studio/dist COPY docker/openviking-console-entrypoint.sh /usr/local/bin/openviking-console-entrypoint COPY docker/pending_health_server.py /usr/local/bin/openviking-pending-health RUN mkdir -p /app/.openviking \ @@ -101,7 +122,8 @@ RUN mkdir -p /app/.openviking \ ENV HOME="/app" \ PATH="/app/.venv/bin:$PATH" \ OPENVIKING_CONFIG_FILE="/app/.openviking/ov.conf" \ - OPENVIKING_CLI_CONFIG_FILE="/app/.openviking/ovcli.conf" + OPENVIKING_CLI_CONFIG_FILE="/app/.openviking/ovcli.conf" \ + OPENVIKING_WEB_STUDIO_DIR="/app/web-studio/dist" EXPOSE 1933 8020 diff --git a/openviking/server/app.py b/openviking/server/app.py index d23425800..115722aa8 100644 --- a/openviking/server/app.py +++ b/openviking/server/app.py @@ -596,11 +596,19 @@ def _current_role(account_id: str, user_id: str) -> Role: except Exception as e: # noqa: BLE001 logger.warning("Skipping OAuth router registration: %s", e) - # Favicon: shared with the console static assets so 1933/console use the same logo. - # Some MCP clients (claude.ai connector cards) resolve the icon relative to the - # connector URL (e.g. {mcp_url}/favicon.ico) rather than the origin, so the same - # files are also exposed under /mcp/. - _static_dir = Path(__file__).resolve().parent.parent / "console" / "static" + # Web Studio SPA: serve the static bundle when present so the same OV + # server origin can host the new frontend at /studio. The directory is + # populated by the docker `web-studio-builder` stage; outside docker, set + # OPENVIKING_WEB_STUDIO_DIR to a local `web-studio/dist` to enable it. + # Favicon assets are bundled with web-studio (see web-studio/public/), + # so the top-level /favicon.* and /mcp/favicon.* routes are served from + # the same dist directory — no separate server-side static folder. + _studio_env = os.environ.get("OPENVIKING_WEB_STUDIO_DIR", "").strip() + if _studio_env: + _studio_dir = Path(_studio_env) + else: + _studio_dir = Path(__file__).resolve().parents[2] / "web-studio" / "dist" + _favicon_headers = {"Cache-Control": "public, max-age=86400"} _favicon_files = { "/favicon.ico": ("favicon.ico", "image/x-icon"), @@ -610,17 +618,53 @@ def _current_role(account_id: str, user_id: str) -> Role: "/mcp/favicon.png": ("favicon-32.png", "image/png"), "/mcp/apple-touch-icon.png": ("apple-touch-icon.png", "image/png"), } + _favicon_source = _studio_dir if _studio_dir.is_dir() else None + if _favicon_source is not None and all( + (_favicon_source / fname).is_file() for fname, _ in _favicon_files.values() + ): + + def _make_favicon_handler(filename: str, media_type: str): + path = _favicon_source / filename + + async def _handler(): + return FileResponse(path, media_type=media_type, headers=_favicon_headers) + + return _handler + + for _route, (_fname, _mime) in _favicon_files.items(): + app.add_api_route(_route, _make_favicon_handler(_fname, _mime), include_in_schema=False) - def _make_favicon_handler(filename: str, media_type: str): - path = _static_dir / filename + if _studio_dir.is_dir() and (_studio_dir / "index.html").is_file(): + _studio_root = _studio_dir.resolve() + _studio_index = _studio_root / "index.html" + _studio_no_store = {"Cache-Control": "no-store"} - async def _handler(): - return FileResponse(path, media_type=media_type, headers=_favicon_headers) + def _studio_response(path: Path, *, no_store: bool = False) -> FileResponse: + return FileResponse(path, headers=_studio_no_store if no_store else None) - return _handler + @app.get("/studio", include_in_schema=False) + async def _studio_root_handler(): + return _studio_response(_studio_index, no_store=True) - for _route, (_fname, _mime) in _favicon_files.items(): - app.add_api_route(_route, _make_favicon_handler(_fname, _mime), include_in_schema=False) + @app.get("/studio/{path:path}", include_in_schema=False) + async def _studio_assets(path: str): + # SPA fallback: serve real files when present, otherwise return + # index.html so TanStack Router can resolve the deep link. + try: + requested = (_studio_root / path).resolve() + except OSError: + return _studio_response(_studio_index, no_store=True) + + if not requested.is_relative_to(_studio_root): + return _studio_response(_studio_index, no_store=True) + + if requested.is_file(): + return _studio_response(requested) + return _studio_response(_studio_index, no_store=True) + + logger.info("Web Studio mounted at /studio from %s", _studio_root) + else: + logger.info("Web Studio bundle not found at %s; skipping /studio mount", _studio_dir) # MCP endpoint — serves 5 tools (search, read, store, forget, health) # via streamable HTTP for Claude Code and other MCP clients. diff --git a/web-studio/public/apple-touch-icon.png b/web-studio/public/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..60289db06fd81c7936ed32360c86c0978fe4bd75 GIT binary patch literal 26166 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 LNkvXXu0mjfdl)Nk literal 0 HcmV?d00001 diff --git a/web-studio/public/favicon-32.png b/web-studio/public/favicon-32.png new file mode 100644 index 0000000000000000000000000000000000000000..2a1662ed7db0531b115b982a1313b9ba981dfeb6 GIT binary patch literal 1823 zcmV+)2jKXLP)dNoT1?eo@!`AeEgSPPavY4C+eo(Q&=8;$EOu1%7&_ zEwcA&_rxkf2wf^KxC8(oAq0Dd>17K5w`pzaikia%U9zMcXbpr5-ELwoB@$dB0N9Ua zm_y+>;3Jg|H9W3m4~NB<;>32^?+>SP97pdI1P2Lo1eX?dk@bObu&x;ehw zRRDDWGYcY5laVJ7iGrDA6&;&OEh}qwTDsZ-`m0GU@zJ4p);>2O=I7bqmI}bxGz%c6 zVK#d*OfXctObW8CS2)j4W|IWZMTAh1SJ|Hs|jJN0~KX}tQ0Pn zWyhY2zq>p*Y+O?8V!FWbyV!qCO~BtZ08N!Z*{B(0Ga`UVfoLXxVpC?ltf`?RV@C7z zG+>fdc(Tglmntj4T&!GkOEK>8AXh^O-Sc7~7YMB7W}tx7+aIUCk8hrx|4tUUwt-K8 zSQTZ}&TV-Qu_$dFZk~jcQH4sKR!Jcye%3L`^wSmz+#2F?5&S8W1Dm*^nbzkfo zoj3K@HkMdUEYBC9vp)j;L0rMAVpSF64u0kgAuPrFs-ItQ*li>&$u6&-G(kW_Vek{< zV&{OkX6verSC;39#WpKI{W*ZakU>+^5Zb$@avmryIc#0Ccg`WZbx; z2zUSpQ+RmuvS&7wXIBq}D7O4N_S|@AysWmN=TJmXetKn|SggmuIQqV(U+bMVr!>Q~ zr^p&eisSd*TiJ{oNjCXudrHReyz$SA0P%Xg+%;zv^WG>tIaHABQ1xk@KK0lG3S&IN zsyEYi?cC5@;51D%sM+vwjMEGXNl83kn5Xi2E`WHP#v7W<$>uiW*O4gIzFk44eLiG% zjRgU)J#spMY}rB#x7*E1DR>ciH$V>%GLr`$pHHZ+tTey>+krnR7OVZerhpcnqO!}G z1V#6#kZRSYQY3oXMuc}UK632wEjeZ5ny|0Fd7zl-H#N=Hk{K^=7y5Tvv| z_97Bt-~I8ev9&#;wj`&ieZvNmvh@QHE*xYUe-aa4w9u)hfpB(Ie z@cFv%I>bdINSZog+q=h%f%n@HxBcS!zQyhm&TZ~GAa!oG_p|I@m;;D}<2gKJQt|a)wH+QC(Z6GA(=;nxH6PpH;)<51 zp$US(lK0jv3O9Nq*R6^G4x5;2l}$%(jzw6R50GmEIMo`5lV|$QJ+e{Wwz)Jv%o^8x z>Mag{3G3ag;_(E3{O0K1Xp}rB%Zk)`rH`hllKEh%3p0{s)N~?PB%YTzfK-bK=)D*8k<3Oqb^fc$o3q2)W%PBTe00%6!;0 z_0`pR*6pE5Sl>Bp+w6X%inh&e#tSin7M`q_dJ`}3vF0V_G{{VeRW~Z};%AEiJ N002ovPDHLkV1kO-ZgBtr literal 0 HcmV?d00001 diff --git a/web-studio/public/favicon.ico b/web-studio/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..13eb46e092b58a2e655cb75273e7eeba4ec9e8fb GIT binary patch literal 704 zcmV;x0zdr#0096201yxW0000W0IC8202TlM0EtjeM-2)Z3IG5A4M|8uQUCw|5C8xG z5C{eU001BJ|6u?C0&z)1K~#90jZ$4mQ*jhO=iaO0bqGs|Ae2C&GW%F!MO083^%Ood zqCu~#hv>z(9-KxmVL_M+13yA))E!}INh)g#*QX9E8FT5R&2ny{G3Ru;CM8%?i&zASmwY_sta!dcqTGF;63B*g1V^4y!WJV;_h zJiIT*i3&cSnM-bo4)N7SBgh;gLRHlp?!@_{$21!5KYKcur*9*kn@0e(Ad{1=yrA+Rh)NB7HUb*pbvQ%~Z z(ml%aOw&1cn1<`xr<&3&Q_2H15EdNOU31kK>c*(y9nlL9QL{swvN#h__axCrKhe#W zAq8b_X;>0Vgn6lA4)dSOWx m5wWLRgE`AbO_bh| Date: Thu, 21 May 2026 12:27:01 +0800 Subject: [PATCH 2/2] fix(web-studio): drop hardcoded 127.0.0.1:1933 fallback in ovClient default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ovClient was initialized with `baseUrl: ENV_BASE_URL || 'http://127.0.0.1:1933'`, which short-circuited normalizeBaseUrl's window.location.origin fallback. As a result, when web-studio was served by the OV server itself (e.g. bundled in the OV docker image at /studio/), the SPA would try to call back to the user's local 127.0.0.1:1933 instead of the same origin it was loaded from — making the bundled deployment unusable. Removing the literal fallback lets normalizeBaseUrl pick window.location.origin when ENV_BASE_URL is empty, which is the right default for every "served by OV" shape (bundled image, port-forwarded docker, reverse-proxied custom domain). Dev workflows running vite separately can opt in by setting VITE_OV_BASE_URL or overriding the URL in the in-app Connection dialog (which persists to localStorage). --- web-studio/src/lib/ov-client/client.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/web-studio/src/lib/ov-client/client.ts b/web-studio/src/lib/ov-client/client.ts index 52bc8af4e..7e0952f74 100644 --- a/web-studio/src/lib/ov-client/client.ts +++ b/web-studio/src/lib/ov-client/client.ts @@ -299,9 +299,7 @@ export function createOvClient(options: OvClientOptions = {}): OvClientAdapter { } } -const DEFAULT_LOCAL_BASE_URL = 'http://127.0.0.1:1933' - export const ovClient = createOvClient({ - baseUrl: ENV_BASE_URL || DEFAULT_LOCAL_BASE_URL, + baseUrl: ENV_BASE_URL, bindSdkClient: true, })