Skip to content

Feeds tab: read RSS feeds in groups, preview and add articles as Posts - #236

Open
fboucher wants to merge 5 commits into
v-nextfrom
feature/issue-231-feeds-data-model
Open

fboucher wants to merge 5 commits into
v-nextfrom
feature/issue-231-feeds-data-model

Conversation

@fboucher

@fboucher fboucher commented Oct 31, 2026 •

Copy link
Copy Markdown
Owner

Requested by Frank · project thread

Implements #230 in one PR, one commit per sub-issue.

Closes #231
Closes #232
Closes #233
Closes #234
Closes #235

Commits

  1. Feeds 1/5: data model, storage and API for groups, feeds and item state #231 Data model and storage: FeedGroup, Feed, FeedItemState (Added/Skipped by URL) in Domain, tables FeedGroups, Feeds and FeedItemStates, and the api/feeds CRUD endpoints. A group can't be deleted while it still has feeds (409). Feed URLs and item URLs are compared after normalizing them (FeedItemState.NormalizeUrl), and item-state keys are a SHA-256 hash of that. Skipped records older than 60 days are pruned when states are listed.
  2. Feeds 2/5: fetch feed items in the API with cache and per-feed errors #232 Fetching: GET api/feeds/items?groupId=&feedId=&refresh=. Feeds are fetched in parallel (8 at a time), with a 10 s timeout per feed and a 15 min in-memory cache. refresh=true skips the cache. A failing feed returns its error and the other feeds still load. Items older than 30 days are hidden, and so are Added/Skipped items, items already saved as Posts, and the same article appearing in two feeds. The parser (FeedParser) reads RSS 2.0, RSS 1.0/RDF and Atom, ignores DTDs, and handles odd date formats. Feed HTML goes through an allow-list HtmlSanitizer (HtmlAgilityPack, already a dependency) before it reaches the UI.
  3. Feeds 3/5: Manage feeds panel, feed auto-discovery and OPML import #233 Manage feeds: GET api/feeds/discover (works with a feed URL or a site URL, via <link rel="alternate">) and POST api/feeds/import-opml (Feedly export: top-level folders become groups, nested folders go into their top-level group, feeds you already follow are skipped). /feeds/manage page to add feeds, create/rename/delete groups, move or delete feeds, and upload an OPML file. The IFeedService/FeedClient interface and client are kept separate from IDataService, so the MAUI app's OfflineDataService needed no changes.
  4. Feeds 4/5: Feeds tab layout with groups pane and item list #234 Tab layout: /feeds page plus a nav link in the web app's NavMenu only. On the left: "All" and the groups with pending counts, expandable to their feeds, with a warning icon and the error as tooltip on failing feeds. On the right: items newest first. Refresh re-fetches the current selection. "Mark all as skipped" works on the selected group or feed (disabled on "All").
  5. Feeds 5/5: item preview dialog with Add, Skip, Close and Open original #235 Preview dialog: shows the source, date, author, lead image and sanitized body. Add goes through ExtractPostDetailsAndSave, then marks the item Added and shows a toast. Skip marks it Skipped. Both remove the item from the list. Close does nothing else. Open original opens the URL in a new tab through IUrlLauncher.

Not in this PR (per #230)

MAUI support, background polling, a feed in several groups, authenticated feeds. The new pages live in SharedUI but IFeedService is only registered in the web app, so they aren't reachable from MAUI.

Tests

  • New: API tests for the endpoints (Azurite), parser, sanitizer, OPML, feed reader with a stub HTTP handler (cache, refresh, timeout, errors, discovery), plus bUnit tests for the Feeds page, Manage feeds page, preview dialog, FeedClient and the nav link.
  • Local run: AIServices 31/31, BlazorApp 112/112, MauiApp 66/66, Api 350/351. The one Api failure is ExtractPostDetails_WithValidUrl_ReturnsOkWithPost, which fetches docs.microsoft.com; my sandbox can't reach it, and the test also fails without this PR there.
  • Not tested: running the whole app end to end (Aspire + Keycloak) and the MAUI build. Please give /feeds a quick click-through.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RmvEuU4c1z8w9onm9taU73

First part of the Feeds tab (#230): FeedGroup, Feed and FeedItemState
domain types, Azure Table storage (FeedGroups, Feeds, FeedItemStates)
and api/feeds endpoints for groups, feeds and Added/Skipped item state.
Skipped records older than 60 days are pruned when states are listed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RmvEuU4c1z8w9onm9taU73
GET api/feeds/items returns pending items, newest first, for all feeds,
one group or one feed. Feeds download in parallel (max 8) with a 10 s
timeout each and a 15 minute in-memory cache; refresh=true bypasses it.
A failing feed reports its error while the others still load. Items
older than 30 days, already Added/Skipped, already saved as Posts, or
duplicated across feeds are hidden. Feed HTML is sanitized with an
allow-list before it reaches the UI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RmvEuU4c1z8w9onm9taU73
API: GET api/feeds/discover finds the feed behind a feed or site URL
(<link rel="alternate">); POST api/feeds/import-opml imports a Feedly
style OPML (top-level folders become groups, nested folders flatten
into their top-level group, known feeds are skipped).

UI: IFeedService/FeedClient in SharedUI (registered in the web app
only) and a /feeds/manage page to add feeds, create/rename/delete
groups, move or delete feeds and upload an OPML file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RmvEuU4c1z8w9onm9taU73
New /feeds page in SharedUI, linked from the web app's nav menu only.
Left pane: "All" and the groups with pending counts, expandable to
their feeds; failing feeds show a warning icon with the error as
tooltip. Right pane: items newest first with title, feed, age and a
one-line excerpt. Refresh bypasses the API cache for the current
selection; "Mark all as skipped" skips the selected group or feed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RmvEuU4c1z8w9onm9taU73
Clicking an item on the Feeds tab opens a FluentUI dialog with the
feed-provided content (source, date, author, lead image, sanitized
body). Add saves the article through ExtractPostDetailsAndSave (same
path as Search suggestions and QuickAdd) and marks it Added; Skip
marks it Skipped; both remove it from the list. Close leaves it, and
Open original opens the real URL in a new browser tab.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RmvEuU4c1z8w9onm9taU73
@fboucher fboucher changed the title Feeds 1/5: data model, storage and API for groups, feeds and item state Feeds tab: read RSS feeds in groups, preview and add articles as Posts Oct 31, 2026
@github-actions

Copy link
Copy Markdown

Code Coverage

Package Line Rate Branch Rate Health
NoteBookmark.AIServices 97% 64% ✔
NoteBookmark.Api 90% 82% ✔
NoteBookmark.BlazorApp 14% 15% ❌
NoteBookmark.Domain 94% 77% ✔
NoteBookmark.ServiceDefaults 96% 75% ✔
NoteBookmark.SharedUI 48% 52% ❌
Summary 71% (2542 / 3557) 68% (1100 / 1613) ➖

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In review

Development

Successfully merging this pull request may close these issues.

3 participants