diff --git a/public/index.html b/public/index.html
index 8cadc0b..33ac30c 100644
--- a/public/index.html
+++ b/public/index.html
@@ -485,6 +485,21 @@ body::before{content:'';position:fixed;inset:0;background:radial-gradient(ellips
+
📂
@@ -816,6 +831,9 @@ let currentUser = localStorage.getItem('dw_user') || null;
let currentRole = localStorage.getItem('dw_role') || null;
let uploadMode = 'http';
let selectedPrefix = '';
+let selectedAmppFolderId = '';
+let selectedAmppFolderName = '';
+let amppFolderCache = [];
let folderTree = [];
let selectedFiles = [];
@@ -861,6 +879,7 @@ function showApp() {
loadS3Config(); loadRelayConfig(); loadAmppConfig(); loadUsers(); loadShareLinks(); populateSlFolderSelect();
}
loadFolders();
+ loadAmppFolders();
loadAmppJobs();
}
@@ -1065,6 +1084,76 @@ async function deleteFolder(pathArr) {
} catch(e) { showToast(e.message,'error'); }
}
+// ============================================================
+// AMPP FOLDER PICKER
+// ============================================================
+async function loadAmppFolders() {
+ const box = document.getElementById('ampp-folder-box');
+ const statusEl = document.getElementById('ampp-folder-status');
+ if (!box) return;
+ box.innerHTML = '
Loading AMPP folders…
';
+ if (statusEl) statusEl.textContent = '— loading…';
+ try {
+ const d = await api('GET', '/api/ampp/folders/list');
+ if (!d.success) {
+ if (d.error && d.error.toLowerCase().includes('not configured')) {
+ box.innerHTML = '
AMPP not configured — set up in Admin → AMPP
';
+ if (statusEl) statusEl.textContent = '— not configured';
+ } else {
+ box.innerHTML = `
Error: ${esc(d.error||'Failed to load folders')}
`;
+ if (statusEl) statusEl.textContent = '— error';
+ }
+ return;
+ }
+ amppFolderCache = d.folders || [];
+ if (statusEl) statusEl.textContent = `— ${amppFolderCache.length} folder${amppFolderCache.length!==1?'s':''}`;
+ renderAmppFolderList();
+ } catch(e) {
+ box.innerHTML = `
Error: ${esc(e.message)}
`;
+ if (statusEl) statusEl.textContent = '— error';
+ }
+}
+
+function renderAmppFolderList() {
+ const box = document.getElementById('ampp-folder-box');
+ if (!box) return;
+ box.innerHTML = '';
+ const searchEl = document.getElementById('ampp-folder-search');
+ const filter = (searchEl ? searchEl.value.trim().toLowerCase() : '');
+ const folders = amppFolderCache.filter(f => !filter || (f.path||f.name||'').toLowerCase().includes(filter));
+
+ // "None" row — always first
+ const noneRow = document.createElement('div');
+ noneRow.className = 'folder-tree-row' + (selectedAmppFolderId === '' ? ' active' : '');
+ noneRow.innerHTML = `
🚫(no AMPP placement)`;
+ noneRow.onclick = () => { selectedAmppFolderId = ''; selectedAmppFolderName = ''; updateAmppFolderDisplay(); renderAmppFolderList(); };
+ box.appendChild(noneRow);
+
+ if (!folders.length && amppFolderCache.length > 0) {
+ box.innerHTML += '
No folders match your search
';
+ return;
+ }
+
+ // Sort by path/name
+ const sorted = [...folders].sort((a, b) => (a.path||a.name||'').localeCompare(b.path||b.name||''));
+ sorted.forEach(f => {
+ const id = f.id || f['folder:id'] || '';
+ const displayName = f.path || f.name || f['name:text'] || id;
+ const depth = (displayName.match(/\//g)||[]).length;
+ const row = document.createElement('div');
+ row.className = 'folder-tree-row' + (selectedAmppFolderId === id ? ' active' : '');
+ row.style.paddingLeft = (0.75 + depth * 1.0) + 'rem';
+ row.innerHTML = `
📁${esc(displayName)}`;
+ row.onclick = () => { selectedAmppFolderId = id; selectedAmppFolderName = displayName; updateAmppFolderDisplay(); renderAmppFolderList(); };
+ box.appendChild(row);
+ });
+}
+
+function updateAmppFolderDisplay() {
+ const el = document.getElementById('ampp-folder-display');
+ if (el) el.textContent = selectedAmppFolderName || '(none — skip placement)';
+}
+
// ============================================================
// FILE HANDLING
// ============================================================
@@ -1223,6 +1312,7 @@ async function uploadFilePresigned(item, idx) {
setFileStatus(idx, 'uploading', 'Getting URL\u2026');
const pre = await api('POST', '/api/presigned', {
filename: item.name, prefix: selectedPrefix, contentType: mime, size: item.file.size,
+ amppFolderId: selectedAmppFolderId, amppFolderName: selectedAmppFolderName,
});
if (!pre.success) throw new Error(pre.error || 'Failed to get presigned URL');
return new Promise((resolve, reject) => {
@@ -1237,7 +1327,7 @@ async function uploadFilePresigned(item, idx) {
});
xhr.addEventListener('load', async () => {
if (xhr.status >= 200 && xhr.status < 300) {
- try { await api('POST', '/api/presigned/complete', { key: pre.key, size: item.file.size }); } catch(_) {}
+ try { await api('POST', '/api/presigned/complete', { key: pre.key, size: item.file.size, filename: item.name, amppFolderId: selectedAmppFolderId, amppFolderName: selectedAmppFolderName }); } catch(_) {}
resolve({ key: pre.key });
} else {
reject(new Error(`S3 returned ${xhr.status}: ${xhr.statusText}`));
@@ -1311,8 +1401,8 @@ async function uploadFilePresigned(item, idx) {
if (pb) pb.style.width = '100%';
setFileStatus(idx, 'uploading', '100%');
- // Notify server for quota tracking
- try { await api('POST', '/api/presigned/complete', { key, size: item.file.size }); } catch(_) {}
+ // Notify server for quota tracking + queue AMPP placement
+ try { await api('POST', '/api/presigned/complete', { key, size: item.file.size, filename: item.name, amppFolderId: selectedAmppFolderId, amppFolderName: selectedAmppFolderName }); } catch(_) {}
return { key };
}