feat(260928-ujj): Dashboard-Hintergrund pro Benutzer in der Datenbank

- Spalte User.dashboardBackground (JSONB) samt Migration
- PATCH /users/me/dashboard-background, geprueft mit parseDashboardBackground aus @tessera/shared (Allowlist, UUID-Bildkennung)
- getMe liefert dashboardBackground normalisiert neben accentColor
- Web liest die Wahl aus dem Auth-Store, speichert ueber die Server-Aktion, alte localStorage-Wahl wird einmalig uebernommen
- Hinweistext: gilt auf jedem Geraet

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-28 22:13:49 +02:00
parent 76f6d87973
commit 0aaa15240d
16 changed files with 333 additions and 66 deletions
@@ -52,7 +52,9 @@ describe('useDashboardBackground (quick-260928-ujj)', () => {
});
it('liest die Wahl aus dem Auth-Store; null ergibt „kein Hintergrund“', async () => {
useAuthStore.setState({ user: makeUser({ dashboardBackground: { kind: 'preset', id: 'dunes' } }) });
useAuthStore.setState({
user: makeUser({ dashboardBackground: { kind: 'preset', id: 'dunes' } }),
});
const { result } = await renderBackgroundHook();
expect(result.current.background).toEqual({ kind: 'preset', id: 'dunes' });
@@ -68,17 +70,26 @@ describe('useDashboardBackground (quick-260928-ujj)', () => {
result.current.choose({ kind: 'preset', id: 'mosaic' });
});
expect(useAuthStore.getState().user?.dashboardBackground).toEqual({ kind: 'preset', id: 'mosaic' });
expect(useAuthStore.getState().user?.dashboardBackground).toEqual({
kind: 'preset',
id: 'mosaic',
});
expect(result.current.background).toEqual({ kind: 'preset', id: 'mosaic' });
expect(updateDashboardBackgroundAction).toHaveBeenCalledWith({ kind: 'preset', id: 'mosaic' });
});
it.each([
['Fehlerantwort', () => updateDashboardBackgroundAction.mockResolvedValue({ success: false, error: 'saveError' })],
[
'Fehlerantwort',
() =>
updateDashboardBackgroundAction.mockResolvedValue({ success: false, error: 'saveError' }),
],
['Ausnahme', () => updateDashboardBackgroundAction.mockRejectedValue(new Error('offline'))],
])('Speichern schlaegt fehl (%s): der vorige Wert wird zurueckgesetzt', async (_label, arrange) => {
arrange();
useAuthStore.setState({ user: makeUser({ dashboardBackground: { kind: 'preset', id: 'dunes' } }) });
useAuthStore.setState({
user: makeUser({ dashboardBackground: { kind: 'preset', id: 'dunes' } }),
});
const { result } = await renderBackgroundHook();
act(() => {
@@ -86,7 +97,10 @@ describe('useDashboardBackground (quick-260928-ujj)', () => {
});
await waitFor(() => expect(result.current.background).toEqual({ kind: 'preset', id: 'dunes' }));
expect(useAuthStore.getState().user?.dashboardBackground).toEqual({ kind: 'preset', id: 'dunes' });
expect(useAuthStore.getState().user?.dashboardBackground).toEqual({
kind: 'preset',
id: 'dunes',
});
});
it('Server-Wert null und alte localStorage-Wahl: wird genau einmal gespeichert, Schluessel entfernt', async () => {