1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
|
import type { SourceModule } from "../registry";
import {
BriefsConfig,
BriefsPayload,
type BriefsConfig as Config,
type BriefsPayload as Payload,
} from "@/lib/schemas/sources/briefs";
// Shape of a single category file on screamer.humdrum.one.
interface RawBrief {
date?: string;
category?: string;
stories?: Array<{
title?: string;
summary?: string;
articles?: Array<{ publication?: string; link?: string }>;
}>;
}
// Newest-first list of dates that have at least one published brief.
async function fetchDates(baseUrl: string, signal?: AbortSignal): Promise<string[]> {
try {
const res = await fetch(`${baseUrl}/data/index.json`, { signal });
if (!res.ok) return [];
const j = (await res.json()) as { dates?: string[] };
return Array.isArray(j.dates) ? j.dates : [];
} catch {
return [];
}
}
// Fetch the newest available brief for one category, walking back through dates
// until one resolves (briefs publish at staggered times overnight, so today's
// may not exist yet at 6 AM). Returns null if none found in the window.
async function fetchCategory(
baseUrl: string,
key: string,
dates: string[],
signal?: AbortSignal,
): Promise<{ date: string; raw: RawBrief } | null> {
for (const date of dates) {
try {
const res = await fetch(`${baseUrl}/data/${date}-${key}.json`, { signal });
if (!res.ok) continue;
const raw = (await res.json()) as RawBrief;
if (raw.stories && raw.stories.length > 0) return { date, raw };
} catch {
// try the next date
}
}
return null;
}
export const briefsModule: SourceModule<Config, Payload> = {
kind: "briefs",
label: "News Briefs",
keyless: true,
defaultRefreshSeconds: 3600,
configSchema: BriefsConfig,
payloadSchema: BriefsPayload,
isConfigured: (config) => config.categories.length > 0,
async fetch({ config, signal }) {
const baseUrl = config.baseUrl.replace(/\/$/, "");
const allDates = await fetchDates(baseUrl, signal);
// index.json is newest-first; cap how far back we'll look per category.
const dates = (allDates.length ? allDates : []).slice(0, config.lookbackDays);
const categories = await Promise.all(
config.categories.map(async ({ key, name }) => {
const hit = await fetchCategory(baseUrl, key, dates, signal);
const stories = (hit?.raw.stories ?? []).map((s) => ({
title: s.title ?? "",
summary: s.summary ?? "",
sources: [
...new Set((s.articles ?? []).map((a) => a.publication).filter((p): p is string => !!p)),
],
link: (s.articles ?? []).find((a) => a.link)?.link,
}));
return { key, name, date: hit?.date ?? null, stories };
}),
);
return { categories };
},
};
|