fix: treat District 'content not found' as waiting, not as a failure

A film that is announced but has no showtimes yet answers 400 with code 32, which
is exactly the state you are monitoring for before a release. Counting that as a
check failure meant pointing District at an upcoming film produced a false BROKEN
alert every six minutes until the listing appeared.

Code 32 now reports closed and carries a note through to the status endpoint and
the daily heartbeat, so a typo'd content id still surfaces rather than looking
like a quiet 'not open yet' forever. Other 400s, including the missing-parameter
error, still fail loudly.
This commit is contained in:
Aculix Technologies committed 2026-09-08 14:57:31 +05:30
1 parent 9442bca696
commit 67e97f5a6a
5 files changed
+75 -6

No files matched your search

+4 -3
View File
@@ -76,15 +76,15 @@ async function tickProvider(cfg, p, state, now) {
const res = await p.module.check(cfg, p, cfg.targetDate);
input = res.kind === "open"
? { kind: "open", extract: p.module.extract(res.data, cfg, p), now, probeMax }
: { kind: "closed", now, probeMax };
: { kind: "closed", now, probeMax, note: res.note ?? null };
} catch (err) {
input = { kind: "error", reason: String(err.message ?? err), now, probeMax };
}
const { baseState, alerts } = decideProvider(pstate, input, cfg, p);
lastTick[p.key] = { at: ts(), kind: input.kind, reason: input.reason ?? null };
lastTick[p.key] = { at: ts(), kind: input.kind, reason: input.reason ?? input.note ?? null };
const matched = input.extract?.matched?.length ?? 0;
console.log(ts(), `[${p.key}] ${input.kind}${input.reason ? ` (${input.reason})` : ""}`,
console.log(ts(), `[${p.key}] ${input.kind}${input.reason || input.note ? ` (${input.reason ?? input.note})` : ""}`,
`| matched: ${matched} | failures: ${baseState.failures}`);
return { pstate: baseState, alerts, input };
}
@@ -123,6 +123,7 @@ export async function tick(cfg) {
label: p.label,
kind: input.kind === "error" ? "checks failing" : merged.providers[p.key].lastGood?.kind,
showDatesMax: merged.providers[p.key].lastGood?.showDatesMax,
note: merged.providers[p.key].lastGood?.note,
});
}
}
+5 -3
View File
@@ -73,8 +73,9 @@ export function decideProvider(pstate, input, cfg, p) {
const ex = input.kind === "open" ? input.extract : null;
const effectiveKind = ex && ex.sessionCount > 0 ? "open" : "closed"; // open-but-empty = closed
if (base.lastGood?.kind !== effectiveKind) {
base.lastGood = { ...(base.lastGood ?? {}), kind: effectiveKind, at: now.toISOString() };
const note = input.note ?? null;
if (base.lastGood?.kind !== effectiveKind || base.lastGood?.note !== note) {
base.lastGood = { ...(base.lastGood ?? {}), kind: effectiveKind, note, at: now.toISOString() };
}
if (effectiveKind === "open") {
@@ -131,7 +132,8 @@ export function decideGlobal(state, cfg, now, summaries) {
if (heartbeatDue(state, now, cfg)) {
const lines = summaries.map((s) =>
`${s.label}: ${s.kind ?? "unknown"}${s.showDatesMax ? ` (open through ${s.showDatesMax})` : ""}`);
`${s.label}: ${s.kind ?? "unknown"}${s.note ? `, ${s.note}` : ""}` +
`${s.showDatesMax ? ` (open through ${s.showDatesMax})` : ""}`);
alerts.push({
title: `Still watching ${cfg.targetDate}`,
priority: "1",
+16
View File
@@ -56,6 +56,22 @@ export async function check(cfg, p, date) {
throw new CheckError(`unparseable 200 body: ${err.message}`);
}
}
// A film announced but not yet given showtimes answers 400 "Content not found"
// (code 32). That is a waiting state, not a fault, so it must not raise the failure
// streak. A typo'd content id looks identical, so the reason is carried through to
// the status endpoint and the daily heartbeat rather than being swallowed.
if (res.status === 400) {
let body = null;
try {
body = await res.json();
} catch {
// fall through to the generic error below
}
if (body?.code === 32) {
return { kind: "closed", note: "not listed yet (District: content not found)" };
}
if (body?.msg) throw new CheckError(`HTTP 400: ${body.msg}`);
}
throw new CheckError(`HTTP ${res.status}`);
}