Skip to content

Commit fcf2d67

Browse files
committed
fix(studio): fix dev proxy, WebSocket paths, and export action for CORS-free relative routing
1 parent b178aec commit fcf2d67

5 files changed

Lines changed: 171 additions & 8 deletions

File tree

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ dist/
1111
downloads/
1212
eggs/
1313
.eggs/
14-
lib/
15-
lib64/
14+
/lib/
15+
/lib64/
1616
parts/
1717
sdist/
1818
var/

studio/frontend/src/lib/api.ts

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
// Typesafe API client for ScrapeWizard Studio
2+
3+
export const API_BASE = '';
4+
5+
export interface SettingData {
6+
provider: string;
7+
model: string;
8+
ai_mode: string;
9+
has_key: boolean;
10+
visual_threshold: number;
11+
retention: number;
12+
}
13+
14+
export interface StepData {
15+
id?: number;
16+
test_id: number;
17+
order: number;
18+
action: string;
19+
value: string;
20+
selectors: Array<{ kind: string; value: string }>;
21+
assertions: Array<{ kind: string; value: string }>;
22+
fingerprint: any;
23+
}
24+
25+
export interface TestData {
26+
id: number;
27+
name: string;
28+
url: string;
29+
step_count?: number;
30+
last_run?: {
31+
id: number;
32+
status: string;
33+
started_at: string;
34+
} | null;
35+
steps?: StepData[];
36+
}
37+
38+
export interface RunSummary {
39+
id: number;
40+
test_id: number;
41+
test_name: string;
42+
status: string;
43+
started_at: string;
44+
finished_at: string | null;
45+
duration_ms: number | null;
46+
ai_calls: number;
47+
ai_cost_usd: number;
48+
}
49+
50+
export interface StepResultData {
51+
id: number;
52+
run_id: number;
53+
step_name: string;
54+
status: string;
55+
duration_ms: number;
56+
screenshot_path: string | null;
57+
visual_diff_score: number | null;
58+
console_errors: string[];
59+
network_errors: string[];
60+
a11y_violations: any[];
61+
healed: boolean;
62+
error_message: string | null;
63+
}
64+
65+
export interface RunDetailData extends RunSummary {
66+
step_results: StepResultData[];
67+
}
68+
69+
export interface DashboardStats {
70+
tests: number;
71+
pass_rate_7d: number;
72+
runs_today: number;
73+
ai_spend: number;
74+
}
75+
76+
async function request<T>(path: string, options?: RequestInit): Promise<T> {
77+
const url = `${API_BASE}${path}`;
78+
const response = await fetch(url, {
79+
...options,
80+
headers: {
81+
'Content-Type': 'application/json',
82+
...(options?.headers || {}),
83+
},
84+
});
85+
86+
if (!response.ok) {
87+
let message = `API request failed with status ${response.status}`;
88+
try {
89+
const errData = await response.json();
90+
message = errData.detail || errData.message || message;
91+
} catch {
92+
// Ignore
93+
}
94+
throw new Error(message);
95+
}
96+
97+
// Handle empty bodies (e.g. DELETE or updates)
98+
if (response.status === 204) {
99+
return {} as T;
100+
}
101+
102+
return response.json();
103+
}
104+
105+
export const api = {
106+
getSettings: () => request<SettingData>('/settings'),
107+
updateSettings: (data: Partial<SettingData> & { api_key?: string }) =>
108+
request<{ status: string; message: string }>('/settings', {
109+
method: 'PUT',
110+
body: JSON.stringify(data),
111+
}),
112+
testConnection: (data: { provider: string; model: string; api_key: string }) =>
113+
request<{ ok: boolean; message: string }>('/settings/test-connection', {
114+
method: 'POST',
115+
body: JSON.stringify(data),
116+
}),
117+
118+
listTests: () => request<TestData[]>('/tests'),
119+
createTest: (data: { url: string; name?: string }) =>
120+
request<{ id: number; name: string; url: string }>('/tests', {
121+
method: 'POST',
122+
body: JSON.stringify(data),
123+
}),
124+
getTest: (id: number) => request<TestData>(`/tests/${id}`),
125+
updateTest: (id: number, data: { name?: string; steps?: Partial<StepData>[] }) =>
126+
request<{ status: string; message: string }>(`/tests/${id}`, {
127+
method: 'PUT',
128+
body: JSON.stringify(data),
129+
}),
130+
deleteTest: (id: number) =>
131+
request<{ status: string; message: string }>(`/tests/${id}`, {
132+
method: 'DELETE',
133+
}),
134+
recordTest: (id: number) =>
135+
request<{ status: string }>([/tests/, id, '/record'].join(''), {
136+
method: 'POST',
137+
}),
138+
getRecordStatus: (id: number) =>
139+
request<{ recording: boolean; step_count: number }>(`/tests/${id}/record/status`),
140+
141+
triggerRun: (test_id: number) =>
142+
request<{ run_id: number; status: string }>(`/tests/${test_id}/run`, {
143+
method: 'POST',
144+
}),
145+
listRuns: (test_id?: number, status?: string) => {
146+
const params = new URLSearchParams();
147+
if (test_id) params.append('test_id', String(test_id));
148+
if (status) params.append('status', status);
149+
return request<RunSummary[]>(`/runs?${params.toString()}`);
150+
},
151+
getRun: (run_id: number) => request<RunDetailData>(`/runs/${run_id}`),
152+
153+
getStats: () => request<DashboardStats>('/stats'),
154+
};

