const BASE = '/api' let _credentials = null // { user, pass } export function setCredentials(user, pass) { _credentials = { user, pass } } export function clearCredentials() { _credentials = null } export function authHeaders() { if (!_credentials) return {} return { 'Authorization': `Basic ${btoa(`${_credentials.user}:${_credentials.pass}`)}` } } async function request(method, path, body, isFormData = false) { const opts = { method, headers: {} } if (_credentials) { opts.headers['Authorization'] = `Basic ${btoa(`${_credentials.user}:${_credentials.pass}`)}` } if (body) { if (isFormData) { opts.body = body } else { opts.headers['Content-Type'] = 'application/json' opts.body = JSON.stringify(body) } } const res = await fetch(BASE + path, opts) if (res.status === 401) { clearCredentials() const err = new Error('Unauthorized') err.status = 401 throw err } const data = await res.json() if (!res.ok) throw new Error(data.error || 'Request failed') return data } export const api = { // Sources getSources: () => request('GET', '/sources'), getSource: (name) => request('GET', `/sources/${name}`), createSource: (body) => request('POST', '/sources', body), updateSource: (name, body) => request('PUT', `/sources/${name}`, body), deleteSource: (name) => request('DELETE', `/sources/${name}`), suggestSource: (file) => { const fd = new FormData() fd.append('file', file) return request('POST', '/sources/suggest', fd, true) }, getImportLog: (name) => request('GET', `/sources/${name}/import-log`), getAllImportLog: () => request('GET', '/sources/import-log'), deleteImport: (name, id) => request('DELETE', `/sources/${name}/import-log/${id}`), getStats: (name) => request('GET', `/sources/${name}/stats`), importCSV: (name, file) => { const fd = new FormData() fd.append('file', file) return request('POST', `/sources/${name}/import`, fd, true) }, transform: (name) => request('POST', `/sources/${name}/transform`), reprocess: (name) => request('POST', `/sources/${name}/reprocess`), generateView: (name) => request('POST', `/sources/${name}/view`), getFields: (name) => request('GET', `/sources/${name}/fields`), getViewData: (name, limit = 100, offset = 0, sortCol = null, sortDir = 'asc', filters = null) => { const params = new URLSearchParams({ limit, offset }) if (sortCol) { params.set('sort_col', sortCol); params.set('sort_dir', sortDir) } if (filters && filters.length > 0) params.set('filters', JSON.stringify(filters)) return request('GET', `/sources/${name}/view-data?${params}`) }, // Rules getRules: (source) => request('GET', `/rules/source/${source}`), createRule: (body) => request('POST', '/rules', body), updateRule: (id, body) => request('PUT', `/rules/${id}`, body), deleteRule: (id) => request('DELETE', `/rules/${id}`), testRule: (id, limit = 20) => request('GET', `/rules/${id}/test?limit=${limit}`), previewRule: (source, field, pattern, flags, function_type = 'extract', replace_value = '', limit = 20) => request('GET', `/rules/preview?source=${encodeURIComponent(source)}&field=${encodeURIComponent(field)}&pattern=${encodeURIComponent(pattern)}&flags=${encodeURIComponent(flags || '')}&function_type=${function_type}&replace_value=${encodeURIComponent(replace_value)}&limit=${limit}`), // Mappings getGlobalValues: () => request('GET', '/mappings/global-values'), getMappings: (source, rule) => request('GET', `/mappings/source/${source}${rule ? `?rule_name=${rule}` : ''}`), getMappingCounts: (source, rule) => request('GET', `/mappings/source/${source}/counts${rule ? `?rule_name=${rule}` : ''}`), getUnmapped: (source, rule) => request('GET', `/mappings/source/${source}/unmapped${rule ? `?rule_name=${rule}` : ''}`), getAllValues: (source, rule) => request('GET', `/mappings/source/${source}/all-values${rule ? `?rule_name=${rule}` : ''}`), exportMappingsUrl: (source, rule) => `${BASE}/mappings/source/${source}/export.tsv${rule ? `?rule_name=${rule}` : ''}`, importMappingsCSV: (source, file) => { const fd = new FormData() fd.append('file', file) return request('POST', `/mappings/source/${source}/import-csv`, fd, true) }, createMapping: (body) => request('POST', '/mappings', body), bulkMappings: (mappings) => request('POST', '/mappings/bulk', { mappings }), updateMapping: (id, body) => request('PUT', `/mappings/${id}`, body), deleteMapping: (id) => request('DELETE', `/mappings/${id}`), // Global remap searchMappingOutputs: (search) => request('GET', `/mappings/outputs?search=${encodeURIComponent(search)}`), getMappingsByOutputField: (col, val) => request('GET', `/mappings/outputs/${encodeURIComponent(col)}/${encodeURIComponent(val)}`), remapOutputField: (col, from_val, to_val) => request('POST', '/mappings/remap-field', { col, from_val, to_val }), // Pivot layouts getPivotLayouts: (source) => request('GET', `/sources/${source}/layouts`), savePivotLayout: (source, layout_name, config) => request('POST', `/sources/${source}/layouts`, { layout_name, config }), deletePivotLayout: (source, id) => request('DELETE', `/sources/${source}/layouts/${id}`), // Stacks getStacks: () => request('GET', '/stacks'), getStack: (name) => request('GET', `/stacks/${name}`), createStack: (body) => request('POST', '/stacks', body), updateStack: (name, body) => request('PUT', `/stacks/${name}`, body), deleteStack: (name) => request('DELETE', `/stacks/${name}`), upsertStackSource: (name, source, body) => request('PUT', `/stacks/${name}/sources/${source}`, body), removeStackSource: (name, source) => request('DELETE', `/stacks/${name}/sources/${source}`), generateStackView: (name) => request('POST', `/stacks/${name}/view`), getStackBalance: (name) => request('GET', `/stacks/${name}/balance`), calibrateBalance: (name, source, body) => request('POST', `/stacks/${name}/calibrate`, { ...body, source_name: source || null }), // Status getStatus: () => request('GET', '/status'), // Records getRecords: (source, limit = 100, offset = 0) => request('GET', `/records/source/${source}?limit=${limit}&offset=${offset}`), getRecord: (id) => request('GET', `/records/${id}`), setRecordOverrides: (id, overrides) => request('PUT', `/records/${id}/overrides`, { overrides }), clearRecordOverrides: (id) => request('DELETE', `/records/${id}/overrides`), }