# Changelog All notable changes to this project are documented here. This project follows [Conventional Commits](https://www.conventionalcommits.org/) and [Semantic Versioning](https://semver.org/). ## [Unreleased] ## [1.15.0] - 2026-11-08 ### Added - **Choose a person's Overseerr account by hand** ([#181](https://github.com/stevezau/shortlist/issues/181)). Shortlist links someone to Overseerr automatically when they signed in to it with their Plex account. When that doesn't fit, open the person's page, go to **Settings**, and pick their account under **Overseerr account**. You need this when they use a local Overseerr account, when they are a managed profile (which can't sign in to Overseerr), or when you request as one account and watch on another. The account you choose counts as theirs for the **Your requests** row, in place of whoever it signed in as. One account can belong to one person, and the choice survives a name change. It doesn't need Overseerr's **Tag Requests** option. On the **Users** page, **No account** and **Can't use Overseerr** are now one **Not linked** pill that opens the person's settings. The tag field is renamed **Tag on their requests in Radarr/Sonarr**. Adds migration 0115. ### Fixed - **A pick someone watched keeps its credit after the show's watched episodes are deleted** (follow-up to [#173](https://github.com/stevezau/shortlist/issues/173)). Since 1.14.0, the nightly watch sync could treat such a show as unwatched and drop a recent "watched a pick" credit. Marking the whole series unwatched in Plex still removes the credit. - **A tag that names your Request as account no longer gives one person the titles Shortlist requested.** If someone's **Tag on their requests** matched the account Shortlist requests as, they were credited with every title Shortlist asked for. - **Overseerr tags for names with no Latin letters are now read.** Overseerr tags a request from such a user as just their number (for example `12-`), and Shortlist skipped those tags. ## [1.14.0] - 2026-11-01 ### Added - **Lean towards older releases** ([#174](https://github.com/stevezau/shortlist/issues/174)). The **Recent releases** slider now runs both ways. Left of the middle (**Older**) it gives older titles a boost instead: at the far left a 40-year-old title scores about 32% higher than an equally good new one, and nothing is boosted by more than 50%. The middle still ignores age and the right side works as before, so no saved value changes meaning. It is a preference, not a filter: it only reorders titles your sources already found, and a show's age is its first-air year. To rule eras out, use a row's minimum and maximum year. ### Fixed - **A show you'd started no longer comes back as new after its watched episodes are deleted** ([#173](https://github.com/stevezau/shortlist/issues/173)). When the episodes someone watched are removed from the library, Plex still records the show as played, but reports none of the remaining episodes as watched. Shortlist only looked at the episodes, so a row set to **Already-watched titles: 0%** or **Only series they have not started** could recommend that show again. Shortlist now also counts the show's own play record. To make a show count as unstarted again, use **Mark as unwatched** on the whole series in Plex. That includes a show whose Agregarr trailer someone played: once the trailer is cleaned up, Plex still records the play, so those rows leave the show out for that person. ## [1.13.0] - 2026-10-25 ### Added - **Choose which of a row's libraries it asks for titles for** ([#169](https://github.com/stevezau/shortlist/issues/169)). In the row editor's **Requests** section, **Which libraries get requests** lists the row's libraries, each with a switch. Turn TV Shows off and the row still asks for films but never for shows. Every library is on by default, so nothing changes until you turn one off, and a library you add to Plex later starts on. Titles already waiting in your inbox stay there until you send or dismiss them. Adds migration 0114. ### Fixed - **A watch date in the future no longer pins a person's rows to one title** ([#162](https://github.com/stevezau/shortlist/issues/162)). Plex can hold a last-watched date years ahead of today. Shortlist took it as the person's newest watch, so that title led their row every night and everything they really watched read as years old on the run page. A date more than a day ahead is now treated as unknown: a show takes the date of its newest watched episode instead, and the bad date is replaced at the next watch sync. - **"How we picked" now says how long before the run each watch was.** It used to count back from the person's newest watch, so someone who hadn't watched anything for a week saw every title a week short, and their last watch read "watched most recently" rather than "7 days ago". Runs from before this update keep the old labels. ## [1.12.2] - 2026-10-21 ### Fixed - **A row in two libraries of the same type no longer starves one of them** ([#162](https://github.com/stevezau/shortlist/issues/162)). With a TV Shows and an Anime library, a row was built once from all of a person's show watches, and its suggestions were shared out afterwards. The library holding their newest watches took every suggestion, so the other got few or none and kept last night's titles. On a Because you watched row it also kept an old watch's name, while the busy library was named after a show it doesn't hold. Each library is now built from the watches it holds, with its own suggestions, and a Because you watched row is named after one of them. A library holding none of their watches is built from all their watches of its type. Two libraries of a type the row follows no watch of (someone who watches only shows, on a row with Movies and 4K Movies) also pick from their own suggestions, with the row's watches as before. The run page's list of watches a person's rows followed now includes each library's own. Rows in one movies library and one TV library are unchanged. A library that shares its type with another, and holds watches the row follows, searches on its own, so a row using AI web search makes one AI call for each of those libraries plus one for the rest of the row. - **An AI web search shared by two rows is no longer left out of a run's AI cost when the first row fails to use it.** The next row using it now records the cost. ## [1.12.1] - 2026-10-16 ### Fixed - **In Firefox, a number changed with the box's up/down arrows is saved again** ([#160](https://github.com/stevezau/shortlist/issues/160)). Firefox's arrows change the number without selecting the box, and Shortlist only saved a number when you left the box, so Save kept the old value: **Take turns between their last N watches** went back to 1, and the same happened to the blend count, row size, limits and the refresh and hold days. Typing a number still waits until you leave the box or press Enter. - **Backups are smaller.** A backup used to copy the database's empty space too, which on a busy server can be a large part of the file. Each backup is now compacted once it is written, so the 10 backups Shortlist keeps take less disk. Nothing in a backup changes, and restoring works as before. ## [1.12.0] - 2026-10-12 ### Added - **AI web search can look further back in someone's history** ([#152](https://github.com/stevezau/shortlist/issues/152)). Until now it only looked at what they watched lately. **Settings, Defaults, How much of their history it searches from** adds their long-time favourites and older watches from across the years, as presets from Recent only (the default, nothing changes) to Deep. A row or one person on a row can set their own, a person's page has **Show this week's mix** with a **Don't use** button on each title, and **How we picked** shows what each search was for. With Exa or SearXNG each extra title is one more search; with the AI's own search there is no extra cost. A title they rated at or below the "didn't like it" line in any library is never used as a favourite. Adds migration 0112. - **Choose how many seasons of a show to request, on either route** ([#154](https://github.com/stevezau/shortlist/issues/154)). **How much of a show to grab** now has a **Start from** (First or Last seasons) and a **How many seasons** count, or **Every season**, and it works when requests go through Overseerr, Jellyseerr or Seerr as well as straight to Sonarr. Rows can set their own. **If you request through Overseerr, check this setting after upgrading:** a First or Last Season choice made for Sonarr used to be ignored there (every season was requested), and now applies. Pilot episode and Add unmonitored stay Sonarr-only; through Overseerr a show set to one of them is not sent, and waits in the inbox with the reason until you pick another choice. Rows already set to First or Last Season keep taking one season. On both routes, Last seasons skips a season that has been announced but hasn't started airing, so a request downloads something now. Adds migration 0113. ### Fixed - **A request that failed before anything was sent no longer blocks that title for good.** If TMDB couldn't be reached to find a show's TheTVDB id, or Sonarr or Radarr failed while looking a title up, the request was treated as possibly sent and never tried again until you released it on the Requests page. It now waits in the inbox with the reason and is tried again next run. A send that may really have reached Sonarr or Radarr is still held back for you to check. A title that would be refused again the next night (Sonarr or Radarr can't find it, TMDB lists no seasons yet, or a mode Overseerr can't take) waits in the inbox and is only sent automatically again after a week, or sooner if you change where or how it is requested, so it never takes an auto-send slot from the titles below it. You can still send it from the inbox at any time. - **A SearXNG search whose engines were all failing no longer counts as "nothing found"** ([#152](https://github.com/stevezau/shortlist/issues/152)). An empty page from blocked or rate-limited engines was cached for a week as a real empty answer; it is now a failed search, retried next run. - **A local Ollama model that answered AI web search in prose now says why** ([#152](https://github.com/stevezau/shortlist/issues/152)). Ollama's default context window cuts off the start of Shortlist's prompt; the run trace and Settings Test now point to `OLLAMA_CONTEXT_LENGTH=16384`. - **AI web search titles now reach rows** ([#152](https://github.com/stevezau/shortlist/issues/152)). The titles the AI found were ranked below every title TMDB suggested, so they almost never made it into a row: in one 46-person run, 65 of 92 "Picked for You" rows held none. Each is now credited to the watches that led to it, so it competes with those watches' similar titles (with Exa or SearXNG; the AI's own search is unchanged, as crediting it made rows less accurate). With SearXNG the AI is asked to name the watched title behind each proposal, a few extra output tokens. **Run details** shows how much of a run the AI search supplied. - **Agregarr trailer placeholders no longer count as the real title** ([#151](https://github.com/stevezau/shortlist/issues/151)). Playing one of Agregarr's stand-in trailers used to count as watching the film, and as finishing a show that wasn't on the server yet. A title with both a real copy and a trailer could also put the trailer in a row. Trailer plays are now ignored and rows use the real copy. A title that only has a trailer can still go in a row, and playing its trailer no longer counts as watching that pick in the reports. - **AI web search no longer describes titles it shouldn't** ([#152](https://github.com/stevezau/shortlist/issues/152)). The list of someone's recent watches sent to the AI now leaves out titles marked "Don't seed" and titles they rated at or below the "didn't like it" line (when "Respect Plex ratings" is on). A row pinned to some libraries now describes only what was watched in them, as its seeds already did. - **Adding a season to a row is quicker and keeps your place.** The Add button no longer waits for the whole rows list to reload, which took seconds while a run was going, and the page no longer jumps up to the season list afterwards. - **An upgrade no longer stops on one unreadable history record.** Upgrading to 1.11 refused to start if a run, job or activity-log entry held text that was not valid JSON (only possible after a hand edit). It is now skipped with a warning in the log. ## [1.11.0] - 2026-10-10 Back up `/config` before upgrading: this release adds database migrations 0102 to 0111. No settings need changing, and assistant access stays off until you opt in. ### Added - **Assistant access (MCP, #141).** Optional, and off until `SHORTLIST_MCP_URL` is set. Connect a local or hosted AI assistant that can view Shortlist or manage its supported settings, rows and saved configuration. You approve each named connection yourself, and the assistant never sees the owner API token. Migrations 0103 to 0108 add the tables for its connections, approvals, budgets and request history. See [Connect an assistant](https://shortlistapp.dev/guides/assistant-access/). - **Hold requests by genre or TMDB tag.** **Settings, Requests** has a new "Don't request these automatically" filter: movies with a chosen genre or TMDB tag wait in the inbox for you to approve instead of being sent on their own. - **More seasonal presets**, and a simpler way to add a season to a row. - **Requests has its own Settings tab.** Settings now has four tabs. Changed settings are marked with their default and a reset. - **Warnings view in Activity.** The log can be filtered to warnings, with context, and warning lines are tinted. - **A per-user Kometa guide** on the docs site. - **A webhook event for a skipped scheduled job** (`job.skipped`). Webhook events can now be chosen before an address is saved; only the on switch waits for one. - **Pick reasons name what the picks share.** "Because you watched Dune — more sci-fi and action", and the person page groups a person's picks by reason. - **Dashboard.** "Last run" sums the night's scheduled runs, Finished says how many are still going, and both bar charts say which shade is finished and which is still going. Durations of an hour or more read as hours and minutes. - **First-run wizard** has a TMDB key walkthrough, and its last step shows the rows it built. ### Changed - **Pause all users moved** from the Danger zone to **Settings, System, Run speed**. Only the full uninstall stays in the Danger zone. - **Assistant consent** names the read-only scopes it asks for, and access is a simple choice of view or manage. - **Numbers agree across pages.** People watching counts enabled people only, a person's header counts shared-row watches like the dashboard does, and shared rows say they are one copy for everyone. The Requests figures count only the titles that really reached the inbox. - **Redesigned screens.** The dashboard, rows list, row editor, **Add a row** (now its own page), Users and person page, Privacy, Runs and run detail, Activity, Settings and the first-run wizard were reworked. Runs show one verdict ("OK · N warnings") everywhere, and the Privacy page leads with its enforcement check and a who-sees-what grid. Breadcrumb headers, plain-word row names and a 12px text floor apply throughout. - **Requests page.** Each waiting request has one Send action, with Reject and Dismiss in its menu. Waiting requests sit in one divided card, and the bulk bar shows how many are selected. - **Jobs page** shows each job's details in plain English. - **The Plex card** in **Settings, Connections** has a "Find your token" link. - **Linking a Plex server checks the address.** A Plex URL that is not http or https, or that points at a cloud-metadata address, is refused. - **The uninstall preview** is grouped by library, person and row, and names a shared row "Shared row". - **Confirmed row membership is kept separately from run logs** (migration 0102), so clearing run history no longer loses what a row delivered or where a watch is credited. - **Deleted row ids are never reused** (migration 0109). It seeds the counter above every id that grants and history still refer to, so a new row can never inherit an old one's access. ### Fixed - A watch is credited to the verified Plex owner, for both live watches and completed history. - Shared rows render their configured poster, and retried rows keep their delivery evidence. - Row names fill `{user}` instead of stripping it. - The requests-row check redacts credentials in a source failure before logging or returning it. - Privacy never shows a green all-clear above an exposure: the Off banner, the exposed-rows badge, the first-run finish and the person page all follow the engine's reading, including during a plex.tv outage. - A run that could not save or check someone's hide rules reads as a warning. - The Most watched shelf stays inside its scroll area on a phone. - A schedule rebuild no longer drops that night's past-due row run. - A scheduled job that is skipped is reported (webhook and bell) instead of vanishing. - Watch sessions left open by a restart close as `orphaned` instead of `timeout`. - Web search no longer caches a SearXNG answer that was not JSON. An unusable Exa answer is cached for an hour, and the run's trace lists the seeds that failed. - Retention pruning also drops the cached dashboard report, so the dashboard cannot show pruned data. - The log view widens its window once when a stretch of debug lines pushed every info line out. - Tab strips keep the active tab in view, small controls have larger touch targets, and the rows, Users and Privacy pages read cleanly from 320px wide up. ### Removed - **`GET /api/report/engagement` no longer returns `losing`, `stop_points` or `observed`.** Nothing in the app read them; `people` is unchanged. - **`GET /api/system/syncs` is gone.** Nothing in the app used it. ## [1.10.0] - 2026-10-05 ### Added - **An AI row: describe a row in plain words.** In **Add a row**, pick the AI filter and the **Describe a row** template, write what you want ("slow-burn heist films", "cosy mysteries for a rainy Sunday"), and the AI builds a theme once: a name, rules, TMDB tags and genres, and about 60 named titles with a one-line reason each. Shortlist checks every title against TMDB, your library and the row's limits. After that, every run fills the row for each person from that theme with no AI, ranked by their own taste. The editor has **Write the list**, **Adjust the list** (shows a diff, and works before the row is saved), **Try it** (a practice run for one person that writes nothing to Plex, and works on a switched-off row), **Advanced: how the AI is instructed** (your guidance, plus the locked mechanics), usage per row, and **Pause AI for this row**. A new AI row goes live like any other row. Per-person rows only, and library-only: a title your server lacks is never requested. With no AI provider you can still hand-edit the tags, genres and limits. See [An AI row](https://shortlistapp.dev/guides/ai/#an-ai-row). (#138) - **Explore: an AI row that picks a new theme every few days (#138).** In an AI row's editor, choose **Pick a new theme every few days** and each person gets their own theme, chosen from what they watch (or from a brief you write), for 7 days by default. The next theme is written a day early and shown as **Up next**, where you can **Change it** or **Pick another**; the last six are listed as **Recent themes**. A new **Pick new row themes** job (setting `themes.rotate_cron`, daily by default) does the switching. One AI call per person per change. Every AI row starts on **Keep the same theme**, so nothing changes on upgrade. - **An AI row only covers the kinds of title its AI named (#138).** A films-only list no longer fills your TV libraries with genre filler on a row that covers both. If a change or an Explore theme drops a kind, the row's existing collection in those libraries is removed on its next run. - **An AI row's list is topped up once (#138).** When someone has watched most of the titles the AI named and their row has started filling with tag and genre matches, the nightly **Pick new row themes** job asks the AI once for about 40 more titles for that theme and adds them, keeping what the list already has. Never repeated for a theme (a night the AI doesn't answer doesn't count), and the theme card says when it happened. - **Over-time controls on AI rows (#138).** **How much changes each time** (a fifth, a third by default, half, or almost everything), **Don't repeat a title for N days** (off by default), and **Keep out titles already in** other per-person rows. See [Explore](https://shortlistapp.dev/guides/ai/#explore-a-new-theme-every-few-days). - **AI instructions: tell AI web search what to look for**, server-wide in Settings → Defaults → Title sources and per row under What goes in. A row can use the default, add to it, or write its own. See [AI instructions](https://shortlistapp.dev/guides/rows/what-goes-in/#ai-instructions). - **A "Your requests" row.** Choose _Your requests_ as a row's kind and each person gets a private row of the titles they asked for in Overseerr (or Jellyseerr), or that carry their requester tag in Radarr or Sonarr, once the title is on Plex and until they have watched it. Newest arrival first, no AI, no padding; a person with nothing ready has no row. The Users page gains a **Requests** column saying whether each person is linked to an Overseerr account, and the row editor shows where requests are read from and whether Overseerr's Tag Requests is on. (#127) - **Length, year and rating limits on a row.** Under What goes in, a row can leave out titles longer than a set number of minutes, released outside a range of years, or rated below a minimum. All are off by default, and a title TMDB has no data for (or that TMDB failed to answer for) is kept. They also apply to Watch it again rows and the popular-on-this-server fallback. See [Length, year and rating limits](https://shortlistapp.dev/guides/rows/what-goes-in/#length-year-and-rating-limits). (#138) ### Changed - **A run reports "OK with warnings" when an account can see other people's rows.** A run used to read plain OK while a Plex restriction profile kept someone's sharing filter from hiding the rest; the run page and the dashboard now name those accounts, and a run that could not measure privacy says so. - **Activity has a "Changes on Plex" tab** listing every write Shortlist made to your server, newest first. - **The dashboard leads with last run, next run, privacy and the Plex connection**, and a run's page opens on its rows, each with what happened for every person. - **Laptop-width and phone layouts are tidier.** The Users table switches to cards below 1280px instead of wrapping every cell, run logs read one line per entry on a phone, the uninstall preview groups repeated rows, and the website's reference tables stack into readable cards on a phone. - **On-switches are neutral, and amber is kept for each screen's one main button.** Settings text is full size, and no section draws a box inside a box. - **Picking a template is a page of its own.** The template picker shows each row kind with what it holds and who it is for, instead of a drop-down. - **The web interface has one consistent layout** across every page, with navigation that works on a phone and a row editor whose sections you can jump to. - **The Users page has a Status column** (Active, Paused, Off or Restricted) with search and filters, and shows how many different picks each person watched in the last 30 days. - **Failed web searches say how long they took, and an unreadable AI reply is kept.** The log now names the attempt that failed and how long it ran before failing. When the AI's reply to a web search could not be read as a list of titles, the whole reply is saved in that person's run details so it can be diagnosed. ### Fixed - **A dry run, or a run cancelled before someone's turn, no longer empties their user page.** The page showed "No picks in this row yet" for everyone after one cancelled test run, while Plex still held their rows; it now shows the last titles actually built. - **The Users page's Requests column, the Requests page, and the nightly run's request read are faster.** Radarr, Sonarr and Overseerr are now read at the same time instead of one after another; on a large library that took the wait from 12–25 seconds to about 8. - **A Radarr, Sonarr or Overseerr that doesn't answer is explained in plain words** ("Radarr didn't answer at http://host:7878. Check the address and that it's running.") instead of a Python error name. - **A cancelled run says it was cancelled**, instead of "nothing was due" or that it predates the run page. - **The Users page's last column says "Last run"**, not "Last built": it counts dry and cancelled runs too. - **A row run that starts late still runs.** It used to be skipped if it started more than 30 seconds late. Any other scheduled job that is skipped for starting late now shows a bell alert, "A scheduled job didn't run", and is recorded in the change log. - **Stopping or updating the container while Shortlist is open in a browser now shuts down cleanly.** It used to hang until Docker force-killed it, which also left in-progress plays marked as timed out. - **Everything a background job changes on Plex is now recorded.** Share-filter changes, rows removed and rows taken off Home by the privacy sync, restore, row-visibility and sync check jobs used to leave no event, and so did the orphaned rows the sync check deletes. Runs now also record the rows they take off Home and the orphans they delete. - **"Your requests": a tag that could belong to two different people now goes to nobody.** It used to go to whichever person matched first. A person matched by the tags of two rows now gets the title in both. - **"Your requests": an unreadable reply from Radarr, Sonarr or Overseerr no longer removes someone's row.** It is treated as a failed read, so the row is kept. - **An upgrade no longer goes ahead when its safety backup could not be written.** Shortlist stops with a message saying why, and the database is left unchanged. Free some disk space or fix the config folder's permissions, then restart. - **A half-written backup is never offered as a restore point.** A backup stopped part-way through, for example by a container stop, is now discarded instead of listed. - **A job that succeeded on a retry no longer shows its earlier error.** - **A Plex server that can't be reached is explained in plain words** instead of showing the Python error. (#139) - **The run log no longer says "existing rows are left as they are" for a person whose rows that run removed.** It says how many rows were removed. ### Security - **Updated cryptography to 50.0.2 and urllib3 to 2.8.0.** The build-tool dependency brace-expansion is also updated. ## [1.9.3] - 2026-09-27 ### Changed - **The row editor asks what kind of row you are making, and shows only that kind's settings.** The kinds are Picked for You, Because you watched, Watch it again, Seasonal and Popular on this server. Switching kind asks first and says what will change on Plex; switching back restores the row as it was. "Because you watched" rows pick how many watches they are "Based on", and "Take turns" is always shown. Request settings are switches, show only what can take effect, and warn when one person could never meet the minimum demand. Templates are grouped by kind, and "Happy to see again" is now "Watch it again". ### Fixed - **A new shared row for only some people is hidden from everyone else as soon as it is written.** It used to be listed in other people's library Collections tab until the run finished. Rows for everyone are unaffected; the same applies to people you have disabled. - **Switching a row from "each person" to "shared" no longer leaves someone's old copy behind.** A copy the save failed to remove used to stay on that person's Home for good. Each night's run now removes it. - **Rename… renames a "Because you watched X" row.** It used to report "renamed 0". Renaming such a row to a plain name now happens straight away; giving a row a "Because you watched X" name says it takes effect at the row's next run, which is when it can be filled in. - **Changing one row's days or seasons updates only that row on Plex.** It used to re-apply every row on the server. - **Deleting, disabling or narrowing a shared row clears Shortlist's record of its old collection.** The kept record could credit plays to a collection that no longer existed. - **The row editor fits a phone, uses the full page width, and its summary scrolls with the page.** The summary names the row a row sits beside instead of its internal id, and saving Settings refreshes the row list, so an editor opened afterwards no longer writes the old default row name back. - **"Because you watched X" no longer stays on an old watch (#133).** When a row's best pick came from a source that follows no watch (discover or web search), the row could miss that the person had watched something newer. It then kept the old title, and most of the old picks, night after night, and restarting or running the row by hand did not help. It now switches to the new watch on the next run. Rows whose watch had not changed but had such a pick at the top were also rebuilt from scratch every night; they now keep most of their picks as intended. - **A "Because you watched X" row no longer freezes on Plex (#133).** When someone's newest watch had no look-alikes in your library, every pick in the new row came from discover or web search, which follow no single watch, so the row had nothing to be named after. Shortlist then left the old row on Plex, old title and old titles in it, until a later watch happened to have look-alikes, while the run page showed picks that never arrived. The row is now named after the watch it was built from and updated as normal. If you set a fallback name on such a row, people with watch history now get "Because you watched X" in its place; the fallback is for people with too little history. - **A Movies and TV "Because you watched X" row names each library after its own watch.** The TV row used to borrow the film's name whenever none of its picks came from look-alikes of the show, so it only changed when the person watched a film. ## [1.9.2] - 2026-09-24 ### Added - **A run's log shows its own warnings.** Warnings and errors raised while a run works used to reach only the container log. They now appear in that run's log on the Runs page, in amber, and the log refreshes once the run finishes so its last lines are not missed. - **The run summary counts people and shared rows separately.** "47 ok" was 46 people and one shared row. - **A night that only queued titles for your approval now leaves a record.** The `run.requests` event is written whenever the run queued or sent anything, with how many of each. A night that only queued titles used to leave no trace. - **Watched titles Shortlist cannot match are reported once, with a count.** Titles Plex holds without a TMDB id cannot be used and are skipped. The warning fired once per person; it now fires once per library per run, saying how many people it affects. - **Every failed attempt of a retried job is kept.** A retried job kept only its latest error, so the first failure was lost. Each failed attempt is now recorded as a `job.attempt_failed` event, with the same error text a failed job shows. ### Fixed - **"database is locked" during the watch sync.** Reading someone's watch history kept the database locked for writing while Plex answered for their next library — 12 to 18 seconds for a large TV library — so other work waiting to write gave up. It showed as jobs failing to start during the nightly history fill, and as lost playback updates. Each library is now saved before the next one is read. - **A shared row showing twice on Home is cleaned up.** Renaming a shared row before 1.9.1 could leave a second copy of it, and everyone saw both. The next run deletes the extra copy and records the delete in the audit log. - **A row whose titles all left Plex before it was built now says which row.** If every title picked for a library was deleted from Plex before the row was written, the error named nobody. It now names the person, the row and the library. The row is still skipped that night and rebuilt on the next run. - **Seasonal picks no longer claim "in genres you watch".** Seasonal titles are chosen for the season, not for someone's genres, so that reason could be untrue. It now reads "Right for the season". - **The Jobs page no longer says a hidden seasonal row comes straight back.** That holds for a row on its day off; between seasons, the row is rebuilt the night before its next season opens. ## [1.9.1] - 2026-09-16 ### Added - **Seasonal rows that follow the calendar.** A row can follow seasons — Valentine's Day, Halloween, Christmas. In season it holds that season's films from your library, ranked for each person; between seasons it is hidden but kept, and the night before its next season it is rebuilt for that season while still hidden, so it appears on the day already filled. Pick the seasons on the row, and how many days before and after each one it shows (30 before and none after, to begin with). Start from the new **Seasonal** template. The row's name can use `{season}` and `{season_emoji}`, and follows the season it is in (#124). - **Choose what the webhook tells you, and give it a key.** The webhook sent one thing: a run failing. In Settings → Notifications you now tick what to send — runs starting, finishing, partly failing, failing or stopping; jobs starting, finishing or failing; someone able to see a row that isn't theirs; titles waiting for your approval; and a new version being out. A failed run and a privacy problem are ticked to begin with, so a webhook you already set up only gains the privacy alert. Jobs that run every few minutes, like the privacy sync, only speak up when something changed or went wrong, and no message ever names anybody. The webhook's address has moved to a **Webhook** card in Settings → Connections, beside every other service, with Send a test and Remove. Once an address is saved, the card says whether anything is being sent yet and links to Notifications, where you turn it on and choose what it sends. If your receiver needs a key (ntfy, Gotify, n8n), add a header name and value on the same card. It is sent with every message and stored encrypted; clear the name to stop sending it. ### Changed - **The API refuses a row setting it does not know.** A misspelt field sent to create or edit a row (`POST`/`PATCH /api/collections`) used to be ignored and the row saved as if nothing were wrong. It is now answered with an error naming the field. The Shortlist app itself is unaffected; this only matters if you script against the API. ### Fixed - **Renamed rows stay where you put them, and "Something else is reordering your shelf" stops blaming other tools for it.** When a row changed its name overnight — a "Because you watched…" row picking a new title — Plex kept showing Shortlist the old name in its shelf list, so Shortlist stopped recognising the row as its own and could leave it out of place. Most nights the whole shelf was also rebuilt and every row was counted as "put back", which set off the alert after three runs in a day even with Agregarr told to leave Shortlist's rows alone. Rows are now recognised by Plex's id for them rather than by name, and the alert only counts rows that were actually out of place. - **A new row keeps the request settings you gave it.** The row editor lets you set a row's own request floors and Radarr/Sonarr target before you first save it, but those were dropped on create and only stuck once you edited the row again. - **"Avoid these genres" holds at the front of a row.** When a row had more candidates than it could keep, the kept titles were re-sorted without the genre, franchise and cast dials, so an avoided genre could still lead the row. - **Renaming a shared row no longer builds a second copy of it.** The rename dropped the row's invisible marker, so the next run did not recognise the renamed row and built it again. - **The row editor stops asking which watch a shared row follows.** A shared row follows nobody's watches, but a leftover setting from before it was shared still brought the question back. - **The run's AI token tile stops repeating its total.** Web search is the only AI step now, so the "breakdown" was just the total again; it shows only when more than one step used tokens. - **Un-pausing someone on a row's day off no longer puts that row back on their Home.** A row named after a title they watched could reappear for the rest of the day when Shortlist couldn't tell which row it was, even though its schedule said to hide it that day. It is now left hidden, as the nightly run already did. ## [1.9.0] - 2026-09-14 ### Added - **See what changed after you update.** Shortlist told you when an update was out, but once you had installed it, nothing said what was in it. Now the first page you open after an update shows the release notes for every version since you last read them. If you skipped a few, you see all of them. Close it and it stays closed, in every browser, until the next release. A new install doesn't show it. - **Star on GitHub, or buy me a coffee.** The sidebar's "Support this project" link opened GitHub Sponsors, which only works if you have a GitHub account and a card to hand. It is now two links: a star, which is free and is how other Plex owners find Shortlist, and a Ko-fi coffee, which takes a guest checkout with card, Apple Pay, Google Pay or PayPal. GitHub Sponsors is still on the repo. - **A row can now take days off.** Rows were permanent fixtures: once built, one sat on everybody's Home until you switched it off or deleted it. Open a row and set **When it appears → Only on these days**, and it shows on the days you pick and is hidden the rest of the time. Two rows can cover a week between them — one on Monday, Wednesday and Friday, another filling the rest — so the Home screen is not the same every night. On its off days a row is hidden, not deleted: it keeps its titles and comes straight back on its next day without being rebuilt, so this costs a few milliseconds rather than the minutes a rebuild takes on a large TV library. Days turn over at midnight, on the server's clock. Changing them takes effect immediately rather than waiting for the night. The Rows page shows **Showing today** or **Hidden today** against any row that has a schedule, because a row that is simply missing is indistinguishable from a broken one — and some Plex apps only notice the change once you leave the Home screen and come back. A Roku re-reads it on its own; a Shield needs the nudge. (#102) - **Requests can go through Overseerr or Jellyseerr instead of straight to Radarr and Sonarr.** If you already run Overseerr, Jellyseerr or Seerr, add it in Settings → **Connections**, then set **Where requests go** to it under **Requests**. Overseerr then fetches each title with its own quality profiles and folders. Pick a **Request as** account and that account's own approval settings apply: choose one with auto-approve off, and Shortlist's requests wait in Overseerr for your yes. The Requests settings now say in one sentence what will happen to a title, instead of leaving you to work it out from three separate settings. Radarr and Sonarr stay the default, so nothing changes until you switch. (#110) - **Someone with an "allow only" restriction now sees their own row.** Plex's allow list shows only the labels or ratings it names, and a Shortlist row carries neither, so a person restricted that way could not see their own row at all. Shortlist now adds that person's own row label to their allow list, and nothing else: everything the list hid stays hidden, inside the row too. Their row is also built only from titles their restrictions let them see, so it isn't half-empty. (#115) - **A row can set its Plex description and sort title.** Two optional fields in the row editor. A **Description** is what Plex shows when someone opens the collection, and takes the same placeholders as the row's name. A **Sort title prefix** orders the row in the library's Collections tab (not on Home), and keeps working when the row is renamed. Leave either empty and Shortlist never touches that field, so a summary or sort title set in agregarr stays. (#120) - **A row can wait while its person isn't watching.** A row rebuilt for someone who has watched nothing since it was last built just swaps in more titles from the same taste, and on a large TV library every rebuild is a slow Plex write. Set a hold in Settings, or per row with **Hold when they aren't watching**, and the row waits until they watch something — but only for the number of days you set, because the row nobody watches is the one that most needs to look different. Off by default. (#109) - **Get told when a run fails.** Shortlist runs overnight, and a failed run was only visible to someone who opened the app. Turn on **Send failures to a webhook** in Settings and give it an address, and a failed run posts a message there: a Discord or Slack channel, Home Assistant, n8n, or anything else that accepts a webhook. A button sends a test first. The address is stored encrypted, because a webhook address works as a password. - **Serve Shortlist from a subpath.** Set `APP_BASE_PATH` (for example `/shortlist`) to run it behind a reverse proxy at `https://your-host/shortlist`, with no prefix-stripping in the proxy. Thanks to @jaredledvina. (#105) - **Posters where you look at titles.** Pick lists, each person's **Watched** tab and the Dashboard now show posters, read from your own Plex server, so they are the artwork your library actually has. The Dashboard's **Most watched** is a poster shelf, **Recently watched** groups titles under the day they were watched, and titles carry TMDB, IMDb and Trakt links. - **A Sharing and privacy page.** It checks every account's Plex sharing live from plex.tv, rather than trusting what Shortlist last wrote, and names any account that can see a row it shouldn't. Open it from **Users**. - **Web search with Exa no longer needs an AI provider.** Exa returns the titles its pages recommend, so it can now feed recommendations on its own. You can choose how hard it searches (Instant, Auto, Deep lite — the default — or Deep), and the hint beside each says how it performed when measured. Searches are cached for 7 days instead of 14, so a new release or a new season shows up sooner. ### Changed - **The privacy sync runs every 30 minutes by default, instead of once a night.** It is what hides everyone's rows from a Plex account you have just shared your server with. Run once a night, that account could browse every person's rows in the library's Collections tab for up to a day. A scheduled pass that finds nothing to change takes about 20 seconds, writes nothing, and is no longer listed under **Recent**; one that fails, changes something, raises a new warning, or that you caused by changing a setting still is. A failure is no longer reported once a later pass succeeds, and passes no longer pile up behind a long run. If you set your own schedule for it, yours is kept. (#119) - **The Dashboard no longer shows a row of "needs attention" chips.** Each chip repeated an alert the notification bell already lists, so the bell is now the one place to look. - **The Dashboard's watch rate counts what people watched, not what they were shown.** The old figure divided watched picks by every title a row had ever shown, most of which nobody will ever watch, so it sat under 1% whether Shortlist was working or not. It now shows, of the titles people watched, the share a row of theirs was showing at the time: on the maintainer's server, 99 of 535 over 30 days, where the old figure read 71 of 10,898. It says when it is too early to judge instead of reporting an all-clear. - **A "watch it again" row is built from what people have finished.** It used to draw from the same similar-titles pool as every other row, so someone with hundreds of finished films could get a row of two, topped up with things they had never seen. It now leads with their favourites (4 stars or more, when Plex ratings are on), then titles close to what they watch now, then whatever they have gone longest without seeing. Anything finished in the last 30 days stays out; change that with **Skip titles finished in the last**. (#114) - **The row editor is grouped around what people see.** Six groups instead of eight, in the order you'd ask the questions: how it looks on Plex, who gets it, what goes in it, when it updates, where people see it, and requests. The first puts the name, description and poster together, beside an **On Plex** card showing them filled in for a sample person. - **The Requests inbox has tabs and one toolbar.** Waiting, Sent and Rejected are tabs. One toolbar holds the movie/TV split, a search that finds a title by name or by who wanted it, filters for rating, votes and language, and sort. Anything narrowing the list shows as a chip you can remove. - **The run page says what a live run is doing,** person by person, and "waiting for Plex" while it waits its turn to write. AI tokens are shown as input and output, because output costs several times more. Someone whose rows came out the same as last night now gets a line saying why. - **The Watched tab lists each title once.** A film held in two libraries was two lines, each with a Block button that blocked both. It is now one line, with its libraries as tags beside the title and a library filter when there is more than one to choose from. (#111) - **Two rows can share a name when they build in different libraries.** A Movies-only row and a TV-only row can now both be called "{library_name} Picked For You". (#121) - **The documentation moved to [shortlistapp.dev](https://shortlistapp.dev).** ### Fixed - **Restrictions you set on someone in Plex were being switched off.** If you had set a restriction on someone — excluding a content rating like R, or an allow-list of ratings or labels — Shortlist added its hide rule to it joined with `|`. Plex reads that as "either/or", not "both", so your restriction quietly stopped applying. With an exclude rule, that person could also see every other person's row; with an allow-list, they could see the whole library. Plex stored the rule exactly as written, so the Sharing page and the check before promotion both reported the account as hidden. Measured on a real server before fixing it. Shortlist now joins its rule with `&`, which Plex applies together with yours. The first privacy pass after you update repairs every account an earlier version wrote, and a notice names the accounts it fixed. Those people may notice less on the server than before: that is your original restriction working again. Someone with an allow-list still sees their own row: see the entry on "allow only" restrictions above. One case Shortlist can't fix for you: a label with a literal `&` in its name ("Kids & Family") in someone's restrictions. Plex returns an error for their Home screen with a restriction like that, so Shortlist can't add its rule to it. You'll get a notice naming the person and the label to rename; the Sharing page says the same, and everyone else's rows carry on as normal. (#116, #115) - **Schedules with a day of the week now run on that day, not the day after.** `0 4 * * 1,4` is Mondays and Thursdays, but it ran on Tuesdays and Fridays: the scheduler library counts weekdays from Monday where cron counts from Sunday. The same slip moved every **Weekly** row from Sunday to Monday, and "mondays at 9pm" to Tuesday. Saved schedules are unchanged and simply run on the day they always said. If you shifted a day number to work around this, shift it back. `7` is now accepted as Sunday too, and a list of days now gets a description ("Every Monday and Thursday at 4:00 AM") instead of none. A schedule that sets both a day of the month and a day of the week now runs when either one matches, as cron does: `0 4 1 * 1` used to run only on a 1st that fell on a Monday. (#123) - **A person's first row is hidden from everyone else as soon as it is built, not at the end of the run.** Until Shortlist has written the exclusion onto every other account's share, a brand-new row could be seen in the library's Collections tab by the other people on your server: the row's name, which can say what the person watched, and the titles in it. That lasted until the privacy step at the end of the run, which on a large library could be an hour or more. Now someone's first row is hidden from every other account as soon as it is written, before any other row is. If plex.tv is failing, the privacy step at the end of the run still hides it, as before. People who already had rows were not affected. A first run, where everyone is new, makes more share-filter writes as a result — one round per person rather than one at the end — and the rows pause while each round is written. (#119) - **A row keeps its Plex collection when its titles change, so agregarr settings stick.** A row that lost five or more titles in a run used to be deleted and created again, because that was quicker on a large TV library. The new collection had a new Plex id, and tools that remember a collection by its id lost what you had set on it: agregarr's custom summary and sort title were wiped every time the row refreshed. Shortlist now always updates the existing collection in place. The trade is time. Plex removes titles from a collection one at a time, and on a very large TV library each removal is slow, so a nightly run there can take a lot longer than before. Nothing changes for a row whose titles did not change. While a row is being written, the run page now says what it is doing: which library, and how many titles it is adding and removing, or that it is creating a new row. (#119) - **"Something else is reordering your shelf" no longer fires because two people watched the same film.** A "Because you watched X" row is named after the film, so everyone who watched that film gets a row with the *same* name. The check that spots another tool fighting Shortlist for the shelf was counting those as one row being moved over and over — so two people sharing a film was enough to set it off, and the alert then pointed at Kometa or Agregarr, which had done nothing. It now counts each row once per pass, which is what it always meant to do. A shelf genuinely being fought over still raises it. (#106) - **"Something else is reordering your shelf" no longer fires when nothing else is.** A row rebuilt during a run got a new Plex id, and the shelf-ordering pass at the end of that same run was still working from the delivery record read before the run started. So it did not recognise the row as one you had placed by hand: it swept it to the library default instead, and the next pass — the privacy sync minutes later, or the next run — put it back where you asked. Every rebuilt row moved twice per cycle, for ever, and moving the same row over and over is exactly what Shortlist treats as proof that another tool (Kometa, Agregarr) is fighting it for the shelf. Rows now land in their configured slot in the run that rebuilds them. On the maintainer's server one run moved 70 rows and the pass twenty seconds later moved the same 70 straight back; that is now zero. If you still see the alert after this, something really is reordering your shelf. (#106) - **Rows land where you put them on the Recommended shelf again.** Plex stores each row's shelf position as a number and places a row by halving the gap between its neighbours. After about fifty moves there is no gap left, and Plex then accepts every move and applies none — in every Plex app, its own web app included. One library on the maintainer's server had been stuck like that for over a week. Shortlist now arranges the shelf by sending each row to the end in turn, the one move Plex always has room for, which also repairs a library that is already stuck. Other tools' rows move only as far as fitting Shortlist's among them requires, and keep their order relative to each other. Placing a row "before" another row, next to a collection that isn't on the shelf, or in a loop of rows that point at each other now settles too. (#106) - **A series marked watched by hand is picked up the same night.** Marking a TV series watched in Plex could leave it out of Shortlist for up to a week, because the sync read history in pieces and a show's own date can be older than its episodes'. Every sync now reads each library in full. A series marked watched is dated from its episodes, not left at the last time it was played, and that date decides how strongly it steers recommendations, so a show you finish today counts as today. Unmarking a title takes back its pick's credit that night, not a week later. (#108) - **Renaming a row onto a name Plex refuses now works.** Plex keeps a collection's name reserved after the collection is deleted and refuses to rename anything onto it, so a row named after its top title could keep the old title's name over the new title's picks while the run page showed the new name. Shortlist now frees a name a deleted collection left behind. Renaming from the row editor lists each person as renamed, waiting for the next run, or refused with the reason, instead of stopping at the first refusal. - **Restoring a backup actually restores it.** The restore copied the backup in while the app still had the database open, and the restart you were told to do then wrote the old data back over it. A restore now waits and is applied at the next start, before anything opens the database. **Jobs → Backups** shows a waiting restore with a Cancel button, and one still waiting after a day is dropped. - **A nightly run cut short by a restart finishes for the people it never reached.** When a container update restarted Shortlist part-way through a run, everyone after that point went without new rows — the same people each time. The next start now runs for them, listed on Runs as **Resumed after a restart**. Only a scheduled run from the last 20 hours is resumed, and never twice. - **Losing `secret.key` no longer destroys your saved credentials.** If `/config/secret.key` went missing or was regenerated, the next start re-encrypted every saved Plex token and API key with the new key, overwriting the only copy the old key could read, and logged it as a success. They are now left untouched, so putting the original key back still works. - **A failed Support check could write your Plex token to the log file.** The checks on the Support page removed tokens from what they showed you but not from the log line they wrote, and the log file always records at full detail, so a failed check put a live Plex token on disk and into any support bundle made after it. If you have posted a support bundle publicly, remove it or check it for `X-Plex-Token` first. - **On a server with a single Shortlist row, one bad read from Plex could delete it.** Before deleting a row it can't match to anyone, Shortlist cross-checks the read against every other row — which was skipped when there was only one row on the server, the case with nothing to check against. Every such delete is now confirmed with a second read first. - **A row Plex had stopped accepting titles for is rebuilt.** Plex occasionally breaks a collection so that it refuses every title added to it, and that person's row then stayed empty night after night. Shortlist now deletes and recreates a row in that state. - **Auto-send no longer spends its nightly slots on titles that can never be added.** A show with no TheTVDB id, or a title whose Radarr or Sonarr isn't fully set up, took a send slot, failed, and took it again the next night: on one server, 25 of 40 slots in a week. Those titles now wait in the Requests inbox with the reason. - **Going back in the setup wizard no longer resets your row name and size.** The customize step forgot what you had saved, and the next save — including **Skip for now** — wrote the defaults over it. - **AI token counts are right after a failed call.** A failed call counted the previous call's tokens again, usually another person's, which inflated the run's total. - **Web search rides out a slow or busy Exa.** A timeout or a server error from Exa lost that search outright, and on a busy run Exa also starts turning requests away; both now retry with a pause. A dead MDBList no longer uses up the whole lookup budget before giving up. ## [1.8.0] - 2026-08-26 ### Added - **Shortlist can now tell you whether its picks actually get watched.** It only ever read one signal — Plex's binary watched flag — which cannot answer either question that matters. *When* did they watch it, since a rewatch overwrites the original date? And what about the film someone is two nights into, which the flag calls unwatched right up until the credits? Shortlist now watches playback itself. A pick is credited at the moment someone presses play, a part-watch is captured when playback stops rather than waiting for the next sync, and picks delivered through shared rows count too — previously they credited nothing at all, because a shared row writes no per-person pick record to match a play against. If tracking stops working, the dashboard says so rather than quietly reporting zero. A watch still in progress is no longer called an abandonment. "Gave up part-way" is the harshest thing this app says about a pick, and the rule behind it was invisible — so it now explains itself where the claim is made, instead of leaving you to conclude the tracking is broken. - **The dashboard leads with whether your setup is working.** It had grown to six stat tiles and eleven sections, with the one number that actually judges a setup — the share of delivered picks that got watched — nowhere near the top. It now opens with whether it is working and what isn't, and every person and row links through to its own page. - **Requests can be tagged with the person they were picked for.** Radarr and Sonarr have no field for "who wanted this", only tags, so the Requests inbox's why-line never reached them. Turn it on and each request carries the wanting person's name as a second tag, so you can tell in Radarr and Sonarr who a title was added for. Off by default, and any row can opt in or out on its own. - **Settings changes now record who made them.** The audit log said what changed and when, but never who — so a value that moved with nobody owning up to it was simply unanswerable. That happened on a live server: a request threshold shifted by 0.2 between two runs and no record could say what did it. Each `settings.change` event now carries the actor (a browser session or an API token, plus a short client hint). Deliberately not the client IP: these rows are immutable and the support bundle exports them, and a LAN address is the kind of environment detail that should not end up in something people paste into public issues. - **Prefer a language when requesting.** The request pool is by definition what your library _lacks_, so on a library that already holds the popular English titles what's left missing skews non-English before any setting applies — and the rating floor then favours it further, because TMDB's audience rates anime and K-drama generously. Nightly runs ended up mostly asking Sonarr and Radarr for subtitled titles. Settings → Requests → Guardrails now has a **Language** choice, and any per-person row can override it. **Any language** is the default and behaves exactly as before, so an upgrade changes nothing. **Prefer these** keeps your languages on the normal bars and makes everything else clear a higher rating to be sent on its own — below that bar a title waits in the inbox with the reason on it rather than being dropped, so a great foreign film is still one click away. **Only these** never asks for another language at all. The higher bar has no fixed default: it follows your own minimum rating plus 1.5 and keeps following it, so a permissive 6.0 server starts at 7.5 and a strict 8.0 server at 9.5. Type a number to pin it. The ranking is deliberately unchanged — a foreign title that clears the bar still competes on merit and usually wins, because it out-rates the English titles around it. This thins the middle; it doesn't exclude a language. The inbox now shows a language chip on any title outside your languages, so a held-back one explains itself without opening TMDB. - **Choose how much of a show to grab.** Every show Shortlist requested arrived with all seasons monitored, so one pick of a long-running series started the whole back catalogue downloading that night — enough to fill a disk, and enough to spend a day's worth of indexer queries on a show nobody had watched an episode of yet. Settings → Requests now has **How much of a show to grab** on the Sonarr card, and any per-person row can override it in the row editor. The choices are Sonarr's own Add Series _Monitor_ options, passed through unchanged, so they mean exactly what they mean in Sonarr: **All Episodes** (still the default), **First Season** for a taster you can extend later, **Last Season**, **Pilot Episode**, and **None** to file the show in Sonarr without downloading anything. Anything other than **All Episodes** also switches off Sonarr's separate **Monitor New Seasons** for that show, so "first season only" still means that when season eight airs. Without it a restriction on a still-running series quietly grows back a season at a time. It applies only to shows Shortlist adds. One Sonarr already tracks is left exactly as you have it, as before. ### Fixed - **Moving your watching to a second account now copies your history exactly.** It was marking whole shows watched when you were part-way through one. If you were 400 episodes into One Piece, the new account said you had finished all 1,100 — and the same happened to any series you had not completed. On a real 50-account server, 342 of 535 watched shows were part-watched, so this was the normal outcome rather than an edge case. The cause was that Shortlist only ever knew _how many_ episodes you had watched, never _which_. It now reads your account episode by episode and copies that, so the new account matches yours: - the exact episodes of each show, not the whole show - how many times you have rewatched something, not just that you watched it - anything you are part-way through, at the same position, back in Continue Watching — these could not be carried across at all before It also **removes** anything watched on the new account that you have not watched, which is what makes the two match, and what repairs an account an earlier version over-marked. The preview lists what would be removed **by name** and asks you to confirm before anything is written. Your own account is never written to. It is now reversible: the new account's state is saved before the first write, and **Undo** puts it back exactly — rewatch counts and positions included. The one exception is an account that can't see all of your libraries, where a complete picture can't be saved; the preview tells you that before you agree to anything. Afterwards Shortlist re-reads the account and tells you what did not land, instead of reporting the writes it sent. Plex still records everything as watched _today_ — no Plex API accepts a date, and that has not changed. The writes now go oldest-first so Continue Watching still comes out in the right order, and Shortlist keeps your real dates itself, so your recommendations are unaffected. ## [1.7.0] - 2026-08-18 ### Added - **Each row can have its own Sonarr/Radarr settings.** Until now every row asked for titles under one set of rules, which is wrong the moment your rows are for different people. A kids' row and a film-buff row want different rating floors, different quality profiles, and different folders — and a run with three rows was really one pass that mixed them together. Open a row, go to **Requests**, and override what that row needs: rating and vote floors, year bounds, how many titles it may ask for, whether it sends automatically, and which quality profile and root folder it files into. Anything you leave alone is inherited from the global settings, so a row you never touch behaves exactly as it does today. Two limits stay global on purpose. **Max per run** is the ceiling for the whole run and a row can only ask for _less_ than it, never more — otherwise the one setting protecting your download queue would be a suggestion. And the rating source (plus its API key) stays global, because it is one account shared by everything. When several rows want titles in the same run, the run's slots are split evenly between them, and each row is evaluated on its own rules. A row that wants fewer than its share hands the remainder back to the others rather than wasting it. **A title two rows both want costs one slot, not two** — it is requested once, and the run says which row claimed it. - **A share filter that Plex stores but does not apply is now caught and reported** ([#88](https://github.com/stevezau/shortlist/discussions/88)). Shortlist keeps each row private by adding label exclusions to everyone else's Plex share, and every check it had said the same thing: the exclusions were written, plex.tv stored them, and reading them back confirmed it. None of that proves Plex is _applying_ them. Six Home accounts saw all six rows anyway, and the first person to notice was a user rather than the owner. Each run now looks at one account's home screen through that account's own eyes and reports it if rows belonging to other people are showing. It never blocks anything — it measures and tells you, because nothing you change in Plex fixes it and the useful action is a bug report. ### Fixed - **Nothing was reaching Sonarr or Radarr, and nothing said why.** Titles were being examined and discarded before any request could be made, and every screen reported a confident zero. The cause was a lookup budget spent on cached answers, so a run could exhaust it without ever asking about a new title. Rating lookups are now billed only when they actually cost something, and rejections are remembered rather than re-fetched nightly. - **The screens that said "0 requested" now explain their own zero** — whether nothing cleared the bar, or titles are sitting in the queue waiting for you to approve them, which the old wording reported as if none had been good enough. - **A row's delivery no longer collapses because one title was deleted from Plex.** A title that disappears between being picked and being delivered took the whole row down with it. Runs also now report what Plex actually accepted rather than what was asked for, so a partial delivery reads as partial instead of complete. - **Copying a watching account's history moved nothing**, because the owner's own history was never read. - Approvals for titles from different rows each get their own Radarr/Sonarr connection, so a title approved from one row can no longer be filed using another row's settings. ## [1.6.1] - 2026-08-17 ### Added - **Leave one person's Plex sharing settings alone** ([#92](https://github.com/stevezau/shortlist/discussions/92)). To keep each row private, Shortlist adds label exclusions to every other account's Plex restrictions — which is right until it collides with a restriction you set yourself. The case that surfaced it: a child's managed account with an **allow only** label list, where the whole point is that the account sees nothing but the labels you named, and Shortlist's exclusions were fighting it. Switching that person off in Shortlist did not help, because off means "no row for them", not "stop touching their account". Open the person, go to **Settings → Plex sharing**, and turn off **Manage their Plex sharing settings**. Shortlist takes back out the exclusions it added and never touches that account again; the Users list badges them **Sharing untouched** so it is visible at a glance, and Support → Sharing lists them separately rather than reporting them as a fault. Said plainly at the switch: that account can then see other people's rows, unless — as with an allow-only list — its own Plex restrictions already keep it away from them. Two things stay true regardless. Everyone else still hides _that_ person's row, so leaving one account alone never makes their row public. And a **shared row you have limited to certain people** stays hidden from them: that exclusion is the only thing keeping the row away from people you did not pick, so removing it would undo a choice you made on the row itself. The trade-off to know is that later changes to who a shared row is for stop reaching a left-alone account — turn management back on if you need them picked up. ### Changed - Frontend dependencies bumped (lucide-react, sonner, eslint, typescript-eslint, testing-library), and the CI checkout action brought in line with the rest of the workflow. ## [1.6.0] - 2026-08-17 ### Added - **Judge a request without leaving the inbox** ([#87](https://github.com/stevezau/shortlist/discussions/87)). Deciding on a queue of unfamiliar titles meant opening a tab per row to find out what each one was, then coming back to the bulk toolbar to act on them. Every waiting title now carries its synopsis, and every card has its own Send, Delete and Reject. The synopsis rides in the same TMDB response the poster already came from, so it costs nothing extra for a title a TMDB source surfaced; rows already waiting fill in on the next run that re-surfaces them. - **"Watched" and "finished" are different questions, and the dashboard now asks both.** Plex's watched flag trips on a series' FIRST finished episode, so one episode of a sixty-episode show scored exactly like a whole film — on a real 47-user server, of 158 show picks credited as watched only 21 had been finished and 31 were a single episode, which ranked TV rows above movie rows for a purely structural reason. Every breakdown now reads "N watched · M finished". ### Fixed - **A name collision took one person's whole night down with it.** A Plex collection is keyed by title within a library, so renaming a row onto a title that already exists there answers 409 Conflict — and a `{top_seed}` row renames itself whenever the seed it is named after changes. Recorded on a 46-user server as 45 people fine and one with nothing at all, over a name. The row now keeps its old title for that night and still gets its titles; only the collision is survivable, so a dead server or a refused write still fails loudly. - **The recent-watches feed said "watched" for a show somebody sampled one episode of.** Every other figure on that page already drew the distinction; this one feed never asked for it. Films still read "watched" — they have no middle state — and series now read "started" or "finished". - **"8 failed" on the Jobs page led nowhere, and the Failed tab claimed nothing had failed.** The badge counts every failed job on record; the feed behind it could only fetch the newest hundred and filter those in the browser. On a real server the eight failures sat well past that window, so following the badge produced "No failed jobs. Nothing has given up — that's the good outcome." underneath a badge reading "8 failed". The badge is now a way in, and the list asks the server. - **A run could report itself failed and then explain nothing.** The banner only appeared for a failure that belonged to the run itself, so a run where 45 people succeeded and one did not announced "Failed" and showed no reason — leaving one bad account to be found by eye among forty-six. Failures are now named and grouped by cause, because forty people failing one outage share one cause and printing it forty times buries the only fact that matters. - **The run header said "Finishing up" for the whole per-user stretch**, and the ordering pass counted itself out of its own progress. - **A local AI server can be a hosted one, and those want a key.** ### Changed - **A UI pass over every screen.** Eight measured layout defects: the dashboard scrolled sideways at exactly 1280px; row names were squeezed to 32px and rendered as "Lat…"; the users and runs tables pushed columns — including a switch you could not press — outside their own cards on a phone; job names ellipsed into text you could not tell apart; the logs page opened already scrolled past its own heading and gave each message a third of the width it needed. All gone, verified at 320 through 1536. - **Less prose in front of the controls.** On-screen explanatory text is down by roughly a third. The pattern removed throughout was a page-header explainer, then a section explainer, then a card subtitle, then a per-field paragraph — four layers before a value. Anything that prevents a real mistake stayed. ## [1.5.2] - 2026-08-15 ### Fixed - **One row, two names on Plex** ([#84](https://github.com/stevezau/shortlist/issues/84)). A row named with `{top_seed}` and set to **movies & shows** could deliver a correctly named collection to one library and a plain "✨ Picked for You" to the other — from the same row, for the same person, so both showed up side by side. Each library named the row from its OWN picks, and if the person's seeds were all films, the TV half had nothing to name itself after and fell back. A library still uses its own seed when it has one — a row spanning two libraries genuinely can follow a different watch in each, and its titles should say so — but it now borrows the row's other seed before giving up and using the default name. - **The owner caveat is offered during setup, not pointed at** ([#85](https://github.com/stevezau/shortlist/issues/85)). Plex cannot hide anyone's row from the account that owns the server, so the owner sees everybody's on each library's shelf. Shortlist said so in the wizard — and then ended with "look for **You see everyone's rows** on the Users page", which is homework, handed out during setup, for a problem that only becomes visible in Plex days later. Someone was told exactly that, never went to the Users page, and reported 22 rows on their shelf as a bug. The wizard now asks the question where the decision is being made — _do you watch on this admin account?_ — and opens the real "move my watching to a separate account" flow in place, watch-history transfer and all. Skip it and nothing changes: the note on the Users page and the alert are both still there, both dismissable. - **Dismissing that note now clears the matching alert too.** Choosing "Got it — don't show this again" while reading the full explanation is a considered answer; the same message sitting in the bell afterwards was just nagging. The reverse still does not hold — clearing the bell is a light "seen it" and leaves the explanation on the Users page where you can find it again. - **Shortlist no longer invents a name for a row** ([#84](https://github.com/stevezau/shortlist/issues/84)). A row called "Because you watched {top_seed}" needs a title the person has actually watched. Someone new to your server hasn't got one — so the row had no name, and Shortlist substituted a hardcoded "✨ Picked for You". On a 22-user server with a French row-name template, 19 of 22 people got that: not your words, and a claim about a watch that never happened, over a row filled with whatever rates highest. Rows are now named by you or not built. The row editor asks, right beside the name that raises the question: **"Name for people with nothing watched yet"**. Fill it in and they get the row under that name; leave it empty and they simply don't get this row until they've watched enough — often the right answer, since "because you watched" can't be true for them yet. **Shortlist tells you when this affects a row**, in the notification bell — naming the row, saying nothing was deleted, and linking to the field. A behaviour change nobody is told about reads as a new bug. **On upgrade, nothing is deleted** — but a `{top_seed}` row stops appearing for people with nothing watched until you give it that name. Their existing row stays on Plex exactly as it is, still private, just no longer updated; type a name and it comes back on the next rebuild. Shortlist deliberately does not pick one for you: the obvious candidate is your default row's own title, and handing two of someone's rows the same name is what makes them fight over a single collection. - **A skipped show request now says what to do about it.** Sonarr identifies shows by their TheTVDB id, and Shortlist looks that up on TMDB — so when TMDB hasn't recorded one, there is no way to name the show to Sonarr and the request is skipped. (Guessing would be worse: the closest title match for "The Haunting of Bly Manor" is "The Haunting", a different and much larger series.) The reason on the **Requests** page used to read "no TheTVDB id for this show" — true, and no help at all unless you already knew what a TVDB id was. It now says TMDB has none and that adding the show in Sonarr yourself is the way through. A lookup that _failed_ — TMDB down or slow — says something different again, because that one is worth waiting a night for. - **The errors notification can be dismissed.** "N errors in the last day" counts what has already happened, and there is nothing to do about the past except acknowledge it — but it was pinned open, so the bell carried a badge for a full day with no way to clear it. It now dismisses, and the next error brings it straight back rather than staying hidden behind the last dismissal. The two alerts that stay pinned are the two describing something still true right now: runs paused, and an account that can see other people's rows. ## [1.5.1] - 2026-08-14 ### Added - **A way to support the project, for anyone who wants to.** A quiet "Support this project" line at the bottom of the sidebar, under the version, opening GitHub Sponsors. Deliberately in the chrome rather than among the navigation, and nothing floats over the page — people self-host to get away from being sold to, and anyone inclined to give goes looking rather than needing to be asked twice. - **Somewhere to ask, rather than only somewhere to report.** GitHub Discussions is open, and Have an issue? now offers **Ask a question** beside **Report a bug**. Not everyone who gets stuck is looking at a defect — plenty are looking at Shortlist working exactly as designed and not understanding why, and with one button on the page those get filed as bugs. Questions go to the Q&A category, where an answer can be marked as the answer, so the next person searching the same problem finds one instead of an abandoned thread. - **The "something else is reordering your shelf" alert now tells you how to stop it.** It used to say to exclude `shortlist_*`, which is a wildcard no tool's exclusion list accepts. Agregarr has since gained an **Exclude from Ordering (Plex Label)** field, and it matches a label exactly or as a prefix followed by `_` — so the single word `shortlist` covers the constant label and every person's own. The alert names the field and the value. - **What a row actually cost, per row.** A run page showed one number per person — their whole night — so a row that took four minutes and a row that took four seconds looked identical, and the shared work every row depends on was billed to whichever row happened to be first. Each row now reports its own time and its own AI spend, with the shared setup shown separately and the pools that fed more than one row named. Waiting for the Plex write-lock is charged to the row that waited, so a slow night points at what was slow rather than at whoever was unlucky. - **A download that says it is working.** The Logs page's **Download .zip** and Have an issue?'s **Download everything** were plain links: the browser fetched them in silence while the server gathered and zipped the logs, which takes seconds, so nothing on screen changed and the natural reading was that the click had missed. Both now say **Preparing…** while the file is built. - **One label for co-managing tools.** Every row now carries a constant `shortlist` label (Plex stores it as `Shortlist`) alongside its per-person one. Tools that share your Recommended shelf — Agregarr's **Exclude from Ordering (Plex Label)**, Kometa's equivalents — take a list of labels to leave alone, and Shortlist's per-person labels were no use there: a 46-account server has 46 of them plus one per shared row, and the list goes stale the moment somebody joins or leaves. One entry now covers every row, for good. It hides nothing and changes nothing about who sees what — privacy still runs entirely off the per-person label. Existing rows pick it up the next time they are built, so there is nothing to run — but that first run does one extra label write per row you already have, and on a server whose Plex is slow to answer writes it may be a noticeably longer night. Once only; the log names every row as it goes. ### Fixed - **A `{top_seed}` row no longer falls back to "✨ Picked for You" for people who have plenty of history** ([#84](https://github.com/stevezau/shortlist/issues/84)). A row titled "Because you watched…" needs a pick that came from something the person watched, and it only ever looked at the single best pick. Some sources suggest a title without following one — what's trending, what's popular on your server, a web-search find — so if one of those ranked first, the row acted as though the person had no history at all, with a dozen perfectly good seeded picks right behind it. It now takes the strongest pick that actually has a seed. That is why this was happening on established accounts and not just new ones. Still open in that issue: when there genuinely is no seed — a brand-new account — the row is named from a hardcoded "✨ Picked for You" that ignores your own row-name setting. Fixing that properly means changing what a row with no title of its own is called, and per-user rows are told apart by their titles, so it is a change with teeth. It is not shipping in a patch release. - **"Remove or delete" looks like what it does.** In a row's editor it was grey text that did not read as a control at all, let alone one that ends in deleting a row. It now matches the Delete on the Rows page — red, outlined, with a bin. It still only takes you to the section at the bottom of the page rather than acting, because removing and deleting reach into other people's Plex and Cancel does not undo them. ## [1.5.0] - 2026-08-13 — withdrawn Tagged and published for about half an hour, then pulled before anyone was told about it. Everything in it ships in 1.5.1; the notes are kept here because the images were briefly public. ### Added - **A row can be positioned after another Shortlist row.** Choosing where a row sits on the Recommended shelf offered your own library's collections, but never your other Shortlist rows — so "put Because you watched right after Picked for You", the obvious thing to want, could not be asked for (issue #81). It can now, and it is chosen as a ROW rather than as a collection: a per-person row is one Plex collection per person, so on a 46-account server the old picker would have had to list 46 identical-looking entries, and picking any one of them would have placed the row for that one account and nobody else. Rows are then placed in dependency order, so a row lands only once the row it follows is itself in position. Pointing two rows at each other, or a row at itself, is refused when you save it rather than silently doing nothing every night; and deleting a row clears the placements that pointed at it, so the rows that followed it fall back to the library default instead of never being placed again. - **Run one row, from the row.** Rebuilding a single row was already possible — it was behind a dialog on the Runs page that made you pick, from a list, the row you were already looking at. **Run now** sits on each row's card and at the top of its editor, and takes you to the run it started so you can watch it happen. A row that is switched off can't be run, because a run doesn't build a disabled row, it takes it off Plex — and a button called "Run" doing that is the opposite of what the word promises. The editor also gains a **Runs** link to that row's own history, which until now only existed on the card the editor replaced. If you press Run with unsaved changes on screen, it says so first: a run rebuilds the row as it was last saved. - **"Where is each row actually showing?"** — a new check on the Have an issue? page, and the only one that can see your OWN Home screen. Every other row-visibility check reads the share filters that hide a row from other people, and you, as the server owner, don't have one — so the flag that keeps somebody else's row off your Home was the one thing nothing could report on. It also separates a real fault from a Plex limitation: another person's row on your Home is always a bug, while their row on your Recommended shelf is what showing rows on friends' library shelves does, and is a setting rather than a defect. - **Runs are now shown by row, not by person.** A run's unit of work is a row, but the run page listed people — so a **shared row**, built once for the whole server and belonging to nobody, had nowhere to appear at all. On a night when no per-person row was due, the page was 46 lines of "skipped" with the run's only real output, a shared row of 40 picks, nowhere on screen. Runs now open on a **Rows** tab showing the rows that run actually built. Each row expands to what it produced: a per-person row puts a person picker in front of the picks, a shared row shows them directly, and that is the only difference between the two kinds. Rows the run didn't touch are one quiet line at the bottom rather than a card each, so a run of one row looks like a run of one row. A row is named once, by its own name, with the libraries it delivered to beside it — a row that builds both Movies and TV Shows reads "✨ Picked for You · Movies · TV Shows" instead of taking one library's title and hiding the other. The People tab is gone: everything it did — the searchable, status-grouped person list and that person's own results, trace included — now sits inside the row they belong to, so you read a run one row at a time rather than choosing between two ways of slicing it. - **A row's on/off switch is on its own editing page.** It lived only on the Rows card, and the editor sent you back there for it — a page change to do the one reversible thing to the row you already had open. It now sits beside Run now, with the same confirmation: turning a row off takes it off Plex for everyone who has it at the next run, and turning it back on rebuilds it. Removing and deleting stay at the bottom of the page, away from the rebuild controls, because they reach into other people's Plex and Cancel doesn't undo them — but the header now says they are there and takes you to them, which is the part that was missing. - **Shared rows have a trace.** The same view a per-person row has always had, minus the per-person watch history a shared row doesn't use — so "why did it pick that" is answerable for a shared row for the first time. - **The sidebar now names the exact build you're running.** A release shows its version (`Shortlist · 1.4.0`); a `:dev` build shows the branch and commit instead (`Shortlist · dev · 2ee14f8`), with the full commit on hover and in the debug bundle. The version is deliberately absent on `:dev`: it is the last RELEASED version, so on a dev build it names the release you came after rather than what you are running — five commits ago, in the case that prompted this. The commit is the only thing that tells two dev builds apart anyway, since every push between two releases carries the same version number, so "is my container actually on the fix?" had no answer short of shelling in. ### Removed - **The Agregarr connection is gone**, and with it the Agregarr card under **Settings → Connections**. It existed for one job: after each run it wrote Shortlist's shelf order back into Agregarr so Agregarr's own half-hourly sync would reproduce it instead of undoing it. Maintaining a client against a private, unversioned API to stop one third-party tool fighting us over hub positions was not worth carrying. **If you had it connected**, the Recommended shelf goes back to being contested — Agregarr reorders it on its clock, Shortlist puts its rows back on the next run. Two ways to settle that, both configuration: exclude collections labelled `shortlist_*` in Agregarr (or stop its "Randomize Home Order" job), or set **Row placement** to "Wherever Plex puts them" and let Agregarr own the order outright. Your rows are still built, delivered and kept private either way — only their position on the shelf changes. Shortlist still tells you when it is happening: the "Something else is reordering your shelf" notification is unchanged, and now also names the maintained Agregarr fork. Your stored Agregarr address and API key are **deleted** on upgrade (migration `0067`) rather than left behind — the key was held encrypted and there is no longer a screen to remove it from. Past `run.agregarr_order` and `shelf.agregarr` events stay in your history; they record writes that really happened. - **A note for Agregarr users, wherever Shortlist mentions it.** The original at `agregarr/agregarr` is no longer actively released, and reordering a shelf on it re-promotes collections with Plex's defaults — which puts other people's rows on the **server owner's** Home, the one place no share filter can cover. Shortlist already clears that on every run, so it is a gap between runs rather than a standing leak. The maintained fork at [bitr8/agregarr-dev](https://github.com/bitr8/agregarr-dev) (image `bitr8/agregarr`) fixes it at the source and is a drop-in swap. It still reorders the shelf, so the placement advice above applies either way. ### Changed - **The Have an issue? page now answers the question you picked.** Choosing a problem ran one check; three of the cards named two or three things in their description and then opened only the first — "checks the queue, the schedule and the clocks" checked the queue. Each card now runs every check it promises, the answers stack up on the page instead of replacing one another so you can gather several into one report, and eleven more checks state their finding as a sentence rather than leaving you to read a table. A check that fails now offers its failure to copy — previously the one moment you had nothing to send. Filing a bug no longer needs the checks switched on at all; only attaching the diagnostics does. - **The Jobs page's row schedules read as schedules.** Rows that build at the same time were joined into one line of comma-separated names, truncated, with the time as its subtitle and a single Edit button that could only take you to the rows list — there was no one row it could mean. The time now leads, with how many rows it builds, and each row is its own link to its own editor. - **The dashboard's "Recently watched" says how much it is showing** and folds the rest away, rather than silently stopping. **"N more people watched something"** under By person is now "Show N more", because it was never a separate finding — it is the rest of the list above it, which now says it is ordered by watches. - **The Logs page drops the paragraph explaining its own buttons.** What mattered in it — that everything down to DEBUG is recorded whatever level you are viewing — moved to the line at the bottom that already tells you the full history is in the download. - **A "Popular on this server" row now actually shows what's popular on this server.** It was built the same way a personal row is: your server's most-watched titles became the _starting point_ for a TMDB similar-titles search and an AI web search — and that search deliberately excludes the titles it started from, so the most-watched titles on your server were the one thing the row could never contain. Every pick was a suggestion captioned "Popular on this server", which was untrue of all of them. It also cost around 10,000 AI tokens and a minute a night to produce. The row is now the count: the titles the most people on your server have actually watched, in that order, and each says how many ("11 people watched it"). No searching, no AI, and it builds in seconds. Your existing shared rows will change contents on their next run — this is the row you were asking for the whole time. A shared row's editor drops every control that only made sense for a search — where the picks come from, how many recent watches to build on, the AI web-search dials and **Recent releases** — because a tally has nothing to point them at, and a control the app ignores is worse than no control. What is left is what still decides the row: its libraries, how many people must have watched a title, its size, and its order (use **Newest first** to lean modern). Your **Don't seed** list now keeps a title out of a shared row entirely, rather than only stopping it being a search starting point. ### Fixed - **Cancel stops a run.** Pressing it could leave a run writing to Plex for minutes, and a run that was only queued would not stop at all until the run ahead of it had finished — the code meant to stop it could not run until the thing it was waiting for got out of the way. A queued run is now finished on the spot, having done nothing. A running one stops at the next row boundary: every person's writes queue on a single lock, so most of them are waiting inside delivery when you press it, and each used to go on to write a whole row as its turn came. Measured on a 46-user server, a cancel that took over five minutes now takes about one — the remainder being the person whose row was already half-written, which is finished on purpose, and the privacy merge that hides everything delivered so far. Cancelling mid-retry also no longer erases the record of a collection that had already been written. - **The Runs page notices when a run ends.** It had no live updates at all, so a run that finished while you watched kept its "Running" badge and a ticking timer until you navigated away and back — which made a cancel that had worked indistinguishable from one that was ignored. - **A run that never ran says so.** Queue three runs, cancel them nine minutes later, and each reported "9m 26s" of work none of them had done: the duration was measured from the moment the run was asked for, so it was timing the queue. Runs now record when they actually started, and one that never got that far reads "never ran" instead of billing you for the wait. - **The "How we picked" trace is reachable again.** It had gone with the People tab it lived in, so a per-person trace could not be opened from anywhere in the app. It now sits in the person's own panel, along with who they are, how long they took and what they cost — and shared rows keep their own trace beside them. - **The watches-per-week chart tells you the numbers.** The count was on a tooltip attached to each bar, so on a quiet week the only place you could hover was a three-pixel sliver at the bottom of the chart and most of the column did nothing at all. Hovering anywhere in a week's column now reads it out, and the chart is dated at both ends — sixteen unlabelled bars never said _when_. - **A downloading request updates itself.** The Sonarr/Radarr status on the Requests page was fetched once when the page opened and never again, so a title that finished downloading while you were looking at it went on saying "Searching" until you reloaded, and a title you had just sent showed nothing at all. It now checks while you watch and says **Checking…** while it does — and only while something is actually moving, so an inbox where everything has downloaded stops asking rather than re-reading your whole Radarr library every half minute for as long as the tab is open. - **A Sonarr or Radarr that can't be reached says so.** A failed lookup is deliberately ignored so that one app being down doesn't blank the other's titles — but that left an unreachable app looking exactly like one that simply isn't tracking anything, and the inbox showed no badges for ever with nothing anywhere explaining why. Each title now says which app couldn't be reached. - **"Where is each row actually showing?" no longer reports all-clear over a real leak.** Its headline sentence looked only at rows claiming the owner's Home screen, so two findings the check itself had already made were read straight past: a collection of ours carrying **no label** — which nothing can hide, so everyone on the server can see it — and rows Plex refused to answer for at all. Both produced the same empty result as a healthy server, and the banner said so. It now names an unlabelled row as the bug the server calls it, refuses to call anything clean when the reads failed, and — following the same rule the delete path uses — treats _every_ row reading unlabelled as a failed read rather than as a server full of leaks. - **A run that is only queued no longer counts a duration up.** `started_at` is stamped the moment a run is _asked for_, not the moment it begins, so a run still waiting for the Plex writer lock ticked a stopwatch up from the button press — under a tooltip reading "Running…" beside a badge reading "queued". It now shows nothing until it actually starts, and the Started column says "queued 5m ago" rather than claiming it began then. The run's own page said "started … · still running" for the same reason, which — since **Run now** takes you straight there — was the first thing you saw after pressing it. - **A background job waiting to be retried no longer shows how long it took.** After a failure the job goes back in the queue keeping the timestamps of the attempt that failed, so the Jobs page displayed that attempt's duration beside "Retrying" — a time for work that had not started. - **A shared row's result is kept.** It was thrown away at the end of every run: the row was built, labelled and promoted on Plex, but only a one-line audit note survived — no trace, no per-library breakdown, no token cost, and not even the list of what it picked. Older runs can't be recovered; from now on it is all recorded. - **A shared row's collections are now tracked like everyone else's.** They never entered the internal ledger Shortlist uses to find a row again later, which is what lets it clean up a row whose title changes every night. - **Docker installs no longer report themselves as source checkouts.** The app read `GIT_SHA` and `GIT_BRANCH` to tell a container from a development checkout, but nothing ever set them — the image carried those facts only as OCI labels, which nothing running inside the container can read. The build now bakes them in, so the install type is right and the commit is visible. - **Accounts that were switched off when they left your server now show as gone.** The daily roster sweep only compared **enabled** accounts against Plex, so anyone already turned off when they were deleted from Plex stayed in the Users list looking like an ordinary disabled account — with nothing to say why, and no way to clear them out, because **Remove** only offers itself for someone Plex no longer lists. Deleted Home users were the common case (one real server had five of them stuck). They are now badged **Left the server** and offer **Remove** like anyone else. Nothing is created or promoted on Plex for them — an account that was already off has no rows to take down — though the stale rule hiding their old row does become eligible for tidying out of everyone else's share filter on the next run, which is the point. The protection that refuses to act when half the server seems to vanish at once now covers the badge as well as the switch-off, and the two are judged separately, so a server carrying old departures nobody cleared can't block a real one from being handled today. ## [1.4.0] - 2026-08-12 ### Added - **Shortlist now tells you when it cannot make a row private.** Plex refuses a label share-filter for a managed account with a parental **Restriction Profile** set, so those accounts were skipped — on the stated grounds that such an account "sees zero collections anyway". That is true of _Younger Kid_ and false of _Older Kid_, which on a real server listed three collections. So for those accounts nothing hid other people's rows and nothing said so. Every run now checks each profiled account **with that account's own token** and reports what it can actually see: a dashboard alert that cannot be dismissed while it is true, a **"Sees N rows of others'"** badge in the Users list, and an explanation on the person's page. Shortlist still cannot fix it — hiding a row _is_ the filter Plex is refusing — so the one remedy that works (clear the Restriction Profile) is named, and disabling the account is explicitly called out as _not_ a fix, since that removes their own row rather than their view of everyone else's. (#76) - **People who leave your Plex server are handled properly.** Losing access already switched someone off and removed their rows, but the Users list then showed them exactly like an account you had turned off yourself, and the share-filter exclude for their deleted row stayed in every other account's filter permanently (one real server had reached 990-character filter strings). Departed accounts are now badged **Left the server** and offer **Remove**, which drops their pick and run history and clears them out of the list. Remove deliberately keeps their original Plex share settings, so uninstalling Shortlist can still restore that account exactly as it was found — that record is the only copy. A departure is re-checked every sync, so re-inviting somebody brings them straight back. - **Shortlist tells you when another tool is fighting it over the Recommended shelf.** That shelf is one server-wide list, so anything else that manages Plex recommendations (Kometa, agregarr, Plex-Meta-Manager) reorders your rows along with everything else. It is invisible from any single pass — Shortlist moves its rows, re-reads the shelf, confirms the new order, and is right; the other tool moves them back minutes later. When the same row has had to be put back three or more times in a day, an alert now says so, names the likely tools, and gives the two ways out: exclude `shortlist_*` collections in that tool, or turn off **Let Shortlist order the Recommended shelf** and hand the order over. Your rows are still built, delivered and kept private either way. - **Shortlist and Agregarr stop fighting over the Recommended shelf.** If you also run [Agregarr](https://github.com/agregarr/agregarr), both tools arrange the same shelf and each undoes the other: Shortlist puts the rows up top, Agregarr re-applies its own stored order within half an hour, and around it goes — on one real server 35 of 91 rows were left below position 21. Connect Agregarr under **Settings → Connections** (its address plus the API key from Agregarr's own Settings → General) and the end of each run tells Agregarr where the rows ended up, so its next sync reproduces the shelf instead of undoing it. It stores the shelf **as Plex is already showing it**, never an order of its own invention, so your other Agregarr rows keep their order relative to each other and simply move down to make room. Nothing else about them is touched — not posters, visibility, titles or summaries. The connection is entirely optional: without it nothing changes and not one extra call is made. If Agregarr is unreachable the run finishes normally with a warning rather than failing, and gives up in about twenty seconds rather than stalling. Every attempt is recorded — `run.agregarr_order` for a nightly run, `shelf.agregarr` for the **Fix privacy** and **Check server** buttons — each carrying the before and after ordering, since Agregarr's previous order is not otherwise recoverable. ### Fixed - **A failed run no longer clears the "can see other people's rows" alert.** The alert and the **Sees N rows of others'** badges are driven by the most recent run that actually checked — but every run recorded itself as having checked, including one that died before it got anywhere near the check. So a single failed run silently removed the alert, zeroed every badge and switched the person's page to its reassuring wording, while the exposure itself was untouched. Runs now record whether they got as far as looking, and only a run that did can clear a finding. The same silence in two other places is fixed with it: an account whose token could not be obtained was counted as seeing nothing rather than as unchecked, and the badge suggested turning the person off — which removes their own row, not their view of everyone else's, as the rest of the feature already said. - **Rows promoted nowhere are no longer shuffled around the shelf.** A row belonging to a paused or disabled person sits on no surface at all, so its position is invisible to everyone — but it is still listed among the managed hubs, and every ordering pass moved it back into place. That was four pointless Plex writes per library per pass, and it made a settled shelf look contested: a co-managing tool rightly ignores those rows, so Shortlist alone kept moving them. A row on any surface — including the owner's own Home — is still placed as before. - **Rows no longer get stranded at the bottom of the Recommended shelf.** Three separate faults, each enough on its own to make it unfixable. A run with no users — which is what every privacy sync is — came out with no libraries to order, so the whole pass silently did nothing. A per-library shelf anchor made the pass take its rows from the run in progress, so a run with no users had nothing to move and said nothing about it; it also dropped the row of anyone paused, errored or skipped. And the reorder itself re-issued a move for _every_ row whenever any one was misplaced (47 writes where 19 were needed) and then reported success without ever re-reading the shelf. Ordering now happens on every privacy sync and on **Check and fix rows on Plex** rather than only overnight, moves only what is out of place, and re-reads to confirm — recording the result as unverified rather than claiming a shelf it did not get. - **Dead privacy excludes are cleaned up.** A `label!=shortlist_` exclude for a row that no longer exists is now removed from everyone's share filter — but only once **two independent checks** agree the row is gone: a complete collections read in which nothing carries that label, _and_ Shortlist's own record that the person departed. Either alone can be wrong in the direction that un-hides a live row, so neither is trusted by itself. - **The nightly departure sweep is now tested.** It disables people and deletes collections unattended, and had no test coverage at all — including the two limits that stop it acting on a truncated read from plex.tv (an empty roster is ignored; more than half the server appearing to leave at once is refused and recorded). ## [1.3.0] - 2026-08-11 ### Added - **Rows can prefer recent releases.** Ranking had no age term at all — a title's score was seed frequency x rating x seed weight x affinity — so a well-rated, highly-similar 1996 film beat a 2024 one every time and rows filled with catalog titles. **Settings → Finding titles → Recent releases** is a weight, never a filter: an older title still reaches a row, it just has to be a better match. It applies to the pool CUT as well as the order, so a newer title below the line can still get in. **This applies to every install, existing servers included** — the default is 0.5 ("leans towards recent releases"), and no migration pins the old age-blind behaviour. Nothing is rewritten at upgrade: each row adopts it on its next refresh night, so rows shift towards newer titles over the following days rather than all at once. Set **Recent releases** to 0 to rank as before, globally or per row. - **Changing a setting rebuilds the row instead of waiting out freshness.** Freshness is a cadence for suppressing churn when nothing has changed — but nothing recorded WHICH settings a row's picks were chosen under, so it also delayed changes made on purpose. On a real server, raising "Recent releases" left 36 of 42 rows redelivering byte-identical picks for up to a fortnight, which from the outside is indistinguishable from the setting not working. Each row now stores a fingerprint of the settings that decide its CONTENTS and rebuilds on a mismatch, whatever the cadence says. - **`/api/support/surfaces`** — a read-only diagnostic that reports which Plex surfaces each row is actually on, included in the support bundle. - **A run says what year each pick is and how well rated it was.** "Why is this row full of old films?" was unanswerable on the one page built to answer it. - **Web search can now run entirely on your own hardware, via SearXNG.** The AI web-search source previously had two backends: your AI provider's own search (Claude, GPT and Gemini only) or the hosted Exa API. Neither suited a fully self-hosted server — a local Ollama model can't search on its own, so Exa was the only way to use the feature at all, and that meant a third-party account and sending queries off the box. **Settings → Finding titles → AI web search** now offers **SearXNG** as a third backend: point it at your own instance and the whole search stays local. It works with every AI provider, exactly as Exa does. ([#78](https://github.com/stevezau/shortlist/issues/78)) Set it up in **Connections → Web search** — one card where you pick the backend and enter what it needs, replacing the separate per-vendor cards — or inline on the web-search card itself. One prerequisite, which the card and the **Test** button both state outright: SearXNG's JSON API is **off in a stock install**, so add `json` to `search.formats` in its `settings.yml` and restart. Without it SearXNG answers with a bare `403` that explains nothing — Shortlist translates that into the exact fix. A reverse-proxy login is supported via its own username/password fields, where the password is encrypted at rest. A login embedded in the address (`http://user:pass@host`) is refused outright, with a message pointing at those fields: the address itself is stored in the clear, returned by the API and written verbatim into the immutable `settings.change` audit event that support bundles export, so a password must never be allowed into it. A subpath deployment such as `https://example.com/searxng` is kept intact. Choosing a backend by name means only that backend — an unconfigured Exa or SearXNG never falls through to the other, which would have sent a self-hoster's queries to a paid vendor they never picked. ### Changed - **The "Auto" search backend is gone.** It was the default, so it is what nearly everyone ran, and it meant "the provider's own search UNIONED with whichever external is configured" — a real behaviour that its name described not at all. You now pick exactly one backend, and it is the one used. Migration 0063 pins every install to what it was actually using: a configured SearXNG, else a configured Exa key, else the provider's own search. **A server that had both loses the combined search**, which did widen the candidate pool; raise a row's seed count if you want that reach back. - **Which backend the web search uses now lives in one place.** It was briefly both on the Connections card and on the Finding-titles card — the same control twice, with no way to tell which was authoritative. Connections → Web search owns the backend and its credentials (the AI provider already works that way); Settings → Finding titles owns whether the source runs, and names the backend with a link rather than repeating the form. - **"Test" on the provider's own web search now really searches.** Being Claude, GPT or Gemini says the provider offers a web-search tool; it does not say your plan or model may use it. When it may not, the call failed at run time, logged a warning and returned no titles — so the source quietly contributed nothing, every night, with nothing in the UI to say so. The Test button now performs one real search and tells you if the tool came back empty. - **The run trace names every candidate and what became of it.** "What survived, and what release date did to it" reported counts — "40 candidates survived filtering" — which is a summary, not a trace: it could not answer "so why isn't X in my row". Every candidate is now listed under what happened to it (made the shortlist / not in your libraries / lost the cut / already watched), each with the rating and the release-date multiplier actually applied to it. The per-title verdicts were already being recorded; they were simply never shown. - **The "not in your libraries" group says what Radarr/Sonarr did with it.** That group IS the request pool, and nothing on the page said so. Each title now reads "requested — added to Sonarr and searching" or the reason it is still waiting. Which exposed the gap behind it: the engine worked out per-title why a title stayed queued and then discarded it into an aggregate log line, so every pending row had an empty reason and the inbox's one question had no answer anywhere. - **"How we ordered the shortlist" shows the ordering instead of describing it.** The claim an owner most wants checked — that one heavily-watched favourite cannot swallow the row — was the one prose could not settle. The rank list now marks where each source's and each watched title's turn begins. - **Run stats say "Web searches" rather than "Exa searches".** The same counter now serves both external backends, so naming one vendor was simply wrong on a SearXNG server. A run's "How we picked" trace also records _which_ backend actually ran. ### Fixed - **A share filter written by Plex Web is merged, not corrupted.** `parse_filter` split conditions on `|` and values on `,` only, but Plex Web writes a combined allow+exclude filter as `label=Age%200%2CAge%203&label!=X`. That parsed into one condition whose field swallowed the allow clause, so the existing exclude was invisible, the merge appended a second one, and the filter grew corrupt. This is the privacy machinery, so it is the most important fix in this release. **It stops the corruption; it does not undo it.** An account whose filter already carries two `label!=` clauses keeps both — future runs merge into the first and leave the second alone, and Plex Web is unhappy with the pair. If you hit this before upgrading, open **Plex → Settings → Users → that account → Restrictions** and delete the duplicate exclude line; Shortlist rebuilds what it needs on the next run. Healing them automatically means rewriting a filter we only partly wrote, which is how they got corrupted in the first place, so it is deliberately left as a manual step. - **The broken-row sweep asks twice before deleting.** It is the only destructive Plex path, and an empty label read is indistinguishable from a genuinely unlabelled collection — so a collection is now re-read to confirm before removal, and a sweep where NOT ONE of our rows reads as labelled is treated as a failed read rather than a server full of orphans. - **A refresh night no longer collapses a row onto one taste.** The refresh branch merged survivors with newcomers and then truncated by pool order, re-applying the very ordering `diversify_by_seed` had just defeated — so a heavily-watched title's look-alikes took the row back over. - **Shared rows honour their display order** and stop offering dials they ignore: a shared row set to "Shuffled" or "Highest rated" was delivering ranking order, because the shared path never called the code those settings live in. - **Cold-start rows are full size and from the right library**, and "unstarted only" is rechecked — three findings from the pipeline audit, all in paths whose victims are least likely to report them. - **An exclusion written URL-encoded is no longer reported as missing.** The sharing report compared filter values raw while the privacy code compares them decoded, so an encoded copy of a label read as absent — and the one question that report exists to answer named a leak that wasn't there. - **Cancel stops lying about what it did.** It worked on the first press and then answered "this run isn't currently running" to every press after, about a run that was. Asking a stopping run to stop is now a no-op rather than an error, and the accepted cancel is recorded on the run so a reloaded page still shows "Stopping…" instead of a live-looking button that can only fail. - **A connection card says what it needs before what it is** — the part that actually blocks you was the easiest to skim past. ## [1.2.1] - 2026-08-07 ### Added - **A run now says what Plex ratings did to it.** Open a run → **How we picked** for a person, and the "Watched recently" step states the rating policy that run used in one line: off, on but nobody rated anything low enough, on but the account's ratings were disbelieved as tool-written, or how many titles a rating dropped. Until now all four looked identical — the trace listed dropped titles when there were any and said nothing at all when there weren't, so a run where the feature silently did nothing read exactly like a healthy one. The distrusted case is the one worth catching: a Kometa rating sync on that account means every rating is ignored, all run, invisibly. The line reports the setting **as it was when the run happened**, not as Settings reads today, so changing it later doesn't rewrite the history of a run you open a fortnight afterwards. Runs recorded before this release don't carry it and still show just the dropped titles. It also catches the half-case the account-level check is designed to let through: if a tool has written _some_ of someone's ratings but not enough to condemn the account, the counted ratings and the skipped ones are reported separately, rather than a single total that quietly speaks for values nothing ever looked at. ### Fixed - **A pick's "why" said "Because you liked…" when it only ever knew what someone watched.** Every pick with a genre in common with its seed claimed a preference nobody had expressed — seeds are weighted purely by how recently something was watched, and a Plex rating can only ever _remove_ a seed, never mark one as liked. It now reads "Because you watched…", which is the fact we have. Reasons are written into each run as it happens, so runs already recorded keep the old wording. ## [1.2.0] - 2026-08-05 A minor release with **one behaviour change** — read the first item below before upgrading if any of your rows are set to 0% already-watched, which is the default. ### Changed - **A row set to 0% already-watched now also excludes shows someone has only STARTED.** It used to exclude only shows they had _finished_, where "finished" meant 80% of the episodes or a length-scaled floor of about three. So a series you were two episodes into was, as far as the row was concerned, a fresh discovery — and could be recommended straight back to you. That was reported, and it was doing exactly what it was told. Plex draws no such line: its own watched filter returns a series from its first episode. A probe of a real server found it returning shows as little as 1.1% watched (2 of 176), and on that server five of ten started shows were still eligible to be suggested to the person watching them. At 0%, started now means watched — the two agree. **Rows above 0% are unchanged.** There the percentage is still a ceiling on _finished_ titles, because "N% of the row" needs a definite line to mean anything. If you relied on the old behaviour, set the cap above 0. Rows re-pick their titles on their own refresh night, so the change reaches a row when it next rebuilds rather than immediately. - **"Only series they haven't started" is now offered on a films-and-shows row**, not just a shows-only one. The API and the engine always accepted it there; the switch was hidden, so the default row most installs have could never turn it on. On a 0% row it is now a no-op — that row excludes started series anyway — and it still does real work on a row whose cap is above 0%. ### Added - **"Have an issue?"** — a new page, replacing the sidebar's separate _Report a bug_ and _Copy diagnostics_ buttons. It runs twenty-one read-only checks against your own server and, for most problems, names the cause outright: which library refused someone's token, which setting actually applied, why a row is short, whether Plex still matches what Shortlist thinks it delivered. Nothing on the page changes anything — not your Plex server, not your rows, not your settings. The checks stay off until you switch them on and switch themselves off again after 24 hours, because they read share filters and per-user tokens. Every check has a **Copy for support** button, and what it copies is shown on screen first so a paste holds no surprises. The last section files the report: a pre-filled GitHub issue plus a diagnostic to attach, as a paste or a file. That diagnostic masks credentials, IP addresses and your server's machine id. It is a good first pass rather than a guarantee: logs carry whatever a dependency decided to print, so give the report a skim before posting it publicly. It does name the people on your server by their Plex username — replace those yourself if you'd rather not publish them. If someone reports a problem on a server you don't administer, sending them there is usually faster than a list of questions. ### Fixed - **A downloaded log zip no longer carries your server's IP address or machine id.** `Logs → Download` calls itself "the attachment for a bug report" and had only ever removed credentials — so it shipped your Plex server's address on every line that recorded a call to it (17,234 of them in one real export) and, where a collection write was logged, the machine id that identifies your server. Both are now removed there and in the diagnostic report, which had the same gap in the log files it bundles. A URL keeps its scheme and port (`https://:32400`) because those are the parts that answer a question. The live Logs view is deliberately unchanged: it renders on your own screen, where your server's address is what makes a line readable. - The documentation described a `recommendations.watched_show_pct` setting that has never existed. The finished threshold is fixed in the engine; the docs now say so. ## [1.1.0] - 2026-08-05 A minor release: one new setting, four fixes reported by v1 users, and no breaking changes. An existing install upgrades in place and behaves exactly as it did until you change the new setting. ### Added - **You can now choose what someone with too little watch history gets** ([#66](https://github.com/stevezau/shortlist/issues/66)). Until now they always got a row of the server's highest-rated titles. That is still the default, but **Settings → Finding titles → When someone hasn't watched enough** can now be set to **Don't build their row** instead — no row is created, and any row they already have is removed, so "skip" means gone rather than left to go stale. It comes back on its own the night they cross the threshold. **Any row can override this in its editor**, which is the point of having it per row. A `{top_seed}` row ("Because you watched X") is the one worth skipping: it has no favourite to name itself after, so for these people it quietly falls back to a plain title. A general "Picked for you" row is perfectly happy holding popular titles in the meantime. - **Where that line sits is now yours to set.** **Enough watch history** (default 10 titles) was fixed in the engine and unreachable; owners of small or new servers can lower it. ### Fixed - **A run in progress no longer disagrees with itself about when it started** ([#67](https://github.com/stevezau/shortlist/issues/67)). "Started 8m ago" sat beside a duration of "10m 54s" — the two cells read the same timestamp against the same clock, but were evaluated at different moments. They now share one. - **A first run no longer dies because the container started before its network did.** The one plex.tv read whose failure aborts a whole run had about three seconds of retry, which is not enough to outlast a slow network stack; it now gets about thirty. Every other read is unchanged, and a run still fails loudly when plex.tv is genuinely gone. - **The Rows page now says why the admin's Collections tab lists everyone's rows.** The answer existed but only covered the Recommended shelf, and only while that setting was on — an owner who found the rows in Plex's Collections tab had nothing in front of them. - **The "How we picked" button no longer strands mid-header** on a person whose display name is long enough to wrap the run card's header. ### Changed - A muted row whose title depends on its picks (a `{top_seed}` row) is now genuinely removed from Plex. It could not be matched by title, so it survived every run — private, but never actually gone. Muting has always meant "gone"; now it is. ## [1.0.0] - 2026-08-04 First stable release. No breaking changes from `0.1.0-beta.9` — the version number is a statement about stability, not a rewrite. An existing install upgrades in place; the migrations run on boot after taking a pre-migration backup. ### Added - **A "Because you watched" row can cycle its seed** instead of sitting on the newest watch for weeks, so the row keeps moving even when someone's viewing does not. - **The row editor is a page**, showing what the row will actually do — and whether it is working — beside the settings that decide it. Rows can be renamed and deleted from there. - **A way back from Off**, and a request filter that reaches past the 500-row inbox cap: picking a name under "Wanted by" now asks the SERVER, so it searches every title on file rather than the page that happened to load. - **Two new row orders, for when the front of a row feels stuck** ([#63](https://github.com/stevezau/shortlist/issues/63)): **Just added** puts whatever is new to the row at the front, and **Taking turns** advances the front by one title a day so every pick gets a spell there. Both are presentation only — neither changes which titles a row holds, or how often it refreshes, which is still **Freshness**. "Newest" is now labelled **Newest released**, to keep it distinct from "Just added". - **A rebuilt documentation site**, split into eight task-shaped guides. - **The row editor leads with how the row is doing** — delivered, watched, runs and last built, across the top. Runs links to that row's own history, and counts the runs that list actually holds, so the number and the page behind it can never disagree. ### Fixed - **An un-watch is noticed within the night's read** rather than waiting up to a week for the full re-read. Only when the read proves it covered its window — a truncated walk deletes nothing, so a PMS that omits `totalSize` can never be mistaken for "they un-watched everything". - **A library removed from the server no longer counts as watched for ever.** Its cached titles and cursor are swept on the weekly pass. A library that is merely unshared is left alone — that history is still true. - **`fetch_section` raises instead of returning an empty set** when plex.tv will not mint a token. Reported as "nothing watched", it made a full read wipe the section and stamp the sync a success. - **The dashboard and Rows pages no longer scroll sideways on a phone** (134px and 184px past a 390px screen; Rows put the Delete button out of reach entirely). Every route, the wizard, the nav drawer and every dialog now fit 390px, enforced by `tests/e2e/test_mobile_audit.py`. - **The drift check's off switch now turns it off.** - **Pages no longer wait on Plex.** The library list behind every row card was read live from the server on each page load, so while a job was deleting collections — one DELETE took 15.8s inside Plex's own write lock — every page queued behind it. It is cached now, concurrent misses collapse into one read, and a failed refresh serves the last good answer instead of an error. - **Turning a row off asks first.** The next run takes that row off Plex for everyone who has it, which a bare switch gave no hint of. Turning one back on is unchanged — it removes nothing. - **Rename only offers itself when the name has changed**, and now does the rename rather than showing you another button to press. Pressed on an unchanged name it used to rewrite every collection, for every person, to the name they already had. - Warnings look like warnings, small grey text is readable, and the settings are explained in words a first-time user already knows. ### Changed - The read-only Plex audit moved out of the Danger zone into Advanced — it changes nothing, and filing it under a destructive heading made the safest control on the page look like the riskiest. - Renaming moved off the Rows list and into the editor, beside the name it changes. ## [0.1.0-beta.9] - 2026-08-02 ### Added - **Rows choose their own order.** Best match (the ranking, unchanged and still the default), Highest rated, Newest, or Shuffled. Plex only sorts a collection by release date, alphabetically, or by a custom order, so every one of these is applied by Shortlist and written as that custom order — which is what the Home row displays. - **Highest rated can use IMDb, Trakt, Rotten Tomatoes or Metacritic** instead of TMDB, via MDBList (Settings → Finding titles → "Rated by", also editable straight from the row editor). TMDB needs no setup and costs no lookups. Without an MDBList key, or once its daily quota is spent, a row falls back to TMDB for its whole ordering rather than sorting half of itself on one scale and half on another. ### Fixed - **A "Because you watched X" row now follows the watch it names.** Its title renders from the top pick, and every refresh carried that pick forward — so the row stayed named after the first thing that ever seeded it while newer watches quietly filled its tail. It now rebuilds around the new watch and renames itself when the seed moves. That template also refreshes nightly, since an eight-day cadence kept it naming last week's film for a week. - **The top of a row moves again.** A refresh kept the strongest two-thirds pinned to the head, so on a 20-title row thirteen positions could never change however the candidates scored. Survivors and newcomers are now ranked together, so a better new suggestion can reach the front. - **The library picker no longer ticks libraries a row never builds in.** An empty selection means "every library of this row's type"; it was drawn as every library, so a movies-only row showed its TV ones ticked — and touching them flipped the row to cover both, which on a one-seed row silently built an empty collection. ### Changed - **The row editor is five decisions instead of nineteen.** Name, who gets it, order, schedule and size stand alone; artwork, what it draws on, where it appears and requests fold away, each captioned with its current values so a closed section still answers "is what I want in here?". - Several settings now say what they do: "Make this a 'watch it again' row" (was "Lead with things they've seen"), "Watches the AI searches from" (was "Recent watches to search", and it is hidden on rows that do not use AI web search, where it did nothing), and freshness now says it decides _which_ titles a row holds rather than the order they appear in. ## [0.1.0-beta.5] - 2026-07-22 ### Fixed - **The run page really does show where each pick came from now.** There were three places that build a pick, and the run page renders the one that was still missing provenance — a stored per-(row, library) breakdown, not the picks table. beta.4 fixed the renderer; the data feeding it was still blank. - **Existing runs explain themselves too.** Provenance is joined onto the breakdown from the picks rows when a run is read, so runs recorded before this don't stay blank until they're rebuilt. A pick with no matching row stays blank rather than being given an invented source. ## [0.1.0-beta.4] - 2026-07-22 ### Fixed - **The run page now shows where each pick came from.** beta.3 added the "suggested by TMDB · loosely related" line, but the run detail page renders its picks with its own component — so the line appeared on the user page and nowhere else, including the one screen people open to ask exactly that question. Picks kept from an earlier run still show nothing, which is correct: those were written before provenance was recorded, so it genuinely isn't known. They gain it the first time they are rebuilt. ## [0.1.0-beta.3] - 2026-07-22 Picks that actually resemble what you watched. ### Ranking A beta user's row seeded by **The Pitt** — a medical drama — came back as The Sandman, Servant, Torchwood and King & Conqueror. TMDB was not at fault: its recommendations for that show are ER, Chicago Med, Grey's Anatomy, Code Black, The Good Doctor. Shortlist was reading the right list and picking from the wrong end of it. - **TMDB's ordering is no longer thrown away.** Suggestions were pooled into one bag, so "#1 closest match" and "#19, loosely related" arrived indistinguishable — and `/similar` (keyword matching) was weighted the same as `/recommendations` (what people actually watch together). - **Ranking now asks whether a title is similar, not just well-rated.** With position discarded, the only thing separating candidates was TMDB's average vote — which on real data put _Traitors_, a reality competition show, at the top of a medical drama's row. - **Genre coherence.** Position alone wasn't enough: TMDB tags The Pitt simply "Drama", as it does nearly everything it suggests. But Torchwood and The Sandman are _also_ "Sci-Fi & Fantasy", and that foreign genre is the whole difference. Sources with no ranking of their own — discover, Trakt, the AI sources — are unaffected. They are deliberate picks, not the tail of a list. ### Rows can be short now Filling a half-empty row from the tail is how a weak association became a delivered title. Padding now draws only from candidates that are genuinely related, so **a row may come up short** — four titles that fit beat ten where six are filler. The run log says so, naming the closest rejected title, so a short row reads as the filter working rather than a failure. ### Where every pick came from Each pick records the source that surfaced it and how strongly that source vouched, shown under the title: ``` #3 The Sandman — Because you watched The Pitt suggested by TMDB · loosely related ``` Nothing claims a strength it didn't measure: sources that don't rank their suggestions say only who suggested it. The run log carries the same per row at DEBUG — every pick with its seed, source and affinity — so a "why did it pick that?" report is answerable from a downloaded log. ### Also - Release tags now publish `:dev` as well as `:latest` and the version tag — a tag is cut from `dev`, so `:dev` was being left a build behind. ## [0.1.0-beta.2] - 2026-07-22 Second beta. Mostly the things the first beta's users ran into. ### The owner is a user now - **You get a row too.** Shortlist only ever built rows for accounts you _share with_, so on a one-person server it did nothing at all — plex.tv's user list never includes the account that owns the server ([#1]). The owner is now synced like anyone else, disabled by default so an existing install gains a switch rather than a row appearing unannounced. Their watch history is read from the PMS local account, which is named after your plex.tv **username**, not your display title. - **The honest caveat, stated up front.** Plex cannot hide a collection from the server owner, so your own Home shows _every_ user's row. The app says so where it matters instead of leaving you to discover it. ### Say why, not just what - **Every skip explains itself** ([#3]). "Skipped" used to be the whole message. A run now records the reason per person — no watch history yet, no candidates survived filtering, the row's libraries don't match their share — and shows it in the run detail. - **A failed run names the account that blocked it** and what went wrong, rather than "promotion skipped — a privacy sync failed this run". - **A skipped person is no longer counted as a success.** Three skipped users reported as "3 succeeded". ### Logs, in the app - **A Logs view** — filter by level, search, follow live, copy, or download every log file as a zip. Built because diagnosing the first beta meant asking people to fish `logs.log` out of a container. - **Redacted before you ever see it.** Plex tokens, bearer credentials and provider API keys (Anthropic, OpenAI including `sk-proj-`/`sk-or-v1-`, Google, xAI, Groq) are stripped from every line served, copied, or exported — the whole point of the view is that the output is shareable. ### Rows and users - **Nicknames** ([#4]) — call someone what they're actually called in a row title, without touching their Plex username. The label never moves, so their row stays private. A Tautulli rename now renames the collections already on Plex instead of leaving a stale duplicate. - **Watch history is scoped to the row's own libraries.** A row built from your 4K library was seeded by what you watched anywhere, so its picks could be shaped by history from a library that row never touches. ### One local-AI provider - **"Local / OpenAI-compatible" replaces the separate Ollama and OpenAI-compatible options** ([#7]). llama.cpp, LM Studio, vLLM, LocalAI, Ollama and OpenRouter all speak the same `/v1/chat/completions`, so one provider with a base URL covers all of them. Existing Ollama setups migrate automatically. A bare host gains `/v1` for you; **Test** lists your models instead of making one generate, so it answers instantly. - **It now survives the servers it exists for.** The request degrades from OpenAI's strict JSON-schema mode to plain JSON mode to neither, since older local builds reject the strict form outright; and a blank **Model** resolves to a chat model the server actually reports, rather than OpenAI's default (which vLLM and LM Studio reject) or the alphabetically-first name (which on a stock Ollama box is an embedding model that cannot chat). ### Also - The users roster can be re-synced after setup, not only during it. - Unraid Community Applications template and CA profile. - CI tests only Python 3.12 — the version the image actually ships. [#1]: https://github.com/stevezau/shortlist/issues/1 [#3]: https://github.com/stevezau/shortlist/issues/3 [#4]: https://github.com/stevezau/shortlist/issues/4 [#7]: https://github.com/stevezau/shortlist/issues/7 ## [0.1.0-beta] - 2026-07-21 First public beta. Everything below ships in this release. ### Personalized rows - **Engine** — the full nightly pipeline per user: watch history (Tautulli, with a per-user fallback to Plex's own history; episodes de-duplicated to distinct shows) → candidate sources → heuristic ranking → optional LLM curation → per-user collection delivery → merge-only share-filter privacy sync with snapshots. - **Candidate sources** — TMDB similar, TMDB discover-by-taste, Trakt related titles, "AI suggests from your library", and **AI web search** for current/well-reviewed titles (via the curator's own web search or an Exa key — the latter also gives a local Ollama model web search). - **Optional AI curator** — Anthropic / OpenAI / Google / Ollama, with a fetched model picker; or **None** (heuristic mode), the default. The curator only ever picks from titles verified to exist in your library, and writes the one-line "Because you watched X" reason. - **Multiple rows + shared rows** — several rows per person and server-wide shared rows, each with its own sources, size, libraries, curation style/prompt, audience, schedule, placement, and poster. _Later removed: per-row curation styles and prompts, the AI curator and "AI suggests from your library" were withdrawn before 0.1.0-beta.9, when ranking and reasons moved into code. Per-row AI instructions for AI web search returned in 1.10.0._ - **Freshness as a cadence** — rows stay stable and refresh every N days (nightly → fortnightly), so a person's row isn't reshuffled every night; unchanged rows skip the Plex write entirely. - **Row placement** — choose the Plex shelf (Home / library Recommended / both) and position, per row; coexists with other shelf-ordering tools. - **Custom / AI row posters** — upload artwork or generate it from text (with `{user}` / `{library_name}` placeholders), reusing your AI key; cached across runs. ### Privacy & safety - **Leak-safe row privacy** — each row is labelled `shortlist_`; a `label!=shortlist_` exclusion is merged (read-modify-write, never rebuilt) into every other account's share filter. Rows are swept/delivered **unpromoted**, exclusions merged, and only then promoted — a row is never visible before the exclusion that hides it exists. - **Provable uninstall** — restores every user's share filters from the snapshot taken before the first restriction write, and deletes only `shortlist_*`-labelled collections; dry-run preview. - **Safe mode** — `SHORTLIST_DRY_RUN=1` forces every run to dry-run (writes nothing to Plex) — try it against a real server first. - **Secrets** — Plex tokens and LLM/API keys encrypted at rest (Fernet), redacted in the UI, never logged. ### App - **Web app** — FastAPI backend (SQLite, APScheduler, SSE) + React SPA: an impact dashboard (delivered vs actually-watched hit rate), users, rows, live run activity, requests inbox, and a first-run onboarding wizard. Programmatic API token for automation. - **Login with Plex** — PIN flow, owner-only sessions, CSRF-protected mutations. - **Requests** — an approval inbox for wanted-but-missing titles, optionally auto-sent to Sonarr/Radarr, with a choice of rating source (TMDB, or IMDb/RT/Metacritic/Trakt via MDBList). Each entry shows which person and row wanted it and why; a **Sent** log records what went out. Rejected titles are never re-queued. - **Packaging** — multi-arch Docker image (GHCR), compose example, Unraid template, healthcheck, PUID/PGID, configurable PMS timeout (`plex.timeout_s`). ### Notes - The label-based share exclusions require PMS **≥ 1.43.2.10687** (older builds ignore the exclusion). The setup wizard shows the server version but never blocks a run over it. - Collections without a `shortlist_*` label are never modified or deleted (Kometa coexistence). - Plex cannot hide collections from the **server owner** — the owner's own Home shows every row.