CLAUDE.md
CLAUDE.mdCLAUDE.mdroot
Quality
84/100
Scores the file, not the repository.Length
3,128 words
65 headings · 9 code blocksRepository
10.0k
— · pushed 0 days agoLast changed
2 days ago
First indexed 2 days ago.1# CLAUDE.md - Dawarich Development Guide23This file contains essential information for Claude to work effectively with the Dawarich codebase.45## Project Overview67**Dawarich** is a self-hostable web application built with Ruby on Rails 8.0 that serves as a replacement for Google Timeline (Google Location History). It allows users to track, visualize, and analyze their location data through an interactive web interface.89### Key Features10- Location history tracking and visualization11- Interactive maps with multiple layers (heatmap, points, lines, fog of war)12- Import from various sources (Google Maps Timeline, OwnTracks, Strava, GPX, GeoJSON, photos)13- Export to GeoJSON and GPX formats14- Statistics and analytics (countries visited, distance traveled, etc.)15- Public sharing of monthly statistics with time-based expiration16- Trips management with photo integration17- Areas and visits tracking18- Integration with photo management systems (Immich, Photoprism)1920## Technology Stack2122### Backend23- **Framework**: Ruby on Rails 8.024- **Database**: PostgreSQL with PostGIS extension25- **Background Jobs**: Sidekiq with Redis26- **Authentication**: Devise27- **Authorization**: Pundit28- **API Documentation**: rSwag (Swagger)29- **Monitoring**: Prometheus, Sentry30- **File Processing**: AWS S3 integration3132### Frontend33- **CSS Framework**: Tailwind CSS with DaisyUI components34- **JavaScript**: Stimulus, Turbo Rails, Hotwired35- **Maps**: Leaflet.js36- **Charts**: Chartkick3738## Conventions39- **Enums over strings:** Prefer Rails enums (integer columns) over string columns for status/type fields. Use `enum :field_name, { ... }, prefix: :field_name` to get scoped predicate methods and avoid name collisions.40- **Turbo first:** Follow Rails 8 conventions — use Turbo Frames and Turbo Streams/broadcasts wherever appropriate to avoid full page reloads and provide smooth, in-place UI updates.41- **SVGs as files:** Never inline SVG markup in views. Instead, save SVGs to `app/assets/svg/icons` and use `inline_svg_tag "name.svg"` to render them. This keeps views clean and SVGs reusable. Use `rails_icons` to manage SVG assets and ensure consistent styling.4243## Code Style4445- Follow rubocop conventions (see `.rubocop.yml`)46- Rails defaults: convention over configuration47- Prefer Hotwire (Turbo Frames/Streams + Stimulus) over custom JS48- Use importmap for JS dependencies — no npm/yarn4950### Key Gems51- `activerecord-postgis-adapter` - PostgreSQL PostGIS support52- `geocoder` - Geocoding services53- `rgeo` - Ruby Geometric Library54- `gpx` - GPX file processing55- `parallel` - Parallel processing56- `sidekiq` - Background job processing57- `chartkick` - Chart generation5859## Project Structure6061```62├── app/63│ ├── controllers/ # Rails controllers64│ ├── models/ # ActiveRecord models with PostGIS support65│ ├── views/ # ERB templates66│ ├── services/ # Business logic services67│ ├── jobs/ # Sidekiq background jobs68│ ├── queries/ # Database query objects69│ ├── policies/ # Pundit authorization policies70│ ├── serializers/ # API response serializers71│ ├── javascript/ # Stimulus controllers and JS72│ └── assets/ # CSS and static assets73├── config/ # Rails configuration74├── db/ # Database migrations and seeds75├── docker/ # Docker configuration76├── spec/ # RSpec test suite77└── swagger/ # API documentation78```7980## Core Models8182### Primary Models83- **User**: Authentication and user management84- **Point**: Individual location points with coordinates and timestamps85- **Track**: Collections of related points forming routes86- **Area**: Geographic areas drawn by users87- **Visit**: Detected visits to areas88- **Trip**: User-defined travel periods with analytics89- **Import**: Data import operations90- **Export**: Data export operations91- **Stat**: Calculated statistics and metrics with public sharing capabilities9293### Geographic Features94- Uses PostGIS for advanced geographic queries95- Implements distance calculations and spatial relationships96- Supports various coordinate systems and projections9798## Development Environment99100### Setup1011. **Docker Development**: Use `docker-compose -f docker/docker-compose.yml up`1022. **DevContainer**: VS Code devcontainer support available1033. **Local Development**:104 - `bundle exec rails db:prepare`105 - `bundle exec sidekiq` (background jobs)106 - `bundle exec bin/dev` (main application)107108### Default Credentials109- Username: `demo@dawarich.app`110- Password: `safepassword`111112## Testing113114### Test Suite115- **Framework**: RSpec116- **System Tests**: Capybara + Selenium WebDriver117- **E2E Tests**: Playwright118- **Coverage**: SimpleCov119- **Factories**: FactoryBot120- **Mocking**: WebMock121122### Test Commands123```bash124bundle exec rspec # Run all specs125bundle exec rspec spec/models/ # Model specs only126npx playwright test # E2E tests127```128129### Testing Best Practices — Test Behavior, Not Implementation130131When writing or modifying tests, always test **observable behavior** (return values, state changes, side effects) rather than **implementation details** (which internal methods are called, in what order, with what exact arguments).132133**Anti-patterns to AVOID:**1341351. **Never mock the object under test** — `allow(subject).to receive(:internal_method)` makes the test a tautology1362. **Never test private methods via `send()`** — test through the public interface instead; if creating a user triggers a trial, test by creating the user and checking `user.trial?`, not by calling `user.send(:start_trial)`1373. **Never use `receive_message_chain`** — `allow(x).to receive_message_chain(:a, :b, :c)` breaks on any scope reorder; use real data instead1384. **Avoid over-stubbing** — if every collaborator is mocked, the test proves nothing; mock only at external boundaries (HTTP, geocoder, external APIs)1395. **Don't test wiring without outcomes** — `expect(Service).to receive(:new).with(args)` only proves a method was called, not that it works; verify the returned data or state change instead1406. **Prefer `have_enqueued_job` over `expect(Job).to receive(:perform_later)`** — the former tests real ActiveJob integration; the latter just tests a mock1417. **Don't assert on cache key formats or internal metric JSON shapes** — test that caching works (2nd call doesn't requery) or that metrics fire, not exact internal formats1428. **Use real factory data over `allow(user).to receive(:active?).and_return(true)`** — set the actual user state: `create(:user, status: :active)`143144**Good test pattern:**145```ruby146# Test behavior: creating an export enqueues processing147it 'enqueues processing job' do148 expect { create(:export, file_type: :points) }.to have_enqueued_job(ExportJob)149end150```151152**Bad test pattern:**153```ruby154# Tests implementation: mocks the callback interaction155it 'enqueues processing job' do156 expect(ExportJob).to receive(:perform_later) # mock, not real157 build(:export).save!158end159```160161## Background Jobs162163### Sidekiq Jobs164- **Import Jobs**: Process uploaded location data files165- **Calculation Jobs**: Generate statistics and analytics166- **Notification Jobs**: Send user notifications167- **Photo Processing**: Extract EXIF data from photos168169### Key Job Classes170- `Tracks::ParallelGeneratorJob` - Generate track data in parallel171- Various import jobs for different data sources172- Statistical calculation jobs173174## Public Sharing System175176### Overview177Dawarich includes a comprehensive public sharing system that allows users to share their monthly statistics with others without requiring authentication. This feature enables users to showcase their location data while maintaining privacy control through configurable expiration settings.178179### Key Features180- **Time-based expiration**: Share links can expire after 1 hour, 12 hours, 24 hours, or be permanent181- **UUID-based access**: Each shared stat has a unique, unguessable UUID for security182- **Public API endpoints**: Hexagon map data can be accessed via API without authentication when sharing is enabled183- **Automatic cleanup**: Expired shares are automatically inaccessible184- **Privacy controls**: Users can enable/disable sharing and regenerate sharing URLs at any time185186### Technical Implementation187- **Database**: `sharing_settings` (JSONB) and `sharing_uuid` (UUID) columns on `stats` table188- **Routes**: `/shared/month/:uuid` for public viewing, `/stats/:year/:month/sharing` for management189- **API**: `/api/v1/maps/hexagons` supports public access via `uuid` parameter190- **Controllers**: `Shared::StatsController` handles public views, sharing management integrated into existing stats flow191192### Security Features193- **No authentication bypass**: Public sharing only exposes specifically designed endpoints194- **UUID-based access**: Sharing URLs use unguessable UUIDs rather than sequential IDs195- **Expiration enforcement**: Automatic expiration checking prevents access to expired shares196- **Limited data exposure**: Only monthly statistics and hexagon data are publicly accessible197198### Usage Patterns199- **Social sharing**: Users can share interesting travel months with friends and family200- **Portfolio/showcase**: Travel bloggers and photographers can showcase location statistics201- **Data collaboration**: Researchers can share aggregated location data for analysis202- **Public demonstrations**: Demo instances can provide public examples without compromising user data203204## API Documentation205206- **Framework**: rSwag (Swagger/OpenAPI)207- **Location**: `/api-docs` endpoint208- **Authentication**: API key (Bearer) for API access, UUID-based access for public shares209210## Database Schema211212### Key Tables213- `users` - User accounts and settings214- `points` - Location points with PostGIS geometry215- `tracks` - Route collections216- `areas` - User-defined geographic areas217- `visits` - Detected area visits218- `trips` - Travel periods219- `imports`/`exports` - Data transfer operations220- `stats` - Calculated metrics with sharing capabilities (`sharing_settings`, `sharing_uuid`)221222### PostGIS Integration223- Extensive use of PostGIS geometry types224- Spatial indexes for performance225- Geographic calculations and queries226227## Configuration228229### Environment Variables230See `.env.template` for available configuration options including:231- Database configuration232- Redis settings233- AWS S3 credentials234- External service integrations235- Feature flags236237### Key Config Files238- `config/database.yml` - Database configuration239- `config/sidekiq.yml` - Background job settings240- `config/schedule.yml` - Cron job schedules241- `docker/docker-compose.yml` - Development environment242243## Deployment244245### Docker246- Production: `docker/docker-compose.production.yml`247- Development: `docker/docker-compose.yml`248- Multi-stage Docker builds supported249250### Procfiles251- `Procfile` - Production Heroku deployment252- `Procfile.dev` - Development with Foreman253- `Procfile.production` - Production processes254255## Code Quality256257### Tools258- **Ruby Linting**: RuboCop with Rails extensions259- **JS/CSS Linting**: Biome (formatting, lint, import sorting)260- **Security**: Brakeman, bundler-audit261- **Dependencies**: Strong Migrations for safe database changes262- **Performance**: Stackprof for profiling263264### Commands265```bash266bundle exec rubocop # Ruby linting267npx @biomejs/biome check --write . # JS/CSS auto-fix (safe fixes)268npx @biomejs/biome check --write --unsafe . # JS/CSS auto-fix (all fixes)269npx @biomejs/biome ci . # JS/CSS CI check (read-only)270bundle exec brakeman # Security scan271bundle exec bundle-audit # Dependency security272```273274### Lint Rules275- **Always run RuboCop** on modified Ruby files before committing: `bundle exec rubocop <files>`276- **Always run Biome** on modified JS/CSS files before committing: `npx @biomejs/biome check --write <files>`277- If Biome `--write` leaves remaining errors, use `--write --unsafe` to apply fixes like `parseInt` radix and `Number.isNaN`278- CI runs `biome ci --changed --since=dev` — it compares against the `dev` branch, not `master`279- The `noStaticOnlyClass` warning is acceptable and does not fail CI280- Tailwind CSS files (`*.tailwind.css`) have `@import` position rules disabled in `biome.json` because `@tailwind` directives must come first281282## Frontend: Hotwire-First Approach283284**Always prefer Turbo + Stimulus over custom JavaScript.** This project uses the Hotwire stack (Turbo Drive, Turbo Frames, Turbo Streams, Stimulus) as its primary frontend architecture. Direct `fetch()` calls, manual DOM manipulation, and standalone JS modules should only be used when Hotwire cannot handle the use case (e.g., map rendering with Leaflet/MapLibre).285286### Decision Hierarchy287288When adding frontend behavior, follow this order of preference:2892901. **Turbo Drive** — Default. Links and forms work as SPAs with zero JS.2912. **Turbo Frames** — Partial page updates. Wrap a section in `<turbo-frame>` and target it from links/forms.2923. **Turbo Streams** — Server-pushed DOM updates. Use for CRUD operations that need to update multiple page sections. Respond with `turbo_stream` format from controllers.2934. **Stimulus controller** — Client-side behavior that Turbo can't handle (toggles, form validation, UI interactions). Keep controllers thin.2945. **Direct JS** — Last resort. Only for complex map interactions, canvas rendering, or third-party library integration (Leaflet, MapLibre, Chartkick).295296### Turbo Stream Responses297298For CRUD actions (create, update, destroy), respond with Turbo Streams instead of redirects or JSON:299300```ruby301# Controller302def create303 @area = current_user.areas.new(area_params)304 if @area.save305 respond_to do |format|306 format.turbo_stream307 format.html { redirect_to areas_path }308 end309 end310end311312# app/views/areas/create.turbo_stream.erb313<%= turbo_stream.prepend "areas-list", partial: "areas/area", locals: { area: @area } %>314<%= stream_flash(:notice, "Area created successfully") %>315```316317Use the `FlashStreamable` concern (included in controllers) to send flash messages via Turbo Streams:318319```ruby320include FlashStreamable321322# In turbo_stream responses:323stream_flash(:notice, "Success message")324stream_flash(:error, "Error message")325```326327### Flash Messages328329- **Server-side (Turbo Stream):** Use `stream_flash` from the `FlashStreamable` concern. This appends a flash partial to the `#flash-messages` container.330- **Client-side (Stimulus/JS):** Import `Flash` from `flash_controller.js` and call `Flash.show(type, message)`:331```javascript332 import Flash from "./flash_controller"333 Flash.show("notice", "Operation completed")334 Flash.show("error", "Something went wrong")335```336- **Never** use raw `alert()`, `console.log` for user-facing messages, or create ad-hoc notification DOM elements.337338### Stimulus Controllers339340- Location: `app/javascript/controllers/`341- Naming: `<name>_controller.js` maps to `data-controller="<name>"` in HTML342- Use `static targets` for DOM references, `static values` for data from HTML attributes343- Always clean up in `disconnect()` (event listeners, timers, subscriptions)344- Prefer `data-action` attributes in HTML over `addEventListener` in JS345- For forms, prefer `this.formTarget.requestSubmit()` over manual `fetch()` calls — this preserves Turbo form handling, CSRF tokens, and Turbo Stream responses346347### File Uploads348349Use the unified `upload` controller (`upload_controller.js`) for all file upload forms. Configure via `data-upload-*-value` attributes:350351```erb352<%= form_with data: {353 controller: "upload",354 upload_url_value: rails_direct_uploads_url,355 upload_field_name_value: "import[files][]",356 upload_multiple_value: true,357 upload_target: "form"358} do |f| %>359```360361### What NOT to Do362363- **No `fetch()` for form submissions** — Use `form_with` with Turbo. If you need custom headers (API key), use Stimulus to submit the form via `requestSubmit()`.364- **No `document.getElementById()` for updates** — Use Turbo Frames/Streams to replace DOM sections server-side.365- **No `showFlashMessage()` or ad-hoc flash functions** — Use `Flash.show()` (client) or `stream_flash` (server).366- **No ActionCable subscriptions for CRUD updates** — Use Turbo Stream broadcasts from models/controllers instead.367- **No separate upload controllers per form** — Use the unified `upload` controller with value attributes for configuration.368369### When Direct JS Is Acceptable370371- **Map rendering**: Leaflet (Maps v1) and MapLibre GL JS (Maps v2) require imperative JS for layers, markers, and interactions.372- **Chart rendering**: Chartkick handles its own DOM.373- **Third-party integrations**: Libraries that don't have Hotwire adapters.374- **Complex client-side computation**: Haversine distance, coordinate transforms, etc.375376Even in these cases, wrap the integration in a Stimulus controller and connect it to the DOM via `data-controller`.377378## Important Notes for Development3793801. **Location Data**: Always handle location data with appropriate precision and privacy considerations3812. **PostGIS**: Leverage PostGIS features for geographic calculations rather than Ruby-based solutions3822.1 **Coordinates**: Use `lonlat` column in `points` table for geographic calculations3833. **Background Jobs**: Use Sidekiq for any potentially long-running operations3844. **Testing**: Include both unit and integration tests for location-based features3855. **Performance**: Consider database indexes for geographic queries3866. **Security**: Never log or expose user location data inappropriately3877. **Migrations**: Put all migrations (schema and data) in `db/migrate/`, not `db/data/`. Data manipulation migrations use the same `ActiveRecord::Migration` class and should run in the standard migration sequence.3888. **Public Sharing**: When implementing features that interact with stats, consider public sharing access patterns:389 - Use `public_accessible?` method to check if a stat can be publicly accessed390 - Support UUID-based access in API endpoints when appropriate391 - Respect expiration settings and disable sharing when expired392 - Only expose minimal necessary data in public sharing contexts393394### Route Drawing Implementation (Critical)395396⚠️ **IMPORTANT: Unit Mismatch in Route Splitting Logic**397398Both Map v1 (Leaflet) and Map v2 (MapLibre) contain an **intentional unit mismatch** in route drawing that must be preserved for consistency:399400**The Issue**:401- `haversineDistance()` function returns distance in **kilometers** (e.g., 0.5 km)402- Route splitting threshold is stored and compared as **meters** (e.g., 500)403- The code compares them directly: `0.5 > 500` = always **FALSE**404405**Result**:406- The distance threshold (`meters_between_routes` setting) is **effectively disabled**407- Routes only split on **time gaps** (default: 60 minutes between points)408- This creates longer, more continuous routes that users expect409410**Code Locations**:411- **Map v1**: `app/javascript/maps/polylines.js:390`412 - Uses `haversineDistance()` from `maps/helpers.js` (returns km)413 - Compares to `distanceThresholdMeters` variable (value in meters)414415- **Map v2**: `app/javascript/maps_maplibre/layers/routes_layer.js:82-104`416 - Has built-in `haversineDistance()` method (returns km)417 - Intentionally skips `/1000` conversion to replicate v1 behavior418 - Comment explains this is matching v1's unit mismatch419420**Critical Rules**:4211. ❌ **DO NOT "fix" the unit mismatch** - this would break user expectations4222. ✅ **Keep both versions synchronized** - they must behave identically4233. ✅ **Document any changes** - route drawing changes affect all users4244. ⚠️ If you ever fix this bug:425 - You MUST update both v1 and v2 simultaneously426 - You MUST migrate user settings (multiply existing values by 1000 or divide by 1000 depending on direction)427 - You MUST communicate the breaking change to users428429**Additional Route Drawing Details**:430- **Time threshold**: 60 minutes (default) - actually functional431- **Distance threshold**: 500 meters (default) - currently non-functional due to unit bug432- **Sorting**: Map v2 sorts points by timestamp client-side; v1 relies on backend ASC order433- **API ordering**: Map v2 must request `order: 'asc'` to match v1's chronological data flow434435## Plan System (Lite vs Pro)436437Dawarich Cloud has a two-tier plan system. Self-hosted instances bypass all plan restrictions (`DawarichSettings.self_hosted?` returns true, all users effectively have Pro).438439### Plans440441- **Pro** (`plan: :pro`, enum value `1`) — Full access to all features, no data window442- **Lite** (`plan: :lite`, enum value `0`) — Free tier with restricted feature set443444Plan is stored as an integer enum on the `users` table. New cloud users start on Lite via trial flow.445446### Lite Plan Restrictions447448**Data visibility window (12 months):**449- Lite users only see data from the last 12 months (`DawarichSettings::LITE_DATA_WINDOW`)450- Implemented as a query-time filter in `PlanScopable` concern (`app/models/concerns/plan_scopable.rb`)451- Scoped methods: `scoped_points`, `scoped_tracks`, `scoped_visits`, `scoped_stats`452- Data is **never deleted** — only filtered from UI and API reads. Export uses unscoped `user.points` etc.453- `plan_restricted?` returns `true` only when `!self_hosted? && lite?`454455**Disabled map layers (Pro-only):**456- Heatmap, Fog of War, Scratch Map, Globe View457- Lite users get a 20-second timed preview, then auto-hide with upgrade prompt458- Gating logic: `app/javascript/maps_maplibre/utils/layer_gate.js`459- UI components: `Toast` (countdown) and `UpgradeBanner` (post-preview CTA)460461**API restrictions:**462- Write API returns 403 (`require_write_api!` in `ApiController`)463- Read API scopes results to 12-month window (`apply_plan_scope` in `ApiController`)464- Rate limit: 200 req/hr (Lite) vs 1,000 req/hr (Pro) via `rack-attack` (`config/initializers/rack_attack.rb`)465466**Disabled features:**467- Integrations (Immich, Photoprism)468- Public sharing of stats469- Full digest view470471**Plan endpoint:** `GET /api/v1/plan` returns current plan and feature flags (`Api::V1::PlanController`)472473### Archival Warning System474475`Lite::ArchivalWarningJob` runs daily for Lite users and sends warnings at three thresholds:4761. **11 months** — In-app notification warning data will archive in 30 days4772. **11.5 months** — Email notification4783. **12 months** — In-app notification that data has been archived (hidden from view)479480Warnings are deduped via `settings['archival_warnings']` JSONB on the user record.481482### Development Guidelines for Plan Gating483484- Use `user.plan_restricted?` to check if restrictions apply (returns false for self-hosted)485- Use `user.scoped_*` methods instead of `user.points`/`user.tracks` etc. for plan-aware queries486- Use `require_pro_api!` or `require_write_api!` before_actions in API controllers487- Use `apply_plan_scope(relation)` when scoping points that don't start from `user.points`488- Frontend: use `isGatedPlan(userPlan)` and `gatedToggle()` from `layer_gate.js` for map layer toggling489- Export must always use unscoped relations — users can export all their data regardless of plan490491## Contributing492493- **Main Branch**: `master`494- **Development**: `dev` branch for pull requests495- **Issues**: GitHub Issues for bug reports496- **Discussions**: GitHub Discussions for feature requests497- **Community**: Discord server for questions498499## Resources500501- **Documentation**: https://dawarich.app/docs/502- **Repository**: https://github.com/Freika/dawarich503- **Discord**: https://discord.gg/pHsBjpt5J8504- **Changelog**: See CHANGELOG.md for version history505- **Development Setup**: See DEVELOPMENT.md506
Also in Freika/dawarich
Diff this repo’s formatsOne repository carrying more than one format is the comparison this product exists for: does anyone actually write different content in each file, or is one a copy of the other?
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| Freika/dawarichAGENTS.md · 10.0k | AGENTS.md | buildtestlint-formatstyle+4 | 94/100 | 2 days ago |
Similar configs
Same format, overlapping stack, ranked by quality.
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| microsoft/playwrightCLAUDE.md · 94k | CLAUDE.md | buildtestlint-formatstyle+7 | 100/100 | 3 days ago | |
| nimbalyst/nimbalystpackages/android/CLAUDE.md · 1.4k | CLAUDE.md | setupbuildstylearch+2 | 100/100 | 3 days ago | |
| Adit-Jain-srm/NightmareNetCLAUDE.md · 45 | CLAUDE.md | buildtestlint-formatstyle+6 | 100/100 | 3 days ago | |
| bagisto/bagistoCLAUDE.md · 28k | CLAUDE.md | setupbuildteststyle+5 | 100/100 | 3 days ago | |
| MetaMask/metamask-design-systemCLAUDE.md · 34 | CLAUDE.md | buildtestlint-formatstyle+5 | 97/100 | 3 days ago | |
| supabase/supabase.claude/CLAUDE.md · 108k | CLAUDE.md | testlint-formatstylearch+1 | 97/100 | 3 days ago | |
| wodsmith/thewodappCLAUDE.md · 2 | CLAUDE.md | buildtestlint-formatstyle+7 | 96/100 | 3 days ago | |
| summer-marie/conditions-translatorCLAUDE.md · 0 | CLAUDE.md | setupbuildtestlint-format+7 | 96/100 | 2 days ago |
