import { z } from "zod"; import { httpFetch } from "@/lib/http"; const LEGACY_KEY = "openconcho:config"; const STORE_KEY = "openconcho:instances"; export const configSchema = z.object({ baseUrl: z.string().url({ message: "Must be a valid URL" }), token: z.string().optional().default(""), }); export type Config = z.infer; export const instanceSchema = z.object({ id: z.string().min(1), name: z.string().min(1, { message: "Name is required" }), baseUrl: z.string().url({ message: "Must be a valid URL" }), token: z.string().optional().default(""), }); export type Instance = z.infer; const storeSchema = z.object({ instances: z.array(instanceSchema), activeId: z.string().nullable(), }); export type InstanceStore = z.infer; function newId(): string { if (typeof crypto !== "undefined" && "randomUUID" in crypto) { return crypto.randomUUID(); } return `inst_${Math.random().toString(36).slice(2)}_${Date.now()}`; } function migrateLegacy(): InstanceStore | null { const raw = localStorage.getItem(LEGACY_KEY); if (!raw) return null; try { const parsed = configSchema.parse(JSON.parse(raw)); const inst: Instance = { id: newId(), name: "Default", baseUrl: parsed.baseUrl, token: parsed.token, }; const store: InstanceStore = { instances: [inst], activeId: inst.id }; localStorage.setItem(STORE_KEY, JSON.stringify(store)); localStorage.removeItem(LEGACY_KEY); return store; } catch { return null; } } export function loadStore(): InstanceStore { try { const raw = localStorage.getItem(STORE_KEY); if (raw) return storeSchema.parse(JSON.parse(raw)); } catch { // fall through } const migrated = migrateLegacy(); if (migrated) return migrated; return { instances: [], activeId: null }; } export function saveStore(store: InstanceStore): void { localStorage.setItem(STORE_KEY, JSON.stringify(store)); } export function getActiveInstance(): Instance | null { const store = loadStore(); if (!store.activeId) return null; return store.instances.find((i) => i.id === store.activeId) ?? null; } export function loadConfig(): Config | null { const active = getActiveInstance(); if (!active) return null; return { baseUrl: active.baseUrl, token: active.token ?? "" }; } /** Backwards-compatible single-instance save: replaces or creates a "Default" instance. */ export function saveConfig(config: Config): void { const store = loadStore(); if (store.activeId) { const idx = store.instances.findIndex((i) => i.id === store.activeId); if (idx >= 0) { store.instances[idx] = { ...store.instances[idx], ...config }; saveStore(store); return; } } const inst: Instance = { id: newId(), name: "Default", ...config }; saveStore({ instances: [...store.instances, inst], activeId: inst.id }); } export function clearConfig(): void { localStorage.removeItem(STORE_KEY); localStorage.removeItem(LEGACY_KEY); } export function addInstance(input: Omit): Instance { const store = loadStore(); const inst: Instance = { id: newId(), ...input }; const next: InstanceStore = { instances: [...store.instances, inst], activeId: store.activeId ?? inst.id, }; saveStore(next); return inst; } export function updateInstance(id: string, patch: Partial>): void { const store = loadStore(); const idx = store.instances.findIndex((i) => i.id === id); if (idx < 0) return; store.instances[idx] = { ...store.instances[idx], ...patch }; saveStore(store); } export function deleteInstance(id: string): void { const store = loadStore(); const remaining = store.instances.filter((i) => i.id !== id); const activeId = store.activeId === id ? (remaining[0]?.id ?? null) : store.activeId; saveStore({ instances: remaining, activeId }); } export function setActiveInstance(id: string): void { const store = loadStore(); if (!store.instances.some((i) => i.id === id)) return; saveStore({ ...store, activeId: id }); } export type HealthStatus = "ok" | "auth-required" | "unreachable" | "checking"; export async function checkConnection( baseUrl: string, token?: string, ): Promise<{ status: HealthStatus; message: string; }> { try { const headers: Record = { "Content-Type": "application/json" }; if (token) headers.Authorization = `Bearer ${token}`; const res = await httpFetch(`${baseUrl}/v3/workspaces/list`, { method: "POST", headers, body: JSON.stringify({}), signal: AbortSignal.timeout(5000), }); if (res.ok) return { status: "ok", message: "Connected successfully" }; if (res.status === 401 || res.status === 403) { return { status: "auth-required", message: "Authentication required — provide an API token" }; } return { status: "unreachable", message: `Server returned ${res.status}` }; } catch (err) { const msg = err instanceof Error ? err.message : "Unknown error"; if (msg.includes("AbortError") || msg.includes("timeout")) { return { status: "unreachable", message: "Connection timed out" }; } return { status: "unreachable", message: `Cannot reach server: ${msg}` }; } }