RED-107: Subreddit monitoring with RSS + AI qualification#183
Merged
Conversation
- Add MonitoredSubreddit + MonitoredPost tables (Prisma schema) - RSS feed parser for subreddit new posts (no API needed) - AI post qualifier using gpt-4.1-mini (scores relevance 0-100) - Cron endpoint POST /api/monitor/run (called by Cloudflare Worker every 15min) - API: GET/POST /api/monitor/subreddits (manage monitored subreddits) - API: GET /api/monitor/posts (list qualified posts) - API: POST /api/monitor/posts/[id]/draft-reply (AI comment generation) - Dashboard page at /monitor with post list + draft reply UI - Added Monitor to sidebar navigation - Cloudflare Worker updated with cron trigger (not yet deployed) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
Implements subreddit monitoring via Reddit RSS feeds, storing discovered posts and using OpenAI to qualify relevance and generate draft reply comments, surfaced through a new “Monitor” dashboard section.
Changes:
- Adds Prisma models for monitored subreddits and monitored posts, including relevance scoring and draft reply persistence.
- Introduces RSS fetch/parsing, cron-driven ingestion + AI qualification, and workspace-scoped APIs to list/add subreddits and list/draft posts.
- Adds a new Monitor UI page and navigation entry.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 15 comments.
Show a summary per file
| File | Description |
|---|---|
| prisma/schema.prisma | Adds MonitoredSubreddit + MonitoredPost tables and relations/indexes for monitoring storage. |
| src/lib/subreddit/rssFeed.ts | Fetches and parses subreddit RSS (Atom) into normalized post objects. |
| src/lib/monitor/qualifyPost.ts | Calls OpenAI to score post relevance (0–100) with a short rationale. |
| src/app/api/monitor/run/route.ts | Cron endpoint: fetch RSS, dedupe/store posts, and qualify unscored posts with AI. |
| src/app/api/monitor/subreddits/route.ts | Workspace-scoped list/add (reactivate) monitored subreddits. |
| src/app/api/monitor/posts/route.ts | Workspace-scoped list of qualified monitored posts (minScore filter). |
| src/app/api/monitor/posts/[id]/draft-reply/route.ts | Generates and persists an AI draft reply for a monitored post. |
| src/app/(app)/monitor/page.tsx | Dashboard UI for viewing qualified posts, adding subreddits, and drafting replies. |
| src/components/app/navConfig.ts | Adds “Monitor” entry to the app sidebar nav. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
Comment on lines
+13
to
+20
| export async function GET(req: Request) { | ||
| const ctx = await requireWorkspaceSession(); | ||
| const { searchParams } = new URL(req.url); | ||
| const minScore = Math.max(0, Number(searchParams.get("minScore") ?? 40)); | ||
|
|
||
| const posts = await prisma.monitoredPost.findMany({ | ||
| where: { | ||
| monitoredSubreddit: { workspaceId: ctx.workspaceId }, |
Comment on lines
+92
to
+101
| // Qualify unscored posts with AI (batch limit to control cost) | ||
| const unscored = await prisma.monitoredPost.findMany({ | ||
| where: { relevanceScore: null }, | ||
| take: MAX_QUALIFY_PER_RUN, | ||
| include: { | ||
| monitoredSubreddit: { | ||
| include: { | ||
| project: { select: { name: true, description: true } }, | ||
| }, | ||
| }, |
Comment on lines
+70
to
+76
| // Need a project — use the first one for now | ||
| // In a real UI we'd let user pick | ||
| const res = await fetch("/api/monitor/subreddits", { | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body: JSON.stringify({ subreddit: newSub.trim() }), | ||
| }); |
Comment on lines
+15
to
+22
| const { searchParams } = new URL(req.url); | ||
| const minScore = Math.max(0, Number(searchParams.get("minScore") ?? 40)); | ||
|
|
||
| const posts = await prisma.monitoredPost.findMany({ | ||
| where: { | ||
| monitoredSubreddit: { workspaceId: ctx.workspaceId }, | ||
| relevanceScore: { gte: minScore }, | ||
| }, |
Comment on lines
+29
to
+37
| // Get all active monitored subreddits with their project info | ||
| const monitored = await prisma.monitoredSubreddit.findMany({ | ||
| where: { isActive: true }, | ||
| take: MAX_SUBREDDITS_PER_RUN, | ||
| include: { | ||
| project: { | ||
| select: { name: true, description: true }, | ||
| }, | ||
| }, |
Comment on lines
+21
to
+31
| export async function POST(req: Request) { | ||
| // Auth: only allow calls with the correct secret | ||
| const authHeader = req.headers.get("authorization") ?? ""; | ||
| const secret = process.env.MONITOR_CRON_SECRET; | ||
| if (!secret || authHeader !== `Bearer ${secret}`) { | ||
| return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); | ||
| } | ||
|
|
||
| // Get all active monitored subreddits with their project info | ||
| const monitored = await prisma.monitoredSubreddit.findMany({ | ||
| where: { isActive: true }, |
Comment on lines
+22
to
+31
| export async function GET() { | ||
| const ctx = await requireWorkspaceSession(); | ||
|
|
||
| const subs = await prisma.monitoredSubreddit.findMany({ | ||
| where: { workspaceId: ctx.workspaceId }, | ||
| include: { | ||
| project: { select: { name: true } }, | ||
| _count: { select: { posts: true } }, | ||
| }, | ||
| orderBy: { createdAt: "desc" }, |
Comment on lines
+47
to
+67
| const raw = await generateChatText({ | ||
| systemPrompt: SYSTEM_PROMPT, | ||
| userPrompt, | ||
| feature: "risk-scoring", // uses gpt-4.1-mini | ||
| maxTokens: 150, | ||
| }); | ||
|
|
||
| if (!raw) return { score: 0, reason: "AI unavailable" }; | ||
|
|
||
| try { | ||
| const parsed = JSON.parse(raw) as { score?: number; reason?: string }; | ||
| return { | ||
| score: | ||
| typeof parsed.score === "number" && Number.isFinite(parsed.score) | ||
| ? Math.max(0, Math.min(100, Math.round(parsed.score))) | ||
| : 0, | ||
| reason: | ||
| typeof parsed.reason === "string" ? parsed.reason.slice(0, 200) : "", | ||
| }; | ||
| } catch { | ||
| return { score: 0, reason: "Failed to parse AI response" }; |
Comment on lines
+25
to
+44
| export async function POST( | ||
| req: Request, | ||
| { params }: { params: { id: string } }, | ||
| ) { | ||
| const ctx = await requireWorkspaceSession(); | ||
| const postId = params.id; | ||
|
|
||
| // Get the post + its project context | ||
| const post = await prisma.monitoredPost.findUnique({ | ||
| where: { id: postId }, | ||
| include: { | ||
| monitoredSubreddit: { | ||
| include: { | ||
| project: { | ||
| select: { name: true, description: true, workspaceId: true }, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| }); |
| }, | ||
| }, | ||
| }, | ||
| orderBy: { discoveredAt: "desc" }, // newest first |
- Remove Approvals and Scheduling from sidebar nav - Onboarding: 3 steps → 2 steps (removed Connect Reddit) - Create project now redirects to roadmap (skips connect-reddit) - Roadmap no longer requires Reddit account to proceed - All OAuth/scheduling code kept but unreachable (not deleted) - Updated onboarding guidance hints to reference Monitor instead of scheduling Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix params pattern on draft-reply route (Promise<{id}> with await)
- Frontend: add project selector, send projectId when adding subreddit
- Frontend: add loading state on Add button to prevent double-click
- Cron: parallel RSS fetches in batches of 5 (was sequential, risked Vercel timeout)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
New feature: monitor subreddits for relevant posts and draft AI reply comments.
How it works:
No Reddit API needed — uses RSS feeds which are free and unblocked.
Changes
prisma/schema.prisma: 2 new tables (MonitoredSubreddit, MonitoredPost)src/lib/subreddit/rssFeed.ts: RSS feed parsersrc/lib/monitor/qualifyPost.ts: AI relevance scorersrc/app/api/monitor/run/route.ts: Cron endpointsrc/app/api/monitor/subreddits/route.ts: CRUD for monitored subssrc/app/api/monitor/posts/route.ts: List qualified postssrc/app/api/monitor/posts/[id]/draft-reply/route.ts: AI comment draftersrc/app/(app)/monitor/page.tsx: Dashboard UIsrc/components/app/navConfig.ts: Added Monitor to sidebarDeploy steps
prisma db pushon productionMONITOR_CRON_SECRETenv var to both Vercel and CloudflareTest Plan
🤖 Generated with Claude Code