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
5 changes: 5 additions & 0 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,8 @@ jobs:
- name: Build dashboard
working-directory: openweights/dashboard/frontend
run: npm ci --no-audit --no-fund && npm run build
- name: Verify dashboard browser startup
run: |
pip install playwright
playwright install --with-deps chromium
python tests/dashboard_browser_smoke.py
9 changes: 9 additions & 0 deletions docs/release-0.13.2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# OpenWeights 0.13.2

Fixes the blank dashboard caused by missing frontend Supabase environment
variables in packaged builds. The dashboard now reads its public configuration
from the backend at startup, so releases do not require build-time credentials.

Adds API coverage for public configuration and a Chromium startup check in CI.
No database migration is required. Worker images remain at v0.13.1 because this
patch only changes the dashboard.
14 changes: 13 additions & 1 deletion openweights/dashboard/backend/main.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import json
from decimal import Decimal
from typing import Dict, List, Optional

Expand All @@ -9,7 +10,7 @@
from dotenv import load_dotenv
from fastapi import Depends, FastAPI, Header, HTTPException, Query
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, PlainTextResponse
from fastapi.responses import FileResponse, PlainTextResponse, Response
from fastapi.staticfiles import StaticFiles
from models import (
Job,
Expand Down Expand Up @@ -73,6 +74,17 @@ async def get_db(authorization: str = Header(None)) -> Database:
)


@app.get("/config.js", include_in_schema=False)
async def dashboard_config():
"""Public browser configuration; never expose service-role credentials."""
config = {"supabaseUrl": _SUPABASE_URL, "supabaseAnonKey": _SUPABASE_ANON_KEY}
return Response(
content="window.__OPENWEIGHTS_CONFIG__ = " + json.dumps(config) + ";",
media_type="application/javascript",
headers={"Cache-Control": "no-store"},
)


# Auth endpoints
@app.post("/auth/exchange-api-key")
async def exchange_api_key(api_key: dict):
Expand Down

Large diffs are not rendered by default.

3,855 changes: 3,855 additions & 0 deletions openweights/dashboard/backend/static/assets/MetricsPlots-nOeyXer1.js

Large diffs are not rendered by default.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

179 changes: 179 additions & 0 deletions openweights/dashboard/backend/static/assets/index-BgDIHOp8.js

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions openweights/dashboard/backend/static/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Development fallback: use VITE_SUPABASE_* settings.
// In production, FastAPI serves /config.js with runtime public configuration.
3 changes: 2 additions & 1 deletion openweights/dashboard/backend/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
<link rel="icon" type="image/svg+xml" href="/ow.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Open Weights</title>
<script type="module" crossorigin src="/assets/index-gTAMxuvh.js"></script>
<script type="module" crossorigin src="/assets/index-BgDIHOp8.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DpYPsR6j.css">
</head>
<body>
<div id="root"></div>
<script src="/config.js"></script>
</body>
</html>
66 changes: 16 additions & 50 deletions openweights/dashboard/frontend/README.md
Original file line number Diff line number Diff line change
@@ -1,50 +1,16 @@
# React + TypeScript + Vite

This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.

Currently, two official plugins are available:

- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh

## Expanding the ESLint configuration

If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:

- Configure the top-level `parserOptions` property like this:

```js
export default tseslint.config({
languageOptions: {
// other options...
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
},
})
```

- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
- Optionally add `...tseslint.configs.stylisticTypeChecked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:

```js
// eslint.config.js
import react from 'eslint-plugin-react'

