Merge branch 'main' into feat/deep-linking

This commit is contained in:
Offending Commit
2026-05-04 11:31:55 -05:00
committed by GitHub
6 changed files with 575 additions and 47 deletions

View File

@@ -1,7 +1,8 @@
import { z } from "zod";
import { httpFetch } from "@/lib/http";
const CONFIG_KEY = "openconcho:config";
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" }),
@@ -10,23 +11,127 @@ export const configSchema = z.object({
export type Config = z.infer<typeof configSchema>;
export function loadConfig(): Config | null {
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<typeof instanceSchema>;
const storeSchema = z.object({
instances: z.array(instanceSchema),
activeId: z.string().nullable(),
});
export type InstanceStore = z.infer<typeof storeSchema>;
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 raw = localStorage.getItem(CONFIG_KEY);
if (!raw) return null;
const parsed = JSON.parse(raw);
return configSchema.parse(parsed);
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 {
localStorage.setItem(CONFIG_KEY, JSON.stringify(config));
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(CONFIG_KEY);
localStorage.removeItem(STORE_KEY);
localStorage.removeItem(LEGACY_KEY);
}
export function addInstance(input: Omit<Instance, "id">): 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<Omit<Instance, "id">>): 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";