const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8080'; export type RuntimeRecordType = 'role' | 'dashboard' | 'onboarding'; export type RuntimeRecordStatus = 'draft' | 'published'; export type RuntimeStoredRecord = { status: RuntimeRecordStatus; updatedAt: string; payload: T; }; export type RuntimeListItem = RuntimeStoredRecord & { key: string; }; // Types corresponding to Rust backend models type RoleRecord = { id: string; key: string; name: string; audience: string; }; export async function saveRuntimeConfig(type: RuntimeRecordType, key: string, payload: unknown, status: RuntimeRecordStatus) { const normalizedKey = key.trim().toUpperCase(); if (!normalizedKey) return; try { // 1. Ensure the Role exists first. We lookup by key. // If it doesn't exist, we create it generically. let roleRes = await fetch(`${API_URL}/api/admin/roles/${normalizedKey}`); let role: RoleRecord; if (!roleRes.ok && roleRes.status === 404) { // Create it const createRes = await fetch(`${API_URL}/api/admin/roles`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ key: normalizedKey, name: normalizedKey, // Using key as name for now audience: 'EXTERNAL', }) }); if (!createRes.ok) throw new Error("Failed to create role"); role = await createRes.json(); } else { role = await roleRes.json(); } // 2. Attach the specific config payload to the role const endpointMap: Record = { role: '/api/runtime-config', // Storing the "role config wrapper" as runtime_config dashboard: '/api/admin/dashboard-config', onboarding: '/api/admin/onboarding-config', }; let bodyPayload: any = { role_id: role.id, }; if (type === 'dashboard') { bodyPayload.audience = 'EXTERNAL'; bodyPayload.config_json = payload; } else if (type === 'onboarding') { bodyPayload.schema_json = payload; } else if (type === 'role') { bodyPayload.config_json = payload; } const saveRes = await fetch(`${API_URL}${endpointMap[type]}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(bodyPayload) }); if (!saveRes.ok) { throw new Error(`Failed to save ${type} config`); } console.log(`Saved ${type} mapping for role ${normalizedKey} successfully to Rust backend`); } catch (err) { console.error(`Error saving runtime config [${type}]:`, err); } } export async function listRuntimeConfigs(type: RuntimeRecordType): Promise>> { const endpointMap: Record = { role: '/api/admin/roles', dashboard: '/api/admin/dashboard-config', onboarding: '/api/admin/onboarding-config', }; try { const res = await fetch(`${API_URL}${endpointMap[type]}`); if (!res.ok) throw new Error(`Failed to list ${type} configs`); const data = await res.json(); // Map Rust backend models to UI expected format return data.map((item: any) => ({ key: item.role_key || item.key, status: item.is_active ? 'published' : 'draft', updatedAt: item.updated_at || item.created_at, payload: item.config_json || item.schema_json || item, // include raw id for routing convenience id: item.id, role_id: item.role_id })); } catch (err) { console.error(`Error listing runtime config [${type}]:`, err); return []; } } export async function getRuntimeConfig(type: RuntimeRecordType, roleId: string): Promise | null> { const endpointMap: Record = { role: `/api/admin/roles/${roleId}`, // Assume roleId is key if testing dashboard: `/api/admin/dashboard-config/${roleId}?audience=EXTERNAL`, onboarding: `/api/admin/onboarding-config/${roleId}`, }; try { const res = await fetch(`${API_URL}${endpointMap[type]}`); if (!res.ok) return null; const data = await res.json(); return { status: data.is_active ? 'published' : 'draft', updatedAt: data.updated_at || data.created_at, payload: data.config_json || data.schema_json || data, }; } catch (err) { return null; } } export async function deleteRuntimeConfig(type: RuntimeRecordType, key: string): Promise { // Mock delete since hard deletes usually aren't ideal for configs (we just soft-disable them on new save) console.log(`Delete requested for ${type} config with key ${key}. In Rust, we just save a new one with is_active=true overriding the old one.`); return true; }