Skip to content

Releases: jshields-ca/LatestArr

v0.12.0

Choose a tag to compare

@github-actions github-actions released this 13 Oct 02:59
9ee4fab

Highlights

Roles, backups, and a lot more control over every issue you send

  • Built for more than one person. Give people the admin, editor, or viewer role. Forgot your password? Forgot password? emails a reset link, and a recovery command works even without email. Signed-in users can Link SSO to their account from their profile.
  • Backed up for you. LatestArr now backs up its database every day and before every upgrade, and keeps the backups you choose. The Backups page shows at a glance whether you're covered, and gives you the restore commands ready to copy. Point BACKUP_PATH at another disk or a NAS.
  • Make every issue yours. Add your logo (with a dark mode version), choose colours for labels, buttons, and links, and style every button, including Watch on Plex. Format the intro and footer note with bold, links, and lists. The design editor now follows the order of the email, and each section says what's set. Subjects can now fill in the date, period, and counts, like "{count} new from {period}".
  • Recipients and sending, sorted. Import from a CSV or Excel file with a groups column, see exactly what will change first, and undo the last import. If a send only reaches some people, Send to the rest finishes it without duplicates. Saving works the same way everywhere, including a recipient's groups. The image now runs natively on ARM (Apple Silicon, Raspberry Pi).

Before you upgrade

  • LatestArr backs itself up before upgrading, and migrations run automatically. Keep a copy of your ENCRYPTION_KEY somewhere safe; backups don't include it.
  • Everyone who could sign in before is an admin. Review the Users page and give people the editor or viewer role where that fits.
  • Recipient imports now update names of people already on your list. Before, those rows were skipped.
  • On an ARM host, you can drop any --platform linux/amd64 you added to run earlier versions.

What's changed

New

  • The container image now runs natively on ARM: Apple Silicon Macs, Raspberry Pi 4 and 5 (on a 64-bit OS), ARM NAS boxes and cloud servers. Docker picks the right image for you, so docker compose pull just works, with no --platform linux/amd64 emulation. (#323)

  • Automatic backups. LatestArr now backs up its database every day at 03:00 and keeps the last seven, with no downtime. It also backs up before every upgrade. The new Backups page (admins only) changes the schedule and what's kept, and can Back up now, download, or delete a backup. Point BACKUP_PATH at another disk or a NAS so a failed disk can't take both the database and its backups. To restore one, run docker exec -it latestarr node dist/cli.js restore <file> and restart. (#277)

  • More colour and button options for designs, under Colours and font and Buttons in the design editor. (#312)

    • More colours: choose your own colours for type labels (text and background) and for links in the intro and footer note, and under Buttons, the button colour and button text. Each follows the accent until you set it, and the × puts it back, so existing designs, Default included, look exactly as before. Custom label and link colours get dark-mode versions too.
    • Button style: rounded, square or pill shape, regular or small, for every button. Your own buttons and "Where to watch" buttons can each be filled or outlined.
    • Contrast check: the editor warns when a colour pair would be hard to read (below WCAG AA, 4.5:1). It doesn't stop you saving.
  • Add your logo to the top of a design, under Logo in the design editor. (#315)

    • Upload a PNG, JPEG or GIF (up to 1 MB). It's included in each email, so it shows even where email apps block images from the web. Or use an image URL, either copied into each email (the default) or linked.
    • Link it to your server's site, Overseerr, or your Plex or Jellyfin app.
    • Put it above the newsletter's name or in place of it, aligned left, centre or right, at the width you choose. It shrinks to fit on phones.
    • Add a dark mode version for logos with dark lettering, which would otherwise disappear on dark backgrounds. Switch the preview to Dark to check it.
    • Code designs can place it themselves with {{logo}}.
  • A design's Intro and Footer note support simple formatting and alignment. (#311)

    • Formatting: bold, italic, strikethrough, [links](https://example.com) (in the design's accent colour), and bulleted or numbered lists.
    • Alignment: each note can be left, centre or right aligned.
    • What stays the same: existing notes look as they did, and HTML typed into a note still shows as text. One thing to check: a * or _ around words, or a line starting with - or 1., now formats.
  • A way back in when you forget your password. Choose a System email profile on the SMTP Profiles page, and the sign-in page gets a Forgot password? link that emails you a one-time reset link. Without email, there's a recovery command for whoever runs the server: docker exec -it latestarr node dist/cli.js reset-password --email you@example.com asks you for a new password. Another admin can still reset your password from the Users page. (#276)

  • A groups column in a recipient import adds each person to those groups, and creates any that don't exist yet. (#322)

    • Put several groups in one cell separated by ;, or by commas in a file separated by semicolons. Groups match by name, ignoring case and spaces.
    • The review lists each group: new or existing, and how many people the import adds to it. Nothing changes until you choose Import.
    • An import only adds people to groups. It never removes anyone.
    • The template file now includes a groups column.
  • Import recipients from an Excel file, and undo the last import. (#325)

    • Excel: upload an .xlsx file. Its first sheet is read like a CSV, with the same review before anything changes. Older .xls files get a message asking you to save as .xlsx or CSV.
    • Undo last import appears next to Import for 24 hours, or until your next import. It removes the people and groups the import added, takes people back out of the groups it added them to, and puts back the names it changed. Anything changed since is kept, and Undo tells you what. A person is also kept if they've been sent a newsletter since, so their history stays.
  • Send to the rest finishes a newsletter that only reached some of its recipients, without sending anyone a duplicate. Use it when LatestArr stopped partway, the mail server turned some people away, or a rate limit cut a send off. Open the send's Details in the newsletter's History and choose Send to the rest. It sends the same email, with the same items, subject and images, to only the people who didn't get it, including anyone whose delivery failed. A confirmation shows who it will go to, and their results are added to the same send. It works on the newsletter's latest send, within its lookback window, and is for editors and admins. (#309)

  • A quiet Sponsor link in the web app's footer, and a line under Star on GitHub in the star popover, both linking to GitHub Sponsors. They're plain links: no banners, pop-ups or reminders, and nothing in the newsletters people receive. (#326)

  • Link SSO to an existing account from your profile. (#320)

    • Sign-in from another address now works: Continue with SSO first moves you to the address in OIDC_REDIRECT_URI, where your identity provider sends you back.
    • Link SSO in your profile connects your SSO account to the account you're signed in to. It works even when your provider doesn't mark your email address as verified, which stopped automatic matching before. Unlink SSO is there too, for accounts that also have a password.
    • Plain messages: a failed SSO sign-in now goes back to the sign-in page with a reason, not a page of raw JSON, and the logs say exactly why.
  • Subject variables, so each issue's subject can change with each send, such as "The latest from {period}" or "{count} new: {counts}". (#327)

    • Variables: {newsletter}, {date}, {shortDate}, {period}, {month}, {year}, {count} and {counts}. They're filled in on each send in the newsletter's timezone, so the subject matches the dates in the email.
    • In the form: Insert a variable adds one at the cursor, and a live line shows the next subject (with example counts). A misspelled variable like {dat} is flagged as you type, and saving refuses it, so a typo never reaches an inbox.
    • Preview and history: Preview shows the real next subject, Send test uses it (with [Test]), and History records the subject as sent.
    • Nothing new: a subject built around {count} or {counts} becomes ": nothing new" instead of "0 new".
    • Unchanged: a blank subject still uses the newsletter's name, and existing subjects work exactly as before.
  • Users can now be admins, editors, or viewers. Viewers see newsletters, designs, previews, and send history without being able to change anything. Editors can also create and send newsletters, edit designs, and manage recipients. Admins can do everything, including sources, SMTP, notifications, users, and log...

Read more

v0.11.2

Choose a tag to compare

@github-actions github-actions released this 29 Sep 13:20
5f9dbc2

Highlights

A new icon

  • Meet the stamp. LatestArr's icon is now a postage stamp with an L, lifted off a rose tile. It replaces the envelope and sparkle in the app, your browser tab, and the social previews.
  • Add it to your home screen. LatestArr now has proper icons for phones and desktops, so it looks right when you pin it or add it to your home screen.

What's changed

Improved

  • LatestArr has a new icon: a postage stamp with an L, lifted off a rose tile. It replaces the envelope and sparkle in the app, the browser tab, and the social previews, and comes in light and dark versions. The app now also has icons for adding it to your phone's or computer's home screen. (#270)

Upgrading

docker pull ghcr.io/jshields-ca/latestarr:v0.11.2

Or, with Docker Compose: docker compose pull && docker compose up -d.

Full changelog (including technical details): v0.11.1...v0.11.2 · CHANGELOG.md

v0.11.1

Choose a tag to compare

@github-actions github-actions released this 29 Sep 06:27
5a9b399

Highlights

LatestArr has a home: latestarr.app

  • Docs one click away. The web UI footer now links the new website, the docs, and GitHub Discussions, and the Add source dialog links a setup guide for each source type.
  • A tidier email footer. The Default design's footer now links LatestArr.app instead of GitHub, and its small icons finally appear. They never showed up before.
  • A clearer start-up error. If ENCRYPTION_KEY is missing or invalid, LatestArr now stops straight away and tells you how to fix it. Before, it started anyway, and saving a source failed with a vague error.
  • Questions welcome. GitHub Discussions is open for questions, ideas, sharing your designs, and reports on sources that still need testers.

Before you upgrade

  • Check that ENCRYPTION_KEY is set in .env. Every working install already has it. If the container stops right after upgrading, run docker logs latestarr: the message says what's wrong with the key. If you've already saved sources, restore your original key rather than making a new one, or they can't be read.

What's changed

Improved

  • LatestArr now points to its new home at latestarr.app. The web UI footer links the website, the docs, GitHub Discussions, issue reporting and the license, and credits LatestArr's contributors, as the site does. The Add source dialog links each source type's setup guide, and the Default design's email footer links LatestArr.app instead of GitHub. (#258)

Fixed

  • LatestArr now checks ENCRYPTION_KEY when it starts. If the key is missing or invalid, it stops with a message saying how to generate one. Before, it started anyway, and saving a source or SMTP profile failed with a generic error. (#259)

Upgrading

docker pull ghcr.io/jshields-ca/latestarr:v0.11.1

Or, with Docker Compose: docker compose pull && docker compose up -d.

Full changelog (including technical details): v0.11.0...v0.11.1 · CHANGELOG.md

v0.11.0

Choose a tag to compare

@github-actions github-actions released this 29 Sep 01:51
5a1d28a

Highlights

Design your newsletters, manage them as a team, and a security pass for internet-facing installs

  • Design without code, or with it. The new Designs page replaces Templates: pick colours, font, layout, and what each item shows, with a live preview, or switch a design to code for full control. The refreshed Default design groups items by type, puts a show's new episodes on one row, adds Watch on Plex-style buttons for your sources, and looks right in dark-mode email apps.
  • More sources. Jellyfin and Emby join Plex, Tautulli, BookLore, BookOrbit, Grimmory, Audiobookshelf, and RomM. Import users with their emails from any of them. Several sources are marked 🧪 Needs testers in the README; if you run one, please tell us how it goes.
  • Know before, and after, it sends. Preview exactly what the next newsletter will contain, skip sends when there's nothing new, and get alerted by email, Discord, Slack, ntfy, Apprise, or webhook when a scheduled send fails.
  • Room for more admins, and safer on the internet. A new Users page lets you add people with temporary passwords, deactivate them, and link them to SSO. A security review hardened sign-in, sessions, and outbound requests, and the container no longer runs as root.

Before you upgrade

  • Back up your latestarr-data volume first, as with any upgrade. Everything below is migrated automatically when the new version starts.
  • Templates become designs. Templates built with the old drag-and-drop editor turn into code designs that send exactly as before. Each newsletter's intro, footer note, buttons, and font move into its design.
  • Designs saved before this release keep their settings. To get the new sections by type, turn on Group by type in the design.
  • The container now runs as a regular user, not root. On first start it takes ownership of /app/data, so existing volumes keep working. If you run it with your own user: setting, that user needs write access to the data folder.
  • TRUST_PROXY=true now takes effect. Before, Docker Compose never passed it to the container. Only set it if LatestArr is behind a reverse proxy.

What's changed

New

  • Designs can be edited as code. On any design, Edit as code switches it to hand-written MJML. You start from exactly the markup its options produce, so you never start from a blank page. The code editor highlights Handlebars tags, flags mistakes on the line where they happen before you save, and sits beside the same live preview. A Variables and helpers panel lists everything you can use. The intro, footer note, and buttons stay in Text and buttons, so they work the same in code designs. (#210)
  • Several new episodes of the same show now share one row, like "The Daily Show · 4 new episodes", with each episode listed underneath, so a week of a daily show doesn't fill the email. It's on by default, and you can switch it off in a design's Sections settings. Designs you saved earlier keep their "Group by type" setting; turn it on in the design editor to get the new sections. (#226)
  • Design how your newsletters look without drag-and-drop. The new Designs page (it replaces Templates) lets you pick colours, font, and layout (cards, compact list, or grid); choose which details each item shows; group items by type in your own order; decide what an empty section shows; add a "Most watched" section; and add custom CSS. A live preview updates as you change things, with sample content or any of your real newsletters. Existing newsletters look exactly as before: they use the Default design, which you can duplicate as a starting point. (#210)
  • Jellyfin and Emby sources, for movies, TV episodes, books, and audiobooks. Add one with an API key. Newsletters show each item's poster, details, and a link that opens it in your server's web app, and you can import the server's users as recipients. These need testers: they're built from the published API docs but haven't been tried on a real server yet. If you run Jellyfin or Emby, please tell us how it went. (#211)
  • Preview a newsletter before it goes out. The new Preview button shows exactly what the next send would contain, with real items, images, and your intro and buttons, without emailing anyone. From the same window you can send a test copy to just yourself. (#202)
  • Get alerted when a scheduled newsletter fails. A new Notifications page can email you through one of your SMTP profiles, post to Discord, Slack, ntfy, or Apprise, or send generic JSON to your own endpoint. It covers sends that fail entirely, sends that only reach some recipients, and newsletters that can't send at all, for example because their SMTP profile was deleted. Use Send test alert to check a destination before saving. (#204)
  • A newsletter can skip its scheduled send when there's nothing new, so recipients don't get an empty email. It's on for newsletters you create from now on; existing newsletters keep sending as before until you turn it on under Delivery. Skipped sends show in History as "Skipped (nothing new)", and Send now always sends. (#203)
  • More than one person can manage LatestArr. The new Users page lets you add people with a temporary password, which they replace with their own the first time they sign in. You can also reset passwords, deactivate or reactivate accounts, and delete users. Everyone has full admin access for now. With SSO set up, add someone with the email their provider uses, and they can sign in with SSO straight away. (#230)
  • Newsletters can now show where to watch. Designs add a button for each linked source that has a public URL, such as Watch on Plex, Read on BookLore or Play on RomM. You choose where the buttons go: after the items, under each section, or near the top. You can also place your own buttons above the intro, below it, or at the end. (#238)

Improved

  • The Logs page now records what's actually happening: every newsletter send and how many people it reached, recipients that couldn't be delivered to, sources that couldn't be reached, sign-ins and failed sign-in attempts, and each settings change along with who made it. A new LOG_LEVEL setting controls how much detail is kept. (#177)
  • The Default design has a fresh look to match the LatestArr app. The newsletter sits on a white card with an accent bar, and the header shows the date range, how many items are new, and a count for each type. Items are grouped into Movies, TV, Books, and so on, with smaller posters, tidier spacing, and summaries trimmed to about two lines. (#226)
  • The old drag-and-drop template editor is gone, replaced by designs. Templates you built with it become code designs automatically. They send exactly as before, and you can now edit them in the code editor with a live preview. The web app is also much smaller: the drag-and-drop editor alone was a 2.3 MB download. (#210)
  • Import users works for every source that has users, including Plex, Jellyfin, and Emby. Those servers don't share email addresses, so the import list now has an email field next to anyone without one; fill it in and that person is imported too. Audiobookshelf and RomM sources can now import their users as well, with the emails those servers already have. For Emby, a user linked to Emby Connect with an email address is filled in for you. (#214)
  • Sources now show each service's own logo, including Jellyfin and Emby, and the Source type list is alphabetical. The setup checklist also suggests failure alerts and a second admin, both optional. (#236)
  • The Docker image now runs on Node.js 24, the current long-term support release. Node 22 stops getting security fixes in April 2027. Nothing changes for your setup: pull the new image and restart, and your data and settings carry over. (#215)
  • Building or developing LatestArr from source now needs Node.js 22 or newer. Node 20 reached end of life in April 2026. Nothing changes if you run the Docker image, which already uses Node 22. (#188)
  • A security review for installs exposed to the internet. The container no longer runs as root, and someone given a temporary password must change it before they can do anything else. Sources and mail servers that stop responding now time out instead of holding up a send, and server errors no longer show internal details. A new guide covers exposing LatestArr safely. (#239)
  • The web UI works better on phones. The Recipients table shows each email under the name instead of cutting it off, dialogs fit the screen and scroll when they're long, buttons no longer squash together, small icons are easier to tap, and iPhones no longer zoom in when you tap a text field. (#210)
  • The Add source dialog's example name and addresses now match the source type you pick (for example http://localhost:8096 for Jellyfin), instead of always showing Tautulli's. A design switched to code no longer opens with about 25 lines of generated dark-mode st...
Read more

v0.10.0

Choose a tag to compare

@github-actions github-actions released this 23 Sep 02:08
66591a4

Highlights

Easier recipient management, richer newsletters, and a new Logs page

  • Bring your people in faster. Import recipients from a pasted list or a CSV, or straight from your Plex or Tautulli users, and manage group membership right from the recipient's edit dialog. The Recipients page is now a searchable, sortable table.
  • Say more in each newsletter. Add an intro note, a footer note, and up to four buttons (for example, a link to your Plex app). The default template also has larger, easier-to-read text, and you can pick its font.
  • See what happened. Send history now shows who each newsletter went to, what it included, and a copy of what was sent. A new Logs page shows recent server activity without needing docker logs.

What's changed

New

  • Import a Plex or Tautulli source's known users as recipients, grouped together, from the Sources page. (#147)
  • Newsletters can now include an intro note, a footer note, and up to 4 call-to-action buttons (e.g. a link to your Plex/Jellyfin app, a donation link, "browse the library") in the default template. (#173)
  • A Logs page in the web UI shows recent server activity — failed sends, source sync errors, auth events, and anything else worth troubleshooting — without needing to docker logs the container. (#149)
  • Bulk-import recipients by pasting a list of emails or uploading a CSV, instead of adding people one at a time. (#146)
  • Recent Sends history now shows exactly who a send went to and what it included, with a link to the actual rendered copy — not just aggregate counts. (#148)
  • The Edit recipient dialog now lets you add or remove group membership — and create a brand-new group — without leaving the dialog. (#169)

Improved

  • The expanded newsletter row is now organized into two clearly labeled panels — Schedule (with its own Delivery subsection) and Content (Template/Sources/Groups) — instead of one long stack of boxes, and the collapsed row's schedule line now highlights the frequency and time instead of reading as one flat muted sentence. (#161)

  • The Logs page now looks and behaves like an actual console — a dense, monospace-font scrolling view instead of separated card panels — and can auto-refresh live instead of requiring a manual Refresh click. (#156)

  • The default newsletter template now looks more like LatestArr — bigger, more readable text, warmer Bloom-tinted colors instead of generic gray, a restyled footer with small GitHub/Report-an-issue icons — and each newsletter can now pick which font it sends with (Ubuntu, Arial, Georgia, or Verdana). (#165)

  • The dashboard's setup checklist collapses to a thin, full-width bar once setup is complete, positioned above the stat tiles instead of sharing a half-width row with Recent sends — and every dashboard card now uses the same circular icon-chip treatment instead of a mix of bare icons and colored tiles. (#144)

  • The Recipients page is now a dense, sortable table with a search box and real pagination — click Name/Email/Status to sort, search narrows results live, and large lists page 25 at a time — instead of one long scrolling list of cards. (#159)

  • The newsletter row is easier to act on: "Send now" and the "Enabled" toggle now sit in the row header next to Delete, so neither needs the row expanded, and Send now no longer sits crowded next to "Save changes". (#171)

  • The Dashboard's "Recent sends" card now shows the same expandable send detail as the Newsletters page's History tab — included items, per-recipient delivery status, and a link to view a copy of the sent HTML — instead of just a status line. (#157)

  • The Edit SMTP profile dialog is now split into Connection / Authentication / Sender identity tabs instead of one long scrolling form. (#143)

Fixed

  • A book's release year from Book Orbit/BookLore could show one year earlier than its actual publication year, for anyone self-hosting in a timezone west of UTC. (#170)
  • The page footer now stays pinned to the bottom of the browser window on short pages instead of sitting mid-page above empty background. (#139)
  • The Recipients table's Name column no longer reads as cramped against the left border. (#169)
  • SMTP profile cards now correctly label STARTTLS connections (e.g. port 587) instead of showing a misleading "No TLS", and give the "Authenticated" badge success styling instead of neutral. (#142)

Upgrading

This release includes database changes. They're applied automatically the first time the new version starts. Back up your data volume first if you want an easy way to roll back.

docker pull ghcr.io/jshields-ca/latestarr:v0.10.0

Or, with Docker Compose: docker compose pull && docker compose up -d.

Full changelog (including technical details): v0.9.0...v0.10.0 · CHANGELOG.md

v0.9.0

Choose a tag to compare

@github-actions github-actions released this 21 Sep 19:57
fec9891
  • Improved: Button hovers and the "Star on GitHub" icon now feel smoother and easier to notice at the same time — a longer, gentler light sweep on buttons, and a clear pop-and-glow on the star when you hover it.
    Technical details Addresses production feedback that round 1's button animation was "clunky and abrupt" while the GitHub-star hover was "too subtle" — two different problems, not opposite fixes. Buttons: the primary variant's hover lift (button.tsx) swaps its bouncy cubic-bezier(0.34, 1.56, 0.64, 1) easing (56% overshoot) for a gentle spring, cubic-bezier(0.22, 1.08, 0.36, 1) (~8% overshoot) at 240ms — enough life to feel intentional without the bounce that read as jittery on a 2px lift. .btn-glint's light-sweep pseudo-element (index.css) gets the same easing swap (a pure ease-out-expo curve, cubic-bezier(0.22, 1, 0.36, 1), since a straight-line translateX sweep has no business overshooting) plus a wider/brighter gradient band (30–70% stops instead of 40–60%, 0.45 alpha instead of 0.35) and a longer 0.85s duration (was 0.6s) so it reads as a deliberate, visible sweep rather than a quick flicker. GitHub star (app-shell.tsx's ProjectInfoCard): previously just text-amber-500 → hover:text-amber-400. Now a .star-glow class scales the star up (scale(1.22)) and adds a warm amber drop-shadow glow on hover/focus, plus a one-shot .star-glow-intro keyframe pulse (scale + glow, ~1.1s, 0.5s after mount, both fill mode so it never replays) to catch the eye once on first paint without becoming a nagging loop. Both respect prefers-reduced-motion (animations/transitions disabled, falling back to a plain color change).
  • New: Newsletter items are now clickable — a movie, show, book, audiobook, or game in a sent newsletter links straight to that item on its source (Plex, Tautulli, Audiobookshelf, RomM, or a BookLore-family reader), instead of being plain unlinked text.
    Technical details Two gaps combined to make nothing in a sent newsletter clickable before this: sourceConnections had only one URL column (baseUrl), used both for an adapter's own API calls and as the one place a link was ever rendered (the Media List block's emptyFallback="link" fallback) — which breaks for Tautulli (baseUrl is the Tautulli API host, not a Plex-watchable URL) and for a source whose baseUrl is a Tailscale/LAN address unreachable by an email recipient. And NewItem.externalUrl (packages/adapters/core/src/source-adapter.ts), though already threaded through the render pipeline (RenderableItem.externalUrl in apps/server/src/render/mjml-template.ts), was never actually set by any adapter. Adds an optional publicUrl column to source_connections (packages/db/src/schema.ts, migration packages/db/drizzle/0001_nostalgic_ulik.sql, generated with drizzle-kit generate) — the address a recipient can actually reach, falling back to baseUrl when unset so every existing source connection keeps behaving exactly as it does today. Threaded through SourceConnectionConfig (packages/adapters/core/src/source-adapter.ts) and the Sources API (apps/server/src/http/routes/sources.ts, validated as a URL when present, or an empty string to clear it). Every adapter now builds a real per-item deep link where the source's URL scheme supports it: - Plex: {publicUrl}/web/index.html#!/server/{machineIdentifier}/details?key=%2Flibrary%2Fmetadata%2F{ratingKey} — fetches the server's machineIdentifier from /identity (new getServerIdentity in plex-client.ts) once per fetch, falling back to a plain library link if that lookup fails. An unset publicUrl falls back to building the same link from baseUrl, matching today's behavior for a fully-public single-Plex setup. - Tautulli: the same Plex web deep-link format, built from Tautulli's own get_server_id API (proxying the underlying Plex server's machineIdentifier) and rating_key — shares buildPlexWebDeepLink with the Plex adapter (new export in packages/adapters/core/src/url-utils.ts). Unlike every other adapter here, an unset publicUrl means no externalUrl is set at all (not a baseUrl fallback) — Tautulli's own baseUrl is its API host, never a page a recipient should be sent to, so building a link from it would produce a plausible-looking link to a page that doesn't exist there; this exactly matches Tautulli-linked items' behavior before this release (no link) rather than introducing a new broken one. The get_server_id lookup itself is skipped entirely (not just its result discarded) when there's no publicUrl to build a link from, so a Tautulli connection with no publicUrl set makes no additional API calls at all. - BookLore family (BookLore/BookOrbit/Grimmory): reads an OPDS entry's own rel="alternate"/rel="self" link (new getEntryPermalinkHref in booklore-client.ts) and rebases it onto publicUrl's origin (new withOrigin helper) instead of the internal host the feed itself was served from — but only when the permalink resolves to baseUrl's own origin; a permalink that already points at a genuinely different, distinct host (some catalogs point rel="alternate" at a publisher/mirror page) is left untouched instead of being force-rebased into a broken URL. Falls back to the library root when an entry carries no usable permalink at all. An entry's OPDS <link> is untrusted content from the source server itself, so its scheme is validated (isHttpUrl, new export alongside withOrigin) before ever being used — a javascript:/data: href is treated as "no usable permalink" rather than reaching a sent email's <a href> (Handlebars' default {{}} escaping used to render {{externalUrl}} guards markup characters, not URL schemes, so nothing downstream would otherwise have caught this). An unset publicUrl falls back to baseUrl. - Audiobookshelf: {publicUrl}/item/{itemId}, falling back to baseUrl when unset. - RomM: {publicUrl}/rom/{id}, falling back to baseUrl when unset. The Sources API's publicUrl field is itself restricted to http/https (apps/server/src/http/routes/sources.ts), for the same reason: it flows straight into every adapter's link construction. The default (non-custom-template) newsletter layout (apps/server/src/render/newsletter-template.ts) now wraps an item's title and poster in <a href="{{externalUrl}}"> when present, styled to inherit the surrounding text color with no underline so a linked title doesn't read differently from an unlinked one; falls back to plain text/image otherwise. The Media List block's emptyFallback="link" "browse the library" link (apps/server/src/pipeline/run-newsletter.ts's buildSourceLinksByContentType, consumed by mjml-template.ts) now prefers publicUrl over baseUrl too.
  • Fixed: Every item in a sent newsletter now shows a small badge naming what it is — "Movie", "TV Episode", "TV Season", "Game", "Book", or "Audiobook". Previously only Ebooks, Comics, Audiobooks and Podcasts (from BookLore-family and Audiobookshelf) got a badge; movies, TV episodes, TV seasons and games showed no badge at all.
    Technical details The badge markup in both the default template (apps/server/src/render/newsletter-template.ts) and the GrapesJS-authored one (apps/web/src/lib/grapesjs-blocks.ts's CONTENT_LABEL_BADGE) has always rendered {{contentLabel}}, an optional, adapter-set free-text field on NewItem — only BookLore-family and Audiobookshelf ever set it (to distinguish Ebook/Comic within "book" or Audiobook/Podcast within "audiobook"). The kind field itself (movie/tv_episode/tv_season/book/audiobook/game) was never mapped to a display label anywhere in the render path. Rather than duplicating a label map in both consumers, the fallback is resolved once, upstream of both: apps/server/src/render/mjml-template.ts's toRenderable() (the single function both the default and custom/GrapesJS-authored templates' items pass through before either ever sees them) now resolves contentLabel via a new KIND_LABELS map when the adapter didn't set one, so RenderableItem.contentLabel is always populated. Both templates already just read {{contentLabel}}, so neither needed any change — grapesjs-blocks.ts's badge markup works unmodified. An adapter-set contentLabel (Comic, Podcast, etc.) still takes priority over the kind fallback.
  • Fixed: On pages with little content, the page footer no longer gets pushed off the bottom of the screen behind a wall of empty space — it now sits right after the content, without needing to scroll to find it.
    Technical details Addresses production feedback that there was "a lot of dead space between the bottom of content and the footer" and that short pages forced a scroll just to reach it. The root cause (app-shell.tsx): the content row (flex-1, between the sidebar and <main>) and the sidebar (h-[calc(100vh-3.5rem)], a viewport-relative fixed height) were both independently forcing that row to be at least one full viewport tall on every page, regardless of how much actual content there was — pushing the footer to just past the fold on every page, not only long ones. Fix: drop flex-1 from the content row and the sidebar's explicit h-[calc(100vh-3.5rem)]. The sidebar now sizes via the row's default align-items: stretch, so its border-r divider still spans the full height of whichever of the sidebar/main is taller on any page with real content — no visual change there. The root <div>'s min-h-screen is left as-is, so short pages still fill at least one viewport; any leftover space now falls below the footer (ordinary bottom-of-page whitespace) instead of being forced in above it. Verified visually (headless Chromium against the dev build): a minimal page's document.documentElement.scrollHeight now equals `w...
Read more

v0.8.0

Choose a tag to compare

@github-actions github-actions released this 20 Sep 20:53
7984fec
  • New: Add a single "All New (This Period)" block to the template editor — drop it into a newsletter and it shows everything added recently, grouped by type (movies, TV episodes, TV seasons, books, audiobooks, games), instead of needing to drag in and configure six separate blocks by hand.
    Technical details Addresses the most common request in production feedback on the Media List blocks: "there isn't any options for 'all new' based on the lookback settings in the Newsletter itself... they would expect all of the latest media (audiobooks, tv shows, movies, books, etc) for that lookback period." Getting that today meant dragging in all six content-kind presets one at a time and turning on each one's "Show all items in the period" trait by hand — this is the same result as one block. Dragging in "All New (This Period)" exports one heading + Media List pair per adapter content kind, each reading from the same lookback-scoped items pool a per-kind block already does and each already using the existing, shipped showAll="true" mechanism (from a previous release's "Show all items in the period" trait) rather than a capped count. A kind with nothing added this period is skipped entirely — heading included, not just its (empty) list — via a new ifAnyItems Handlebars block helper (apps/server/src/render/mjml-template.ts) that applies the exact same pool + content-type filter mediaList itself does, purely to decide whether to render a heading. Without it, a newsletter with e.g. no new games this week would still show a bare "Games" heading over nothing. It's registered as its own GrapesJS component type (media-list-all-new), not a second implementation of the card markup: the poster+text <table> layout and the {{#mediaList ...}} hash-argument tag are now both extracted into shared functions (mediaListCardBody/mediaListOpenTag in apps/web/src/lib/grapesjs-blocks.ts) that the standalone Media List block's own toHTML() also calls, so the two can't drift apart. Like the standalone block, its canvas preview is a friendly static summary (real per-kind grouping only happens at MJML export time, against real item data at send time) and has no configurable traits — narrowing to one kind is what the existing per-kind presets are for.
  • Improved: Buttons now feel smoother and more premium to hover over — a subtle light sweep and an easier lift, instead of an abrupt bump.
    Technical details Addresses production feedback that the primary button's hover lift felt "bumpy" rather than premium. The hover-triggered lift/shadow now transitions on a slower, spring-like cubic-bezier(0.34, 1.56, 0.64, 1) curve at 220ms (up from the shared 150ms linear-ish default), scoped to the default variant only via hover: utilities so every other button's snappy press-state timing is untouched. A one-shot diagonal light sweep (.btn-glint in index.css, a ::after gradient translated across the button and clipped by overflow-hidden) plays once per hover-enter rather than looping, and is disabled under prefers-reduced-motion.
  • Improved: The LatestArr logo is more prominent, the version/GitHub/Star links now sit right next to it on mobile too (matching how they already looked on desktop), and the old hidden "About" info icon has been replaced with a real page footer showing the license, the author, and a link to report an issue.
    Technical details Makes the wordmark more prominent (a larger LogoMark, larger tracked-out text with a subtle rose gradient fill) and moves the version/GitHub/Star cluster up next to the Logo in the mobile nav sheet, matching where it already sits in the desktop header — the two surfaces now read as one consistent design. Removes the sidebar's "About" info-icon popover (author/license/site) entirely and replaces it with a real page footer, visible on every page instead of hidden behind a click: author link, GPLv3 license link, a "Report an issue" link to the GitHub issue tracker, and an "In Active Development" badge.
  • Fixed: Saving a template no longer briefly rebuilds the entire editor canvas behind the scenes (harmless today, but wasteful and a source of subtle glitches down the line).
    Technical details Root-caused while investigating a test that failed intermittently on CI (template-editor-page.test.tsx's "warns on tab close/refresh..." test, "expected true to be false"). Two separate issues, found via instrumented reproduction rather than assumed: 1. Real production bug: handleSave's setTemplate(updated) on a successful save gives template a new object identity every time. The GrapesJS-init useEffect was keyed on [template], so this retriggered it — and a dependency-array change always runs the previous run's cleanup (editor.destroy(), editorRef.current = null) before the new run's own editorRef.current guard is even evaluated, so the guard couldn't prevent it. Every save destroyed and fully reinitialized the GrapesJS editor (confirmed via an instrumented test run: mockInit called twice, destroy called once, for a single save). Fixed by keying the effect on Boolean(template) instead — true exactly once, on the null-to-loaded transition — which matches the effect's actual intent ("initialize once, when data first arrives") without discarding the canvas on every subsequent save. Added a regression assertion (mockInit/mockEditor.destroy call counts) to the existing save test. 2. Real test-helper race (this was the CI flake's actual cause, confirmed by reproducing it locally — 2 failures in 60 runs before the fix, 0 in 80 after): simulateEditorContentChange() fired GrapesJS's mocked content-change handlers immediately after await screen.findByText("Weekly Digest") resolved. But findByText's MutationObserver-based resolution isn't guaranteed to happen after every passive effect from the same commit has flushed — the GrapesJS-init effect (which registers the content-change handler at all) is a separate, independently-scheduled effect, so contentChangeHandlers could still be empty at that point, making the simulated "edit" a silent no-op. Fixed by having the shared test helper itself wait for a handler to actually be registered before firing it, rather than relying on each call site to remember to check — the same synchronization other tests in this file already used for mockInit, just not applied here.
  • Improved: It's now easier to see which page you're on — the active item in the sidebar gets a clear accent bar, not just a subtle color change.
    Technical details Gives the sidebar/nav-sheet's active nav item a left-edge accent bar (a 3px bg-primary pill) on top of its existing background tint, and gives the hover state a slightly slower, clearer background transition — production feedback was that the active state alone was easy to miss at a glance. navItems remains a flat list (no section grouping was added, since the current six items don't split into a natural, non-arbitrary category split).
  • Improved: Added a new accent color (a calm blue) used for informational badges like the version number and the "In Active Development" label, giving the app's look a bit more depth.
    Technical details Adds a third palette hue — --tertiary/--tertiary-foreground (indigo/blue, hue ~228) — as a genuine design token alongside --primary, following production feedback that the "Bloom" palette had nowhere to go for calm, informational UI beyond the rose primary and the ad hoc violet secondary accent. Indigo/blue was picked as the clearest complementary/triadic partner to rose while staying clear of green (already success) and red/orange (already destructive, and Plex's own brand color); both the light and dark pairs clear 4.5:1+ text contrast the same way every other token pair in index.css does. Wired into Tailwind as bg-tertiary/text-tertiary-foreground and exposed as a new Badge tertiary variant, used for the header's running-version badge and the new footer's "In Active Development" badge — reserved for informational labels, never a semantic success/warning/destructive state.

v0.7.1

Choose a tag to compare

@github-actions github-actions released this 20 Sep 01:14
ef6f962
  • Bump better-sqlite3 (11 → 12.11.1) — the DB driver, and the last of the deferred major-version Dependabot pass. Runtime dependency, and a native module, so this carries the same "native module" build risk as before across platforms, not just app-level behavior. Landed on 12.11.1 rather than Dependabot's proposed 13.0.3: better-sqlite3 13 dropped Node 20 support entirely (engines: {"node": ">=22"}), which would break this repo's Node 20.x CI job — the same class of issue jsdom 30 had (see the earlier bump-eslint-jsdom changeset). 12.11.1 is the latest release that still supports Node 20.x through 26.x. - packages/db/src/client.ts's usage (new Database(path), .pragma()) and migrate.ts's drizzle-orm/better-sqlite3/migrator are both stable, long-standing API surface — unaffected by the 11→12 bump. - drizzle-orm's own peer range for better-sqlite3 is >=7, so no drizzle-orm compatibility concern either. - The native module installs cleanly (prebuild-install resolved a prebuilt binary, no compile-from-source fallback needed) on this platform/Node combination. - pnpm turbo run lint typecheck build test is fully green (40/40 tasks) — packages/db's own tests (WAL mode, foreign keys, migrations) pass, plus every server route test that touches the DB. - The actual cross-compile for the Docker image's node:22-alpine (musl libc) target isn't something this sandbox can validate directly (no Docker daemon available here) — that's what the PR's own CI "Docker build" job checks for real, same as every other bump in this batch.
  • Bump croner (9 → 10), the scheduler library. Runtime dependency — this is the highest-risk bump in the deferred major-version pass, since a subtle regression here could silently stop newsletters from firing on schedule rather than failing loudly. Extra scrutiny beyond the usual test suite, since apps/server/src/scheduler/engine.ts's missed-schedule catch-up logic (nextScheduledRunAfter) always constructs a real Cron directly — it isn't behind the tests' injectable cronFactory mock, so apps/server/src/scheduler/engine.test.ts's catch-up tests already exercise real croner behavior at exact date boundaries, not a stub: - All 11 scheduler tests pass unchanged, including the three missed-schedule catch-up boundary cases. - Directly verified nextRun(since) is still strictly-after-exclusive of since (matching the code's own documented assumption) at the exact same boundary the tests use ("0 9 * * 1" around 2024-01-01T09:00:00Z). - Directly verified the exact 3-arg new Cron(pattern, options, callback) constructor shape defaultCronFactory depends on, plus .stop(), still work unchanged. pnpm turbo run lint typecheck build test is fully green (40/40 tasks).
  • Bump eslint (9 → 10) and @eslint/js (9 → 10) across every package, and jsdom (25 → 29.1.1) in apps/web's test environment. Dev-tooling only, no runtime dependency changes. The Dependabot PRs for eslint/@eslint/js (#97, #105) failed CI with a stale, out-of-sync pnpm-lock.yaml on their branch — reproduced locally with a freshly regenerated lockfile and confirmed all 40 lint/typecheck/build/test tasks pass cleanly; eslint-plugin-jsx-a11y's declared peer range hasn't caught up to eslint 10 yet, but it lints without error in practice. jsdom's own Dependabot PR (#104) proposed 25 → 30, but jsdom 30 dropped Node 20 support entirely (engines: "^22.22.2 || ^24.15.0 || >=26.0.0"), which broke this repo's Node 20.x CI job with TypeError: webidl.util.markAsUncloneable is not a function — a real Node-runtime incompatibility, not a lockfile issue. Landing on 29.1.1 instead (the latest jsdom release that still supports Node 20.19+) gets most of the version currency without dropping Node 20 CI support, which is a bigger call than a routine dependency bump.
  • Bump react and react-dom (18 → 19) and their @types packages. Dev/runtime dependency only. Low migration risk in this codebase: main.tsx already uses ReactDOM.createRoot (no legacy ReactDOM.render to migrate), there's no direct react-dom/test-utils import (React 19 moved act into react itself), and no component uses defaultProps on a function component (removed in 19). @testing-library/react@16 (already in use) and react-router-dom@7 already support React 19. pnpm turbo run lint typecheck build test is fully green (40/40 tasks, 225 web tests, no React deprecation warnings in test output). Beyond the test suite, built the app and drove it with a real headless Chromium session against the built server (signup → dashboard → navigated Sources/Recipients/Newsletters/Templates → opened the Add Source dialog) — no console errors, no visual regressions, confirmed by screenshot.
  • Bump vite (6 → 8, apps/web only), vitest (4 → 5, every package), and @vitejs/plugin-react (4 → 6, apps/web only). Dev-tooling only, no runtime dependency changes. Bumped all three together since they're an interlocking build/test toolchain — vitest 5 pins a vite 6+ peer, and @vitejs/plugin-react needs to track the vite major it's paired with. pnpm turbo run lint typecheck build test is fully green (40/40 tasks, 189 server tests, 225 web tests); no config changes needed in apps/web/vite.config.ts or any vitest.config.

v0.7.0

Choose a tag to compare

@github-actions github-actions released this 19 Sep 23:13
7784e4a
  • Design system refresh addressing v0.6.0 production feedback: typography, type hierarchy, visual personality, and consistent save/error feedback across every admin page. - Typography: adds Plus Jakarta Sans as the app's UI/body face (via --font-sans), replacing the unstyled default system-font stack. Newsreader — previously reserved for the "LatestArr" wordmark — now also sets each page's own <h1> (a new PageHeader component, used on Dashboard, Sources, Recipients, SMTP Profiles, Newsletters, and Templates) at a larger display size, giving the app an actual typographic voice at the one spot per page where it doesn't compete with body text. - Hierarchy: standardizes the in-page section-label tier (previously plain text-sm font-medium — the same weight as plenty of nearby bold body text, e.g. "Template", "Sources", "Recipient groups", "Send history" on the Newsletters page) into a new SubsectionHeading component: a small uppercase, tracked-out label distinct from both the page <h1> and from bold body copy. Page titles and <h2> section headers were already consistent and are unchanged. - Visual personality: adds violet as a restrained secondary accent (Tailwind's own palette, matching how Badge's success/warning variants and the dashboard's stat tiles already work — no new color tokens) via a new accent Badge variant, used on the dashboard's "Optional" checklist tag and a template's "Designed" status. Floating chrome that has real content behind it to blur — Popover, Select, Dialog/Sheet content, and the new toast notifications — gets a translucent, blurred ("glassy") background instead of a flat opaque one; static cards get a faint inset highlight ring for the same read without the wasted compositing cost of blurring nothing. Every button now gets a quick press-down scale on :active (previously only the primary variant's hover-lift existed, from the original Bloom rebrand), and the primary button's hover-lift transition now animates only transform/box-shadow/background-color/color/border-color instead of transition-all. - Toasts: adds a Radix-based toast system (ui/toast.tsx + ui/use-toast.ts + a Toaster mounted once in App.tsx, following this codebase's existing Radix-wrapper pattern) and wires it into every save/update/delete/toggle/test/send action across Sources, Recipients, Groups, SMTP Profiles, Newsletters, and Templates — including several actions (the Newsletters template picker, group-member add/remove, the enabled toggle) that previously gave no feedback at all, or reused unrelated inline error state. The template editor's existing inline "Saved" indicator is untouched; it now gets a corresponding toast alongside it rather than being replaced. - Reuses the existing SourceLogo component on the Newsletters page's linked-source badges, which were missing the per-source-kind icon that the Sources page itself already shows. All recolored/retinted surfaces were checked against WCAG AA (4.5:1) in both themes, including the new translucent surfaces at worst-case backdrop extremes.
  • Redesign the relationship between the top banner and the sidebar footer, addressing feedback from a production user: the header was mostly dead space (just the theme toggle), the sidebar footer's account email got cut off next to the Edit profile/Sign out buttons, and the Star icon linked straight out to GitHub with no context. The version/GitHub/Star/About cluster and the account summary (name, email, edit, sign out) now live in the desktop header, which has the width to give the email room to stay legible (still truncates gracefully with a title tooltip if needed) instead of the cramped 240px sidebar column; the sidebar itself is now just branding-free navigation. The Star icon now opens a small popover — matching the existing About popover's pattern — with a short explanation of why starring helps, and a button that actually links out to GitHub, instead of linking out immediately. The version badge's text is also a size larger while keeping the same mini-card styling. The mobile nav sheet keeps the same compact card layout as before, unchanged.
  • Addresses the most common piece of v0.6.0 production feedback: newsletters rendering "very plain, no images/posters/covers." Only the RomM adapter ever populated posterUrl — Plex, Tautulli, Audiobookshelf, and the BookLore family all left it empty, so their items never got a poster in the default template or a Media List block, even though the rendering side already supported one. Wires poster/cover art through for all four remaining adapters: Plex maps its thumb field to a token-bearing absolute URL; Tautulli adds the thumb/art fields its API was already returning but the client wasn't reading, resolved through its own pms_image_proxy command; Audiobookshelf maps media.coverPath to its token-bearing cover endpoint; and the BookLore-family client gains real OPDS <link rel="...image"> parsing (preferring the full image over the thumbnail), which it had none of before. Rather than putting a source's own URL (frequently LAN-only, and often carrying that source's credentials as a query param) directly into a sent email, a new pipeline step (apps/server/src/pipeline/embed-images.ts) fetches each item's image server-side at send time — using the same stored, decrypted credentials the pipeline already polls that source with, via a new SourceAdapter.fetchImageBytes() (replacing the previously unimplemented resolveImageUrl) — resizes it to a thumbnail with sharp, and attaches it to the outgoing email as a Nodemailer CID attachment. The rendered HTML references it with <img src="cid:...">; a recipient's mail client never makes an outbound request of its own. This is applied uniformly to the default template and to the GrapesJS Media List block. Every step fails soft, per item: an unreachable source, a failed fetch, or an image sharp can't decode falls back to a small blank-pixel data URI for that one item rather than failing the whole send. Embedding only ever happens for images a Media List block's own selection (count/order/showAll/emptyFallback) actually renders — real posterUrls are swapped for opaque placeholder tokens before the template renders, and only the tokens that survive into the compiled HTML get fetched, resized, and attached afterward. Otherwise a block showing 5 items out of an 80-item library would fetch, resize, and attach all 80 posters even though only 5 ever appear in the email, undermining the whole "keep message size sane" point of CID embedding. Referenced images are also fetched/resized concurrently rather than one at a time, and the "most watched" and "empty-pool fallback" item pools are fetched concurrently with each other rather than sequentially. Every adapter's image fetch (fetchImage/fetchOpdsImage) is also now bounded by a 10s AbortSignal.timeout() — since resolvePosterPlaceholders awaits every referenced item's image via Promise.all, one source that hangs instead of erroring would otherwise stall the whole send indefinitely rather than failing that one item's poster softly. Also replaces every adapter client's baseUrl.replace(/\/+$/, "") trailing-slash trim with a new shared trimTrailingSlashes() helper (@latestarr/adapter-core) — a plain linear scan instead of a regex CodeQL flagged as a polynomial-time ("catastrophic backtracking") pattern on uncontrolled input. Also, from the same feedback: - TV episode titles: a tv_episode item's prominent title was just the bare episode name (e.g. "Winter Is Coming"), with the series name buried in the subtitle — it now leads with the series title, with "SxxExx - Episode Name" as the subtitle, for both Plex and Tautulli. Rendered items also now show a release date alongside the added date when the source provides one. - Content-kind labeling: BookLore-family items are now labeled "Ebook", "Comic", or "Book" (derived from the OPDS acquisition link's MIME type), and Audiobookshelf items "Audiobook" or "Podcast", surfaced as a small badge next to the title in both the default template and the Media List block — without widening the shared MediaKind enum for it. - Smarter Media List blocks: a new "Random order" trait picks random items from the matching pool instead of always the first N; a new "Show all items in the period" trait removes the count cap entirely; and a new "When nothing matches this period" trait offers a random-items-instead fallback (sampled from an all-time pool, only fetched when a block actually needs it) or a plain link back to the source, instead of silently rendering an empty section.

v0.6.0

Choose a tag to compare

@github-actions github-actions released this 16 Sep 14:28
a2a2af1
  • Dashboard polish from testing feedback: recent sends now show which newsletter, item/recipient counts, and outcome per entry (reusing the same Send History status logic as the Newsletters page) instead of just a status badge and timestamp; the setup checklist collapses to a small "Setup complete" summary once every required step is done, expandable again to double-check anything, instead of either vanishing or staying full-size forever; the stat tiles (Sources/Recipients/SMTP Profiles/Newsletters) now use per-tile accent-colored icon chips, a hover lift, and a secondary metric where one is meaningful (open sources needing attention, recipient group count, active newsletters); the checklist and Recent sends cards sit side by side on wide screens instead of stacking narrow and leaving the right side of the page empty; and the destructive/warning/success Badge text colors were adjusted to clear WCAG AA contrast against their tinted backgrounds in both themes.
  • Add regression test coverage in renderMjmlTemplate for a bug found while verifying the template editor's new preset blocks (apps/web): a <table> placed directly under <mj-column> (rather than wrapped in <mj-raw>) gets silently dropped by MJML's compiler under "soft" validation, with no thrown error. No production server behavior changes — the fix itself is in apps/web's block library, which is what emits this markup.
  • Expand the template editor's block library beyond the previous three generic blocks (Header, Footer, Media List) so it's actually discoverable: six new "content-kind" preset blocks (Movies, TV episodes, TV seasons, Books, Audiobooks, Games) let a user drag in a ready-to-go section for a specific *arr media kind instead of dragging the generic Media List and then hunting for its Content type trait afterward — each preset is just the existing Media List component with that trait pre-set, not a second implementation. Also add two standard newsletter-builder primitives the library was missing entirely: a Divider (mj-divider) and a Spacer (mj-spacer). The block panel is now organized under "Layout" (Header, Footer, Divider, Spacer) and "Content" (Media List and its six presets) category headers instead of one flat, unlabeled list. Also fixes a real bug surfaced while verifying the new preset blocks against the actual MJML compile pipeline (not just the editor canvas): the Media List component's exported <table> markup sat directly under <mj-column> with no <mj-raw> wrapper, so MJML's compiler silently dropped it (no thrown error under "soft" validation) — meaning the existing, already-shipped Media List block rendered its section as empty in a real compiled/sent newsletter despite looking correct in the editor. This affects every content type, not just the new presets, and is fixed alongside them since it shares the same toHTML() code.
  • Fix a real send-failure bug: a custom template saved from the builder before it's ever had an initial design loaded exports a bare MJML fragment (no <mjml><mj-body> root element). renderMjmlTemplate now wraps such a fragment before compiling instead of letting mjml2html reject it outright — previously this would fail the entire send rather than falling back to best-effort rendering, the same way a malformed template already does.
  • Newsletter admin fixes from design review and a real send-now test run: newsletter cards now show a human-readable schedule ("Weekly on Monday at 8:00 AM") instead of the raw cron string for schedules the Simple picker can express; the Add newsletter dialog defaults Timezone to the browser's own zone instead of always UTC, and pre-selects the SMTP profile when there's exactly one; the Add/Edit newsletter dialogs group their Schedule and Delivery fields into labeled sections instead of one flat list; a "Send now" that completes with nothing actually sent (no linked source or recipient group) is now visually distinguished from a real send in Send History and on the Dashboard; a genuine send failure (e.g. an unreachable source) now returns a specific, structured error instead of a bare "Internal Server Error", shown inline the moment the send fails; and Send History now refreshes automatically right after a "Send now" click resolves instead of requiring a page reload.
  • Add a proper "Page not found" screen (with a link back to the Dashboard) for unknown routes, replacing the blank content area you'd get from mistyping a URL like /smtp-profiles instead of /smtp. Also fix the first-admin signup form only flagging the Password field when submitted empty — Name and Email now get the same red-border-and-inline-message treatment when they're missing too.
  • Replace the native <select> dropdown with a custom-rendered one built on @radix-ui/react-select. Every other primitive in the app (Button, Dialog, Switch, Input) is already fully themed, but the browser's own OS dropdown chrome still showed through whenever a Select was opened — bright white on Linux/Windows, breaking out of the app's dark, all-custom aesthetic. The dropdown popover now matches the app's dialogs (same border/shadow/radius/animation language) in both light and dark mode. Select's props are unchanged for every existing call site.
  • Restyle the Send History list on the newsletter card so each send-run reads as a distinct, scannable row instead of plain wrapped text: a status icon (matched to the existing badge colors), the timestamp and outcome badge on one line, item/recipient counts and any error message below, all inside a bordered row. Failed and partially-failed sends get a red left accent and tinted background so problems stand out without reading every row. No changes to what data is fetched or when it refreshes.
  • Redesign the sidebar's bottom "about" area (version, GitHub links, author, license, site) as a compact bordered mini-card instead of stacked plain text. The version now shows as a small badge, and the "Jeremy Shields · GPLv3 · scootr.ca" attribution moves behind a keyboard-accessible Info popover (built on Radix Popover) so the footer reads as one deliberate row rather than an afterthought, in both the desktop sidebar and the mobile nav sheet.
  • Simplify the SMTP profile Add/Edit dialog's port and encryption copy. Based on direct user testing feedback, the two-sentence paragraph explaining STARTTLS vs. implicit TLS under the "Use implicit TLS" toggle assumed email-server knowledge most self-hosters don't have. It's replaced with a "Port & encryption" section containing a compact, always-visible reference (587 = STARTTLS, 465 = implicit TLS, 25/2525 = usually unencrypted) and a one-line hint under the toggle, both wired up with aria-describedby for accessibility. The toggle's behavior (including auto-defaulting on for port 465) is unchanged.
  • Sources page polish from testing feedback: each source kind now shows its service's own logo (Tautulli, Plex, BookLore, BookOrbit, Grimmory, Audiobookshelf, and RomM) next to its kind label in the sources list and in the Add/Edit dialog's Source type picker, bundled locally rather than hotlinked; and the OPDS username/password fields for the BookLore-family sources (BookLore, BookOrbit, Grimmory) now have a plain-language hint explaining that OPDS is just the login for that app's own web reader, not a separate API key.
  • Fix five usability bugs in the template editor: GrapesJS's default panel buttons (Open Blocks, Settings, Layers, etc.) rendered as blank squares because their built-in icons depend on Font Awesome, which this app never loads; the editor's selection/active-state accent showed GrapesJS's own stock orange (from the grapesjs-mjml plugin's custom theme) instead of the app's Bloom pink; clicking the Media List block's preview card selected an inner generic child instead of the block itself, hiding its real Content type/Sort/Number of items traits behind an unlabeled "select parent" step; the Move up/down toolbar buttons rendered as thin unicode arrows next to GrapesJS's bold icons and silently no-opped at the top/bottom of a list with no feedback; and the editor had no unsaved-changes protection at all, so a refresh or the page's own Back button silently discarded in-progress work.
  • Fix the "default layout" (no custom template picked) newsletter render falling back to a stale, pre-poster hardcoded template with no images and no MJML compilation. It now renders through the same MJML pipeline as a custom GrapesJS template, so a default-layout send gets the same poster + metadata cards, responsive layout, and Outlook/MSO compatibility.
  • Consolidate the admin CRUD pages (Sources, Recipients & Groups, SMTP Profiles, Newsletters, Templates) onto a shared ListRow component instead of five hand-rolled row layouts that had drifted apart in spacing, hover treatment, and how metadata badges sat next to a row's name. Every row now uses the same leading-icon/primary/secondary/actions shape and the same responsive header layout (stacked on mobile, side-by-side from sm: up), so moving between pages no longer shows subtly different row heights or gaps. Also dedupes the identical ConfirmDelete inline prompt that Recipients and Templates each defined on their own into a shared ConfirmDeleteButton. No page's behavior, click targets, or aria-labels changed — this is a visual/structural consolidation only. Also gives Recipient Groups an Edit dialog (previously groups could only be added and deleted, so fixing a typo in a group's name meant losing its members by recreating it). It mirrors Recipients' own Edit dialog and reuses the PATCH /recipient-groups/:id server route that already existed for it.