From 24f84442e894015e76bf555c41a915c1c1aa1048 Mon Sep 17 00:00:00 2001 From: davidmonterocrespo24 Date: Thu, 21 May 2026 04:50:19 +0200 Subject: [PATCH] feat(frontend): runtime API base + desktop overlay extension points Adds `lib/apiBase.ts` so the SPA can be repointed at a non-default backend at runtime (via `window.__VELXIO_API_BASE__`) without losing the existing `VITE_API_BASE` build-time override or the default `/api` reverse-proxy behaviour. compilation / libraryService / projectService / metricsService all flow through it now; axios clients use a request interceptor so the base resolves per-request rather than at module-load time. main.tsx grows a `VITE_DESKTOP` flag: when set, the @pro overlay is skipped (the desktop shell handles license + auth natively) and a small `./desktop/index` module is dynamic-imported in its place. OSS builds tree-shake both branches. LandingPage gets a `data-velxio-slot="landing-hero-primary-cta"` marker above the existing hero CTAs so velxio.dev can inject an OS-detect "Download Velxio Desktop" button as the visual primary. The slot is empty in pure OSS. Co-Authored-By: Claude Opus 4.7 (1M context) --- frontend/src/lib/apiBase.ts | 37 +++++++++++++++++++++++++ frontend/src/main.tsx | 16 ++++++++++- frontend/src/pages/LandingPage.tsx | 6 ++++ frontend/src/services/compilation.ts | 9 +++--- frontend/src/services/libraryService.ts | 11 ++++---- frontend/src/services/metricsService.ts | 9 ++++-- frontend/src/services/projectService.ts | 11 ++++++-- 7 files changed, 82 insertions(+), 17 deletions(-) create mode 100644 frontend/src/lib/apiBase.ts diff --git a/frontend/src/lib/apiBase.ts b/frontend/src/lib/apiBase.ts new file mode 100644 index 00000000..704eef3f --- /dev/null +++ b/frontend/src/lib/apiBase.ts @@ -0,0 +1,37 @@ +/** + * Resolve the base URL of the velxio FastAPI backend at runtime. + * + * Two layers of override: + * + * 1. `window.__VELXIO_API_BASE__` — set by a thin wrapper that hosts + * the SPA against a non-default backend (e.g. the Tauri desktop + * shell injects this before the bundle runs, pointing at the + * locally spawned Python sidecar on `http://127.0.0.1:`). + * 2. `import.meta.env.VITE_API_BASE` — set at build time. Used by + * bespoke deployments that want a fixed backend URL baked in. + * 3. Default `/api` — the standard same-origin reverse-proxy setup + * that velxio.dev and the OSS Docker image use. + * + * Resolved on every call rather than memoised so a host can swap the + * window var late (e.g. on a sidecar restart). The lookup is cheap. + */ + +export function getApiBase(): string { + if (typeof window !== 'undefined') { + const w = window as { __VELXIO_API_BASE__?: string }; + if (typeof w.__VELXIO_API_BASE__ === 'string' && w.__VELXIO_API_BASE__) { + return w.__VELXIO_API_BASE__.replace(/\/+$/, ''); + } + } + const fromEnv = import.meta.env.VITE_API_BASE; + if (typeof fromEnv === 'string' && fromEnv) { + return fromEnv.replace(/\/+$/, ''); + } + return '/api'; +} + +declare global { + interface Window { + __VELXIO_API_BASE__?: string; + } +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index cb6f974a..48fcb845 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -29,8 +29,22 @@ createRoot(document.getElementById('root')!).render(); // open-source build (see vite.config.ts) and to the real overlay only when // VITE_PRO_BUILD=true at build time. The dynamic import keeps the pro chunk // out of the OSS bundle entirely (Vite tree-shakes the never-taken branch). -if (import.meta.env.VITE_PRO_BUILD) { +// +// VITE_DESKTOP=true is set by the Tauri desktop build. The desktop shell +// owns its own license + auth UI (Phase 3 of paid-clients) and runs against +// a locally spawned sidecar, so the velxio.dev-coupled overlay (trackers, +// billing, cloud auth, admin) is intentionally NOT loaded — even if a +// build accidentally sets both flags. +if (import.meta.env.VITE_PRO_BUILD && !import.meta.env.VITE_DESKTOP) { import('@pro/index') .then((m) => m.mountPro?.()) .catch((err) => console.warn('[pro] failed to load overlay:', err)); } + +// Desktop-only hooks (ESP32 QEMU prompt now, welcome screen in Phase 3). +// Dynamic import so the OSS bundle never pulls this in. +if (import.meta.env.VITE_DESKTOP) { + import('./desktop/index') + .then((m) => m.mountDesktop?.()) + .catch((err) => console.warn('[desktop] failed to load hooks:', err)); +} diff --git a/frontend/src/pages/LandingPage.tsx b/frontend/src/pages/LandingPage.tsx index b03dc96d..130053c8 100644 --- a/frontend/src/pages/LandingPage.tsx +++ b/frontend/src/pages/LandingPage.tsx @@ -680,6 +680,12 @@ export const LandingPage: React.FC = () => { {t('landing.hero.titleAccent')}

{t('landing.hero.subtitle')}

+ {/* + Slot for the pro overlay's OS-detect Velxio Desktop download + CTA. Pure OSS leaves it empty; velxio.dev mounts a + DesktopDownloadButton here as the visual primary. + */} +
{ - console.log('Sending compilation request to:', `${API_BASE}/compile/start`); + console.log('Sending compilation request to:', `${getApiBase()}/compile/start`); console.log('Board:', board); console.log( 'Files:', @@ -100,7 +99,7 @@ export async function compileCode( let jobId: string; try { const startResp = await axios.post( - `${API_BASE}/compile/start`, + `${getApiBase()}/compile/start`, { files, board_fqbn: board, @@ -139,7 +138,7 @@ export async function compileCode( let status: CompileStatusResponse; try { const resp = await axios.get( - `${API_BASE}/compile/status/${jobId}`, + `${getApiBase()}/compile/status/${jobId}`, { withCredentials: true, timeout: 30000 }, ); status = resp.data; diff --git a/frontend/src/services/libraryService.ts b/frontend/src/services/libraryService.ts index 4f7b3f75..b314544c 100644 --- a/frontend/src/services/libraryService.ts +++ b/frontend/src/services/libraryService.ts @@ -1,4 +1,5 @@ -const API_BASE = `${import.meta.env.VITE_API_BASE || '/api'}/libraries`; +import { getApiBase } from '../lib/apiBase'; +const apiBase = () => `${getApiBase()}/libraries`; export interface ArduinoLibrary { name: string; @@ -34,7 +35,7 @@ export interface InstalledLibrary { } export async function searchLibraries(query: string): Promise { - const res = await fetch(`${API_BASE}/search?q=${encodeURIComponent(query)}`); + const res = await fetch(`${apiBase()}/search?q=${encodeURIComponent(query)}`); if (!res.ok) { const err = await res.json().catch(() => ({ detail: 'Unknown error' })); throw new Error(err.detail || 'Failed to search libraries'); @@ -44,7 +45,7 @@ export async function searchLibraries(query: string): Promise } export async function installLibrary(name: string, version?: string): Promise<{ success: boolean; error?: string; fallback?: boolean; requested_version?: string }> { - const res = await fetch(`${API_BASE}/install`, { + const res = await fetch(`${apiBase()}/install`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, version: version ?? null }), @@ -54,7 +55,7 @@ export async function installLibrary(name: string, version?: string): Promise<{ } export async function uninstallLibrary(name: string): Promise<{ success: boolean; error?: string }> { - const res = await fetch(`${API_BASE}/uninstall`, { + const res = await fetch(`${apiBase()}/uninstall`, { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name }), @@ -64,7 +65,7 @@ export async function uninstallLibrary(name: string): Promise<{ success: boolean } export async function getInstalledLibraries(): Promise { - const res = await fetch(`${API_BASE}/list`); + const res = await fetch(`${apiBase()}/list`); if (!res.ok) { const err = await res.json().catch(() => ({ detail: 'Unknown error' })); throw new Error(err.detail || 'Failed to fetch installed libraries'); diff --git a/frontend/src/services/metricsService.ts b/frontend/src/services/metricsService.ts index 0af022f1..9ada177d 100644 --- a/frontend/src/services/metricsService.ts +++ b/frontend/src/services/metricsService.ts @@ -1,8 +1,11 @@ import axios from 'axios'; +import { getApiBase } from '../lib/apiBase'; -const API_BASE = import.meta.env.VITE_API_BASE || '/api'; - -const api = axios.create({ baseURL: API_BASE, withCredentials: true }); +const api = axios.create({ withCredentials: true }); +api.interceptors.request.use((config) => { + config.baseURL = getApiBase(); + return config; +}); // ── Client-side tracking ───────────────────────────────────────────────────── diff --git a/frontend/src/services/projectService.ts b/frontend/src/services/projectService.ts index a76c86b8..b75714b4 100644 --- a/frontend/src/services/projectService.ts +++ b/frontend/src/services/projectService.ts @@ -1,8 +1,13 @@ import axios from 'axios'; +import { getApiBase } from '../lib/apiBase'; -const API_BASE = import.meta.env.VITE_API_BASE || '/api'; - -const api = axios.create({ baseURL: API_BASE, withCredentials: true }); +// baseURL is resolved on every request so a host (e.g. the Tauri desktop +// shell) can swap the backend port at runtime. +const api = axios.create({ withCredentials: true }); +api.interceptors.request.use((config) => { + config.baseURL = getApiBase(); + return config; +}); export interface SketchFile { name: string;