feat(settings): storage usage tile with persistence status (#120)
CI / Build & Quality Checks (push) Canceled after 0s
CI / Trigger Desktop Build (push) Canceled after 0s
CI / Secret scan (gitleaks) (push) Canceled after 0s
CI / Docker image build & smoke test (push) Canceled after 0s
CI / Playwright smoke (e2e) (push) Canceled after 0s

One tile under Settings → General → Storage: total local usage vs the
browser's quota from navigator.storage.estimate(), the Chromium
usageDetails breakdown when available (IndexedDB = sync cache + encryption
keys + search index; Cache Storage = offline app files), and whether the
browser granted persistent storage — green "Protected" or amber "May be
evicted" with a "Keep my data" button that calls storage.persist(). That
last bit is the useful diagnostic for the KE-1 storage-eviction cluster.

No clear button: media lives in the browser's own HTTP cache (not in the
estimate, and not clearable from a page), the crypto store must never be
casually cleared, and the search-index clear already lives in Message
Search. The About page's "Clear Cache & Reload" now says it deletes this
device's encryption keys too. Hidden entirely when estimate() is missing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
2026-09-20 14:53:42 -04:00
co-authored by Claude Opus 5
parent dc0d524989
commit c6c2e88df5
4 changed files with 161 additions and 1 deletions
+55
View File
@@ -0,0 +1,55 @@
/**
* [Gitea #120] What this device stores for Lotus, from the APIs that are cheap
* to ask. `estimate()` is the total the browser accounts for; Chromium adds a
* per-bucket breakdown (`usageDetails`), Firefox/Safari don't. Media files are
* not in here at all: they live in the browser's own HTTP cache, which manages
* and evicts them itself.
*/
export type StorageUsage = {
usage: number;
quota: number;
/** Chromium only: IndexedDB (sync store, crypto store, search index) bytes. */
indexedDB?: number;
/** Chromium only: Cache Storage (offline app shell) bytes. */
caches?: number;
/** Whether the browser granted persistent storage (won't evict under pressure). */
persisted: boolean | undefined;
};
type EstimateWithDetails = StorageEstimate & {
usageDetails?: { indexedDB?: number; caches?: number; serviceWorkerRegistrations?: number };
};
export const storageUsageSupported = (): boolean =>
typeof navigator !== 'undefined' && !!navigator.storage && !!navigator.storage.estimate;
export async function readStorageUsage(): Promise<StorageUsage | undefined> {
if (!storageUsageSupported()) return undefined;
const est = (await navigator.storage.estimate()) as EstimateWithDetails;
let persisted: boolean | undefined;
try {
persisted = navigator.storage.persisted ? await navigator.storage.persisted() : undefined;
} catch {
persisted = undefined;
}
return {
usage: est.usage ?? 0,
quota: est.quota ?? 0,
indexedDB: est.usageDetails?.indexedDB,
caches: est.usageDetails?.caches,
persisted,
};
}
/** Ask the browser to keep this origin's storage; returns the new state. */
export async function requestPersistentStorage(): Promise<boolean | undefined> {
if (!navigator.storage?.persist) return undefined;
try {
return await navigator.storage.persist();
} catch {
return undefined;
}
}
export const percentOf = (usage: number, quota: number): number =>
quota > 0 ? Math.min(100, Math.round((usage / quota) * 100)) : 0;