commit ce63f663c012fd7a3e572011fd5a9bedebbe508b
parent 89c1f7ffff64802e662dfa179be3a0cb245f7244
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Mon, 28 Sep 2026 18:00:50 -0400
common: the homepage fold counts a transcript with no transcribedDate
The fold required `hasTranscript && transcribedDate` for everything, so a
stats page whose transcripts carry no date showed a site serving 1,889 videos
as 0 transcripts, 0 channels, 0 hours. A transcript now counts for totals,
channels, hours, the site card's total and upload-month placement; only the
transcribed time series, "this month" and the recent rail need the date.
buildStats guarantees one since stats schema 6; this is for pages from before.
HOMEPAGE_SUMMARY_VERSION stays 5: no field is added.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat:
2 files changed, 73 insertions(+), 6 deletions(-)
diff --git a/common/lib/homepageSummary.test.ts b/common/lib/homepageSummary.test.ts
@@ -198,6 +198,46 @@ test("a site's accent is published as a hex: an id becomes its on-dark value", (
assert.ok(plain.sites.every((x) => !("accent" in x)));
});
+// Stats schema 6 guarantees a transcript a date, but a stats page written
+// before it could carry a whole channel of transcripts with none — and the fold
+// used to require the date for everything, so a site serving 1,889 videos
+// showed 0 transcripts, 0 channels, 0 hours. The date is only for the time axis.
+test("a transcript with no transcribedDate (a pre-schema-6 page) is counted, just not charted", () => {
+ const sites = [...SITES, site("gamma", ["g1"], "https://gamma.example")];
+ const undated = [
+ stat({ channelSlug: "g1", id: "u1", uploadDate: "20251101", duration: 3600, transcribedDate: null }),
+ stat({ channelSlug: "g1", id: "u2", uploadDate: "20260210", duration: 3600, transcribedDate: null }),
+ ];
+ const base = buildHomepageSummary(STATS, CHANNEL_SITES, sites, NOW);
+ const s = buildHomepageSummary([...STATS, ...undated], { ...CHANNEL_SITES, g1: ["gamma"] }, sites, NOW);
+
+ const gamma = s.sites.find((x) => x.siteId === "gamma");
+ assert.ok(gamma, "a site whose transcripts are all undated is still on the family page");
+ assert.deepEqual(
+ [gamma.channels, gamma.transcribed.total, gamma.hoursArchived, gamma.recordings],
+ [1, 2, 2, 2],
+ );
+ // No month to put them in: not this month, not the sparkline, not a series.
+ assert.equal(gamma.transcribed.thisMonth, 0);
+ assert.deepEqual(gamma.transcribed.last12, new Array(12).fill(0));
+ assert.equal(s.series.transcribed.month.bySite.gamma, undefined);
+ assert.deepEqual(s.series.transcribed.month.total, base.series.transcribed.month.total);
+ assert.equal(s.totals.transcribedThisMonth, base.totals.transcribedThisMonth);
+ assert.ok(!s.recent.some((r) => r.siteId === "gamma"), "the recent rail needs a date");
+ // Counted everywhere a count lives, and placed by UPLOAD month like any other.
+ assert.equal(s.totals.transcripts, base.totals.transcripts + 2);
+ assert.equal(s.totals.channels, base.totals.channels + 1);
+ assert.equal(s.official!.transcripts, base.official!.transcripts + 2);
+ assert.equal(s.official!.channels, base.official!.channels + 1);
+ assert.equal(s.monthly!.find((m) => m.month === "2025-11")!.bySite.gamma, 1);
+ assert.equal(s.monthly!.find((m) => m.month === "2026-02")!.bySite.gamma, 1);
+ const placed = s.monthly!.reduce(
+ (a, m) => a + Object.values(m.bySite).reduce((x, y) => x + y, 0),
+ 0,
+ );
+ assert.equal(placed + s.monthlyUnplaced!, s.official!.transcripts);
+});
+
test("a named accent also travels as its id; a custom hex and no accent carry none", () => {
const sites = [
{ ...site("alpha", ["a1", "a2"], "https://alpha.example"), accent: " Brass " },
diff --git a/common/lib/homepageSummary.ts b/common/lib/homepageSummary.ts
@@ -61,6 +61,9 @@ export type MetricSeries = {
};
export type SiteMetricStat = {
+ // For `transcribed`, also counts transcripts with no transcribedDate (a stats
+ // page from before stats schema 6) — they have no month, so they are in
+ // `total` and in neither `thisMonth` nor `last12`.
total: number;
thisMonth: number;
last12: number[]; // last 12 month buckets (zero-padded left), for the sparkline
@@ -322,6 +325,12 @@ function siteStatFrom(
return { total, thisMonth, last12 };
}
+// A site's transcribed card counts its undated transcripts too. They have no
+// month, so they join `total` only — never `thisMonth` or the sparkline.
+function withUndated(stat: SiteMetricStat, undated: number): SiteMetricStat {
+ return undated > 0 ? { ...stat, total: stat.total + undated } : stat;
+}
+
export function buildHomepageSummary(
stats: readonly VideoStat[],
channelSites: ChannelSitesMap,
@@ -362,11 +371,14 @@ export function buildHomepageSummary(
// seconds, gone. `transcripts` is counted HERE, in the same pass that places
// or leaves unplaced each transcript, so `placed + unplaced =
// official.transcripts` holds even for a `transcribedDate` that bucketize
- // drops (a future or malformed month).
+ // drops (a future or malformed month). `undated` counts the transcripts with
+ // no `transcribedDate` at all (a stats page from before stats schema 6): no
+ // transcribed bucket can hold them, so they join the card's total directly.
type SiteAcc = {
channels: Set<string>;
recordings: number;
transcripts: number;
+ undated: number;
seconds: number;
gone: number;
};
@@ -376,7 +388,7 @@ export function buildHomepageSummary(
if (!a)
siteAcc.set(
id,
- (a = { channels: new Set(), recordings: 0, transcripts: 0, seconds: 0, gone: 0 }),
+ (a = { channels: new Set(), recordings: 0, transcripts: 0, undated: 0, seconds: 0, gone: 0 }),
);
return a;
};
@@ -385,12 +397,20 @@ export function buildHomepageSummary(
let monthlyUnplaced = 0;
for (const s of stats) {
- const hasTx = s.hasTranscript && !!s.transcribedDate;
+ // A transcript COUNTS whether or not it carries a date: transcripts,
+ // channels, hours, upload-month placement. Only the transcribed time series
+ // (and "this month", and the recent rail) need `transcribedDate`. buildStats
+ // guarantees one since stats schema 6, but a page written before that could
+ // hold a whole channel of transcripts with none — and requiring the date
+ // here made a site serving 1,889 videos show 0 transcripts, 0 channels and
+ // 0 hours.
+ const hasTx = s.hasTranscript;
+ const dated = hasTx && !!s.transcribedDate;
if (hasTx) {
transcripts += 1;
channelSet.add(s.channelSlug);
hoursSeconds += s.duration > 0 ? s.duration : 0;
- if (monthOf(s.transcribedDate) === nowMonth) transcribedThisMonth += 1;
+ if (dated && monthOf(s.transcribedDate) === nowMonth) transcribedThisMonth += 1;
}
if (s.downloadedDate) {
downloads += 1;
@@ -410,7 +430,11 @@ export function buildHomepageSummary(
const um = uploadMonthOf(s.uploadDate);
if (um && um < nowMonth) uploadItems.push({ month: um, siteId });
else monthlyUnplaced += 1;
- transcribedItems.push({ date: s.transcribedDate as string, channelSlug: s.channelSlug, siteId });
+ if (dated) {
+ transcribedItems.push({ date: s.transcribedDate as string, channelSlug: s.channelSlug, siteId });
+ } else {
+ acc.undated += 1;
+ }
}
if (s.downloadedDate) {
downloadedItems.push({ date: s.downloadedDate, channelSlug: s.channelSlug, siteId });
@@ -433,7 +457,10 @@ export function buildHomepageSummary(
siteTitle: s.siteTitle,
siteDescription: s.siteDescription,
siteUrl: s.siteUrl,
- transcribed: siteStatFrom(series.transcribed.month, s.siteId, nowMonth),
+ transcribed: withUndated(
+ siteStatFrom(series.transcribed.month, s.siteId, nowMonth),
+ siteAcc.get(s.siteId)?.undated ?? 0,
+ ),
downloaded: siteStatFrom(series.downloaded.month, s.siteId, nowMonth),
channels: siteAcc.get(s.siteId)?.channels.size ?? 0,
recordings: siteAcc.get(s.siteId)?.recordings ?? 0,