// Orchard Web UI const API_BASE = '/api/v1'; // State let currentGrove = null; let currentTree = null; // Initialize document.addEventListener('DOMContentLoaded', () => { setupNavigation(); loadGroves(); }); // Navigation function setupNavigation() { document.querySelectorAll('.nav-link').forEach(link => { link.addEventListener('click', (e) => { e.preventDefault(); const view = link.dataset.view; showView(view); document.querySelectorAll('.nav-link').forEach(l => l.classList.remove('active')); link.classList.add('active'); }); }); } function showView(viewName) { document.querySelectorAll('.view').forEach(v => v.classList.remove('active')); document.getElementById(`${viewName}-view`).classList.add('active'); // Load data for view if (viewName === 'groves') { loadGroves(); } else if (viewName === 'upload') { loadGrovesForUpload(); } } // Groves async function loadGroves() { const container = document.getElementById('groves-list'); container.innerHTML = '
Loading groves...
'; try { const response = await fetch(`${API_BASE}/groves`); const groves = await response.json(); if (!groves || groves.length === 0) { container.innerHTML = `

No groves yet

Create your first grove to get started

`; return; } container.innerHTML = groves.map(grove => `

🌳 ${escapeHtml(grove.name)} ${grove.is_public ? 'Public' : 'Private'}

${escapeHtml(grove.description || 'No description')}

Created ${formatDate(grove.created_at)}
`).join(''); } catch (error) { container.innerHTML = `

Error loading groves: ${error.message}

`; } } async function viewGrove(groveName) { currentGrove = groveName; document.getElementById('grove-detail-title').textContent = groveName; // Load grove info try { const response = await fetch(`${API_BASE}/groves/${groveName}`); const grove = await response.json(); document.getElementById('grove-info').innerHTML = `
${escapeHtml(grove.name)}
${grove.is_public ? 'Public' : 'Private'}
${formatDate(grove.created_at)}
${escapeHtml(grove.description || 'No description')}
`; } catch (error) { console.error('Error loading grove:', error); } // Load trees await loadTrees(groveName); // Show view document.querySelectorAll('.view').forEach(v => v.classList.remove('active')); document.getElementById('grove-detail-view').classList.add('active'); } async function loadTrees(groveName) { const container = document.getElementById('trees-list'); container.innerHTML = '
Loading trees...
'; try { const response = await fetch(`${API_BASE}/grove/${groveName}/trees`); const trees = await response.json(); if (!trees || trees.length === 0) { container.innerHTML = `

No trees yet

Create a tree to store artifacts

`; return; } container.innerHTML = trees.map(tree => `

🌲 ${escapeHtml(tree.name)}

${escapeHtml(tree.description || 'No description')}

Created ${formatDate(tree.created_at)}
`).join(''); } catch (error) { container.innerHTML = `

Error loading trees: ${error.message}

`; } } async function viewTree(groveName, treeName) { currentGrove = groveName; currentTree = treeName; document.getElementById('tree-detail-title').textContent = `${groveName} / ${treeName}`; document.getElementById('tree-info').innerHTML = `
${escapeHtml(groveName)}
${escapeHtml(treeName)}
`; // Load grafts await loadGrafts(groveName, treeName); // Show view document.querySelectorAll('.view').forEach(v => v.classList.remove('active')); document.getElementById('tree-detail-view').classList.add('active'); } async function loadGrafts(groveName, treeName) { const container = document.getElementById('grafts-list'); container.innerHTML = '
Loading versions...
'; try { const response = await fetch(`${API_BASE}/grove/${groveName}/${treeName}/grafts`); const grafts = await response.json(); if (!grafts || grafts.length === 0) { container.innerHTML = `

No versions yet

Upload an artifact to create the first version

`; return; } container.innerHTML = ` ${grafts.map(graft => ` `).join('')}
Tag Fruit ID Created Actions
${escapeHtml(graft.name)} ${graft.fruit_id.substring(0, 16)}... ${formatDate(graft.created_at)} Download
`; } catch (error) { container.innerHTML = `

Error loading versions: ${error.message}

