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
+
+
+
+
+
User Audit
+
+
+
+
+
+
@@ -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=`${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);}
});