/** * MAC API client. Keep browser fetch calls here so Svelte pages stay thin. */ const BASE = '/api/v1'; function getToken() { if (typeof localStorage === 'undefined') return null; return localStorage.getItem('mac_token'); } function headers(extra = {}) { /** @type {Record} */ const h = { 'Content-Type': 'application/json', ...extra }; const token = getToken(); if (token) h.Authorization = `Bearer ${token}`; return h; } async function handleResponse(res) { if (res.ok) { const ct = res.headers.get('content-type') || ''; if (ct.includes('application/json')) return res.json(); return res.text(); } let detail = `HTTP ${res.status}`; try { const body = await res.json(); detail = body?.detail?.message || body?.detail || body?.message || JSON.stringify(body); } catch {} const err = /** @type {Error & { status?: number }} */ (new Error(detail)); err.status = res.status; throw err; } async function get(path, params = {}) { const url = new URL(BASE + path, location.origin); Object.entries(params).forEach(([k, v]) => { if (v !== undefined && v !== null && v !== '') url.searchParams.set(k, v); }); return handleResponse(await fetch(url, { headers: headers() })); } async function post(path, body = {}) { return handleResponse(await fetch(BASE + path, { method: 'POST', headers: headers(), body: JSON.stringify(body), })); } async function patch(path, body = {}) { return handleResponse(await fetch(BASE + path, { method: 'PATCH', headers: headers(), body: JSON.stringify(body), })); } async function put(path, body = {}) { return handleResponse(await fetch(BASE + path, { method: 'PUT', headers: headers(), body: JSON.stringify(body), })); } async function del(path) { return handleResponse(await fetch(BASE + path, { method: 'DELETE', headers: headers() })); } async function formPost(path, formData) { const token = getToken(); return handleResponse(await fetch(BASE + path, { method: 'POST', headers: token ? { Authorization: `Bearer ${token}` } : {}, body: formData, })); } export const auth = { login: (roll_number, password) => post('/auth/login', { roll_number, password }), verify: (roll_number, dob) => post('/auth/verify', { roll_number, dob }), refresh: (refresh_token) => post('/auth/refresh', { refresh_token }), me: () => get('/auth/me'), logout: () => post('/auth/logout'), changePassword: (old_password, new_password) => post('/auth/change-password', { old_password, new_password }), setPassword: (new_password, confirm_password) => post('/auth/set-password', { new_password, confirm_password }), updateProfile: (data) => put('/auth/me/profile', data), }; export const setup = { status: () => get('/setup/status'), createAdmin: (name, email, password) => post('/setup/create-admin', { name, email, password }), recovery: () => get('/setup/recovery'), }; export const query = { chatStream(messages, model = 'auto', options = {}) { const token = getToken(); return fetch(BASE + '/query/chat', { method: 'POST', headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) }, body: JSON.stringify({ messages, model, stream: true, ...options }), }); }, complete: (messages, model = 'auto') => post('/query/chat', { messages, model, stream: false }), }; export const models = { list: () => get('/models'), legacyList: () => get('/models/list'), health: () => get('/models/health'), community: () => get('/models/community'), submissions: () => get('/models/submissions'), }; export const usage = { mine: (_days = 30) => get('/usage/me'), stats: () => get('/usage/me'), history: (page = 1, per_page = 50, filters = {}) => get('/usage/me/history', { page, per_page, ...filters }), quota: () => get('/usage/me/quota'), all: (page = 1, per_page = 50, department = '') => get('/usage/admin/all', { page, per_page, department }), models: () => get('/usage/admin/models'), }; export const quota = { limits: () => get('/quota/limits'), mine: () => get('/quota/me'), remaining: () => get('/quota/me'), setUser: (roll_number, data) => put(`/quota/admin/user/${roll_number}`, data), exceeded: () => get('/quota/admin/exceeded'), }; export const keys = { list: () => get('/keys/my-key').then((key) => (key ? [key] : [])), generate: (label = '') => post('/keys/generate', { label }), stats: () => get('/keys/my-key/stats'), revoke: (_key_id = null) => del('/keys/my-key'), adminAll: () => get('/keys/admin/all'), adminRevoke: (roll_number) => post('/keys/admin/revoke', { roll_number }), }; export const scopedKeys = { create: (data) => post('/scoped-keys', data), mine: () => get('/scoped-keys/my'), revoke: (id) => del(`/scoped-keys/${id}`), adminAll: (page = 1, per_page = 100) => get('/scoped-keys/admin/all', { page, per_page }), adminRevoke: (id) => del(`/scoped-keys/admin/${id}`), }; export const features = { status: () => get('/features/status'), toggle: (key, enabled) => patch(`/admin/features/${key}`, { enabled }), }; export const hardware = { local: () => get('/hardware/local'), recommendations: () => get('/hardware/recommendations'), }; export const network = { localIp: () => get('/network/local-ip'), discover: (timeout = 2) => get('/network/discover', { timeout }), }; export const system = { version: () => get('/system/version'), updateStatus: () => get('/system/update-status'), restart: () => post('/admin/system/restart'), logs: (lines = 200) => get('/admin/system/logs', { lines }), }; export const users = { list: (page = 1, limit = 50, search = '') => get('/auth/admin/users', { page, per_page: limit, search }), update: (user_id, data) => put(`/auth/admin/users/${user_id}`, data), updateRole: (user_id, role) => put(`/auth/admin/users/${user_id}/role`, { role }), updateStatus: (user_id, is_active) => put(`/auth/admin/users/${user_id}/status`, { is_active }), create: (data) => post('/auth/admin/users', data), delete: (user_id) => del(`/auth/admin/users/${user_id}`), resetPassword: (user_id) => post(`/auth/admin/users/${user_id}/reset-password`), regenerateKey: (user_id) => post(`/auth/admin/users/${user_id}/regenerate-key`), stats: () => get('/auth/admin/stats'), registry: () => get('/auth/admin/registry'), addRegistry: (data) => post('/auth/admin/registry', data), bulkRegistry: (students) => post('/auth/admin/registry/bulk', { students }), uploadRegistry: (formData) => formPost('/auth/admin/registry/upload', formData), }; export const guardrails = { getRules: () => get('/guardrails/rules'), updateRules: (data) => put('/guardrails/rules', data), addRule: (data) => post('/guardrails/rules', data), toggleRule: (id) => patch(`/guardrails/rules/${id}/toggle`), deleteRule: (id) => del(`/guardrails/rules/${id}`), }; export const rag = { list: () => get('/rag/documents'), upload: (formData) => formPost('/rag/ingest', formData), delete: (doc_id) => del(`/rag/documents/${doc_id}`), collections: () => get('/rag/collections'), }; export const notifications = { list: (page = 1, per_page = 30) => get('/notifications', { page, per_page }), markRead: (id) => post(`/notifications/${id}/read`), markAllRead: () => post('/notifications/read-all'), auditLogs: (page = 1, per_page = 100) => get('/notifications/audit-logs', { page, per_page }), activity: (limit = 100) => get('/notifications/activity-stream', { limit }), }; export const cluster = { nodes: () => get('/cluster/nodes'), node: (id) => get(`/cluster/nodes/${id}`), nodeAction: (id, action) => post(`/cluster/nodes/${id}/action`, { action }), history: (id, limit = 60) => get(`/cluster/nodes/${id}/history`, { limit }), enrollTokens: () => get('/cluster/enroll-tokens'), createEnrollToken: (label = 'Worker Node', expires_hours = 24) => post('/cluster/enroll-token', { label, expires_hours }), deployModel: (node_id, data) => post(`/cluster/nodes/${node_id}/deploy`, data), removeDeployment: (node_id, dep_id) => del(`/cluster/nodes/${node_id}/deploy/${dep_id}`), }; export const academic = { branches: () => get('/academic/branches'), createBranch: (data) => post('/academic/branches', data), updateBranch: (id, data) => patch(`/academic/branches/${id}`, data), deleteBranch: (id) => del(`/academic/branches/${id}`), sections: () => get('/academic/sections'), branchSections: (branch_id) => get(`/academic/branches/${branch_id}/sections`), createSection: (data) => post('/academic/sections', data), }; export const files = { list: () => get('/files'), upload: (formData) => formPost('/files/upload', formData), download: (id) => `${BASE}/files/${id}/download`, deleteFile: (id) => del(`/files/${id}`), stats: (id) => get(`/files/${id}/stats`), }; export const notebooks = { list: (include_archived = false) => get('/notebooks', { include_archived }), create: (data) => post('/notebooks', data), get: (id) => get(`/notebooks/${id}`), update: (id, data) => patch(`/notebooks/${id}`, data), delete: (id) => del(`/notebooks/${id}`), addCell: (id, data) => post(`/notebooks/${id}/cells`, data), updateCell: (cell_id, data) => patch(`/notebooks/cells/${cell_id}`, data), deleteCell: (cell_id) => del(`/notebooks/cells/${cell_id}`), runCell: (cell_id) => post(`/notebooks/cells/${cell_id}/run`), executions: (cell_id) => get(`/notebooks/cells/${cell_id}/executions`), reorder: (id, cell_ids) => post(`/notebooks/${id}/reorder`, { cell_ids }), }; export const doubts = { create: (data) => post('/doubts', data), mine: (page = 1, per_page = 20) => get('/doubts/my', { page, per_page }), all: (filters = {}) => get('/doubts/all', filters), get: (id) => get(`/doubts/${id}`), reply: (id, body) => post(`/doubts/${id}/reply`, { body }), close: (id) => post(`/doubts/${id}/close`), }; export const attendance = { settings: () => get('/attendance/settings'), updateSettings: (data) => put('/attendance/settings', data), subjects: () => get('/attendance/subjects'), faceStatus: () => get('/attendance/face-status'), registerFace: (face_image_base64) => post('/attendance/register-face', { face_image_base64 }), sessions: (filters = {}) => get('/attendance/sessions', filters), createSession: (data) => post('/attendance/sessions', data), closeSession: (id) => post(`/attendance/sessions/${id}/close`), mark: (session_id, face_image_base64) => post('/attendance/mark', { session_id, face_image_base64 }), report: (id) => get(`/attendance/sessions/${id}/report`), adminOverview: (filters = {}) => get('/attendance/admin/overview', filters), summary: (department = '') => get('/attendance/summary', { department }), reportCsvUrl: (id) => `${BASE}/attendance/sessions/${id}/report/csv`, reportPdfUrl: (id) => `${BASE}/attendance/sessions/${id}/report/pdf`, summaryCsvUrl: (department = '') => `${BASE}/attendance/summary/csv${department ? `?department=${encodeURIComponent(department)}` : ''}`, }; export const copyCheck = { sessions: (page = 1, per_page = 50) => get('/copy-check/sessions', { page, per_page }), createSession: (formData) => formPost('/copy-check/sessions', formData), getSession: (id) => get(`/copy-check/sessions/${id}`), students: (id) => get(`/copy-check/sessions/${id}/students`), uploadSheet: (id, formData) => formPost(`/copy-check/sessions/${id}/sheets`, formData), evaluate: (id) => post(`/copy-check/sessions/${id}/evaluate`), plagiarism: (id) => post(`/copy-check/sessions/${id}/plagiarism`), archive: (id) => patch(`/copy-check/sessions/${id}/archive`), reportUrl: (id) => `${BASE}/copy-check/sessions/${id}/report/pdf`, };