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
@@ -11,6 +11,7 @@ import { useUpdateStore } from '@/stores/update-store';
import { ExternalLink } from 'lucide-react'; import { ExternalLink } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { getPathPrefix } from '@/lib/browser-navigation'; import { getPathPrefix } from '@/lib/browser-navigation';
import { clearCachedData } from '@/lib/clear-cached-data';
import { SpamSiegeGame } from './spam-siege-game'; import { SpamSiegeGame } from './spam-siege-game';
const APP_VERSION = process.env.NEXT_PUBLIC_APP_VERSION || "0.0.0"; const APP_VERSION = process.env.NEXT_PUBLIC_APP_VERSION || "0.0.0";
@@ -54,6 +55,7 @@ export function AboutDataSettings() {
useSettingsStore(); useSettingsStore();
const { settingsSyncEnabled } = useConfig(); const { settingsSyncEnabled } = useConfig();
const [showResetConfirm, setShowResetConfirm] = useState(false); const [showResetConfirm, setShowResetConfirm] = useState(false);
const [showRefreshConfirm, setShowRefreshConfirm] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
const { isFeatureEnabled } = usePolicyStore(); const { isFeatureEnabled } = usePolicyStore();
const [showGame, setShowGame] = useState(false); const [showGame, setShowGame] = useState(false);
@@ -105,6 +107,15 @@ export function AboutDataSettings() {
reader.readAsText(file); reader.readAsText(file);
}; };
const handleRefreshCache = () => {
if (showRefreshConfirm) {
clearCachedData(); // reloads the page
} else {
setShowRefreshConfirm(true);
setTimeout(() => setShowRefreshConfirm(false), 5000);
}
};
const handleReset = () => { const handleReset = () => {
if (showResetConfirm) { if (showResetConfirm) {
resetToDefaults(); resetToDefaults();
@@ -187,6 +198,16 @@ export function AboutDataSettings() {
</SettingItem> </SettingItem>
)} )}
<SettingItem label={t('refresh_cache.label')} description={t('refresh_cache.description')}>
<Button
variant={showRefreshConfirm ? 'default' : 'outline'}
size="sm"
onClick={handleRefreshCache}
>
{showRefreshConfirm ? tCommon('yes') : t('refresh_cache.button')}
</Button>
</SettingItem>
<SettingItem label={t('reset_settings.label')} description={t('reset_settings.description')}> <SettingItem label={t('reset_settings.label')} description={t('reset_settings.description')}>
<Button <Button
variant={showResetConfirm ? 'destructive' : 'outline'} variant={showResetConfirm ? 'destructive' : 'outline'}
+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();
}
+5
View File
@@ -1628,6 +1628,11 @@
"description": "Zobrazit dostupné klávesové zkratky", "description": "Zobrazit dostupné klávesové zkratky",
"button": "Zobrazit zkratky" "button": "Zobrazit zkratky"
}, },
"refresh_cache": {
"label": "Refresh cached data",
"description": "Reload contacts, calendars, and folders from the server. Keeps your accounts and sessions \u2014 fixes a stale or wrong view without signing out.",
"button": "Refresh"
},
"reset_settings": { "reset_settings": {
"label": "Resetovat nastavení", "label": "Resetovat nastavení",
"description": "Obnovit všechna nastavení na výchozí hodnoty", "description": "Obnovit všechna nastavení na výchozí hodnoty",
+5
View File
@@ -1631,6 +1631,11 @@
"description": "Se tilgængelige tastaturgenveje", "description": "Se tilgængelige tastaturgenveje",
"button": "Vis genveje" "button": "Vis genveje"
}, },
"refresh_cache": {
"label": "Refresh cached data",
"description": "Reload contacts, calendars, and folders from the server. Keeps your accounts and sessions \u2014 fixes a stale or wrong view without signing out.",
"button": "Refresh"
},
"reset_settings": { "reset_settings": {
"label": "Nulstil indstillinger", "label": "Nulstil indstillinger",
"description": "Gendan alle indstillinger til standardværdier", "description": "Gendan alle indstillinger til standardværdier",
+5
View File
@@ -1628,6 +1628,11 @@
"description": "Verfügbare Tastaturkürzel anzeigen", "description": "Verfügbare Tastaturkürzel anzeigen",
"button": "Tastaturkürzel anzeigen" "button": "Tastaturkürzel anzeigen"
}, },
"refresh_cache": {
"label": "Refresh cached data",
"description": "Reload contacts, calendars, and folders from the server. Keeps your accounts and sessions \u2014 fixes a stale or wrong view without signing out.",
"button": "Refresh"
},
"reset_settings": { "reset_settings": {
"label": "Einstellungen zurücksetzen", "label": "Einstellungen zurücksetzen",
"description": "Alle Einstellungen auf Standardwerte zurücksetzen", "description": "Alle Einstellungen auf Standardwerte zurücksetzen",
+5
View File
@@ -1637,6 +1637,11 @@
"description": "View available keyboard shortcuts", "description": "View available keyboard shortcuts",
"button": "View Shortcuts" "button": "View Shortcuts"
}, },
"refresh_cache": {
"label": "Refresh cached data",
"description": "Reload contacts, calendars, and folders from the server. Keeps your accounts and sessions \u2014 fixes a stale or wrong view without signing out.",
"button": "Refresh"
},
"reset_settings": { "reset_settings": {
"label": "Reset Settings", "label": "Reset Settings",
"description": "Restore all settings to default values", "description": "Restore all settings to default values",
+5
View File
@@ -1628,6 +1628,11 @@
"description": "Ver atajos de teclado disponibles", "description": "Ver atajos de teclado disponibles",
"button": "Ver Atajos" "button": "Ver Atajos"
}, },
"refresh_cache": {
"label": "Refresh cached data",
"description": "Reload contacts, calendars, and folders from the server. Keeps your accounts and sessions \u2014 fixes a stale or wrong view without signing out.",
"button": "Refresh"
},
"reset_settings": { "reset_settings": {
"label": "Restablecer Configuración", "label": "Restablecer Configuración",
"description": "Restaurar toda la configuración a los valores predeterminados", "description": "Restaurar toda la configuración a los valores predeterminados",
+5
View File
@@ -1635,6 +1635,11 @@
"description": "مشاهده میانبرهای صفحه کلید موجود", "description": "مشاهده میانبرهای صفحه کلید موجود",
"button": "مشاهده میانبرها" "button": "مشاهده میانبرها"
}, },
"refresh_cache": {
"label": "Refresh cached data",
"description": "Reload contacts, calendars, and folders from the server. Keeps your accounts and sessions \u2014 fixes a stale or wrong view without signing out.",
"button": "Refresh"
},
"reset_settings": { "reset_settings": {
"label": "بازنشانی تنظیمات", "label": "بازنشانی تنظیمات",
"description": "بازگردانی همه تنظیمات به مقادیر پیش‌فرض", "description": "بازگردانی همه تنظیمات به مقادیر پیش‌فرض",
+5
View File
@@ -1628,6 +1628,11 @@
"description": "Voir les raccourcis clavier disponibles", "description": "Voir les raccourcis clavier disponibles",
"button": "Voir les raccourcis" "button": "Voir les raccourcis"
}, },
"refresh_cache": {
"label": "Refresh cached data",
"description": "Reload contacts, calendars, and folders from the server. Keeps your accounts and sessions \u2014 fixes a stale or wrong view without signing out.",
"button": "Refresh"
},
"reset_settings": { "reset_settings": {
"label": "Réinitialiser les paramètres", "label": "Réinitialiser les paramètres",
"description": "Restaurer tous les paramètres par défaut", "description": "Restaurer tous les paramètres par défaut",
+5
View File
@@ -1631,6 +1631,11 @@
"description": "Elérhető billentyűparancsok megtekintése", "description": "Elérhető billentyűparancsok megtekintése",
"button": "Parancsok megtekintése" "button": "Parancsok megtekintése"
}, },
"refresh_cache": {
"label": "Refresh cached data",
"description": "Reload contacts, calendars, and folders from the server. Keeps your accounts and sessions \u2014 fixes a stale or wrong view without signing out.",
"button": "Refresh"
},
"reset_settings": { "reset_settings": {
"label": "Beállítások visszaállítása", "label": "Beállítások visszaállítása",
"description": "Összes beállítás visszaállítása az alapértelmezett értékekre", "description": "Összes beállítás visszaállítása az alapértelmezett értékekre",
+5
View File
@@ -1628,6 +1628,11 @@
"description": "Visualizza le scorciatoie da tastiera disponibili", "description": "Visualizza le scorciatoie da tastiera disponibili",
"button": "Visualizza scorciatoie" "button": "Visualizza scorciatoie"
}, },
"refresh_cache": {
"label": "Refresh cached data",
"description": "Reload contacts, calendars, and folders from the server. Keeps your accounts and sessions \u2014 fixes a stale or wrong view without signing out.",
"button": "Refresh"
},
"reset_settings": { "reset_settings": {
"label": "Ripristina impostazioni", "label": "Ripristina impostazioni",
"description": "Ripristina tutte le impostazioni ai valori predefiniti", "description": "Ripristina tutte le impostazioni ai valori predefiniti",
+5
View File
@@ -1628,6 +1628,11 @@
"description": "利用可能なキーボードショートカットを表示", "description": "利用可能なキーボードショートカットを表示",
"button": "ショートカットを表示" "button": "ショートカットを表示"
}, },
"refresh_cache": {
"label": "Refresh cached data",
"description": "Reload contacts, calendars, and folders from the server. Keeps your accounts and sessions \u2014 fixes a stale or wrong view without signing out.",
"button": "Refresh"
},
"reset_settings": { "reset_settings": {
"label": "設定をリセット", "label": "設定をリセット",
"description": "すべての設定をデフォルト値に戻す", "description": "すべての設定をデフォルト値に戻す",
+5
View File
@@ -1628,6 +1628,11 @@
"description": "사용 가능한 키보드 단축키를 확인해 보세요", "description": "사용 가능한 키보드 단축키를 확인해 보세요",
"button": "단축키 보기" "button": "단축키 보기"
}, },
"refresh_cache": {
"label": "Refresh cached data",
"description": "Reload contacts, calendars, and folders from the server. Keeps your accounts and sessions \u2014 fixes a stale or wrong view without signing out.",
"button": "Refresh"
},
"reset_settings": { "reset_settings": {
"label": "설정 초기화", "label": "설정 초기화",
"description": "모든 설정을 기본값으로 되돌려요", "description": "모든 설정을 기본값으로 되돌려요",
+5
View File
@@ -1609,6 +1609,11 @@
"description": "Skatīt pieejamos tastatūras īsinājumtaustiņus", "description": "Skatīt pieejamos tastatūras īsinājumtaustiņus",
"button": "Rādīt īsinājumtaustiņus" "button": "Rādīt īsinājumtaustiņus"
}, },
"refresh_cache": {
"label": "Refresh cached data",
"description": "Reload contacts, calendars, and folders from the server. Keeps your accounts and sessions \u2014 fixes a stale or wrong view without signing out.",
"button": "Refresh"
},
"reset_settings": { "reset_settings": {
"label": "Atiestatīt iestatījumus", "label": "Atiestatīt iestatījumus",
"description": "Atjaunot visus iestatījumus uz noklusējuma vērtībām", "description": "Atjaunot visus iestatījumus uz noklusējuma vērtībām",
+5
View File
@@ -1628,6 +1628,11 @@
"description": "Bekijk beschikbare sneltoetsen", "description": "Bekijk beschikbare sneltoetsen",
"button": "Sneltoetsen bekijken" "button": "Sneltoetsen bekijken"
}, },
"refresh_cache": {
"label": "Refresh cached data",
"description": "Reload contacts, calendars, and folders from the server. Keeps your accounts and sessions \u2014 fixes a stale or wrong view without signing out.",
"button": "Refresh"
},
"reset_settings": { "reset_settings": {
"label": "Instellingen resetten", "label": "Instellingen resetten",
"description": "Herstel alle instellingen naar standaardwaarden", "description": "Herstel alle instellingen naar standaardwaarden",
+5
View File
@@ -1628,6 +1628,11 @@
"description": "Wyświetl dostępne skróty klawiszowe", "description": "Wyświetl dostępne skróty klawiszowe",
"button": "Pokaż skróty" "button": "Pokaż skróty"
}, },
"refresh_cache": {
"label": "Refresh cached data",
"description": "Reload contacts, calendars, and folders from the server. Keeps your accounts and sessions \u2014 fixes a stale or wrong view without signing out.",
"button": "Refresh"
},
"reset_settings": { "reset_settings": {
"label": "Resetuj ustawienia", "label": "Resetuj ustawienia",
"description": "Przywróć wszystkie ustawienia do wartości domyślnych", "description": "Przywróć wszystkie ustawienia do wartości domyślnych",
+5
View File
@@ -1628,6 +1628,11 @@
"description": "Visualizar atalhos de teclado disponíveis", "description": "Visualizar atalhos de teclado disponíveis",
"button": "Ver Atalhos" "button": "Ver Atalhos"
}, },
"refresh_cache": {
"label": "Refresh cached data",
"description": "Reload contacts, calendars, and folders from the server. Keeps your accounts and sessions \u2014 fixes a stale or wrong view without signing out.",
"button": "Refresh"
},
"reset_settings": { "reset_settings": {
"label": "Redefinir Configurações", "label": "Redefinir Configurações",
"description": "Restaurar todas as configurações para valores padrão", "description": "Restaurar todas as configurações para valores padrão",
+5
View File
@@ -1635,6 +1635,11 @@
"description": "Vedeți comenzile rapide de la tastatură disponibile", "description": "Vedeți comenzile rapide de la tastatură disponibile",
"button": "Comenzi rapide de vizualizare" "button": "Comenzi rapide de vizualizare"
}, },
"refresh_cache": {
"label": "Refresh cached data",
"description": "Reload contacts, calendars, and folders from the server. Keeps your accounts and sessions \u2014 fixes a stale or wrong view without signing out.",
"button": "Refresh"
},
"reset_settings": { "reset_settings": {
"label": "Resetare setări", "label": "Resetare setări",
"description": "Restabiliți toate setările la valorile implicite", "description": "Restabiliți toate setările la valorile implicite",
+5
View File
@@ -1628,6 +1628,11 @@
"description": "Просмотреть доступные сочетания клавиш", "description": "Просмотреть доступные сочетания клавиш",
"button": "Показать сочетания" "button": "Показать сочетания"
}, },
"refresh_cache": {
"label": "Refresh cached data",
"description": "Reload contacts, calendars, and folders from the server. Keeps your accounts and sessions \u2014 fixes a stale or wrong view without signing out.",
"button": "Refresh"
},
"reset_settings": { "reset_settings": {
"label": "Сбросить настройки", "label": "Сбросить настройки",
"description": "Восстановить все настройки до значений по умолчанию", "description": "Восстановить все настройки до значений по умолчанию",
+5
View File
@@ -1628,6 +1628,11 @@
"description": "Mevcut klavye kısayollarını görüntüleyin", "description": "Mevcut klavye kısayollarını görüntüleyin",
"button": "Kısayolları Görüntüle" "button": "Kısayolları Görüntüle"
}, },
"refresh_cache": {
"label": "Refresh cached data",
"description": "Reload contacts, calendars, and folders from the server. Keeps your accounts and sessions \u2014 fixes a stale or wrong view without signing out.",
"button": "Refresh"
},
"reset_settings": { "reset_settings": {
"label": "Ayarları Sıfırla", "label": "Ayarları Sıfırla",
"description": "Tüm ayarları varsayılan değerlerine geri yükle", "description": "Tüm ayarları varsayılan değerlerine geri yükle",
+5
View File
@@ -1628,6 +1628,11 @@
"description": "Переглянути доступні комбінації клавіш", "description": "Переглянути доступні комбінації клавіш",
"button": "Перегляд ярликів" "button": "Перегляд ярликів"
}, },
"refresh_cache": {
"label": "Refresh cached data",
"description": "Reload contacts, calendars, and folders from the server. Keeps your accounts and sessions \u2014 fixes a stale or wrong view without signing out.",
"button": "Refresh"
},
"reset_settings": { "reset_settings": {
"label": "Скинути налаштування", "label": "Скинути налаштування",
"description": "Відновити всі налаштування до значень за замовчуванням", "description": "Відновити всі налаштування до значень за замовчуванням",
+5
View File
@@ -1628,6 +1628,11 @@
"description": "查看可用快捷键", "description": "查看可用快捷键",
"button": "查看快捷方式" "button": "查看快捷方式"
}, },
"refresh_cache": {
"label": "Refresh cached data",
"description": "Reload contacts, calendars, and folders from the server. Keeps your accounts and sessions \u2014 fixes a stale or wrong view without signing out.",
"button": "Refresh"
},
"reset_settings": { "reset_settings": {
"label": "重置设置", "label": "重置设置",
"description": "将所有设置恢复为默认值", "description": "将所有设置恢复为默认值",