Forked from Forthena (PERSONAL/forthena) and retargeted: - NO login, NO LLM: the edition is public and the ranking is deterministic (stories that name Sen. BAM lead; freshest first) — Daniel: no like/dislike or store profile for visitors. - Crawler scoped to BAM: Bing news keyword queries + Inquirer/Rappler fallback wires, with a two-tier relevance gate (named-BAM always; Aquino+legislative-context only from fallback) so op-eds and non-Bam senators are dropped. - Desks: BAM (lead), Gaming, Education, Politics, Energy, PH, Business. - Community board /board: 'Thank you Senator BAM' — nickname-only posts, HTML-stripped, URL-only rejected, IP rate-limited (3/10min, 20/day). - Yellow-gamer theme (Orbitron/Rajdhani/JetBrains Mono, dark + gold, HUD corners), supporter disclaimer in header and footer. - Tests: category/date/relevance/text suites + Mongo board suite + full HTTP e2e (all passing). Infra: 66.181.46.132 (bidaangmamamayan.com), local Mongo 7.0, nginx + certbot TLS, PM2 bama (:3000) + 15-min crawl cron.
42 lines
1.8 KiB
JavaScript
42 lines
1.8 KiB
JavaScript
// 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);
|
|
});
|