October 17, 2026
Juhandu v0.4.0
Added
- VisibleUI Each feed has a tier, a folder, and a check interval, set when it is added and changeable later. The add box on Reading has a Folder box and a menu for who may read the feed’s text: Public lets a hosted model read it, and Private keeps it to local models. The Feeds screen labels private feeds and gives every feed an Edit control for its tier, its folder, and how often it is checked, anywhere from every 5 minutes to once a day. Items follow their feed’s tier right away, so once a feed is private, hosted models stop seeing its text. In the API,
PATCH /api/sources/<id>takesfolder,tier, andpollMinutes(a whole number from 5 to 1440), and fields left out stay as they were. - VisibleCompatibility An iCalendar (.ics) address can be added like any feed. Each event becomes an item named after its summary, and the item’s text holds the start time, the place, and the description. Start times with no zone are read as UTC. Items are dated when Juhandu fetched them, so an upcoming event shows up as new. Reminder (alarm) text is dropped, and only http and https event links are kept. A calendar address with a private token in it is added as a private link, so the token never ends up as an item link. Trakt’s personal shows calendar is one that works.
- VisibleUI Rules can now match YouTube videos by length with the new Video length condition. You set it to shorter or longer than a number of minutes, from 0.1 to 10080, so a hide rule for videos under 2 minutes keeps Shorts-length clips out of Reading. The condition works only on YouTube videos from public feeds, and it never matches items from a private feed, live streams, removed or private videos, or any video whose length cannot be found. A video whose length is not known yet stays undecided until a later check, and the rule preview counts only videos with a known length. Juhandu reads each length from the video’s own page, or from the YouTube Data API when
JUHANDU_YOUTUBE_API_KEYor theyoutube/keysecret is set, andJUHANDU_DURATIONS=offturns the lookups off. In the API the condition is{"type":"duration","op":"under","seconds":120}, or"op":"over"for longer videos. - VisibleUI The new Watch screen shows the next episode to watch for each show you follow on Trakt. Each show has its episode number and title, a progress bar of the aired episodes you have seen, a countdown when the episode is not out yet, and where to watch it in your country, with your own services first. Juhandu only reads from Trakt and never marks anything watched. The screen needs a Trakt app’s client id in
JUHANDU_TRAKT_CLIENT_ID, and where to watch also needs a TMDB read token inJUHANDU_TMDB_READ_TOKEN.
Changed
- VisibleUI The open app checks for new items every five minutes instead of every minute when the browser asks to save data.
- VisibleSecurity Regular expressions in rules and in pipe filter and rewrite steps cannot use backreferences, lookahead, or lookbehind. Saving a rule or pipe with
\1,\k<name>,(?=...),(?!...),(?<=...), or(?<!...)fails with a message naming the construct, and a stored pattern that already uses one never matches. Every other pattern runs on a linear-time engine, so one with nested repeats such as(a+)+$finishes in milliseconds, and groups, alternation, character classes, and named groups work as before. - VisibleSecurity Feed fetches now follow redirects one hop at a time, and a feed or site at a public address can no longer redirect a fetch to this machine or a private network. The fetch stops with an error when a redirect points at loopback, 10.x, 172.16-31.x, 192.168.x, link-local addresses such as 169.254.x, shared 100.64.x, IPv6 local ranges, or names such as localhost, .local, .internal, or a bare single-word host, and when you add a page, links to those addresses on a public page are skipped during feed discovery. An address you type in yourself can still be on your own network, so feeds from a home server keep working, but only numeric addresses and those reserved names are checked, so a public name that resolves to a private address is not caught.
- VisibleSecurity Off-machine backups are encrypted before they leave the Docker host.
scripts/backup-to-r2.shencrypts each copy with the passphrase inJUHANDU_BACKUP_PASSPHRASEand uploads it as a.sqlite.gz.encfile, and without a passphrase it stops before uploading anything unlessJUHANDU_BACKUP_ENCRYPT=offis set. A restore reads both the encrypted backups and the older plain ones. - VisibleSecurity A merged story line, the one summary shown for several items about the same story, is now private when any item in the story is private, even one already read or outside the briefing’s time window. Email, Telegram, and Discord digests and the MCP briefing leave the line out unless that channel allows private items, and the app still shows it. Briefings saved before this change treat every story line as private.
- VisibleSecurity The JSON API’s Origin check now fills in the scheme’s default port before comparing ports. An Origin must use the port named in the Host, or port 80 or 443 when the Host names none, so a page at http://host no longer passes as the same site as a server on another port. The mark_read MCP tool no longer accepts item ids above 2147483648.
- VisibleSecurity Removing a stored secret reports an error when a source the app cannot change, such as an environment variable, still provides it. Before, the secret looked removed while that source kept supplying it.
- VisibleSecurity The Jev gateway token can be kept in the secret store as
jev/gateway_token. It fillsJUHANDU_JEV_GATEWAY_TOKENwhen neither that variable norJUHANDU_JEV_GATEWAY_TOKEN_FILEis set. - VisibleSecurity Juhandu now refuses a feed address, a redirect, or a feed link found on a page when its host name resolves to a private or reserved address. Before, only literal addresses such as 10.0.0.1 were refused, so a name like 10.0.0.1.nip.io or an internal hostname got through. A name that does not resolve still fails the usual way. An address that changes between the name lookup and the connection is not caught.
- VisibleSecurity The /mcp endpoint now rejects a request whose Origin is a local page on a different port from the server. Before, any loopback Origin was accepted. A page on the server’s own port still works, and so does a request with no Origin.
- VisibleSecurity The MCP list_rules tool now leaves out rules scoped to a private feed unless the MCP connection is allowed to see private feeds. A rule like that names its feed and can quote the feed’s text.
- VisibleSecurity A rule sentence drafted from your votes is now embedded at the private tier when any voted item behind it is private. That sentence goes only to a private (local) embedding endpoint. Before, it was always embedded at the public tier.
- InternalSecurity The XML parser no longer rescans its whole open-element stack for each closing tag that matches no open element. A document with thousands of stray closing tags now parses in linear time.
- VisibleSecurity The embedding endpoint must be on this machine, because embeddings carry item text and rule sentences. It counts as local at a loopback address or with
JUHANDU_EMBED_LOCAL=1, and at any other address embeddings stay off and the start-up log says so.