Search showed the last 100 matches by created_at, so SQLite collected every
message containing the words and sorted them before keeping a page. A common
word in a large account meant sorting most of its history on every search.
Ordering by the full-text index's rowid, which is the message id, lets SQLite
walk the index from the newest match and stop once the page is full.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JVFo3Lt9T8M5NR7KxVvsZ2
install-edge.svg lives in app/assets/images/external/, alongside the
other install icons, but the Edge branch of pwa/_install_instructions
asked for it at the top level. Propshaft raises MissingAssetError, so a
browser the useragent gem reports as Edge got a 500 on the profile page
and anywhere else the partial renders.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The flash icons were followed by `</span>` with no opening tag.
Browsers discard them, so nothing renders differently, but HTML-aware
tools such as Herb report them as errors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`as: "json"` is a @rails/request.js option; plain fetch ignores it and
sends `Accept: */*`, so Autocompletable::UsersController answered with
its HTML format and response.json() threw. The new ping form never
showed any suggestions. Send an explicit Accept header instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
link_to passed `action: "soft-keyboard#open"` as a plain option, so it
rendered as an `action` attribute, which Stimulus never reads. The link
in messages/_actions.html.erb already passes it under `data:`; this one
now does too, so tapping "Add a boost" on a touch device opens the
keyboard for the boost form as the menu's link does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The composite index doesn't serve everything the room_id one did. In
SQLite that index is (room_id, rowid), so it also reads a room's messages
in id order, and it is the narrower one to count a room with. Keeping both,
as memberships already does, leaves those lookups as they were.
The migration was named after local time, two hours ahead of the UTC
timestamp Rails generates; it now carries a UTC one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NuTtwJKhb77Dv7EQqv2C3C
The search box keeps only word characters, but FTS5 still reads AND, OR,
NOT and NEAR in the query as operators, so searching for "AND" or for
"salt AND" raised "fts5: syntax error" and answered with a 500. Quoting
each word makes FTS5 look for it as text, which is how it already treats
the same words in lower case.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NuTtwJKhb77Dv7EQqv2C3C
The original room shows its welcome box until it holds more than a page of
messages, and paged? answered that with a COUNT(*) over every message in
the room on each visit. Asking whether there is a row past the first page
answers the same question while reading at most a page of index entries.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NuTtwJKhb77Dv7EQqv2C3C
Every page of a room's messages (opening the room, scrolling back, the
refresh after reconnecting, the bot API) selects WHERE room_id = ? ORDER BY
created_at LIMIT 40. With only the room_id index, SQLite reads every
message in the room and sorts them in a temporary B-tree to return 40, so
these requests grow with the room's history.
A composite index on (room_id, created_at) lets SQLite read the 40 rows in
order straight from the index. It covers every lookup the room_id index
served, so that one goes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NuTtwJKhb77Dv7EQqv2C3C
* Document other Campfire implementations and benchmarks
* Remeasure README benchmarks with the latest Ruby optimizations
* Shorten the README benchmark context
* Preload only uncached messages and reduce rendering overhead
* Keep benchmark summaries without raw JSON results
* Use Ruby benchmark drivers and keep generated results out of the repo
The composer editor carries the lexxy-content class, so the !important
list rules for message bodies hit the menu's ul and li too, leaving the
items cramped and pushed in from the left.
The editor keeps an attachment's content as it finds it, so a hand-written
embed with a url but no href reached the editor with its markup unvalidated,
and a mention saved under Trix's editor carried the generic octet-stream
content type the editor doesn't permit and was dropped on save. Rebuilding
each attachment from its attachable renders the hardened preview partial
and restores the mention content type.
Main strips a javascript: href from a preview node before it's parsed, so
tell legacy Trix attachments apart by their filename instead. escapeHTML
moved to the string helpers, and the unfurling system test now drives
the Lexxy editor.
The companion to #279: the same per-instance `extend` appeared in
test/lib/rails_ext/action_text_attachables_test.rb. `attachable_sgid` is
`to_sgid(expires_in: nil, for: ActionText::Attachable::LOCATOR_NAME).to_s`,
so mint that directly; the minted bytes and the assertion are unchanged.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Agent worktrees under .claude/worktrees are whole extra checkouts of
this repo, and the build context picks them up. .claude is local
session state and never belongs in an image.