| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| export function isGoogleNewsWrapper(url) { |
| return typeof url === 'string' && url.includes('news.google.com/rss/'); |
| } |
|
|
| |
| |
| |
| |
| |
| export const SILENT_ZERO_THRESHOLD = 2; |
|
|
| |
| |
| |
| |
| |
| export function buildFeedHealthPayload(results, previousPayload, nowMs) { |
| const prevFeeds = previousPayload && typeof previousPayload === 'object' && previousPayload.feeds |
| ? previousPayload.feeds |
| : {}; |
|
|
| const feeds = {}; |
| const summary = { ok: 0, stale: 0, dead: 0, empty: 0, skipped: 0 }; |
| const silentZeros = []; |
|
|
| for (const result of results) { |
| const key = result.url; |
| const status = result.status; |
| if (status === 'OK') summary.ok++; |
| else if (status === 'STALE') summary.stale++; |
| else if (status === 'DEAD') summary.dead++; |
| else if (status === 'EMPTY') summary.empty++; |
| else summary.skipped++; |
|
|
| const wrapper = isGoogleNewsWrapper(result.url); |
| const prevStreak = Number.isFinite(prevFeeds[key]?.consecutiveEmpty) |
| ? prevFeeds[key].consecutiveEmpty |
| : 0; |
| |
| |
| const deliveredNothing = status === 'EMPTY' || status === 'DEAD'; |
| const consecutiveEmpty = deliveredNothing ? prevStreak + 1 : 0; |
|
|
| const entry = { |
| name: result.name, |
| status, |
| catalog: result.catalog ?? 'client', |
| wrapper, |
| consecutiveEmpty, |
| }; |
| if (result.detail) entry.detail = String(result.detail).slice(0, 120); |
| feeds[key] = entry; |
|
|
| if (wrapper && consecutiveEmpty >= SILENT_ZERO_THRESHOLD) { |
| silentZeros.push({ name: result.name, url: result.url, consecutiveEmpty }); |
| } |
| } |
|
|
| silentZeros.sort((a, b) => b.consecutiveEmpty - a.consecutiveEmpty || a.name.localeCompare(b.name)); |
|
|
| return { |
| v: 1, |
| checkedAt: nowMs, |
| summary, |
| feedCount: results.length, |
| silentZeros, |
| feeds, |
| }; |
| } |
|
|