diff --git a/AGENT_HANDOFF.md b/AGENT_HANDOFF.md index f95136b..a5ddec3 100644 --- a/AGENT_HANDOFF.md +++ b/AGENT_HANDOFF.md @@ -135,6 +135,27 @@ Do not touch: ## Current Handoff +Agent: Claude Code (Opus 4.8) +Date/time: 2026-05-30T17:30:00+02:00 +Task: Fix user-reported bug — "RESETTA TUTTI I DATI" (reset all data) in Settings wiped shifts locally but, for users with an active (authenticated) account, all deleted shifts reappeared on reload because the reset never propagated to Firestore; the realtime `onSnapshot` listeners restored them. +Status: done. Committed and pushed to `claude/firebase-data-reset-sync-fOJuQ` (this is a web session whose explicit task is to deliver the fix on that branch). +Root cause: `ShiftService.deleteAllShifts()` only called `userDataService.setState(EMPTY_SHIFT_DATA_STATE)` — a local-only wipe. The Firestore docs under `users/{uid}/{shiftSeries,manualShifts,shiftOverrides}` were never deleted, so on the next reload `FirestoreUserDataService.start()` re-hydrated them and `UserDataService`'s effect mirrored them back to local state. `deleteUserDataTree` existed but was wired only into account deletion. +Fix (3 source files): +- `firestore-user-data.service.ts`: new `clearShiftData(uid)` — batch-deletes only the three shift collections (`shiftSeries`, `manualShifts`, `shiftOverrides`), deliberately leaving `devices`/`profile`/`settings` intact (unlike `deleteUserDataTree`, which is the full account-deletion nuke). +- `user-data.service.ts`: new `clearAll()` — sets local state to EMPTY synchronously, then, when authenticated, awaits `firestore.clearShiftData(uid)`. +- `shift.service.ts`: `deleteAllShifts()` now calls `void this.userDataService.clearAll()` instead of `setState(EMPTY...)`. +No-resurrection reasoning: local wipe is synchronous; the `UserDataService` effect tracks `firestore.state()` (not local `_state`), so the local empty isn't overwritten until the cloud delete's snapshot fires empty too → consistent. Account-deletion path (`clearLocalAccountData`) is unaffected: it runs after `deleteAccount()` (which already calls `deleteUserDataTree` + `deleteUser`), so by then auth is no longer authenticated and `clearAll` issues no redundant cloud delete. +Files changed: `src/services/firestore-user-data.service.ts` (+spec), `src/services/user-data.service.ts` (+spec), `src/services/shift.service.ts`, `AGENT_HANDOFF.md` (this block). +Tests red: none. +Tests green: full Jest suite 689/689 (was 686; +3 new: `clearShiftData` collection-scope test, `clearAll` authenticated + guest tests); `npm run lint` clean; `npm run build` OK. +Open concerns: +- Behavior verified by unit tests + build only; no authenticated browser/E2E run (Firestore deletion path requires a real logged-in session). Recommend a quick manual check on a real account: add shifts → reset → reload → confirm they stay gone. +- The environment's `grep -n` line numbers were unreliable this session (shell output layer glitching); all edits were verified by re-reading file content, not by trusting line numbers. +Next agent starts from: fix is on `claude/firebase-data-reset-sync-fOJuQ`. If the user wants it in production it must be merged to `main` (triggers Cloudflare Pages deploy). +Do not touch: do not delete `web9.png` (local-only, ignored). + +--- + Agent: Claude Code (Opus 4.7) Date/time: 2026-05-25T00:15:00+02:00 Task: Create `update.md` — a maintenance/update schedule documenting every dependency/framework/native group to re-check every 30/60 days, with cadence table, exact commands, and a check log. User-requested, doc-only. diff --git a/src/services/firestore-user-data.service.spec.ts b/src/services/firestore-user-data.service.spec.ts index 172708c..302fde2 100644 --- a/src/services/firestore-user-data.service.spec.ts +++ b/src/services/firestore-user-data.service.spec.ts @@ -442,6 +442,37 @@ describe('FirestoreUserDataService', () => { ); }); + it('deletes only the shift data collections in clearShiftData, leaving devices/profile/settings intact', async () => { + const service = TestBed.inject(FirestoreUserDataService); + (firestore.getDocs as jest.Mock).mockResolvedValue({ + docs: [{ ref: 'ref-1' }, { ref: 'ref-2' }], + }); + + await service.clearShiftData('uid-1'); + + // 3 collections (shiftSeries, manualShifts, shiftOverrides) * 2 docs each = 6 deletes. + // No devices/profile/settings deletes so the account stays alive. + expect(batch.delete).toHaveBeenCalledTimes(6); + expect(batch.commit).toHaveBeenCalledTimes(1); + expect(firestore.getDocs).toHaveBeenCalledTimes(3); + expect(firestore.collection).toHaveBeenCalledWith( + expect.anything(), + 'users/uid-1/shiftSeries' + ); + expect(firestore.collection).toHaveBeenCalledWith( + expect.anything(), + 'users/uid-1/manualShifts' + ); + expect(firestore.collection).toHaveBeenCalledWith( + expect.anything(), + 'users/uid-1/shiftOverrides' + ); + expect(firestore.collection).not.toHaveBeenCalledWith( + expect.anything(), + 'users/uid-1/devices' + ); + }); + it('deletes all user data collections and profile/settings in deleteUserDataTree', async () => { const service = TestBed.inject(FirestoreUserDataService); (firestore.getDocs as jest.Mock).mockResolvedValue({ diff --git a/src/services/firestore-user-data.service.ts b/src/services/firestore-user-data.service.ts index 0b451a5..966b524 100644 --- a/src/services/firestore-user-data.service.ts +++ b/src/services/firestore-user-data.service.ts @@ -180,6 +180,22 @@ export class FirestoreUserDataService { await batch.commit(); } + /** + * Clears all shift data (series, manual shifts and overrides) for the user + * while leaving the device registry, profile and settings intact. Backs the + * "reset all data" action so the deletion propagates to the cloud and is not + * restored by the realtime listeners on the next reload. + */ + async clearShiftData(uid: string): Promise { + const db = this.firebase.firestore; + const batch = writeBatch(db); + for (const path of ['shiftSeries', 'manualShifts', 'shiftOverrides']) { + const snapshot = await getDocs(collection(db, `users/${uid}/${path}`)); + snapshot.docs.forEach(document => batch.delete(document.ref)); + } + await batch.commit(); + } + async deleteUserDataTree(uid: string): Promise { const db = this.firebase.firestore; const batch = writeBatch(db); diff --git a/src/services/shift.service.ts b/src/services/shift.service.ts index eaf3079..df9ac08 100755 --- a/src/services/shift.service.ts +++ b/src/services/shift.service.ts @@ -696,7 +696,7 @@ export class ShiftService { deleteAllShifts(): void { void this.notificationService.cancelAllNotifications(); - this.userDataService.setState(EMPTY_SHIFT_DATA_STATE); + void this.userDataService.clearAll(); } exportBackupPayload(): string { diff --git a/src/services/user-data.service.spec.ts b/src/services/user-data.service.spec.ts index 42ac0fe..835a6ec 100644 --- a/src/services/user-data.service.spec.ts +++ b/src/services/user-data.service.spec.ts @@ -21,6 +21,7 @@ describe('UserDataService', () => { upsertShiftOverride: jest.Mock; applyBatch: jest.Mock; removeDevice: jest.Mock; + clearShiftData: jest.Mock; }; beforeEach(() => { @@ -41,6 +42,7 @@ describe('UserDataService', () => { upsertShiftOverride: jest.fn().mockResolvedValue(undefined), applyBatch: jest.fn().mockResolvedValue(undefined), removeDevice: jest.fn().mockResolvedValue(undefined), + clearShiftData: jest.fn().mockResolvedValue(undefined), }; TestBed.resetTestingModule(); @@ -162,6 +164,46 @@ describe('UserDataService', () => { }); }); + describe('clearAll', () => { + const nonEmpty: ShiftDataState = { + schemaVersion: 2, + shiftSeries: [], + manualShifts: [ + { + id: 'm-clear', + title: 'To be cleared', + start: '2026-03-01T08:00:00.000Z', + end: '2026-03-01T16:00:00.000Z', + color: 'sky', + createdAt: '2026-03-01T00:00:00.000Z', + updatedAt: '2026-03-01T00:00:00.000Z', + }, + ], + shiftOverrides: [], + }; + + it('clears local state and deletes cloud shift data when authenticated', async () => { + authMock.state.set({ mode: 'authenticated', uid: 'uid-abc' }); + const service = TestBed.inject(UserDataService); + service.setState(nonEmpty); + + await service.clearAll(); + + expect(service.state()).toEqual(EMPTY_SHIFT_DATA_STATE); + expect(firestoreMock.clearShiftData).toHaveBeenCalledWith('uid-abc'); + }); + + it('clears local state without touching Firestore in guest mode', async () => { + const service = TestBed.inject(UserDataService); + service.setState(nonEmpty); + + await service.clearAll(); + + expect(service.state()).toEqual(EMPTY_SHIFT_DATA_STATE); + expect(firestoreMock.clearShiftData).not.toHaveBeenCalled(); + }); + }); + describe('mutate', () => { it('mutates state locally in guest mode without calling Firestore', async () => { const service = TestBed.inject(UserDataService); diff --git a/src/services/user-data.service.ts b/src/services/user-data.service.ts index 2737d75..7db36f2 100644 --- a/src/services/user-data.service.ts +++ b/src/services/user-data.service.ts @@ -39,6 +39,19 @@ export class UserDataService { this._state.set(next); } + /** + * Clears all shift data locally and, when authenticated, deletes it from + * Firestore as well. Without the cloud deletion the realtime listeners would + * restore the wiped shifts on the next reload. + */ + async clearAll(): Promise { + this._state.set(EMPTY_SHIFT_DATA_STATE); + const auth = this.auth.state(); + if (auth.mode === 'authenticated' && auth.uid) { + await this.firestore.clearShiftData(auth.uid); + } + } + /** Removes a device from the cloud registry, freeing one installation slot. */ async removeDevice(deviceId: string): Promise { const auth = this.auth.state();