Dandelion is a fair source project. View the code on Github · See the design guide

Commits on Nov 05, 2026
c8d404d

Hide the star placeholder in your upcoming events as the star it stands in for is hidden, so it doesn't wrap under a long title and leave a blank line until it loads

Commits on Nov 04, 2026
77f9dd3

Always show the ticket count above the places filled bar

Events with a capacity showed only places filled in the bar on your upcoming events, which reads as tickets sold but counts each ticket's places, so 2 tickets of a type taking 2 places showed as 4.

a494baa

Say why a resold ticket's refund counts it in its order's total, and test the amounts refunded

A ticket on resale is removed when a sale claims it, before it's refunded, so its order's tickets in the database leave it out. Its refund is still capped right because loading the ticket's order puts the ticket in that order's tickets (Mongoid's belongs_to binding). A comment says so, and tests check the amounts refunded: a resold ticket in an order that keeps a cheaper one, two tickets resold from one order, and a removed ticket in an order with credit. They fail if the order is read afresh.

42e3110

Fit the tickets sold bar to the value column of the phone stats sheet, not the whole sheet

379b154

Show the tickets sold bar full width in the phone stats sheet, with places filled inside it where they fit

The sheet copied the bar into a right-aligned span only as wide as the count, so it shrank to a few pixels. It now goes on its own line below the label, and the bar's data-progress-label (e.g. 23/80) shows inside its filled part when it fits.

fbfd5fc

Share the Stripe connect token exchange between the organisation and personal routes

Both routes repeated the token exchange and its invalid_grant handling. Move it into stripe_connect_token_exchange, which returns nil for a spent or expired code and raises on anything else, so each route only keeps its own flash and redirect.

64e5acc

Tell members a reused personal Stripe connect link has expired, as organisations already are

The personal route /o/:slug/stripe_connect had the same single-use code problem: a reload or back button replays a spent code, Stripe answers invalid_grant, and it was raised as an error. Apply the same handling as the organisation route.

3691832

Tell organisers a reused Stripe connect link has expired, instead of reporting it as an error

Stripe's OAuth codes are single-use, so a reload or back button on the callback replays a spent code and Stripe answers invalid_grant with a 400. That was raised as an error and reached Sentry. Show a plain message for invalid_grant, and keep reporting other failures with Stripe's error code in the message.

Fixes DANDELION-4H

a9a0214

Pin minimatch to 11.0.1 to fix Dependabot alert #347

minimatch below 11.0.1 has a potential denial of service via globparts amplification. It came in through ESLint's dependency tree, which depends on minimatch ^10, so an override pins it instead of a plain bump. 11.0.1 is the first release past the cooldown that has the fix.

Commits on Nov 03, 2026
4782394

Check what the docs say with tests: each rule the docs state is a claim, marked <!-- claim: id --> after it, and a test that checks one starts with covers 'page/id@hash', the hash of the claim's text. test/lint_test.rb fails when a claim has no test, a test covers a claim that's gone, or a claim's text has changed since its tests were checked against it. lib/doc_claims.rb finds the claims and strips the markers before the docs render, as Redcarpet escapes an HTML comment inside a paragraph. Every page is marked (264 claims), with tests written for the rules that had none, including test/zapier_test.rb for the Zapier endpoints. Where the docs and the app disagreed, the docs now say what the app does: which checkouts have a donation box and who gets it, where evergreen events show first, that a reply shows only under feedback with a public extract, who can unlock a locked event, a magic number of 0, and that the free email gift counts all followers.

3d107cd

Don't ask organisers to refund a resold ticket again when its refund email fails, and tell them what its order paid

Refundable#refund_payment emails the refund after the provider has made it, so a failing email made the resale think the refund had failed and tell the organisers to refund the holder themselves. That email is now reported rather than raised, which also stops order refunds and an organiser's refund and remove raising after the money has gone back.

The organiser's email for a ticket from an order with credit or a fixed discount said what the order paid from its total, which leaves out its tickets already resold: a sale reselling both tickets of a £20 order reported £5 for each. It now uses the order's value, set when it was paid for.

a81504f

Have the user query agent answer only mail for Dandelion: threads with a message to contact@dandelion.events or one of the Ask a question form's [Question] notifications, including users' later replies in them. Other mail, even from Dandelion users, is Stephen's to answer. The Apps Script that starts the workflow applies the same rule, but a run sees all the mail since the last one.

6e4c6ff

Resell each ticket on resale once, as many as a sale needs, without stopping the sale

Payments completing at once could both find the same oldest ticket on resale, refund it twice and leave the next one unsold with its place taken. Each ticket on resale is now claimed with one update that only matches while it's still on resale and not removed, so they resell different tickets.

A sale resold one ticket on resale whatever places it took, so a ticket taking two places at a full event resold one of two that take one and overbooked the event. A sale now resells tickets until it has freed as many places as it takes.

The resale ran inside the buyer's order completion, so a refund or email that raised stopped the order completing after it was marked paid, and its tickets were never sent. Errors are now reported instead, and a refund that raises leaves the ticket for the organisers to refund.

f9f2339

Let the user query agent run git log, show and blame, so it can tell when something that used to work for a user changed. Its settings allow those three commands and no other Bash (headless, anything not allowed is refused), and its workflow fetches dandelion's full history; the prompt says when to use them.

a32b640

Add an agent that drafts replies to users' emails, moving the Cursor automation into the repo to run on a Claude subscription. Its prompt is .github/prompts/user-query-replies.md, with its tool permissions beside it: only tools that read (Mongo, Stripe, Mailgun, the repo's files) and create_draft, with no Bash, edits or web, as the emails are untrusted. script/gmail_mcp.rb is its Gmail MCP server, on the Gmail API with only Ruby's standard library: it lists and reads threads and drafts replies, but can't send, and builds each draft's MIME itself (HTML only, quoted-printable, CRLF, In-Reply-To unfolded, References copied, a Gmail quote without images), deleting a draft Gmail doesn't keep in the thread. Its workflow runs from a private repo, as its logs hold emails and this repo's are public, which AGENTS.md now says.

