Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand All @@ -94,14 +114,16 @@ 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 \
&& chmod +x /usr/local/bin/openviking-console-entrypoint /usr/local/bin/openviking-pending-health
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

Expand Down
68 changes: 56 additions & 12 deletions openviking/server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand All @@ -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.
Expand Down
Binary file added web-studio/public/apple-touch-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added web-studio/public/favicon-32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added web-studio/public/favicon.ico
Binary file not shown.
4 changes: 1 addition & 3 deletions web-studio/src/lib/ov-client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})
Loading