[{"data":1,"prerenderedAt":489},["ShallowReactive",2],{"project-moonlight":3},{"id":4,"title":5,"body":6,"date":476,"description":477,"extension":478,"meta":479,"navigation":484,"path":485,"seo":486,"stem":487,"__hash__":488},"projects\u002Fprojects\u002Fmoonlight.md","Moonlight",{"type":7,"value":8,"toc":460},"minimark",[9,14,23,29,37,81,84,89,92,100,102,106,109,119,121,125,212,214,218,228,232,234,238,311,313,317,320,326,331,334,338,353,357,371,373,377,383,385,389,450],[10,11,13],"h1",{"id":12},"moonlight-news-told-well","Moonlight — news, told well",[15,16,17,18,22],"p",{},"Moonlight is personal news intelligence for the Nigerian information ecosystem and global tech. It reads news sites, YouTube channels, and hard-to-reach pages on your behalf; classifies and embeds everything; filters it through your natural-language preferences; publishes a curated brief at ",[19,20,21],"strong",{},"19:00 WAT"," every day; and answers questions about any story through a chat agent that cites its sources.",[15,24,25,28],{},[19,26,27],{},"The goal:"," stop drowning in feeds. Let an agent do the reading, then hand you the eight stories that actually matter — and let you interrogate any of them in plain language.",[30,31,36],"a",{"href":32,"rel":33,"target":35},"https:\u002F\u002Fmoonlight.iamibrahim.xyz",[34],"nofollow","_blank","Live App",[15,38,39,42,43,47,48,47,51,47,54,47,57,47,60,47,63,47,66,47,69,47,72,47,75,47,78],{},[19,40,41],{},"Tech Stack:"," ",[44,45,46],"code",{},"FastAPI"," · ",[44,49,50],{},"Python",[44,52,53],{},"Postgres 17",[44,55,56],{},"pgvector",[44,58,59],{},"SQLAlchemy 2.0",[44,61,62],{},"Alembic",[44,64,65],{},"Gemini",[44,67,68],{},"Crawl4AI",[44,70,71],{},"Nuxt 4",[44,73,74],{},"Nuxt UI v4",[44,76,77],{},"TanStack vue-query",[44,79,80],{},"Coolify",[82,83],"hr",{},[85,86,88],"h2",{"id":87},"why-this-exists","Why This Exists",[15,90,91],{},"The Nigerian information ecosystem is fast, noisy, and spread across a dozen outlets, YouTube channels, and pages that don't even publish clean RSS. Staying informed means manually checking Guardian, Vanguard, Premium Times, Punch, Channels, plus a handful of tech feeds — every single day. Most of it is noise, the same wire story rewritten eight times.",[15,93,94,95,99],{},"Moonlight inverts that. Instead of you reading the feeds, an agent reads them, dedupes the repeats, clusters the rewrites into single stories, ranks them against what ",[96,97,98],"em",{},"you"," said you care about, and hands you a brief once a day. Everything it surfaces is traceable back to the source.",[82,101],{},[85,103,105],{"id":104},"how-it-works","How It Works",[15,107,108],{},"The whole system is a pipeline: pull raw items in, enrich them in stages, then serve the result two ways — a daily brief and a chat agent.",[110,111,116],"pre",{"className":112,"code":114,"language":115},[113],"language-text","RSS feeds ─┐\nYouTube ───┼─→ ingestion (30 min) ─→ raw_items ─→ pipeline (hourly)\nCrawl4AI ──┘        dedupe: sha256(url)             ├─ embed   (pgvector, Gemini)\n                                                    ├─ classify (topics\u002Fentities\u002F\n                                                    │            substance, LLM)\n                                                    ├─ chunk    (RAG retrieval)\n                                                    └─ cluster  → stories\n                                                                    │\n        ┌────────────────────────┬──────────────────────────┬──────┘\n   preference engine        7pm digest                 RAG chat agent\n   (3-gate filter:          (ranked stories,           (tool loop: search,\n   source→rules→semantic)   cached, idempotent)        timeline, comments,\n        │                        │                     transcripts; cites\n        └────────────┬───────────┘                     [item:N])\n                     ▼\n            FastAPI ─→ Nuxt web app\n","text",[44,117,114],{"__ignoreMap":118},"",[82,120],{},[85,122,124],{"id":123},"whats-built","What's Built",[126,127,128,141],"table",{},[129,130,131],"thead",{},[132,133,134,138],"tr",{},[135,136,137],"th",{},"Feature",[135,139,140],{},"Description",[142,143,144,158,168,178,188,198],"tbody",{},[132,145,146,152],{},[147,148,149],"td",{},[19,150,151],{},"Multi-source ingestion",[147,153,154,155],{},"RSS, YouTube (uploads + comments), and Crawl4AI for sites without clean feeds — polled every 30 minutes, deduped by ",[44,156,157],{},"sha256(url)",[132,159,160,165],{},[147,161,162],{},[19,163,164],{},"Embedding + classification",[147,166,167],{},"Every item is embedded with Gemini into pgvector, then classified for topics, entities, and substance by an LLM",[132,169,170,175],{},[147,171,172],{},[19,173,174],{},"Story clustering",[147,176,177],{},"Near-duplicate coverage of the same event is clustered into a single story, so you read it once, not eight times",[132,179,180,185],{},[147,181,182],{},[19,183,184],{},"Preference engine",[147,186,187],{},"A 3-gate filter — source → rules → semantic — ranks items against your natural-language preferences",[132,189,190,195],{},[147,191,192],{},[19,193,194],{},"7pm daily brief",[147,196,197],{},"A ranked, cached, idempotent digest of the day's strongest stories, published at 19:00 WAT",[132,199,200,205],{},[147,201,202],{},[19,203,204],{},"RAG chat agent",[147,206,207,208,211],{},"Ask about any story; a tool-loop agent searches, builds timelines, reads comments and transcripts, and cites ",[44,209,210],{},"[item:N]"," for every claim",[82,213],{},[85,215,217],{"id":216},"demo","Demo",[219,220,222,223],"div",{"style":221},"display:flex;justify-content:center;margin-bottom:2rem","\n  ",[219,224],{"role":225,"ariaLabel":226,"style":227},"img","Moonlight walkthrough — the 7pm brief, sources dashboard, and citation-backed chat agent in action","width:100%;max-width:960px;aspect-ratio:1920\u002F936;background-image:url('\u002Fprojects\u002Fmoonlight\u002Fmoonlight_demo.gif');background-size:cover;background-position:top center;border-radius:12px;border:1px solid rgba(255,255,255,0.1)",[229,230],"demo-gallery",{":items":231},"[{\"type\":\"image\",\"src\":\"\u002Fprojects\u002Fmoonlight\u002Fdaily-brief.png\",\"alt\":\"The 7pm brief — clustered stories ranked by relevance, each traceable to its sources\"},{\"type\":\"image\",\"src\":\"\u002Fprojects\u002Fmoonlight\u002Fsources.png\",\"alt\":\"Sources dashboard — RSS, crawl, and YouTube pollers with live health and item counts\"},{\"type\":\"image\",\"src\":\"\u002Fprojects\u002Fmoonlight\u002Fbrief-empty.png\",\"alt\":\"Brief empty state — moonlight is still reading today's coverage, with a generate-now escape hatch\"}]",[82,233],{},[85,235,237],{"id":236},"tech-stack","Tech Stack",[126,239,240,250],{},[129,241,242],{},[132,243,244,247],{},[135,245,246],{},"Layer",[135,248,249],{},"Choice",[142,251,252,260,268,287,295,303],{},[132,253,254,257],{},[147,255,256],{},"API",[147,258,259],{},"FastAPI (async), SSE streaming chat",[132,261,262,265],{},[147,263,264],{},"DB",[147,266,267],{},"Postgres 17 + pgvector, SQLAlchemy 2.0 async, Alembic migrations",[132,269,270,273],{},[147,271,272],{},"Pipeline LLMs",[147,274,275,276,279,280,283,284],{},"Gemini — embeddings ",[44,277,278],{},"gemini-embedding-001",", classify ",[44,281,282],{},"flash-lite",", chat ",[44,285,286],{},"flash",[132,288,289,292],{},[147,290,291],{},"Scraping",[147,293,294],{},"Crawl4AI + headless Chromium (robots.txt-respecting)",[132,296,297,300],{},[147,298,299],{},"Web",[147,301,302],{},"Nuxt 4, Nuxt UI v4, Tailwind 4, TanStack vue-query, bun",[132,304,305,308],{},[147,306,307],{},"Deploy",[147,309,310],{},"Coolify on AWS EC2 — one multi-stage image, three roles (api \u002F ingestion \u002F digest)",[82,312],{},[85,314,316],{"id":315},"architecture","Architecture",[15,318,319],{},"Moonlight is one codebase that deploys as a single Docker image with three runtime roles. The Postgres ORM models are the source of truth; the ingestion and pipeline workers are stateless and idempotent, so a missed run just catches up on the next tick.",[110,321,324],{"className":322,"code":323,"language":115},[113],"server\u002F      FastAPI app: routes, ORM models (source of truth), agent, migrations\ningestion\u002F   source pollers: RSS, YouTube (uploads + comments), Crawl4AI\npipeline\u002F    embed · classify · chunk · cluster · digest workers\nweb\u002F         Nuxt app (per-page component folders, vue-query data layer)\ndeploy\u002F      Dockerfiles + production runbook\ntests\u002F       pytest — pure-logic units for contracts, gates, chunking\n",[44,325,323],{"__ignoreMap":118},[327,328,330],"h3",{"id":329},"why-pgvector-instead-of-a-dedicated-vector-db","Why pgvector instead of a dedicated vector DB?",[15,332,333],{},"The corpus is personal-scale — thousands of items, not billions. Postgres with pgvector keeps embeddings, relational metadata, and the classification graph in one database, one transaction boundary, one backup. A story's chunks, its source items, and its vector all live together, so RAG retrieval is a single SQL join, not a cross-service fan-out. No second datastore to operate.",[327,335,337],{"id":336},"why-a-3-gate-preference-filter","Why a 3-gate preference filter?",[15,339,340,341,344,345,348,349,352],{},"Ranking purely by semantic similarity drowns you in plausible-but-irrelevant matches. Moonlight gates in order of cost: a cheap ",[19,342,343],{},"source"," allowlist first, then deterministic ",[19,346,347],{},"rules",", and only then the expensive ",[19,350,351],{},"semantic"," comparison against your stated preferences. Most noise is rejected before it ever touches an embedding comparison.",[327,354,356],{"id":355},"why-one-image-three-roles","Why one image, three roles?",[15,358,359,360,363,364,363,367,370],{},"The API, the ingestion pollers, and the digest generator share the same models and config. Building three separate services would mean three deploys and drifting dependencies. Instead, a multi-stage Dockerfile produces one image; the role is chosen at container start (",[44,361,362],{},"api",", ",[44,365,366],{},"ingestion",[44,368,369],{},"digest","). One thing to build, one thing to version.",[82,372],{},[85,374,376],{"id":375},"the-chat-agent","The Chat Agent",[15,378,379,380,382],{},"The brief tells you what happened. The chat agent lets you dig in. It's a tool-loop agent over the same pgvector store — it can search the corpus, assemble a timeline of how a story developed, pull YouTube comments and transcripts, and synthesise an answer. Every claim it makes carries an inline ",[44,381,210],{}," citation that links straight back to the source item, so nothing it says is unverifiable.",[82,384],{},[85,386,388],{"id":387},"status","Status",[126,390,391,399],{},[129,392,393],{},[132,394,395,397],{},[135,396,246],{},[135,398,388],{},[142,400,401,409,416,422,429,435,442],{},[132,402,403,406],{},[147,404,405],{},"Ingestion (RSS · YouTube · Crawl4AI)",[147,407,408],{},"✅ Shipped",[132,410,411,414],{},[147,412,413],{},"Embedding + classification pipeline",[147,415,408],{},[132,417,418,420],{},[147,419,174],{},[147,421,408],{},[132,423,424,427],{},[147,425,426],{},"3-gate preference engine",[147,428,408],{},[132,430,431,433],{},[147,432,194],{},[147,434,408],{},[132,436,437,440],{},[147,438,439],{},"RAG chat agent with citations",[147,441,408],{},[132,443,444,447],{},[147,445,446],{},"Production (Coolify on EC2)",[147,448,449],{},"✅ Live",[15,451,452],{},[96,453,454,455,459],{},"Live at ",[30,456,458],{"href":32,"rel":457},[34],"moonlight.iamibrahim.xyz"," — reading the news at 19:00 WAT, daily.",{"title":118,"searchDepth":461,"depth":461,"links":462},2,[463,464,465,466,467,468,474,475],{"id":87,"depth":461,"text":88},{"id":104,"depth":461,"text":105},{"id":123,"depth":461,"text":124},{"id":216,"depth":461,"text":217},{"id":236,"depth":461,"text":237},{"id":315,"depth":461,"text":316,"children":469},[470,472,473],{"id":329,"depth":471,"text":330},3,{"id":336,"depth":471,"text":337},{"id":355,"depth":471,"text":356},{"id":375,"depth":461,"text":376},{"id":387,"depth":461,"text":388},"2026-06-11","Personal news intelligence for the Nigerian information ecosystem and global tech — reads news sites, YouTube, and hard-to-reach pages on your behalf, then publishes a curated 7pm brief and answers questions through a citation-backed chat agent.","md",{"tags":480,"live":32,"status":483,"featured":484},[481,482,46,56,71,65],"AI","RAG","Shipped",true,"\u002Fprojects\u002Fmoonlight",{"title":5,"description":477},"projects\u002Fmoonlight","fzhzkvq6Aoj_Q1_FLnuIZXutR1t70SvjHIa88TkDrlU",1781344604236]