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) <noreply@anthropic.com>
This commit is contained in:
parent
be24243e4b
commit
24f84442e8
|
|
@ -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:<port>`).
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -29,8 +29,22 @@ createRoot(document.getElementById('root')!).render(<App />);
|
|||
// 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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -680,6 +680,12 @@ export const LandingPage: React.FC = () => {
|
|||
<span className="hero-accent">{t('landing.hero.titleAccent')}</span>
|
||||
</h1>
|
||||
<p className="hero-subtitle">{t('landing.hero.subtitle')}</p>
|
||||
{/*
|
||||
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.
|
||||
*/}
|
||||
<div data-velxio-slot="landing-hero-primary-cta" />
|
||||
<div className="hero-ctas">
|
||||
<Link
|
||||
to={localize('/editor')}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
import axios from 'axios';
|
||||
import { getApiBase } from '../lib/apiBase';
|
||||
import type { ESP32BoardOptions, SpiffsFile } from '../types/boardOptions';
|
||||
|
||||
const API_BASE = import.meta.env.VITE_API_BASE || '/api';
|
||||
|
||||
export interface SketchFile {
|
||||
name: string;
|
||||
content: string;
|
||||
|
|
@ -82,7 +81,7 @@ export async function compileCode(
|
|||
onProgress?: CompileProgress,
|
||||
extras?: CompileExtras,
|
||||
): Promise<CompileResult> {
|
||||
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<CompileStartResponse>(
|
||||
`${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<CompileStatusResponse>(
|
||||
`${API_BASE}/compile/status/${jobId}`,
|
||||
`${getApiBase()}/compile/status/${jobId}`,
|
||||
{ withCredentials: true, timeout: 30000 },
|
||||
);
|
||||
status = resp.data;
|
||||
|
|
|
|||
|
|
@ -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<ArduinoLibrary[]> {
|
||||
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<ArduinoLibrary[]>
|
|||
}
|
||||
|
||||
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<InstalledLibrary[]> {
|
||||
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');
|
||||
|
|
|
|||
|
|
@ -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 ─────────────────────────────────────────────────────
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue