diff --git a/CLAUDE.md b/CLAUDE.md index 8ebaf876..d8c9d141 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -18,6 +18,12 @@ npm run lint # eslint ALWAYS bump the version in package.json for each new PR. If the bungie manifest version changes, also bump package.json's "manifest" value. These two values are used to bust caches on browser clients, if not bumped the clients won't see the new values. +New workspaces/worktrees are missing the gitignored `src/environments/keys.ts` and `keys-prod.ts` (Bungie API credentials), so builds and tests fail with "Cannot find module './keys'". Copy them from the base checkout: + +```bash +cp ~/projects/d2-checklist/src/environments/keys.ts ~/projects/d2-checklist/src/environments/keys-prod.ts src/environments/ +``` + ## Project structure - `src/app/` - Angular application source diff --git a/package-lock.json b/package-lock.json index 9566805e..d5164152 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "d2-checklist", - "version": "30.0.0", + "version": "32.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "d2-checklist", - "version": "30.0.0", + "version": "32.0.1", "license": "MIT", "dependencies": { "@angular/animations": "^19.2.20", diff --git a/package.json b/package.json index ddf0e76f..15155dde 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "d2-checklist", - "version": "32.0.0", + "version": "32.0.1", "manifest": "244019.26.05.29.1640-4-bnet.65312", "license": "MIT", "scripts": { diff --git a/src/app/service/gear-parser.service.spec.ts b/src/app/service/gear-parser.service.spec.ts index 3af6c1d6..9c9db51d 100644 --- a/src/app/service/gear-parser.service.spec.ts +++ b/src/app/service/gear-parser.service.spec.ts @@ -120,5 +120,17 @@ describe('GearParserService', () => { it('should return null for tracker plugs (hash 2947756142)', () => { expect(GearParserService.getPlugName(makePlugDesc('Kill Tracker', 'tracker', 2947756142))).toBeFalsy(); }); + + it('should return null when plugDesc is undefined (hash missing from manifest)', () => { + expect(GearParserService.getPlugName(undefined)).toBeFalsy(); + }); + + it('should return null when plugDesc is null', () => { + expect(GearParserService.getPlugName(null)).toBeFalsy(); + }); + + it('should return null when displayProperties is missing', () => { + expect(GearParserService.getPlugName({ plug: {} })).toBeFalsy(); + }); }); }); diff --git a/src/app/service/gear-parser.service.ts b/src/app/service/gear-parser.service.ts index 64c41f6e..d1685dd3 100644 --- a/src/app/service/gear-parser.service.ts +++ b/src/app/service/gear-parser.service.ts @@ -164,6 +164,9 @@ export class GearParserService { } public static getPlugName(plugDesc: any): string { + // manifest updates can leave plug hashes that no longer resolve, so guard against missing descriptions + if (plugDesc == null) { return null!; } + if (plugDesc.displayProperties == null) { return null!; } const name = plugDesc.displayProperties.name; if (name == null) { return null!; } if (name.trim().length == 0) { return null!; } diff --git a/src/app/service/milestone-parser.service.spec.ts b/src/app/service/milestone-parser.service.spec.ts index e571a8ef..c3d42b3a 100644 --- a/src/app/service/milestone-parser.service.spec.ts +++ b/src/app/service/milestone-parser.service.spec.ts @@ -63,6 +63,56 @@ describe('MilestoneParserService', () => { }); }); + describe('parsePublicMilestones with missing manifest entries', () => { + function makeMilestoneDesc(hash: number): any { + return { + hash, + milestoneType: 3, + displayProperties: { name: 'Test Milestone', description: 'desc', icon: 'icon.png' } + }; + } + + function makePublicMs(milestoneHash: number, activityHash: number): any { + return { + milestoneHash, + order: 1, + startDate: '2026-06-09T17:00:00Z', + endDate: '2026-06-16T17:00:00Z', + activities: [{ activityHash, challengeObjectiveHashes: [], modifierHashes: [] }] + }; + } + + it('should skip activities whose hash is missing from the manifest', async () => { + const mockCache: any = { + getMilestone: (hash: any) => Promise.resolve(makeMilestoneDesc(hash)), + getActivity: () => Promise.resolve(undefined) // hash dropped by manifest update + }; + const svc = new MilestoneParserService(mockCache); + const result = await svc.parsePublicMilestones({ '1': makePublicMs(1, 555) }); + expect(result.publicMilestones.length).toBe(1); + expect(result.publicMilestones[0].activities.length).toBe(0); + }); + + it('should tolerate a missing activity mode when falling back for icons', async () => { + const aDesc = { + displayProperties: { name: 'Test Activity', description: 'd', icon: 'missing_icon.png' }, + activityLightLevel: 100, + tier: 1, + activityModeHashes: [999] + }; + const mockCache: any = { + getMilestone: (hash: any) => Promise.resolve(makeMilestoneDesc(hash)), + getActivity: () => Promise.resolve(aDesc), + getActivityMode: () => Promise.resolve(undefined) // hash dropped by manifest update + }; + const svc = new MilestoneParserService(mockCache); + const result = await svc.parsePublicMilestones({ '1': makePublicMs(1, 555) }); + expect(result.publicMilestones.length).toBe(1); + expect(result.publicMilestones[0].activities.length).toBe(1); + expect(result.publicMilestones[0].activities[0].name).toBe('Test Activity'); + }); + }); + describe('hasChallenge (static)', () => { it('should return true when challenge with matching hash exists', () => { const act = { diff --git a/src/app/service/milestone-parser.service.ts b/src/app/service/milestone-parser.service.ts index dc58eff8..d2c0dc77 100644 --- a/src/app/service/milestone-parser.service.ts +++ b/src/app/service/milestone-parser.service.ts @@ -368,7 +368,11 @@ export class MilestoneParserService { if (ms.activities != null) { for (const act of ms.activities) { const aDesc = await this.destinyCacheService.getActivity(act.activityHash); - + // manifest updates can leave activity hashes that no longer resolve + if (aDesc == null) { + continue; + } + if (act.challengeObjectiveHashes) { // skip weekly dungeons that aren't active // Grasp of Avarice 1092691445 does not show up in the weekly public profile if active @@ -411,7 +415,9 @@ export class MilestoneParserService { if (aDesc.activityModeHashes && aDesc.activityModeHashes.length > 0) { const amHash = aDesc.activityModeHashes[0]; const amDesc = await this.destinyCacheService.getActivityMode(amHash); - activityIcon = amDesc.displayProperties.icon; + if (amDesc != null) { + activityIcon = amDesc.displayProperties.icon; + } } } activities.push({ @@ -520,34 +526,37 @@ export class MilestoneParserService { const GOA_ACT_HASH = 3774021532; const desc = await this.destinyCacheService.getMilestone(GOA_HASH); const aDesc = await this.destinyCacheService.getActivity(GOA_ACT_HASH); - const pushMe: PublicMilestone = { - hash: GOA_HASH + '', - name: `Weekly Dungeon Challenge: ${desc.displayProperties.name}`, - desc: desc.displayProperties.description, - start: raid.start, - end: raid.end, - order: 31, - icon: desc.displayProperties.icon, - activities: [ - { - hash: GOA_ACT_HASH+'', - name: aDesc.displayProperties.name, - desc: aDesc.displayProperties.description, - ll: aDesc.activityLightLevel, - tier: aDesc.tier, - icon: aDesc.displayProperties.icon, - modifiers: [] - } - ], - rewards: 'Pinnacle Gear', - boost: this.parseMilestonePl('Pinnacle Gear'), - milestoneType: desc.milestoneType, - dependsOn: [], - doubled: false, - weeklyDungeon: true, - weeklyRaid: false - }; - returnMe.push(pushMe); + // manifest updates can drop these hashes, in which case just skip the GoA workaround + if (desc != null && aDesc != null) { + const pushMe: PublicMilestone = { + hash: GOA_HASH + '', + name: `Weekly Dungeon Challenge: ${desc.displayProperties.name}`, + desc: desc.displayProperties.description, + start: raid.start, + end: raid.end, + order: 31, + icon: desc.displayProperties.icon, + activities: [ + { + hash: GOA_ACT_HASH+'', + name: aDesc.displayProperties.name, + desc: aDesc.displayProperties.description, + ll: aDesc.activityLightLevel, + tier: aDesc.tier, + icon: aDesc.displayProperties.icon, + modifiers: [] + } + ], + rewards: 'Pinnacle Gear', + boost: this.parseMilestonePl('Pinnacle Gear'), + milestoneType: desc.milestoneType, + dependsOn: [], + doubled: false, + weeklyDungeon: true, + weeklyRaid: false + }; + returnMe.push(pushMe); + } } }