feat(settings): add "Refresh cached data" recovery action

When the mailbox view gets into a stale or wrong state, the only escape
was the browser's "clear site data" — which also wipes the saved account
list, forcing a re-login of every account.

Add a non-destructive "Refresh cached data" button under Settings →
Data. It clears the server-derived caches (contacts, calendars,
identities, per-account snapshots) and reloads so they re-fetch fresh,
while preserving accounts, sessions, settings, themes and user content
(templates, S/MIME). Two-click confirm to avoid an accidental reload.

English strings added across all locales (translation follow-up); unit
tests cover the cache-clear (keeps account-registry/auth/prefs) and the
reload.
This commit is contained in:
Shuki Vaknin
2026-07-21 20:58:55 +02:00
committed by Linus Rath
parent f2703bcc27
commit f51ec50443
23 changed files with 216 additions and 0 deletions
+50
View File
@@ -0,0 +1,50 @@
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import { clearCachedData } from '../clear-cached-data';
describe('clearCachedData', () => {
let reload: ReturnType<typeof vi.fn>;
let originalLocation: Location;
beforeEach(() => {
localStorage.clear();
reload = vi.fn();
originalLocation = window.location;
Object.defineProperty(window, 'location', {
configurable: true,
value: { ...originalLocation, reload },
});
});
afterEach(() => {
Object.defineProperty(window, 'location', { configurable: true, value: originalLocation });
});
it('clears re-fetchable caches but keeps accounts, sessions and prefs', () => {
localStorage.setItem('contact-storage', '1');
localStorage.setItem('calendar-storage', '1');
localStorage.setItem('identity-storage', '1');
localStorage.setItem('calendar-notification-storage', '1');
// Must survive — losing these is exactly the pain we're avoiding.
localStorage.setItem('account-registry', 'accounts');
localStorage.setItem('auth-storage', 'session');
localStorage.setItem('settings-storage', 'prefs');
localStorage.setItem('template-storage', 'my templates');
clearCachedData();
expect(localStorage.getItem('contact-storage')).toBeNull();
expect(localStorage.getItem('calendar-storage')).toBeNull();
expect(localStorage.getItem('identity-storage')).toBeNull();
expect(localStorage.getItem('calendar-notification-storage')).toBeNull();
expect(localStorage.getItem('account-registry')).toBe('accounts');
expect(localStorage.getItem('auth-storage')).toBe('session');
expect(localStorage.getItem('settings-storage')).toBe('prefs');
expect(localStorage.getItem('template-storage')).toBe('my templates');
});
it('reloads so data is re-fetched fresh', () => {
clearCachedData();
expect(reload).toHaveBeenCalledOnce();
});
});
+45
View File
@@ -0,0 +1,45 @@
import { evictAll } from '@/lib/account-state-manager';
// localStorage keys holding server-derived, re-fetchable caches. The "Refresh
// cached data" action clears these so a stale or wrong-account view can be
// fixed WITHOUT signing out (which would drop the whole account list).
//
// Deliberately excluded:
// - 'account-registry' / 'auth-storage' → keep accounts + sessions
// - 'settings-storage' / 'theme-storage' / 'locale-storage' → user prefs
// - 'template-storage' / 'smime-preferences' → user-created content
const CACHE_STORAGE_KEYS = [
'identity-storage',
'contact-storage',
'calendar-storage',
'calendar-notification-storage',
];
/**
* Clear cached, server-derived data (contacts, calendars, identities, and the
* in-memory per-account snapshots), then reload so everything is re-fetched
* fresh for the active account. Accounts and sessions are preserved — this is
* the non-destructive alternative to the browser's "clear site data", which
* also wipes the account list.
*/
export function clearCachedData(): void {
// Drop the in-memory per-account store snapshots so a reload can't restore
// stale cached state for any account.
try {
evictAll();
} catch {
/* snapshots are best-effort */
}
if (typeof window === 'undefined') return;
for (const key of CACHE_STORAGE_KEYS) {
try {
window.localStorage.removeItem(key);
} catch {
/* ignore storage access errors */
}
}
window.location.reload();
}