Repository navigation
Releases: neostryder/juhandu
Release list
Juhandu 0.5.0
Changed
- [Visible] [Compatibility] Juhandu now finds Cogitator's discovery file in
D:\Cogitatorbefore the older%LOCALAPPDATA%\Cogitatorfolder.JUHANDU_COGITATOR_HOMEnames any other install folder and is checked first, so a later move needs a setting instead of an update. - [Visible] [Compatibility] Juhandu now waits out a Cogitator request that a person's own request pushed aside. When Cogitator answers 503 with the code
preempted, Juhandu waits theRetry-Aftertime (20 seconds when there is none) and sends the same request again, for background work as well as interactive work. It does not count the wait as a failure, and after two minutes of it the work stays queued for the next cycle. - [Visible] [Compatibility] Juhandu no longer falls back to Cogitator's retired
embedrole. Embeddings come from theembed-gemma2role only, so a gateway that has just the old role leaves embedding work queued instead of getting vectors from a model that is gone. - [Visible] [Compatibility] A plain embedding endpoint now defaults to the model name and tag
embeddinggemma-2, where it used to default to Nomic. An endpoint that still serves Nomic needsJUHANDU_EMBED_MODELandJUHANDU_EMBED_TAGset to the old names, or its items are embedded again under the new tag.
Juhandu 0.4.0
Added
- [Visible] [UI] 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. - [Visible] [Compatibility] 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.
- [Visible] [UI] 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. - [Visible] [UI] 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
- [Visible] [UI] The open app checks for new items every five minutes instead of every minute when the browser asks to save data.
- [Visible] [Security] 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. - [Visible] [Security] 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.
- [Visible] [Security] 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. - [Visible] [Security] 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.
- [Visible] [Security] 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.
- [Visible] [Security] 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.
- [Visible] [Security] 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. - [Visible] [Security] 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.
- [Visible] [Security] 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.
- [Visible] [Security] 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.
- [Visible] [Security] 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.
- [Internal] [Security] 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.
- [Visible] [Security] 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.
Juhandu 0.3.0
Added
- [Visible] [UI] Reading can be triaged by swipe or keyboard. On a touch screen, swiping an item right Likes it and swiping left Dislikes it, and a swipe starts only on the card body, never on its buttons or links. On a keyboard, j and k move between items, l likes, d dislikes, o opens, and r refreshes. The shortcuts button in the Reading toolbar turns both the keys and the swipes off or back on.
- [Visible] [UI] Pulling down at the top of Reading on a phone refreshes it. While the app is open it also checks for new items every minute and shows a "N new. Show them" pill, so the list stays put under the item being read.
- [Visible] [UI] The home-screen icon shows the unread count and has shortcuts. On systems that support app badges, the installed icon carries the number of unread items. A long press on it opens Briefing, Reading, Filtered out, or Add a feed.
- [Visible] [UI] Juhandu appears in the phone's share sheet. Sharing a link to it opens Add a feed with the address already filled in.
- [Visible] [Compatibility] The
jevjudge can send its requests through an Augur Jev gateway. WithJUHANDU_JEV_GATEWAY_URLandJUHANDU_JEV_GATEWAY_TOKEN(orJUHANDU_JEV_GATEWAY_TOKEN_FILE) set, every Jev request goes to the gateway first, and private items are marked sensitive so the gateway keeps them off hosted Jev. A request goes straight to hosted Jev only when the gateway cannot be reached or fails on its own and a direct key is set, and never after the gateway refuses it. The gateway alone is enough to turn on thejevjudge. - [Visible] [Backup] A nightly copy of the database can go to an S3-compatible bucket such as Cloudflare R2.
scripts/backup-to-r2.shruns on the Docker host, takes a consistent copy while Juhandu keeps running, and can download the newest backup for a restore.
Changed
-
[Visible] [UI] Reading shows 40 items at a time and loads more while scrolling. The next 40 arrive as the list nears the bottom, and "That is everything." marks the end. Rows outside the screen are not drawn, so long lists scroll smoothly.
-
[Visible] [UI] The app opens faster. The Rules, Feeds, Pipes, and Connect screens load when first opened, and they are fetched quietly in the background after start. The scripts the first screen needs are preloaded, and the web fonts no longer hold up the first paint.
-
[Visible] [UI] Every screen fits a phone without sideways scrolling. The browser's own pull-to-refresh and bounce no longer compete with the app's, the tap highlight is gone, and the Getting started links are easier to tap.
-
[Visible] [UI] Juhandu has a new home-screen and browser-tab icon, a golden herald's horn with a burgundy banner. The icon is a photographic image in 192 and 512 pixel sizes, with a separate maskable version that fits Android's circular and rounded crops, and it replaces the drawn seal icon and the script that generated it. A phone fetches a linked icon without the sign-in cookie, so behind an access proxy the old icon links got a login page back and the home screen showed a plain letter tile; the page now carries its small icons inside itself, so they always load.
-
[Visible] [Compatibility] Juhandu embeds text with EmbeddingGemma 2 through Cogitator's
embed-gemma2role. A gateway without that role gets the requests on itsembedrole (Nomic Embed Text v1.5), as before. Vectors from different models are never compared, so the first run after the upgrade embeds every item again in the background. Each item keeps its old vector until the new one is ready, and nothing waits on the work. Each model now gets the prefixes it was trained with: EmbeddingGemma 2 puts "title: none | text: " before items and "task: search result | query: " before rule sentences and examples, and other models keep "clustering: " for both.JUHANDU_EMBED_DOC_PREFIXand the newJUHANDU_EMBED_QUERY_PREFIXoverride them, and the newJUHANDU_COGITATOR_EMBED_ROLEnames the role that embeds text. Two cutoffs moved so they let through about as many pairs as before. Two items now need a similarity of 0.82 (was 0.8) before the judge is asked whether they are the same story, set byJUHANDU_MERGE_SIMILARITY, and two voted items count as the same topic for rule proposals at 0.75 (was 0.72).
Fixed
- [Visible] [Compatibility] Embeddings answered by another machine in a Cogitator role's chain are accepted. Cogitator can name the serving model with its machine attached, such as
embeddinggemma-2@bilbo, and Juhandu used to read that as a different model and discard the vectors. It now ignores the machine name. - [Visible] [Compatibility] A single private or restricted YouTube video no longer pauses Gemini for an hour. When Google answers a video request with a 403, Juhandu checks the key with a plain request that names no video, and pauses Gemini only if that request is refused too; otherwise only that video is marked as one Gemini cannot read, and the next video goes to Gemini as usual. A 401 or an invalid-key 400 still pauses Gemini at once.
- [Visible] [Compatibility] Screens loaded on demand now carry the version in their address. Before this, an upgrade could leave a cache serving the old copy of a screen.
- [Visible] [Compatibility] The home-screen manifest is now requested with the sign-in cookie. Behind an access proxy, the phone now finds the app's name and icon instead of a login page.
- [Visible] [Security] Assistant write tools only change items the assistant can read. The vote_item and mark_read MCP tools leave items from private feeds untouched unless private feeds are turned on for the assistant.
- [Visible] [Security] Rule proposal drafts go to the model at the private tier whenever they include a title from a private feed. Every liked or disliked title in the request now sets the tier, where before only the items being clustered did.
- [Visible] [Security] Removing a stored secret clears it from every backend, including an older key file kept beside the current store. The removal reports an error if any backend could not delete the secret. Removing a feed deletes its stored login first and keeps the feed if that step fails, so the owner can retry.
- [Visible] [Security] The JSON API now compares the port as well as the host name when it checks a request's Origin. A request whose Origin names a different port than the Host it was sent to counts as cross-site, even when that host name is allowed. Setups behind a proxy on port 80 or 443 behave as before.
Juhandu 0.2.1
Juhandu can use a Cogitator gateway on another machine. Requests used to go to the loopback address in the gateway's discovery document and were refused, so items never got summaries. Juhandu now sends them to the host it reached the gateway at.
The full list of changes is in CHANGELOG.md.
Juhandu 0.2.0
Juhandu 0.2.0 runs in Docker, and models and video reading are now set up from the web app with no restart. This release also fixes two security problems, so anyone running 0.1.0 should upgrade.
Still no sign-in
Juhandu has no accounts yet, so anyone who can reach the port can read your feeds and change your rules. Keep it on 127.0.0.1, or put an authenticating reverse proxy in front of it. SECURITY.md has the details.
New in 0.2.0
- A
Dockerfileandcompose.yamlrun Juhandu as an unprivileged user, with the database and secrets in a data volume, a health check on/api/health, and the port published on 127.0.0.1 only. - The Connect screen saves chat and embedding endpoints, the Gemini key, and the speech and vision servers, each with a Test button. Keys go to the secret store.
- A Getting started checklist on the Briefing and Reading screens links each first step to the screen where it happens.
- The briefing can arrive as a direct message from a Discord bot, as well as through a channel webhook.
- Keyword and regular expression rules can search an item's link. A rule matching
youtube.com/shorts/hides YouTube Shorts.
Fixes
The MCP endpoint used to treat a request that came through a proxy as local. Without an access token, such a request is now refused. OPML import used to store a login found inside a feed address in the clear; that feed now becomes a masked private link, with the real address kept in the secret store. Paging the reading list with before now visits every item once instead of repeating or skipping some.
Upgrading
From a git checkout, pull and restart. With Docker, rebuild the image and recreate the container, and keep the data volume, which holds your database and secrets. Never remove it.
Known gaps
A feed's tier and folder still cannot be changed after it is added. That is tracked in issue #1.
The full list of changes is in CHANGELOG.md.
Juhandu 0.1.0
Juhandu is a self-hosted feed reader that reads your feeds for you. It hides what your rules say to hide, learns what you like from your votes, and has a briefing ready each morning. This is its first release.
There is no sign-in yet
Juhandu has no accounts and no password. Sign-in is planned for a later release, with no date set, and until then anyone who can reach the port can read your feeds, change your rules and replace feed logins. The server listens on 127.0.0.1 by default and prints a warning at start-up if JUHANDU_HOST points it anywhere else. If you expose it to a network, put an authenticating reverse proxy such as Cloudflare Access in front of it and set JUHANDU_PUBLIC_HOST to the proxy's public name, or the web app's API calls are refused. SECURITY.md has the details.
What works
- You can add RSS, Atom and JSON feeds by feed, site or YouTube address, or import an OPML file with its folders.
- Rules hide, favor or demote items by keyword, pattern, domain or resemblance to a sentence, and the Filtered out screen shows what they hid, with a Restore button.
- Your likes and dislikes order the briefing, and a model turns votes that agree into rule proposals for you to accept.
- Each shown item gets a one-sentence summary backed by a quote that Juhandu checks against the item's text.
- The daily briefing can go out by email, Telegram or Discord.
- Pipes build and publish new feeds from your existing ones, and an MCP server lets an AI assistant read your briefing, items and rules.
The full list is in CHANGELOG.md.
Getting started
Juhandu needs Node 26 or newer and has no runtime dependencies. Run npm start in the cloned repository, open http://127.0.0.1:8787 and add a feed on the Reading screen. AI features need an OpenAI-style embedding and chat endpoint, local or hosted. Without one, Juhandu still works as a reader, and briefings and pipes show headlines in place of summaries. docs/SETUP.md covers every setting and how to connect a model.
Known gaps
A feed added on the Reading screen is public and has no folder; an OPML import or POST /api/sources can set both. A feed's tier and folder cannot be changed after it is added, and every feed is polled every 30 minutes with no setting to change that.
Report bugs in GitHub issues, and report security problems privately through a security advisory, as SECURITY.md describes.