Skip to content

Commit a873a90

Browse files
authored
feat: Phase B — migrate cron routes to Sanity config (#613)
Migrates cron routes to Sanity config singletons. ingest/route.ts: enableNotebookLmResearch, qualityThreshold, systemInstruction from pipeline_config/content_config. check-research/route.ts: stuckTimeoutMinutes (with proportional sub-thresholds), qualityThreshold, systemInstruction. All use getConfigValue() with hardcoded fallbacks for backward compatibility. Also fixes pre-existing REDACTED SECRET corruption in FALLBACK_TRENDS. Secrets/API keys stay as process.env.
1 parent 2e92162 commit a873a90

File tree

2 files changed

+50
-15
lines changed

2 files changed

+50
-15
lines changed

app/api/cron/check-research/route.ts

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { NotebookLMClient } from '@/lib/services/notebooklm/client';
88
import { initAuth } from '@/lib/services/notebooklm/auth';
99
import { ArtifactTypeCode, ArtifactStatus } from '@/lib/services/notebooklm/types';
1010
import { generateWithGemini, stripCodeFences } from '@/lib/gemini';
11+
import { getConfigValue } from '@/lib/config';
1112
import type { ResearchPayload } from '@/lib/services/research';
1213

1314
// ---------------------------------------------------------------------------
@@ -94,12 +95,15 @@ interface StepResult {
9495
// Constants
9596
// ---------------------------------------------------------------------------
9697

97-
/** Stuck thresholds per status (ms) */
98-
const STUCK_THRESHOLDS: Record<string, number> = {
99-
researching: 30 * 60 * 1000, // 30 minutes
100-
infographics_generating: 15 * 60 * 1000, // 15 minutes
101-
enriching: 10 * 60 * 1000, // 10 minutes
102-
};
98+
/** Build stuck thresholds from config (with fallbacks) */
99+
async function buildStuckThresholds(): Promise<Record<string, number>> {
100+
const stuckMinutes = await getConfigValue('pipeline_config', 'stuckTimeoutMinutes', 30);
101+
return {
102+
researching: stuckMinutes * 60 * 1000,
103+
infographics_generating: Math.round(stuckMinutes * 0.5) * 60 * 1000, // half the main timeout
104+
enriching: Math.round(stuckMinutes * 0.33) * 60 * 1000, // third of main timeout
105+
};
106+
}
103107

104108
/** Max docs to process per status per run — keeps total time well under 60s */
105109
const MAX_DOCS_PER_STATUS = 2;
@@ -132,12 +136,13 @@ function getSanityWriteClient(): SanityClient {
132136
async function flagStuckDocs(
133137
docs: PipelineDoc[],
134138
sanity: SanityClient,
139+
stuckThresholds: Record<string, number>,
135140
): Promise<StepResult[]> {
136141
const results: StepResult[] = [];
137142
const now = Date.now();
138143

139144
for (const doc of docs) {
140-
const threshold = STUCK_THRESHOLDS[doc.status];
145+
const threshold = stuckThresholds[doc.status];
141146
if (!threshold) continue;
142147

143148
const docAge = now - new Date(doc._updatedAt).getTime();
@@ -419,6 +424,11 @@ async function stepEnriching(
419424
// Generate enriched script with Gemini
420425
let enrichedScript: EnrichedScript | null = null;
421426
try {
427+
const SYSTEM_INSTRUCTION = await getConfigValue(
428+
'content_config',
429+
'systemInstruction',
430+
SYSTEM_INSTRUCTION_FALLBACK,
431+
);
422432
const prompt = buildEnrichmentPrompt(doc, researchPayload);
423433
const rawResponse = await generateWithGemini(prompt, SYSTEM_INSTRUCTION);
424434
const cleaned = stripCodeFences(rawResponse);
@@ -434,7 +444,8 @@ async function stepEnriching(
434444
const criticScore = criticResult.score;
435445
console.log(`[check-research] Critic score: ${criticScore}/100 — ${criticResult.summary}`);
436446

437-
const isFlagged = criticScore < 50;
447+
const qualityThreshold = await getConfigValue('pipeline_config', 'qualityThreshold', 50);
448+
const isFlagged = criticScore < qualityThreshold;
438449

439450
await sanity
440451
.patch(doc._id)
@@ -481,7 +492,9 @@ async function stepEnriching(
481492
// Gemini Script Enrichment
482493
// ---------------------------------------------------------------------------
483494

484-
const SYSTEM_INSTRUCTION = `You are a content strategist and scriptwriter for CodingCat.dev, a web development education channel run by Alex Patterson.
495+
// SYSTEM_INSTRUCTION fallback — used when content_config singleton doesn't exist yet in Sanity.
496+
// The live value is fetched from getConfigValue() inside stepEnriching().
497+
const SYSTEM_INSTRUCTION_FALLBACK = `You are a content strategist and scriptwriter for CodingCat.dev, a web development education channel run by Alex Patterson.
485498
486499
Your style is inspired by Cleo Abram's "Huge If True" — you make complex technical topics feel exciting, accessible, and important. Key principles:
487500
- Start with a BOLD claim or surprising fact that makes people stop scrolling
@@ -813,7 +826,8 @@ export async function GET(request: NextRequest) {
813826
const results: StepResult[] = [];
814827

815828
// Phase 1: Stuck detection — runs FIRST, no external API calls
816-
const stuckResults = await flagStuckDocs(docs, sanity);
829+
const stuckThresholds = await buildStuckThresholds();
830+
const stuckResults = await flagStuckDocs(docs, sanity, stuckThresholds);
817831
results.push(...stuckResults);
818832

819833
// Remove flagged docs from further processing

app/api/cron/ingest/route.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { NextRequest } from "next/server";
44

55
import { generateWithGemini, stripCodeFences } from "@/lib/gemini";
66
import { writeClient } from "@/lib/sanity-write-client";
7+
import { getConfigValue } from "@/lib/config";
78
import { discoverTrends, type TrendResult } from "@/lib/services/trend-discovery";
89
import type { ResearchPayload } from "@/lib/services/research";
910
import { NotebookLMClient } from "@/lib/services/notebooklm/client";
@@ -102,7 +103,7 @@ const FALLBACK_TRENDS: TrendResult[] = [
102103
slug: "webassembly-web-apps",
103104
score: 60,
104105
signals: [{ source: "blog", title: "WebAssembly", url: "https://webassembly.org/", score: 60 }],
105-
whyTrending: "WASM adoption growing in [REDACTED SECRET: NEXT_PUBLIC_SANITY_DATASET] apps",
106+
whyTrending: "WASM adoption growing in production apps",
106107
suggestedAngle: "Real-world use cases where WASM outperforms JS",
107108
},
108109
];
@@ -111,7 +112,9 @@ const FALLBACK_TRENDS: TrendResult[] = [
111112
// Gemini Script Generation
112113
// ---------------------------------------------------------------------------
113114

114-
const SYSTEM_INSTRUCTION = `You are a content strategist and scriptwriter for CodingCat.dev, a web development education channel run by Alex Patterson.
115+
// SYSTEM_INSTRUCTION fallback — used when content_config singleton doesn't exist yet in Sanity.
116+
// The live value is fetched from getConfigValue() inside the GET handler.
117+
const SYSTEM_INSTRUCTION_FALLBACK = `You are a content strategist and scriptwriter for CodingCat.dev, a web development education channel run by Alex Patterson.
115118
116119
Your style is inspired by Cleo Abram's "Huge If True" — you make complex technical topics feel exciting, accessible, and important. Key principles:
117120
- Start with a BOLD claim or surprising fact that makes people stop scrolling
@@ -330,10 +333,11 @@ async function createSanityDocuments(
330333
script: GeneratedScript,
331334
criticResult: CriticResult,
332335
trends: TrendResult[],
336+
qualityThreshold: number,
333337
research?: ResearchPayload,
334338
researchMeta?: { notebookId: string; taskId: string },
335339
) {
336-
const isFlagged = criticResult.score < 50;
340+
const isFlagged = criticResult.score < qualityThreshold;
337341
// When research is in-flight, status is "researching" (check-research cron will transition to script_ready)
338342
const isResearching = !!researchMeta?.notebookId;
339343
const status = isFlagged ? "flagged" : isResearching ? "researching" : "script_ready";
@@ -404,6 +408,23 @@ export async function GET(request: NextRequest) {
404408
}
405409

406410
try {
411+
// Fetch config values once per invocation (5-min in-memory cache)
412+
const SYSTEM_INSTRUCTION = await getConfigValue(
413+
"content_config",
414+
"systemInstruction",
415+
SYSTEM_INSTRUCTION_FALLBACK,
416+
);
417+
const enableNotebookLmResearch = await getConfigValue(
418+
"pipeline_config",
419+
"enableNotebookLmResearch",
420+
false,
421+
);
422+
const qualityThreshold = await getConfigValue(
423+
"pipeline_config",
424+
"qualityThreshold",
425+
50,
426+
);
427+
407428
// Step 1: Discover trending topics (replaces fetchTrendingTopics)
408429
console.log("[CRON/ingest] Discovering trending topics...");
409430
let trends: TrendResult[];
@@ -425,7 +446,7 @@ export async function GET(request: NextRequest) {
425446
// When research is enabled, we create a notebook and start research
426447
// but DON'T wait for it — the check-research cron will poll and enrich later
427448
let researchMeta: { notebookId: string; taskId: string } | undefined;
428-
if (process.env.ENABLE_NOTEBOOKLM_RESEARCH === "true") {
449+
if (enableNotebookLmResearch) {
429450
console.log(`[CRON/ingest] Starting fire-and-forget research on: "${trends[0].topic}"...`);
430451
try {
431452
const auth = await initAuth();
@@ -494,7 +515,7 @@ export async function GET(request: NextRequest) {
494515
);
495516

496517
console.log("[CRON/ingest] Creating Sanity documents...");
497-
const result = await createSanityDocuments(script, criticResult, trends, undefined, researchMeta);
518+
const result = await createSanityDocuments(script, criticResult, trends, qualityThreshold, undefined, researchMeta);
498519

499520
console.log("[CRON/ingest] Done!", result);
500521

0 commit comments

Comments
 (0)