Commit Graph

78 Commits

Author SHA1 Message Date
Mike Dalessio 18b3319a94 Extract hex_to_base36 and base36_to_hex methods in Uuid type
replacing normalize_base36. This should make it easier to convert
UUIDs between hex and base36 formats, useful when debugging production
logs that contain hex UUIDs.

For example, if in a log you see:

    User Load (0.3ms) SELECT `users`.* FROM `users` WHERE
    `users`.`account_id` = x'019afab815897a4f920f3a24fab75400' AND
    `users`.`id` = x'019afab815a2790688c58c7f8326e700' LIMIT 1

and you want to find that account or that user in the console, you can
now do:

    id = ActiveRecord::Type::Uuid.hex_to_base36("019afab815897a4f920f3a24fab75400")
    => "03f6bilrvt3oghzhurll4pp8g"
2025-12-16 14:16:55 -05:00
Jeremy Daer 761d0b4a76 Speedy, auditable, deadlock-resistant storage tracking (#2026)
An append-only storage ledger replaces deadlock-prone synchronous
counter updates and drift-prone async updates.

All content storage (card images, card/comment/board description embeds)
is tracked. Account exports (which expire) and avatars are not tracked.

Storage is accounted for by Account and Board. Both consume the same
event stream independently: no bubble-up storage bumps triggering
deadlocks due to lock sequencing. Each calculates its own total from
the underlying ledger with independent cursors and materialization.

* Storage::Entry: Append-only ledger recording attach/detach/transfer
  events with delta bytes. Single event stream indexed for both
  Account and Board cursor queries.

* Storage::Total: Polymorphic snapshot cache with cursor (last_entry_id)
  tracking which entries have been materialized.

* Storage::Totaled concern: Provides bytes_used (fast snapshot) and
  bytes_used_exact (snapshot + pending) query modes, plus
  materialize_storage! to roll up pending entries.

* Storage::Tracked concern: For models owning attachments (Card,
  Comment, Board). Provides board_for_storage_tracking for models
  where board is determined differently (Board returns self).
  Handles board transfers by recording transfer_out/transfer_in entries.

* Storage::AttachmentTracking: Hooks ActiveStorage::Attachment lifecycle
  to record attach/detach entries. Handles ActionText::RichText embeds
  by traversing to the actual model. Snapshots context in before_destroy
  to handle cascading deletes where parent record may be gone by
  after_destroy_commit.

* MaterializeJob: Rolls up pending entries into snapshot. Concurrency
  limited per owner to prevent duplicate work.

* ReconcileJob: On-demand reconciliation against actual attachment
  storage for support/debugging. Compares ledger total to real bytes
  from card images, card embeds, comment embeds, and board embeds.

Usage:
* account.bytes_used / board.bytes_used: fast, slightly stale bytesize
* account.bytes_used_exact / board.bytes_used_exact: real-time bytesize
* Storage::Entry: audit trail for debugging and point-in-time queries
2025-12-10 16:04:30 -08:00
Jeremy Daer 496851b255 Security: Web Push SSRF and IP range bypass
Add SSRF protection for web push endpoints:
- Resolve endpoint IP once and pin it for connection
- Validate endpoints resolve to public IPs
- Whitelist permitted push service hosts

Add missing IP ranges to SsrfProtection:
- 100.64.0.0/10 (Carrier-grade NAT, RFC6598)
- 198.18.0.0/15 (Benchmark testing, RFC2544)

Note: link-local (169.254.0.0/16) is already covered by ip.link_local?
2025-12-04 21:35:55 -08:00
Mike Dalessio c4a8996562 Retry mailer jobs on common networking and SMTP errors
This concern is lifted nearly verbatim from Basecamp.

ref: https://app.fizzy.do/5986089/cards/3300
2025-12-04 10:26:24 -05:00
Jorge Manrubia 6ccf655597 Move yabeda/prometheus stuff to the gem 2025-11-28 15:53:58 +01:00
Jorge Manrubia f03fbba5ac Restore local CI, sensible setup for open source users 2025-11-28 15:53:58 +01:00
Jorge Manrubia 0328149ba2 Initial github action setup 2025-11-28 15:53:58 +01:00
Jorge Manrubia a5b02755b2 Add rake tasks for enabling/disabling saas mode 2025-11-28 15:53:58 +01:00
Jorge Manrubia 9132f6b835 Fix path to saas. 2025-11-28 15:53:58 +01:00
Jorge Manrubia ad6f3a9ff7 Instead of a dedicated group, use a completely separate Gemfile for the saas version
The group-based approach won't work if you don't have access to the gems!
2025-11-28 15:53:58 +01:00
Jorge Manrubia 03483a0526 Format 2025-11-28 15:53:58 +01:00
Jorge Manrubia fb9aa5dfac Extract common method to configure bundle 2025-11-28 15:53:48 +01:00
Jorge Manrubia bab914d4e0 Move db check to the Fizzy module 2025-11-28 15:53:48 +01:00
Jorge Manrubia 6134407f5e Make sass property depend on txt file or env var 2025-11-28 15:53:48 +01:00
Jorge Manrubia e1e9a01720 Make open source the default, and check for sass instead 2025-11-28 15:53:48 +01:00
Donal McBreen c4498212dc Merge branch 'main' into sqlite
* main: (116 commits)
  Ensure avatar thumbnails are square
  Update useragent to recognize twitterbot/facebot
  Add defensive styles for non-square avatar images
  Update test for copy changes
  Missed commit
  AI: standardize on https://agents.md
  Make it clear this is just notifications, not comprehensive activity
  AI: configure MCP servers for Chrome, Grafana, and Sentry (#1727)
  Allow requests from Google Image Proxy
  Update to basecamp's useragent fork
  Clean up a little bit the CSRF reporting code
  Claude: production observability guidance (#1725)
  Prevent autoscroll to the root columns container to prevent jump on page load
  Include full name string so you can type your name to filter
  Prioritize current user and assigned users in assignment dropdown
  Check and report on Sec-Fetch-Site header for forgery protection
  bundle update
  Bump bootsnap from 1.18.6 to 1.19.0
  Bump rails from `077c3ad` to `17f6e00`
  Fix cards getting stuck in edit mode
  ...
2025-11-26 10:07:41 +00:00
Donal McBreen b571303385 Don't use Current.account for search records
It doesn't actually work, and even if we could make it work reliably
we are better off if the records always know to go to the right shard.

It does make the interface a bit more complicated as we need to select
the right shard class with `for(account_id)`.
2025-11-25 11:34:41 +00:00
Mike Dalessio 9f117159a6 Restore structured logging of queenbee_id
and clean up some other small remaining "plan b" to-dos.
2025-11-24 15:50:45 -05:00
Mike Dalessio b2e3c7b1c7 Drop the "max previewable file size" limit
which was originally introduced to work around timing issues causing
us to try (and fail) to generate previews on sqlite read replicas.
2025-11-21 10:55:20 -05:00
Donal McBreen 01d16f96d4 Remove Search::Record.for_account
Instead we'll compute the table name dynamically based on
Current.account where needed. Also we'll prevent searchable records
from being saved if Current.account is not set, otherwise the after
commit callbacks will fail.
2025-11-21 12:09:21 +00:00
Donal McBreen 19b0e25eac Split sqlite/mysql record stuff 2025-11-21 09:15:19 +00:00
Donal McBreen a682a807ed Handle SQLite date subtraction 2025-11-21 09:15:19 +00:00
Donal McBreen cc7e091508 Disable multi-db for SQLite 2025-11-21 09:15:19 +00:00
Donal McBreen a2333d9a37 Add SQLite support
- UUID support
- Schema compatibility layer, ignore MySQL specific options
- Search not working yet
2025-11-21 09:15:19 +00:00
Mike Dalessio 2c2a1c4b4a Make sure jobs are enqueued only after all transactions are committed
This should fix the recurring ActiveStorage::FileNotFoundError errors
from ActiveStorage::AnalyzeJob.

The issue is that, when images are not direct uploaded (e.g. a card
background image or avatar image), the AnalyzeJob has been getting
enqueued before the file itself was uploaded to blob storage.

Setting ActiveJob::Base.enqueue_after_transaction_commit = true makes
sure that jobs are always enqueued only once *all* transactions have
been committed.

This will be the Rails default at some point, but for now we still
need to explicitly set it.

Big thanks to @jeremy for the assist. 👏
2025-11-20 18:47:42 -05:00
Stanko K.R. e8a7b19647 Retry file analisys on FileNotFoundError 2025-11-20 15:09:43 +01:00
Kevin McConnell 9b3b2f5d29 Prevent pinning writes in non-primary DCs 2025-11-18 11:03:58 +00:00
Mike Dalessio d8f935498a ActiveRecord::Type::Uuid#hex
to make it easier to convert when we have to look in the logs
2025-11-17 20:04:43 -05:00
Donal McBreen 28efe28f24 Replace Search::Index with Search::Records
Lean on ActiveRecord models for searching and strip out the raw SQL.
Replaces the search_index_* tables with sharded search_records_* tables
as that allows us to use a Search::Record model name.

A Class is dynamically created for each record table shard so that we
and we can access it via the Search::Record.for_account(account_id)
method.
2025-11-17 09:12:40 -05:00
Donal McBreen a8412449e3 Don't cast and extract normalization
We are not trying to handle user input so `cast` can be a no-op. Also
extract the base36 normalization logic, let the binary class handle the
serialize encoding and add some tests.
2025-11-17 09:12:36 -05:00
Donal McBreen 8ec5c1af5c Use Binary::Data for serialization
This handles quoting for us automatically
2025-11-17 09:12:36 -05:00
Donal McBreen f6ec9bbeea Use a custom class for Uuid values 2025-11-17 09:12:35 -05:00
Donal McBreen fc56ad9d7c Combine uuid-old and uuid branch changes
- Switch to binary 16 for UUID keys
- Remove AccountScopedRecord base class, all model use binary uuids now
- Fix the search sql to serialize uuids properly
- Patch the MySQL schema dumper to output binary lengths
2025-11-17 09:12:34 -05:00
Donal McBreen 3aa4a1a562 Shard the search index into 16 tables
Create search_index_0 to search_index_15 tables and shard each index by
account id. MySQL has no ability to pre-filter fulltext indexes by
another field so this is the best bet for improving performance.

Each fulltest index internally creates 11 sub tables (see
https://dev.mysql.com/doc/refman/8.4/en/innodb-fulltext-index.html) so
actually we have 192 tables in total here.

The search_index table name is generated dynamically based on the
account_id.
2025-11-17 09:12:17 -05:00
Donal McBreen e8abc66eba MySQL search
Add a single search_index table for full-text search of cards and
comments.

For the search there is a full-text index on the title and content
columns. The board_ids is also included in the table and accessible
board ids are pre-loaded and included in the search query. This allows
us to filter out inaccessible records before joining with other tables.

Right now the search is just using boolean search. This would give us
a bunch of syntax options
(see https://dev.mysql.com/doc/refman/8.4/en/fulltext-boolean.html)
except the search query filters those out.

I've removed the searchable_by method for now - everything is built
on the assumption that there's a single search index table and all data
must fit into it.

Queries are written in SQL, we don't have a SearchIndex ActiveRecord
model. That's because we'll likely want to shard the table and it will
be simpler to just keep with the raw SQL for that.

There's no stemming, highlighting or snippet extraction yet - we are
dumping the full description in the search results.

Data can be reindexed with the search:reindex rake task.
2025-11-17 09:12:17 -05:00
Mike Dalessio d41d50d52b Account.sole → Current.account
and some other de-tenant changes, including removing the controller
tenanting concerns
2025-11-17 09:11:40 -05:00
Jorge Manrubia e8920ceb0e Prevent page refreshes when posting comments/descriptions with attachments 2025-11-13 16:03:48 +01:00
Jason Zimdars 0f28ded99e Send email from support@fizzy.do 2025-11-11 10:47:30 -06:00
Fernando Álvarez 514c0f45a2 Migrate to fizzy.do 2025-11-04 13:17:10 +01:00
Jorge Manrubia 445ee48b0d Format 2025-11-04 05:07:26 +01:00
Jorge Manrubia 4e999eaf86 Use existing ARTENANT env var for selecting the tenant
Thanks Mike for the tip!
2025-11-03 15:04:06 +01:00
Jorge Manrubia ac507aa754 Add support for remote images in the seeding system 2025-11-02 09:24:38 +01:00
Jorge Manrubia e726335833 WIP for seeding accounts 2025-11-02 00:05:38 +01:00
Fernando Álvarez 7f78514a58 Move to app.box-car.com 2025-10-30 11:12:39 +01:00
Fernando Álvarez 3946733b65 Migrate to box-car.com 2025-10-28 18:44:25 +01:00
Mike Dalessio 320558919c Rename LOCAL_AUTHENTICATION to OSS_CONFIG 2025-10-02 16:44:24 -04:00
Mike Dalessio 4442964408 Introduce yabeda for Solid Queue metrics 2025-09-15 11:05:04 -04:00
Mike Dalessio 2630e4bd74 Extract 37id and QB controllers, models, and tests 2025-09-13 16:03:02 -04:00
Jorge Manrubia 218519d075 Configure letter opener 2025-08-26 10:21:13 +02:00
Mike Dalessio a711975904 Turn off attachment previews on large files
ref: https://fizzy.37signals.com/5986089/collections/7/cards/1302
2025-08-19 12:16:24 -04:00