export const DEFAULT_PACKAGE_BASE_URL = normalizeUrl( import.meta.env.VITE_PACKAGE_BASE_URL || window.location.origin ); export const DEFAULT_AGENT_BASE_URL = normalizeUrl( import.meta.env.VITE_AGENT_BASE_URL || 'http://127.0.0.1:5010' ); export const DEFAULT_APP_OPEN_URL = normalizeUrl( import.meta.env.VITE_APP_OPEN_URL || 'http://127.0.0.1' ); export function normalizeUrl(value) { const text = String(value || '').trim(); return text.replace(/\/+$/, ''); } export function joinUrl(baseUrl, path) { const normalizedBaseUrl = normalizeUrl(baseUrl); const normalizedPath = String(path || '').startsWith('/') ? path : `/${path || ''}`; return `${normalizedBaseUrl}${normalizedPath}`; } function normalizeOpenUrl(value) { const text = normalizeUrl(value); if (!text) return ''; try { const parsed = new URL(text); return parsed.protocol === 'http:' || parsed.protocol === 'https:' ? text : ''; } catch { return ''; } } export function getAppOpenUrl(app) { return normalizeOpenUrl( app?.openUrl || app?.open_url || app?.webUrl || app?.web_url || app?.homepageUrl || app?.homepage_url || app?.homepage ) || normalizeOpenUrl(DEFAULT_APP_OPEN_URL); } async function requestJson(baseUrl, path, options = {}) { const { timeoutMs = 8000, body, headers, ...fetchOptions } = options; const url = joinUrl(baseUrl, path); const controller = new AbortController(); const timeout = window.setTimeout(() => controller.abort(), timeoutMs); try { const response = await fetch(url, { ...fetchOptions, headers: { Accept: 'application/json', ...(body ? { 'Content-Type': 'application/json' } : {}), ...headers }, body: body ? JSON.stringify(body) : undefined, signal: controller.signal }); const text = await response.text(); let payload = null; if (text) { try { payload = JSON.parse(text); } catch { payload = text; } } if (!response.ok) { throw new Error(`${response.status} ${formatErrorDetail(payload || response.statusText)}`); } return payload; } catch (error) { if (error?.name === 'AbortError') { throw new Error(`Request timeout: ${url}`); } if (error instanceof TypeError) { throw new Error(`Cannot fetch ${url}. Check endpoint reachability and CORS for this Web Client origin.`); } throw error; } finally { window.clearTimeout(timeout); } } function formatErrorDetail(detail) { if (Array.isArray(detail)) { return detail.map(formatErrorDetail).filter(Boolean).join('; '); } if (detail && typeof detail === 'object') { const location = Array.isArray(detail.loc) ? detail.loc.join('.') : ''; const messageParts = [ detail.msg, detail.message, detail.error, detail.detail ] .map((item) => String(item || '').trim()) .filter(Boolean); if (Array.isArray(detail.missingPackageFiles) && detail.missingPackageFiles.length > 0) { const missingFiles = detail.missingPackageFiles .map(formatMissingPackageFile) .filter(Boolean) .join('; '); if (missingFiles) { messageParts.push(`Missing package files: ${missingFiles}`); } } const message = [...new Set(messageParts)].join('. '); if (message) { return location ? `${location}: ${message}` : String(message); } try { return JSON.stringify(detail); } catch { return String(detail); } } return String(detail || 'Request failed'); } function formatMissingPackageFile(item) { if (!item || typeof item !== 'object') return String(item || '').trim(); const packageName = String(item.packageName || item.componentId || 'package').trim(); const version = String(item.version || '').trim(); const downloadUrl = String(item.downloadUrl || '').trim(); const label = [packageName, version].filter(Boolean).join(' '); return downloadUrl ? `${label} (${downloadUrl})` : label; } export async function fetchPackageApps(packageBaseUrl) { const payload = await requestJson(packageBaseUrl, '/api/apps', { timeoutMs: 10000 }); return Array.isArray(payload?.apps) ? payload.apps.map(normalizePackageApp) : []; } export async function fetchLatestAgentPackage(packageBaseUrl, arch = 'amd64') { const query = arch ? `?arch=${encodeURIComponent(arch)}` : ''; return normalizeLatestAgentPackage( await requestJson(packageBaseUrl, `/api/agent/latest${query}`, { timeoutMs: 7000 }) ); } export async function fetchApplicationDetail(packageBaseUrl, appId) { return requestJson(packageBaseUrl, `/api/apps/${encodeURIComponent(appId)}`, { timeoutMs: 10000 }); } export async function fetchApplicationManifest(packageBaseUrl, appId, version) { return requestJson( packageBaseUrl, `/api/apps/${encodeURIComponent(appId)}/versions/${encodeURIComponent(version)}/manifest`, { timeoutMs: 10000 } ); } export async function fetchAgentHealth(agentBaseUrl) { return requestJson(agentBaseUrl, '/health', { timeoutMs: 2800 }); } export async function fetchAgentSystemInfo(agentBaseUrl) { return requestJson(agentBaseUrl, '/system-info', { timeoutMs: 5000 }); } export async function fetchInstalledApps(agentBaseUrl) { const payload = await requestJson(agentBaseUrl, '/apps/installed', { timeoutMs: 7000 }); return Array.isArray(payload) ? payload.map(normalizeInstalledApp) : []; } export async function queueInstall(agentBaseUrl, app) { return requestJson(agentBaseUrl, '/apps/install', { method: 'POST', timeoutMs: 10000, body: { appId: app.appId, appName: app.appName, version: app.version } }); } export async function queueUpdate(agentBaseUrl, app, installedApp) { return requestJson(agentBaseUrl, '/apps/update', { method: 'POST', timeoutMs: 10000, body: { appId: app.appId, appName: app.appName, currentVersion: installedApp?.version || '', targetVersion: app.version } }); } export async function queueRemove(agentBaseUrl, app) { return requestJson(agentBaseUrl, '/apps/remove', { method: 'POST', timeoutMs: 10000, body: { appId: app.appId, purge: false } }); } export async function fetchTaskStatus(agentBaseUrl, taskId) { return normalizeTask(await requestJson(agentBaseUrl, `/tasks/${encodeURIComponent(taskId)}`, { timeoutMs: 7000 })); } export async function fetchTaskLogs(agentBaseUrl, taskId) { const payload = await requestJson(agentBaseUrl, `/tasks/${encodeURIComponent(taskId)}/logs`, { timeoutMs: 7000 }); return Array.isArray(payload?.logs) ? payload.logs.map(normalizeLog) : []; } export async function fetchTaskComponents(agentBaseUrl, taskId) { const payload = await requestJson(agentBaseUrl, `/tasks/${encodeURIComponent(taskId)}/components`, { timeoutMs: 7000 }); return Array.isArray(payload?.components) ? payload.components.map(normalizeComponent) : []; } function normalizePackageApp(app) { return { appId: String(app.appId || app.app_id || app.id || '').trim(), appCode: String(app.appCode || app.app_code || app.code || app.appId || app.app_id || '').trim(), appName: String(app.appName || app.app_name || app.name || '').trim(), version: String(app.version || '').trim(), status: String(app.status || 'Released').trim(), packageCount: Number(app.packageCount || app.package_count || 0), openUrl: normalizeOpenUrl( app.openUrl || app.open_url || app.webUrl || app.web_url || app.homepageUrl || app.homepage_url || app.homepage ) }; } function normalizeInstalledApp(app) { return { appId: String(app.appId || app.app_id || '').trim(), appName: String(app.appName || app.app_name || '').trim(), version: String(app.installedVersion || app.version || app.package_version || '').trim(), status: String(app.status || 'installed').trim(), installedAt: app.installedAt || app.installed_at || '', updatedAt: app.updatedAt || app.updated_at || '', openUrl: normalizeOpenUrl( app.openUrl || app.open_url || app.webUrl || app.web_url || app.homepageUrl || app.homepage_url || app.homepage ) }; } function normalizeLatestAgentPackage(agentPackage) { return { version: String(agentPackage?.version || '').trim(), arch: String(agentPackage?.arch || '').trim(), fileName: String(agentPackage?.fileName || agentPackage?.file_name || '').trim(), sizeLabel: String(agentPackage?.sizeLabel || agentPackage?.size_label || '').trim(), downloadUrl: String(agentPackage?.downloadUrl || agentPackage?.download_url || '').trim(), installCommand: String(agentPackage?.installCommand || agentPackage?.install_command || '').trim() }; } function normalizeTask(task) { return { taskId: task.taskId || task.task_id, type: task.type, appId: task.appId || task.app_id, appName: task.appName || task.app_name, status: task.status, progress: Number(task.progress || 0), currentStep: task.currentStep || task.current_step, currentComponentId: task.currentComponentId || task.current_component_id, errorMessage: task.errorMessage || task.error_message, createdAt: task.createdAt || task.created_at, startedAt: task.startedAt || task.started_at, finishedAt: task.finishedAt || task.finished_at }; } function normalizeLog(log) { return { time: log.time || log.timestamp || '', level: log.level || 'info', message: log.message || '' }; } function normalizeComponent(component) { return { componentId: component.componentId || component.component_id, type: component.type, status: component.status, progress: Number(component.progress || 0), currentStep: component.currentStep || component.current_step, errorMessage: component.errorMessage || component.error_message, startedAt: component.startedAt || component.started_at, finishedAt: component.finishedAt || component.finished_at }; }