// BidaAngMamamayan feed read path — the public edition. // // No profiles, no learning, no LLM. The ranking is deliberate and // deterministic: stories that NAME Sen. Bam lead; within each tier, freshness // decides. This is a tribute site, not a taste engine — the "taste" is baked // in by the crawler's relevance gate. import { getCollections } from "./db.js"; import { normalizeCategory, sectionName, CATEGORY_LABELS, CATEGORIES } from "./categories.js"; export { sectionName }; const POOL_HOURS = 24 * 14; // the site's memory window for the edition // Simple, stable score: named-BAM beats context, fresh beats old, image // presence nudges a card up (a photoless tribute edition looks broken). function rank(s, nowMs) { let score = 0; if (s.named) score += 1000; if (s.image) score += 50; const ageH = (nowMs - new Date(s.published || s.discoveredAt || 0).getTime()) / 3600e3; if (Number.isFinite(ageH)) score += Math.max(0, 100 - ageH); // 0..100, decaying return score; } function shape(s) { return { url: s.url, title: s.title, summary: s.summary, image: s.image || null, source: s.source, category: normalizeCategory(s.category), desk: s.desk || null, published: s.published, discoveredAt: s.discoveredAt, named: !!s.named, fallback: !!s.fallback, score: 0, feedKind: s.feedKind || "native", }; } export async function getFeed({ limit = 60 } = {}) { const { stories } = await getCollections(); const now = new Date(); const rows = await stories .find({ discoveredAt: { $gte: new Date(now.getTime() - POOL_HOURS * 3600e3) }, desk: { $ne: "system" }, }) .sort({ discoveredAt: -1 }) .limit(1000) .toArray(); const nowMs = now.getTime(); const items = rows .map((s) => { const it = shape(s); it.score = rank(s, nowMs); return it; }) .sort((a, b) => b.score - a.score || new Date(b.published || b.discoveredAt || 0) - new Date(a.published || a.discoveredAt || 0)) .slice(0, limit); return { items, generatedAt: now, mode: "public", ranked: true, // masthead counts named: rows.filter((s) => s.named).length, total: rows.length, }; } // One (or a few) headline(s) per desk, straight from the pool — the front // page's desk grid is built from this, exactly as forthena's was. Zero LLM. export async function deskHeadlines() { try { const { stories } = await getCollections(); const nowMs = Date.now(); const rows = await stories .find({ discoveredAt: { $gte: new Date(nowMs - POOL_HOURS * 3600e3) }, desk: { $ne: "system" }, }) .limit(2000) .toArray(); const per = new Map(); const count = new Map(); for (const s of rows) { const cat = normalizeCategory(s.category); count.set(cat, (count.get(cat) || 0) + 1); const it = shape(s); it.score = rank(s, nowMs); const arr = per.get(cat) || []; arr.push(it); per.set(cat, arr); } return CATEGORIES .map((c) => { const arr = (per.get(c.key) || []).sort((a, b) => b.score - a.score); if (!arr.length) return null; return { key: c.key, label: sectionName(c.key), blurb: c.blurb || "", count: count.get(c.key) || arr.length, stories: arr.slice(0, 12) }; }) .filter(Boolean); } catch (e) { console.warn("[bama] desk headlines unavailable:", String((e && e.message) || e).slice(0, 140)); return []; } } // Masthead line: how much of the wire the desk has scoured lately. export async function editionStats() { try { const { stories } = await getCollections(); const [total, last24] = await Promise.all([ stories.countDocuments({ discoveredAt: { $gte: new Date(Date.now() - 14 * 86400e3) } }), stories.countDocuments({ discoveredAt: { $gte: new Date(Date.now() - 24 * 3600e3) } }), ]); const board = await (await getCollections()).board.countDocuments({}); return { stories: total, today: last24, boardPosts: board }; } catch { return { stories: 0, today: 0, boardPosts: 0 }; } }