const TOKEN_KEY = 'archnest_token' export function getToken(): string | null { return localStorage.getItem(TOKEN_KEY) } export function setToken(token: string | null) { if (token) localStorage.setItem(TOKEN_KEY, token) else localStorage.removeItem(TOKEN_KEY) } export class ApiError extends Error { status: number constructor(status: number, message: string) { super(message) this.status = status } } export async function apiFetch(path: string, options: RequestInit = {}): Promise { const token = getToken() const headers: Record = { ...(options.body ? { 'Content-Type': 'application/json' } : {}), ...(options.headers as Record | undefined), } if (token) headers.Authorization = `Bearer ${token}` const res = await fetch(`/api${path}`, { ...options, headers }) if (!res.ok) { let message = res.statusText try { const body = await res.json() message = body.error ?? message } catch { // ignore non-JSON error bodies } throw new ApiError(res.status, message) } if (res.status === 204) return undefined as T return res.json() as Promise } export const api = { getSetupStatus: () => apiFetch<{ needsSetup: boolean }>('/system/setup-status'), setup: (username: string, password: string) => apiFetch<{ token: string }>('/setup', { method: 'POST', body: JSON.stringify({ username, password }) }), login: (username: string, password: string) => apiFetch<{ token: string }>('/auth/login', { method: 'POST', body: JSON.stringify({ username, password }) }), me: () => apiFetch<{ user: AuthUser }>('/auth/me'), updateMe: (data: Partial<{ displayName: string | null; email: string | null; avatarDataUrl: string | null }>) => apiFetch<{ user: AuthUser }>('/auth/me', { method: 'PUT', body: JSON.stringify(data) }), listIntegrations: () => apiFetch<{ integrations: Integration[] }>('/integrations'), createIntegration: (data: { type: string; name: string; config?: Record; secrets?: Record }) => apiFetch<{ integration: Integration }>('/integrations', { method: 'POST', body: JSON.stringify(data) }), updateIntegration: (id: number, data: Partial<{ name: string; config: Record; secrets: Record }>) => apiFetch<{ integration: Integration }>(`/integrations/${id}`, { method: 'PUT', body: JSON.stringify(data) }), deleteIntegration: (id: number) => apiFetch(`/integrations/${id}`, { method: 'DELETE' }), testIntegration: (id: number) => apiFetch<{ ok: boolean; message: string }>(`/integrations/${id}/test`, { method: 'POST' }), listBookmarks: () => apiFetch<{ bookmarks: Bookmark[] }>('/bookmarks'), listBookmarkCategories: () => apiFetch<{ categories: BookmarkCategory[] }>('/bookmarks/categories'), createBookmarkCategory: (data: { name: string; icon?: string; sortOrder?: number }) => apiFetch<{ id: number }>('/bookmarks/categories', { method: 'POST', body: JSON.stringify(data) }), createBookmark: (data: { categoryId?: number | null; title: string; url: string; icon?: string; favorite?: boolean }) => apiFetch<{ id: number }>('/bookmarks', { method: 'POST', body: JSON.stringify(data) }), updateBookmark: (id: number, data: Partial<{ categoryId: number | null; title: string; url: string; icon: string; favorite: boolean }>) => apiFetch<{ ok: boolean }>(`/bookmarks/${id}`, { method: 'PUT', body: JSON.stringify(data) }), deleteBookmark: (id: number) => apiFetch(`/bookmarks/${id}`, { method: 'DELETE' }), listEvents: (limit = 20) => apiFetch<{ events: Event[] }>(`/events?limit=${limit}`), listResources: () => apiFetch<{ resources: Resource[] }>('/integrations/resources'), listTunnels: () => apiFetch<{ tunnels: Tunnel[] }>('/tunnels'), createTunnel: (data: { name: string integrationId: number mode: 'local' | 'remote' | 'dynamic' sourcePort: number endpointHost?: string endpointPort?: number autoStart?: boolean maxRetries?: number retryIntervalMs?: number }) => apiFetch<{ tunnel: Tunnel }>('/tunnels', { method: 'POST', body: JSON.stringify(data) }), deleteTunnel: (id: number) => apiFetch(`/tunnels/${id}`, { method: 'DELETE' }), connectTunnel: (id: number) => apiFetch<{ ok: boolean; status: string; error: string | null; retryCount: number }>(`/tunnels/${id}/connect`, { method: 'POST' }), disconnectTunnel: (id: number) => apiFetch<{ ok: boolean; status: string; error: string | null; retryCount: number }>(`/tunnels/${id}/disconnect`, { method: 'POST' }), } export interface AuthUser { id: number username: string display_name: string | null email: string | null avatar_data_url: string | null } export interface Integration { id: number type: string name: string enabled: boolean status: string config: Record lastCheckedAt: string | null createdAt: string } export interface Tunnel { id: number name: string integrationId: number mode: 'local' | 'remote' | 'dynamic' sourcePort: number endpointHost: string endpointPort: number autoStart: boolean maxRetries: number retryIntervalMs: number createdAt: string status: 'stopped' | 'connecting' | 'connected' | 'retrying' | 'error' error: string | null retryCount: number } export interface Bookmark { id: number category_id: number | null title: string url: string icon: string | null favorite: number status: string last_checked_at: string | null created_at: string } export interface BookmarkCategory { id: number name: string icon: string | null sort_order: number } export interface Event { id: number type: string title: string source: string | null created_at: string } export interface Resource { name: string status: 'healthy' | 'warning' | 'critical' | 'unknown' detail?: string integration: string }