6efc8d8

Leave a resold ticket from an order with credit or a fixed discount for the organiser to refund

Credit and fixed discounts come off an order as a whole, not off its tickets, so a resold ticket's own price could be more than was paid for it: two £15 tickets bought with £10 credit refunded £15 when one was resold, of £20 paid. Such tickets now need a manual refund, like GoCardless instalments: the resale skips the automatic refund, and the organiser's email says what the order paid in all, from before the ticket is removed.

3249f54

Test what the resale docs say that wasn't tested

Which ticket on resale goes first, that a sale leaves tickets on resale alone while places are left, that putting one on resale at a sold-out event emails the waitlist, that a resold ticket is refunded only its own price, who the organiser emails go to, and that turning resales off at a full event gives the tickets back to their holders.

2137999

Move the resale tests to their own file

7d96ba6

Don't let a ticket come off resale while someone is buying its place

A buyer's unpaid order holds the place a ticket on resale freed, and the ticket is replaced once it's paid for. The holder could still take the ticket off resale in the meantime, so the payment found nothing to replace and the event was overbooked. Taking a ticket off resale now needs its place to be free (Ticket#fits_back_in_counts?, which turning resales off also uses). Tests cover this, a sale of another ticket type past the event's capacity and a sale past a ticket group's capacity, and the unpaid holds in the resale tests are now unpaid: free tickets are saved as paid.

The docs on resales said a ticket is only resold when its ticket type sells out, and left out its group and the event, which ticket goes first, what's refunded, taking a ticket off resale and turning resales off. They now cover them, and the setting's label is no longer marked experimental.

3668bbc

Sign in to the Sentry and Cloudflare MCP servers with tokens rather than OAuth

Sentry takes its token as Sentry-Bearer, which Codex can't send, so Codex's copy leaves that header out and Codex signs in to Sentry with OAuth.