diff --git a/public/index.html b/public/index.html index bd2e62d..cf77d10 100644 --- a/public/index.html +++ b/public/index.html @@ -474,7 +474,8 @@ body::before{content:'';position:fixed;inset:0;background:radial-gradient(ellips
Destination Folder
-
+ +
@@ -671,6 +672,17 @@ body::before{content:'';position:fixed;inset:0;background:radial-gradient(ellips
+ + + @@ -970,14 +982,29 @@ function renderFolderTree() { const box = document.getElementById('folder-tree-box'); if (!box) return; box.innerHTML = ''; - // Root row - const rootRow = document.createElement('div'); - rootRow.className = 'folder-tree-row' + (selectedPrefix==='' ? ' active' : ''); - rootRow.innerHTML = `๐Ÿ Root (no folder)`; - rootRow.onclick = () => { selectedPrefix=''; updatePrefixDisplay(); renderFolderTree(); }; - box.appendChild(rootRow); + const searchEl = document.getElementById('folder-search'); + const filter = (searchEl ? searchEl.value.trim().toLowerCase() : ''); + + // Helper: does a node (or any descendant) match the filter? + function matchesFilter(node, pathArr) { + const key = [...pathArr, node.name].join('/'); + if (key.toLowerCase().includes(filter)) return true; + if (node.children) return node.children.some(c => matchesFilter(c, [...pathArr, node.name])); + return false; + } + + // Root row (always shown unless filtering) + if (!filter) { + const rootRow = document.createElement('div'); + rootRow.className = 'folder-tree-row' + (selectedPrefix==='' ? ' active' : ''); + rootRow.innerHTML = `๐Ÿ Root (no folder)`; + rootRow.onclick = () => { selectedPrefix=''; updatePrefixDisplay(); renderFolderTree(); }; + box.appendChild(rootRow); + } + function addRows(nodes, pathArr, container) { nodes.forEach(n => { + if (filter && !matchesFilter(n, pathArr)) return; const fullPath = [...pathArr, n.name]; const key = fullPath.join('/'); const indent = pathArr.length; @@ -999,6 +1026,10 @@ function renderFolderTree() { }); } addRows(folderTree, [], box); + + if (!box.children.length) { + box.innerHTML = '
No folders match your search
'; + } } // Legacy aliases so other code still works @@ -1024,7 +1055,7 @@ async function deleteFolder(pathArr) { try { await api('POST','/api/folders/delete',{path:pathArr}); if (selectedPrefix.startsWith(pathArr.join('/'))) { selectedPrefix=''; updatePrefixDisplay(); } - await loadFolders(); showToast('Folder deleted','success'); + await loadFolders(); await loadAdminFolders(); showToast('Folder deleted','success'); } catch(e) { showToast(e.message,'error'); } } @@ -1218,14 +1249,17 @@ async function loadAmppJobs() { list.innerHTML='
No jobs in queue
'; return; } + // Log first job's keys for debugging field names + if (jobs.length) console.log('[AMPP] Sample job keys:', Object.keys(jobs[0]), 'Full:', JSON.stringify(jobs[0]).substring(0, 500)); jobs.forEach(job => { const el=document.createElement('div'); el.className='job-item'; // AMPP API uses colon-namespaced keys e.g. "state:jobState", "name:text", "job:id" const st=(job['state:jobState']||job.status||job.state||job.jobStatus||'unknown').toLowerCase(); const cls=st.includes('run')||st.includes('active')?'running':st.includes('complet')||st.includes('success')||st.includes('done')?'completed':st.includes('fail')||st.includes('error')||st.includes('abort')?'failed':st.includes('queue')||st.includes('wait')||st.includes('pend')?'queued':'unknown'; - const name=job['name:text']||job['assetName:text']||job.name||job.displayName||job['job:id']||job.id||'Job'; + // Try many possible asset name fields โ€” AMPP responses vary by job type + const name=job['name:text']||job['assetName:text']||job['source:text']||job['sourceFile:text']||job['inputFile:text']||job['input:text']||job.name||job.displayName||job.assetName||job.sourceName||job.sourceFile||job.inputFile||job['job:id']||job.id||'Job'; const jobType=(job['type:jobType']||job['subtype:jobSubtype']||job.type||job.jobType||'').replace(/([A-Z])/g,' $1').trim(); - const creator=job['creator:id']||''; + const creator=job['creator:id']||job.creator||''; const created=job['created:dateTime']||job.created||''; const meta=[created?new Date(created).toLocaleString():'', jobType, creator].filter(Boolean).join(' ยท '); el.innerHTML=`
${esc(name)}
${esc(meta)}
${cls.charAt(0).toUpperCase()+cls.slice(1)}`; @@ -1352,6 +1386,7 @@ async function loadUsers() { ${u.created?new Date(u.created).toLocaleDateString():''} + ${u.username!==currentUser?``:'(you)'} `; tbody.appendChild(tr); @@ -1452,6 +1487,83 @@ function fmtBytes(b) { return (b/1073741824).toFixed(2) + ' GB'; } +// ============================================================ +// USER AUDIT +// ============================================================ +async function openUserAudit(username) { + const modal = document.getElementById('audit-modal'); + modal.style.display = 'flex'; + document.getElementById('audit-modal-title').textContent = `Audit โ€” ${username}`; + const content = document.getElementById('audit-content'); + content.innerHTML = '
Loadingโ€ฆ
'; + try { + const [pd, fd] = await Promise.all([ + api('GET', `/api/users/${encodeURIComponent(username)}/permissions`), + api('GET', '/api/folders') + ]); + if (!pd.success) throw new Error(pd.error); + const allFolders = flattenFolders(fd.tree || []); + const allowed = pd.allowedFolders || []; + const hasRestrictions = allowed.length > 0; + const visibleFolders = hasRestrictions ? allFolders.filter(f => allowed.some(a => f === a || f.startsWith(a + '--'))) : allFolders; + + let html = ''; + + // Role & Quota + html += `
`; + html += `
+
Role
+
${pd.role||'user'}
+
`; + html += `
+
Upload Quota
+
${pd.quotaMB ? `${fmtBytes(pd.uploadedBytes||0)} / ${pd.quotaMB} MB` : 'Unlimited'}
+
`; + html += `
`; + + // Visible pages + const pages = ['Upload']; + if (pd.role === 'admin') pages.push('AMPP Monitor', 'Admin Panel'); + else pages.push('AMPP Monitor'); + html += `
+
Visible Pages
+
${pages.map(p => `${p}`).join('')}
+
`; + + // Admin features + if (pd.role === 'admin') { + html += `
+
Admin Capabilities
+
+ S3 Storage settings, AMPP config, User management, Share Links, Folder management, Add/delete folders on upload page +
+
`; + } + + // Folder access + html += `
+
Folder Access ${hasRestrictions ? '(restricted)' : '(all folders)'}
+
`; + if (!visibleFolders.length) { + html += '
No folders configured
'; + } else { + visibleFolders.forEach(f => { + const depth = (f.match(/--/g) || []).length; + const name = f.includes('--') ? f.split('--').pop() : f; + const isAllowed = !hasRestrictions || allowed.includes(f); + html += `
+ ${isAllowed ? 'โœ…' : '๐Ÿšซ'} ${esc(name)} +
`; + }); + } + html += `
`; + + content.innerHTML = html; + } catch(e) { + content.innerHTML = `
Error: ${e.message}
`; + } +} + // ============================================================ // SHARE LINKS // ============================================================ @@ -1540,7 +1652,14 @@ function renderAdminFolderTree(nodes,container,pathArr) { nodes.forEach(n=>{ const fp=[...pathArr,n.name]; const div=document.createElement('div');div.style.paddingLeft=pathArr.length*20+'px';div.style.marginBottom='.3rem'; - div.innerHTML=`
${n.children.length?'๐Ÿ“':'๐Ÿ“„'}${esc(n.name)}
`; + const row=document.createElement('div'); + row.style.cssText='display:flex;align-items:center;gap:.5rem;padding:.3rem .5rem;border:1px solid var(--border);border-radius:7px;background:var(--bg-card)'; + row.innerHTML=`${n.children.length?'๐Ÿ“':'๐Ÿ“„'}${esc(n.name)}`; + const delBtn=document.createElement('button'); + delBtn.className='btn-danger';delBtn.style.cssText='padding:.18rem .48rem;font-size:.66rem';delBtn.textContent='Delete'; + delBtn.onclick=()=>{ deleteFolder(fp).then(()=>{loadAdminFolders();}); }; + row.appendChild(delBtn); + div.appendChild(row); container.appendChild(div); if(n.children.length){const sub=document.createElement('div');renderAdminFolderTree(n.children,sub,fp);container.appendChild(sub);} });