feat: manage private device storage (Closes #998)
CI / lint (pull_request) Successful in 2m51s
CI / build-release (pull_request) Successful in 6s
CI / browser-journey (pull_request) Successful in 2m5s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-17 03:37:21 +00:00
1 parent f30143b5ea
commit 70c8ea6584
12 files changed
+256 -5

No files matched your search

+4
View File
@@ -60,6 +60,10 @@ button { background: linear-gradient(180deg,#1f3a5f,#15324d); border:1px solid #
.device-setup-deadline-controls label { font-size:12px; color:#bfdbfe; }
.device-setup-deadline-controls select, .device-setup-deadline-controls button, #push-deadline-hour, #push-deadline-days { min-height:44px; }
.device-setup-ready { margin:0; padding:12px; border-radius:10px; background:#0f2237; color:#bfdbfe; font-weight:700; }
.device-storage { display:grid; gap:12px; margin:14px 0; padding:12px; border:1px solid #31577f; border-radius:12px; background:#0d1d31; }
.device-storage h3, .device-storage p { margin:0 0 5px; }
.device-storage-actions { display:flex; gap:8px; flex-wrap:wrap; }
.device-storage-actions button { min-height:44px; flex:1 1 180px; }
.device-readiness-card { display:none; min-width:0; margin:10px 0; padding:12px; border:1px solid #3b82b8; border-radius:12px; background:#102b46; overflow-x:hidden; }
.device-readiness-card p { margin:5px 0 0; overflow-wrap:anywhere; }
.device-readiness-summary { color:#bfdbfe; font-weight:700; }
+1
View File
@@ -7744,6 +7744,7 @@
await controller.enableDeadline();
},
});
await createDeviceStorage.mount(document).start();
await deviceSetup.start();
return deviceSetup;
}
+96
View File
@@ -0,0 +1,96 @@
(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,
clearPrivateDeviceData:root.stackchainPrivateDeviceData,
});
}
})(typeof self !== 'undefined' ? self : this, function createDeviceStorage(options) {
let privateItemCount = 0;
let fullClearArmed = false;
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 refresh() {
let estimate = null;
try { estimate = await options.storageManager?.estimate?.(); }
catch (_error) { /* Storage estimates are optional. */ }
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 cacheCount = (await ownedCaches()).length;
options.detail.textContent = `${itemCount} private browser item${itemCount === 1 ? '' : 's'} · `
+ `${options.privateDatabases.length} private work stores · `
+ `${cacheCount} cached app cop${cacheCount === 1 ? 'y' : 'ies'}`;
return { itemCount, 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 && !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 = '0 private browser items · 0 private work stores · 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 };
});
+13
View File
@@ -108,6 +108,17 @@
</div>
</li>
</ol>
<section class="device-storage" aria-labelledby="device-storage-heading">
<div>
<h3 id="device-storage-heading">Private device storage</h3>
<p class="small" id="device-storage-summary" role="status" aria-live="polite">Checking private storage…</p>
<p class="small muted" id="device-storage-detail"></p>
</div>
<div class="device-storage-actions">
<button id="clear-device-caches" type="button">Clear cached copies</button>
<button id="clear-private-device-data" type="button">Clear all private data</button>
</div>
</section>
<p id="device-setup-ready-status" class="device-setup-ready" role="status" aria-live="polite"></p>
</section>
</div>
@@ -1638,6 +1649,8 @@
<script src="static/mobile-insights.js"></script>
<script src="static/mobile-app-shortcuts.js"></script>
<script src="static/install-app.js"></script>
<script src="static/private-device-data.js"></script>
<script src="static/device-storage.js"></script>
<script src="static/mobile-device-setup.js"></script>
<script src="static/mobile-search-viewport.js"></script>
<script src="static/mobile-composer-viewport.js"></script>
+1 -1
View File
@@ -55,9 +55,9 @@
}
return async function clearPrivateDeviceData() {
await stopWorkerOutbox();
removeOwnedStorage(localStorage);
if (sessionStorage !== localStorage) removeOwnedStorage(sessionStorage);
await stopWorkerOutbox();
for (const name of privateDatabases) await deletePrivateDatabase(name);
const keys = await caches?.keys?.() || [];
await Promise.all(
+2
View File
@@ -118,6 +118,8 @@ const SHELL = [
BASE + 'static/mobile-insights.js',
BASE + 'static/mobile-app-shortcuts.js',
BASE + 'static/install-app.js',
BASE + 'static/private-device-data.js',
BASE + 'static/device-storage.js',
BASE + 'static/mobile-device-setup.js',
BASE + 'static/mobile-search-viewport.js',
BASE + 'static/mobile-composer-viewport.js',