refactor(gui): centralize API origin handling
Some checks are pending
Security Scan / security (push) Waiting to run
Test / test (push) Waiting to run

This commit is contained in:
Virgil 2026-04-02 20:12:35 +00:00
parent b50149af5d
commit dd9e8da619
5 changed files with 58 additions and 26 deletions

View file

@ -165,7 +165,7 @@ export class DashboardComponent {
protected readonly providers = this.discovery.providers;
protected readonly providerCount = computed(() => this.providers().length);
protected readonly connected = this.websocket.connected;
protected readonly apiBase = computed(() => this.apiConfig.baseUrl || window.location.origin);
protected readonly apiBase = computed(() => this.apiConfig.effectiveBaseUrl);
protected readonly featuredProviders = computed<ProviderInfo[]>(() =>
this.providers().filter((provider) => provider.element?.tag).slice(0, 6),

View file

@ -110,7 +110,7 @@ export class ProviderHostComponent implements OnInit, OnChanges {
// Create and append the custom element
const el = this.renderer.createElement(this.tag);
const url = this.apiUrl || this.apiConfig.baseUrl;
const url = this.apiUrl || this.apiConfig.effectiveBaseUrl;
if (url) {
this.renderer.setAttribute(el, 'api-url', url);
}

View file

@ -16,6 +16,11 @@ export class ApiConfigService {
return this._baseUrl;
}
/** The effective API base URL, falling back to the current origin. */
get effectiveBaseUrl(): string {
return this._baseUrl || window.location.origin;
}
/** Override the base URL. Strips trailing slash if present. */
set baseUrl(url: string) {
this._baseUrl = url.replace(/\/+$/, '');
@ -24,6 +29,6 @@ export class ApiConfigService {
/** Build a full URL for the given path. */
url(path: string): string {
const cleanPath = path.startsWith('/') ? path : `/${path}`;
return `${this._baseUrl}${cleanPath}`;
return `${this.effectiveBaseUrl}${cleanPath}`;
}
}

View file

@ -33,42 +33,69 @@ export class ProviderDiscoveryService {
readonly providers = this._providers.asReadonly();
private discovered = false;
private discoveryPromise: Promise<void> | null = null;
private discoveryGeneration = 0;
constructor(private apiConfig: ApiConfigService) {}
/** Fetch providers from the API and load custom element scripts. */
async discover(): Promise<void> {
if (this.discovered) {
async discover(force = false): Promise<void> {
if (!force && this.discovered) {
return;
}
if (!force && this.discoveryPromise) {
return this.discoveryPromise;
}
const generation = ++this.discoveryGeneration;
const promise = this.doDiscover(generation);
this.discoveryPromise = promise;
try {
const res = await fetch(this.apiConfig.url('/api/v1/providers'));
if (!res.ok) {
console.warn('ProviderDiscoveryService: failed to fetch providers:', res.statusText);
return;
}
const data = await res.json();
const providers: ProviderInfo[] = data.providers ?? [];
this._providers.set(providers);
this.discovered = true;
// Load custom elements for Renderable providers
for (const p of providers) {
if (p.element?.tag && p.element?.source) {
await this.loadElement(p.element.tag, p.element.source);
}
}
await promise;
} catch (err) {
console.warn('ProviderDiscoveryService: discovery failed:', err);
} finally {
if (this.discoveryPromise === promise) {
this.discoveryPromise = null;
}
}
}
/** Refresh the provider list (force re-discovery). */
async refresh(): Promise<void> {
this.discovered = false;
await this.discover();
await this.discover(true);
}
private async doDiscover(generation: number): Promise<void> {
const res = await fetch(this.apiConfig.url('/api/v1/providers'));
if (!res.ok) {
console.warn('ProviderDiscoveryService: failed to fetch providers:', res.statusText);
return;
}
const data = await res.json();
const providers: ProviderInfo[] = data.providers ?? [];
if (generation !== this.discoveryGeneration) {
return;
}
this._providers.set(providers);
this.discovered = true;
// Load custom elements for renderable providers.
for (const provider of providers) {
if (generation !== this.discoveryGeneration) {
return;
}
if (provider.element?.tag && provider.element?.source) {
await this.loadElement(provider.element.tag, provider.element.source);
}
}
}
/** Dynamically load a custom element script if not already registered. */

View file

@ -32,10 +32,10 @@ export class WebSocketService implements OnDestroy {
}
this.shouldReconnect = true;
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const base = this.apiConfig.baseUrl || window.location.origin;
const cleanPath = path.startsWith('/') ? path : `/${path}`;
const base = this.apiConfig.effectiveBaseUrl;
const wsBase = base.replace(/^http/, 'ws');
const url = `${wsBase.length > 0 ? wsBase : `${protocol}//${window.location.host}`}${path}`;
const url = `${wsBase}${cleanPath}`;
this.ws = new WebSocket(url);