Autumn Cleaning

It's been two weeks since the last update, and this one is a bit different. After Journal landed, I had an AI-assisted review go through the whole app, hunting for ways things could go wrong. It found quite a few. 😬

Most of this post is about that, because you deserve to know what was wrong, what is fixed, and what changes for you.

Security fixes

I checked the logs and found no signs of any of this being abused. Still, I'm sorry these were there in the first place. The silver lining is that they're fixed now, and I know a lot more about where the sharp edges are.

Some of it was about things being visible to the wrong people. Hidden shot times, for example, could leak through related shots and pagination links. Then there was the account side: resetting your password now also revokes OAuth access, passkeys, and push subscriptions, so nobody who got in earlier keeps a back door. Passkey sign-in now asks for your PIN or biometric and is rate limited. OAuth secrets are properly hashed, Airtable tokens are encrypted, and Airtable sync and custom fields now check for Premium on the server, not just in the UI.

The scariest category was keeping the site up. A single crafted upload could have kept workers busy long enough to take the whole site down. Parsing is bounded now, files over 10 MB are rejected, and web push endpoints and webhook events are validated too.

Many other small things were fixed along the way, and you can find them all in the full changelog.

What might break for you

Tightening things up means a few API behaviors changed, so if you build on it, please check these:

  1. Access tokens in query or body params no longer work. Send them in the Authorization: Bearer header.
  2. Tokens without the read scope see public data only. Shot reads treat them as anonymous, with no error.
  3. Unconfirmed and disabled accounts get a 403 from the API.
  4. Premium and authorization failures are always a 403 JSON error.
  5. After a password reset, integrations must be authorized again.
  6. Old OAuth app secrets can't be recovered. They're only stored hashed now, so if you never saved one, you'll need a new app.

Sign-ups

I noticed a lot of bots and fake accounts when I sent the newsletter and got a bunch of bounced emails and spam reports. Not a great look. 😅

So new accounts now need to confirm their email, sign-ups from domains without MX records are rejected, and sign-up attempts are rate limited. All existing users were marked as confirmed. Accounts left unconfirmed for 7 days are deleted, or disabled if they have shots.

Uploads, API, and behind the scenes

Invalid files now get a clear message (a 422 in the API) instead of a 500, and dropping lots of files uploads them in batches. Smart Espresso Profiler CSVs parse faster, and Decent .tcl files about 9x faster with a new hand-written parser. I also fixed a .tcl export bug that produced files the Decent app couldn't load.

On the API side, archiving coffee bags was requested, so integrations can now do exactly that. Roaster and coffee bag images can be uploaded too, and the documented upload scope, which was actually being refused as invalid, works now. The OpenAPI spec got a full rewrite while I was at it.

Images are now decoded and resized in a locked-down sidecar container called Hot Cell, introduced by 37signals, instead of inside the Rails app, so a malicious image file stays contained there.

Grind suggestions

Last time I said I was experimenting with Jev for grind suggestions. I removed it, since it was only enabled for me and wasn't as useful as I hoped it would be. I even tried replacing it with Cloudflare's Clef, but it was even worse. 🤦‍♂️

If something looks off after all these changes, especially in the API, please let me know through GitHub issues or email.

Thanks for reading, and enjoy your weekend! ☕


Bending Tables

It’s been two months since the last update, and this update is one of the biggest in a really long time. I could probably call it Visualizer v5 - it’s been almost three years since v4, after all. 🤔

The biggest addition is Journal, and it all started when Bending Spoons acquired Airtable. Their usual playbook is to buy a service, let go of everyone deemed non-essential, hike up the prices, and squeeze as much out of it as possible. And for years, Airtable had been my answer to bulk editing.

Way back in 2022, someone asked for a way to select the 15 shots they made from one bag and set the coffee details once, instead of going through “15 time consuming steps.” In 2023, I closed that request by explaining that I had tried several approaches before deciding to use a tool that already did it, and did it “better than I ever could.” That became the Airtable integration.

I’m a backend developer, so JavaScript-heavy interfaces have never been my comfort zone. But coding agents have gotten good enough that I finally felt confident tackling this properly. So I did. 😅

Your shots, at a glance

Journal puts your shots in an editable table, with coffee, profile, dose, grind setting, yield, time, and enjoyment alongside each other. The Columns button next to search lets you choose which columns to show, hide the ones you don’t use, and drag them into whatever order makes sense for you. Tags, private notes, custom fields, and tasting assessments can be added as columns too.

Simple values can be edited directly in the table. Coffee, tags, and rich text notes open an editor without taking you away from your history. You can also select up to 100 shots and apply the same values to all of them in one go. Select two shots and you can jump straight into a comparison, too.

For those direct table edits, Enter saves your change and moves down one row, while Shift+Enter saves and moves up. Both keep you in the same column, so you can work through grind settings or enjoyment scores without reaching for the mouse.

