const API = ''; function escapeHtml(text) { if (text == null) return ''; return String(text) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } function formatApiError(detail) { if (!detail) return null; if (typeof detail === 'string') return detail; if (Array.isArray(detail)) { return detail.map((d) => (d && d.msg) || JSON.stringify(d)).join('; '); } return String(detail); } async function ddaApi(method, path, options = {}) { const headers = { ...options.headers }; if (options.body && !(options.body instanceof FormData)) { headers['Content-Type'] = 'application/json'; } const res = await fetch(API + path, { method, headers, credentials: 'include', ...options }); const text = await res.text(); let data = null; try { data = text ? JSON.parse(text) : null; } catch (_) {} if (!res.ok) throw new Error(formatApiError(data?.detail) || res.statusText || 'Request failed'); return data; } function showDdaError(msg) { const el = document.getElementById('dda-error'); if (!el) return; el.textContent = msg; el.classList.remove('hidden'); } function hideDdaError() { document.getElementById('dda-error')?.classList.add('hidden'); } function showDdaSuccess(msg) { const el = document.getElementById('dda-success'); if (!el) return; el.textContent = msg; el.classList.remove('hidden'); setTimeout(() => el.classList.add('hidden'), 4000); } function formatBytes(n) { if (n >= 1024 ** 3) return (n / 1024 ** 3).toFixed(1) + ' GB'; if (n >= 1024 ** 2) return (n / 1024 ** 2).toFixed(1) + ' MB'; return (n / 1024).toFixed(0) + ' KB'; } let ddaConfig = null; const selectedNode = { id: null, path: '' }; window.ddaState = { get config() { return ddaConfig; }, get localCfg() { return window._localCfg; }, get selectedNode() { return { ...selectedNode }; }, get userRole() { return window._ddaUserRole || 'analyst'; }, set userRole(r) { window._ddaUserRole = r; }, setNode(n) { selectedNode.id = n.id; selectedNode.path = n.path || ''; }, clearNode() { selectedNode.id = null; selectedNode.path = ''; }, refreshImages: () => loadLibraryImages(), rescan: () => rescanLibrary(), }; document.querySelectorAll('.dda-tab').forEach((btn) => { btn.addEventListener('click', () => { document.querySelectorAll('.dda-tab').forEach((b) => b.classList.remove('active')); document.querySelectorAll('.dda-panel').forEach((p) => p.classList.remove('active')); btn.classList.add('active'); const tab = btn.dataset.tab; document.getElementById('tab-' + tab)?.classList.add('active'); if (tab === 'detect' && typeof loadCompareLibraryGrid === 'function') { loadCompareLibraryGrid(); } }); }); async function rescanLibrary() { const data = await ddaApi('POST', '/api/dda/local/rescan'); if (typeof renderAllTrees === 'function') renderAllTrees({ tree: data.tree }, []); else if (typeof renderTree === 'function') renderTree({ tree: data.tree }, []); if (typeof populateManageNodeSelect === 'function') populateManageNodeSelect(); await loadLibraryImages(); if (typeof loadCompareLibraryGrid === 'function') await loadCompareLibraryGrid(); return data; } async function initDda() { hideDdaError(); try { try { const me = await ddaApi('GET', '/api/dda/me'); window.ddaState.userRole = me.role || 'analyst'; } catch (_) { window.ddaState.userRole = 'analyst'; } ddaConfig = await ddaApi('GET', '/api/dda/config'); const localCfg = await ddaApi('GET', '/api/dda/local/config'); window._localCfg = localCfg; document.getElementById('btn-manage-library')?.classList.toggle('hidden', window.ddaState.userRole !== 'admin'); const hint = document.getElementById('lib-config-hint'); if (hint) hint.textContent = localCfg.geotiffEnabled ? 'GeoTIFF ready' : 'GeoTIFF limited'; const pathEl = document.getElementById('lib-path-display'); if (pathEl) { pathEl.textContent = [ 'Storage root:', localCfg.storageRoot || localCfg.writablePath || '', 'Layout: {zone}/{area}/…/Images/file.tif', ].filter(Boolean).join('\n'); } const folderPath = document.getElementById('lib-folder-path'); if (folderPath) { folderPath.textContent = localCfg.isHosted ? 'Hugging Face — tree library' : `Storage: ${localCfg.storageRoot || ''}`; } const instr = document.getElementById('lib-instructions'); if (instr && localCfg.instructions) instr.textContent = localCfg.instructions; const uploadLimit = document.getElementById('upload-limit-hint'); if (uploadLimit && localCfg.maxUploadGb) { uploadLimit.textContent = `Select a tree node and image type. Max ${localCfg.maxUploadGb} GB per file.`; } const resHint = document.getElementById('dda-detect-res-hint'); if (resHint && localCfg.detectionMaxSide) { resHint.textContent = `Detection runs at up to ${localCfg.detectionMaxSide}px per side.`; } const urlTab = new URLSearchParams(window.location.search).get('tab'); if (urlTab) document.querySelector(`.dda-tab[data-tab="${urlTab}"]`)?.click(); if (typeof loadTree === 'function') await loadTree(); await loadLibraryImages(); } catch (err) { showDdaError(err.message || 'Failed to load library'); } } function selectionTitle() { return selectedNode.path || 'All images'; } async function loadLibraryImages() { const grid = document.getElementById('lib-grid'); const title = document.getElementById('lib-grid-title'); if (!grid) return; const q = document.getElementById('lib-filter')?.value?.trim() || ''; const params = new URLSearchParams(); if (selectedNode.id) params.set('node_id', String(selectedNode.id)); if (q) params.set('q', q); if (title) title.textContent = `Images — ${selectionTitle()}`; try { const items = await ddaApi('GET', '/api/dda/local/images?' + params.toString()); window.ddaState.libraryItems = items; if (!items.length) { grid.innerHTML = `
No images in ${escapeHtml(selectionTitle())}. Select a node and upload, or use Manage to create folders.
`; return; } grid.innerHTML = items.map((img) => { const thumb = img.thumbUrl || ''; const crumb = img.breadcrumb || img.nodePath || img.filename; return `Could not load images: ${err.message}
`; } } document.getElementById('lib-filter')?.addEventListener('input', () => { clearTimeout(window._libFilterTimer); window._libFilterTimer = setTimeout(loadLibraryImages, 300); }); document.getElementById('btn-refresh-lib')?.addEventListener('click', async () => { const btn = document.getElementById('btn-refresh-lib'); btn.disabled = true; try { const data = await rescanLibrary(); const sync = data.sync || {}; const parts = [`${data.totalImages || 0} image(s)`]; if (sync.nodesCreated) parts.push(`${sync.nodesCreated} folder(s) imported`); if (sync.imagesIndexed) parts.push(`${sync.imagesIndexed} image(s) indexed`); showDdaSuccess(`Library synced — ${parts.join(', ')}.`); } catch (err) { showDdaError(err.message); } finally { btn.disabled = false; } }); initDda();