// labdb — Admin web client const api = { async _fetch(path, opts) { const o = Object.assign({ credentials: 'same-origin' }, opts || {}); o.headers = Object.assign({ 'Content-Type': 'application/json' }, o.headers || {}); const res = await fetch(path, o); if (res.status === 401 && !location.pathname.endsWith('/login.html')) { location.href = '/login.html'; return { error: 'unauthorized' }; } try { return await res.json(); } catch (_) { return { error: 'invalid response' }; } }, get(p) { return this._fetch(p); }, post(p, body) { return this._fetch(p, { method: 'POST', body: JSON.stringify(body || {}) }); }, patch(p, body) { return this._fetch(p, { method: 'PATCH', body: JSON.stringify(body || {}) }); }, del(p) { return this._fetch(p, { method: 'DELETE' }); }, }; function esc(s) { if (s === null || s === undefined) return ''; return String(s) .replace(/&/g, '&').replace(//g, '>') .replace(/"/g, '"').replace(/'/g, '''); } function formatDate(iso) { if (!iso) return '—'; const d = new Date(iso); if (isNaN(d)) return '—'; const pad = (n) => String(n).padStart(2, '0'); return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; } function formatDuration(start, end) { if (!start || !end) return '—'; const ms = new Date(end) - new Date(start); if (isNaN(ms) || ms < 0) return '—'; const sec = Math.floor(ms / 1000); const h = Math.floor(sec / 3600); const m = Math.floor((sec % 3600) / 60); const s = sec % 60; return `${String(h).padStart(2,'0')}:${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}`; }