Skip to content

Conversation

@reteps
Copy link
Member

@reteps reteps commented Jan 16, 2026

Description

This PR adds a dismissible alert on the PrairieLearn homepage that displays up to 3 most recent unread blog posts from the RSS feed. The feature is intended for instructors only - students do not see the alert.

Requirements:

Key changes:

  • New database tables: cached_blog_posts and user_blog_read_timestamps for tracking read status
  • RSS feed fetching service that runs hourly via cron job
  • Homepage component showing blog posts with dismiss functionality
  • Blog post model functions for database operations

Testing

To test locally:

INSERT INTO cached_news_items (title, link, pub_date, guid)
VALUES (
  'Another Test Post',
  'https://prairielearn.org/blog/another-test',
  NOW(),
  'manual-test-' || gen_random_uuid()
);
CleanShot 2026-01-19 at 11 39 02@2x

After dismissing, the alert should not re-appear.

CleanShot 2026-01-19 at 11 41 55@2x

@codecov
Copy link

codecov bot commented Jan 16, 2026

Codecov Report

❌ Patch coverage is 24.00000% with 57 lines in your changes missing coverage. Please review.
✅ Project coverage is 52.74%. Comparing base (489a167) to head (e9768a6).
⚠️ Report is 9 commits behind head on master.

Files with missing lines Patch % Lines
apps/prairielearn/src/lib/news-feed.ts 14.70% 28 Missing and 1 partial ⚠️
apps/prairielearn/src/models/news-items.ts 23.07% 10 Missing ⚠️
apps/prairielearn/src/tests/e2e/serverUtils.ts 0.00% 8 Missing ⚠️
apps/prairielearn/src/pages/home/home.tsx 50.00% 4 Missing and 1 partial ⚠️
...airielearn/src/pages/home/components/NewsAlert.tsx 20.00% 3 Missing and 1 partial ⚠️
apps/prairielearn/src/tests/e2e/fixtures.ts 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #13815      +/-   ##
==========================================
+ Coverage   52.63%   52.74%   +0.10%     
==========================================
  Files         886      893       +7     
  Lines       33636    33728      +92     
  Branches     5093     5103      +10     
==========================================
+ Hits        17705    17789      +84     
- Misses      14579    14593      +14     
+ Partials     1352     1346       -6     
Flag Coverage Δ
javascript 50.29% <24.00%> (+0.12%) ⬆️
python 70.86% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions
Copy link
Contributor

github-actions bot commented Jan 16, 2026

All images
Image Platform Old Size New Size Change
prairielearn/executor:e9768a6207156d374507d331412f6e27fb5d0473 linux/amd64 1270.19 MB 1269.56 MB -0.05%
prairielearn/executor:e9768a6207156d374507d331412f6e27fb5d0473 linux/arm64 1237.37 MB 1236.75 MB -0.05%
prairielearn/prairielearn:e9768a6207156d374507d331412f6e27fb5d0473 linux/amd64 1270.18 MB 1269.55 MB -0.05%
prairielearn/prairielearn:e9768a6207156d374507d331412f6e27fb5d0473 linux/arm64 1237.36 MB 1236.74 MB -0.05%

No significant size changes

reteps and others added 8 commits January 19, 2026 11:34
Show up to 3 most recent unread blog posts from the PrairieLearn RSS feed
as a dismissible alert to instructors. Includes RSS fetching cron job that
runs hourly, database schema for caching posts and tracking read status,
and homepage integration with dismiss functionality.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Rename all "blog" references to "news" throughout the codebase
- Add RSS category filtering (configurable via newsFeedCategories)
- Default newsFeedUrl to null (feature disabled by default)
- Add e2e tests for news alert visibility and dismissal

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@reteps reteps force-pushed the reteps/blog-alert-ui branch from a763ffd to ce422f1 Compare January 19, 2026 16:34
@github-actions github-actions bot added the documentation Related to user documentation (not code docs) label Jan 19, 2026
@reteps reteps marked this pull request as ready for review January 19, 2026 16:42
@reteps reteps requested a review from a team as a code owner January 19, 2026 16:42
@reteps reteps requested review from nwalters512 and removed request for a team January 19, 2026 16:42
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Jan 19, 2026

Confidence Score: 5/5

  • This PR is safe to merge with no blocking issues
  • The implementation is well-structured with proper database migrations, schema definitions, type safety, error handling, and E2E test coverage. All changes follow established codebase conventions and patterns.
  • No files require special attention

Important Files Changed

Filename Overview
apps/prairielearn/src/migrations/20260116150000_news_alerts__create.sql Creates two tables for caching news items from RSS feed and tracking user read timestamps
apps/prairielearn/src/lib/news-feed.ts RSS parser service that fetches, filters by category, validates, and caches news items
apps/prairielearn/src/models/news-items.ts Model functions for selecting unread news and marking as read, with sequential upserts
apps/prairielearn/src/pages/home/components/NewsAlert.tsx React component rendering dismissible alert with news items and form submit button
apps/prairielearn/src/pages/home/home.tsx Route handler that fetches unread news for instructors and handles dismiss action via POST

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

17 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 19, 2026

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

Adds a news-alert feature: a cron job fetches and caches RSS feed items, new DB tables track cached items and user read timestamps, server models and UI expose unread alerts to instructors with a dismiss action, configuration/schema updates, tests, and an rss-parser dependency.

Changes

Cohort / File(s) Summary
Cron
apps/prairielearn/src/cron/fetchNewsItems.ts, apps/prairielearn/src/cron/index.ts
New cron job module that calls fetchAndCacheNewsItems() and registers a periodic job with configurable interval.
Config & Schemas
apps/prairielearn/src/lib/config.ts, docs/assets/config-prairielearn.schema.json, docs/assets/config-unified.schema.json
Added cronIntervalFetchNewsItemsSec (default 10800), newsFeedUrl (nullable, default null), and newsFeedCategories (array of strings, default []).
Database migrations / tables
apps/prairielearn/src/migrations/20260116150000_news_alerts__create.sql, database/tables/cached_news_items.pg, database/tables/user_news_read_timestamps.pg, database/tables/users.pg
New tables cached_news_items and user_news_read_timestamps, pub_date index, unique GUID, and FK from user_news_read_timestamps.user_idusers(id) with ON UPDATE/DELETE CASCADE.
DB types & models
apps/prairielearn/src/lib/db-types.ts, apps/prairielearn/src/models/news-items.sql, apps/prairielearn/src/models/news-items.ts
Added Zod schemas/types CachedNewsItem, UserNewsReadTimestamp; SQL and TypeScript functions: selectUnreadNewsItemsForUser, markNewsItemsAsReadForUser, upsertCachedNewsItem(s), and NewsItemInput.
News feed logic
apps/prairielearn/src/lib/news-feed.ts
New fetchAndCacheNewsItems() implemented using rss-parser: reads config, filters by categories, validates fields, converts items, and upserts cached items with logging and error handling.
UI / Pages
apps/prairielearn/src/pages/home/components/NewsAlert.tsx, apps/prairielearn/src/pages/home/home.html.tsx, apps/prairielearn/src/pages/home/home.tsx
New NewsAlert component; Home page props now include unreadNewsItems; server handlers fetch unread items for instructors, add dismiss_news_alert POST action to mark read, and use typedAsyncHandler.
Dependency
apps/prairielearn/package.json
Added rss-parser dependency (^3.13.0) (entry present twice in diff).
Tests & test utils
apps/prairielearn/src/tests/e2e/newsAlert.spec.ts, apps/prairielearn/src/tests/e2e/fixtures.ts, apps/prairielearn/src/tests/e2e/serverUtils.ts
New Playwright e2e tests for news alert display and dismiss; fixtures set git default branch to master; server utils add temporary uncaughtException handler to ignore ECONNRESET during shutdown.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'Add blog post alert UI on homepage' directly matches the main change—adding a dismissible alert component to display blog posts on the homepage.
Description check ✅ Passed The pull request description clearly relates to the changeset, describing the addition of a dismissible blog/news alert on the homepage with RSS feed fetching and database tables.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/prairielearn/src/lib/db-types.ts (1)

1559-1661: Add new table names to the TableNames array.

The new tables cached_news_items and user_news_read_timestamps should be added to the TableNames array for consistency with other tables that have corresponding Zod schemas. Based on learnings, all database tables should have corresponding entries here.

Proposed fix
 export const TableNames = [
   'access_logs',
   'access_tokens',
   'administrators',
   'ai_grading_jobs',
   'ai_question_generation_prompts',
   'alternative_groups',
   'assessment_access_rules',
   'assessment_instances',
   'assessment_modules',
   'assessment_question_role_permissions',
   'assessment_questions',
   'assessment_score_logs',
   'assessment_sets',
   'assessment_state_logs',
   'assessments',
   'audit_events',
   'audit_logs',
   'authn_providers',
   'authors',
   'batched_migration_jobs',
   'batched_migrations',
+  'cached_news_items',
   'chunks',
   'client_fingerprints',
   ...
   'users',
+  'user_news_read_timestamps',
   'variant_view_logs',
   ...
🤖 Fix all issues with AI agents
In `@apps/prairielearn/src/lib/news-feed.ts`:
- Around line 14-22: hasMatchingCategory currently returns false for items with
no categories even when allowedCategories is empty; move or adjust the check so
that when allowedCategories.length === 0 the function returns true
unconditionally (i.e., allow all, including uncategorized items). Update the
logic in hasMatchingCategory to check allowedCategories.length === 0
before/independent of the categories existence check, and only perform the
categories empty check when filtering is enabled; keep using the
normalizedAllowed Set and the categories.some(...) match logic for the filtered
case.
- Line 1: hasMatchingCategory currently filters out items without categories
before honoring an empty-allowedCategories rule; change the logic in
hasMatchingCategory so it first returns true if allowedCategories is empty, then
returns false if item.categories is missing, and otherwise checks for any
intersection between item.categories and allowedCategories; also add
"rss-parser" to the apps/prairielearn package.json dependencies (or
workspace-specific deps) so the import Parser from 'rss-parser' is declared.
🧹 Nitpick comments (2)
apps/prairielearn/src/tests/e2e/newsAlert.e2e.spec.ts (1)

47-71: Consider test isolation for dismiss behavior.

The test correctly inserts a new news item to avoid dependency on the first test's state. However, if the first test dismisses the alert (it doesn't currently, but could in future refactoring), the beforeAll item would already be dismissed for the second test.

For better isolation, consider using test.describe.serial() explicitly or ensuring each test is fully independent by cleaning up read timestamps.

apps/prairielearn/src/models/news-items.ts (1)

51-57: Consider batching/parallelizing cached item upserts.

Sequential awaits can add noticeable latency if the feed grows. A simple Promise.all is a low-effort improvement.

Possible refactor
 export async function upsertCachedNewsItems(items: NewsItemInput[]): Promise<CachedNewsItem[]> {
-  const results: CachedNewsItem[] = [];
-  for (const item of items) {
-    const result = await upsertCachedNewsItem(item);
-    results.push(result);
-  }
-  return results;
+  return await Promise.all(items.map((item) => upsertCachedNewsItem(item)));
 }

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@apps/prairielearn/src/tests/e2e/newsAlert.spec.ts`:
- Around line 40-52: The 'can dismiss the news alert' test mutates persistent
state and can race with 'shows news alert to instructors with unread items'; fix
by isolating state: either wrap both tests in a Playwright test.describe.serial
block to enforce order (use test.describe.serial(...) and keep the existing test
names 'can dismiss the news alert' and 'shows news alert to instructors with
unread items'), or add per-test setup/teardown that ensures a fresh news item
and clears dismissals — implement a test.beforeEach that calls your test helper
(e.g., insertNewsItem or API endpoint) to create a fresh news alert and/or a
test.afterEach that resets the dismissal state via the same helper (or call a
resetDismissal API) so 'can dismiss the news alert' no longer affects other
tests.

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

20 files reviewed, 1 comment

title: 'Test News Item for E2E',
link: 'https://example.com/news/test-e2e-item',
pub_date: new Date(),
guid: `test-news-item-${Date.now()}`,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Using timestamp in GUID could cause test flakiness in parallel execution

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/prairielearn/src/tests/e2e/newsAlert.spec.ts
Line: 22:22

Comment:
**style:** Using timestamp in GUID could cause test flakiness in parallel execution

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Say more? how would it do that

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue is that Date.now() returns a timestamp in milliseconds. When tests run in parallel, there's a possibility that multiple tests could execute within the same millisecond, causing GUID collisions. Here's how this could cause flakiness:

  1. Parallel execution timing: If two test workers start at nearly the same time, Date.now() might return the same value
  2. Database conflicts: If the GUID is a unique key or used for deduplication, duplicate GUIDs could cause database errors or unexpected behavior
  3. Test isolation issues: One test's data might interfere with another's expectations

The solution is to use a more unique identifier. Here's the fix:

Suggested change
guid: `test-news-item-${Date.now()}`,
guid: `test-news-item-${Date.now()}-${Math.random().toString(36).substring(2)}`,

This combines:

  • Date.now() for temporal uniqueness
  • Math.random().toString(36).substring(2) for additional randomness to prevent collisions even within the same millisecond

Alternatively, you could use crypto.randomUUID() if available:

Suggested change
guid: `test-news-item-${Date.now()}`,
guid: `test-news-item-${crypto.randomUUID()}`,

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

huh??

If the GUID is a unique key or used for deduplication, duplicate GUIDs could cause database errors or unexpected behavior

Is that true? I don't think so

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

20 files reviewed, no comments

@reteps reteps added do not merge This pull request should not be merged yet blocking This PR is blocking other in-progress work. labels Jan 20, 2026
@reteps reteps removed the do not merge This pull request should not be merged yet label Jan 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

blocking This PR is blocking other in-progress work. documentation Related to user documentation (not code docs)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants