Skip to content

Releases: jakublibik/readfine

v0.14.0

Choose a tag to compare

@jakublibik jakublibik released this 30 Jul 12:21

Added

  • Articles with nothing to read now offer the original page. Feeds that carry only a headline and a link (Hacker News is the clearest case) produce articles with no text at all, and opening one showed a blank pane and a line saying so. Such an article now shows an "Open original" button, plus "Retry extraction" when a previous attempt failed, so a source that was temporarily unreachable can be tried again without digging through the menu. Settings → Preferences adds "Open original for empty articles", off by default: with it on, clicking one of these articles opens the source in a new tab straight away, while the article itself still opens in the reader behind it, so it is marked read and stays starrable and labellable. Only articles that will never have text are affected, never one whose extraction is still running or could still be started. Time spent on the source counts towards reading time.

  • The interest profile used by AI scoring can now update itself. Settings → AI has an "Auto-generate" switch next to the Generate button with Off (the default), every 2 weeks and every 4 weeks. Until now the profile only changed when you opened settings and pressed a button, so scoring kept ranking articles by whatever you were interested in months ago. An automatic run saves straight away, keeps the version it replaced and offers a one-click revert, and the settings page always shows when the profile last changed and whether it was you or the schedule. Several things have to line up before it spends anything: the interval has to be up, at least 20 articles you read or starred have to be new since the last change, and there have to be enough reading signals that the profile is not padded with feed names. Output that does not look like a profile is thrown away rather than saved, and any attempt that reaches the model, successful or not, waits a full interval before the next one, so a dead API key cannot cost you a call a day. After three failures the schedule turns itself off and says why, in settings and on the admin dashboard.

  • Bot protection on the registration form, on by default and with nothing to configure. Open registration means the instance will email any address a visitor types, which bots abuse to flood scraped third-party inboxes using your domain as the sender. The form now carries a hidden honeypot field that only a script fills in, and a signed timestamp that rejects submissions arriving faster than a person could type. A caught submission creates no account and sends no email. Only the honeypot, which nothing legitimate can fill in, answers with the normal "check your email" page so the script cannot tell it was caught; the timing check is a heuristic, so it puts the form back in front of you instead, as does a form left open for hours. Correcting a mistyped password and submitting again does not restart the timer either, so a password manager refilling the form in one click cannot trip it. The README explains what else to put in front of a public instance.

  • LOG_OUTBOUND_REQUESTS (off by default): a diagnostic switch that logs one line per outbound HTTP request, covering feed fetches, scraping and readable extraction. Each line records the host, status, HTTP version, elapsed time and any rate-limit headers the server sent (Retry-After, X-RateLimit-*). Per-feed error records only show failures, and only per feed, so they cannot answer how often a host is really being hit when several feeds and the extraction pipeline share it. Turn this on when a site starts returning 403 or 429, read the real request rate and spacing from the log, then turn it off again.

  • LOG_LEVEL (DEBUG/INFO/WARNING/ERROR, default WARNING): how much the app logs. WARNING keeps the log to things that need attention, INFO adds the running commentary from the scheduler and fetcher. Noisy libraries (httpx, APScheduler) stay at WARNING either way, so raising the level surfaces Readfine's own records rather than a wall of third-party chatter.

  • FETCH_SCHEDULE_OFFSET_MIN (minutes, default 0): shifts the four 15-minute feed-fetch ticks off the usual :00/:15/:30/:45. Useful when two instances share a host (for example a staging instance next to production) and you don't want both firing their fetch round at the same wall-clock moment. Set staging to 7 and it polls at :07/:22/:37/:52 instead. The value is folded into 0–14, and the predicted next-fetch times in the UI follow the shifted schedule.

Fixed

  • A filter regex could quietly fail to match on a busy instance. Filter patterns run under a time limit so that a pathological one cannot freeze the app, and a pattern that runs out of time counts as "no match", which means the filter silently does not fire. The limit was 0.1 seconds of wall-clock time, close enough to what an ordinary pattern costs on a long article that a busy fetch round was enough to trip it: a production log showed \bAI\b timing out. The limit is now a full second, which still stops a runaway pattern but leaves normal ones far below it.

  • A feed whose server dropped the connection mid-request is now retried once instead of counting as a failed fetch. Reusing a kept-alive connection races with the server closing it, and HTTP/2 servers close them routinely, so a request could die with nothing sent back through no fault of the request itself. Fetches are plain GETs, so a second attempt on a fresh connection is safe, and it recovers feeds that were losing a poll here and there for this reason.

  • Settings → AI no longer announces a nightly interest-profile generation that cannot happen. The status line checked that a quality model was picked, but not that there was a key to use it with, so an account whose key had been removed (or a restore that lost the encryption key) read "The next generation runs tonight" indefinitely while the job skipped every night without recording anything. It now says which provider is missing a key.

  • Three AI settings actions (saving a provider key, verifying a model, queueing summaries for all starred articles) were served with no rate limit, despite carrying one in the code. The limit was attached to the route in the wrong order, so it wrapped a copy of the handler that nothing ever called. Generating the interest profile by hand had no limit declared at all, which mattered more: it is the most expensive call the app can make, it builds its prompt from the whole reading history, and one stuck button could run it as fast as the provider would answer. It now allows 5 per hour, configurable through RATE_LIMIT_AI_PREFERENCE. A test now checks every rate-limited route is really limited, since the failure left no trace anywhere.

  • The general AI chat no longer scrolls the page behind it once you have sent a message. The message list was set to swallow the scroll at its own end, but only until the first reply arrived, because the server rebuilt the panel without that setting. Most visible on a phone, where reaching the bottom of a conversation started dragging the article list underneath.

  • The label next to the paperclip in an article's chat now updates when you detach or reattach the article, instead of only doing so after your first message. The panel was drawn without the hook the script needed to find it, and a later redraw quietly added it back.

  • Saving Settings → AI with scoring switched off no longer erases the interest profile. The profile field is disabled while scoring is off, and a disabled field sends nothing at all, so the save read it as "cleared" and wiped the text. Fields that were not part of the submit now keep their stored value, which also covers the new auto-generate schedule and the score-in-list toggle.

  • The scoring checkbox in Settings → AI now enables and disables the fields below it as you click it, instead of only after saving. The checkbox carried two id attributes and the script hooked onto the one the browser had thrown away.

  • Feeds on sites that block automated clients no longer get marked as failing and eventually switched off. Some sites (Reddit is the obvious one) refuse a share of requests with HTTP 403 or a bare 429 and let the rest through, in waves that last minutes to hours and hit every feed on the site at once. Readfine counted each refusal as a fetch error, so five of them in a row disabled a feed that was working fine. Refusals are now tracked separately from real errors: the feed keeps its normal state, backs off progressively instead of retrying on its usual schedule, and is only switched off after ten in a row. Settings → Feeds and the admin feed table label such a feed "throttled" once it has been refused three times running, in amber rather than red, and one stray refusal shows nothing at all. Any successful fetch clears the count.

  • Readfine could learn a fetch spacing that no site had actually asked for. A site that reports its rate limit as exhausted also reports how long until the limit resets, and that countdown was read as if it were the sustainable gap between requests. It is not: it depends on when the request happened to land in the site's current window, so the value swung between 0 and 60 seconds on identical traffic, and the "never loosen" rule kept the highest number seen. On Reddit this settled at 78 seconds, throttling every feed on the host for no reason. The countdown is now used for what it is, a deadline before the next request, and the learned spacing only comes from a limit with room left in it or from an explicit Retry-After. The stored values are cleared once on upgrade so nothing carries the old numbers forward; sites that advertise a real limit are re-learned on the next fetch.

  • Admin → Rate limits could list a host at a spacing of 0 seconds, which read as a learned limit but meant the opposite: a single rate-limit response had created the row before anything was learned, and it then stayed forever. Such rows are no longer written, and existing ones are removed.

  • F...

Read more

v0.13.0

Choose a tag to compare

@jakublibik jakublibik released this 19 Jul 17:37

Added

  • Preferences → "Number & date format": a per-user choice of how numbers and dates are written, independent of the interface language. Five profiles cover the common conventions (US, UK/International, Europe, DE/AT, ISO), each differing in the decimal separator, thousands separator and date order, so you can keep the app in English yet see 1 234,56 and 25.06.2026. New accounts are detected from the browser at sign-up; existing accounts start on the Europe profile and can switch anytime. The setting drives numbers across the stats and AI cost views and the numeric date formats throughout the app, including the date shown in briefing emails (in your timezone). Times stay 24-hour for now.
  • A categorized feature list at /features, linked from the landing page and the help guide, plus a matching FEATURES.md at the repo root. Both are generated from one source (backend/app/content/features.yml): the app renders it at runtime, and scripts/gen_features.py projects it into FEATURES.md, so the list is never maintained in two places. CI regenerates the Markdown and fails if the committed copy is stale.
  • Adaptive fetch intervals: a feed left on "Auto" is now polled at a cadence derived from how often it actually publishes, rather than the flat default. Readfine counts each feed's items over the last 7 days and targets an interval a bit shorter than its real publish gap, so busy feeds refresh more often and quiet ones less. New feeds and feeds without enough history keep using the global default, and an explicit per-feed interval still wins. Admin → Settings gains a "Maximum fetch interval" cap for how rarely a quiet feed may be polled; the feed edit screen shows the interval Auto would pick. Cadence is recomputed daily and at startup. The feeds tables (Settings → Feeds and the admin panel) show each feed's effective interval and its predicted next fetch (relative, e.g. "next ~1h") under the last-fetch column, with intervals of an hour or more rendered in hours.
  • Preferences → "Advance after mark all as read" (off by default): after you mark a feed, folder or label read from the sidebar, Readfine selects and opens the next one that still has unread articles, expanding a collapsed folder if needed. Feeds advance across folder boundaries; empty scopes are skipped, and the special views (All articles, Starred, Archived) are left alone.
  • Admin → Feeds: an "Edit" action on the table's ··· menu for shared feed fields (title, status, fetch interval, and the scrape article-links selector with a live preview). Per-subscriber preferences and feed credentials are intentionally left out, since an admin usually is not the subscriber.
  • Admin dashboard: a "Briefing errors" section listing catch-up configs whose scheduled briefing is currently failing (user, config, error, retries, next send). Configs with no scheduled retry, for example when SMTP is unconfigured, sort first and are flagged as needing manual attention; the entry clears once a send succeeds.
  • Admin → Users: per-user columns showing whether an account is currently active, not just its lifetime totals. "Read 7d" counts articles genuinely read in the last 7 days (marked read with at least 30 seconds of dwell, the same signal the reading stats use), "AI 7d" counts AI operations in the last 7 days across summaries, scoring, context, chat and catch-up, and a filter count.

Changed

  • Fetch interval dropdowns (Admin → Settings and the feed edit screens) now show longer intervals in hours, for example 6h or 24h, instead of raw minutes like 360 min. This matches how the feeds tables already render intervals. The admin settings also spell out how the default, minimum and maximum intervals interact with Auto mode (the minimum floors Auto too, and Auto never polls faster than 30 minutes).
  • Readable extraction backfills an article's publication date from the article page when the feed listing carried no date, so undated articles sort and expire correctly instead of all landing at fetch time. It reads the page's structured datePublished (via htmldate) rather than the oldest date on the page, guards against implausible future dates, and never overrides a date the feed already provided. The reader's date updates once extraction finishes.
  • Deleting a feed subscription or folder now cleans up references to it left dangling in filter scopes and catch-up/briefing scopes. As a safeguard against silently widening, a filter or briefing whose scope would empty out is deactivated or disabled instead, and the affected filter and briefing names are surfaced in the feeds settings banner.

Fixed

  • Admin → Feeds: the "Force fetch" button no longer returns a 500 when the fetch fails (for example a rate-limited or erroring feed). A failed fetch rolls back the database session, which expires the loaded feed and admin objects; the handler then read an attribute off one of them and crashed with MissingGreenlet. It now captures what it needs before fetching and reloads the feed's post-fetch state safely, so the usual "Rate-limited, try again in..." toast shows instead of an error.
  • The sidebar now highlights the active category right after opening or reloading the app, not only after a click. On load the highlight was applied to the collapsed rail copy of each nav item (which is hidden) instead of the visible full-sidebar copy, so nothing appeared selected until you clicked a category.
  • Feeds behind some CDNs (most visibly Reddit, via Fastly) that kept failing with 403 Blocked or persistent 429 now fetch normally. The fetcher spoke plain HTTP/1.1, which these CDNs treat as a bot signal and answer with a header-less 403 or a near-zero rate budget, while serving HTTP/2 clients as usual. Server-side fetches (feeds, scraping, readable extraction) now negotiate HTTP/2 when the server offers it and fall back to HTTP/1.1 otherwise. The User-Agent was never the cause.
  • Scrape-type feeds now record a "last published" date in the feeds tables (Settings → Feeds and the admin panel), like RSS feeds already did. The scrape fetcher tracked every other feed field but never set this one, so the column stayed empty even when the scraped listing carried article dates. It now advances to the newest dated link on each fetch and stays empty only when the listing exposes no dates at all.
  • The single-feed API endpoints (GET/POST/PATCH /api/v1/feeds/{id}) now report unread_count computed fresh from the database, like the feed list already did. They previously serialized a cached column that no longer had a live writer on every path, so it could read as stale or zero. The cached column has been dropped (migration 0079); every response counts unread on read.
  • Server-side feed and scrape fetches now pin each connection to the IP address they validated, closing a DNS-rebinding gap: previously the SSRF check resolved the hostname once and the HTTP client resolved it again at connect time, so a hostname whose DNS flipped between the two could pass validation yet connect to a private or cloud-metadata address. Every request and redirect hop now connects to the checked IP, carrying the original Host header and, for HTTPS, the original hostname for TLS SNI and certificate verification.
  • Readable extraction is no longer enabled for Tumblr feeds, which already deliver the full post in the feed. Extracting the page instead pulled in the likes/reblogs "notes" list as the body and duplicated the post text; Tumblr (and other feeds that advertise themselves as full-content via their <generator>) are now treated as full-content at subscribe time, so their feed content is shown as-is. Should extraction still run on a Tumblr page, the notes list and tracking pixels are stripped before extraction as a safeguard.
  • Marking a feed or folder read from the sidebar now refreshes the whole sidebar, so counts that share those articles (labels, other feeds) update right away instead of going stale until the next reload.
  • Readable extraction removes duplicate images: some news sites emit the same photo as several renditions (lead, inline, responsive) and each was extracted, so the body showed the same picture two or three times. Matching on the image filename now collapses them to a single copy.
  • Admin → Feeds: on narrow screens the "Feed" column no longer collapses to a couple of unreadable characters. As the table grew it stopped fitting the panel, and the flexible feed column shrank to nothing while the rest scrolled; it now keeps a readable minimum width and the table scrolls horizontally instead.
  • Admin → Feeds: a host group that contains a failing feed no longer paints its entire header red, which overstated severity and blended into the group separator; only the host name turns red now.
  • Scheduled fetching no longer crashes while arming per-host pacing: after a fetch committed, the scheduler re-read the feed's URL off an expired ORM object, which raised a MissingGreenlet error, failed the fetch, and skipped arming the adaptive per-host spacing. The spacing was therefore never enforced between same-host feeds, so hosts with several feeds (Reddit, YouTube) were fetched in bursts and more likely to answer 403. The URL is now captured before the fetch, so pacing is armed as intended.
  • A feed marked errored in the sidebar now clears its red indicator immediately when a manual refresh succeeds, instead of staying red until the next full page reload. The refresh only swapped the unread count, which sits apart from the error marker; it now updates the marker out-of-band from the feed's fresh status.

v0.12.0

Choose a tag to compare

@jakublibik jakublibik released this 07 Jul 18:06

Added

  • Copy article action on the article ··· menu (desktop) and the bottom action bar (mobile). It copies the title, source and body to the clipboard as both rich HTML and plain text, so it pastes with formatting and images into rich editors and as clean text everywhere else. Relative image and link URLs are rewritten to absolute so they still resolve after pasting.
  • The Stats backlog cards (labeled and starred) now link straight into the matching reader view, so you can jump from a count to the actual articles.
  • Admin → Feeds: a "Rate limits" view (shown only when some exist) lists the fetch pace Readfine has learned per host, with the host, its spacing, and how and when that was learned, each with a Clear action. Errored feeds in the admin table also show their predicted next fetch, and a feed with no per-feed interval override shows the effective default instead of a blank dash.
  • Admin → Feeds: a "By host / A-Z" toggle that groups the feed list by fetch host, so all of a site's feeds (say, every Reddit feed) sit under one host header with a count instead of a flat alphabetical list. Hosts sort alphabetically, single-feed hosts fall into an "Other" bucket, and any host holding an errored feed floats to the top so problems stay visible. Within a group, and in the flat list, feeds now sort by status first (errored, disabled, paused, active) and then by name. The choice is remembered per browser.

Changed

  • A single HTTP 403 no longer disables a feed. Reddit and YouTube return 403 as a transient anti-bot or rate-adjacent block (datacenter IP, generic user-agent) far more often than as a permanent denial, so 403 now backs off through the error tier like 408/429 and 5xx and only disables after several consecutive failures. Genuinely permanent 4xx (400, 401, 404, 410) still disable immediately.
  • The fetcher reads rate-limit headers (Retry-After, RateLimit-*, X-RateLimit-*) on both successful and 429 responses and applies a per-host cooldown. Once a host reports its budget is spent (for example Reddit's x-ratelimit-remaining: 0), other feeds on that host wait out the reset instead of hammering it with more 429s. The waiting happens inside the fetch round, up to a budget that keeps the round short enough not to miss the next slot; anything over that defers to the next round. Feeds on other hosts still fetch in parallel.
  • Manually refreshing a feed (the sidebar ↻ and the admin "force fetch") now respects a known rate-limit window instead of firing straight into another 429. While the host is cooling down it shows "Rate-limited, try again in …" (seconds or minutes, from the server's reset headers). A bare 403 anti-bot block is treated differently: only the background scheduler paces itself on those, since a manual retry often succeeds.
  • Readfine learns a sustainable fetch pace per host and spaces its requests accordingly, rather than only reacting once a host reports its budget already spent. It reads the pace from a host's rate-limit headers on successful responses and tightens it when the host keeps answering 429. The learned pace only ever tightens, so it never oscillates, and is capped so a feed can't stall forever. This keeps aggressive hosts like Reddit, where a burst of same-host feeds fetched back to back would trip a 403/429, from being throttled. The pace is stored and survives restarts and deploys, so a host isn't re-probed into a rate limit on every restart, and manual refreshes respect it too.
  • Feeds are fetched at whichever of the four 15-minute ticks (:00/:15/:30/:45) first follows their interval, instead of being pinned to the top of the hour. This spreads load across the hour on each feed's own phase rather than piling every hourly feed onto :00, and it improves freshness: an hourly feed first fetched a few minutes past the hour used to wait until the next :00 (up to ~2 h between fetches) and now refreshes about an hour later as intended. Feeds that miss a tick (a host cooldown, a transient error, a restart mid-round) recover at the next tick instead of waiting a full interval.
  • The per-feed refresh button (↻) now reloads the article list when you're viewing that feed, so newly fetched articles appear right away instead of only after re-selecting it. Refreshing a feed you're not viewing still just updates its unread badge.
  • The fetch-interval selector spells out the server default next to the "Default" option (for example "Default (60 min)") on the subscribe, scrape-setup and feed-edit forms, and wraps better on narrow screens.
  • Adding a feed now shows specific messages for rate-limiting (429, including when to retry) and temporary server errors (5xx) instead of a bare "HTTP error {status}".
  • The Feeds, Filters and Labels settings pages and the admin Users page show an item count next to the heading, kept current as items are added or removed without a reload.
  • Labels and filters sort case-insensitively everywhere now: settings lists, label pickers and chips. Previously the database collation put all uppercase names before any lowercase one, so a new lowercase label or filter got stuck at the end of the list.
  • The filter list shows a "priority N" badge on filters whose priority isn't the default, making it clear why a filter sorts and runs ahead of alphabetical order.
  • Briefings sent to extra recipients now put the account owner in To: and the additional recipients in Bcc, so co-subscribers no longer see each other's addresses. The modal also notes that delivery can lag the scheduled time by up to 15 minutes (the scheduler tick).
  • The admin "force fetch" button shows a spinner and blocks double-clicks while the synchronous fetch runs, instead of looking like it did nothing for several seconds.
  • Settings → AI cost estimates now cover the current Anthropic, OpenAI and Google model families. A configured model that isn't in the built-in price list is estimated from a typical model for its provider (shown with a "~" and a note under the table) instead of showing as unknown or zero.
  • The Trend column in the AI cost table tracks estimated cost rather than raw operation count, and the Fast/Quality/Total rows show a trend too (previously blank), so the arrows reflect what actually moves your spend, such as longer articles costing more at the same number of runs.

Fixed

  • Collapsing and expanding the sidebar is instant now. It used to refetch the whole sidebar from the server on every toggle, so the old layout lingered, briefly squished into the new width, until the request returned. Both the collapsed rail and the full sidebar are rendered up front and the toggle just switches between them in the browser, with no round-trip. On the mobile "collapsible" sidebar, opening the overlay no longer reflows the article-list text either, because the rail is a fixed strip now and the list keeps a constant width.
  • Toast notifications (a feed's error when you open it, or a manual refresh result) no longer render at roughly half-width on mobile. They stretch edge to edge with a small gutter on narrow screens and stay centred with a sensible max-width on wider ones.
  • Filters that share a priority run in a fixed order now, exactly the order the Settings → Filters list shows (priority, then name). Their order used to be left to the database, so a "stop on match" filter could behave differently between fetches.
  • The green "Feed added successfully" banner no longer reappears when you refresh the Feeds settings page after subscribing.
  • Articles that carry a label stay visible in their label view even after their feed is deleted or unsubscribed. The view used to inner-join the feed and hide them, leaving the sidebar badge counting an apparently empty category.
  • The article-list loading overlay matches the neutral dark-mode background instead of a blue-tinted grey, and is delayed slightly so quick cached loads don't flash a spinner.
  • The AI cost table's total is no longer quietly understated when a model slot uses a model missing from the price list. That slot used to be added as $0, making the total look complete while dropping part of the cost.

Security

  • Filter regex conditions run under a per-match timeout now, closing a denial-of-service hole. The old create-time heuristic could be bypassed by a catastrophic-backtracking pattern (for example ([a-z]+)*), and because matching ran synchronously on the event loop during fetch, filter tests and retroactive apply, and CPython's re neither times out nor releases the GIL, a single crafted filter could freeze the whole app for every user. Evaluation now uses the regex module with a hard timeout; a timed-out pattern counts as no match, and normal filter behaviour is unchanged.

v0.11.0

Choose a tag to compare

@jakublibik jakublibik released this 30 Jun 15:20

Added

  • Search is now also a filter view: alongside the text query you can scope to
    feeds/folders, filter by labels (any / specific) and read status (all / unread /
    read), and choose the sort (relevance / newest / oldest). Leaving the text empty
    applies the filters on their own. Search moved from the user menu to an icon in
    the sidebar.
  • Feeds are now fetched conditionally: Readfine remembers each feed's ETag /
    Last-Modified and sends them back on the next poll, so an unchanged feed answers
    304 Not Modified with no body and the download and parse are skipped entirely.
    Less bandwidth, and lighter on rate-limited sites.
  • Catch me up & briefings now have a dedicated label filter (any label / specific
    labels, OR) shown alongside the feed scope, replacing the old "Labeled only"
    relevance radio. The minimum-score filter is now an independent toggle (shown only
    when scoring is configured) rather than bundled with labels. The "Since yesterday"
    period is now labelled "Yesterday+".
  • Adding a feed lets you set its fetch interval from the subscribe form, and owners
    of a private or solely-subscribed feed can change the interval when editing it.
    Shared public feeds show the interval read-only (only an admin can change it).
  • Errored feeds now show when they will next be retried, both on the feed list and
    the feed detail page; a feed auto-disabled after repeated failures says so
    explicitly instead of leaving the next fetch ambiguous.

Changed

  • Switching between sections (Starred, Labeled, folders, feeds) now shows a brief
    loading overlay over the article list, so the sidebar highlight no longer appears
    to change before the list it points at has loaded.
  • Creating, renaming, or deleting a folder immediately updates the folder dropdown in
    the add-feed form without a page reload.

Fixed

  • Feeds where every item points at one shared link (e.g. a podcast whose episodes all
    link to the show page) no longer have every new item after the first silently
    dropped as a duplicate; items are now de-duplicated by links that actually identify
    a single item, falling back to the unique GUID otherwise.

  • Reddit (and similar) article content built from a header-less layout table no longer
    overflows the reading panel: such tables now stack the image above the text, images
    are constrained to the column width, and genuine data tables scroll horizontally
    instead of overflowing.

  • Text search combined with a read-status filter no longer skips results while
    scrolling: mark-as-read-on-scroll is disabled for that specific case (where it
    shifted the offset-paginated result set), leaving plain search and the filter view
    unaffected.

  • A feed returning HTTP 429 (Too Many Requests) is no longer disabled on the first
    hit. 429 and 408 are now treated as transient: the feed backs off via the normal
    error tier and is only disabled after the usual run of consecutive failures. When
    the server sends a Retry-After header, the scheduler waits at least that long
    before re-fetching.

  • Adding a feed now costs a single network request instead of up to three. The
    "Test" step caches the fetched feed briefly and Subscribe reuses it for both the
    title and the initial article import, so rate-limited sites (e.g. Reddit) no
    longer return 429 mid-subscribe.

  • When several feeds share a host (e.g. multiple Reddit subreddits), a scheduled
    fetch no longer requests them all at once. Requests to a given host are now
    serialized within a fetch round — different hosts still run in parallel — which
    flattens the burst that made some of those feeds return HTTP 429.

  • Readable extraction that returns no usable content — e.g. a Reddit article page
    that serves a bot-verification wall (HTTP 200) instead of the article — is no
    longer saved as a blank "successful" extraction that rendered an empty body. Such
    articles now show their original feed content, and a feed whose pages keep
    extracting nothing auto-disables full-content extraction after repeated empties
    (the same way persistent HTTP 403 blocks already did) instead of re-fetching every
    page forever.

  • The auto-disabled notice for full-content extraction now states why it was turned
    off — the feed already delivers full articles, or the site blocked extraction /
    returned no readable content — instead of always claiming the site blocked it.

  • The article view no longer flickers an endless "Extracting full content…" spinner
    for an article whose extraction failed and is waiting to retry; it shows the feed
    content quietly, and the spinner appears only while a first attempt is in flight.

  • "Extract full content" from the article menu no longer momentarily drops the
    article's star, archive, or label state from the action bar.

v0.10.1

Choose a tag to compare

@jakublibik jakublibik released this 27 Jun 08:39

Added

  • One-command local demo: docker compose -f docker-compose.demo.yml up brings the
    app up on http://localhost:8000 with a seeded admin and no setup wizard, for
    trying it out before a full install. Demo only — plain HTTP, DEBUG=true, and
    hard-coded throwaway secrets; not for production. See README → Quick demo.

Fixed

  • Infinite scroll in unread/label views could stop early or silently skip articles
    when rows were marked read while scrolling (the unread set shrank under the
    numeric page offset). The article list now uses keyset (cursor) pagination, so
    scrolling reliably loads every remaining article regardless of mark-read-on-scroll.
  • Mobile: the active tab in the horizontal side-nav strip now scrolls into view on
    load, instead of staying off-screen when the strip was left scrolled elsewhere.
  • Docker: the db healthcheck now probes the actual database (pg_isready -d),
    so a DB_USER that differs from DB_NAME no longer logs a Postgres FATAL on
    every check.

v0.10.0

Choose a tag to compare

@jakublibik jakublibik released this 25 Jun 15:05

Added

  • In-app feedback / bug report: a "Send feedback" item in the user menu opens a
    form (type, subject, message) that emails all admins via the configured SMTP,
    with Reply-To set to the sender's account email. Off by default; admins enable
    it in Admin → Settings (requires SMTP).
  • AI error badge: a red dot on the user menu and the Settings → AI nav item when a
    background AI call (e.g. scoring) last failed, so credit/quota errors are visible
    without opening Settings. Self-clears on the next successful AI call, or dismiss it
    manually via the × on the error panel in Settings → AI.
  • Filter action archive: alongside label / mark-as-read / star, a filter can now
    archive matching articles (removes them from the inbox and exempts them from
    retention purge). Available in Settings → Filters and via OPML round-trip.

Changed

  • Stats: the single "Backlog" figure is split into labeled backlog (unread items
    carrying a label) and starred backlog (your read-later pile); both are now
    all-time rather than capped at 90 days. Reading streak, per-day reads and the most
    active hour are computed in your own timezone instead of UTC.
  • OPML import: Tiny Tiny RSS filter scope (feed / category) is now matched by name and
    mapped to the corresponding Readfine feed/folder scope, instead of being dropped and
    imported as global. Mixed scoped/global filters still import as global with a warning.
  • Admin → Settings: the SMTP test now shows the underlying error detail on failure,
    making misconfiguration easier to diagnose.

Fixed

  • Stats: corrected the engagement funnel bars.
  • Mobile: the collapsible sidebar reliably reappears after a refresh instead of
    occasionally staying hidden.
  • Favicon: app pages now declare the raster apple-touch-icon, so Firefox/Android use
    it for link previews and home-screen tiles instead of rasterizing the SVG.

Security

  • Migrated JWT handling from the unmaintained python-jose to PyJWT.

v0.9.0 — first public release

Choose a tag to compare

@jakublibik jakublibik released this 20 Jun 10:34

First public release. Self-hosted RSS reader — try the hosted instance at readfine.app or run your own with the steps below.

  • RSS/Atom feeds and web-scraping feeds (CSS selectors), folders, scheduled fetching
  • Readable extraction (trafilatura → readability-lxml fallback)
  • Adaptive reading UI (HTMX + Tailwind): choose a 2- or 3-panel layout separately for medium and large screens, plus list density (compact / comfortable / summary), mark-read-on-scroll, configurable sidebar behavior on small screens, and dark mode
  • Article states (read / starred / archived), labels
  • Filters (conditions → actions, regex, AND/OR, feed/folder scoping) with retroactive apply
  • AI summaries, relevance scoring, chat over articles, and Catch me up & briefings (Anthropic / OpenAI / Gemini, bring-your-own-key)
  • Per-user settings, admin panel, SMTP, API tokens (JWT), tiered retention/purge
  • OPML import/export, including web-scraping feeds (round-trips via custom outline attributes) and Tiny Tiny RSS compatibility
  • /healthz endpoint (lightweight DB ping, GET + HEAD) for uptime/monitoring probes
  • backup.sh — off-site PostgreSQL backups via pg_dump + restic (encrypted, deduplicated, retention), with a Cloudflare R2 example config. See README → Backups.

Notes for self-hosters:

  • Registration is closed by default on a fresh install — only the admin account exists; enable sign-ups in the admin panel to open the instance.
  • Shell scripts are pinned to LF line endings (.gitattributes) so setup.sh runs correctly when the repo is cloned/unzipped on Windows.

See CHANGELOG.md and README.md for full details.