This table view works especially well with Coffee Management. Select the shots you brewed from a bag, assign the coffee once, and then look through your grind settings, recipes, and tasting notes together. Much less clicking around when you’re trying to understand what worked during a dial-in session. And those “15 time consuming steps” from 2022? Now it’s one. Only took me four years. 🙈

If you’ve been following Visualizer for a while, you know I keep coming back to the same problem: what did I do last time with this coffee? I first took a stab at it with Recent Grinder Settings, because I kept forgetting the grind setting for my decaf. Then came Instant Filters, and last year the Coffee Bags page, where you can tap a bag and see all of its shots. With Journal, those shots now show up in a table, with dose, grind, yield, time, and enjoyment side by side. Enjoyment is color-coded too, so the good ones jump right out. 🤩

Outside of Visualizer, I’ve been writing a journal every day for years now, and what makes the biggest difference for me is reading it back. That’s where the magic happens. Coffee is no different. Your machine records how the shot ran, but the coffee, the grind setting, and how it tasted are up to you. Until now, adding those meant opening every shot, editing it, saving, and going back. No wonder it was so easy to skip. 😬

With Journal, it’s a couple of keystrokes, even on your phone while you’re still sipping. And once your shots have those details, patterns that used to be buried across dozens of shot pages show up on a single screen. Your shot history finally becomes something you actually use, not just something you save and forget. Game changer.

I think Journal should cover everything I used Airtable for, but more integrated and faster. You’re working directly with your shots on Visualizer, without jumping between two tools and waiting for them to sync.

The Airtable integration is not going away, unless they kill the API. If you like using it, keep using it. I wanted a good option inside Visualizer that doesn’t depend on what happens to another company’s product. And I’ve also spent so much time fixing the integration with their shitty API that I don’t want to throw all that work away. 😂

Journal is available to Premium users and is off by default. On desktop, just flip the new toggle on your shot list, and you can switch back and forth between Journal and the familiar card view whenever you want. You can also enable it in your profile settings.

Quite a bit of work went into the phone layout as well, including touch-friendly column rearranging, a more compact interface, and pull to refresh. But please let me know if something doesn’t work quite right, as this is a massive code change.

Search for everyone

Instant Filters are now available to everyone, and are no longer a Premium-only feature. You can search as you type and filter by date or enjoyment in the regular shot list, including on free accounts.

Search also got simpler: one box searches across coffee, roasters, profiles, grinders, and notes. Premium search also includes tags and private notes. That now works in both Journal and the regular shot list, so you don’t have to remember which field you put something in.

The only difference that remains: free users can only search shots added in the last month, while Premium includes your complete history.

No history file? No problem

You can now create shots manually, without uploading a file.

Use Create on your shot list, enter your brew details and notes, and the shot joins your history alongside the uploaded ones. This makes Visualizer useful even if your machine doesn’t export shot data, or you simply forgot to record a brew.

Manual shot creation is also available to everyone, including free accounts. 🥳

A few other things

Following a request to update tags through the API, Premium users can now set or clear shot tags through their integrations. I also fixed unexpected 406 errors and several pagination and error-handling issues.

The coffee URL importer in Coffee Management also got reliability improvements and more careful instructions for extracting coffee details. There were older-browser fixes, performance improvements, security hardening, and the usual pile of dependency updates too.

I’m also experimenting with Jev for grind suggestions based on your own shot history: which way to adjust your next shot, and where to start when opening a new coffee bag. These are still in testing, so I’ll share more once I’m happy with how they behave.

All together, that’s 107 commits across 228 files, with 6,063 lines added and 1,375 removed since the last update. You can explore the full diff for all the details.

With that many changes, I wouldn’t be surprised if I’ve missed a workflow or two in testing. Please send any and all bug reports through GitHub issues or email.

Thanks for reading, and enjoy your weekend! ☕


A Richer Note Taking Experience

It has not been long since the last update, but I have already shipped another substantial change: notes in Visualizer now use a proper rich text editor.

Until now, bean notes, espresso notes, private notes, and coffee bag notes were plain text fields with GitHub Flavored Markdown support. Markdown is great if you already know it, but it is not exactly welcoming when you just want to make something bold, add a link, or create a list.

Those fields now use Lexxy, a modern rich text editor built on Rails Action Text. You can format text directly, add headings, links, tables, lists, quotes, and code blocks, and see the result while you write it.

Existing notes were migrated automatically, including their Markdown formatting, so nothing should be lost or suddenly appear as raw syntax. The same editor now powers these update posts too, which made this post a convenient real world test. 😅

One important change for Airtable users: notes are still sent from Visualizer to Airtable as plain text, but edits made to notes in Airtable no longer sync back. Airtable does not support the same rich text format, so syncing them in both directions would lose formatting and create incompatible results.

There were also several smaller fixes around dark mode, copied shot values, coffee bag forms, code highlighting, and displaying rich content consistently across the app, plus a bunch of other changes.

As always, bug reports and feature requests are very welcome on GitHub. This change touched quite a few places, so please let me know if any existing notes look strange.

Thanks for reading, and enjoy the rest of your weekend! ☕