export default tseslint.config({
// Set the react version
settings: { react: { version: '18.3' } },
plugins: {
// Add the react plugin
react,
},
rules: {
// other rules...
// Enable its recommended rules
...react.configs.recommended.rules,
...react.configs['jsx-runtime'].rules,
},
})
```
# Dashboard frontend

Packaged dashboards served by `ow serve` load their public Supabase URL and anon
key from the backend's `/config.js` endpoint before starting React. Configure
`SUPABASE_URL` and `SUPABASE_ANON_KEY` on the server; no frontend build-time
configuration is required. The endpoint never returns service-role credentials.

For standalone Vite development, set `VITE_SUPABASE_URL` and
`VITE_SUPABASE_ANON_KEY` in `.env.local`, then run `npm ci` and `npm run dev`.
Runtime configuration takes precedence over these development settings.

Run `npm run build` to update the packaged backend static assets. CI builds
without Vite environment variables and runs `python tests/dashboard_browser_smoke.py`
from the repository root to verify the login page actually renders in Chromium.
The smoke test requires Playwright and its Chromium browser. Pass `--url URL`
to check an existing deployment.
1 change: 1 addition & 0 deletions openweights/dashboard/frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
</head>
<body>
<div id="root"></div>
<script src="/config.js"></script>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
2 changes: 2 additions & 0 deletions openweights/dashboard/frontend/public/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Development fallback: use VITE_SUPABASE_* settings.
// In production, FastAPI serves /config.js with runtime public configuration.
4 changes: 2 additions & 2 deletions openweights/dashboard/frontend/src/supabaseClient.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { createClient } from '@supabase/supabase-js'
import { Database } from './types/supabase'

const supabaseUrl = import.meta.env.VITE_SUPABASE_URL
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY
const supabaseUrl = window.__OPENWEIGHTS_CONFIG__?.supabaseUrl || import.meta.env.VITE_SUPABASE_URL
const supabaseAnonKey = window.__OPENWEIGHTS_CONFIG__?.supabaseAnonKey || import.meta.env.VITE_SUPABASE_ANON_KEY

if (!supabaseUrl || !supabaseAnonKey) {
throw new Error('Missing Supabase environment variables')
Expand Down
7 changes: 7 additions & 0 deletions openweights/dashboard/frontend/src/vite-env.d.ts
Original file line number Diff line number Diff line change
@@ -1 +1,8 @@
/// <reference types="vite/client" />

interface Window {
__OPENWEIGHTS_CONFIG__?: {
supabaseUrl: string;
supabaseAnonKey: string;
};
}
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "openweights"
version = "0.13.1"
version = "0.13.2"
description = "An openai-like sdk for finetuning and batch inference"
readme = "README.md"
requires-python = ">=3.11"
Expand Down
83 changes: 83 additions & 0 deletions tests/dashboard_browser_smoke.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"""Verify the built dashboard bootstraps in Chromium, without build-time secrets."""

import argparse
import os
import socket
import subprocess
import sys
import time
import urllib.request
from contextlib import contextmanager
from pathlib import Path

from playwright.sync_api import sync_playwright


@contextmanager
def dashboard(url):
if url:
yield url
return
with socket.socket() as sock:
sock.bind(("127.0.0.1", 0))
port = sock.getsockname()[1]
backend = Path(__file__).resolve().parents[1] / "openweights/dashboard/backend"
process = subprocess.Popen(
[
os.getenv("OW_SERVER_PYTHON", sys.executable),
"-m",
"uvicorn",
"main:app",
"--host",
"127.0.0.1",
"--port",
str(port),
],
cwd=backend,
)
url = f"http://127.0.0.1:{port}"
try:
for _ in range(100):
if process.poll() is not None:
raise RuntimeError("Dashboard server exited before startup")
try:
with urllib.request.urlopen(url + "/config.js", timeout=1):
break
except OSError:
time.sleep(0.1)
else:
raise RuntimeError("Dashboard server did not start")
yield url
finally:
process.terminate()
process.wait(timeout=10)


def main():
parser = argparse.ArgumentParser()
parser.add_argument(
"--url", help="Check an existing deployment instead of a local server"
)
args = parser.parse_args()
with dashboard(args.url) as url, sync_playwright() as playwright:
browser = playwright.chromium.launch(
executable_path=os.getenv("OW_CHROMIUM_PATH"),
args=["--no-sandbox"],
)
try:
page = browser.new_page()
errors = []
page.on("pageerror", lambda error: errors.append(str(error)))
response = page.goto(url, wait_until="networkidle")
assert response.ok
page.get_by_label("Email Address").wait_for(state="visible")
config = page.evaluate("window.__OPENWEIGHTS_CONFIG__")
assert config and config["supabaseUrl"] and config["supabaseAnonKey"]
assert not errors, errors
print("Dashboard browser startup passed")
finally:
browser.close()


if __name__ == "__main__":
main()
23 changes: 23 additions & 0 deletions tests/test_cost_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,26 @@ def test_database_permission_error_stays_forbidden(client):
).status_code
== 403
)


def test_dashboard_bootstrap_uses_public_runtime_configuration(client, monkeypatch):
import json

main = importlib.import_module("main")
monkeypatch.setattr(main, "_SUPABASE_URL", "https://example.supabase.co")
monkeypatch.setattr(main, "_SUPABASE_ANON_KEY", "public-anon-key")
monkeypatch.setenv("SUPABASE_SERVICE_ROLE_KEY", "must-never-reach-browser")
http, db = client
response = http.get("/config.js")
assert response.status_code == 200
assert response.headers["cache-control"] == "no-store"
assert response.headers["content-type"].startswith("application/javascript")
config = json.loads(
response.text.removeprefix("window.__OPENWEIGHTS_CONFIG__ = ").removesuffix(";")
)
assert config == {
"supabaseUrl": "https://example.supabase.co",
"supabaseAnonKey": "public-anon-key",
}
assert "must-never-reach-browser" not in response.text
db.rpc.assert_not_called()
Loading