`; } } function backToGrove() { if (currentGrove) { viewGrove(currentGrove); } else { showView('groves'); } } // Create Grove function showCreateGroveModal() { document.getElementById('create-grove-modal').classList.remove('hidden'); document.getElementById('grove-name').focus(); } async function createGrove(e) { e.preventDefault(); const name = document.getElementById('grove-name').value; const description = document.getElementById('grove-description').value; const isPublic = document.getElementById('grove-public').checked; try { const response = await fetch(`${API_BASE}/groves`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, description, is_public: isPublic }) }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to create grove'); } showToast('Grove created successfully!', 'success'); closeModals(); loadGroves(); // Reset form document.getElementById('grove-name').value = ''; document.getElementById('grove-description').value = ''; document.getElementById('grove-public').checked = true; } catch (error) { showToast(error.message, 'error'); } } // Create Tree function showCreateTreeModal() { document.getElementById('create-tree-modal').classList.remove('hidden'); document.getElementById('tree-name').focus(); } async function createTree(e) { e.preventDefault(); const name = document.getElementById('tree-name').value; const description = document.getElementById('tree-description').value; try { const response = await fetch(`${API_BASE}/grove/${currentGrove}/trees`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, description }) }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to create tree'); } showToast('Tree created successfully!', 'success'); closeModals(); loadTrees(currentGrove); // Reset form document.getElementById('tree-name').value = ''; document.getElementById('tree-description').value = ''; } catch (error) { showToast(error.message, 'error'); } } // Upload async function loadGrovesForUpload() { const select = document.getElementById('upload-grove'); select.innerHTML = ''; try { const response = await fetch(`${API_BASE}/groves`); const groves = await response.json(); select.innerHTML = '' + (groves || []).map(g => ``).join(''); } catch (error) { select.innerHTML = ''; } } async function loadTreesForUpload() { const groveName = document.getElementById('upload-grove').value; const select = document.getElementById('upload-tree'); if (!groveName) { select.innerHTML = ''; return; } select.innerHTML = ''; try { const response = await fetch(`${API_BASE}/grove/${groveName}/trees`); const trees = await response.json(); select.innerHTML = '' + (trees || []).map(t => ``).join(''); } catch (error) { select.innerHTML = ''; } } function updateFileName() { const input = document.getElementById('upload-file'); const display = document.getElementById('file-name'); display.textContent = input.files[0]?.name || ''; } async function uploadArtifact(e) { e.preventDefault(); const grove = document.getElementById('upload-grove').value; const tree = document.getElementById('upload-tree').value; const file = document.getElementById('upload-file').files[0]; const tag = document.getElementById('upload-tag').value; const formData = new FormData(); formData.append('file', file); if (tag) formData.append('tag', tag); const resultDiv = document.getElementById('upload-result'); resultDiv.innerHTML = '
Uploading...
'; resultDiv.classList.remove('hidden', 'success', 'error'); try { const response = await fetch(`${API_BASE}/grove/${grove}/${tree}/cultivate`, { method: 'POST', body: formData }); const result = await response.json(); if (!response.ok) { throw new Error(result.error || 'Upload failed'); } resultDiv.classList.add('success'); resultDiv.innerHTML = `

Upload Successful!

Fruit ID
${result.fruit_id}
Size
${formatBytes(result.size)}
Grove
${escapeHtml(result.grove)}
Tree
${escapeHtml(result.tree)}
${result.tag ? `
Tag
${escapeHtml(result.tag)}
` : ''}
Download Artifact
`; showToast('Artifact uploaded successfully!', 'success'); } catch (error) { resultDiv.classList.add('error'); resultDiv.innerHTML = `

Upload Failed

${escapeHtml(error.message)}

`; showToast(error.message, 'error'); } } async function uploadToTree(e) { e.preventDefault(); const file = document.getElementById('tree-upload-file').files[0]; const tag = document.getElementById('tree-upload-tag').value; const formData = new FormData(); formData.append('file', file); if (tag) formData.append('tag', tag); try { const response = await fetch(`${API_BASE}/grove/${currentGrove}/${currentTree}/cultivate`, { method: 'POST', body: formData }); const result = await response.json(); if (!response.ok) { throw new Error(result.error || 'Upload failed'); } showToast('Artifact uploaded successfully!', 'success'); // Reload grafts loadGrafts(currentGrove, currentTree); // Reset form document.getElementById('tree-upload-file').value = ''; document.getElementById('tree-upload-tag').value = ''; } catch (error) { showToast(error.message, 'error'); } } // Search function handleSearchKeyup(e) { if (e.key === 'Enter') { searchFruit(); } } async function searchFruit() { const fruitId = document.getElementById('search-input').value.trim(); const resultDiv = document.getElementById('search-result'); if (!fruitId) { showToast('Please enter a fruit ID', 'error'); return; } resultDiv.innerHTML = '
Searching...
'; resultDiv.classList.remove('hidden', 'success', 'error'); try { const response = await fetch(`${API_BASE}/fruit/${fruitId}`); const result = await response.json(); if (!response.ok) { throw new Error(result.error || 'Fruit not found'); } resultDiv.classList.add('success'); resultDiv.innerHTML = `

Fruit Found

Fruit ID
${result.id}
Original Name
${escapeHtml(result.original_name || 'Unknown')}
Size
${formatBytes(result.size)}
Content Type
${escapeHtml(result.content_type || 'Unknown')}
Created
${formatDate(result.created_at)}
Reference Count
${result.ref_count}
`; } catch (error) { resultDiv.classList.add('error'); resultDiv.innerHTML = `

Not Found

${escapeHtml(error.message)}

`; } } // Modals function closeModals() { document.querySelectorAll('.modal').forEach(m => m.classList.add('hidden')); } // Close modal on outside click document.addEventListener('click', (e) => { if (e.target.classList.contains('modal')) { closeModals(); } }); // Close modal on Escape document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { closeModals(); } }); // Toast notifications function showToast(message, type = 'info') { const container = document.getElementById('toast-container'); const toast = document.createElement('div'); toast.className = `toast ${type}`; toast.textContent = message; container.appendChild(toast); setTimeout(() => { toast.remove(); }, 3000); } // Utilities function escapeHtml(str) { if (!str) return ''; const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } function formatDate(dateStr) { if (!dateStr) return 'Unknown'; const date = new Date(dateStr); return date.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); } function formatBytes(bytes) { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } function copyToClipboard(text) { navigator.clipboard.writeText(text).then(() => { showToast('Copied to clipboard!', 'success'); }); }