From 95699306c051c2b416c5984b5e982ca8c9f84273 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 22 Jul 2026 15:55:20 +0200 Subject: [PATCH] Infer daily statistics failures from expected podcasts --- src/api/StatusApi.test.ts | 44 +---------------- src/api/StatusApi.ts | 54 ++++++++------------- src/db/StatusRepository.test.ts | 24 --------- src/db/StatusRepository.ts | 30 +----------- views/partials/statistics_endpoint_card.hbs | 10 ++-- views/statistics.hbs | 4 +- 6 files changed, 27 insertions(+), 139 deletions(-) diff --git a/src/api/StatusApi.test.ts b/src/api/StatusApi.test.ts index 8482061..4ffd7b2 100644 --- a/src/api/StatusApi.test.ts +++ b/src/api/StatusApi.test.ts @@ -26,7 +26,6 @@ describe('StatusApi', () => { it('returns endpoint statistics from the same status data as the status endpoint', async () => { const periodDays = 90 const dates = getDateRange(periodDays) - const firstAvailableDate = dates[1] const today = dates[dates.length - 1] const recentDate = new Date() recentDate.setDate(recentDate.getDate() - 1) @@ -67,32 +66,6 @@ describe('StatusApi', () => { date: today, }, ]), - getEndpointFirstUpdates: jest.fn().mockResolvedValue([ - { - accountId: 1, - provider: 'spotify', - endpoint: 'aggregate', - date: firstAvailableDate, - }, - { - accountId: 2, - provider: 'spotify', - endpoint: 'aggregate', - date: firstAvailableDate, - }, - { - accountId: 3, - provider: 'spotify', - endpoint: 'aggregate', - date: firstAvailableDate, - }, - { - accountId: 1, - provider: 'apple', - endpoint: 'trends', - date: today, - }, - ]), } as unknown as StatusRepository const statusApi = new StatusApi(statusRepo) @@ -102,7 +75,6 @@ describe('StatusApi', () => { expect(statusRepo.getDailyEndpointUpdates).toHaveBeenCalledWith( periodDays ) - expect(statusRepo.getEndpointFirstUpdates).toHaveBeenCalledTimes(1) expect(statistics).toHaveLength(2) expect(statistics[0]).toEqual( expect.objectContaining({ @@ -125,20 +97,7 @@ describe('StatusApi', () => { expect(statistics[0].days).toHaveLength(periodDays) expect(statistics[0].days[0]).toEqual({ date: dates[0], - title: `${dates[0]}: no data available`, - isUnavailable: true, - isDegraded: false, - requests: { - total: 0, - successful: 0, - failed: 0, - successRate: 0, - }, - }) - expect(statistics[0].days[1]).toEqual({ - date: firstAvailableDate, - title: `${firstAvailableDate}: 0/3 successful, 3 failed`, - isUnavailable: false, + title: `${dates[0]}: 0/3 successful, 3 failed`, isDegraded: true, requests: { total: 3, @@ -150,7 +109,6 @@ describe('StatusApi', () => { expect(statistics[0].days[periodDays - 1]).toEqual({ date: today, title: `${today}: 2/3 successful, 1 failed`, - isUnavailable: false, isDegraded: true, requests: { total: 3, diff --git a/src/api/StatusApi.ts b/src/api/StatusApi.ts index 8a8f02f..79017b2 100644 --- a/src/api/StatusApi.ts +++ b/src/api/StatusApi.ts @@ -15,7 +15,6 @@ type FailedPodcast = { type DailyEndpointStatistics = { date: string title: string - isUnavailable: boolean isDegraded: boolean requests: { total: number @@ -95,10 +94,6 @@ const getDayTitle = ( date: string, requests: EndpointStatistics['requests'] ) => { - if (requests.total === 0) { - return `${date}: no data available` - } - return `${date}: ${requests.successful}/${requests.total} successful, ${requests.failed} failed` } @@ -170,7 +165,6 @@ class StatusApi { const dailyUpdates = await this.statusRepo.getDailyEndpointUpdates( periodDays ) - const firstUpdates = await this.statusRepo.getEndpointFirstUpdates() const maxAgeHours = periodDays * 24 const dates = getDateRange(periodDays) const statistics: EndpointStatistics[] = [] @@ -178,8 +172,8 @@ class StatusApi { const successfulPodcastsByEndpointAndDate: { [endpointIdentifier: string]: { [date: string]: Set } } = {} - const firstUpdateByEndpointAndPodcast: { - [endpointIdentifier: string]: { [podcastId: number]: string } + const expectedPodcastsByEndpoint: { + [endpointIdentifier: string]: Set } = {} Object.entries(status).forEach(([podcastIdStr, podcastData]) => { @@ -198,8 +192,7 @@ class StatusApi { failedPodcasts: [], days: dates.map((date) => ({ date, - title: `${date}: no data available`, - isUnavailable: true, + title: '', isDegraded: false, requests: emptyRequests(), })), @@ -209,6 +202,14 @@ class StatusApi { ) } + if (!expectedPodcastsByEndpoint[endpointIdentifier]) { + expectedPodcastsByEndpoint[endpointIdentifier] = + new Set() + } + expectedPodcastsByEndpoint[endpointIdentifier].add( + podcastId + ) + const endpointStatistics = statisticsByEndpoint[endpointIdentifier] const ageHours = getEndpointAgeHours(endpointDate) @@ -228,20 +229,6 @@ class StatusApi { ) }) - firstUpdates.forEach((update) => { - const endpointIdentifier = getEndpointIdentifier( - update.provider, - update.endpoint - ) - if (!firstUpdateByEndpointAndPodcast[endpointIdentifier]) { - firstUpdateByEndpointAndPodcast[endpointIdentifier] = {} - } - - firstUpdateByEndpointAndPodcast[endpointIdentifier][ - update.accountId - ] = update.date - }) - dailyUpdates.forEach((update) => { const endpointIdentifier = getEndpointIdentifier( update.provider, @@ -273,13 +260,16 @@ class StatusApi { ) const successfulPodcastsByDate = successfulPodcastsByEndpointAndDate[endpointIdentifier] ?? {} - const firstUpdateByPodcast = - firstUpdateByEndpointAndPodcast[endpointIdentifier] ?? {} - + const expectedPodcasts = + expectedPodcastsByEndpoint[endpointIdentifier] ?? new Set() + + // The updates table only records successful imports. Until failed + // attempts are stored explicitly, daily failures are inferred as + // expected podcasts without a successful update for that endpoint on + // that day. Expected podcasts are all podcasts that have ever + // reported for the endpoint. endpointStatistics.days.forEach((day) => { - day.requests.total = Object.values(firstUpdateByPodcast).filter( - (firstUpdateDate) => firstUpdateDate <= day.date - ).length + day.requests.total = expectedPodcasts.size day.requests.successful = successfulPodcastsByDate[day.date]?.size ?? 0 day.requests.failed = Math.max( @@ -287,9 +277,7 @@ class StatusApi { 0 ) updateSuccessRate(day.requests) - day.isUnavailable = day.requests.total === 0 - day.isDegraded = - day.requests.total > 0 && day.requests.failed > 0 + day.isDegraded = day.requests.failed > 0 day.title = getDayTitle(day.date, day.requests) }) }) diff --git a/src/db/StatusRepository.test.ts b/src/db/StatusRepository.test.ts index 887dd94..48fa2ec 100644 --- a/src/db/StatusRepository.test.ts +++ b/src/db/StatusRepository.test.ts @@ -94,28 +94,4 @@ describe('StatusRepository', () => { }, ]) }) - - it('returns the first update date per podcast and endpoint', async () => { - const rows = [ - { - account_id: 1, - provider: 'spotify', - endpoint: 'aggregate', - first_update_date: '2026-07-01', - }, - ] as RowDataPacket[] - - mockPool.query.mockResolvedValue([rows, []] as any) - - const updates = await statusRepository.getEndpointFirstUpdates() - - expect(updates).toEqual([ - { - accountId: 1, - provider: 'spotify', - endpoint: 'aggregate', - date: '2026-07-01', - }, - ]) - }) }) diff --git a/src/db/StatusRepository.ts b/src/db/StatusRepository.ts index 450f4e1..279e05f 100644 --- a/src/db/StatusRepository.ts +++ b/src/db/StatusRepository.ts @@ -19,13 +19,6 @@ type DailyEndpointUpdate = { date: string } -type EndpointFirstUpdate = { - accountId: number - provider: string - endpoint: string - date: string -} - class StatusRepository { pool: Pool @@ -108,27 +101,6 @@ class StatusRepository { })) } - async getEndpointFirstUpdates(): Promise { - const query = ` - SELECT - account_id, - provider, - endpoint, - DATE(MIN(created)) AS first_update_date - FROM updates - GROUP BY account_id, provider, endpoint - ORDER BY first_update_date, provider, endpoint, account_id - ` - const response = (await this.pool.query(query)) as RowDataPacket[] - - return response[0].map((row: RowDataPacket) => ({ - accountId: row.account_id, - provider: row.provider, - endpoint: row.endpoint, - date: row.first_update_date, - })) - } - // Write the raw JSON status data into the `updates` table // Use the current timestamp as the update time async updateStatus(accountId: number, update: StatusPayload): Promise { @@ -142,4 +114,4 @@ class StatusRepository { } } -export { StatusRepository, Status, DailyEndpointUpdate, EndpointFirstUpdate } +export { StatusRepository, Status, DailyEndpointUpdate } diff --git a/views/partials/statistics_endpoint_card.hbs b/views/partials/statistics_endpoint_card.hbs index 2d4c12b..966c68f 100644 --- a/views/partials/statistics_endpoint_card.hbs +++ b/views/partials/statistics_endpoint_card.hbs @@ -10,14 +10,10 @@
{{#each days}} - {{#if isUnavailable}} -
+ {{#if isDegraded}} +
{{else}} - {{#if isDegraded}} -
- {{else}} -
- {{/if}} +
{{/if}} {{/each}}
diff --git a/views/statistics.hbs b/views/statistics.hbs index 5998ac9..cbbde98 100644 --- a/views/statistics.hbs +++ b/views/statistics.hbs @@ -129,9 +129,7 @@ background: #d4a72c; } - .daily-bar.unavailable { - background: #d1d5db; - } + .stats-row { align-items: center;