{"record":{"id":"123cdc4591378e0e","repo":"Kareadita/Kavita","slug":"reading-profiles-device-overlap","errorCode":null,"errorMessage":"reading-profiles-device-overlap","messagePattern":"reading-profiles-device-overlap","errorType":"exception","errorClass":"KavitaException","httpStatus":500,"severity":"error","filePath":"Kavita.Services/Reading/ReadingProfileService.cs","lineNumber":358,"sourceCode":"            .ToListAsync();\n\n        foreach (var profile in profiles)\n        {\n            profile.DeviceIds.Remove(deviceId);\n            unitOfWork.AppUserReadingProfileRepository.Update(profile);\n        }\n    }\n\n    private static void DeviceOverlapGuard(List<AppUserReadingProfile> profiles)\n    {\n        var anyOverlap = profiles\n            .Any(rp => profiles\n                .Where(other => other.Id != rp.Id)\n                .Any(other => other.DeviceIds.Intersect(rp.DeviceIds).Any()));\n\n        if (anyOverlap)\n        {\n            throw new KavitaException(\"reading-profiles-device-overlap\");\n        }\n    }\n\n    /// <summary>\n    /// Deletes all implicit profiles with overlapping ids (For devices 0 overlaps with 0). And removes links with\n    /// series & libraries\n    /// </summary>\n    /// <param name=\"profiles\"></param>\n    /// <param name=\"seriesIds\"></param>\n    /// <param name=\"libraryIds\"></param>\n    /// <param name=\"deviceIds\"></param>\n    private void DeleteImplicitAndRemoveFromUserProfiles(IList<AppUserReadingProfile> profiles, IList<int> seriesIds, IList<int> libraryIds, List<int>? deviceIds)\n    {\n        var implicitProfiles = profiles\n            .Where(DeviceIdFilter)\n            .Where(rp => rp.SeriesIds.Intersect(seriesIds).Any())\n            .Where(rp => rp.Kind == ReadingProfileKind.Implicit)\n            .ToList();","sourceCodeStart":340,"sourceCodeEnd":376,"githubUrl":"https://github.com/Kareadita/Kavita/blob/9c3e5400007f8a0282f7d883f2ad5e71716e514d/Kavita.Services/Reading/ReadingProfileService.cs#L340-L376","documentation":"Thrown by the private DeviceOverlapGuard in ReadingProfileService, invoked from three create/update paths (lines ~196, 220, 252). It scans the selected set of User reading profiles and rejects the batch when any two distinct profiles share at least one device id. Kavita uses device ids to pick which profile applies on a given device, so overlapping devices would make profile selection ambiguous; the guard enforces a one-device-to-one-user-profile invariant.","triggerScenarios":"Bulk or multi-profile operations (e.g. updating several User profiles at once) where the payload assigns the same deviceId to two different profiles. Triggered whenever DeviceOverlapGuard detects profiles.Any(rp => profiles.Where(other => other.Id != rp.Id).Any(other => other.DeviceIds.Intersect(rp.DeviceIds).Any())).","commonSituations":"A device was reassigned to a new profile without being removed from the old one in the same batch; an import/sync job assigned overlapping device lists; or the UI allowed editing multiple profiles' device sets independently and they were committed together.","solutions":["De-duplicate device assignments across the profiles in the payload before submitting: each deviceId must appear in exactly one profile.","If you intentionally moved a device, remove it from the previous profile's DeviceIds in the same request.","Inspect the selectedProfiles passed to DeviceOverlapGuard and reconcile the conflicting DeviceIds lists.","Add client-side validation that warns when a selected device is already bound to another profile."],"exampleFix":"// before\nDeviceOverlapGuard(selectedProfiles); // throws on first overlap\n\n// after - reconcile before guarding\nforeach (var p in selectedProfiles)\n    p.DeviceIds = p.DeviceIds.Distinct().Except(devicesClaimedByOthers(p, selectedProfiles)).ToList();\nDeviceOverlapGuard(selectedProfiles);","handlingStrategy":"validation","validationCode":"static bool HasDeviceOverlap(IEnumerable<AppUserReadingProfile> profiles)\n{\n    var list = profiles.ToList();\n    return list.Any(a => list.Any(b => b.Id != a.Id && b.DeviceIds.Intersect(a.DeviceIds).Any()));\n}\n// if (HasDeviceOverlap(selectedProfiles)) return BadRequest(\"...\");","typeGuard":"static bool DeviceSetsAreUnique(IEnumerable<AppUserReadingProfile> profiles) => !HasDeviceOverlap(profiles);","tryCatchPattern":null,"preventionTips":["Enforce one-device-per-user-profile in the UI before bulk save.","When moving a device between profiles, remove it from the old one in the same batch.","Unit-test DeviceOverlapGuard with overlapping inputs to assert the throw."],"tags":["reading-profiles","validation","device-binding","business-rule"],"backgroundTag":null,"analyzedSha":"9c3e5400007f8a0282f7d883f2ad5e71716e514d","analyzedAt":"2026-08-13T19:06:05.897Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}