Two unrelated tweaks grouped into one PR to keep churn low. ## Ports Backend 8000 → 3900, Vite dev 5173 → 3901, 3902 reserved for future IPC. Port 8000 conflicts with Django/Rails/Jupyter/Airflow on most dev machines; the uncommon 3900 range dodges that. Touched: - frontend/src-tauri/src/lib.rs (BACKEND_PORT) - frontend/src-tauri/tauri.conf.json (devUrl) - frontend/vite.config.js (server.port) - frontend/src/api/client.ts (hardcoded API base) - frontend/src/App.jsx (PREVIEW_API fallback) - backend/main.py (CORS allowlist + uvicorn.run default) Rust sidecar launcher and FastAPI uvicorn port stay in sync via the `BACKEND_PORT` constant + explicit port=3900. ## CI caches Build time shaves across ci.yml and release.yml: - `astral-sh/setup-uv@v3` → `enable-cache: true` keyed on uv.lock (~45 s saved per run after uv.lock stabilises) - `awalsh128/cache-apt-pkgs-action` for ffmpeg (~25 s saved) - `actions/cache@v4` on `~/.bun/install/cache` keyed on bun.lock (~15 s saved; applied to both test gate and build matrix) Expected warm test job: ~45-60 s (was ~2-3 min). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
66 lines
2.2 KiB
TypeScript
66 lines
2.2 KiB
TypeScript
// Backend always listens on localhost:3900 — both in dev (Vite @ 3901 talking
|
|
// to a separate uvicorn) and in the built .app (Tauri webview @ tauri://localhost
|
|
// talking to the venv-bootstrapped sidecar). Relative fetches against
|
|
// tauri://localhost don't reach the sidecar, so we hardcode the absolute host.
|
|
// Port 3900 chosen to avoid common 8000 conflicts (Django/Rails/Jupyter).
|
|
export const API = 'http://localhost:3900';
|
|
|
|
export class ApiError extends Error {
|
|
status?: number;
|
|
detail?: unknown;
|
|
constructor(message: string, init: { status?: number; detail?: unknown } = {}) {
|
|
super(message);
|
|
this.name = 'ApiError';
|
|
this.status = init.status;
|
|
this.detail = init.detail;
|
|
}
|
|
}
|
|
|
|
export function apiUrl(path?: string): string {
|
|
if (!path) return API;
|
|
return path.startsWith('http') ? path : `${API}${path.startsWith('/') ? '' : '/'}${path}`;
|
|
}
|
|
|
|
async function readError(res: Response): Promise<string> {
|
|
const text = await res.text().catch(() => '');
|
|
try {
|
|
const j = JSON.parse(text);
|
|
return j.detail || j.error || text || res.statusText;
|
|
} catch {
|
|
return text || res.statusText;
|
|
}
|
|
}
|
|
|
|
export async function apiFetch(path: string, opts: RequestInit = {}): Promise<Response> {
|
|
const res = await fetch(apiUrl(path), opts);
|
|
if (!res.ok) {
|
|
const detail = await readError(res);
|
|
throw new ApiError(`${res.status} ${res.statusText}: ${detail}`, { status: res.status, detail });
|
|
}
|
|
return res;
|
|
}
|
|
|
|
export async function apiJson<T = unknown>(path: string, opts: RequestInit = {}): Promise<T> {
|
|
const res = await apiFetch(path, opts);
|
|
return res.json() as Promise<T>;
|
|
}
|
|
|
|
export async function apiPost<T = unknown>(
|
|
path: string,
|
|
body?: unknown,
|
|
opts: RequestInit = {},
|
|
): Promise<T> {
|
|
const init: RequestInit = { method: 'POST', ...opts };
|
|
if (body instanceof FormData) {
|
|
init.body = body;
|
|
} else if (body !== undefined) {
|
|
init.headers = { 'Content-Type': 'application/json', ...(opts.headers as Record<string, string> || {}) };
|
|
init.body = JSON.stringify(body);
|
|
}
|
|
return apiJson<T>(path, init);
|
|
}
|
|
|
|
export async function apiDelete(path: string, opts: RequestInit = {}): Promise<Response> {
|
|
return apiFetch(path, { method: 'DELETE', ...opts });
|
|
}
|