QOL improvements: folder search, user audit, admin folders fix, AMPP debug
- Add search/filter input to destination folder browser with 320px scrollable container - Add User Audit button showing each user's visible pages, role, quota, and folder access permissions - Fix admin Folders tab delete buttons (were broken due to JSON.stringify quote conflicts in innerHTML onclick handlers) - Add more AMPP job name field fallbacks and debug logging to diagnose asset name display issue Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
ad5f9a4186
commit
0f81cac6ec
1 changed files with 130 additions and 11 deletions
|
|
@ -474,7 +474,8 @@ body::before{content:'';position:fixed;inset:0;background:radial-gradient(ellips
|
||||||
|
|
||||||
<div style="margin-bottom:1.5rem">
|
<div style="margin-bottom:1.5rem">
|
||||||
<div class="section-title">Destination Folder</div>
|
<div class="section-title">Destination Folder</div>
|
||||||
<div class="folder-tree-box" id="folder-tree-box"></div>
|
<input class="form-input" id="folder-search" placeholder="🔍 Search folders…" oninput="renderFolderTree()" style="margin-bottom:.4rem;font-size:.78rem;padding:.4rem .65rem"/>
|
||||||
|
<div class="folder-tree-box" id="folder-tree-box" style="max-height:320px;overflow-y:auto"></div>
|
||||||
<div class="add-row" style="margin-top:.55rem" id="add-folder-row">
|
<div class="add-row" style="margin-top:.55rem" id="add-folder-row">
|
||||||
<input class="add-input" id="new-folder-input" placeholder="New folder name…" onkeydown="if(event.key==='Enter')addFolder()"/>
|
<input class="add-input" id="new-folder-input" placeholder="New folder name…" onkeydown="if(event.key==='Enter')addFolder()"/>
|
||||||
<button class="btn-small" onclick="addFolder()">+ Add</button>
|
<button class="btn-small" onclick="addFolder()">+ Add</button>
|
||||||
|
|
@ -671,6 +672,17 @@ body::before{content:'';position:fixed;inset:0;background:radial-gradient(ellips
|
||||||
<div class="status-msg" id="perm-status"></div>
|
<div class="status-msg" id="perm-status"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- User Audit modal -->
|
||||||
|
<div id="audit-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.7);z-index:500;align-items:center;justify-content:center">
|
||||||
|
<div style="background:var(--bg-card);border:1px solid var(--border);border-radius:16px;padding:2rem;width:520px;max-width:95vw;max-height:90vh;overflow-y:auto">
|
||||||
|
<div class="section-title" id="audit-modal-title">User Audit</div>
|
||||||
|
<div id="audit-content" style="margin-top:1rem"></div>
|
||||||
|
<div class="btn-row" style="margin-top:1.5rem">
|
||||||
|
<button class="btn-secondary" onclick="document.getElementById('audit-modal').style.display='none'">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Share Links -->
|
<!-- Share Links -->
|
||||||
|
|
@ -970,14 +982,29 @@ function renderFolderTree() {
|
||||||
const box = document.getElementById('folder-tree-box');
|
const box = document.getElementById('folder-tree-box');
|
||||||
if (!box) return;
|
if (!box) return;
|
||||||
box.innerHTML = '';
|
box.innerHTML = '';
|
||||||
// Root row
|
const searchEl = document.getElementById('folder-search');
|
||||||
const rootRow = document.createElement('div');
|
const filter = (searchEl ? searchEl.value.trim().toLowerCase() : '');
|
||||||
rootRow.className = 'folder-tree-row' + (selectedPrefix==='' ? ' active' : '');
|
|
||||||
rootRow.innerHTML = `<span class="ftr-icon">🏠</span><span class="ftr-name" style="font-style:${selectedPrefix===''?'normal':'italic'};color:${selectedPrefix===''?'':'var(--text-dim)'}">Root (no folder)</span>`;
|
// Helper: does a node (or any descendant) match the filter?
|
||||||
rootRow.onclick = () => { selectedPrefix=''; updatePrefixDisplay(); renderFolderTree(); };
|
function matchesFilter(node, pathArr) {
|
||||||
box.appendChild(rootRow);
|
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 = `<span class="ftr-icon">🏠</span><span class="ftr-name" style="font-style:${selectedPrefix===''?'normal':'italic'};color:${selectedPrefix===''?'':'var(--text-dim)'}">Root (no folder)</span>`;
|
||||||
|
rootRow.onclick = () => { selectedPrefix=''; updatePrefixDisplay(); renderFolderTree(); };
|
||||||
|
box.appendChild(rootRow);
|
||||||
|
}
|
||||||
|
|
||||||
function addRows(nodes, pathArr, container) {
|
function addRows(nodes, pathArr, container) {
|
||||||
nodes.forEach(n => {
|
nodes.forEach(n => {
|
||||||
|
if (filter && !matchesFilter(n, pathArr)) return;
|
||||||
const fullPath = [...pathArr, n.name];
|
const fullPath = [...pathArr, n.name];
|
||||||
const key = fullPath.join('/');
|
const key = fullPath.join('/');
|
||||||
const indent = pathArr.length;
|
const indent = pathArr.length;
|
||||||
|
|
@ -999,6 +1026,10 @@ function renderFolderTree() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
addRows(folderTree, [], box);
|
addRows(folderTree, [], box);
|
||||||
|
|
||||||
|
if (!box.children.length) {
|
||||||
|
box.innerHTML = '<div style="color:var(--text-dim);font-size:.8rem;padding:.75rem;text-align:center">No folders match your search</div>';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Legacy aliases so other code still works
|
// Legacy aliases so other code still works
|
||||||
|
|
@ -1024,7 +1055,7 @@ async function deleteFolder(pathArr) {
|
||||||
try {
|
try {
|
||||||
await api('POST','/api/folders/delete',{path:pathArr});
|
await api('POST','/api/folders/delete',{path:pathArr});
|
||||||
if (selectedPrefix.startsWith(pathArr.join('/'))) { selectedPrefix=''; updatePrefixDisplay(); }
|
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'); }
|
} catch(e) { showToast(e.message,'error'); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1218,14 +1249,17 @@ async function loadAmppJobs() {
|
||||||
list.innerHTML='<div style="color:var(--text-dim);text-align:center;padding:2rem;font-size:.85rem">No jobs in queue</div>';
|
list.innerHTML='<div style="color:var(--text-dim);text-align:center;padding:2rem;font-size:.85rem">No jobs in queue</div>';
|
||||||
return;
|
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 => {
|
jobs.forEach(job => {
|
||||||
const el=document.createElement('div'); el.className='job-item';
|
const el=document.createElement('div'); el.className='job-item';
|
||||||
// AMPP API uses colon-namespaced keys e.g. "state:jobState", "name:text", "job:id"
|
// 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 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 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 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 created=job['created:dateTime']||job.created||'';
|
||||||
const meta=[created?new Date(created).toLocaleString():'', jobType, creator].filter(Boolean).join(' · ');
|
const meta=[created?new Date(created).toLocaleString():'', jobType, creator].filter(Boolean).join(' · ');
|
||||||
el.innerHTML=`<div class="job-dot ${cls}"></div><div class="job-info"><div class="job-name">${esc(name)}</div><div class="job-meta">${esc(meta)}</div></div><span class="job-status ${cls}">${cls.charAt(0).toUpperCase()+cls.slice(1)}</span>`;
|
el.innerHTML=`<div class="job-dot ${cls}"></div><div class="job-info"><div class="job-name">${esc(name)}</div><div class="job-meta">${esc(meta)}</div></div><span class="job-status ${cls}">${cls.charAt(0).toUpperCase()+cls.slice(1)}</span>`;
|
||||||
|
|
@ -1352,6 +1386,7 @@ async function loadUsers() {
|
||||||
<td style="color:var(--text-dim);font-size:.73rem;font-family:'JetBrains Mono',monospace">${u.created?new Date(u.created).toLocaleDateString():''}</td>
|
<td style="color:var(--text-dim);font-size:.73rem;font-family:'JetBrains Mono',monospace">${u.created?new Date(u.created).toLocaleDateString():''}</td>
|
||||||
<td style="display:flex;gap:.35rem;flex-wrap:wrap">
|
<td style="display:flex;gap:.35rem;flex-wrap:wrap">
|
||||||
<button class="btn-secondary" style="padding:.22rem .55rem;font-size:.68rem" onclick="openPermissions('${esc(u.username)}')">⚙ Perms</button>
|
<button class="btn-secondary" style="padding:.22rem .55rem;font-size:.68rem" onclick="openPermissions('${esc(u.username)}')">⚙ Perms</button>
|
||||||
|
<button class="btn-secondary" style="padding:.22rem .55rem;font-size:.68rem" onclick="openUserAudit('${esc(u.username)}')">👁 Audit</button>
|
||||||
${u.username!==currentUser?`<button class="btn-danger" style="padding:.22rem .55rem;font-size:.68rem" onclick="deleteUser('${esc(u.username)}')">Delete</button>`:'<span style="color:var(--text-dim);font-size:.73rem">(you)</span>'}
|
${u.username!==currentUser?`<button class="btn-danger" style="padding:.22rem .55rem;font-size:.68rem" onclick="deleteUser('${esc(u.username)}')">Delete</button>`:'<span style="color:var(--text-dim);font-size:.73rem">(you)</span>'}
|
||||||
</td>`;
|
</td>`;
|
||||||
tbody.appendChild(tr);
|
tbody.appendChild(tr);
|
||||||
|
|
@ -1452,6 +1487,83 @@ function fmtBytes(b) {
|
||||||
return (b/1073741824).toFixed(2) + ' GB';
|
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 = '<div style="color:var(--text-dim);font-size:.82rem">Loading…</div>';
|
||||||
|
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 += `<div style="display:grid;grid-template-columns:1fr 1fr;gap:.75rem;margin-bottom:1.25rem">`;
|
||||||
|
html += `<div style="background:var(--bg-secondary);border:1px solid var(--border);border-radius:8px;padding:.75rem">
|
||||||
|
<div style="font-size:.68rem;text-transform:uppercase;letter-spacing:.08em;color:var(--text-dim);margin-bottom:.25rem">Role</div>
|
||||||
|
<div style="font-size:.88rem;font-weight:600"><span class="role-badge ${pd.role||'user'}">${pd.role||'user'}</span></div>
|
||||||
|
</div>`;
|
||||||
|
html += `<div style="background:var(--bg-secondary);border:1px solid var(--border);border-radius:8px;padding:.75rem">
|
||||||
|
<div style="font-size:.68rem;text-transform:uppercase;letter-spacing:.08em;color:var(--text-dim);margin-bottom:.25rem">Upload Quota</div>
|
||||||
|
<div style="font-size:.88rem;font-weight:600">${pd.quotaMB ? `${fmtBytes(pd.uploadedBytes||0)} / ${pd.quotaMB} MB` : 'Unlimited'}</div>
|
||||||
|
</div>`;
|
||||||
|
html += `</div>`;
|
||||||
|
|
||||||
|
// Visible pages
|
||||||
|
const pages = ['Upload'];
|
||||||
|
if (pd.role === 'admin') pages.push('AMPP Monitor', 'Admin Panel');
|
||||||
|
else pages.push('AMPP Monitor');
|
||||||
|
html += `<div style="margin-bottom:1.25rem">
|
||||||
|
<div style="font-size:.72rem;text-transform:uppercase;letter-spacing:.08em;color:var(--text-dim);margin-bottom:.5rem">Visible Pages</div>
|
||||||
|
<div style="display:flex;gap:.4rem;flex-wrap:wrap">${pages.map(p => `<span style="background:var(--accent-glow);color:var(--blue-bright);padding:.2rem .6rem;border-radius:6px;font-size:.75rem;font-weight:500">${p}</span>`).join('')}</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
// Admin features
|
||||||
|
if (pd.role === 'admin') {
|
||||||
|
html += `<div style="margin-bottom:1.25rem">
|
||||||
|
<div style="font-size:.72rem;text-transform:uppercase;letter-spacing:.08em;color:var(--text-dim);margin-bottom:.5rem">Admin Capabilities</div>
|
||||||
|
<div style="font-size:.8rem;color:var(--text-secondary);line-height:1.6">
|
||||||
|
S3 Storage settings, AMPP config, User management, Share Links, Folder management, Add/delete folders on upload page
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Folder access
|
||||||
|
html += `<div>
|
||||||
|
<div style="font-size:.72rem;text-transform:uppercase;letter-spacing:.08em;color:var(--text-dim);margin-bottom:.5rem">Folder Access ${hasRestrictions ? '<span style="color:var(--warning)">(restricted)</span>' : '<span style="color:var(--success)">(all folders)</span>'}</div>
|
||||||
|
<div style="max-height:200px;overflow-y:auto;background:var(--bg-secondary);border:1px solid var(--border);border-radius:8px;padding:.5rem">`;
|
||||||
|
if (!visibleFolders.length) {
|
||||||
|
html += '<div style="color:var(--text-dim);font-size:.78rem;padding:.25rem">No folders configured</div>';
|
||||||
|
} 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 += `<div style="padding:.2rem .4rem .2rem ${.4 + depth * 1}rem;font-family:'JetBrains Mono',monospace;font-size:.75rem;color:${isAllowed ? 'var(--text-secondary)' : 'var(--text-dim)'};display:flex;align-items:center;gap:.4rem">
|
||||||
|
<span>${isAllowed ? '✅' : '🚫'}</span> ${esc(name)}
|
||||||
|
</div>`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
html += `</div></div>`;
|
||||||
|
|
||||||
|
content.innerHTML = html;
|
||||||
|
} catch(e) {
|
||||||
|
content.innerHTML = `<div style="color:var(--error);font-size:.82rem">Error: ${e.message}</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// SHARE LINKS
|
// SHARE LINKS
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -1540,7 +1652,14 @@ function renderAdminFolderTree(nodes,container,pathArr) {
|
||||||
nodes.forEach(n=>{
|
nodes.forEach(n=>{
|
||||||
const fp=[...pathArr,n.name];
|
const fp=[...pathArr,n.name];
|
||||||
const div=document.createElement('div');div.style.paddingLeft=pathArr.length*20+'px';div.style.marginBottom='.3rem';
|
const div=document.createElement('div');div.style.paddingLeft=pathArr.length*20+'px';div.style.marginBottom='.3rem';
|
||||||
div.innerHTML=`<div style="display:flex;align-items:center;gap:.5rem;padding:.3rem .5rem;border:1px solid var(--border);border-radius:7px;background:var(--bg-card)"><span style="font-size:.82rem">${n.children.length?'📁':'📄'}</span><span style="font-family:'JetBrains Mono',monospace;font-size:.78rem;flex:1">${esc(n.name)}</span><button class="btn-danger" style="padding:.18rem .48rem;font-size:.66rem" onclick="deleteFolder(${JSON.stringify(fp)})">Delete</button></div>`;
|
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=`<span style="font-size:.82rem">${n.children.length?'📁':'📄'}</span><span style="font-family:'JetBrains Mono',monospace;font-size:.78rem;flex:1">${esc(n.name)}</span>`;
|
||||||
|
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);
|
container.appendChild(div);
|
||||||
if(n.children.length){const sub=document.createElement('div');renderAdminFolderTree(n.children,sub,fp);container.appendChild(sub);}
|
if(n.children.length){const sub=document.createElement('div');renderAdminFolderTree(n.children,sub,fp);container.appendChild(sub);}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue