diff --git a/Dockerfile b/Dockerfile
index 9e5fd02..50d8f94 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -51,6 +51,10 @@ RUN mkdir -p /app/data && chown nextjs:nodejs /app/data
COPY --from=builder --chown=nextjs:nodejs /app/monitor.js ./
COPY --from=builder --chown=nextjs:nodejs /app/node_modules/sqlite ./node_modules/sqlite
COPY --from=builder --chown=nextjs:nodejs /app/node_modules/sqlite3 ./node_modules/sqlite3
+COPY --from=builder --chown=nextjs:nodejs /app/node_modules/rss-parser ./node_modules/rss-parser
+COPY --from=builder --chown=nextjs:nodejs /app/node_modules/xml2js ./node_modules/xml2js
+COPY --from=builder --chown=nextjs:nodejs /app/node_modules/sax ./node_modules/sax
+COPY --from=builder --chown=nextjs:nodejs /app/node_modules/xmlbuilder ./node_modules/xmlbuilder
COPY --chown=nextjs:nodejs start.sh ./
RUN chmod +x start.sh
diff --git a/app/api/rss/feeds/route.ts b/app/api/rss/feeds/route.ts
new file mode 100644
index 0000000..05893f9
--- /dev/null
+++ b/app/api/rss/feeds/route.ts
@@ -0,0 +1,91 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { getDb } from '@/lib/db';
+import Parser from 'rss-parser';
+
+export const dynamic = 'force-dynamic';
+
+export async function GET() {
+ const db = await getDb();
+ const feeds = await db.all(`
+ SELECT f.*, (SELECT COUNT(*) FROM rss_items WHERE feed_id = f.id) as item_count
+ FROM rss_feeds f ORDER BY f.created_at ASC
+ `);
+ return NextResponse.json(feeds);
+}
+
+export async function POST(req: NextRequest) {
+ const { url, name } = await req.json();
+
+ if (!url || typeof url !== 'string') {
+ return NextResponse.json({ error: 'URL is required' }, { status: 400 });
+ }
+
+ // Validate URL format
+ try {
+ new URL(url);
+ } catch {
+ return NextResponse.json({ error: 'Invalid URL format' }, { status: 400 });
+ }
+
+ // Validate it's actually an RSS feed
+ const parser = new Parser({ timeout: 5000 });
+ let parsed;
+ try {
+ parsed = await parser.parseURL(url);
+ } catch {
+ return NextResponse.json({ error: 'Could not parse RSS feed at this URL' }, { status: 400 });
+ }
+
+ const feedName = name?.trim() || parsed.title || new URL(url).hostname;
+
+ const db = await getDb();
+
+ try {
+ const result = await db.run(
+ 'INSERT INTO rss_feeds (name, url) VALUES (?, ?)',
+ feedName, url.trim()
+ );
+ const feedId = result.lastID;
+
+ // Immediately fetch and cache items
+ 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 (?, ?, ?, ?, ?, ?)`,
+ feedId,
+ 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(), feedId
+ );
+
+ const feed = await db.get('SELECT * FROM rss_feeds WHERE id = ?', feedId);
+ return NextResponse.json(feed, { status: 201 });
+ } catch (err: unknown) {
+ if (err instanceof Error && err.message.includes('UNIQUE')) {
+ return NextResponse.json({ error: 'Feed URL already exists' }, { status: 409 });
+ }
+ throw err;
+ }
+}
+
+export async function DELETE(req: NextRequest) {
+ const { id } = await req.json();
+ if (!id) {
+ return NextResponse.json({ error: 'Feed id is required' }, { status: 400 });
+ }
+
+ const db = await getDb();
+ await db.run('DELETE FROM rss_items WHERE feed_id = ?', id);
+ await db.run('DELETE FROM rss_feeds WHERE id = ?', id);
+
+ return NextResponse.json({ ok: true });
+}
diff --git a/app/api/rss/route.ts b/app/api/rss/route.ts
index 181714b..5f28f53 100644
--- a/app/api/rss/route.ts
+++ b/app/api/rss/route.ts
@@ -1,23 +1,76 @@
-import { NextResponse } from 'next/server';
-import Parser from 'rss-parser';
+import { NextRequest, NextResponse } from 'next/server';
+import { getDb } from '@/lib/db';
export const dynamic = 'force-dynamic';
-export async function GET() {
- const parser = new Parser();
- try {
- const feed = await parser.parseURL('https://news.ycombinator.com/rss');
+export async function GET(req: NextRequest) {
+ const { searchParams } = new URL(req.url);
+ const q = searchParams.get('q');
+ const feedId = searchParams.get('feed_id');
+ const bookmarked = searchParams.get('bookmarked');
+ const limit = parseInt(searchParams.get('limit') || '50', 10);
+ const offset = parseInt(searchParams.get('offset') || '0', 10);
- const items = feed.items.slice(0, 10).map(item => ({
- title: item.title,
- link: item.link,
- pubDate: item.pubDate,
- creator: item.creator || 'Unknown',
- contentSnippet: item.contentSnippet,
- }));
+ const db = await getDb();
- return NextResponse.json(items);
- } catch (error) {
- return NextResponse.json({ error: 'Failed to parse RSS' }, { status: 500 });
+ const conditions: string[] = [];
+ const params: (string | number)[] = [];
+
+ if (q) {
+ conditions.push('(i.title LIKE ? OR i.snippet LIKE ?)');
+ params.push(`%${q}%`, `%${q}%`);
}
+ if (feedId) {
+ conditions.push('i.feed_id = ?');
+ params.push(parseInt(feedId, 10));
+ }
+ if (bookmarked === '1') {
+ conditions.push('i.bookmarked = 1');
+ }
+
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
+
+ const items = await db.all(
+ `SELECT i.*, f.name as feed_name, f.url as feed_url
+ FROM rss_items i
+ LEFT JOIN rss_feeds f ON f.id = i.feed_id
+ ${where}
+ ORDER BY i.pub_date DESC
+ LIMIT ? OFFSET ?`,
+ ...params, limit, offset
+ );
+
+ return NextResponse.json(items);
+}
+
+export async function PATCH(req: NextRequest) {
+ const body = await req.json();
+ const { id, read, bookmarked } = body;
+
+ if (!id) {
+ return NextResponse.json({ error: 'Item id is required' }, { status: 400 });
+ }
+
+ const db = await getDb();
+ const updates: string[] = [];
+ const params: (number)[] = [];
+
+ if (read !== undefined) {
+ updates.push('read = ?');
+ params.push(read ? 1 : 0);
+ }
+ if (bookmarked !== undefined) {
+ updates.push('bookmarked = ?');
+ params.push(bookmarked ? 1 : 0);
+ }
+
+ if (updates.length === 0) {
+ return NextResponse.json({ error: 'Nothing to update' }, { status: 400 });
+ }
+
+ params.push(id);
+ await db.run(`UPDATE rss_items SET ${updates.join(', ')} WHERE id = ?`, ...params);
+
+ const item = await db.get('SELECT * FROM rss_items WHERE id = ?', id);
+ return NextResponse.json(item);
}
diff --git a/app/page.tsx b/app/page.tsx
index ed75215..1112dba 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -14,13 +14,8 @@ export default function Home() {
{/* Row 1 & 2 (Globe spans 2 rows) */}
{item.snippet}
+ )} +