// Same-origin: in dev, Vite proxies /api and /uploads to the API server // (see vite.config.js); in production, nginx serves the built client and // reverse-proxies the same paths to the Node server (see client/nginx.conf). // Relative URLs are what make the app reachable from a phone/PWA over the LAN — // an absolute http://localhost:3001 would resolve to the *visiting device*. export const SERVER_BASE = '' const API_BASE = `${SERVER_BASE}/api` // Lets the auth gate react to a session expiring mid-session: any protected // request that comes back 401 flips the app to the login screen instead of // surfacing a generic error in each page. let onUnauthorized = null export function setUnauthorizedHandler(handler) { onUnauthorized = handler } async function request(path, options = {}) { const isJsonBody = typeof options.body === 'string' const res = await fetch(`${API_BASE}${path}`, { headers: isJsonBody ? { 'Content-Type': 'application/json' } : undefined, credentials: 'same-origin', ...options, }) if (res.status === 401) { if (onUnauthorized) onUnauthorized() const body = await res.json().catch(() => ({})) throw new Error(body.error || 'Unauthorized') } if (!res.ok) { const body = await res.json().catch(() => ({})) throw new Error(body.error || `Request failed with status ${res.status}`) } if (res.status === 204) return null return res.json() } function withQuery(path, params = {}) { const search = new URLSearchParams() for (const [key, value] of Object.entries(params)) { if (value !== undefined && value !== null && value !== '') { search.set(key, value) } } const query = search.toString() return query ? `${path}?${query}` : path } // Rabbits export const getRabbits = (params) => request(withQuery('/rabbits', params)) export const searchRabbits = (params) => request(withQuery('/rabbits/search', params)) // Global quick-search bar: prefix-matches across farm/name/ear_number/color. // `fields` is a comma-joined subset of those keys (omit for all). Each result // carries `matched_fields` so the dropdown can show which filter it hit. export const quickSearchRabbits = (params) => request(withQuery('/rabbits/quick-search', params)) export const getRabbit = (id) => request(`/rabbits/${id}`) export const getRabbitAncestors = (id) => request(`/rabbits/${id}/ancestors`) export const createRabbit = (data) => request('/rabbits', { method: 'POST', body: JSON.stringify(data) }) export const updateRabbit = (id, data) => request(`/rabbits/${id}`, { method: 'PATCH', body: JSON.stringify(data) }) // Which existing rabbit an Add Rabbit submission would top up (same matching // as POST /rabbits), or null when it would be created new. export const findExistingRabbit = (data) => request('/rabbits/find-existing', { method: 'POST', body: JSON.stringify(data) }) export const deleteRabbit = (id) => request(`/rabbits/${id}`, { method: 'DELETE' }) export const uploadRabbitPhoto = (id, file) => { const formData = new FormData() formData.append('photo', file) return request(`/rabbits/${id}/photo`, { method: 'POST', body: formData }) } // Pedigree scanning (AI vision on the server; see server/lib/pedigreeScan.ts). // `getPedigreeScanStatus` → { enabled } decides whether the scan button shows. export const getPedigreeScanStatus = () => request('/pedigree-scan') export const scanPedigree = (imageBlob) => { const formData = new FormData() formData.append('image', imageBlob, 'pedigree.jpg') return request('/pedigree-scan', { method: 'POST', body: formData }) } // Litters export const getLitters = () => request('/litters') export const getLitter = (id) => request(`/litters/${id}`) export const createLitter = (data) => request('/litters', { method: 'POST', body: JSON.stringify(data) }) export const updateLitter = (id, data) => request(`/litters/${id}`, { method: 'PATCH', body: JSON.stringify(data) }) export const deleteLitter = (id) => request(`/litters/${id}`, { method: 'DELETE' }) // Events — manually-logged timeline events (show / sale / other) for a rabbit export const getEvents = (rabbitId) => request(withQuery('/events', { rabbit_id: rabbitId })) export const createEvent = (data) => request('/events', { method: 'POST', body: JSON.stringify(data) }) export const updateEvent = (id, data) => request(`/events/${id}`, { method: 'PATCH', body: JSON.stringify(data) }) export const deleteEvent = (id) => request(`/events/${id}`, { method: 'DELETE' }) // Shows — ARBA show listing (scraped + cached server-side). `getArbaShows` // returns { fetched_at, stale, shows: [...] }; `refreshArbaShows` forces a // server re-scrape and returns the same shape. export const getArbaShows = () => request('/shows/arba') export const refreshArbaShows = () => request('/shows/arba/refresh', { method: 'POST' }) // Show events — user-created groupings of ARBA shows. A `show` added to an event // is a snapshot of the ARBA listing row (see routes/showEvents.ts). export const getShowEvents = () => request('/shows/events') export const getShowEvent = (id) => request(`/shows/events/${id}`) export const createShowEvent = (data) => request('/shows/events', { method: 'POST', body: JSON.stringify(data) }) export const updateShowEvent = (id, data) => request(`/shows/events/${id}`, { method: 'PATCH', body: JSON.stringify(data) }) export const deleteShowEvent = (id) => request(`/shows/events/${id}`, { method: 'DELETE' }) export const addShowToEvent = (id, show) => request(`/shows/events/${id}/shows`, { method: 'POST', body: JSON.stringify(show) }) export const removeShowFromEvent = (id, showId) => request(`/shows/events/${id}/shows/${showId}`, { method: 'DELETE' }) // Show entries — a herd rabbit entered into a specific show within an event. export const addShowEntry = (id, showId, data) => request(`/shows/events/${id}/shows/${showId}/entries`, { method: 'POST', body: JSON.stringify(data) }) export const updateShowEntry = (id, showId, entryId, data) => request(`/shows/events/${id}/shows/${showId}/entries/${entryId}`, { method: 'PATCH', body: JSON.stringify(data) }) export const removeShowEntry = (id, showId, entryId) => request(`/shows/events/${id}/shows/${showId}/entries/${entryId}`, { method: 'DELETE' }) export const getEventReport = (id) => request(`/shows/events/${id}/report`) // Judges (per show within an event) and placements (one per entry; PUT upserts). export const addShowJudge = (id, showId, data) => request(`/shows/events/${id}/shows/${showId}/judges`, { method: 'POST', body: JSON.stringify(data) }) export const updateShowJudge = (id, showId, judgeId, data) => request(`/shows/events/${id}/shows/${showId}/judges/${judgeId}`, { method: 'PATCH', body: JSON.stringify(data) }) export const removeShowJudge = (id, showId, judgeId) => request(`/shows/events/${id}/shows/${showId}/judges/${judgeId}`, { method: 'DELETE' }) export const setShowEntryPlacement = (id, showId, entryId, data) => request(`/shows/events/${id}/shows/${showId}/entries/${entryId}/placement`, { method: 'PUT', body: JSON.stringify(data), }) // Settings export const getSettings = () => request('/settings') export const updateSettings = (data) => request('/settings', { method: 'PATCH', body: JSON.stringify(data) }) // Stats export const getStats = () => request('/stats') // Export export const exportDatabaseUrl = () => `${API_BASE}/export` // Import — replaces ALL rabbits (and litters) with the contents of a CSV/JSON file export const importData = (file) => { const formData = new FormData() formData.append('file', file) return request('/import', { method: 'POST', body: formData }) } // Auth (user accounts — see memory-bank/FEATURE_User_Accounts.md) export const getAuthStatus = () => request('/auth/status') export const login = (username, password) => request('/auth/login', { method: 'POST', body: JSON.stringify({ username, password }) }) export const logout = () => request('/auth/logout', { method: 'POST' }) // First-run onboarding: creates the first account (409 once any user exists). export const setupAccount = (username, password) => request('/auth/setup', { method: 'POST', body: JSON.stringify({ username, password }) }) // Users (shared herd — users are logins, not data owners) export const getUsers = () => request('/users') export const createUser = (username, password) => request('/users', { method: 'POST', body: JSON.stringify({ username, password }) }) export const resetUserPassword = (id, password) => request(`/users/${id}`, { method: 'PATCH', body: JSON.stringify({ password }) }) export const deleteUser = (id) => request(`/users/${id}`, { method: 'DELETE' })