dashboard-v2 hotfix: relative asset base so /clawstor mount works
Build with clawstor cache / Cargo build (clawstor-cached) (pull_request) Failing after 2s
Build with clawstor cache / Cargo build (clawstor-cached) (pull_request) Failing after 2s
Browser hit /clawstor/ then requested /v2/assets/index-XXX.css (absolute path baked in by vite base '/v2/') which 404'd because Tailscale Serve only mounted /clawstor and /clawstor/api. Switch vite base to './' — assets resolve relative to whatever URL the SPA loaded from. Works for local (:7700/v2/) + Tailscale (/clawstor) with no config coupling.
This commit is contained in:
+27
-11
@@ -1,22 +1,38 @@
|
||||
// Thin fetch wrappers around /api/v2/*. Kept minimal — no state
|
||||
// library; each page owns its own useEffect + useState.
|
||||
// The backend runs on the same origin the SPA loads from, so
|
||||
// absolute URLs are unnecessary. Dev proxy handles the :5173 →
|
||||
// :7700 hop.
|
||||
// API base:
|
||||
// * dev (vite proxy) → '/api'
|
||||
// * prod local → '/api' (SPA at :7700/v2/, API at :7700/api/)
|
||||
// * prod via Tailscale → '/clawstor/api' (SPA at /clawstor, API at /clawstor/api)
|
||||
//
|
||||
// Detect at load time by checking the current URL's pathname
|
||||
// prefix. Cheap + no build-time coupling.
|
||||
const API_BASE = (() => {
|
||||
if (typeof window === 'undefined')
|
||||
return '/api';
|
||||
const p = window.location.pathname;
|
||||
if (p.startsWith('/clawstor/') || p === '/clawstor')
|
||||
return '/clawstor/api';
|
||||
return '/api';
|
||||
})();
|
||||
async function get(path) {
|
||||
const resp = await fetch(path, { headers: { Accept: 'application/json' } });
|
||||
const full = `${API_BASE}${path}`;
|
||||
const resp = await fetch(full, { headers: { Accept: 'application/json' } });
|
||||
if (!resp.ok) {
|
||||
throw new Error(`${path} → ${resp.status} ${resp.statusText}`);
|
||||
throw new Error(`${full} → ${resp.status} ${resp.statusText}`);
|
||||
}
|
||||
return resp.json();
|
||||
}
|
||||
// All paths are relative to API_BASE. E.g. '/v2/fleet' becomes
|
||||
// '/api/v2/fleet' locally or '/clawstor/api/v2/fleet' via Tailscale.
|
||||
export const api = {
|
||||
nodeStatus: (name = 'local') => get(`/api/v2/node/${name}/status`),
|
||||
blobs: (limit = 200, offset = 0) => get(`/api/v2/storage/blobs?limit=${limit}&offset=${offset}`),
|
||||
tags: (prefix = '') => get(`/api/v2/storage/tags?prefix=${encodeURIComponent(prefix)}`),
|
||||
refs: (limit = 200, offset = 0) => get(`/api/v2/storage/refs?limit=${limit}&offset=${offset}`),
|
||||
snapshots: () => get(`/api/v2/storage/snapshots`),
|
||||
refTracking: (repo = '') => get(`/api/v2/storage/ref-tracking?repo=${encodeURIComponent(repo)}`),
|
||||
fleet: () => get('/v2/fleet'),
|
||||
nodeStatus: (name) => get(`/v2/node/${name}/status`),
|
||||
blobs: (limit = 200, offset = 0) => get(`/v2/storage/blobs?limit=${limit}&offset=${offset}`),
|
||||
tags: (prefix = '') => get(`/v2/storage/tags?prefix=${encodeURIComponent(prefix)}`),
|
||||
refs: (limit = 200, offset = 0) => get(`/v2/storage/refs?limit=${limit}&offset=${offset}`),
|
||||
snapshots: () => get('/v2/storage/snapshots'),
|
||||
refTracking: (repo = '') => get(`/v2/storage/ref-tracking?repo=${encodeURIComponent(repo)}`),
|
||||
};
|
||||
/** Format bytes as MB / GB / TB as needed. */
|
||||
export function fmtBytes(n) {
|
||||
|
||||
+36
-11
@@ -3,6 +3,7 @@
|
||||
|
||||
export interface NodeStatusV2 {
|
||||
node_name: string;
|
||||
zone: string;
|
||||
blob_store_root: string | null;
|
||||
blob_count: number;
|
||||
tag_count: number;
|
||||
@@ -10,6 +11,15 @@ export interface NodeStatusV2 {
|
||||
snapshot_count: number;
|
||||
ref_tracking_count: number;
|
||||
blob_store_bytes: number;
|
||||
rustc_release: string | null;
|
||||
online: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export interface FleetSnapshot {
|
||||
aggregator_name: string;
|
||||
fetched_at_unix: number;
|
||||
nodes: NodeStatusV2[];
|
||||
}
|
||||
|
||||
export interface BlobSummary {
|
||||
@@ -43,28 +53,43 @@ export interface RefTrackingItem {
|
||||
last_seen_unix: number;
|
||||
}
|
||||
|
||||
// The backend runs on the same origin the SPA loads from, so
|
||||
// absolute URLs are unnecessary. Dev proxy handles the :5173 →
|
||||
// :7700 hop.
|
||||
// API base:
|
||||
// * dev (vite proxy) → '/api'
|
||||
// * prod local → '/api' (SPA at :7700/v2/, API at :7700/api/)
|
||||
// * prod via Tailscale → '/clawstor/api' (SPA at /clawstor, API at /clawstor/api)
|
||||
//
|
||||
// Detect at load time by checking the current URL's pathname
|
||||
// prefix. Cheap + no build-time coupling.
|
||||
const API_BASE = (() => {
|
||||
if (typeof window === 'undefined') return '/api';
|
||||
const p = window.location.pathname;
|
||||
if (p.startsWith('/clawstor/') || p === '/clawstor') return '/clawstor/api';
|
||||
return '/api';
|
||||
})();
|
||||
|
||||
async function get<T>(path: string): Promise<T> {
|
||||
const resp = await fetch(path, { headers: { Accept: 'application/json' } });
|
||||
const full = `${API_BASE}${path}`;
|
||||
const resp = await fetch(full, { headers: { Accept: 'application/json' } });
|
||||
if (!resp.ok) {
|
||||
throw new Error(`${path} → ${resp.status} ${resp.statusText}`);
|
||||
throw new Error(`${full} → ${resp.status} ${resp.statusText}`);
|
||||
}
|
||||
return resp.json();
|
||||
}
|
||||
|
||||
// All paths are relative to API_BASE. E.g. '/v2/fleet' becomes
|
||||
// '/api/v2/fleet' locally or '/clawstor/api/v2/fleet' via Tailscale.
|
||||
export const api = {
|
||||
nodeStatus: (name = 'local') => get<NodeStatusV2>(`/api/v2/node/${name}/status`),
|
||||
fleet: () => get<FleetSnapshot>('/v2/fleet'),
|
||||
nodeStatus: (name: string) => get<NodeStatusV2>(`/v2/node/${name}/status`),
|
||||
blobs: (limit = 200, offset = 0) =>
|
||||
get<BlobSummary[]>(`/api/v2/storage/blobs?limit=${limit}&offset=${offset}`),
|
||||
get<BlobSummary[]>(`/v2/storage/blobs?limit=${limit}&offset=${offset}`),
|
||||
tags: (prefix = '') =>
|
||||
get<TagSummary[]>(`/api/v2/storage/tags?prefix=${encodeURIComponent(prefix)}`),
|
||||
get<TagSummary[]>(`/v2/storage/tags?prefix=${encodeURIComponent(prefix)}`),
|
||||
refs: (limit = 200, offset = 0) =>
|
||||
get<RefSummary[]>(`/api/v2/storage/refs?limit=${limit}&offset=${offset}`),
|
||||
snapshots: () => get<SnapshotSummary[]>(`/api/v2/storage/snapshots`),
|
||||
get<RefSummary[]>(`/v2/storage/refs?limit=${limit}&offset=${offset}`),
|
||||
snapshots: () => get<SnapshotSummary[]>('/v2/storage/snapshots'),
|
||||
refTracking: (repo = '') =>
|
||||
get<RefTrackingItem[]>(`/api/v2/storage/ref-tracking?repo=${encodeURIComponent(repo)}`),
|
||||
get<RefTrackingItem[]>(`/v2/storage/ref-tracking?repo=${encodeURIComponent(repo)}`),
|
||||
};
|
||||
|
||||
/** Format bytes as MB / GB / TB as needed. */
|
||||
|
||||
Reference in New Issue
Block a user