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
6 changes: 6 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
12 changes: 12 additions & 0 deletions src/app/service/gear-parser.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
});
3 changes: 3 additions & 0 deletions src/app/service/gear-parser.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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!; }
Expand Down
50 changes: 50 additions & 0 deletions src/app/service/milestone-parser.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
69 changes: 39 additions & 30 deletions src/app/service/milestone-parser.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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);
}
}
}

Expand Down
Loading