studio/frontend/src/pages/RunDetail.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,10 @@ export default function RunDetail({ runId, onNavigate }: RunDetailProps) {
3535
useEffect(() => {
3636
loadRun();
3737

38-
// Establish WebSocket for live updates
39-
const wsUrl = `${API_BASE.replace('http', 'ws')}/runs/${runId}/live`;
38+
const loc = window.location;
39+
const wsProto = loc.protocol === 'https:' ? 'wss:' : 'ws:';
40+
const wsHost = API_BASE ? API_BASE.replace(/^http(s)?:\/\//, '').replace(/\/$/, '') : loc.host;
41+
const wsUrl = `${wsProto}//${wsHost}/runs/${runId}/live`;
4042
const socket = new WebSocket(wsUrl);
4143
socketRef.current = socket;
4244

studio/frontend/src/pages/TestDetail.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,8 @@ export default function TestDetail({ testId, onNavigate }: TestDetailProps) {
5454
};
5555

5656
const handleExport = () => {
57-
window.open(`${api.exportPytest(testId)}`); // Trigger standard download
58-
// Or we can create an anchor tag and click it to download:
5957
const a = document.createElement('a');
60-
a.href = `http://127.0.0.1:8000/tests/${testId}/export`;
58+
a.href = `/tests/${testId}/export`;
6159
a.download = `test_${testId}.py`;
6260
document.body.appendChild(a);
6361
a.click();

studio/frontend/vite.config.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,16 @@ export default defineConfig({
99
tailwindcss(),
1010
],
1111
server: {
12-
port: 30001,
12+
port: 5173,
1313
strictPort: true,
14+
// Proxy all API calls to the FastAPI backend — avoids CORS in dev
15+
proxy: {
16+
'/tests': { target: 'http://127.0.0.1:8000', changeOrigin: true },
17+
'/runs': { target: 'http://127.0.0.1:8000', changeOrigin: true, ws: true },
18+
'/settings': { target: 'http://127.0.0.1:8000', changeOrigin: true },
19+
'/stats': { target: 'http://127.0.0.1:8000', changeOrigin: true },
20+
'/artifacts': { target: 'http://127.0.0.1:8000', changeOrigin: true },
21+
'/health': { target: 'http://127.0.0.1:8000', changeOrigin: true },
22+
}
1423
}
1524
})

0 commit comments

Comments
 (0)