// public/js/adminStorage.js
import { t } from './i18n.js?v={{APP_QVER}}';
import { showToast } from './domUtils.js?v={{APP_QVER}}';
import { sendRequest } from './networkUtils.js?v={{APP_QVER}}';
// tiny helper like tf in adminPanel
const tf = (key, fallback) => {
const v = t(key);
return (v && v !== key) ? v : fallback;
};
function formatBytes(bytes) {
bytes = Number(bytes) || 0;
if (bytes <= 0) return '0 B';
const units = ['B','KB','MB','GB','TB','PB'];
const i = Math.min(
units.length - 1,
Math.floor(Math.log(bytes) / Math.log(1024))
);
const val = bytes / Math.pow(1024, i);
return `${val.toFixed(val >= 100 ? 0 : val >= 10 ? 1 : 2)} ${units[i]}`;
}
function formatDate(ts) {
if (!ts) return '';
try {
const d = new Date(ts * 1000);
if (Number.isNaN(d.getTime())) return '';
return d.toLocaleString();
} catch {
return '';
}
}
function getCsrfToken() {
return (document.querySelector('meta[name="csrf-token"]')?.content || '');
}
let confirmModalEl = null;
let showAllTopFolders = false;
function ensureConfirmModal() {
if (confirmModalEl) return confirmModalEl;
const wrapper = document.createElement('div');
wrapper.innerHTML = `
${tf('confirm','Confirm')}
`.trim();
confirmModalEl = wrapper.firstChild;
document.body.appendChild(confirmModalEl);
return confirmModalEl;
}
function showConfirmDialog({ title, message, confirmLabel }) {
// Fallback to window.confirm if Bootstrap is not available
if (!window.bootstrap || !window.bootstrap.Modal) {
return Promise.resolve(window.confirm(message));
}
return new Promise(resolve => {
const el = ensureConfirmModal();
const titleEl = el.querySelector('#adminStorageConfirmTitle');
const msgEl = el.querySelector('#adminStorageConfirmMessage');
const okBtn = el.querySelector('#adminStorageConfirmOk');
if (titleEl) titleEl.textContent = title || tf('confirm','Confirm');
if (msgEl) msgEl.textContent = message || '';
if (okBtn) okBtn.textContent = confirmLabel || tf('delete','Delete');
const modal = window.bootstrap.Modal.getOrCreateInstance(el);
const handleOk = () => {
cleanup();
resolve(true);
};
const handleHidden = () => {
cleanup();
resolve(false);
};
function cleanup() {
if (!el) return;
el.removeEventListener('hidden.bs.modal', handleHidden);
if (okBtn) okBtn.removeEventListener('click', handleOk);
}
if (okBtn) okBtn.addEventListener('click', handleOk, { once: true });
el.addEventListener('hidden.bs.modal', handleHidden, { once: true });
modal.show();
});
}
// --- module-level tracking ---
// snapshot / scanning
let lastGeneratedAt = 0;
let scanPollTimer = null;
// Pro-only dangerous mode: deep delete for folders
let deepDeleteEnabled = false;
// pro explorer
let isProGlobal = false;
let currentFolderKey = 'root';
let currentExplorerTab = 'folders'; // "folders" | "topFiles"
let folderMinSizeBytes = 0;
let topFilesMinSizeBytes = 0;
// ---------- Scan status ----------
function setScanStatus(isScanning) {
const statusEl = document.getElementById('adminStorageScanStatus');
if (!statusEl) return;
if (!isScanning) {
statusEl.innerHTML = '';
return;
}
statusEl.innerHTML = `
${tf('storage_scan_in_progress', 'Disk usage scan in progress...')}
`;
}
// Make sure delete buttons visually reflect whether deep delete is enabled
function updateDeleteButtonsForDeepDelete() {
const host = document.getElementById('adminStorageProTeaser');
if (!host) return;
host.querySelectorAll('.admin-storage-delete-folder').forEach(btn => {
const icon = btn.querySelector('.material-icons');
if (deepDeleteEnabled) {
btn.classList.remove('btn-outline-danger');
btn.classList.add('btn-outline-warning');
// let the icon inherit currentColor so it goes white on hover
if (icon) icon.classList.remove('text-warning');
btn.title = tf('storage_deep_delete_folder_title', 'Deep delete folder (no Trash)');
} else {
btn.classList.remove('btn-outline-warning');
btn.classList.add('btn-outline-danger');
if (icon) icon.classList.remove('text-warning');
btn.title = tf('delete_folder', 'Delete folder');
}
});
}
// Wire the toggle switch in the explorer header
function wireDeepDeleteToggle() {
const toggle = document.getElementById('adminStorageDeepDeleteToggle');
if (!toggle) return;
if (toggle.dataset.wired === '1') return;
toggle.dataset.wired = '1';
toggle.addEventListener('change', () => {
deepDeleteEnabled = !!toggle.checked;
updateDeleteButtonsForDeepDelete();
});
}
// ---------- Layout ----------
/**
* Render the basic layout (header, summary area, tabs placeholder) into storageContent.
* Pro explorer UI gets injected into #adminStorageProTeaser later.
*/
function renderBaseLayout(container, { isPro }) {
container.innerHTML = `
${tf('storage_disk_usage', 'Storage / Disk Usage')}
${tf(
'storage_disk_usage_help',
'Analyze which folders and files are consuming space under your FileRise upload root.'
)}
${
isPro
? tf(
'storage_rescan_hint_pro',
'Run a fresh disk usage snapshot when storage changes.'
)
: tf(
'storage_rescan_cli_hint',
'Click Rescan to run a snapshot now, or schedule the CLI scanner via cron.'
)
}
${tf('loading', 'Loading...')}
${
isPro
? `
`
: `
${tf('storage_explorer', 'Storage explorer')}
Pro
${tf(
'storage_explorer_help',
'Drill down into folders or inspect the largest files.'
)}
${tf('name','Name')}
${tf('size','Size')}
%
${tf('files','Files')}
${tf('modified','Modified')}
Pro
${tf('storage_pro_locked_title','Storage explorer is a Pro feature')}
${tf(
'storage_pro_locked_body',
'Upgrade to FileRise Pro to unlock folder drill-down, top files view, and inline cleanup tools.'
)}
`
}
`;
}
// ---------- Summary / volumes ----------
/**
* Fetch summary JSON only (no UI changes) – used for polling after rescan.
*/
async function fetchSummaryRaw() {
try {
const res = await fetch('/api/admin/diskUsageSummary.php?topFolders=20&topFiles=0', {
credentials: 'include',
cache: 'no-store',
headers: { 'Cache-Control': 'no-store' }
});
const text = await res.text();
return JSON.parse(text || '{}');
} catch (e) {
console.error('fetchSummaryRaw error', e);
return null;
}
}
async function refreshStorageSummary() {
const summaryEl = document.getElementById('adminStorageSummary');
if (!summaryEl) return;
summaryEl.innerHTML = `
${tf('loading', 'Loading...')}
`;
const data = await fetchSummaryRaw();
if (!data || !data.ok) {
if (data && data.error === 'no_snapshot') {
const cmd = 'php src/cli/disk_usage_scan.php';
summaryEl.innerHTML = `
${tf(
'storage_no_snapshot',
'No disk usage snapshot found. Run the CLI scanner once to generate the first snapshot.'
)}
${cmd}
`;
return;
}
summaryEl.innerHTML = `
${tf('storage_summary_error', 'Unable to load disk usage summary.')}
${tf('storage_children_error','Please upgrade to the latest FileRise Pro bundle to use the Storage explorer.')}
`;
return;
}
if (!data || !data.ok) {
if (data && data.error === 'no_snapshot') {
inner.innerHTML = `
${tf('storage_no_snapshot','No disk usage snapshot found. Run the disk usage scan first.')}
`;
} else {
// Special-case: backend missing ProDiskUsage / outdated Pro bundle
let msgKey = 'storage_children_error';
let fallback = 'Please upgrade to the latest FileRise Pro bundle to use the Storage explorer.';
if (
data &&
data.error === 'internal_error' &&
data.message &&
/ProDiskUsage/i.test(String(data.message))
) {
msgKey = 'storage_pro_bundle_outdated';
fallback = 'Please upgrade to the latest FileRise Pro bundle to use the Storage explorer.';
}
inner.innerHTML = `