Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
## What is Cherry?

Cherry (cherrypush.com) is a technical debt tracking tool. Users configure codebase patterns to track, and Cherry reports stats on every commit to a dashboard. The app is a Rails 7 monolith with a React SPA for authenticated pages.

## Commands

### Development

```sh
bin/dev # Start dev server (Rails + Tailwind + Vite) on port 3001
docker compose up -d # Start PostgreSQL (required)
rails db:setup # Create and seed database
```

### Testing

```sh
HEADLESS=1 bin/rails test:all # Run all tests (unit + system, headless)
bin/rails test # Unit/integration tests only
HEADLESS=1 bin/rails test:system # System tests only (headless Chrome)
bin/rails test test/models/metric_test.rb # Single test file
bin/rails test test/models/metric_test.rb:42 # Single test by line number
bin/rails test:system # System tests with visible Chrome (no HEADLESS)
```

When making changes, always run the related tests before considering the work done.

### Linting

```sh
bundle exec rubocop # Ruby linter
npx eslint app/javascript/ # JS/TS linter
npx prettier --check . # Formatting check
```

## Architecture

### Hybrid Rails + React SPA

- **Static/public pages**: Server-rendered ERB templates (`app/views/pages/`), styled with Tailwind CSS
- **Authenticated app (`/user/*`)**: Single-page React app. Rails serves a shell via `User::ApplicationController#spa`, then React Router handles client-side routing. All `/user/*` JSON endpoints serve data to the React SPA via axios.
- **Frontend bundling**: Vite (via `vite_rails` gem and `vite-plugin-ruby`). React components live in `app/javascript/components/`, hooks in `app/javascript/hooks/`, helpers in `app/javascript/helpers/`.

### API layers

- **CLI API (`/api/*`)**: Used by the `cherrypush` npm CLI to push metrics. Authenticates via `api_key` param. Controllers in `app/controllers/api/`.
- **SPA API (`/user/*` JSON)**: Internal API for the React frontend. Authenticates via session cookie (Google OAuth via OmniAuth). Controllers in `app/controllers/user/`.

### Key models

The core domain: `Project` has many `Metric`s, each `Metric` has many `Occurrence`s (individual code locations) and `Contribution`s (who changed what). `Organization` groups projects; `User` belongs to organizations via `Membership`. `Dashboard` has many `Chart`s (via `ChartMetric` join).

### Testing

- Minitest with `minitest-spec-rails` (allows `describe`/`it` blocks)
- `FactoryBot` for test data (factories in `test/factories/`)
- System tests use Capybara + Selenium with Chrome. `HEADLESS` env var controls headless mode.
- Tests run in parallel by default (`parallelize(workers: :number_of_processors)`)

### Key conventions

- Ruby style: `rubocop` with compact class/module style (`class Foo::Bar` not nested)
- Memoized instance variables require leading underscore (`_` prefix) per rubocop config
- Authorization via Pundit policies
- Background jobs via DelayedJob
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ group :development do
end

group :test do
gem 'mini_magick'
gem 'minitest-spec-rails' # allows rspec-like syntax
gem 'minitest-stub-const' # provides stub_const helper for tests

Expand Down
3 changes: 3 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ GEM
marcel (1.0.4)
matrix (0.4.2)
method_source (1.1.0)
mini_magick (5.3.1)
logger
mini_mime (1.1.5)
minitest (5.25.1)
minitest-spec-rails (7.3.0)
Expand Down Expand Up @@ -398,6 +400,7 @@ DEPENDENCIES
heroicon
hiredis
kaminari
mini_magick
minitest-spec-rails
minitest-stub-const
omniauth
Expand Down
2 changes: 1 addition & 1 deletion app/views/pages/home.html.erb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<% title 'Home' %>

<div class="mr-auto place-self-center lg:col-span-7 text-center mt-24 mb-12">
<div class="mx-auto place-self-center lg:col-span-7 text-center mt-24 mb-12">
<h1 class="mb-6 max-w-3xl text-4xl font-extrabold tracking-tight md:text-5xl xl:text-6xl dark:text-white mx-auto">
Create beautiful <%= render 'components/highlight', text: 'dashboards' %> to track your
<span class="underline underline-offset-3 decoration-8 decoration-blue-500 dark:decoration-blue-600"
Expand Down
10 changes: 9 additions & 1 deletion bin/dev
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
#!/usr/bin/env sh

export PORT="${PORT:-3001}"

if ! gem list foreman -i --silent; then
echo "Installing foreman..."
gem install foreman
fi

exec foreman start -f Procfile.dev "$@" -p 3001
# Kill any process listening on the Rails or Vite ports
kill -9 $(lsof -ti :$PORT) 2>/dev/null || true
kill -9 $(lsof -ti :3036) 2>/dev/null || true

mkdir -p tmp/pids

foreman start -f Procfile.dev "$@"
2 changes: 2 additions & 0 deletions test/application_system_test_case.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
# frozen_string_literal: true

require 'test_helper'
require 'support/screenshot_helpers'

class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
include SignInHelper
include NavigationHelper
include ScreenshotHelpers

Capybara.server = :puma, { Silent: true } # removes noisy logs when launching tests

Expand Down
Binary file added test/screenshots/home.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
44 changes: 44 additions & 0 deletions test/support/screenshot_helpers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# frozen_string_literal: true

require "mini_magick"
require "open3"

module ScreenshotHelpers
SCREENSHOTS_DIR = Rails.root.join("test/screenshots")
SCREENSHOT_DIFF_THRESHOLD = 0.01 # 1% difference

def capture_screenshot(name)
return if ENV["CI"]

hide_toasts

target_path = SCREENSHOTS_DIR.join("#{name}.png")
FileUtils.mkdir_p(target_path.dirname)

Tempfile.create(["screenshot", ".png"]) do |temp_file|
page.save_screenshot(temp_file.path) # rubocop:disable Lint/Debugger

should_save = !File.exist?(target_path) || images_differ?(temp_file.path, target_path)
FileUtils.cp(temp_file.path, target_path) if should_save
end
end

private

def hide_toasts
page.execute_script("document.querySelector('[data-rht-toaster]')?.remove()")
rescue StandardError # ignore if JS execution fails
nil
end

def images_differ?(new_image_path, existing_image_path)
new_image = MiniMagick::Image.open(new_image_path)
existing_image = MiniMagick::Image.open(existing_image_path)
return true if new_image.dimensions != existing_image.dimensions

_, stderr, = Open3.capture3("compare", "-metric", "AE", existing_image_path.to_s, new_image_path.to_s, "null:")
different_pixels = stderr.to_i
total_pixels = existing_image.width * existing_image.height
(different_pixels.to_f / total_pixels) > SCREENSHOT_DIFF_THRESHOLD
end
end
1 change: 1 addition & 0 deletions test/system/static_pages_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class StaticPagesTest < ApplicationSystemTestCase
sign_in create(:user)
visit root_url
assert_text 'START NOW'
capture_screenshot('home')
click_on 'Terms'
assert_text 'Terms of Service'
click_on 'Privacy'
Expand Down
Loading