Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions AGENT_HANDOFF.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
31 changes: 31 additions & 0 deletions src/services/firestore-user-data.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
16 changes: 16 additions & 0 deletions src/services/firestore-user-data.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
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<void> {
const db = this.firebase.firestore;
const batch = writeBatch(db);
Expand Down
2 changes: 1 addition & 1 deletion src/services/shift.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
42 changes: 42 additions & 0 deletions src/services/user-data.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ describe('UserDataService', () => {
upsertShiftOverride: jest.Mock;
applyBatch: jest.Mock;
removeDevice: jest.Mock;
clearShiftData: jest.Mock;
};

beforeEach(() => {
Expand All @@ -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();
Expand Down Expand Up @@ -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);
Expand Down
13 changes: 13 additions & 0 deletions src/services/user-data.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
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<void> {
const auth = this.auth.state();
Expand Down
Loading