Repository navigation
feat: honest last-checked, no history on connect, opt-in analyze-history window - #69
Conversation
🤖 CodeAnt AI — Review Status
|
✅ Deploy Preview for moderaty ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
📝 WalkthroughSummary by CodeRabbit
WalkthroughThe relative-time utility now supports month and year labels. New channel connections initialize scan boundaries at connection time. The dashboard adds tenant-scoped history analysis with selectable periods, scan-state resets, validation, authorization checks, and updated channel status data. ChangesRelative-time display
Historical comment analysis
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant DashboardForm
participant analyzeHistory
participant ChannelsDatabase
DashboardForm->>analyzeHistory: Submit selected history period
analyzeHistory->>ChannelsDatabase: Validate tenant-scoped channel and update cursor
ChannelsDatabase-->>analyzeHistory: Return updated scan state
analyzeHistory-->>DashboardForm: Return success or validation/not-found error
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 2📝 Generate docstrings 💡
🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
🏁 CodeAnt Quality Gate ResultsCommit: ✅ Overall Status: PASSEDQuality Gate Details
|
Sequence DiagramThis PR starts new channels at connection time and makes historical analysis an explicit bounded action. The scheduler then scans within the selected boundary, moderates only unseen comments, and records the actual run time used by the dashboard. sequenceDiagram
participant User
participant Backend
participant Dashboard
participant Channel database
participant Scheduler
participant Comment API
User->>Backend: Connect channel
Backend->>Channel database: Create channel with connection time boundary
User->>Dashboard: Request bounded history window
Dashboard->>Channel database: Move boundary back and reset paging state
Scheduler->>Comment API: Fetch comments within boundary
Comment API-->>Scheduler: Return comment pages
Scheduler->>Scheduler: Deduplicate and moderate unseen comments
Scheduler->>Channel database: Save scan progress and actual run time
Generated by CodeAnt AI |
There was a problem hiding this comment.
Reviewed all code changes across 9 files. The implementation correctly addresses the issues described in the PR:
- Bug fix: Dashboard now displays
lastRunAtinstead ofcursorfor "last checked", fixing the confusing display of old comment timestamps - Scan window changes: New channel connections initialize
cursorto connection time (no historical analysis), while reconnects preserve existing cursor values - Analyze history feature: New server action with preset time windows (1/3/6/12/24 months) and proper validation, correctly bounded to control API costs
- Time formatting: Added month/year units to
relativeTimeto prevent degradation to large week counts
The changes include proper tenancy scoping, validation, and error handling. All test files show comprehensive coverage of the new functionality. No blocking issues found.
You can now have the agent implement changes and create commits directly on your pull request's source branch. Simply comment with /q followed by your request in natural language to ask the agent to make changes.
Not up to standards ⛔🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | ✅ 18 (≤ 100 complexity) |
| Duplication |
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
PR Summary by QodoFix dashboard “last checked” and add opt-in bounded history scan window
AI Description
Diagram
High-Level Assessment
Files changed (9)
|
PR Code Suggestions ✨Latest suggestions up to commit
|
| Category | Suggestion | Severity | Generated at (UTC) |
| Race condition |
An in-flight scan can overwrite a newly requested history boundary and drain stateThe history-window reset is not coordinated with an in-flight cron run. A run can src/routes/(app)/dashboard/+page.server.ts [108-111] Why it matters? 🤔
(Use Cmd/Ctrl + Click for best experience) Prompt for AI Agent 🤖This is a comment left during a code review.
**Path:** src/routes/(app)/dashboard/+page.server.ts
**Line:** 108:111
**Comment:**
*Race Condition: The history-window reset is not coordinated with an in-flight cron run. A run can snapshot the channel before this update, perform moderation work, and then `persistResults` unconditionally write its stale `cursor`, `nextPageToken`, and `scanCursor` afterward, overwriting the requested boundary or restoring the old continuation token. Coordinate the reset with the channel lease or use an optimistic/versioned update so stale runs cannot commit their scan state.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix | Major | 2026-08-04 17:11
|
| Possible bug |
History-action failures are rendered in an unrelated account-deletion error areaThe history action form does not use src/routes/(app)/dashboard/+page.svelte [107-117] Why it matters? 🤔
(Use Cmd/Ctrl + Click for best experience) Prompt for AI Agent 🤖This is a comment left during a code review.
**Path:** src/routes/(app)/dashboard/+page.svelte
**Line:** 107:117
**Comment:**
*Possible Bug: The history action form does not use `use:enhance`, so a failed `analyzeHistory` request performs a full form navigation and places the returned `form.error` in the account-deletion danger zone instead of next to the history control. Invalid windows and not-found errors are therefore displayed detached from the action that caused them and can be mistaken for deletion errors. Enhance this form and render its errors in the channel card.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix | Major | 2026-08-04 17:11
|
There was a problem hiding this comment.
Warning
CodeRabbit couldn't request changes on this pull request because it doesn't have sufficient GitHub permissions.
Please grant CodeRabbit Pull requests: Read and write permission and re-run the review.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/routes/`(app)/dashboard/+page.server.ts:
- Around line 93-115: Update the channels update in analyzeHistory to require
eq(channels.userId, user.id) alongside the existing orgId and channelId
predicates, preserving the 404 response when no row matches. Add a test proving
a same-org user cannot reset another user’s channel scan state.
In `@src/routes/`(app)/dashboard/+page.svelte:
- Around line 107-120: Update the history form around the analyzeHistory action
to use an operation-scoped result, rendering its error in an .error-box, success
status, and pending skeleton beside the form. Ensure the loading, empty, error,
and populated states required by the page are represented without reusing the
Delete account feedback state.
In `@src/routes/`(app)/dashboard/actions.test.ts:
- Around line 116-126: Update the rejected-request tests around analyzeHistory
to seed non-null values for every scan-state field, including nextPageToken and
scanCursor, then assert the complete scanWindowOf('UC1') object remains
unchanged after each 400 response. Apply the same full-state assertion to the
additional test case covering lines 135-143, preserving the existing cursor
value and rejection expectations.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 1daec4a7-58b2-4c1e-83ce-1c9ae348af7a
📒 Files selected for processing (9)
src/lib/relative-time.test.tssrc/lib/relative-time.tssrc/lib/server/channelConnect.tssrc/lib/server/db/schema.tssrc/routes/(app)/dashboard/+page.server.tssrc/routes/(app)/dashboard/+page.sveltesrc/routes/(app)/dashboard/actions.test.tssrc/routes/(app)/dashboard/dashboard.test.tssrc/routes/api/auth/google/callback/callback.test.ts
| analyzeHistory: async ({ request, locals }) => { | ||
| const user = requireUser(locals); | ||
| const f = await request.formData(); | ||
| const channelId = String(f.get('channelId') ?? ''); | ||
| const months = Number(f.get('months')); | ||
| // Preset windows only — the scan drains newest-first at 300 comments per | ||
| // run, so an unbounded window is an unbounded API/AI cost (I10). | ||
| if (![1, 3, 6, 12, 24].includes(months)) { | ||
| return fail(400, { error: 'history window must be 1, 3, 6, 12, or 24 months' }); | ||
| } | ||
| // Move the scan boundary back and reset the drain state: cron's next runs | ||
| // page from the newest comment down to this boundary. Already-seen | ||
| // comments are skipped before scoring (decideNewComments dedupes by id), | ||
| // so only the unscanned history costs moderation calls. Tenancy-scoped: | ||
| // another team's channel reads as "not found". | ||
| const boundary = new Date(Date.now() - months * 30 * 24 * 60 * 60 * 1000).toISOString(); | ||
| const updated = await db | ||
| .update(channels) | ||
| .set({ cursor: boundary, nextPageToken: null, scanCursor: null }) | ||
| .where(and(eq(channels.id, channelId), eq(channels.orgId, user.orgId))) | ||
| .returning({ id: channels.id }); | ||
| if (updated.length === 0) return fail(404, { error: 'channel not found' }); | ||
| console.info(`history analysis requested for channel ${channelId}: scanning back to ${boundary}`); |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== candidate file outline =="
ast-grep outline 'src/routes/(app)/dashboard/+page.server.ts' --view expanded || true
echo "== candidate file relevant sections =="
wc -l 'src/routes/(app)/dashboard/+page.server.ts'
sed -n '1,180p' 'src/routes/(app)/dashboard/+page.server.ts'
echo "== schema/model references for channels =="
rg -n "channels\.|eq\(channels|createTable.*channels|userId|orgId" src -g '*.ts' -g '*.tsx' | head -200Repository: Bonobo791/Moderaty
Length of output: 27619
Authorization Bypass (CWE-862): Missing Authorization
Reachability: External
Scope analyzeHistory to the caller’s channel.
channelId is form input and the update here only checks channels.orgId === user.orgId, so any same-org user can reset another user’s channel scan state. Add eq(channels.userId, user.id) to the where, return 404 on no match, and add a same-org ownership test.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/routes/`(app)/dashboard/+page.server.ts around lines 93 - 115, Update the
channels update in analyzeHistory to require eq(channels.userId, user.id)
alongside the existing orgId and channelId predicates, preserving the 404
response when no row matches. Add a test proving a same-org user cannot reset
another user’s channel scan state.
Source: Coding guidelines
| <form method="POST" action="?/analyzeHistory" class="history-form"> | ||
| <input type="hidden" name="channelId" value={ch.id} /> | ||
| <label for="history-months-{ch.id}">Analyze history</label> | ||
| <select id="history-months-{ch.id}" name="months" aria-label="How far back to analyze comments on {ch.title}"> | ||
| <option value="1">last month</option> | ||
| <option value="3" selected>last 3 months</option> | ||
| <option value="6">last 6 months</option> | ||
| <option value="12">last 12 months</option> | ||
| <option value="24">last 24 months</option> | ||
| </select> | ||
| <button class="btn secondary small" type="submit">Analyze history on {ch.title}</button> | ||
| </form> | ||
| <p class="muted" style="margin:12px 0 0"> | ||
| last checked {ch.cursor ? relativeTime(ch.cursor) : 'never'} · ID: {ch.id} | ||
| last checked {ch.lastRunAt ? relativeTime(ch.lastRunAt) : 'never'} · ID: {ch.id} |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Render history-action feedback in this card.
A failed analyzeHistory action sets form.error, but this page renders that error only in the Delete account card. The new form also has no success or pending state. Return an operation-scoped result and render its error, success status, and pending skeleton beside this form.
As per coding guidelines, “Invariant I12: Every page has all four states — loading (skeleton), empty (EmptyState component), error (.error-box), and populated.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/routes/`(app)/dashboard/+page.svelte around lines 107 - 120, Update the
history form around the analyzeHistory action to use an operation-scoped result,
rendering its error in an .error-box, success status, and pending skeleton
beside the form. Ensure the loading, empty, error, and populated states required
by the page are represented without reusing the Delete account feedback state.
Source: Coding guidelines
Code Review by Qodo
Context used✅ Compliance rules (platform):
78 rules 1.
|
| const updated = await db | ||
| .update(channels) | ||
| .set({ cursor: boundary, nextPageToken: null, scanCursor: null }) | ||
| .where(and(eq(channels.id, channelId), eq(channels.orgId, user.orgId))) |
There was a problem hiding this comment.
1. analyzehistory not scoped by userid 📘 Rule violation ⛨ Security
The new analyzeHistory channel mutation scopes the update by channels.orgId but does not also scope by channels.userId as required, allowing any user in the org to mutate a channel connected by a different user. This can enable unauthorized history scans and associated API/AI cost on another user’s connected channel.
Agent Prompt
## Issue description
The `analyzeHistory` action updates `channels.cursor` without scoping the mutation by `channels.userId` (only by `channels.orgId`), which violates the requirement that channel operations be scoped to the authenticated user and return 404 for unauthorized access.
## Issue Context
`channels` includes both `userId` and `orgId`. The compliance requirement explicitly calls for scoping channel mutations by `channels.userId` to prevent a user from operating on another user's channel.
## Fix Focus Areas
- src/routes/(app)/dashboard/+page.server.ts[109-113]
- src/lib/server/db/schema.ts[89-96]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| const boundary = new Date(Date.now() - months * 30 * 24 * 60 * 60 * 1000).toISOString(); | ||
| const updated = await db | ||
| .update(channels) | ||
| .set({ cursor: boundary, nextPageToken: null, scanCursor: null }) |
There was a problem hiding this comment.
3. Backfill delays new comments 🐞 Bug ≡ Correctness
analyzeHistory can initiate a multi-run historical drain that stores nextPageToken, and subsequent cron runs will continue from that continuation token rather than restarting at the newest page. This provides no guarantee that comments posted after the backfill begins will be moderated promptly (they may be delayed until the drain completes and pagination resets).
Agent Prompt
## Issue description
When `analyzeHistory` moves `cursor` far back, `fetchNewComments()` may return `nextPageToken` for large windows. While `channels.nextPageToken` is non-null, `runChannel()` passes it back into `fetchNewComments()`, so the scan resumes mid-history instead of re-checking newest comments first.
## Issue Context
- `analyzeHistory` resets to a far-back cursor and clears tokens to start a drain.
- `persistResults()` stores `nextPageToken` + a fixed `scanCursor` to continue the drain in later cron runs.
- `runChannel()` always supplies `pageToken: channel.nextPageToken`.
## Fix Focus Areas
- src/routes/(app)/dashboard/+page.server.ts[93-117]
- src/lib/server/pipeline.ts[470-494]
- src/lib/server/pipeline.ts[505-559]
- src/lib/server/youtube.ts[223-264]
## Recommended fix
Add a “live-first” behavior during long drains, for example:
- If `channel.nextPageToken` is set (drain in progress), **first** fetch/process page(s) from the newest end to catch up on new comments since `channel.scanCursor` (or since the current high-water), **then** continue the historical drain using `nextPageToken`.
This may require storing separate state for the drain (e.g., `historyNextPageToken` / `historyBoundary`) so the live cursor can keep advancing without aborting the backfill. Ensure dedupe remains in place so comments aren’t re-scored.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
…ll-state test assertions
|
Review triage complete — fixes pushed in Fixed (valid findings):
Not a bug (skipped with reason):
Acknowledged, follow-up filed:
Gate: |
|




User description
Behavior
Bug fix — "last checked": the dashboard labeled the newest comment seen (
channels.cursor) as "last checked", so a channel whose newest comment is ~5 years old read "last checked 258 weeks ago". The card now renderschannels.lastRunAt(the actual last cron run), which the load now projects.relative-timealso gains month/year units so old timestamps stop degrading to giant week counts.Scan window — how far back the app pulls comments (changed per product decision): previously a newly connected channel had NO time boundary — cron drained the channel's entire comment history, years back, at 300 comments per run. Now:
cursor = createdAt, so only comments published after connection are moderated. Reconnects (token handover) keep the existing window.decideNewCommentsdedupes by id), so only unscanned history costs YouTube/AI calls. Preset windows only — an unbounded window is an unbounded API cost (I10). Tenancy-scoped like every channel mutation (another team's channel → 404).Existing channels are unaffected — their cursors are already established; no backfill.
Tests (failing first)
cursor = createdAt; reconnect preserves the pre-existing cursor.analyzeHistory: boundary lands ~N×30 days back, drain state (nextPageToken/scanCursor) reset; invalid months (0/2/25/'x') → 400 unchanged; unknown/cross-team channel → 404; signed-out → 401.lastRunAt;relative-timemonth/year buckets incl. singular forms.Verification
npm run test420/420 ·npm run check0 errors ·npm run buildclean.Note: touches one dashboard svelte form +
relative-time.ts(frontend layer) — minimal, functional styling only; visual polish is the frontend owner's call.CodeAnt-AI Description
Make scan history explicit and show the actual last check time
What Changed
Impact
✅ No unexpected historical moderation on channel connection✅ Controlled moderation cost for history scans✅ Accurate last-checked timestamps💡 Usage Guide
Checking Your Pull Request
Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.
Talking to CodeAnt AI
Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:
This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.
Example
Preserve Org Learnings with CodeAnt
You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:
This helps CodeAnt AI learn and adapt to your team's coding style and standards.
Example
Retrigger review
Ask CodeAnt AI to review the PR again, by typing:
Check Your Repository Health
To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.