diff --git a/.openhands_instructions b/.openhands_instructions deleted file mode 100644 index b384155..0000000 --- a/.openhands_instructions +++ /dev/null @@ -1,3 +0,0 @@ -Read and follow `/AGENTS.md` — it is the single source of truth for project rules, coding standards, and design constraints. - -Do not commit documentation files, standalone test scripts, or debugging artifacts. Only commit production code and its corresponding spec files. diff --git a/.openhands_instructions b/.openhands_instructions new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/.openhands_instructions @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 3ecbe18..8d1d682 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -164,6 +164,11 @@ Every card (ticket, PR, todo, idea) has exactly one state: Cards never have colored backgrounds. Color lives only in badges, icons, text, and the amber attention bar. Type badges ("Fehler", "Aufgabe", "User Story") are always neutral stone — only status badges carry semantic colors. +## Commit Hygiene + +- Do not commit documentation files, standalone test scripts, or debugging artifacts +- Only commit production code and its corresponding spec files + ## Accessibility - Must pass all AXE checks diff --git a/src/app/jira/jira.service.spec.ts b/src/app/jira/jira.service.spec.ts index d5b46d2..f7dfc94 100644 --- a/src/app/jira/jira.service.spec.ts +++ b/src/app/jira/jira.service.spec.ts @@ -9,6 +9,8 @@ describe('JiraService', () => { let httpMock: HttpTestingController; beforeEach(() => { + localStorage.removeItem('jira.userDisplayNameCache'); + TestBed.configureTestingModule({ providers: [provideHttpClient(withFetch()), provideHttpClientTesting()], }); @@ -369,4 +371,76 @@ describe('JiraService', () => { .flush(makeRawIssue({ description: 'Kein Erwähnungen hier.' })); httpMock.expectNone((r) => r.url.includes('/user')); }); + + it('loads user display name cache from localStorage on service initialization', () => { + localStorage.setItem( + 'jira.userDisplayNameCache', + JSON.stringify({ u1: 'User One', u2: 'User Two' }), + ); + + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + providers: [provideHttpClient(withFetch()), provideHttpClientTesting()], + }); + const freshService = TestBed.inject(JiraService); + httpMock = TestBed.inject(HttpTestingController); + + expect(freshService['userDisplayNameCache'].size).toBe(2); + expect(freshService['userDisplayNameCache'].get('u1')).toBe('User One'); + expect(freshService['userDisplayNameCache'].get('u2')).toBe('User Two'); + }); + + it('saves resolved user display names to localStorage', () => { + let result: JiraTicket | undefined; + service.getTicketByKey('TEST-1').subscribe((t) => (result = t)); + + httpMock + .expectOne((r) => r.url.includes('/issue/TEST-1')) + .flush(makeRawIssue({ description: '[~newuser]' })); + httpMock + .expectOne((r) => r.url.includes('/user') && r.params.get('username') === 'newuser') + .flush({ displayName: 'New User' }); + + expect(result!.description).toBe('[~New User]'); + + const parsed = JSON.parse(localStorage.getItem('jira.userDisplayNameCache')!); + expect(parsed['newuser']).toBe('New User'); + }); + + it('uses cached user display names from localStorage instead of making API calls', () => { + localStorage.setItem('jira.userDisplayNameCache', JSON.stringify({ cacheduser: 'Cached User' })); + + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + providers: [provideHttpClient(withFetch()), provideHttpClientTesting()], + }); + const freshService = TestBed.inject(JiraService); + httpMock = TestBed.inject(HttpTestingController); + + let result: JiraTicket | undefined; + freshService.getTicketByKey('TEST-1').subscribe((t) => (result = t)); + + httpMock + .expectOne((r) => r.url.includes('/issue/TEST-1')) + .flush(makeRawIssue({ description: '[~cacheduser]' })); + + httpMock.expectNone((r) => r.url.includes('/user')); + expect(result!.description).toBe('[~Cached User]'); + }); + + it('handles corrupted localStorage cache gracefully', () => { + localStorage.setItem('jira.userDisplayNameCache', 'invalid json data'); + + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + providers: [provideHttpClient(withFetch()), provideHttpClientTesting()], + }); + + expect(() => { + const freshService = TestBed.inject(JiraService); + expect(freshService['userDisplayNameCache'].size).toBe(0); + }).not.toThrow(); + + httpMock = TestBed.inject(HttpTestingController); + }); }); diff --git a/src/app/jira/jira.service.ts b/src/app/jira/jira.service.ts index ef6b680..32766c6 100644 --- a/src/app/jira/jira.service.ts +++ b/src/app/jira/jira.service.ts @@ -119,12 +119,44 @@ export class JiraService { private readonly http = inject(HttpClient); private readonly baseUrl = `${environment.proxyUrl}/jira/rest/api/2`; private readonly userDisplayNameCache = new Map(); + private readonly STORAGE_KEY = 'jira.userDisplayNameCache'; private hasLoadedOnce = false; readonly loading = signal(true); readonly error = signal(false); private readonly _tickets = signal([]); readonly tickets = this._tickets.asReadonly(); + constructor() { + this.loadCacheFromLocalStorage(); + } + + private loadCacheFromLocalStorage(): void { + try { + const cachedData = localStorage.getItem(this.STORAGE_KEY); + if (cachedData) { + const parsed = JSON.parse(cachedData); + if (typeof parsed === 'object' && parsed !== null) { + Object.entries(parsed).forEach(([slug, displayName]) => { + if (typeof slug === 'string' && typeof displayName === 'string') { + this.userDisplayNameCache.set(slug, displayName); + } + }); + } + } + } catch (error) { + console.error('Failed to load user display name cache from localStorage:', error); + } + } + + private saveCacheToLocalStorage(): void { + try { + const cacheObject = Object.fromEntries(this.userDisplayNameCache.entries()); + localStorage.setItem(this.STORAGE_KEY, JSON.stringify(cacheObject)); + } catch (error) { + console.error('Failed to save user display name cache to localStorage:', error); + } + } + loadTickets(): Observable { if (!this.hasLoadedOnce) { this.loading.set(true); @@ -167,9 +199,16 @@ export class JiraService { ), ).pipe( map((results) => { + let cacheUpdated = false; results.forEach((result, i) => { - if (result) this.userDisplayNameCache.set(unknown[i], result.displayName); + if (result) { + this.userDisplayNameCache.set(unknown[i], result.displayName); + cacheUpdated = true; + } }); + if (cacheUpdated) { + this.saveCacheToLocalStorage(); + } }), ); }