/* Slim Uiltjies — Client API wrapper Talks to the Express server. Falls back to local-only mode if the server is unreachable so the app still works offline. */ (function () { 'use strict'; const TOKEN_KEY = 'su_api_token'; const PIN_FALLBACK = '847291'; // used if server unreachable let _token = null; try { _token = sessionStorage.getItem(TOKEN_KEY) || null; } catch {} let _online = true; let _syncing = false; let _lastSyncAt = null; let _listeners = []; function emit(event, data) { for (const fn of _listeners) { try { fn(event, data); } catch {} } } function on(fn) { _listeners.push(fn); return () => { _listeners = _listeners.filter((f) => f !== fn); }; } function isReady() { return !!_token; } function setToken(t) { _token = t; try { if (t) sessionStorage.setItem(TOKEN_KEY, t); else sessionStorage.removeItem(TOKEN_KEY); } catch {} } async function auth(pin) { try { const r = await fetch('/api/auth', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ pin }), }); if (!r.ok) { const data = await r.json().catch(() => ({})); throw new Error(data.error || 'Auth failed'); } const { token } = await r.json(); setToken(token); _online = true; emit('online', true); return { token, online: true }; } catch (e) { // Server unreachable — allow offline mode with matching PIN if (pin === PIN_FALLBACK) { _online = false; setToken('offline'); emit('online', false); return { token: 'offline', online: false }; } throw e; } } async function health() { try { const r = await fetch('/api/health'); return r.ok; } catch { return false; } } async function getState() { if (!_token || _token === 'offline') { throw new Error('Offline'); } const r = await fetch('/api/state', { headers: { 'X-Auth-Token': _token }, }); if (!r.ok) throw new Error('Fetch failed (' + r.status + ')'); return r.json(); } async function pushState(state) { if (!_token || _token === 'offline') { throw new Error('Offline'); } const r = await fetch('/api/state', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Auth-Token': _token, }, body: JSON.stringify(state), }); if (!r.ok) throw new Error('Push failed (' + r.status + ')'); return r.json(); } async function logout() { if (_token && _token !== 'offline') { try { await fetch('/api/logout', { method: 'POST', headers: { 'X-Auth-Token': _token } }); } catch {} } setToken(null); _online = true; } // Pull server state and import to local IndexedDB. Returns true on success. async function pullAndMerge() { if (!_token || _token === 'offline') return false; try { const state = await getState(); // Server wins — replace local with server await window.SUDB.importAll(state, { replace: true }); _lastSyncAt = new Date(); emit('synced', { direction: 'pull', at: _lastSyncAt }); return true; } catch (e) { _online = false; emit('online', false); return false; } } // Push local state to server. Coalesces rapid calls via the syncLock flag. let _pendingSync = null; let _scheduledSync = null; async function doPush() { if (!_token || _token === 'offline') return false; if (_syncing) return false; _syncing = true; emit('syncing', true); try { const state = await window.SUDB.exportAll(); const res = await pushState(state); _lastSyncAt = new Date(res.updatedAt || Date.now()); _online = true; emit('synced', { direction: 'push', at: _lastSyncAt }); // Also fire n8n backup if configured (don't await — fire-and-forget) if (_n8nUrl) { n8nBackup(state).catch(() => {}); } return true; } catch (e) { _online = false; emit('online', false); return false; } finally { _syncing = false; emit('syncing', false); } } function schedulePush() { if (!_token || _token === 'offline') return; if (_scheduledSync) clearTimeout(_scheduledSync); _scheduledSync = setTimeout(() => { _scheduledSync = null; doPush(); }, 500); // debounce 500ms } function status() { return { online: _online, hasToken: !!_token, offlineMode: _token === 'offline', syncing: _syncing, lastSyncAt: _lastSyncAt, }; } // ============================================================ // n8n backup webhook // ============================================================ let _n8nUrl = null; let _n8nToken = null; let _n8nLastBackup = null; let _n8nLastError = null; let _n8nBackingUp = false; function configureN8n(url, token) { _n8nUrl = url || null; _n8nToken = token || null; try { if (url) localStorage.setItem('su_n8n_url', url); else localStorage.removeItem('su_n8n_url'); if (token) localStorage.setItem('su_n8n_token', token); else localStorage.removeItem('su_n8n_token'); } catch {} } function loadN8nConfig() { try { _n8nUrl = localStorage.getItem('su_n8n_url') || null; _n8nToken = localStorage.getItem('su_n8n_token') || null; } catch {} } function n8nStatus() { return { configured: !!_n8nUrl, url: _n8nUrl, lastBackup: _n8nLastBackup, lastError: _n8nLastError, backingUp: _n8nBackingUp, }; } async function n8nBackup(state) { if (!_n8nUrl) return { skipped: true }; if (_n8nBackingUp) return { skipped: true, reason: 'in-progress' }; _n8nBackingUp = true; emit('n8n-syncing', true); try { const headers = { 'Content-Type': 'application/json' }; if (_n8nToken) headers['Authorization'] = `Bearer ${_n8nToken}`; const res = await fetch(_n8nUrl, { method: 'POST', headers, body: JSON.stringify({ ...state, _backupAt: new Date().toISOString() }), }); if (!res.ok) throw new Error('HTTP ' + res.status); _n8nLastBackup = new Date(); _n8nLastError = null; emit('n8n-synced', { at: _n8nLastBackup }); return { ok: true, at: _n8nLastBackup }; } catch (e) { _n8nLastError = e.message; emit('n8n-error', { error: e.message }); return { ok: false, error: e.message }; } finally { _n8nBackingUp = false; emit('n8n-syncing', false); } } // Load n8n config on init loadN8nConfig(); window.SUAPI = { auth, logout, health, getState, pushState, pullAndMerge, schedulePush, status, configureN8n, loadN8nConfig, n8nStatus, n8nBackup, on, isReady, }; })();