(function (root, factory) { if (typeof module === 'object' && module.exports) module.exports = factory; else { root.createDeviceStorage = factory; root.createDeviceStorage.mount = document => factory({ summary:document.querySelector('#device-storage-summary'), detail:document.querySelector('#device-storage-detail'), clearCachesButton:document.querySelector('#clear-device-caches'), clearAllButton:document.querySelector('#clear-private-device-data'), localStorage:root.localStorage, sessionStorage:root.sessionStorage, caches:root.caches, storageManager:root.navigator?.storage, privateDatabases:root.stackchainPrivateDatabases, inspectPrivateDatabases:root.inspectStackchainPrivateDatabases, clearPrivateDeviceData:root.stackchainPrivateDeviceData, }); } })(typeof self !== 'undefined' ? self : this, function createDeviceStorage(options) { let privateItemCount = 0; let privateRecordCount = 0; let inventoryUnavailable = false; let fullClearArmed = false; let persistence = { state:'unavailable', detail:'Storage protection is unavailable in this browser.', label:'Unavailable' }; const ownedCaches = async () => (await options.caches?.keys?.() || []) .filter(name => name.startsWith('stackchain-dashboard-')); function ownedStorageCount(storage) { if (!storage) return 0; let count = 0; for (let index = 0; index < storage.length; index += 1) { if (storage.key(index)?.startsWith('stackchain.')) count += 1; } return count; } function megabytes(value) { return Math.round((Number(value) || 0) / (1024 * 1024)); } async function inspectPersistence() { if (typeof options.storageManager?.persisted !== 'function' || typeof options.storageManager?.persist !== 'function') { persistence = {state:'unavailable', detail:'Storage protection is unavailable in this browser.', label:'Unavailable'}; return persistence; } try { const protectedStorage = await options.storageManager.persisted(); persistence = protectedStorage ? {state:'complete', detail:'Offline work is protected from automatic browser storage cleanup.', label:'Protected'} : {state:'incomplete', detail:'Offline work uses best-effort browser storage and may be removed under storage pressure.', label:'Best effort'}; } catch (_error) { persistence = {state:'incomplete', detail:'Storage protection could not be checked. Retry to protect offline work.', label:'Check failed'}; } return persistence; } function persistenceReadiness() { return {state:persistence.state, detail:persistence.detail}; } async function requestPersistence() { if (typeof options.storageManager?.persist !== 'function') return persistenceReadiness(); let granted = false; try { granted = await options.storageManager.persist(); } catch (_error) { persistence = {state:'incomplete', detail:'Storage protection request failed. Offline work still uses best-effort storage; retry when ready.', label:'Request failed'}; options.detail.textContent = options.detail.textContent.replace(/Storage protection: [^·]+/, 'Storage protection: Request failed '); return persistenceReadiness(); } await refresh(); if (!granted && persistence.state !== 'complete') { persistence = {state:'incomplete', detail:'The browser did not grant storage protection. Offline work still works but may be removed under storage pressure.', label:'Denied'}; options.detail.textContent = options.detail.textContent.replace(/Storage protection: [^·]+/, 'Storage protection: Denied '); } return persistenceReadiness(); } async function refresh() { let estimate = null; try { estimate = await options.storageManager?.estimate?.(); } catch (_error) { /* Storage estimates are optional. */ } await inspectPersistence(); options.summary.textContent = estimate?.quota ? `${megabytes(estimate.usage)} MB of ${megabytes(estimate.quota)} MB browser storage used.` : 'Browser storage usage is unavailable.'; const itemCount = ownedStorageCount(options.localStorage) + (options.sessionStorage === options.localStorage ? 0 : ownedStorageCount(options.sessionStorage)); privateItemCount = itemCount; const inventory = await options.inspectPrivateDatabases?.(options.privateDatabases) || { recordCount: 0, unavailable: true }; privateRecordCount = inventory.recordCount; inventoryUnavailable = inventory.unavailable; const cacheCount = (await ownedCaches()).length; options.detail.textContent = `Storage protection: ${persistence.label} · ${itemCount} private browser item${itemCount === 1 ? '' : 's'} · ` + `${inventoryUnavailable ? 'private work status unknown' : `${privateRecordCount} private work record${privateRecordCount === 1 ? '' : 's'}`} · ` + `${cacheCount} cached app cop${cacheCount === 1 ? 'y' : 'ies'}`; return { itemCount, privateRecordCount, inventoryUnavailable, cacheCount }; } async function clearCaches() { options.clearCachesButton.disabled = true; try { await Promise.all((await ownedCaches()).map(name => options.caches.delete(name))); await refresh(); options.summary.textContent = 'Cached app copies cleared. Private work was kept.'; } catch (error) { options.summary.textContent = `Cached copies could not be cleared: ${error.message}`; } finally { options.clearCachesButton.disabled = false; } } async function clearAll() { if ((privateItemCount > 0 || privateRecordCount > 0 || inventoryUnavailable) && !fullClearArmed) { fullClearArmed = true; options.clearAllButton.textContent = 'Confirm: clear private work'; options.summary.textContent = 'Private drafts or queued work may not be synced. Press confirm to clear them from this device.'; return; } options.clearAllButton.disabled = true; try { await options.clearPrivateDeviceData(); privateItemCount = 0; fullClearArmed = false; options.clearAllButton.textContent = 'Clear all private data'; options.detail.textContent = `Storage protection: ${persistence.label} · 0 private browser items · 0 private work records · 0 cached app copies`; options.summary.textContent = 'All Stackchain private data was cleared from this device.'; } catch (error) { options.summary.textContent = `Private data was not fully cleared: ${error.message}`; } finally { options.clearAllButton.disabled = false; } } async function start() { options.clearCachesButton.addEventListener('click', clearCaches); options.clearAllButton.addEventListener('click', clearAll); return refresh(); } return { refresh, start, persistenceReadiness, requestPersistence }; });