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 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">
|
||||
<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>
|
||||
|
|
@ -671,6 +672,17 @@ body::before{content:'';position:fixed;inset:0;background:radial-gradient(ellips
|
|||
<div class="status-msg" id="perm-status"></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>
|
||||
|
||||
<!-- Share Links -->
|
||||
|
|
@ -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 = `<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);
|
||||
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 = `<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) {
|
||||
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 = '<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
|
||||
|
|
@ -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='<div style="color:var(--text-dim);text-align:center;padding:2rem;font-size:.85rem">No jobs in queue</div>';
|
||||
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=`<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="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="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>'}
|
||||
</td>`;
|
||||
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 = '<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
|
||||
// ============================================================
|
||||
|
|
@ -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=`<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);
|
||||
if(n.children.length){const sub=document.createElement('div');renderAdminFolderTree(n.children,sub,fp);container.appendChild(sub);}
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue