Overhaul RSS feed widget: persistence, multi-feed management, search, bookmarks

- Add rss_feeds + rss_items tables with indexes and HN default seed
- Add 5-min background RSS sync loop in monitor.js with 90-day prune
- New /api/rss/feeds route for feed CRUD with immediate fetch on add
- Rewrite /api/rss route with search, feed filter, pagination, read/bookmark PATCH
- Full NewsFeed component rewrite: feed manager, search bar, filter pills,
  read/unread tracking, bookmarks, favicons, auto-refresh with new items badge
- Remove placeholder widget, NewsFeed now spans 4 cols / 3 rows
- Add rss-parser deps to Dockerfile for standalone monitor
This commit is contained in:
Shivam Patel
2026-02-09 04:50:06 -05:00
parent e47a719d79
commit f95e28202d
7 changed files with 795 additions and 64 deletions

View File

@@ -1,5 +1,6 @@
const sqlite3 = require('sqlite3');
const { open } = require('sqlite');
const RSSParser = require('rss-parser');
// Node 18+ has global fetch built-in
const DEFAULT_SERVICES = [
@@ -42,6 +43,78 @@ async function seedDefaults(db) {
}
}
async function setupRssTables(db) {
await db.exec(`
CREATE TABLE IF NOT EXISTS rss_feeds (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
url TEXT NOT NULL UNIQUE,
last_fetched DATETIME,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS rss_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
feed_id INTEGER NOT NULL,
title TEXT NOT NULL,
link TEXT NOT NULL UNIQUE,
pub_date DATETIME,
creator TEXT,
snippet TEXT,
read INTEGER DEFAULT 0,
bookmarked INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_rss_items_feed ON rss_items(feed_id);
CREATE INDEX IF NOT EXISTS idx_rss_items_pubdate ON rss_items(pub_date DESC);
`);
// Seed default HN feed
const count = await db.get('SELECT COUNT(*) as cnt FROM rss_feeds');
if (count.cnt === 0) {
await db.run(
'INSERT INTO rss_feeds (name, url) VALUES (?, ?)',
'Hacker News', 'https://news.ycombinator.com/rss'
);
console.log('Seeded default RSS feed (Hacker News)');
}
}
async function syncRssFeeds(db) {
const parser = new RSSParser({ timeout: 5000 });
const feeds = await db.all('SELECT * FROM rss_feeds');
for (const feed of feeds) {
try {
const parsed = await parser.parseURL(feed.url);
for (const item of parsed.items || []) {
if (!item.title || !item.link) continue;
await db.run(
`INSERT OR IGNORE INTO rss_items (feed_id, title, link, pub_date, creator, snippet)
VALUES (?, ?, ?, ?, ?, ?)`,
feed.id,
item.title,
item.link,
item.pubDate || item.isoDate || null,
item.creator || item.author || null,
(item.contentSnippet || item.content || '').substring(0, 500) || null
);
}
await db.run(
'UPDATE rss_feeds SET last_fetched = ? WHERE id = ?',
new Date().toISOString(), feed.id
);
console.log(`RSS synced: ${feed.name} (${(parsed.items || []).length} items)`);
} catch (err) {
console.error(`RSS sync error for ${feed.name}:`, err.message);
}
}
// Prune items older than 90 days
try {
await db.run(`DELETE FROM rss_items WHERE created_at < datetime('now', '-90 days')`);
} catch (e) { }
}
async function monitor() {
console.log('Starting monitoring loop...');
@@ -66,6 +139,10 @@ async function monitor() {
await seedDefaults(db);
// Setup RSS tables and do initial sync
await setupRssTables(db);
syncRssFeeds(db); // initial sync, don't await to not block uptime start
setInterval(async () => {
console.log('Running checks...');
const now = new Date().toISOString();
@@ -114,6 +191,9 @@ async function monitor() {
} catch (e) { }
}, 60000); // Run every minute
// RSS sync every 5 minutes
setInterval(() => syncRssFeeds(db), 5 * 60 * 1000);
}
monitor();