// Forthena hourly digest trigger — fires the server's /api/crawl (single batched AI pass happens server-side). // Run at :05 via PM2 cron_restart. The web app itself never calls the LLM. const fs = require("fs"); const path = require("path"); // Load .env.production (KEY=VALUE lines) so the cron process has the real secret + port. const envFile = path.join(__dirname, "..", ".env.production"); try { for (const line of fs.readFileSync(envFile, "utf8").split("\n")) { const m = line.match(/^([A-Z0-9_]+)=(.*)$/); if (m && !process.env[m[1]]) process.env[m[1]] = m[2]; } } catch (e) {} const SECRET = process.env.CRON_SECRET || "forthena-cron"; const PORT = process.env.PORT || "3008"; let run = async () => { const res = await fetch(`http://127.0.0.1:${PORT}/api/crawl`, { method: "POST", headers: { "x-forthena-cron": SECRET }, signal: AbortSignal.timeout(280000), }); const j = await res.json().catch(() => ({})); // Since multi-user, one tick runs an AI pass per reader: j.ranked is an array and // j.primary is the owner's (always first). Log every pass so a per-reader failure is // visible in PM2 output instead of silently shrinking the paper. const passes = Array.isArray(j.ranked) ? j.ranked : j.ranked ? [j.ranked] : []; const detail = passes .map((p) => `${p.key}:${p.error ? "ERR" : `${p.ranked}r/${p.signals}s`}`) .join(" ") || "none"; console.log( `[forthena-digest] ${res.status} added=${j && j.crawl && j.crawl.added} migrated=${j && j.crawl && j.crawl.migrated} retired=${j && j.crawl && j.crawl.retired} passes=${passes.length} [${detail}] articles=${j && j.crawl && j.crawl.articles ? j.crawl.articles.ok + "/" + j.crawl.articles.fetched : "?"}` ); process.exit(res.ok ? 0 : 1); }; run().catch((e) => { console.error("[forthena-digest] FAILED", e && e.message); process.exit(1); });