/** * api.js — API Client Module * * Handles all HTTP communication with the Python backend. * Matches the 4-endpoint REST API from the Java Spring Boot server. */ const API_BASE = 'setting'; /** * GET /setting/get-device * @returns {Promise} Device config or null if not set */ export async function getDevice() { const res = await fetch(`${API_BASE}/get-device`); if (res.status === 204) return null; if (!res.ok) throw new Error(`Failed to load device config: ${res.status}`); return res.json(); } /** * POST /setting/device * @param {Object} data - Device configuration object * @returns {Promise<{success: boolean, message: string, warnings: string[]}>} */ export async function saveDevice(data) { const res = await fetch(`${API_BASE}/device`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); if (!res.ok) { const err = await res.json().catch(() => ({ message: `Save failed: ${res.status}` })); if (res.status === 400 && Array.isArray(err.errors) && err.errors.length > 0) { throw new Error('Invalid value: ' + err.errors.slice(0, 3).join(' · ')); } throw new Error(err.message || `Save failed: ${res.status}`); } return res.json(); } /** * GET /setting/get-protocol * @returns {Promise} Protocol config or null if not set */ export async function getProtocol() { const res = await fetch(`${API_BASE}/get-protocol`); if (res.status === 204) return null; if (!res.ok) throw new Error(`Failed to load protocol config: ${res.status}`); return res.json(); } /** * POST /setting/protocol * @param {Object} data - Protocol configuration object * @returns {Promise} Saved protocol config (with inactive protocols removed) */ export async function saveProtocol(data) { const res = await fetch(`${API_BASE}/protocol`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); if (!res.ok) { const err = await res.json().catch(() => ({})); if (res.status === 400 && Array.isArray(err.errors) && err.errors.length > 0) { throw new Error('Invalid value: ' + err.errors.slice(0, 3).join(' · ')); } throw new Error(err.message || err.error || `Failed to save protocol config: ${res.status}`); } return res.json(); } /** * GET /setting/log-files * @returns {Promise} { files: [...], total_size: number } */ export async function getLogFiles() { const res = await fetch(`${API_BASE}/log-files`); if (!res.ok) throw new Error(`Failed to load log files: ${res.status}`); return res.json(); } /** * POST /setting/log-download * @param {string[]} files - Array of filenames to download * @returns {Promise} tar.gz archive blob */ export async function downloadLogFiles(files) { const res = await fetch(`${API_BASE}/log-download`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ files }), }); if (!res.ok) { const err = await res.json().catch(() => ({ error: `Download failed: ${res.status}` })); throw new Error(err.error || `Download failed: ${res.status}`); } return res.blob(); } /** * GET /setting/log-stats * @returns {Promise} Log directory disk usage stats */ export async function getLogStats() { const res = await fetch(`${API_BASE}/log-stats`); if (!res.ok) throw new Error(`Failed to load log stats: ${res.status}`); return res.json(); } /** * POST /setting/log-compress * @returns {Promise} { success, compressed, message } */ export async function compressLogFiles() { const res = await fetch(`${API_BASE}/log-compress`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}', }); if (!res.ok) throw new Error(`Compression failed: ${res.status}`); return res.json(); } /** * POST /setting/log-delete * @param {string[]} files - Array of filenames to delete * @returns {Promise} { success, deleted, message } */ export async function deleteLogFilesApi(files) { const res = await fetch(`${API_BASE}/log-delete`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ files }), }); if (!res.ok) throw new Error(`Delete failed: ${res.status}`); return res.json(); } /** * GET /api/mac * @returns {Promise} WiFi MAC address or null if unavailable */ export async function getMac() { try { const res = await fetch('api/mac'); if (!res.ok) return null; const data = await res.json(); return data.mac || null; } catch { return null; } } /** * GET /api/system-status * @returns {Promise} Aggregated device status snapshot */ export async function getSystemStatus() { const res = await fetch('api/system-status'); if (!res.ok) throw new Error(`Failed to load status: ${res.status}`); return res.json(); } /** * POST /api/action/test-connections * @returns {Promise<{results: Array}>} Per-endpoint reachability results */ export async function testConnections() { const res = await fetch('api/action/test-connections', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}', }); if (!res.ok) throw new Error(`Connection test failed: ${res.status}`); return res.json(); } /** * POST /api/action/restart-dpworldapp * @returns {Promise<{restarted: boolean, running: boolean, pid?: number}>} */ export async function restartDpworldapp() { const res = await fetch('api/action/restart-dpworldapp', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}', }); if (!res.ok) throw new Error(`Restart failed: ${res.status}`); return res.json(); } /** * GET /api/support-bundle * @returns {Promise} Diagnostics zip blob */ export async function getSupportBundle() { const res = await fetch('api/support-bundle'); if (!res.ok) throw new Error(`Bundle download failed: ${res.status}`); return res.blob(); } // ── Firmware OTA API (v1.5.0 Phase 4a) ─────────────────────────────────────── /** GET /api/firmware/status → rich status snapshot (drives the whole UI). */ export async function getFirmwareStatus() { const res = await fetch('api/firmware/status'); if (!res.ok) throw new Error(`status ${res.status}`); return res.json(); } /** POST /api/firmware/preflight → { ok, checks:[...] } */ export async function preflightFirmware() { const res = await fetch('api/firmware/preflight', { method: 'POST', body: '{}' }); if (!res.ok) throw new Error(`preflight failed: ${res.status}`); return res.json(); } /** * POST /api/firmware/upload (raw ZIP body) → { ok, components:[...] } * Uses XHR for upload-progress events. * @param {File} file * @param {(loaded:number,total:number)=>void} [onProgress] */ export function uploadFirmware(file, onProgress) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('POST', 'api/firmware/upload'); xhr.upload.onprogress = (e) => { if (e.lengthComputable && onProgress) onProgress(e.loaded, e.total); }; xhr.onload = () => { let r; try { r = JSON.parse(xhr.responseText); } catch { r = {}; } if (xhr.status === 200 && r.ok) resolve(r); else reject(new Error(r.error || `upload failed (${xhr.status})`)); }; xhr.onerror = () => reject(new Error('upload network error')); xhr.send(file); }); } /** POST /api/firmware/flash → { ok } (starts background flash) */ export async function startFlash() { const res = await fetch('api/firmware/flash', { method: 'POST', body: '{}' }); const r = await res.json().catch(() => ({})); if (!res.ok) throw new Error(r.error || `flash failed: ${res.status}`); return r; } /** POST /api/firmware/restore-check → { ok, result:{restored,reason,...} } */ export async function restoreCheck() { const res = await fetch('api/firmware/restore-check', { method: 'POST', body: '{}' }); const r = await res.json().catch(() => ({})); if (!res.ok) throw new Error(r.error || `restore-check failed: ${res.status}`); return r; }