1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-07-18 20:59:39 +02:00

Compare commits

...

383 commits
v0.4.1 ... main

Author SHA1 Message Date
Zach Gollwitzer
ab6fdbbb68
Component namespacing (#2463)
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
* [claudesquad] update from 'component-namespacing' on 18 Jul 25 07:23 EDT

* [claudesquad] update from 'component-namespacing' on 18 Jul 25 07:30 EDT

* Update stimulus controller references to use namespace

* Fix remaining tests
2025-07-18 08:30:00 -04:00
Zach Gollwitzer
d5b147f2cd
Add indexes to core models (#2464)
* [claudesquad] update from 'add-indexes-to-core-models' on 18 Jul 25 08:03 EDT

* [claudesquad] update from 'add-indexes-to-core-models' on 18 Jul 25 08:09 EDT
2025-07-18 08:19:44 -04:00
Zach Gollwitzer
8c97c9d31a
Consolidate and simplify account pages (#2462)
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
* Remove ScrollFocusable

* Consolidate and simplify account pages

* Lint fixes

* Fix tab param initialization

* Remove stale files

* Remove stale route, make accountable routes clearer
2025-07-18 05:52:18 -04:00
Zach Gollwitzer
3eea5a9891
Add auto-update strategies for current balance on manual accounts (#2460)
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
* Add auto-update strategies for current balance on manual accounts

* Remove deprecated BalanceUpdater, replace with new methods
2025-07-17 06:49:56 -04:00
Zach Gollwitzer
52333e3fa6
Add reconciliation manager (#2459)
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
* Add reconciliation manager

* Fix notes editing
2025-07-16 11:31:47 -04:00
Zach Gollwitzer
89cc64418e
Add confirmation dialog for balance reconciliation creates and updates (#2457)
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
2025-07-15 18:58:40 -04:00
Zach Gollwitzer
c1d98fe73b
Start and end balance anchors for historical account balances (#2455)
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
* Add kind field to valuation

* Fix schema conflict

* Add kind to valuation

* Scaffold opening balance manager

* Opening balance manager implementation

* Update account import to use opening balance manager + tests

* Update account to use opening balance manager

* Fix test assertions, usage of current balance manager

* Lint fixes

* Add Opening Balance manager, add tests to forward calculator

* Add credit card to "all cash" designation

* Simplify valuation model

* Add current balance manager with tests

* Add current balance logic to reverse calculator and plaid sync

* Tweaks to initial calc logic

* Ledger testing helper, tweak assertions for reverse calculator

* Update test assertions

* Extract balance transformer, simplify calculators

* Algo simplifications

* Final tweaks to calculators

* Cleanup

* Fix error, propagate sync errors up to parent

* Update migration script, valuation naming
2025-07-15 11:42:41 -04:00
Zach Gollwitzer
9110ab27d2
Centralize entry naming (#2454)
Some checks failed
Publish Docker image / ci (push) Has been cancelled
Publish Docker image / Build docker image (push) Has been cancelled
* Centralize entry naming

* Lint fixes, code style
2025-07-10 18:40:38 -04:00
Zach Gollwitzer
afbfb474c2 Remove rate limit for api test cases 2025-07-10 16:04:36 -04:00
Zach Gollwitzer
fe8aebe920 Don't raise on invalid demo data 2025-07-10 16:01:47 -04:00
Zach Gollwitzer
188126d402 Fix activity view "new" button styles 2025-07-10 15:30:22 -04:00
Ezra Adeyinka
1a2d973f4b
chore: fix armenian dram incorrect symbol (#2451) 2025-07-10 12:39:25 -04:00
Josh Pigford
a91441bcc8 Remove old unique index on device_id in mobile_devices migration to allow multiple users to share the same device.
Some checks failed
Publish Docker image / ci (push) Has been cancelled
Publish Docker image / Build docker image (push) Has been cancelled
2025-07-09 09:22:51 -05:00
Josh Pigford
8d0c1c5a56 Update database schema: increment version to 2025_07_02_173231, modify virtual column definition for classification, and add new indexes for entries and exchange rates. 2025-07-09 09:20:56 -05:00
dependabot[bot]
e848db2aa1
Bump sidekiq from 8.0.4 to 8.0.5 (#2441)
Some checks failed
Publish Docker image / ci (push) Has been cancelled
Publish Docker image / Build docker image (push) Has been cancelled
---
updated-dependencies:
- dependency-name: sidekiq
  dependency-version: 8.0.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-07 16:39:36 -04:00
dependabot[bot]
e7043328e4
Bump pagy from 9.3.4 to 9.3.5 (#2444)
---
updated-dependencies:
- dependency-name: pagy
  dependency-version: 9.3.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-07 16:39:19 -04:00
dependabot[bot]
d77c683d59
Bump faraday from 2.13.1 to 2.13.2 (#2442)
---
updated-dependencies:
- dependency-name: faraday
  dependency-version: 2.13.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-07 16:28:24 -04:00
dependabot[bot]
aaf24e1309
Bump stripe from 15.2.1 to 15.3.0 (#2445)
---
updated-dependencies:
- dependency-name: stripe
  dependency-version: 15.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-07 16:25:31 -04:00
dependabot[bot]
f9b131a5db
Bump faker from 3.5.1 to 3.5.2 (#2448)
---
updated-dependencies:
- dependency-name: faker
  dependency-version: 3.5.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-07 16:22:10 -04:00
dependabot[bot]
a63d36d10c
Bump sentry-rails from 5.25.0 to 5.26.0 (#2447)
---
updated-dependencies:
- dependency-name: sentry-rails
  dependency-version: 5.26.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-07 16:22:01 -04:00
Zach Gollwitzer
662f2c04ce
Multi-step account forms + clearer balance editing (#2427)
Some checks failed
Publish Docker image / ci (push) Has been cancelled
Publish Docker image / Build docker image (push) Has been cancelled
* Initial multi-step property form

* Improve form structure, add optional tooltip help icons to form fields

* Add basic inline alert component

* Clean up and improve property form lifecycle

* Implement Account status concept

* Lint fixes

* Remove whitespace

* Balance editing, scope updates for account

* Passing tests

* Fix brakeman warning

* Remove stale columns

* data constraint tweaks

* Redundant property
2025-07-03 09:33:07 -04:00
Eran Avidor
ba7e8d3893
Fix/design system violations (#2422)
Some checks failed
Publish Docker image / ci (push) Has been cancelled
Publish Docker image / Build docker image (push) Has been cancelled
* fix: replace hardcoded bg-white with bg-container in notification notice

* fix: replace hardcoded text-white with fg-inverse in notification CTA

* fix: replace hardcoded text-white with fg-inverse in text tooltip

* fix: replace hardcoded bg-gray-900 text-white with bg-inverse fg-inverse in invitations form

* fix: replace hardcoded bg-gray-800 text-white with bg-inverse fg-inverse in AI consent form

* fix: replace hardcoded text-white with fg-inverse in changelog page

* fix: replace hardcoded text-white and border-gray-500 with fg-inverse and border-secondary in investment tooltip

* fix: replace hardcoded text-white with fg-inverse in holdings missing price tooltip

* fix: replace hardcoded text-white and bg-gray-400 with fg-inverse and bg-surface-inset in settings profiles

* fix: replace hardcoded bg-orange-500 text-white with bg-yellow-600 fg-inverse in settings hosting danger zone

---------

Co-authored-by: Eran Avidor <eavidor@Eran-Avidor-MBP.lan>
2025-07-01 13:53:36 -04:00
Zach Gollwitzer
65329b333d Fix settings labels
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
Fixes #2424
2025-06-30 10:22:37 -04:00
dependabot[bot]
0974783a6b
Bump selenium-webdriver from 4.33.0 to 4.34.0 (#2425)
Bumps [selenium-webdriver](https://github.com/SeleniumHQ/selenium) from 4.33.0 to 4.34.0.
- [Release notes](https://github.com/SeleniumHQ/selenium/releases)
- [Changelog](https://github.com/SeleniumHQ/selenium/blob/trunk/rb/CHANGES)
- [Commits](https://github.com/SeleniumHQ/selenium/compare/selenium-4.33.0...selenium-4.34.0)

---
updated-dependencies:
- dependency-name: selenium-webdriver
  dependency-version: 4.34.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-30 09:29:20 -04:00
dependabot[bot]
48f792c20e
Bump jwt from 2.10.1 to 2.10.2 (#2426)
Bumps [jwt](https://github.com/jwt/ruby-jwt) from 2.10.1 to 2.10.2.
- [Release notes](https://github.com/jwt/ruby-jwt/releases)
- [Changelog](https://github.com/jwt/ruby-jwt/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jwt/ruby-jwt/compare/v2.10.1...v2.10.2)

---
updated-dependencies:
- dependency-name: jwt
  dependency-version: 2.10.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-30 09:29:00 -04:00
Zach Gollwitzer
869462a9a5 Dynamic y-axis baseline value for chart scales
Some checks failed
Publish Docker image / ci (push) Has been cancelled
Publish Docker image / Build docker image (push) Has been cancelled
2025-06-27 12:57:23 -04:00
Zach Gollwitzer
e4a82d85e8
Properly handle Plaid investment account transfers (#2420)
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
2025-06-27 10:50:45 -04:00
Zach Gollwitzer
18148acd69
Fix chart scale issues (#2418)
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
2025-06-26 18:59:11 -04:00
Zach Gollwitzer
8db95623cf
Handle holding quantity generation for reverse syncs correctly when not all holdings are generated for current day (#2417)
* Handle reverse calculator starting portfolio generation correctly

* Fix current_holdings to handle different dates and hide zero quantities

- Use DISTINCT ON to get most recent holding per security instead of assuming same date
- Filter out zero quantity holdings from UI display
- Maintain cash display regardless of zero balance
- Use single efficient query with proper Rails syntax

* Continue to process holdings even if one is not resolvable

* Lint fixes
2025-06-26 16:57:17 -04:00
Zach Gollwitzer
e60b5df442
Handle bad API data for trade quantity signage (#2416)
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
2025-06-26 09:54:25 -04:00
Zach Gollwitzer
f3ab4a27ee
Fix credit card balance history (#2414)
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
2025-06-25 17:03:53 -04:00
Zach Gollwitzer
4b50acff2b
Replace sync spinners with pulse animation (#2413)
* Replace sync spinners with pulse animation

* Remove dev code
2025-06-25 16:51:30 -04:00
Joseph Ho
637d630388
transfer: Support transfers of different currencies between accounts. (#2243)
Fixes part of #1852.

Co-authored-by: Zach Gollwitzer <zach@maybe.co>
2025-06-25 16:34:18 -04:00
Zach Gollwitzer
72a0f87a9c
Fix race condition in sync status monitor (#2412)
Move family timestamp update to after_commit callback to ensure
database visibility before cache invalidation
2025-06-25 15:23:15 -04:00
Kenrick Tandrian
cea49d5038
fix(models): use self.id (#2410)
Some checks failed
Publish Docker image / ci (push) Has been cancelled
Publish Docker image / Build docker image (push) Has been cancelled
2025-06-24 11:15:28 -04:00
Josh Pigford
c0617f74cd Fix linting issues in migration file
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
2025-06-23 11:31:57 -05:00
Josh Pigford
653decbc0b Fix outdated timezone references
Updates outdated timezone identifiers in the database to their current
equivalents. This resolves ArgumentError exceptions when users have
outdated timezones like "Europe/Kiev" stored in their preferences.

Timezone mappings:
- Europe/Kiev → Europe/Kyiv
- Asia/Calcutta → Asia/Kolkata
- Asia/Katmandu → Asia/Kathmandu
- Asia/Rangoon → Asia/Yangon
- Asia/Saigon → Asia/Ho_Chi_Minh
- Pacific/Ponape → Pacific/Pohnpei
- Pacific/Truk → Pacific/Chuuk

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-23 11:25:03 -05:00
dependabot[bot]
1cfa6cfca8
Bump lookbook from 2.3.9 to 2.3.11 (#2405)
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
Bumps [lookbook](https://github.com/lookbook-hq/lookbook) from 2.3.9 to 2.3.11.
- [Release notes](https://github.com/lookbook-hq/lookbook/releases)
- [Commits](https://github.com/lookbook-hq/lookbook/compare/v2.3.9...v2.3.11)

---
updated-dependencies:
- dependency-name: lookbook
  dependency-version: 2.3.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-23 10:21:33 -04:00
dependabot[bot]
e809335a47
Bump faraday-retry from 2.3.1 to 2.3.2 (#2406)
Bumps [faraday-retry](https://github.com/lostisland/faraday-retry) from 2.3.1 to 2.3.2.
- [Release notes](https://github.com/lostisland/faraday-retry/releases)
- [Changelog](https://github.com/lostisland/faraday-retry/blob/main/CHANGELOG.md)
- [Commits](https://github.com/lostisland/faraday-retry/compare/v2.3.1...v2.3.2)

---
updated-dependencies:
- dependency-name: faraday-retry
  dependency-version: 2.3.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-23 10:21:25 -04:00
dependabot[bot]
956008acbf
Bump debug from 1.10.0 to 1.11.0 (#2407)
Bumps [debug](https://github.com/ruby/debug) from 1.10.0 to 1.11.0.
- [Release notes](https://github.com/ruby/debug/releases)
- [Commits](https://github.com/ruby/debug/compare/v1.10.0...v1.11.0)

---
updated-dependencies:
- dependency-name: debug
  dependency-version: 1.11.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-23 10:21:14 -04:00
dependabot[bot]
8b56262573
Bump faraday-multipart from 1.1.0 to 1.1.1 (#2408)
Bumps [faraday-multipart](https://github.com/lostisland/faraday-multipart) from 1.1.0 to 1.1.1.
- [Release notes](https://github.com/lostisland/faraday-multipart/releases)
- [Changelog](https://github.com/lostisland/faraday-multipart/blob/main/CHANGELOG.md)
- [Commits](https://github.com/lostisland/faraday-multipart/compare/v1.1.0...v1.1.1)

---
updated-dependencies:
- dependency-name: faraday-multipart
  dependency-version: 1.1.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-23 10:21:07 -04:00
dependabot[bot]
615912040c
Bump ostruct from 0.6.1 to 0.6.2 (#2409)
Bumps [ostruct](https://github.com/ruby/ostruct) from 0.6.1 to 0.6.2.
- [Release notes](https://github.com/ruby/ostruct/releases)
- [Commits](https://github.com/ruby/ostruct/compare/v0.6.1...v0.6.2)

---
updated-dependencies:
- dependency-name: ostruct
  dependency-version: 0.6.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-23 10:20:58 -04:00
Zach Gollwitzer
fcf14f5f27 Add pre-pull request flow for Claude code
Some checks failed
Publish Docker image / ci (push) Has been cancelled
Publish Docker image / Build docker image (push) Has been cancelled
2025-06-20 17:33:03 -04:00
Zach Gollwitzer
63d8114b05
Separate exclude and one-time transaction handling (#2400)
* Separate exclude and one-time transaction handling

- Split transaction "exclude" and "one-time" toggles into separate controls in transaction detail view
- Updated Transaction::Search to show excluded transactions with grayed-out styling instead of filtering them out
- Modified IncomeStatement calculations to exclude both excluded and one_time transactions from totals
- Added migration to convert existing excluded transactions to also be one_time for backward compatibility
- Updated transaction list view to show asterisk for one_time transactions and gray out excluded ones
- Added controller support for kind parameter in transaction updates

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix linting issues

- Remove trailing whitespace from migration
- Fix ERB formatting throughout templates

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-06-20 17:10:36 -04:00
Zach Gollwitzer
c003e8c6ed Transfer match truncation fix 2025-06-20 14:08:46 -04:00
Zach Gollwitzer
ab1c17ea14 Fix transaction layout alignment regression 2025-06-20 13:58:24 -04:00
Zach Gollwitzer
1aae00f586
perf(transactions): add kind to Transaction model and remove expensive Transfer joins in aggregations (#2388)
* add kind to transaction model

* Basic transfer creator

* Fix method naming conflict

* Creator form pattern

* Remove stale methods

* Tweak migration

* Remove BaseQuery, write entire query in each class for clarity

* Query optimizations

* Remove unused exchange rate query lines

* Remove temporary cache-warming strategy

* Fix test

* Update transaction search

* Decouple transactions endpoint from IncomeStatement

* Clean up transactions controller

* Update cursor rules

* Cleanup comments, logic in search

* Fix totals logic on transactions view

* Fix pagination

* Optimize search totals query

* Default to last 30 days on transactions page if no filters

* Decouple transactions list from transfer details

* Revert transfer route

* Migration reset

* Bundle update

* Fix matching logic, tests

* Remove unused code
2025-06-20 13:31:58 -04:00
Josh Pigford
7aca5a2277 Fix remaining rubocop offenses
Some checks failed
Publish Docker image / ci (push) Has been cancelled
Publish Docker image / Build docker image (push) Has been cancelled
- Fix string literal style in doorkeeper.rb
- Add missing final newlines
- Remove trailing whitespace
- Fix array bracket spacing in migrations

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-18 08:38:04 -05:00
Josh Pigford
8296e10246 Fix linting issues and update API key test for source validation
- Remove trailing whitespace in auth controller and mobile device model
- Update API key test to expect new validation message with source

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-18 08:28:32 -05:00
Josh Pigford
9336719242 Add secure OAuth2-based mobile authentication
- Replace API keys with OAuth2 tokens for mobile apps
- Add device tracking and management for mobile sessions
- Implement 30-day token expiration with refresh tokens
- Add MFA/2FA support for mobile login
- Create dedicated auth endpoints (signup/login/refresh)
- Skip CSRF protection for API endpoints
- Return plaintext tokens (not hashed) in responses
- Track devices with unique IDs and metadata
- Enable seamless native mobile experience without OAuth redirects

This provides enterprise-grade security for the iOS/Android apps while maintaining a completely native authentication flow.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-18 08:20:22 -05:00
Josh Pigford
cba0bdf0e2 Fix OAuth mobile app support with custom URL schemes
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
- Configure Doorkeeper to allow custom URL schemes (maybeapp://)
- Disable force_ssl_in_redirect_uri to support non-HTTPS schemes
- Add custom Doorkeeper views with mobile OAuth detection
- Disable Turbo for mobile OAuth flows to prevent redirect interference
- Add display parameter preservation through OAuth flow
- Create custom Doorkeeper layouts with proper styling
- Add comprehensive integration tests for mobile OAuth flows
- Ensure all OAuth pages use proper doorkeeper/application layout

This allows the mobile app to complete OAuth authorization flows
without the web app interfering with custom URL scheme redirects.

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-18 05:38:23 -05:00
Josh Pigford
404066eaa1 Fix rubocop linting issues in API chat endpoints
- Fix trailing whitespace
- Add missing final newlines
- Fix array bracket spacing
- Auto-corrected all layout issues

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-18 04:36:31 -05:00
Josh Pigford
94202b2a6b Add API v1 chat endpoints
- Add chats#index and chats#show endpoints to list and view AI conversations
- Add messages#create endpoint to send messages to AI chats
- Include API documentation for chat endpoints
- Add controller tests for new endpoints

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-18 04:32:14 -05:00
Josh Pigford
4d3c710291 Fix Active Record encryption for self-hosted deployments
Auto-generate encryption keys based on SECRET_KEY_BASE when not provided.
This ensures API key encryption works out of the box for self-hosted users
without requiring manual setup steps.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-18 04:31:10 -05:00
Josh Pigford
b65e4d376e Fix trailing whitespace in API keys system test
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
2025-06-17 16:22:09 -05:00
Josh Pigford
fc921c0cd2 Fix system test failures in API keys and trades tests
- Fix API key scopes validation in test (only one scope allowed)
- Update validation error test to match actual behavior
- Fix regenerating API key test path assertion
- Fix revoke confirmation dialog test to work with custom modal
- Fix trades test ticker symbol reference
- Add small delays for modal animations in system tests

All 59 system tests now pass.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-17 16:20:01 -05:00
Josh Pigford
b803ddac96
Add comprehensive API v1 with OAuth and API key authentication (#2389)
* OAuth

* Add API test routes and update Doorkeeper token handling for test environment

- Introduced API namespace with test routes for controller testing in the test environment.
- Updated Doorkeeper configuration to allow fallback to plain tokens in the test environment for easier testing.
- Modified schema to change resource_owner_id type from bigint to string.

* Implement API key authentication and enhance access control

- Replaced Doorkeeper OAuth authentication with a custom method supporting both OAuth and API keys in the BaseController.
- Added methods for API key authentication, including validation and logging.
- Introduced scope-based authorization for API keys in the TestController.
- Updated routes to include API key management endpoints.
- Enhanced logging for API access to include authentication method details.
- Added tests for API key functionality, including validation, scope checks, and access control enforcement.

* Add API key rate limiting and usage tracking

- Implemented rate limiting for API key authentication in BaseController.
- Added methods to check rate limits, render appropriate responses, and include rate limit headers in responses.
- Updated routes to include a new usage resource for tracking API usage.
- Enhanced tests to verify rate limit functionality, including exceeding limits and per-key tracking.
- Cleaned up Redis data in tests to ensure isolation between test cases.

* Add Jbuilder for JSON rendering and refactor AccountsController

- Added Jbuilder gem for improved JSON response handling.
- Refactored index action in AccountsController to utilize Jbuilder for rendering JSON.
- Removed manual serialization of accounts and streamlined response structure.
- Implemented a before_action in BaseController to enforce JSON format for all API requests.

* Add transactions resource to API routes

- Added routes for transactions, allowing index, show, create, update, and destroy actions.
- This enhancement supports comprehensive transaction management within the API.

* Enhance API authentication and onboarding handling

- Updated BaseController to skip onboarding requirements for API endpoints and added manual token verification for OAuth authentication.
- Improved error handling and logging for invalid access tokens.
- Introduced a method to set up the current context for API requests, ensuring compatibility with session-like behavior.
- Excluded API paths from onboarding redirects in the Onboardable concern.
- Updated database schema to change resource_owner_id type from bigint to string for OAuth access grants.

* Fix rubocop offenses

- Fix indentation and spacing issues
- Convert single quotes to double quotes
- Add spaces inside array brackets
- Fix comment alignment
- Add missing trailing newlines
- Correct else/end alignment

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix API test failures and improve test reliability

- Fix ApiRateLimiterTest by removing mock users method and using fixtures
- Fix UsageControllerTest by removing mock users method and using fixtures
- Fix BaseControllerTest by using different users for multiple API keys
- Use unique display_key values with SecureRandom to avoid conflicts
- Fix double render issue in UsageController by returning after authorize_scope\!
- Specify controller name in routes for usage resource
- Remove trailing whitespace and empty lines per Rubocop

All tests now pass and linting is clean.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Add API transactions controller warning to brakeman ignore

The account_id parameter in the API transactions controller is properly
validated on line 79: family.accounts.find(transaction_params[:account_id])
This ensures users can only create transactions in accounts belonging to
their family, making this a false positive.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Signed-off-by: Josh Pigford <josh@joshpigford.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-06-17 15:57:05 -05:00
Zach Gollwitzer
13a64a1694 Fix failing CI
Some checks failed
Publish Docker image / ci (push) Has been cancelled
Publish Docker image / Build docker image (push) Has been cancelled
2025-06-16 11:07:38 -04:00
Kenrick Tandrian
b900cc9272
Fix: ticker combobox background and text color (#2370)
* fix(ui): hotwire combobox bg color

* fix(ui): text color
2025-06-16 09:58:37 -04:00
dependabot[bot]
dc505cfcff
Bump rack-mini-profiler from 3.3.1 to 4.0.0 (#2381)
Bumps [rack-mini-profiler](https://github.com/MiniProfiler/rack-mini-profiler) from 3.3.1 to 4.0.0.
- [Release notes](https://github.com/MiniProfiler/rack-mini-profiler/releases)
- [Changelog](https://github.com/MiniProfiler/rack-mini-profiler/blob/master/CHANGELOG.md)
- [Commits](https://github.com/MiniProfiler/rack-mini-profiler/compare/v3.3.1...v4.0.0)

---
updated-dependencies:
- dependency-name: rack-mini-profiler
  dependency-version: 4.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-16 09:05:00 -04:00
dependabot[bot]
96ac1dd45f
Bump lookbook from 2.3.9 to 2.3.10 (#2379)
Bumps [lookbook](https://github.com/lookbook-hq/lookbook) from 2.3.9 to 2.3.10.
- [Release notes](https://github.com/lookbook-hq/lookbook/releases)
- [Commits](https://github.com/lookbook-hq/lookbook/compare/v2.3.9...v2.3.10)

---
updated-dependencies:
- dependency-name: lookbook
  dependency-version: 2.3.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-16 09:04:37 -04:00
dependabot[bot]
5a38159c28
Bump plaid from 40.0.0 to 41.0.0 (#2378)
Bumps [plaid](https://github.com/plaid/plaid-ruby) from 40.0.0 to 41.0.0.
- [Release notes](https://github.com/plaid/plaid-ruby/releases)
- [Changelog](https://github.com/plaid/plaid-ruby/blob/master/CHANGELOG.md)
- [Commits](https://github.com/plaid/plaid-ruby/compare/v40.0.0...v41.0.0)

---
updated-dependencies:
- dependency-name: plaid
  dependency-version: 41.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-16 09:04:30 -04:00
dependabot[bot]
38cad49d6c
Bump sentry-ruby from 5.24.0 to 5.25.0 (#2382)
Bumps [sentry-ruby](https://github.com/getsentry/sentry-ruby) from 5.24.0 to 5.25.0.
- [Release notes](https://github.com/getsentry/sentry-ruby/releases)
- [Changelog](https://github.com/getsentry/sentry-ruby/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-ruby/compare/5.24.0...5.25.0)

---
updated-dependencies:
- dependency-name: sentry-ruby
  dependency-version: 5.25.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-16 09:04:23 -04:00
dependabot[bot]
968cd7981a
Bump aasm from 5.5.0 to 5.5.1 (#2384)
Bumps [aasm](https://github.com/aasm/aasm) from 5.5.0 to 5.5.1.
- [Changelog](https://github.com/aasm/aasm/blob/master/CHANGELOG.md)
- [Commits](https://github.com/aasm/aasm/commits/v5.5.1)

---
updated-dependencies:
- dependency-name: aasm
  dependency-version: 5.5.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-16 09:04:14 -04:00
Zach Gollwitzer
6d9bb7f0eb
Temporary transactions page performance fix (#2372)
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
* Temporary transactions page performance fix

* Fix Cursor bugs

* More bugbot bug fixes
2025-06-15 11:36:21 -04:00
Zach Gollwitzer
a5f1677f60
perf(income statement): cache income statement queries (#2371)
* Leftover cleanup from prior PR

* Benchmark convenience task

* Change default warm benchmark time

* Cache income statement queries

* Fix private method access
2025-06-15 10:09:46 -04:00
Zach Gollwitzer
84b2426e54
Benchmarking setup (#2366)
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
* Benchmarking setup

* Get demo data working in benchmark scenario

* Finalize default demo scenario

* Finalize benchmarking setup
2025-06-14 11:53:53 -04:00
Huy Nguyen Quang
cdad31812a
Fix user deletion foreign key constraint with invitations (#2357)
Some checks failed
Publish Docker image / ci (push) Has been cancelled
Publish Docker image / Build docker image (push) Has been cancelled
2025-06-11 20:26:31 -05:00
Zach Gollwitzer
5a4c955522
Realistic demo data for performance testing (#2361)
* Realistic demo data for performance testing

* Add note about performance testing

* Fix bugbot issues

* More realistic account values
2025-06-11 18:48:39 -04:00
Zach Gollwitzer
0d62e60da1 Fix stale reference to classification group name
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
2025-06-10 21:30:53 -04:00
Zach Gollwitzer
10ce2c8e23
Balance sheet cache layer, non-blocking sync UI (#2356)
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
* Balance sheet cache layer with cache-busting

* Update family cache timestamps during Sync

* Less blocking sync loaders

* Consolidate family data caching key logic

* Fix turbo stream broadcasts

* Remove dev delay

* Add back account group sorting
2025-06-10 18:20:06 -04:00
Josh Pigford
dab693d74f Logtail updates
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
2025-06-10 05:10:57 -05:00
Zach Gollwitzer
019a0d873c Fix dark mode text hover styles
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
2025-06-09 18:39:04 -04:00
Zach Gollwitzer
9fabcf4c72
Redis check for self hosted apps (#2353)
* Redis check for self hosted apps

* Run linter with autocorrect

* Add Redis to CI
2025-06-09 18:30:52 -04:00
Zach Gollwitzer
4044a8519f Add account sync button back to self hosted instances
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
2025-06-09 11:35:59 -04:00
Zach Gollwitzer
9afc50a146
Fix merchant editing (#2349) 2025-06-09 10:50:56 -04:00
Tony Tkachenko
0063921de9
fix(ui): mfa backup codes dark mode (#2323)
* fix(ui): mfa backup codes dark mode

* Update app/views/mfa/backup_codes.html.erb

Signed-off-by: Zach Gollwitzer <zach.gollwitzer@gmail.com>

---------

Signed-off-by: Zach Gollwitzer <zach.gollwitzer@gmail.com>
Co-authored-by: Zach Gollwitzer <zach.gollwitzer@gmail.com>
2025-06-09 10:19:09 -04:00
Iuri G.
1d2e7fcae0
perf: Add index to sync status (#2337)
* - Add index to sync status

* - revert typo

* - revert unrelated schema.rb change
2025-06-09 10:18:52 -04:00
Zach Gollwitzer
9f6c9b4057 Update deps 2025-06-09 09:53:32 -04:00
Adam M. Goyer
d05946596e
Fix typo in docker hosting documentation (#2318)
Some checks failed
Publish Docker image / ci (push) Has been cancelled
Publish Docker image / Build docker image (push) Has been cancelled
2025-06-03 05:14:25 -05:00
Josh Pigford
a76cc2dff8 Configure PlaidSandbox to use sandbox environment regardless of Rails config and set test environment variables for Plaid. Temporarily disable AutoSync functionality in tests.
Some checks failed
Publish Docker image / ci (push) Has been cancelled
Publish Docker image / Build docker image (push) Has been cancelled
2025-06-01 06:37:46 -05:00
Josh Pigford
870b543640 Refactor syncing? method in Family model to optimize query performance. Moved visible scope to the beginning and adjusted joins and where conditions to leverage composite indexing for improved efficiency. 2025-06-01 06:30:38 -05:00
Josh Pigford
1f8a994b4e Comment out auto-sync callback in AutoSync concern to disable family synchronization temporarily. 2025-06-01 06:17:11 -05:00
Josh Pigford
ee9fe1b62d
Update README.md
Some checks failed
Publish Docker image / ci (push) Has been cancelled
Publish Docker image / Build docker image (push) Has been cancelled
Signed-off-by: Josh Pigford <josh@joshpigford.com>
2025-05-27 11:08:59 -05:00
Josh Pigford
4f5068e7e5
feat(assistant): improve chat functionality and update tests - refactor configurable model, update OpenAI provider, enhance chat form UI, and improve test coverage (#2316)
Updated model to GPT 4.1
2025-05-27 05:04:58 -05:00
Josh Pigford
e7f1506728 Refactor sparkline error handling and improve series pre-loading
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
- Added pre-loading of series in AccountableSparklinesController and AccountsController to catch errors before rendering.
- Updated the accounts view to use the pre-loaded sparkline series variable.
- Adjusted the test for graceful handling of errors in the sparkline series method.

This enhances the robustness of the sparkline feature and improves error visibility in the UI.
2025-05-26 20:16:07 -05:00
Josh Pigford
6f67827f14 Implement error handling and logging for sparkline and series methods
- Added rescue blocks to handle exceptions in the Accounts and AccountableSparklines controllers, logging errors and rendering error partials.
- Enhanced error handling in the Account::Chartable and Balance::ChartSeriesBuilder models, logging specific error messages for series generation failures.
- Updated the accounts view to include a timeout for Turbo frame loading.
- Added a test to ensure graceful handling of sparkline errors in the AccountsController.

In reference to bug #2315
2025-05-26 20:05:16 -05:00
Josh Pigford
3cc88f3e98
Fix changelog page crash when GitHub release notes are unavailable (#2314)
* Fix changelog page crash when GitHub release notes are unavailable

* Refactor changelog view to handle missing avatars gracefully and improve session sign-out logic in tests

* Enhance changelog view to display fallback messages for unavailable release notes and publication dates

* Update onboarding system tests to reflect UI changes and improve assertions

- Changed button labels from "Get started" to "Continue" and "Complete" to align with updated UI.
- Updated text assertions for clarity, changing "Set your preferences" to "Configure your preferences".
- Adjusted locale selection options to include language codes.
- Enhanced validation error handling in preferences form.
- Improved navigation assertions to ensure accurate path checks.
2025-05-26 19:53:25 -05:00
Josh Pigford
6dae236fe0 Remove trend initialization from Series model to streamline value calculations.
Added initial pass at onboarding system tests.
2025-05-26 18:59:07 -05:00
Josh Pigford
07ca33f2f4
Taskmaster Stubbing (#2313) 2025-05-26 18:39:39 -05:00
dependabot[bot]
fe33fe086a
Bump selenium-webdriver from 4.32.0 to 4.33.0 (#2307)
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
Bumps [selenium-webdriver](https://github.com/SeleniumHQ/selenium) from 4.32.0 to 4.33.0.
- [Release notes](https://github.com/SeleniumHQ/selenium/releases)
- [Changelog](https://github.com/SeleniumHQ/selenium/blob/trunk/rb/CHANGES)
- [Commits](https://github.com/SeleniumHQ/selenium/compare/selenium-4.32.0...selenium-4.33.0)

---
updated-dependencies:
- dependency-name: selenium-webdriver
  dependency-version: 4.33.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-26 06:17:26 -04:00
dependabot[bot]
bf2426ce82
Bump view_component from 3.22.0 to 3.23.2 (#2308)
Bumps [view_component](https://github.com/viewcomponent/view_component) from 3.22.0 to 3.23.2.
- [Release notes](https://github.com/viewcomponent/view_component/releases)
- [Changelog](https://github.com/ViewComponent/view_component/blob/main/docs/CHANGELOG.md)
- [Commits](https://github.com/viewcomponent/view_component/compare/v3.22.0...v3.23.2)

---
updated-dependencies:
- dependency-name: view_component
  dependency-version: 3.23.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-26 06:17:18 -04:00
Zach Gollwitzer
6e202bd7ec
Improve chart performance and gapfilling (#2306)
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
2025-05-25 20:40:18 -04:00
Joseph Ho
e1b81ef879
feature: Show total balance in family currency in accounts (#2283)
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
2025-05-25 11:54:22 -04:00
Kenrick Tandrian
151bf25d27
fix(ui): chart view selector bg color (#2303) 2025-05-25 11:53:14 -04:00
Zach Gollwitzer
854a21993a Handle ITEM_NOT_FOUND errors on Plaid deletions 2025-05-25 11:52:29 -04:00
Zach Gollwitzer
d21e385962
Lazy load Plaid link tokens, fix link issues on broadcast (#2302)
* Lazy load Plaid link tokens, fix link issues on broadcast

* Fix alert styles
2025-05-25 08:12:54 -04:00
Zach Gollwitzer
c701755b02 Require upstream item removal to delete plaid item
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
2025-05-24 19:23:36 -04:00
Zach Gollwitzer
43a403e431 Increase specificity of filter when fetching Plaid liabilities 2025-05-24 19:16:55 -04:00
Zach Gollwitzer
03e0230e99 Do not re-raise ITEM_LOGIN_REQUIRED errors 2025-05-24 18:46:40 -04:00
Zach Gollwitzer
ffc5f844b2 Plaid webhook processor 2025-05-24 18:33:59 -04:00
Zach Gollwitzer
5125411822 Handle duplicate sync jobs 2025-05-24 17:58:17 -04:00
Zach Gollwitzer
aecb5aafd8 Pass transactions cursor when fetching plaid transactions 2025-05-24 17:41:14 -04:00
Zach Gollwitzer
6935ffa3d1 Only fetch needed Plaid products, improve Plaid tests and mocks 2025-05-24 16:40:28 -04:00
Zach Gollwitzer
03a146222d
Plaid sync domain improvements (#2267)
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
Breaks our Plaid sync process out into more manageable classes. Notably, this moves the sync process to a distinct, 2-step flow:

1. Import stage - we first make API calls and import Plaid data to "mirror" tables
2. Processing stage - read the raw data, apply business rules, build internal domain models and sync balances

This provides several benefits:

- Plaid syncs can now be "replayed" without fetching API data again
- Mirror tables provide better audit and debugging capabilities
- Eliminates the "all or nothing" sync behavior that is currently in place, which is brittle
2025-05-23 18:58:22 -04:00
Alex Hatzenbuhler
5c82af0e8c
Fix and improve chat title edit (#2285)
* Fix and improve chat title edit

* Put back background color

* use transparent
2025-05-23 15:31:08 -04:00
Josh Pigford
5cfb4addbd Refactor balance sheet weight calculation and improve group weight rendering
- Update BalanceSheet model to directly calculate account weights based on converted balances.
- Modify dashboard view to compute account weight as a percentage of classification total, enhancing clarity.
- Adjust group weight partial to handle effective weight, ensuring accurate rendering of weight representation.
2025-05-23 12:25:18 -05:00
Josh Pigford
fd65b5a747 Implement caching for classification and account groups in BalanceSheet model and optimize sparkline rendering in views
- Add caching for classification groups and account groups in the BalanceSheet model to improve performance.
- Update views for accountable sparklines to utilize caching for rendered HTML, enhancing load times and reducing database queries.
2025-05-23 11:46:12 -05:00
Josh Pigford
6d4509fbe6 Optimize transaction totals caching and improve default date filter behavior
- Implement caching for transaction totals to enhance performance, using a unique cache key based on family ID and search parameters.
- Adjust default date filter logic to use the user's preferred period when no explicit date filters are provided, reducing the load on the database for large datasets.
2025-05-23 11:30:04 -05:00
Zach Gollwitzer
c7d9c94489 Add back schema migration for unique securities
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
2025-05-22 16:11:22 -04:00
Zach Gollwitzer
fcdc42760d Tweak dup securities data migration 2025-05-22 16:02:34 -04:00
Zach Gollwitzer
19804d2b05 temp: remove schema migration 2025-05-22 15:39:45 -04:00
Zach Gollwitzer
fe24117c50 Stronger security unique index and data migration
Note to self hosters:

If you started self hosting prior to this commit, you may have duplicate securities in your database.

This is usually not a problem, but if you'd like to clean things up, you can run the data migration
by opening a terminal on the machine you're hosting with and running:

```sh
rake data_migration:migrate_duplicate_securities
```
2025-05-22 15:15:07 -04:00
Zach Gollwitzer
e4ee06c9f6
Security resolver and health checker (#2281)
* Setup health check

* Security health checker cron

* Use resolver throughout codebase

* Use resolver for trade builder

* Add security health checks to schedule

* Handle no provider

* Lint fixes
2025-05-22 12:43:24 -04:00
Luan Estradioto
857436d894
fix: mobile responsive category color picker (#2280)
* fix: mobile responsiveness on category picker popup

* fix: mobile responsiveness on category picker popup
2025-05-22 11:50:12 -04:00
Luan Estradioto
092350f1f8
Feat: Mobile Settings menu with preserve scroll + scroll on connect (#2278)
* feat: preserve scroll and scroll on connect, better responsive mobile settings menu

* Update app/javascript/controllers/scroll_on_connect_controller.js

Signed-off-by: Zach Gollwitzer <zach.gollwitzer@gmail.com>

* Update app/javascript/controllers/scroll_on_connect_controller.js

Signed-off-by: Zach Gollwitzer <zach.gollwitzer@gmail.com>

---------

Signed-off-by: Zach Gollwitzer <zach.gollwitzer@gmail.com>
Co-authored-by: Zach Gollwitzer <zach.gollwitzer@gmail.com>
2025-05-22 11:46:57 -04:00
Alex Hatzenbuhler
b719a8b80d
Add new ai chat button, tweak ai navigation (#2272)
* Add new chat button

* Tweak chat navigation

* Fix chat nav padding on new chat

* Make the button nicer

* Fix bad tailwind class

* Use menu icon instead of left

* Fix path
2025-05-22 11:38:05 -04:00
Alex Hatzenbuhler
a71b62575c
UI Fixes (#2276)
* Use rounded-full on budget allocation bar

* Fix backgrounds when balance sheet groups are open

* Add rulers between accounts and classification groups in balance sheet and account groups views.
2025-05-22 11:35:55 -04:00
Luan Estradioto
2fbd6cbc5d
fix: remove transaction form controller (#2279) 2025-05-22 11:30:33 -04:00
Josh Pigford
a7438e5c78 Add country_code attribute to Security model and related classes
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
* Update Security model to include country_code in the data definition.
* Modify Provider::SecurityConcept to define country_code for security.
* Enhance Provider::Synth to extract country_code from security data.
* Update Security::Provided to include country_code when creating security instances.
* Adjust Security::SynthComboboxOption to add country_code as an attribute.
* Revise combobox_security partial to conditionally display country flag and code.
2025-05-22 09:45:08 -05:00
Josh Pigford
fd3b583737 Update subscription upgrade view to replace settings button with account settings link for improved clarity and navigation. 2025-05-22 08:46:57 -05:00
Josh Pigford
34b3e4ae20 Add settings button to subscription upgrade view
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
2025-05-21 14:38:55 -05:00
Alex Hatzenbuhler
8070986763
Standardize corners, backgrounds, and borders (#2271)
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
* Create shared ruler view

* Use collection rendering/spacer templates for rules, and new shared_ruler

* Use shared ruler for all the places a ruler is used

* Use shared ruler for imports and balance sheet

* Fix brakeman by using a static partial with a defined collection

* Standardize & improve a bunch of corners, fix some backgrounds, fix merchants for dark mode

* Update balance sheet

* misc cleanup

* Fix import table

* Remove middot
2025-05-21 10:28:56 -04:00
Josh Pigford
3d2213b760 Enhance cash flow dashboard to handle empty data scenarios. Update Sankey diagram rendering to conditionally display a placeholder message when no cash flow data is available, improving user experience and clarity. 2025-05-21 05:34:42 -05:00
Josh Pigford
cc9a75ee28 Refactor expense processing in Sankey diagram to include only top-level categories. Simplify node addition and link creation for improved readability and maintainability.
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
2025-05-20 20:43:31 -05:00
Alex Hatzenbuhler
443b834b46
Create shared ruler and standardize across views (#2240)
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
* Create shared ruler view

* Use collection rendering/spacer templates for rules, and new shared_ruler

* Use shared ruler for all the places a ruler is used

* Use shared ruler for imports and balance sheet

* Fix brakeman by using a static partial with a defined collection

* Update balance sheet
2025-05-20 15:13:18 -04:00
Josh Pigford
868d4ede6e
Sankey Diagram (#2269)
* Enhance cash flow dashboard with new cash flow period handling and improved Sankey diagram rendering. Update D3 and related dependencies for better performance and features.

* Fix Rubocop offenses

* Refactor Sankey chart controller to use Number.parseFloat for value formatting and improve code readability by restructuring conditional logic for node shapes.
2025-05-20 13:31:05 -05:00
Zach Gollwitzer
caf35701ef Fix Docker builds after package updates 2025-05-20 14:00:31 -04:00
Zach Gollwitzer
94a807c3c9 Encapsulate enrichment actions, add tests 2025-05-20 11:33:35 -04:00
Zach Gollwitzer
dd605a577e Bump ruby to 3.4.4
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
2025-05-20 09:09:10 -04:00
Zach Gollwitzer
137219c121 Fix attribute locking namespace conflict, duplicate syncs
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
2025-05-19 16:39:31 -04:00
Zach Gollwitzer
ab5bce3462 Fix provider guards for start price 2025-05-19 15:19:41 -04:00
dependabot[bot]
a262a749fe
Bump ruby-lsp-rails from 0.4.2 to 0.4.3 (#2262)
Bumps [ruby-lsp-rails](https://github.com/Shopify/ruby-lsp-rails) from 0.4.2 to 0.4.3.
- [Release notes](https://github.com/Shopify/ruby-lsp-rails/releases)
- [Commits](https://github.com/Shopify/ruby-lsp-rails/compare/v0.4.2...v0.4.3)

---
updated-dependencies:
- dependency-name: ruby-lsp-rails
  dependency-version: 0.4.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-19 13:39:48 -04:00
dependabot[bot]
7e7ae31216
Bump sidekiq-cron from 2.2.0 to 2.3.0 (#2261)
Bumps [sidekiq-cron](https://github.com/ondrejbartas/sidekiq-cron) from 2.2.0 to 2.3.0.
- [Release notes](https://github.com/ondrejbartas/sidekiq-cron/releases)
- [Changelog](https://github.com/sidekiq-cron/sidekiq-cron/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ondrejbartas/sidekiq-cron/compare/v2.2.0...v2.3.0)

---
updated-dependencies:
- dependency-name: sidekiq-cron
  dependency-version: 2.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-19 13:39:41 -04:00
dependabot[bot]
efdd03cfe7
Bump vernier from 1.7.0 to 1.7.1 (#2260)
Bumps [vernier](https://github.com/jhawthorn/vernier) from 1.7.0 to 1.7.1.
- [Release notes](https://github.com/jhawthorn/vernier/releases)
- [Commits](https://github.com/jhawthorn/vernier/compare/v1.7.0...v1.7.1)

---
updated-dependencies:
- dependency-name: vernier
  dependency-version: 1.7.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-19 13:34:11 -04:00
Alex Hatzenbuhler
1b4577e21e
Fix subconditions and condition group form (#2256) 2025-05-19 13:34:02 -04:00
dependabot[bot]
e569ad0a8c
Bump sentry-sidekiq from 5.23.0 to 5.24.0 (#2265)
Bumps [sentry-sidekiq](https://github.com/getsentry/sentry-ruby) from 5.23.0 to 5.24.0.
- [Release notes](https://github.com/getsentry/sentry-ruby/releases)
- [Changelog](https://github.com/getsentry/sentry-ruby/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-ruby/compare/5.23.0...5.24.0)

---
updated-dependencies:
- dependency-name: sentry-sidekiq
  dependency-version: 5.24.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-19 13:32:06 -04:00
dependabot[bot]
6f68d66eda
Bump bootsnap from 1.18.4 to 1.18.6 (#2266)
Bumps [bootsnap](https://github.com/Shopify/bootsnap) from 1.18.4 to 1.18.6.
- [Changelog](https://github.com/Shopify/bootsnap/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Shopify/bootsnap/compare/v1.18.4...v1.18.6)

---
updated-dependencies:
- dependency-name: bootsnap
  dependency-version: 1.18.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-19 13:31:56 -04:00
Zach Gollwitzer
e26e5c5aec
Auto sync preference, max limit on account CSV imports (#2259)
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
* Auto sync preference, max limit on account CSV imports

* MaxRowCountExceededError
2025-05-18 15:02:51 -04:00
Zach Gollwitzer
f82f77466a Fix Sentry context for security details exception 2025-05-18 10:52:35 -04:00
Zach Gollwitzer
74c7b0941d More exception logging tweaks 2025-05-18 10:27:46 -04:00
Zach Gollwitzer
29a8ac9d8a Tweak exception logging, sync stale behavior 2025-05-18 10:19:15 -04:00
Zach Gollwitzer
9f13b5bb83
Handle stale syncs (#2257)
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
* Handle stale syncs

* Use `visible` sync logic in sidebar groups
2025-05-17 18:28:21 -04:00
Zach Gollwitzer
10f255a9a9
Clarify backend data pipeline naming concepts (importers, processors, materializers, calculators, and syncers) (#2255)
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
* Rename MarketDataSyncer to MarketDataImporter

* Materializers

* Importers

* More reference replacements
2025-05-17 16:37:16 -04:00
Zach Gollwitzer
b8903d0980 Fix start date missing error in market data sync
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
2025-05-16 14:40:52 -04:00
Zach Gollwitzer
35d1447494 Adjust Sentry missing price and rate to warning level 2025-05-16 14:25:35 -04:00
Zach Gollwitzer
6dc1d22672
Market data sync refinements (#2252)
* Exchange rate syncer implementation

* Security price syncer

* Fix issues with provider API

* Add back prod schedule

* Add back price and exchange rate syncs to account syncs

* Remove unused stock_exchanges table
2025-05-16 14:17:56 -04:00
Alex Hatzenbuhler
6917cecf33
Move to 3 decimal place precision for loans (#2245)
* Move to 3 decimal place precision for loans

* kick for tests

* unkick
2025-05-16 11:24:32 -04:00
Alex Hatzenbuhler
5efa8268f6
Attempt name override (#2244) 2025-05-16 11:23:57 -04:00
Zach Gollwitzer
9155e737b2 Capture broadcast error in Sentry
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
2025-05-15 11:08:27 -04:00
Zach Gollwitzer
a565343102 Fix account group broadcast reference 2025-05-15 10:53:15 -04:00
Zach Gollwitzer
10dd9e061a
Improve account sync performance, handle concurrent market data syncing (#2236)
* PlaidConnectable concern

* Remove bad abstraction

* Put sync implementations in own concerns

* Sync strategies

* Move sync orchestration to Sync class

* Clean up sync class, add state machine

* Basic market data sync cron

* Fix price sync

* Improve sync window column names, add timestamps

* 30 day syncs by default

* Clean up market data methods

* Report high duplicate sync counts to Sentry

* Add sync states throughout app

* account tab session

* Persistent account tab selections

* Remove manual sleep

* Add migration to clear stale syncs on self hosted apps

* Tweak sync states

* Sync completion event broadcasts

* Fix timezones in tests

* Cleanup

* More cleanup

* Plaid item UI broadcasts for sync

* Fix account ID namespace conflict

* Sync broadcasters

* Smoother account sync refreshes

* Remove test sync delay
2025-05-15 10:19:56 -04:00
Taylor Brazelton
9793cc74f9
Typo fix for piece to place. (#2242) 2025-05-15 07:59:00 -04:00
Zach Gollwitzer
3f48992aea Fix trade builder logic
Some checks failed
Publish Docker image / ci (push) Has been cancelled
Publish Docker image / Build docker image (push) Has been cancelled
2025-05-13 16:17:25 -04:00
Zach Gollwitzer
bcb47a9d29 Fix auto sync trigger logic and add tests 2025-05-13 16:14:29 -04:00
Alex Hatzenbuhler
bebe7b40d6
Improve rules - add name, allow sorting, improve UI (#2177)
* Add ability to name a rule

* Add sorting by name and date,

* Improve rule page and form design

* Small header tweak

* Improve sorting click areas by including icon

* Fix brakeman

* Use icon helper instead of lucide_icon helper

* Fix double headers with new DialogComponent

* Use updated_at for sorting instead of created_at

* Use copy-plus icon for compound rules

* Remove icons and change IF/THEN/FOR font in edit form

* Use text-secondary on disabled rules

* First pass at redesigning the sorting menu

* New rule list

* Borders instead of shadows

* Apply proper text color to TO in edit form

* Improve dark mode with proper background color classes

* Use border-secondary

* Add touch: true to conditions and actions of a rule, so updated_at works as expected

* Fix db schema

* Change sort direction to be a LinkComponent outside of the form for better sort behavior

* Clean up dropdown design to match figma

* Match tags/categories design

* Fix name text color, add bg-divider background for dividers

* Fix family subscription tests (thanks zach!)
2025-05-13 15:53:13 -04:00
Alex Hatzenbuhler
050d5ebaad
Ensure the condition group "Add condition" button is type button to avoid form submission (#2233)
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
2025-05-13 10:35:23 -04:00
Alex Hatzenbuhler
30d3eef67f
Fix AND prefix on rule form (#2234)
* Fix AND prefix on rule form

This new condition prefix data target is used to ensure the AND prefix is added/removed to additional conditions/groups when there aren't any saved conditions yet.

* Ensure the condition group "Add condition" button is type button to avoid form submission

* Add prefix update when removing a subcondition

* Use data condition to update the prefix on conditions, condition groups, and subconditions

* Use condition remove instead of element remove for condition groups so prefix logic runs

* Add back explicit show_prefixes to ensure subconditions never have a prefix

* Run the prefix update runs on a load of a form, which handles prefixes on an edit since no conditions change

* Ensure saved items that are marked for removal don't impact the index

* Simplify logic since we don't process subconditions

* Clean up comments

* Add primary_condition_title field to rule model
2025-05-13 10:34:41 -04:00
Zach Gollwitzer
df8e22afe9 Stripe tasks 2025-05-13 08:56:32 -04:00
Zach Gollwitzer
0fb689290a Family subscription unique index 2025-05-13 08:32:53 -04:00
Zach Gollwitzer
9e6e4b1ce6 Only run Plaid syncs via webhook after initial sync
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
2025-05-12 18:55:19 -04:00
Zach Gollwitzer
908b3e2489 Temporary disable of sync cascade behavior 2025-05-12 15:41:14 -04:00
Zach Gollwitzer
a268c5a563 Revert "Add env to toggle provider price syncs"
Some checks failed
Publish Docker image / ci (push) Has been cancelled
Publish Docker image / Build docker image (push) Has been cancelled
This reverts commit 0006b6f6ca.
2025-05-09 17:47:35 -04:00
Zach Gollwitzer
0006b6f6ca Add env to toggle provider price syncs 2025-05-09 16:59:23 -04:00
Zach Gollwitzer
48a07d6158 Revert batch upserting 2025-05-09 16:42:44 -04:00
Zach Gollwitzer
5d798fe0a0 Remove retry logic from security upsert 2025-05-09 16:31:16 -04:00
Zach Gollwitzer
f07c41821e Add warn log for security price upsert retries 2025-05-09 16:05:10 -04:00
Zach Gollwitzer
7605b0221d Batch upsert security prices on sync 2025-05-09 15:56:48 -04:00
Zach Gollwitzer
ab2cec55e7
Propagate child sync errors up to parent, fix sync status (#2232)
* Propagate child sync errors up to parent, fix sync status

* Remove testing error
2025-05-09 14:56:49 -04:00
JIBSIL
03e3899541
Config: put Redis service in Docker local network (#2223)
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
old config exposed the Redis server to the internet if the user had not configured a firewall to block port 6379

Signed-off-by: JIBSIL <40243545+JIBSIL@users.noreply.github.com>
2025-05-09 09:52:56 -04:00
Alex Hatzenbuhler
3371243a00
Use single list for desktop and mobile nav bars (#2227)
* Add rules_label to locale file

* Add rules to settings sidebar, use locale text

* Use a single list for mobile and desktop nav
2025-05-09 09:52:18 -04:00
Zach Gollwitzer
d8e058d7c6 Handle case sensitive values when creating securities
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
2025-05-08 14:31:43 -04:00
Zach Gollwitzer
867318cbc1 Improve sync data management 2025-05-08 12:52:40 -04:00
Zach Gollwitzer
1e5edd9f2f
Fix Plaid cash balance double counting (#2222)
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
* Fix Plaid cash balance double counting

* Fix today's cash balance

* Simplify balance trends in activity view
2025-05-08 12:25:53 -04:00
Alex Hatzenbuhler
42207e487e
Fix dark mode welcome screen for self-hosting (#2225) 2025-05-08 08:45:28 -05:00
Zach Gollwitzer
ea1b6f2bd8
Fix chart timezone bug (#2224)
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
2025-05-07 22:19:09 -04:00
Zach Gollwitzer
2707a40a2a
Handle nested child syncs (#2220)
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
2025-05-07 18:12:08 -04:00
Zach Gollwitzer
8b857e9c8a Notify parent sync in ensure block 2025-05-07 16:51:11 -04:00
Zach Gollwitzer
a07e9d40a3
Transactional locks for sync completions (#2219)
* Transactional locks for sync completions

* Lower sync display logic tolerance in UI
2025-05-07 16:28:58 -04:00
Zach Gollwitzer
71be2a04ad Fix rule title reference 2025-05-07 14:10:56 -04:00
Zach Gollwitzer
a67f36bf64
Prevent account deletions when account is linked to a Plaid Item (#2218)
* Prevent account deletions when account is linked to a Plaid Item

* Only guard deletions in UI and controller, not at model level
2025-05-07 13:56:20 -04:00
Zach Gollwitzer
628d266980 Fix merchant assignment to transactions
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
2025-05-07 11:05:29 -04:00
Josh Pigford
64d5a73eb7 Update render-build.sh 2025-05-07 10:00:24 -05:00
Zach Gollwitzer
2b2dfd03e0 Fix select contrast issues in dark mode forms 2025-05-07 10:23:06 -04:00
Zach Gollwitzer
fb7107d614
feature(dark mode): misc design fixes (#2215)
* Fix category dark mode styles

* Fix sidebar account tab states

* Fix dashboard balance sheet group styles

* Fix budget dark mode styles

* Fix chart gradient split

* Fix prose styles in dark mode

* Add back chat nav id for tests
2025-05-07 09:26:06 -04:00
Zach Gollwitzer
c26a7dd2dd Handle super admins for billing emails
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
2025-05-06 14:14:57 -04:00
Zach Gollwitzer
5da4bb6dc3
Subscription tests and domain (#2209)
* Save work

* Subscriptions and trials domain

* Store family ID on customer

* Remove indirection of stripe calls

* Test simplifications

* Update brakeman

* Fix stripe tests in CI

* Update billing page to show subscription details

* Remove legacy columns

* Complete billing settings page

* Fix hardcoded plan name

* Handle subscriptions for self hosting mode

* Lint fixes
2025-05-06 14:05:21 -04:00
Joseph Ho
8c10e87387
holding: Add multi-currency support for average costs calculations. (#2153)
Fixes: #2051.
2025-05-06 12:12:44 -04:00
Alex Hatzenbuhler
60c3a04a48
Add rule option to change transaction name (#2175)
* Add change name rule for transaction

* Use HTML template in the ERB, clone and inject those templates from the stimulus controller

* Put back the ai_enabled check

* Update docs

* Example of what no case statement would look like

* Remove action_type and needs_value now that controller is injecting templates/hiding action target

* add "to" to template, improve no-option selection, ensure text box is cleared
2025-05-06 12:11:56 -04:00
Alex Hatzenbuhler
c0267d5665
Use icon helper for all-the-things (#2191)
* Use icon helper for all-the-things

* Pass size and color to the icon from buttonish components directly
2025-05-06 12:08:18 -04:00
Zach Gollwitzer
0fdeebceb1 Fix bulk edit dialog form structure 2025-05-06 11:53:12 -04:00
Zach Gollwitzer
2e0794b8e1 Fix bulk editing
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
2025-05-06 11:01:15 -04:00
Zach Gollwitzer
2000f05453
Match Plaid holding values on current day (#2212)
* Match Plaid holding values on current day

* Fix chart timezone issue

* Add timezone tests for syncs

* Hide sidebars on trades test
2025-05-06 09:25:49 -04:00
Akshay Birajdar
470b753833
fix: Rule notification should not be triggered when category is unassigned (#2214) 2025-05-06 09:15:14 -04:00
Joseph Ho
fea1baeb1e
import: Align elements correctly while importing CSV. (#2210)
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
2025-05-05 12:49:25 -04:00
Alex Hatzenbuhler
c022e862aa
Add ability to delete all tags (#2200)
* Add ability to delete all tags

* Downcase resource for confirmation

* Clean up deletion resource names

* titleize button
2025-05-05 12:43:46 -04:00
dependabot[bot]
dcc43cb253
Bump ruby-lsp-rails from 0.4.1 to 0.4.2 (#2206)
Bumps [ruby-lsp-rails](https://github.com/Shopify/ruby-lsp-rails) from 0.4.1 to 0.4.2.
- [Release notes](https://github.com/Shopify/ruby-lsp-rails/releases)
- [Commits](https://github.com/Shopify/ruby-lsp-rails/compare/v0.4.1...v0.4.2)

---
updated-dependencies:
- dependency-name: ruby-lsp-rails
  dependency-version: 0.4.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-05 12:40:15 -04:00
Hayden Bleasel
6e4d35d6ae
Update filled_icon_component.rb (#2205) 2025-05-05 12:40:06 -04:00
dependabot[bot]
98644f1b87
Bump plaid from 37.0.0 to 38.0.0 (#2207)
Bumps [plaid](https://github.com/plaid/plaid-ruby) from 37.0.0 to 38.0.0.
- [Release notes](https://github.com/plaid/plaid-ruby/releases)
- [Changelog](https://github.com/plaid/plaid-ruby/blob/master/CHANGELOG.md)
- [Commits](https://github.com/plaid/plaid-ruby/compare/v37.0.0...v38.0.0)

---
updated-dependencies:
- dependency-name: plaid
  dependency-version: 38.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-05 12:39:58 -04:00
dependabot[bot]
fc9961d420
Bump selenium-webdriver from 4.31.0 to 4.32.0 (#2208)
Bumps [selenium-webdriver](https://github.com/SeleniumHQ/selenium) from 4.31.0 to 4.32.0.
- [Release notes](https://github.com/SeleniumHQ/selenium/releases)
- [Changelog](https://github.com/SeleniumHQ/selenium/blob/trunk/rb/CHANGES)
- [Commits](https://github.com/SeleniumHQ/selenium/compare/selenium-4.31.0...selenium-4.32.0)

---
updated-dependencies:
- dependency-name: selenium-webdriver
  dependency-version: 4.32.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-05 12:39:50 -04:00
Zach Gollwitzer
441f436187
Onboarding redirect tests and trial status bar (#2197)
Some checks failed
Publish Docker image / ci (push) Has been cancelled
Publish Docker image / Build docker image (push) Has been cancelled
* Onboarding redirect tests and trial status bar

* use helper method

* Fix time tolerance failure

* Update post-onboarding message to be generic

* Disable turbo frames on Trial start button

* Update flash notice in test
2025-05-02 15:21:46 -04:00
Zach Gollwitzer
bc7e32deab Fix event processor api 2025-05-02 12:05:50 -04:00
Zach Gollwitzer
a7a29b4780 Prevent ai icon shrinking 2025-05-02 11:42:09 -04:00
Zach Gollwitzer
1e1ed5ca45 Light / dark assistant icon 2025-05-02 11:36:32 -04:00
Zach Gollwitzer
793a5d2502 Fix Stripe event retrieval error 2025-05-02 10:09:23 -04:00
Zach Gollwitzer
84eb2c90d4 Fix self host onboarding 2025-05-02 09:34:48 -04:00
Zach Gollwitzer
1210a8f3a3
Update docker.md
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
Signed-off-by: Zach Gollwitzer <zach@maybe.co>
2025-05-02 08:52:19 -04:00
Zach Gollwitzer
752835f492 Fix transaction form account pre-filling and form styles 2025-05-02 08:45:17 -04:00
Zach Gollwitzer
a1d64d6c2e Fix subdued text in transaction form 2025-05-02 08:23:37 -04:00
Zach Gollwitzer
c24ae1762f Fix account value cutoff on mobile 2025-05-02 08:20:31 -04:00
Zach Gollwitzer
adc5bf58d7 Fix clean import dark mode styles 2025-05-02 08:15:12 -04:00
Zach Gollwitzer
0c79b335f1 Fix self hosted subscription redirect 2025-05-02 07:58:14 -04:00
Zach Gollwitzer
be0d51057d Fix syntax error in account template 2025-05-02 07:54:12 -04:00
Alex Hatzenbuhler
cf72f1a387
Add assign merchant rule for transactions (#2174) 2025-05-02 07:30:31 -04:00
Josh Pigford
0946a1497a Add conditional rendering for account links in transfers view
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
- Implemented checks for the existence of accounts before rendering links in the transfers partial.
- Added error messaging for missing accounts to improve user feedback and prevent broken links.
2025-05-01 19:07:50 -05:00
Josh Pigford
aebbb9a3c1 Enhance institution_domain method in Account model
- Improved error handling for invalid institution URLs by rescuing URI::InvalidURIError and logging a warning.
- Refactored the method to use safe navigation and streamline the URL parsing process.
2025-05-01 18:52:09 -05:00
Alex Hatzenbuhler
194dad702d
Fix initials + profile pictures (#2186)
* Improve initials + profile pictures

* Change to url_options
2025-05-01 18:43:21 -04:00
Josh Pigford
17fa5413f6 Enhance onboarding logic to account for recent trial starts
- Added a check to determine if a trial was started within the last few seconds, allowing for the assumption that onboarding was just completed even if the onboarded_at timestamp appears blank momentarily. This improves the user experience during onboarding transitions.
2025-05-01 17:14:59 -05:00
Josh Pigford
38b6e30bea Add conditional rendering for sync all button in transactions index view
- The "Dev only: Sync all" button is now only displayed in the development environment to prevent accidental usage in production.
2025-05-01 17:01:40 -05:00
Zach Gollwitzer
a51c4d2cba
New onboarding, trials, Stripe integration (#2185)
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
* New onboarding, trials, Stripe integration

* Fix tests

* Lint fixes

* Fix subscription endpoints
2025-05-01 16:47:14 -04:00
Josh Pigford
79b4a3769b Update mobile layout padding and navigation styles for better responsiveness
- Adjusted padding for mobile sidebar to account for safe area insets.
- Modified main content and bottom navigation styles to enhance layout consistency on mobile devices.
2025-05-01 10:22:39 -05:00
Josh Pigford
9a291edbc8 Fix loading notification rendering and update loading component background class
Fixes #2141
2025-05-01 09:38:51 -05:00
Zach Gollwitzer
d266b6a35e Fix mobile settings nav styles
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
2025-04-30 22:29:06 -04:00
Zach Gollwitzer
d8cf35eca7
Mobile layout fixes (#2179)
* Consolidate safe area padding

* Fix chat overflow on mobile
2025-04-30 22:24:13 -04:00
Zach Gollwitzer
23adfb2ef0 Mobile console debugging 2025-04-30 21:01:02 -04:00
Zach Gollwitzer
ed8011f792 Add cache sweeper for components directory 2025-04-30 20:37:23 -04:00
Zach Gollwitzer
90a9546f32
Pre-launch design sync with Figma spec (#2154)
* Add lookbook + viewcomponent, organize design system file

* Build menu component

* Button updates

* More button fixes

* Replace all menus with new ViewComponent

* Checkpoint: fix tests, all buttons and menus converted

* Split into Link and Button components for clarity

* Button cleanup

* Simplify custom confirmation configuration in views

* Finalize button, link component API

* Add toggle field to custom form builder + Component

* Basic tabs component

* Custom tabs, convert all menu / tab instances in app

* Gem updates

* Centralized icon helper

* Update all icon usage to central helper

* Lint fixes

* Centralize all disclosure instances

* Dialog replacements

* Consolidation of all dialog styles

* Test fixes

* Fix app layout issues, move to component with slots

* Layout simplification

* Flakey test fix

* Fix dashboard mobile issues

* Finalize homepage

* Lint fixes

* Fix shadows and borders in dark mode

* Fix tests

* Remove stale class

* Fix filled icon logic

* Move transparent? to public interface
2025-04-30 18:14:22 -04:00
Josh Pigford
1aafed5f8b Update error messages in Transfer model tests for clarity and conciseness
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
2025-04-30 14:45:39 -05:00
Josh Pigford
47017a6432 Enhance account links in transfers view to handle missing accounts gracefully. Added conditional checks to display a warning message when account data is unavailable. 2025-04-30 14:29:33 -05:00
Josh Pigford
ae41b3de46 Refactor Transfer model to use safe navigation for entry associations and improve error messages for account validation 2025-04-30 14:20:09 -05:00
Zach Gollwitzer
d8e34cf791 Add tags to sync failures
Some checks failed
Publish Docker image / ci (push) Has been cancelled
Publish Docker image / Build docker image (push) Has been cancelled
2025-04-28 15:54:12 -04:00
dependabot[bot]
e70295394a
Bump plaid from 36.1.0 to 37.0.0 (#2163)
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
Bumps [plaid](https://github.com/plaid/plaid-ruby) from 36.1.0 to 37.0.0.
- [Release notes](https://github.com/plaid/plaid-ruby/releases)
- [Changelog](https://github.com/plaid/plaid-ruby/blob/master/CHANGELOG.md)
- [Commits](https://github.com/plaid/plaid-ruby/compare/v36.1.0...v37.0.0)

---
updated-dependencies:
- dependency-name: plaid
  dependency-version: 37.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-28 15:07:05 -04:00
dependabot[bot]
8019a0b33c
Bump faraday from 2.13.0 to 2.13.1 (#2166)
Bumps [faraday](https://github.com/lostisland/faraday) from 2.13.0 to 2.13.1.
- [Release notes](https://github.com/lostisland/faraday/releases)
- [Changelog](https://github.com/lostisland/faraday/blob/main/CHANGELOG.md)
- [Commits](https://github.com/lostisland/faraday/compare/v2.13.0...v2.13.1)

---
updated-dependencies:
- dependency-name: faraday
  dependency-version: 2.13.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-28 15:06:54 -04:00
Let Avocado
fe578c8f08
add OPENAI_ACCESS_TOKEN to compose example (#2168)
* add OPENAI_ACCESS_TOKEN to compose example

Signed-off-by: Let Avocado <letavocado@gmail.com>

* Update compose.example.yml

Signed-off-by: Zach Gollwitzer <zach.gollwitzer@gmail.com>

---------

Signed-off-by: Let Avocado <letavocado@gmail.com>
Signed-off-by: Zach Gollwitzer <zach.gollwitzer@gmail.com>
Co-authored-by: Zach Gollwitzer <zach.gollwitzer@gmail.com>
2025-04-28 15:06:29 -04:00
dependabot[bot]
9be0553b18
Bump octokit from 9.2.0 to 10.0.0 (#2164)
Bumps [octokit](https://github.com/octokit/octokit.rb) from 9.2.0 to 10.0.0.
- [Release notes](https://github.com/octokit/octokit.rb/releases)
- [Changelog](https://github.com/octokit/octokit.rb/blob/main/RELEASE.md)
- [Commits](https://github.com/octokit/octokit.rb/compare/v9.2.0...v10.0.0)

---
updated-dependencies:
- dependency-name: octokit
  dependency-version: 10.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-28 14:59:52 -04:00
dependabot[bot]
218040584d
Bump rqrcode from 2.2.0 to 3.0.0 (#2162)
Bumps [rqrcode](https://github.com/whomwah/rqrcode) from 2.2.0 to 3.0.0.
- [Release notes](https://github.com/whomwah/rqrcode/releases)
- [Changelog](https://github.com/whomwah/rqrcode/blob/main/CHANGELOG.md)
- [Commits](https://github.com/whomwah/rqrcode/compare/v2.2.0...v3.0.0)

---
updated-dependencies:
- dependency-name: rqrcode
  dependency-version: 3.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-28 14:59:21 -04:00
dependabot[bot]
90d491906e
Bump ruby-lsp-rails from 0.4.0 to 0.4.1 (#2165)
Bumps [ruby-lsp-rails](https://github.com/Shopify/ruby-lsp-rails) from 0.4.0 to 0.4.1.
- [Release notes](https://github.com/Shopify/ruby-lsp-rails/releases)
- [Commits](https://github.com/Shopify/ruby-lsp-rails/compare/v0.4.0...v0.4.1)

---
updated-dependencies:
- dependency-name: ruby-lsp-rails
  dependency-version: 0.4.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-28 14:57:50 -04:00
dependabot[bot]
3370ae260d
Bump sidekiq from 8.0.2 to 8.0.3 (#2167)
Bumps [sidekiq](https://github.com/sidekiq/sidekiq) from 8.0.2 to 8.0.3.
- [Changelog](https://github.com/sidekiq/sidekiq/blob/main/Changes.md)
- [Commits](https://github.com/sidekiq/sidekiq/compare/v8.0.2...v8.0.3)

---
updated-dependencies:
- dependency-name: sidekiq
  dependency-version: 8.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-28 14:57:42 -04:00
Alex Hatzenbuhler
341a800b65
Improve dashboard/account charts and tooltips (#2157)
* Improve net worth hover

* Improve graph tooltip

* Use locales files for some text on net worth and account charts

* Consolidate and simplify trend change between net worth and account charts

* Fix test and self-review stuff

* Clean up some stuff on the holding sidebar
2025-04-28 14:57:32 -04:00
Josh Pigford
e6b69c1f5c
Update README.md
Signed-off-by: Josh Pigford <josh@joshpigford.com>
2025-04-28 09:03:19 -05:00
Diego Gasparis Escobedo
71bc51ca15
Fix: Filter categories by transaction type in forms (#2082)
Some checks failed
Publish Docker image / ci (push) Has been cancelled
Publish Docker image / Build docker image (push) Has been cancelled
Changed transaction form to only display relevant categories based on transaction type (income or expense), improving usability and preventing misclassification. Created a shared transaction type tabs component for consistent navigation between expense, income, and transfer forms, providing a better user experience and reducing code duplication.

Signed-off-by: Zach Gollwitzer <zach@maybe.co>
Co-authored-by: Zach Gollwitzer <zach@maybe.co>
2025-04-25 10:18:10 -04:00
Alex Hatzenbuhler
ce83418f0b
Improve theme selection (#2152)
* Improve theme selectin

* Use HTML form labels and the existing auto-submit controller
2025-04-25 10:15:48 -04:00
Guilherme Mena
210b89cd17
Improve dark mode styles across multiple pages (#2125)
Some checks failed
Publish Docker image / ci (push) Has been cancelled
Publish Docker image / Build docker image (push) Has been cancelled
* fix: improve dark mode readability across the app

* fix: improve dark mode support for asset percentage text

* fix: apply suggested patch for theme-related improvements

* chore: apply PR feedback – remove dark:, align with design tokens, update form builder

* chore: revert background token and restore original style for visual consistency

* chore: remove unnecessary class attributes from form fields using builder

* refactor: move number_field and date_field into metaprogramming block

* refactor: replace bg-divider-adaptive divs with <hr> and border-secondary

* fix: apply requested changes and linting fixes
2025-04-23 09:42:30 -04:00
Alex Hatzenbuhler
47aeaf8cea
Add nice formatting for subtypes on account list (#2138)
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
* Add nice formatting for subtypes on account list

* Fix rubocop linting errors

* Implement better mapping

* Fix rubocop linting

* Add short and long versions of subtypes

* Simplify subtype reference

Co-authored-by: Zach Gollwitzer <zach.gollwitzer@gmail.com>
Signed-off-by: Alex Hatzenbuhler <hatz@hey.com>

* Simplify reference logic, add a small test

* Fix test

* Fix tests

---------

Signed-off-by: Alex Hatzenbuhler <hatz@hey.com>
Co-authored-by: Zach Gollwitzer <zach.gollwitzer@gmail.com>
2025-04-22 14:10:50 -04:00
Durgaswamy
db34f6d7a2
Fixes #2100: this will fix the problem of text not visible in the modal when dark theme is set (#2112)
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
* Fixes #2100: added 'text-primary' in class , this will fix the problem of text not visible in dark theme inside the modals

* added text-primary in class for form fields

---------

Signed-off-by: Zach Gollwitzer <zach@maybe.co>
Co-authored-by: Zach Gollwitzer <zach@maybe.co>
2025-04-22 09:06:52 -04:00
Zach Gollwitzer
b6cf6198f4 Fix html closing tag 2025-04-22 09:05:38 -04:00
Josh Pigford
a7dfafc907 Add Skylight gem for performance monitoring
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
2025-04-21 08:10:04 -05:00
Josh Pigford
9b33e50b89 Update brakeman.ignore
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
2025-04-20 20:31:20 -05:00
Josh Pigford
1b1add38f2 Enhance member removal process by destroying associated invitations and updating flash messages in ProfilesController. Add test to verify invitation deletion upon member removal.
Fixes #2066
2025-04-20 20:15:49 -05:00
Joseph Ho
08091d24f9
UI: improve alignments with submit button & budget donut (#2131) 2025-04-20 20:06:54 -05:00
Zach Gollwitzer
21623eeb2d Fix display for compound conditions
Some checks failed
Publish Docker image / ci (push) Has been cancelled
Publish Docker image / Build docker image (push) Has been cancelled
2025-04-18 16:56:20 -04:00
Zach Gollwitzer
bcfbc4b324 Add more descriptive labels to rule rows 2025-04-18 16:37:10 -04:00
neo773
04ee1e73be
Fix dark mode support and improve notification/MFA UI (#2126)
* Fix dark mode support and improve notification/MFA UI"

* hide auth tabs on mfa/verify
2025-04-18 13:56:44 -05:00
Zach Gollwitzer
79243822bd Fix migration to handle duplicate merchants per family 2025-04-18 12:09:46 -04:00
Zach Gollwitzer
297a695d0f
Transaction rules engine V1 (#1900)
* Domain model sketch

* Scaffold out rules domain

* Migrations

* Remove existing data enrichment for clean slate

* Sketch out business logic and basic tests

* Simplify rule scope building and action executions

* Get generator working again

* Basic implementation + tests

* Remove manual merchant management (rules will replace)

* Revert "Remove manual merchant management (rules will replace)"

This reverts commit 83dcbd9ff0.

* Family and Provider merchants model

* Fix brakeman warnings

* Fix notification loader

* Update notification position

* Add Rule action and condition registries

* Rule form with compound conditions and tests

* Split out notification types, add CTA type

* Rules form builder and Stimulus controller

* Clean up rule registry domain

* Clean up rules stimulus controller

* CTA message for rule when user changes transaction category

* Fix tests

* Lint updates

* Centralize notifications in Notifiable concern

* Implement category rule prompts with auto backoff and option to disable

* Fix layout bug caused by merge conflict

* Initialize rule with correct action for category CTA

* Add rule deletions, get rules working

* Complete dynamic rule form, split Stimulus controllers by resource

* Fix failing tests

* Change test password to avoid chromium conflicts

* Update integration tests

* Centralize all test password references

* Add re-apply rule action

* Rule confirm modal

* Run migrations

* Trigger rule notification after inline category updates

* Clean up rule styles

* Basic attribute locking for rules

* Apply attribute locks on user edits

* Log data enrichments, only apply rules to unlocked attributes

* Fix merge errors

* Additional merge conflict fixes

* Form UI improvements, ignore attribute locks on manual rule application

* Batch AI auto-categorization of transactions

* Auto merchant detection, ai enrichment in batches

* Fix Plaid merchant assignments

* Plaid category matching

* Cleanup 1

* Test cleanup

* Remove stale route

* Fix desktop chat UI issues

* Fix mobile nav styling issues
2025-04-18 11:39:58 -04:00
Josh Pigford
8edd7ecef0 Update general rules and UI/UX guidelines to clarify hardcoding strings in English for development speed and correct TailwindCSS usage examples. 2025-04-18 10:09:10 -05:00
Zach Gollwitzer
c88fe2e3b2
Feature: Add "amount type" configuration column for CSV imports (#1947)
* Rough draft

* Schema conflict update

* Implement signage

* Update system tests

* Lint fixes
2025-04-18 10:48:10 -04:00
Josh Pigford
8cf077f28d Update PWA logo references in manifest and head partials 2025-04-18 09:47:04 -05:00
Josh Pigford
8985592967 Fix for user menu dark mode text
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
2025-04-18 09:21:57 -05:00
tonychen0110
d22a16d8de
Fix: Changing apply button text to respect theme so it is visible (#2117)
Co-authored-by: Josh Pigford <josh@joshpigford.com>
2025-04-18 08:48:39 -05:00
Zach Gollwitzer
77a2d6a048 Proper error handling in Sync class 2025-04-18 09:46:49 -04:00
neo773
65e1bc6edd
Feature: Implement Mobile Responsiveness (#2092)
* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* format

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* fix conflict

* fix conflict

* chore: run rubocop

* fix test

* update PWA logo

* fix tests

* chore: lint

* fix test

* Refactor: Remove duplicate data attribute in activity partial and add chat form rendering in chats index

---------

Co-authored-by: Josh Pigford <josh@joshpigford.com>
2025-04-18 08:23:10 -05:00
Luan Estradioto
6a21f26d2d
Fix: No comma when locality is empty (small fix) (#2111)
Some checks failed
Publish Docker image / ci (push) Has been cancelled
Publish Docker image / Build docker image (push) Has been cancelled
* Fix: No comma when locality is empty

* better cleanup on address string

* fix test to one-liner

* add more testing
2025-04-16 20:26:45 -05:00
Zach Gollwitzer
298e150f43 Fix stale model reference
Some checks failed
Publish Docker image / ci (push) Has been cancelled
Publish Docker image / Build docker image (push) Has been cancelled
2025-04-14 12:51:38 -04:00
Zach Gollwitzer
e657c40d19
Account:: namespace simplifications and cleanup (#2110)
* Flatten Holding model

* Flatten balance model

* Entries domain renames

* Fix valuations reference

* Fix trades stream

* Fix brakeman warnings

* Fix tests

* Replace existing entryable type references in DB
2025-04-14 11:40:34 -04:00
Joseph Ho
f181ba941f
loan: Set the first valuation as the original principal. (#2088)
Fix: #1645.
2025-04-14 09:09:25 -04:00
dependabot[bot]
5cb2183bdf
Bump stripe from 14.0.0 to 15.0.0 (#2105)
Bumps [stripe](https://github.com/stripe/stripe-ruby) from 14.0.0 to 15.0.0.
- [Release notes](https://github.com/stripe/stripe-ruby/releases)
- [Changelog](https://github.com/stripe/stripe-ruby/blob/master/CHANGELOG.md)
- [Commits](https://github.com/stripe/stripe-ruby/compare/v14.0.0...v15.0.0)

---
updated-dependencies:
- dependency-name: stripe
  dependency-version: 15.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Zach Gollwitzer <zach@maybe.co>
2025-04-14 09:05:41 -04:00
dependabot[bot]
6f70a54d6f
Bump faraday from 2.12.2 to 2.13.0 (#2106)
Bumps [faraday](https://github.com/lostisland/faraday) from 2.12.2 to 2.13.0.
- [Release notes](https://github.com/lostisland/faraday/releases)
- [Changelog](https://github.com/lostisland/faraday/blob/main/CHANGELOG.md)
- [Commits](https://github.com/lostisland/faraday/compare/v2.12.2...v2.13.0)

---
updated-dependencies:
- dependency-name: faraday
  dependency-version: 2.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Zach Gollwitzer <zach@maybe.co>
2025-04-14 09:05:33 -04:00
Tony Vincent
f235697178
Fix: Fix unalble to reject automatched transfers (#2102)
Co-authored-by: Zach Gollwitzer <zach@maybe.co>
2025-04-14 09:05:25 -04:00
dependabot[bot]
e517127062
Bump csv from 3.3.3 to 3.3.4 (#2107)
Bumps [csv](https://github.com/ruby/csv) from 3.3.3 to 3.3.4.
- [Release notes](https://github.com/ruby/csv/releases)
- [Changelog](https://github.com/ruby/csv/blob/main/NEWS.md)
- [Commits](https://github.com/ruby/csv/compare/v3.3.3...v3.3.4)

---
updated-dependencies:
- dependency-name: csv
  dependency-version: 3.3.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Zach Gollwitzer <zach@maybe.co>
2025-04-14 08:52:30 -04:00
busybox
b06fd1edf0
Small count fix in hosting section (#2094)
Signed-off-by: busybox <29630035+busybox11@users.noreply.github.com>
2025-04-14 08:47:54 -04:00
Zach Gollwitzer
1e01840fee
Chromium E2E test fixes (#2108)
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
* Change test password to avoid chromium conflicts

* Update integration tests

* Centralize all test password references

* Remove unrelated schema changes
2025-04-14 08:41:49 -04:00
Tony Vincent
48c8499b70
Add tags selection and notes input to new transaction form (#2008)
Some checks failed
Publish Docker image / ci (push) Has been cancelled
Publish Docker image / Build docker image (push) Has been cancelled
* feat: Add tags selection and notes input to new transaction form

* feat: Add tag selection to transactions bulk update form
2025-04-11 12:14:21 -04:00
Zach Gollwitzer
8648f11413
Sync hierarchy updates (#2087)
* Add parent sync orchestration

* Pass sync object to dependents
2025-04-11 12:13:46 -04:00
Zach Gollwitzer
9fa3698823
Bump to v0.5.0
Signed-off-by: Zach Gollwitzer <zach@maybe.co>
2025-04-11 11:57:02 -04:00
Josh Pigford
88a6373e84
Implement dark mode (#2078)
* User theme settings

* Initial rough pass on colors

* More progress on dark mode
2025-04-11 09:28:00 -05:00
Zach Gollwitzer
52d170e36c
Mobile responsive template preparation (#2071)
Some checks failed
Publish Docker image / ci (push) Has been cancelled
Publish Docker image / Build docker image (push) Has been cancelled
* Mobile responsive template

* Fix sidebar mobile conflict

* Lint fix
2025-04-09 12:42:46 -04:00
Akshay Birajdar
2bc3887262
Fix currency symbol for Uncategorized budget to match budget currency (#2058)
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
Previously, the symbol for the 'Uncategorized' segment defaulted to `$`, which is incorrect for non-USD budgets. This change ensures the correct currency symbol is shown based on the budget's currency.

Co-authored-by: Zach Gollwitzer <zach@maybe.co>
2025-04-08 12:02:05 -04:00
dependabot[bot]
0da057b792
Bump sidekiq from 8.0.1 to 8.0.2 (#2059)
Bumps [sidekiq](https://github.com/sidekiq/sidekiq) from 8.0.1 to 8.0.2.
- [Changelog](https://github.com/sidekiq/sidekiq/blob/main/Changes.md)
- [Commits](https://github.com/sidekiq/sidekiq/compare/v8.0.1...v8.0.2)

---
updated-dependencies:
- dependency-name: sidekiq
  dependency-version: 8.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Zach Gollwitzer <zach@maybe.co>
2025-04-08 11:48:14 -04:00
dependabot[bot]
0e1c902b63
Bump selenium-webdriver from 4.30.1 to 4.31.0 (#2060)
Bumps [selenium-webdriver](https://github.com/SeleniumHQ/selenium) from 4.30.1 to 4.31.0.
- [Release notes](https://github.com/SeleniumHQ/selenium/releases)
- [Changelog](https://github.com/SeleniumHQ/selenium/blob/trunk/rb/CHANGES)
- [Commits](https://github.com/SeleniumHQ/selenium/commits/selenium-4.31.0)

---
updated-dependencies:
- dependency-name: selenium-webdriver
  dependency-version: 4.31.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Zach Gollwitzer <zach@maybe.co>
2025-04-08 11:47:53 -04:00
dependabot[bot]
bb0f0239fb
Bump faraday-retry from 2.3.0 to 2.3.1 (#2061)
Bumps [faraday-retry](https://github.com/lostisland/faraday-retry) from 2.3.0 to 2.3.1.
- [Release notes](https://github.com/lostisland/faraday-retry/releases)
- [Changelog](https://github.com/lostisland/faraday-retry/blob/main/CHANGELOG.md)
- [Commits](https://github.com/lostisland/faraday-retry/compare/v2.3.0...v2.3.1)

---
updated-dependencies:
- dependency-name: faraday-retry
  dependency-version: 2.3.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Zach Gollwitzer <zach@maybe.co>
2025-04-08 11:47:46 -04:00
dependabot[bot]
e6c1c5f368
Bump vernier from 1.6.0 to 1.7.0 (#2062)
Bumps [vernier](https://github.com/jhawthorn/vernier) from 1.6.0 to 1.7.0.
- [Release notes](https://github.com/jhawthorn/vernier/releases)
- [Commits](https://github.com/jhawthorn/vernier/compare/v1.6.0...v1.7.0)

---
updated-dependencies:
- dependency-name: vernier
  dependency-version: 1.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Zach Gollwitzer <zach@maybe.co>
2025-04-08 11:47:36 -04:00
dependabot[bot]
02bbeeaec5
Bump stripe from 13.5.0 to 14.0.0 (#2063)
Bumps [stripe](https://github.com/stripe/stripe-ruby) from 13.5.0 to 14.0.0.
- [Release notes](https://github.com/stripe/stripe-ruby/releases)
- [Changelog](https://github.com/stripe/stripe-ruby/blob/master/CHANGELOG.md)
- [Commits](https://github.com/stripe/stripe-ruby/compare/v13.5.0...v14.0.0)

---
updated-dependencies:
- dependency-name: stripe
  dependency-version: 14.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Zach Gollwitzer <zach@maybe.co>
2025-04-08 11:25:49 -04:00
dependabot[bot]
1649e991b4
Bump brakeman from 7.0.1 to 7.0.2 (#2064)
Bumps [brakeman](https://github.com/presidentbeef/brakeman) from 7.0.1 to 7.0.2.
- [Release notes](https://github.com/presidentbeef/brakeman/releases)
- [Changelog](https://github.com/presidentbeef/brakeman/blob/main/CHANGES.md)
- [Commits](https://github.com/presidentbeef/brakeman/compare/v7.0.1...v7.0.2)

---
updated-dependencies:
- dependency-name: brakeman
  dependency-version: 7.0.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-08 11:00:11 -04:00
Akshay Birajdar
7096eefa2b
Fix transfers#update to save notes (#2053)
Some checks failed
Publish Docker image / ci (push) Has been cancelled
Publish Docker image / Build docker image (push) Has been cancelled
2025-04-04 12:15:47 -04:00
Zach Gollwitzer
4c72231312 Update brakeman 2025-04-04 12:15:10 -04:00
Joseph Ho
d86ccd36b6
provider: Ensure data provider exist before fetching for price. (#2045) 2025-04-04 12:14:28 -04:00
Zach Gollwitzer
02bfa9f251
Fix AI sidebar overflow when user hasn't enabled or created a chat yet (#2044)
Some checks failed
Publish Docker image / ci (push) Has been cancelled
Publish Docker image / Build docker image (push) Has been cancelled
2025-04-01 14:36:34 -04:00
Josh Pigford
f2020a816a Apparently capitalization matters
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
2025-04-01 08:21:46 -05:00
dependabot[bot]
5f2a031d4c
Bump ruby-openai from 8.0.0 to 8.1.0 (#2036)
Bumps [ruby-openai](https://github.com/alexrudall/ruby-openai) from 8.0.0 to 8.1.0.
- [Release notes](https://github.com/alexrudall/ruby-openai/releases)
- [Changelog](https://github.com/alexrudall/ruby-openai/blob/main/CHANGELOG.md)
- [Commits](https://github.com/alexrudall/ruby-openai/compare/v8.0.0...v8.1.0)

---
updated-dependencies:
- dependency-name: ruby-openai
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-01 09:11:56 -04:00
Zach Gollwitzer
939244bd3e
Use faraday retry, move retry logic to concrete provider level (#2042) 2025-04-01 08:41:49 -04:00
Joseph Ho
0a17b84566
perf(imports): Bulk import CSV trades (#2040) 2025-04-01 07:58:49 -04:00
Zach Gollwitzer
5cf758bd03
improvements(ai): Improve AI streaming UI/UX interactions + better separation of AI provider responsibilities (#2039)
* Start refactor

* Interface updates

* Rework Assistant, Provider, and tests for better domain boundaries

* Consolidate and simplify OpenAI provider and provider concepts

* Clean up assistant streaming

* Improve assistant message orchestration logic

* Clean up "thinking" UI interactions

* Remove stale class

* Regenerate VCR test responses
2025-04-01 07:21:54 -04:00
Josh Pigford
6331788b33 Update Intercom configuration to use symbol keys for custom data attributes
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
2025-03-31 09:39:17 -05:00
Josh Pigford
83bee295ca Add custom data to Intercom configuration 2025-03-31 09:02:25 -05:00
Zach Gollwitzer
dc17a0a298 Make provider errors more specific
Some checks failed
Publish Docker image / ci (push) Has been cancelled
Publish Docker image / Build docker image (push) Has been cancelled
2025-03-28 17:53:04 -04:00
Zach Gollwitzer
29f445d75e Fix security search 2025-03-28 17:34:29 -04:00
Zach Gollwitzer
9fadfe074b Disable turbo on onboarding form 2025-03-28 17:24:17 -04:00
Josh Pigford
2a505b000c Fix for unnecessary CSS file 2025-03-28 15:35:26 -05:00
Josh Pigford
36a66baf00 Slight adjustments to AI prompt
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
2025-03-28 15:30:11 -05:00
Zach Gollwitzer
67716f3006 Add default queue as fallback 2025-03-28 13:29:56 -04:00
Zach Gollwitzer
1061aacb0f Set AI queue 2025-03-28 13:27:50 -04:00
Zach Gollwitzer
2f6b11c18f
Personal finance AI (v1) (#2022)
* AI sidebar

* Add chat and message models with associations

* Implement AI chat functionality with sidebar and messaging system

- Add chat and messages controllers
- Create chat and message views
- Implement chat-related routes
- Add message broadcasting and user interactions
- Update application layout to support chat sidebar
- Enhance user model with initials method

* Refactor AI sidebar with enhanced chat menu and interactions

- Update sidebar layout with dynamic width and improved responsiveness
- Add new chat menu Stimulus controller for toggling between chat and chat list views
- Improve chat list display with recent chats and empty state
- Extract AI avatar to a partial for reusability
- Enhance message display and interaction styling
- Add more contextual buttons and interaction hints

* Improve chat scroll behavior and message styling

- Refactor chat scroll functionality with Stimulus controller
- Optimize message scrolling in chat views
- Update message styling for better visual hierarchy
- Enhance chat container layout with flex and auto-scroll
- Simplify message rendering across different chat views

* Extract AI avatar to a shared partial for consistent styling

- Refactor AI avatar rendering across chat views
- Replace hardcoded avatar markup with a reusable partial
- Simplify avatar display in chats and messages views

* Update sidebar controller to handle right panel width dynamically

- Add conditional width class for right sidebar panel
- Ensure consistent sidebar toggle behavior for both left and right panels
- Use specific width class for right panel (w-[375px])

* Refactor chat form and AI greeting with flexible partials

- Extract message form to a reusable partial with dynamic context support
- Create flexible AI greeting partial for consistent welcome messages
- Simplify chat and sidebar views by leveraging new partials
- Add support for different form scenarios (chat, new chat, sidebar)
- Improve code modularity and reduce duplication

* Add chat clearing functionality with dynamic menu options

- Implement clear chat action in ChatsController
- Add clear chat route to support clearing messages
- Update AI sidebar with dropdown menu for chat actions
- Preserve system message when clearing chat
- Enhance chat interaction with new menu options

* Add frontmatter to project structure documentation

- Create initial frontmatter for structure.mdc file
- Include description and configuration options
- Prepare for potential dynamic documentation rendering

* Update general project rules with additional guidelines

- Add rule for using `Current.family` instead of `current_family`
- Include new guidelines for testing, API routes, and solution approach
- Expand project-specific rules for more consistent development practices

* Add OpenAI gem and AI-friendly data representations

- Add `ruby-openai` gem for AI integration
- Implement `to_ai_readable_hash` methods in BalanceSheet and IncomeStatement
- Include Promptable module in both models
- Add savings rate calculation method in IncomeStatement
- Prepare financial models for AI-powered insights and interactions

* Enhance AI Financial Assistant with Advanced Querying and Debugging Capabilities

- Implement comprehensive AI financial query system with function-based interactions
- Add detailed debug logging for AI responses and function calls
- Extend BalanceSheet and IncomeStatement models with AI-friendly methods
- Create robust error handling and fallback mechanisms for AI queries
- Update chat and message views to support debug mode and enhanced rendering
- Add AI query routes and initial test coverage for financial assistant

* Refactor AI sidebar and chat layout with improved structure and comments

- Remove inline AI chat from application layout
- Enhance AI sidebar with more semantic HTML structure
- Add descriptive comments to clarify different sections of chat view
- Improve flex layout and scrolling behavior in chat messages container
- Optimize message rendering with more explicit class names and structure

* Add Markdown rendering support for AI chat messages

- Implement `markdown` helper method in ApplicationHelper using Redcarpet
- Update message view to render AI messages with Markdown formatting
- Add comprehensive Markdown rendering options (tables, code blocks, links)
- Enhance AI Financial Assistant prompt to encourage Markdown usage
- Remove commented Markdown CSS in Tailwind application stylesheet

* Missing comma

* Enhance AI response processing with chat history context

* Improve AI debug logging with payload size limits and internal message flag

* Enhance AI chat interaction with improved thinking indicator and scrolling behavior

* Add AI consent and enable/disable functionality for AI chat

* Upgrade Biome and refactor JavaScript template literals

- Update @biomejs/biome to latest version with caret (^) notation
- Refactor AI query and chat controllers to use template literals
- Standardize npm scripts formatting in package.json

* Add beta testing usage note to AI consent modal

* Update test fixtures and configurations for AI chat functionality

- Add family association to chat fixtures and tests
- Set consistent password digest for test users
- Enable AI for test users
- Add OpenAI access token for test environment
- Update chat and user model tests to include family context

* Simplify data model and get tests passing

* Remove structure.mdc from version control

* Integrate AI chat styles into existing prose pattern

* Match Figma design spec, implement Turbo frames and actions for chats controller

* AI rules refresh

* Consolidate Stimulus controllers, thinking state, controllers, and views

* Naming, domain alignment

* Reset migrations

* Improve data model to support tool calls and message types

* Tool calling tests and fixtures

* Tool call implementation and test

* Get assistant test working again

* Test updates

* Process tool calls within provider

* Chat UI back to working state again

* Remove stale code

* Tests passing

* Update openai class naming to avoid conflicts

* Reconfigure test env

* Rebuild gemfile

* Fix naming conflicts for ChatResponse

* Message styles

* Use OpenAI conversation state management

* Assistant function base implementation

* Add back thinking messages, clean up error handling for chat

* Fix sync error when security price has bad data from provider

* Add balance sheet function to assistant

* Add better function calling error visibility

* Add income statement function

* Simplify and clean up "thinking" interactions with Turbo frames

* Remove stale data definitions from functions

* Ensure VCR fixtures working with latest code

* basic stream implementation

* Get streaming working

* Make AI sidebar wider when left sidebar is collapsed

* Get tests working with streaming responses

* Centralize provider error handling

* Provider data boundaries

---------

Co-authored-by: Josh Pigford <josh@joshpigford.com>
2025-03-28 13:08:22 -04:00
Joseph Ho
8e6b81af77
bug: Use correct currency value while setting the currency. (#2018)
Some checks failed
Publish Docker image / ci (push) Has been cancelled
Publish Docker image / Build docker image (push) Has been cancelled
Fixes: #1754.
2025-03-24 10:06:29 -04:00
dependabot[bot]
9f062de6b4
Bump selenium-webdriver from 4.29.1 to 4.30.1 (#2020)
Bumps [selenium-webdriver](https://github.com/SeleniumHQ/selenium) from 4.29.1 to 4.30.1.
- [Release notes](https://github.com/SeleniumHQ/selenium/releases)
- [Changelog](https://github.com/SeleniumHQ/selenium/blob/trunk/rb/CHANGES)
- [Commits](https://github.com/SeleniumHQ/selenium/commits)

---
updated-dependencies:
- dependency-name: selenium-webdriver
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-24 10:06:18 -04:00
dependabot[bot]
3dfdd0aea5
Bump vernier from 1.5.0 to 1.6.0 (#2019)
Bumps [vernier](https://github.com/jhawthorn/vernier) from 1.5.0 to 1.6.0.
- [Release notes](https://github.com/jhawthorn/vernier/releases)
- [Commits](https://github.com/jhawthorn/vernier/compare/v1.5.0...v1.6.0)

---
updated-dependencies:
- dependency-name: vernier
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-24 10:06:05 -04:00
dependabot[bot]
86431e79a3
Bump csv from 3.3.2 to 3.3.3 (#2021)
Bumps [csv](https://github.com/ruby/csv) from 3.3.2 to 3.3.3.
- [Release notes](https://github.com/ruby/csv/releases)
- [Changelog](https://github.com/ruby/csv/blob/main/NEWS.md)
- [Commits](https://github.com/ruby/csv/compare/v3.3.2...v3.3.3)

---
updated-dependencies:
- dependency-name: csv
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-24 10:03:22 -04:00
Joseph Ho
54f5a44a60
devContainer: Use Redis for ActiveJob and ActionCable. (#2017)
* devContainer: Use Redis for ActiveJob and ActionCable

* devContainer: Simplify environment variables for services.

* devContainer: Remove version field as it's no longer required in Compose.
2025-03-24 10:00:42 -04:00
Joseph Ho
b41897b5e5
import: Bulk import transaction data. (#1962)
Fixes: #1846.
2025-03-24 09:59:27 -04:00
Nick Ostrovsky
f8d64561cf
Fix Account Groups wrapping in Balace Sheet (#2010)
Some checks failed
Publish Docker image / ci (push) Has been cancelled
Publish Docker image / Build docker image (push) Has been cancelled
2025-03-21 13:18:12 -04:00
Tony Vincent
5a8074c7ee
fix: Fix incorrect entry sorting in activity view (#2006)
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
2025-03-21 10:32:05 -04:00
Zach Gollwitzer
9122eafd31
Update issue templates
Some checks failed
Publish Docker image / ci (push) Has been cancelled
Publish Docker image / Build docker image (push) Has been cancelled
2025-03-19 14:05:00 -04:00
Zach Gollwitzer
19cc63c8f4
Use Redis for ActiveJob and ActionCable (#2004)
* Use Redis for ActiveJob and ActionCable

* Fix alwaysApply setting

* Update queue names and weights

* Tweak weights

* Update job queues

* Update docker setup guide

* Remove deprecated upgrade columns from users table

* Refactor Redis configuration for Sidekiq and caching in production environment

* Add Sidekiq Sentry monitoring

* queue naming fix

* Clean up schema
2025-03-19 12:36:16 -04:00
Vaibhav Agrawal
a7db914005
Update security price query in demo generator (#2000)
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
2025-03-19 08:49:30 -04:00
Zach Gollwitzer
06468a05b1 Update default DB pool size
Some checks failed
Publish Docker image / ci (push) Has been cancelled
Publish Docker image / Build docker image (push) Has been cancelled
2025-03-17 20:04:38 -04:00
Zach Gollwitzer
087dd720c1 Report ActionCable errors to Sentry
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
2025-03-17 17:26:19 -04:00
Zach Gollwitzer
78baf2b327 Update deps 2025-03-17 13:04:59 -04:00
dependabot[bot]
56203b04d3
Bump sentry-rails from 5.22.4 to 5.23.0 (#1996)
Bumps [sentry-rails](https://github.com/getsentry/sentry-ruby) from 5.22.4 to 5.23.0.
- [Release notes](https://github.com/getsentry/sentry-ruby/releases)
- [Changelog](https://github.com/getsentry/sentry-ruby/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-ruby/compare/5.22.4...5.23.0)

---
updated-dependencies:
- dependency-name: sentry-rails
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-17 11:55:07 -04:00
Zach Gollwitzer
f65b93a352
Data provider simplification, tests, and documentation (#1997)
* Ignore env.test from source control

* Simplification of providers interface

* Synth tests

* Update money to use new find rates method

* Remove unused issues code

* Additional issue feature removals

* Update price data fetching and tests

* Update documentation for providers

* Security test fixes

* Fix self host test

* Update synth usage data access

* Remove AI pr schema changes
2025-03-17 11:54:53 -04:00
Zach Gollwitzer
dd75cadebc
Fix transaction filters when transfers are present (#1986)
Some checks failed
Publish Docker image / ci (push) Has been cancelled
Publish Docker image / Build docker image (push) Has been cancelled
* Proper filtering of transfers in search

* Fix transaction search
2025-03-11 15:38:45 -04:00
Josh Pigford
ed55ef624b Update billing settings view and locales 2025-03-11 13:00:34 -05:00
Zach Gollwitzer
f363fd4a4e
Fix incorrect totals calculation when family has loan payments (#1984)
* Fix income totals calculation error when loan payments exist

* Include transaction totals in totals query
2025-03-11 12:37:57 -04:00
Zach Gollwitzer
b8a3ca7732
Fetch exchange rates for accounts that require conversion for net worth rollups (#1983)
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
* Sync required exchange rates for accounts

* Refactor into concern
2025-03-11 10:10:28 -04:00
Zach Gollwitzer
7b751ac7ca Do not prompt upgrades until user is logged in
Fixes #1982
2025-03-11 09:11:40 -04:00
Josh Pigford
15d59959cf Fix issue of syncing notice covering up user menu
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
2025-03-10 11:20:58 -05:00
dependabot[bot]
c66401dc0f
Bump stripe from 13.4.1 to 13.5.0 (#1970)
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
Bumps [stripe](https://github.com/stripe/stripe-ruby) from 13.4.1 to 13.5.0.
- [Release notes](https://github.com/stripe/stripe-ruby/releases)
- [Changelog](https://github.com/stripe/stripe-ruby/blob/master/CHANGELOG.md)
- [Commits](https://github.com/stripe/stripe-ruby/compare/v13.4.1...v13.5.0)

---
updated-dependencies:
- dependency-name: stripe
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-10 09:34:15 -04:00
dependabot[bot]
9dcb9e8ed2
Bump webmock from 3.25.0 to 3.25.1 (#1968)
Bumps [webmock](https://github.com/bblimke/webmock) from 3.25.0 to 3.25.1.
- [Changelog](https://github.com/bblimke/webmock/blob/master/CHANGELOG.md)
- [Commits](https://github.com/bblimke/webmock/compare/v3.25.0...v3.25.1)

---
updated-dependencies:
- dependency-name: webmock
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-10 09:16:01 -04:00
dependabot[bot]
045fa1931c
Bump good_job from 4.9.0 to 4.9.3 (#1969)
Bumps [good_job](https://github.com/bensheldon/good_job) from 4.9.0 to 4.9.3.
- [Release notes](https://github.com/bensheldon/good_job/releases)
- [Changelog](https://github.com/bensheldon/good_job/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bensheldon/good_job/compare/v4.9.0...v4.9.3)

---
updated-dependencies:
- dependency-name: good_job
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-10 09:15:51 -04:00
dependabot[bot]
3f8351abfe
Bump rubocop-rails-omakase from 1.0.0 to 1.1.0 (#1971)
Bumps [rubocop-rails-omakase](https://github.com/rails/rubocop-rails-omakase) from 1.0.0 to 1.1.0.
- [Release notes](https://github.com/rails/rubocop-rails-omakase/releases)
- [Commits](https://github.com/rails/rubocop-rails-omakase/compare/v1.0.0...v1.1.0)

---
updated-dependencies:
- dependency-name: rubocop-rails-omakase
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-10 09:13:38 -04:00
dependabot[bot]
dc44da6c00
Bump redcarpet from 3.6.0 to 3.6.1 (#1972)
Bumps [redcarpet](https://github.com/vmg/redcarpet) from 3.6.0 to 3.6.1.
- [Release notes](https://github.com/vmg/redcarpet/releases)
- [Changelog](https://github.com/vmg/redcarpet/blob/master/CHANGELOG.md)
- [Commits](https://github.com/vmg/redcarpet/compare/v3.6.0...v3.6.1)

---
updated-dependencies:
- dependency-name: redcarpet
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-10 09:13:25 -04:00
dependabot[bot]
2e4180fbf0
Bump turbo-rails from 2.0.11 to 2.0.13 (#1973)
Bumps [turbo-rails](https://github.com/hotwired/turbo-rails) from 2.0.11 to 2.0.13.
- [Release notes](https://github.com/hotwired/turbo-rails/releases)
- [Commits](https://github.com/hotwired/turbo-rails/compare/v2.0.11...v2.0.13)

---
updated-dependencies:
- dependency-name: turbo-rails
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-10 09:13:15 -04:00
dependabot[bot]
4b19ca50eb
Bump i18n-tasks from 1.0.14 to 1.0.15 (#1974)
Bumps [i18n-tasks](https://github.com/glebm/i18n-tasks) from 1.0.14 to 1.0.15.
- [Release notes](https://github.com/glebm/i18n-tasks/releases)
- [Changelog](https://github.com/glebm/i18n-tasks/blob/main/CHANGES.md)
- [Commits](https://github.com/glebm/i18n-tasks/compare/v1.0.14...v1.0.15)

---
updated-dependencies:
- dependency-name: i18n-tasks
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-10 09:13:05 -04:00
Zach Gollwitzer
a3cd5f4f1d
Format money for trade history in holdings drawer (#1961)
Some checks failed
Publish Docker image / ci (push) Has been cancelled
Publish Docker image / Build docker image (push) Has been cancelled
* Format money for trade history in holdings drawer

* Fix broken tests

* Lint fix
2025-03-07 19:09:54 -05:00
Zach Gollwitzer
86bf47a32e Ensure holdings are normalized to account currency 2025-03-07 18:02:08 -05:00
Zach Gollwitzer
5f8a3c9f50 Search securities with correct exchange mic 2025-03-07 17:48:26 -05:00
Zach Gollwitzer
eac5d5e663
Populate holdings for "offline" securities properly (#1958)
* Placeholder logic for missing prices

* Generate holdings properly for "offline" securities

* Separate forward and reverse calculators for holdings and balances

* Remove unnecessary currency conversion during sync

* Clearer sync process

* Move price caching logic to dedicated model

* Base holding calculator

* Base calculator for balances

* Finish balance calculators

* Better naming

* Logs cleanup

* Remove stale data type

* Remove stale test

* Fix price lookup logic for holdings sync

* Fix Plaid item sync regression

* Remove temp logging

* Calculate cash and holdings series

* Add holdings, cash, and balance series dropdown for investments
2025-03-07 17:35:55 -05:00
Nikhil Badyal
26762477a3
Preference to set default_period (#1941)
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
2025-03-07 10:05:54 -05:00
Zach Gollwitzer
372b64ffea Fix Plaid sync error when current balance is null
Some checks failed
Publish Docker image / ci (push) Has been cancelled
Publish Docker image / Build docker image (push) Has been cancelled
2025-03-05 16:02:07 -05:00
Zach Gollwitzer
9627a6bf6f
Add tagged logging to sync process (#1956)
* Add tagged logging to sync process

* Reduce logging in syncer

* Typo
2025-03-05 15:38:31 -05:00
Josh Pigford
cffafd23f0 Logger cleanup 2025-03-05 13:44:56 -06:00
Josh Pigford
f7fa8fa085 Disable turbo on login forms 2025-03-05 13:32:53 -06:00
Josh Pigford
28bfcda50a Temporary additional logging to continue debugging MFA issues
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
2025-03-05 13:20:36 -06:00
Josh Pigford
e49bda4a2e Another attempt at fixing MFA issues 2025-03-05 13:10:53 -06:00
Josh Pigford
071ad52c7f Potential fix for MFA login issues 2025-03-05 13:04:45 -06:00
Zach Gollwitzer
381e39bea8
Fix: Purge stale holdings from accounts during sync (#1954)
* Fix: Purge stale holdings from accounts during sync

* Fix typo

* Prevent Plaid holding deletions
2025-03-05 12:21:17 -05:00
Zach Gollwitzer
eaa1b6abe0
Update issue templates 2025-03-05 11:01:07 -05:00
Zach Gollwitzer
e384369cfb Adjust graph intervals to show more data
Fixes #1948
2025-03-05 10:20:02 -05:00
Zach Gollwitzer
8d0509fda0 Conditionally show commit sha
Fixes #1951
2025-03-05 10:15:12 -05:00
Zach Gollwitzer
d66c37939a
Update bug_report.md
Signed-off-by: Zach Gollwitzer <zach@maybe.co>
2025-03-05 10:07:29 -05:00
Zach Gollwitzer
cf59fe45e7
Fix ticker filling when Synth is connected (#1950) 2025-03-05 09:30:47 -05:00
Zach Gollwitzer
0544089710
Account-level import configuration templates (#1946)
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
* Account-level import configuration templates

* Default import to family's preferred date format
2025-03-04 13:10:01 -05:00
Zach Gollwitzer
5b2fa3d707 Fix commit resolution for Docker builds 2025-03-04 07:50:21 -05:00
Bryan McKnight
cf0e573533
Fix modal closing on color picker drag #1869 (#1931)
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
* Replaced data-action click event with data-action mousedown to prevent the modal from hiding on mouse up whenever mouse down starts within the modal

* Changed click events to mousedown within dialog elements to trigger the closing of the element
2025-03-03 16:37:12 -05:00
Zach Gollwitzer
4e96ca8376 Add manual Docker publishing trigger in GH action workflow 2025-03-03 14:34:56 -05:00
Zach Gollwitzer
c5da8ea550
Allow CSV imports to be configured with single or multi-account mode (#1943)
* Allow CSV imports to be configured to a single account or multiple accounts

* Initialize import directly from accounts page

* Fix brakeman warnings

* Fix schema

* Fix Synth check
2025-03-03 12:47:30 -05:00
Zach Gollwitzer
e907b073ed
Fix time period key conflicts (#1944) 2025-03-03 12:47:20 -05:00
Tony Vincent
4c4a4026c4
fix: Bug - Transcation Matching Dialog isn't Opening (#1942)
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
2025-03-03 11:34:03 -05:00
Zach Gollwitzer
c95bb082a9
Bump to v0.4.3
Some checks failed
Publish Docker image / ci (push) Has been cancelled
Publish Docker image / Build docker image (push) Has been cancelled
Signed-off-by: Zach Gollwitzer <zach@maybe.co>
2025-02-28 15:11:41 -05:00
Zach Gollwitzer
4d0df9b950
Escape quotations in CSV imports properly (#1929)
* Parse quotes in imports

* Update invalid CSV for test
2025-02-28 12:21:07 -05:00
Zach Gollwitzer
7c66f16750
Invert liability graphs to have correct signage (#1928) 2025-02-28 12:17:25 -05:00
Zach Gollwitzer
fa0248056d
Show UI warning to user when they need provider data but have not setup Synth yet (#1926)
* Simplify provider concerns

* Update tests

* Add UI warning for missing Synth key if family requires external data
2025-02-28 11:35:10 -05:00
Tony Vincent
624faa10d0
fix: Don't show Billings on settings navbar when self-hosted (#1912)
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
* Do not show billing settings navbar item when self hosted

* Do not show billing settings navbar item when self hosted

* Add condition to settings helper

* Let Stripe::AuthenticationError bubble up
2025-02-28 09:35:00 -05:00
Zach Gollwitzer
9138bd2b76
Allow offline trade tickers (#1925) 2025-02-28 09:34:14 -05:00
Zach Gollwitzer
882857fcf0
Add transitions to buttons and other common design system elements (#1924) 2025-02-28 09:29:07 -05:00
Zach Gollwitzer
d6793dec05
Fix import configuration form so number format is applied (#1923)
* Fix number format form error when loading import

* Add test to verify import configuration was properly applied
2025-02-28 08:36:57 -05:00
Zach Gollwitzer
e771c8c1df
Fix value wrapping on account balance in sidebar (#1922) 2025-02-28 08:35:14 -05:00
Zach Gollwitzer
58cc09f5ae
Fix bad link in bug template
Signed-off-by: Zach Gollwitzer <zach@maybe.co>
2025-02-28 08:25:20 -05:00
Zach Gollwitzer
98c842d3b8
Add note about self hosted versions prior to opening bugs 2025-02-28 08:23:46 -05:00
Tony Vincent
fae781e1be
Make tags scrollable again (#1921) 2025-02-28 07:53:05 -05:00
Tony Vincent
8208722247
Feat: Data "reset" button (#1913)
* feat: Allow admins to delete family data

* feat: Allow self-hosting users to delete cached data

* Remove system tests
2025-02-28 07:49:12 -05:00
Harshit Chaudhary
f7064fd4dd
fixed example account balance (#1910)
Some checks failed
Publish Docker image / ci (push) Has been cancelled
Publish Docker image / Build docker image (push) Has been cancelled
2025-02-26 15:13:51 -05:00
Zach Gollwitzer
c610b0ba4b
Dashboard design fixes (#1898)
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
* Dashboard design fixes

* Update dashboard greeting

* Remove sidebar toggle from settings breadcrumbs

* Autofocus and outlines for category dropdowns

* Lint fixes
2025-02-25 17:28:40 -05:00
Josh Pigford
a4874815a6
Add breadcrumbs support across application (#1897)
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
* Add breadcrumbs support across application

Fixes #1896

* Potential fix for tests

* Simplify breadcrumbs implementation

Remove complex breadcrumbs logic from controllers and concern, replacing with a simpler default approach that sets a basic breadcrumb based on the current controller name

* Refactor page header and breadcrumbs rendering

Remove complex breadcrumbs helper method and update layout to use more flexible content_for approach for page headers and breadcrumbs

* Add fallback breadcrumbs rendering to settings layout
2025-02-25 10:14:07 -06:00
Josh Pigford
763e222cdd Add Sentry user context to authentication concern 2025-02-25 08:48:26 -06:00
Josh Pigford
e8390a68d8 Reduce Sentry sampling rates for performance monitoring 2025-02-25 08:44:13 -06:00
Josh Pigford
0e76d753bd Replace StackProf with Vernier for performance profiling 2025-02-25 08:37:51 -06:00
Zach Gollwitzer
f5ff5332d5
Fix parent category sums in budget (#1894)
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
2025-02-24 12:51:13 -05:00
Zach Gollwitzer
0dea36ec7d Fix bulk edit drawer height 2025-02-24 12:48:03 -05:00
Syed Bariman Jan
95989a6c9b
Add new category flow (#1857)
* resolve git issue

* Add new category flow

* Improve contrast checker

* make error message small

* update ui to match figma design

* realign color picker

* changes

* rename color picker controller to new category controller

* cleanup code

* cleanup code

* resize and realign icon avatar

* Fix js lint errors

Signed-off-by: Syed Bariman Jan <syedbarimanjan@gmail.com>

---------

Signed-off-by: Syed Bariman Jan <syedbarimanjan@gmail.com>
2025-02-24 11:08:05 -05:00
dependabot[bot]
ac9703031f
Bump plaid from 36.0.0 to 36.1.0 (#1891)
Bumps [plaid](https://github.com/plaid/plaid-ruby) from 36.0.0 to 36.1.0.
- [Release notes](https://github.com/plaid/plaid-ruby/releases)
- [Changelog](https://github.com/plaid/plaid-ruby/blob/master/CHANGELOG.md)
- [Commits](https://github.com/plaid/plaid-ruby/compare/v36.0.0...v36.1.0)

---
updated-dependencies:
- dependency-name: plaid
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-24 10:01:41 -05:00
dependabot[bot]
457e7062bf
Bump selenium-webdriver from 4.28.0 to 4.29.1 (#1892)
Bumps [selenium-webdriver](https://github.com/SeleniumHQ/selenium) from 4.28.0 to 4.29.1.
- [Release notes](https://github.com/SeleniumHQ/selenium/releases)
- [Changelog](https://github.com/SeleniumHQ/selenium/blob/trunk/rb/CHANGES)
- [Commits](https://github.com/SeleniumHQ/selenium/commits)

---
updated-dependencies:
- dependency-name: selenium-webdriver
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-24 10:01:05 -05:00
David Anyatonwu
32ef6ca154
Add exchange and currency fields to trade imports (#1822)
* Add exchange and currency fields to trade imports

* Add exchange_operating_mic support for trade imports - Added required columns and updated models

* refactor: remove exchange and currency columns

* fix: consolidate import schema and remove redundant columns

* feat: Enhance trade import with exchange_operating_mic support

* Revert changes to existing migration

* Simplify migration to use change method

* Restore previously deleted migration

* Remove unused import_col_labels method

* Update schema.rb after running migrations

* Update trade_import.rb and fix schema.rb with db:migrate:reset

* fix: improve trade import security creation

---------

Signed-off-by: David Anyatonwu <51977119+onyedikachi-david@users.noreply.github.com>
2025-02-24 10:00:24 -05:00
Zach Gollwitzer
fd95f8d2bd
Drop linux/arm/v7 support
Some checks failed
Publish Docker image / ci (push) Has been cancelled
Publish Docker image / Build docker image (push) Has been cancelled
linux/arm/v7 is not compatible with the latest Tailwind package + takes a significant amount of time when using buildx.  Most Raspberry Pi OS now run on 64 bit ARM architecture, so we should still have significant coverage with linux/amd + linux/arm builds.

Signed-off-by: Zach Gollwitzer <zach@maybe.co>
2025-02-21 17:12:06 -05:00
1192 changed files with 45603 additions and 13377 deletions

View file

@ -0,0 +1,53 @@
---
description: Guidelines for creating and maintaining Cursor rules to ensure consistency and effectiveness.
globs: .cursor/rules/*.mdc
alwaysApply: true
---
- **Required Rule Structure:**
```markdown
---
description: Clear, one-line description of what the rule enforces
globs: path/to/files/*.ext, other/path/**/*
alwaysApply: boolean
---
- **Main Points in Bold**
- Sub-points with details
- Examples and explanations
```
- **File References:**
- Use `[filename](mdc:path/to/file)` ([filename](mdc:filename)) to reference files
- Example: [prisma.mdc](mdc:.cursor/rules/prisma.mdc) for rule references
- Example: [schema.prisma](mdc:prisma/schema.prisma) for code references
- **Code Examples:**
- Use language-specific code blocks
```typescript
// ✅ DO: Show good examples
const goodExample = true;
// ❌ DON'T: Show anti-patterns
const badExample = false;
```
- **Rule Content Guidelines:**
- Start with high-level overview
- Include specific, actionable requirements
- Show examples of correct implementation
- Reference existing code when possible
- Keep rules DRY by referencing other rules
- **Rule Maintenance:**
- Update rules when new patterns emerge
- Add examples from actual codebase
- Remove outdated patterns
- Cross-reference related rules
- **Best Practices:**
- Use bullet points for clarity
- Keep descriptions concise
- Include both DO and DON'T examples
- Reference actual code over theoretical examples
- Use consistent formatting across rules

View file

@ -0,0 +1,21 @@
---
description: Miscellaneous rules to get the AI to behave
globs: *
alwaysApply: true
---
# General rules for AI
- Use `Current.user` for the current user. Do NOT use `current_user`.
- Use `Current.family` for the current family. Do NOT use `current_family`.
- Prior to generating any code, carefully read the project conventions and guidelines
- Read [project-design.mdc](mdc:.cursor/rules/project-design.mdc) to understand the codebase
- Read [project-conventions.mdc](mdc:.cursor/rules/project-conventions.mdc) to understand _how_ to write code for the codebase
- Read [ui-ux-design-guidelines.mdc](mdc:.cursor/rules/ui-ux-design-guidelines.mdc) to understand how to implement frontend code specifically
- Ignore i18n methods and files. Hardcode strings in English for now to optimize speed of development.
## Prohibited actions
- Do not run `rails server` in your responses.
- Do not run `touch tmp/restart.txt`
- Do not run `rails credentials`
- Do not automatically run migrations

View file

@ -1,14 +1,9 @@
---
description: This rule explains the project's tech stack and code conventions
globs: *
description:
globs:
alwaysApply: true
---
This rule serves as high-level documentation for how the Maybe codebase is structured.
## Rules for AI
- Use this file to understand how the codebase works
- Treat this rule/file as your "source of truth" when making code recommendations
- When creating migrations, always use `rails g migration` instead of creating the file yourself
This rule serves as high-level documentation for how you should write code for the Maybe codebase.
## Project Tech Stack
@ -18,8 +13,9 @@ This rule serves as high-level documentation for how the Maybe codebase is struc
- Hotwire Turbo/Stimulus for SPA-like UI/UX
- TailwindCSS for styles
- Lucide Icons for icons
- OpenAI for AI chat
- Database: PostgreSQL
- Jobs: GoodJob
- Jobs: Sidekiq + Redis
- External
- Payments: Stripe
- User bank data syncing: Plaid
@ -46,39 +42,32 @@ This codebase adopts a "skinny controller, fat models" convention. Furthermore,
- When concerns are used for code organization, they should be organized around the "traits" of a model; not for simply moving code to another spot in the codebase.
- When possible, models should answer questions about themselves—for example, we might have a method, `account.balance_series` that returns a time-series of the account's most recent balances. We prefer this over something more service-like such as `AccountSeries.new(account).call`.
### Convention 3: Prefer server-side solutions over client-side solutions
### Convention 3: Leverage Hotwire, write semantic HTML, CSS, and JS, prefer server-side solutions
- When possible, leverage Turbo frames over complex, JS-driven client-side solutions
- When writing a client-side solution, use Stimulus controllers and keep it simple!
- Especially when dealing with money and currencies, calculate + format server-side and then pass that to the client to display
- Keep client-side code for where it truly shines. For example, [bulk_select_controller.js](mdc:app/javascript/controllers/bulk_select_controller.js) is a case where server-side solutions would degrade the user experience significantly. When bulk-selecting entries, client-side solutions are the way to go and Stimulus provides the right toolset to achieve this.
### Convention 4: Sacrifice performance, optimize for simplicitly and clarity
This codebase is still young. We are still rapidly iterating on domain designs and features. Because of this, code should be optimized for simplicitly and clarity over performance.
- Focus on good OOP design first, performance second
- Be mindful of large performance bottlenecks, but don't sweat the small stuff
### Convention 5: Prefer semantic, native HTML features
The HTML spec has improved tremendously over the years and offers a ton of functionality out of the box. We prefer semantic, native HTML solutions over JS-based ones. A few examples of this include:
- Using the `dialog` element for modals
- Using `summary` / `details` elements for disclosures (or `popover` attribute)
- Native HTML is always preferred over JS-based components
- Example 1: Use `<dialog>` element for modals instead of creating a custom component
- Example 2: Use `<details><summary>...</summary></details>` for disclosures rather than custom components
- Leverage Turbo frames to break up the page over JS-driven client-side solutions
- Example 1: A good example of turbo frame usage is in [application.html.erb](mdc:app/views/layouts/application.html.erb) where we load [chats_controller.rb](mdc:app/controllers/chats_controller.rb) actions in a turbo frame in the global layout
- Leverage query params in the URL for state over local storage and sessions. If absolutely necessary, utilize the DB for persistent state.
- Use Turbo streams to enhance functionality, but do not solely depend on it
- Format currencies, numbers, dates, and other values server-side, then pass to Stimulus controllers for display only
- Keep client-side code for where it truly shines. For example, @bulk_select_controller.js is a case where server-side solutions would degrade the user experience significantly. When bulk-selecting entries, client-side solutions are the way to go and Stimulus provides the right toolset to achieve this.
- Always use the `icon` helper in [application_helper.rb](mdc:app/helpers/application_helper.rb) for icons. NEVER use `lucide_icon` helper directly.
The Hotwire suite (Turbo/Stimulus) works very well with these native elements and we optimize for this.
### Convention 6: Use Minitest + Fixtures for testing, minimize fixtures
### Convention 4: Optimize for simplicitly and clarity
Due to the open-source nature of this project, we have chosen Minitest + Fixtures for testing to maximize familiarity and predictability.
All code should maximize readability and simplicity.
- Always use Minitest and fixtures for testing.
- Keep fixtures to a minimum. Most models should have 2-3 fixtures maximum that represent the "base cases" for that model. "Edge cases" should be created on the fly, within the context of the test which it is needed.
- For tests that require a large number of fixture records to be created, use Rails helpers such as [entries_test_helper.rb](mdc:test/support/account/entries_test_helper.rb) to act as a "factory" for creating these. For a great example of this, check out [balance_calculator_test.rb](mdc:test/models/account/balance_calculator_test.rb)
- Prioritize good OOP domain design over performance
- Only focus on performance for critical and global areas of the codebase; otherwise, don't sweat the small stuff.
- Example 1: be mindful of loading large data payloads in global layouts
- Example 2: Avoid N+1 queries
### Convention 7: Use ActiveRecord for complex validations, DB for simple ones, keep business logic out of DB
### Convention 5: Use ActiveRecord for complex validations, DB for simple ones, keep business logic out of DB
- Enforce `null` checks, unique indexes, and other simple validations in the DB
- ActiveRecord validations _may_ mirror the DB level ones, but not 100% necessary. These are for convenience when error handling in forms. Always prefer client-side form validation when possible.
- Complex validations and business logic should remain in ActiveRecord
- Complex validations and business logic should remain in ActiveRecord

View file

@ -1,6 +1,7 @@
---
description: This rule explains the system architecture and data flow of the Rails app
globs: *
alwaysApply: true
---
This file outlines how the codebase is structured and how data flows through the app.
@ -54,29 +55,29 @@ All balances are calculated daily by [balance_calculator.rb](mdc:app/models/acco
### Account Holdings
An account [holding.rb](mdc:app/models/account/holding.rb) applies to [investment.rb](mdc:app/models/investment.rb) type accounts and represents a `qty` of a certain [security.rb](mdc:app/models/security.rb) at a specific `price` on a specific `date`.
An account [holding.rb](mdc:app/models/holding.rb) applies to [investment.rb](mdc:app/models/investment.rb) type accounts and represents a `qty` of a certain [security.rb](mdc:app/models/security.rb) at a specific `price` on a specific `date`.
For investment accounts with holdings, [holding_calculator.rb](mdc:app/models/account/holding_calculator.rb) is used to calculate the daily historical holding quantities and prices, which are then rolled up into a final "Balance" for the account in [balance_calculator.rb](mdc:app/models/account/balance_calculator.rb).
For investment accounts with holdings, [base_calculator.rb](mdc:app/models/holding/base_calculator.rb) is used to calculate the daily historical holding quantities and prices, which are then rolled up into a final "Balance" for the account in [base_calculator.rb](mdc:app/models/account/balance/base_calculator.rb).
### Account Entries
An account [entry.rb](mdc:app/models/account/entry.rb) is also a Rails "delegated type". `Account::Entry` represents any record that _modifies_ an `Account` [balance.rb](mdc:app/models/account/balance.rb) and/or [holding.rb](mdc:app/models/account/holding.rb). Therefore, every entry must have a `date`, `amount`, and `currency`.
An account [entry.rb](mdc:app/models/entry.rb) is also a Rails "delegated type". `Entry` represents any record that _modifies_ an `Account` [balance.rb](mdc:app/models/account/balance.rb) and/or [holding.rb](mdc:app/models/holding.rb). Therefore, every entry must have a `date`, `amount`, and `currency`.
The `amount` of an [entry.rb](mdc:app/models/account/entry.rb) is a signed value. A _negative_ amount is an "inflow" of money to that account. A _positive_ value is an "outflow" of money from that account. For example:
The `amount` of an [entry.rb](mdc:app/models/entry.rb) is a signed value. A _negative_ amount is an "inflow" of money to that account. A _positive_ value is an "outflow" of money from that account. For example:
- A negative amount for a credit card account represents a "payment" to that account, which _reduces_ its balance (since it is a `liability`)
- A negative amount for a checking account represents an "income" to that account, which _increases_ its balance (since it is an `asset`)
- A negative amount for an investment/brokerage trade represents a "sell" transaction, which _increases_ the cash balance of the account
There are 3 entry types, defined as [entryable.rb](mdc:app/models/account/entryable.rb) records:
There are 3 entry types, defined as [entryable.rb](mdc:app/models/entryable.rb) records:
- `Account::Valuation` - an account [valuation.rb](mdc:app/models/account/valuation.rb) is an entry that says, "here is the value of this account on this date". It is an absolute measure of an account value / debt. If there is an `Account::Valuation` of 5,000 for today's date, that means that the account balance will be 5,000 today.
- `Account::Transaction` - an account [transaction.rb](mdc:app/models/account/transaction.rb) is an entry that alters the account balance by the `amount`. This is the most common type of entry and can be thought of as an "income" or "expense".
- `Account::Trade` - an account [trade.rb](mdc:app/models/account/trade.rb) is an entry that only applies to an investment account. This represents a "buy" or "sell" of a holding and has a `qty` and `price`.
- `Valuation` - an account [valuation.rb](mdc:app/models/valuation.rb) is an entry that says, "here is the value of this account on this date". It is an absolute measure of an account value / debt. If there is an `Valuation` of 5,000 for today's date, that means that the account balance will be 5,000 today.
- `Transaction` - an account [transaction.rb](mdc:app/models/transaction.rb) is an entry that alters the account balance by the `amount`. This is the most common type of entry and can be thought of as an "income" or "expense".
- `Trade` - an account [trade.rb](mdc:app/models/trade.rb) is an entry that only applies to an investment account. This represents a "buy" or "sell" of a holding and has a `qty` and `price`.
### Account Transfers
A [transfer.rb](mdc:app/models/transfer.rb) represents a movement of money between two accounts. A transfer has an inflow [transaction.rb](mdc:app/models/account/transaction.rb) and an outflow [transaction.rb](mdc:app/models/account/transaction.rb). The Maybe system auto-matches transfers based on the following criteria:
A [transfer.rb](mdc:app/models/transfer.rb) represents a movement of money between two accounts. A transfer has an inflow [transaction.rb](mdc:app/models/transaction.rb) and an outflow [transaction.rb](mdc:app/models/transaction.rb). The Maybe system auto-matches transfers based on the following criteria:
- Must be from different accounts
- Must be within 4 days of each other
@ -110,14 +111,14 @@ Below are brief descriptions of each type of sync in more detail.
### Account Syncs
The most important type of sync is the account sync. It is orchestrated by the account [syncer.rb](mdc:app/models/account/syncer.rb), and performs a few important tasks:
The most important type of sync is the account sync. It is orchestrated by the account's `sync_data` method, which performs a few important tasks:
- Auto-matches transfer records for the account
- Calculates holdings and balances for the account
- Enriches transaction data
- Converts account balances that are not in the family's preferred currency to the preferred currency
- Calculates daily [balance.rb](mdc:app/models/account/balance.rb) records for the account from `account.start_date` to `Date.current` using [base_calculator.rb](mdc:app/models/account/balance/base_calculator.rb)
- Balances are dependent on the calculation of [holding.rb](mdc:app/models/holding.rb), which uses [base_calculator.rb](mdc:app/models/account/holding/base_calculator.rb)
- Enriches transaction data if enabled by user
An account sync happens every time an [entry.rb](mdc:app/models/account/entry.rb) is updated.
An account sync happens every time an [entry.rb](mdc:app/models/entry.rb) is updated.
### Plaid Item Syncs
@ -125,10 +126,124 @@ A Plaid Item sync is an ETL (extract, transform, load) operation:
1. [plaid_item.rb](mdc:app/models/plaid_item.rb) fetches data from the external Plaid API
2. [plaid_item.rb](mdc:app/models/plaid_item.rb) creates and loads this data to [plaid_account.rb](mdc:app/models/plaid_account.rb) records
3. [plaid_item.rb](mdc:app/models/plaid_item.rb) and [plaid_account.rb](mdc:app/models/plaid_account.rb) transform and load data to [account.rb](mdc:app/models/account.rb) and [entry.rb](mdc:app/models/account/entry.rb), the internal Maybe representations of the data.
3. [plaid_item.rb](mdc:app/models/plaid_item.rb) and [plaid_account.rb](mdc:app/models/plaid_account.rb) transform and load data to [account.rb](mdc:app/models/account.rb) and [entry.rb](mdc:app/models/entry.rb), the internal Maybe representations of the data.
### Family Syncs
A family sync happens once daily via [auto_sync.rb](mdc:app/controllers/concerns/auto_sync.rb). A family sync is an "orchestrator" of Account and Plaid Item syncs.
## Data Providers
The Maybe app utilizes several 3rd party data services to calculate historical account balances, enrich data, and more. Since the app can be run in both "hosted" and "self hosted" mode, this means that data providers are _optional_ for self hosted users and must be configured.
Because of this optionality, data providers must be configured at _runtime_ through [registry.rb](mdc:app/models/provider/registry.rb) utilizing [setting.rb](mdc:app/models/setting.rb) for runtime parameters like API keys:
There are two types of 3rd party data in the Maybe app:
1. "Concept" data
2. One-off data
### "Concept" data
Since the app is self hostable, users may prefer using different providers for generic data like exchange rates and security prices. When data is generic enough where we can easily swap out different providers, we call it a data "concept".
Each "concept" has an interface defined in the `app/models/provider/concepts` directory.
```
app/models/
exchange_rate/
provided.rb # <- Responsible for selecting the concept provider from the registry
provider.rb # <- Base provider class
provider/
registry.rb <- Defines available providers by concept
concepts/
exchange_rate.rb <- defines the interface required for the exchange rate concept
synth.rb # <- Concrete provider implementation
```
### One-off data
For data that does not fit neatly into a "concept", an interface is not required and the concrete provider may implement ad-hoc methods called directly in code. For example, the [synth.rb](mdc:app/models/provider/synth.rb) provider has a `usage` method that is only applicable to this specific provider. This should be called directly without any abstractions:
```rb
class SomeModel < Application
def synth_usage
Provider::Registry.get_provider(:synth)&.usage
end
end
```
## "Provided" Concerns
In general, domain models should not be calling [registry.rb](mdc:app/models/provider/registry.rb) directly. When 3rd party data is required for a domain model, we use the `Provided` concern within that model's namespace. This concern is primarily responsible for:
- Choosing the provider to use for this "concept"
- Providing convenience methods on the model for accessing data
For example, [exchange_rate.rb](mdc:app/models/exchange_rate.rb) has a [provided.rb](mdc:app/models/exchange_rate/provided.rb) concern with the following convenience methods:
```rb
module ExchangeRate::Provided
extend ActiveSupport::Concern
class_methods do
def provider
registry = Provider::Registry.for_concept(:exchange_rates)
registry.get_provider(:synth)
end
def find_or_fetch_rate(from:, to:, date: Date.current, cache: true)
# Implementation
end
def sync_provider_rates(from:, to:, start_date:, end_date: Date.current)
# Implementation
end
end
end
```
This exposes a generic access pattern where the caller does not care _which_ provider has been chosen for the concept of exchange rates and can get a predictable response:
```rb
def access_patterns_example
# Call exchange rate provider directly
ExchangeRate.provider.fetch_exchange_rate(from: "USD", to: "CAD", date: Date.current)
# Call convenience method
ExchangeRate.sync_provider_rates(from: "USD", to: "CAD", start_date: 2.days.ago.to_date)
end
```
## Concrete provider implementations
Each 3rd party data provider should have a class under the `Provider::` namespace that inherits from `Provider` and returns `with_provider_response`, which will return a `Provider::ProviderResponse` object:
```rb
class ConcreteProvider < Provider
def fetch_some_data
with_provider_response do
ExampleData.new(
example: "data"
)
end
end
end
```
The `with_provider_response` automatically catches provider errors, so concrete provider classes should raise when valid data is not possible:
```rb
class ConcreteProvider < Provider
def fetch_some_data
with_provider_response do
data = nil
# Raise an error if data cannot be returned
raise ProviderError.new("Could not find the data you need") if data.nil?
data
end
end
end
```

View file

@ -0,0 +1,72 @@
---
description: Guidelines for continuously improving Cursor rules based on emerging code patterns and best practices.
globs: **/*
alwaysApply: true
---
- **Rule Improvement Triggers:**
- New code patterns not covered by existing rules
- Repeated similar implementations across files
- Common error patterns that could be prevented
- New libraries or tools being used consistently
- Emerging best practices in the codebase
- **Analysis Process:**
- Compare new code with existing rules
- Identify patterns that should be standardized
- Look for references to external documentation
- Check for consistent error handling patterns
- Monitor test patterns and coverage
- **Rule Updates:**
- **Add New Rules When:**
- A new technology/pattern is used in 3+ files
- Common bugs could be prevented by a rule
- Code reviews repeatedly mention the same feedback
- New security or performance patterns emerge
- **Modify Existing Rules When:**
- Better examples exist in the codebase
- Additional edge cases are discovered
- Related rules have been updated
- Implementation details have changed
- **Example Pattern Recognition:**
```typescript
// If you see repeated patterns like:
const data = await prisma.user.findMany({
select: { id: true, email: true },
where: { status: 'ACTIVE' }
});
// Consider adding to [prisma.mdc](mdc:.cursor/rules/prisma.mdc):
// - Standard select fields
// - Common where conditions
// - Performance optimization patterns
```
- **Rule Quality Checks:**
- Rules should be actionable and specific
- Examples should come from actual code
- References should be up to date
- Patterns should be consistently enforced
- **Continuous Improvement:**
- Monitor code review comments
- Track common development questions
- Update rules after major refactors
- Add links to relevant documentation
- Cross-reference related rules
- **Rule Deprecation:**
- Mark outdated patterns as deprecated
- Remove rules that no longer apply
- Update references to deprecated rules
- Document migration paths for old patterns
- **Documentation Updates:**
- Keep examples synchronized with code
- Update references to external docs
- Maintain links between related rules
- Document breaking changes
Follow [cursor_rules.mdc](mdc:.cursor/rules/cursor_rules.mdc) for proper rule formatting and structure.

View file

@ -0,0 +1,64 @@
---
description:
globs:
alwaysApply: false
---
This rule describes how to write Stimulus controllers.
- **Use declarative actions, not imperative event listeners**
- Instead of assigning a Stimulus target and binding it to an event listener in the initializer, always write Controllers + ERB views declaratively by using Stimulus actions in ERB to call methods in the Stimulus JS controller. Below are good vs. bad code.
BAD code:
```js
// BAD!!!! DO NOT DO THIS!!
// Imperative - controller does all the work
export default class extends Controller {
static targets = ["button", "content"]
connect() {
this.buttonTarget.addEventListener("click", this.toggle.bind(this))
}
toggle() {
this.contentTarget.classList.toggle("hidden")
this.buttonTarget.textContent = this.contentTarget.classList.contains("hidden") ? "Show" : "Hide"
}
}
```
GOOD code:
```erb
<!-- Declarative - HTML declares what happens -->
<div data-controller="toggle">
<button data-action="click->toggle#toggle" data-toggle-target="button">Show</button>
<div data-toggle-target="content" class="hidden">Hello World!</div>
</div>
```
```js
// Declarative - controller just responds
export default class extends Controller {
static targets = ["button", "content"]
toggle() {
this.contentTarget.classList.toggle("hidden")
this.buttonTarget.textContent = this.contentTarget.classList.contains("hidden") ? "Show" : "Hide"
}
}
```
- **Keep Stimulus controllers lightweight and simple**
- Always aim for less than 7 controller targets. Any more is a sign of too much complexity.
- Use private methods and expose a clear public API
- **Keep Stimulus controllers focused on what they do best**
- Domain logic does NOT belong in a Stimulus controller
- Stimulus controllers should aim for a single responsibility, or a group of highly related responsibilities
- Make good use of Stimulus's callbacks, actions, targets, values, and classes
- **Component controllers should not be used outside the component**
- If a Stimulus controller is in the app/components directory, it should only be used in its component view. It should not be used anywhere in app/views.

87
.cursor/rules/testing.mdc Normal file
View file

@ -0,0 +1,87 @@
---
description:
globs: test/**
alwaysApply: false
---
Use this rule to learn how to write tests for the Maybe codebase.
Due to the open-source nature of this project, we have chosen Minitest + Fixtures for testing to maximize familiarity and predictability.
- **General testing rules**
- Always use Minitest and fixtures for testing, NEVER rspec or factories
- Keep fixtures to a minimum. Most models should have 2-3 fixtures maximum that represent the "base cases" for that model. "Edge cases" should be created on the fly, within the context of the test which it is needed.
- For tests that require a large number of fixture records to be created, use Rails helpers to help create the records needed for the test, then inline the creation. For example, [entries_test_helper.rb](mdc:test/support/entries_test_helper.rb) provides helpers to easily do this.
- **Write minimal, effective tests**
- Use system tests sparingly as they increase the time to complete the test suite
- Only write tests for critical and important code paths
- Write tests as you go, when required
- Take a practical approach to testing. Tests are effective when their presence _significantly increases confidence in the codebase_.
Below are examples of necessary vs. unnecessary tests:
```rb
# GOOD!!
# Necessary test - in this case, we're testing critical domain business logic
test "syncs balances" do
Holding::Syncer.any_instance.expects(:sync_holdings).returns([]).once
@account.expects(:start_date).returns(2.days.ago.to_date)
Balance::ForwardCalculator.any_instance.expects(:calculate).returns(
[
Balance.new(date: 1.day.ago.to_date, balance: 1000, cash_balance: 1000, currency: "USD"),
Balance.new(date: Date.current, balance: 1000, cash_balance: 1000, currency: "USD")
]
)
assert_difference "@account.balances.count", 2 do
Balance::Syncer.new(@account, strategy: :forward).sync_balances
end
end
# BAD!!
# Unnecessary test - in this case, this is simply testing ActiveRecord's functionality
test "saves balance" do
balance_record = Balance.new(balance: 100, currency: "USD")
assert balance_record.save
end
```
- **Test boundaries correctly**
- Distinguish between commands and query methods. Test output of query methods; test that commands were called with the correct params. See an example below:
```rb
class ExampleClass
def do_something
result = 2 + 2
CustomEventProcessor.process_result(result)
result
end
end
class ExampleClass < ActiveSupport::TestCase
test "boundaries are tested correctly" do
result = ExampleClass.new.do_something
# GOOD - we're only testing that the command was received, not internal implementation details
# The actual tests for CustomEventProcessor belong in a different test suite!
CustomEventProcessor.expects(:process_result).with(4).once
# GOOD - we're testing the implementation of ExampleClass inside its own test suite
assert_equal 4, result
end
end
```
- Never test the implementation details of one class in another classes test suite
- **Stubs and mocks**
- Use `mocha` gem
- Always prefer `OpenStruct` when creating mock instances, or in complex cases, a mock class
- Only mock what's necessary. If you're not testing return values, don't mock a return value.

View file

@ -1,13 +1,22 @@
---
description: This file describes Maybe's design system and how views should be styled
globs: app/views/**,app/helpers/**,app/javascript/controllers/**
alwaysApply: true
---
Use this rule whenever you are writing html, css, or even styles in Stimulus controllers that use D3.js.
Use the rules below when:
- You are writing HTML
- You are writing CSS
- You are writing styles in a JavaScript Stimulus controller
## Rules for AI (mandatory)
The codebase uses TailwindCSS v4.x (the newest version) with a custom design system defined in [maybe-design-system.css](mdc:app/assets/tailwind/maybe-design-system.css)
- Always start by referencing [maybe-design-system.css](mdc:app/assets/tailwind/maybe-design-system.css) to see the base primitives and tokens we use in the codebase
- Always generate semantic HTML
- Always start by referencing [maybe-design-system.css](mdc:app/assets/tailwind/maybe-design-system.css) to see the base primitives, functional tokens, and component tokens we use in the codebase
- Always prefer using the functional "tokens" defined in @maybe-design-system.css when possible.
- Example 1: use `text-primary` rather than `text-white`
- Example 2: use `bg-container` rather than `bg-white`
- Example 3: use `border border-primary` rather than `border border-gray-200`
- Never create new styles in [maybe-design-system.css](mdc:app/assets/tailwind/maybe-design-system.css) or [application.css](mdc:app/assets/tailwind/application.css) without explicitly receiving permission to do so
- Always favor the "utility first" Tailwind approach. Reusable style classes should not be created often. Code should be reused primarily through ERB partials.
- Always prefer using the utility "tokens" defined in [maybe-design-system.css](mdc:app/assets/tailwind/maybe-design-system.css) when possible. For example, use `text-primary` rather than `text-gray-900`.
- Always generate semantic HTML

View file

@ -0,0 +1,100 @@
---
description:
globs: app/views/**,app/javascript/**,app/components/**/*.js
alwaysApply: false
---
Use this rule to learn how to write ERB views, partials, and Stimulus controllers should be incorporated into them.
- **Component vs. Partial Decision Making**
- **Use ViewComponents when:**
- Element has complex logic or styling patterns
- Element will be reused across multiple views/contexts
- Element needs structured styling with variants/sizes (like buttons, badges)
- Element requires interactive behavior or Stimulus controllers
- Element has configurable slots or complex APIs
- Element needs accessibility features or ARIA support
- **Use Partials when:**
- Element is primarily static HTML with minimal logic
- Element is used in only one or few specific contexts
- Element is simple template content (like CTAs, static sections)
- Element doesn't need variants, sizes, or complex configuration
- Element is more about content organization than reusable functionality
- **Prefer components over partials**
- If there is a component available for the use case in app/components, use it
- If there is no component, look for a partial
- If there is no partial, decide between component or partial based on the criteria above
- **Examples of Component vs. Partial Usage**
```erb
<%# Component: Complex, reusable with variants and interactivity %>
<%= render DialogComponent.new(variant: :drawer) do |dialog| %>
<% dialog.with_header(title: "Account Settings") %>
<% dialog.with_body { "Dialog content here" } %>
<% end %>
<%# Component: Interactive with complex styling options %>
<%= render ButtonComponent.new(text: "Save Changes", variant: "primary", confirm: "Are you sure?") %>
<%# Component: Reusable with variants %>
<%= render FilledIconComponent.new(icon: "credit-card", variant: :surface) %>
<%# Partial: Static template content %>
<%= render "shared/logo" %>
<%# Partial: Simple, context-specific content with basic styling %>
<%= render "shared/trend_change", trend: @account.trend, comparison_label: "vs last month" %>
<%# Partial: Simple divider/utility %>
<%= render "shared/ruler", classes: "my-4" %>
<%# Partial: Simple form utility %>
<%= render "shared/form_errors", model: @account %>
```
- **Keep domain logic out of the views**
```erb
<%# BAD!!! %>
<%# This belongs in the component file, not the template file! %>
<% button_classes = { class: "bg-blue-500 hover:bg-blue-600" } %>
<%= tag.button class: button_classes do %>
Save Account
<% end %>
<%# GOOD! %>
<%= tag.button class: computed_button_classes do %>
Save Account
<% end %>
```
- **Stimulus Integration in Views**
- Always use the **declarative approach** when integrating Stimulus controllers
- The ERB template should declare what happens, the Stimulus controller should respond
- Refer to [stimulus_conventions.mdc](mdc:.cursor/rules/stimulus_conventions.mdc) to learn how to incorporate them into
GOOD Stimulus controller integration into views:
```erb
<!-- Declarative - HTML declares what happens -->
<div data-controller="toggle">
<button data-action="click->toggle#toggle" data-toggle-target="button">Show</button>
<div data-toggle-target="content" class="hidden">Hello World!</div>
</div>
```
- **Stimulus Controller Placement Guidelines**
- **Component controllers** (in `app/components/`) should only be used within their component templates
- **Global controllers** (in `app/javascript/controllers/`) can be used across any view
- Pass data from Rails to Stimulus using `data-*-value` attributes, not inline JavaScript
- Use Stimulus targets to reference DOM elements, not manual `getElementById` calls
- **Naming Conventions**
- **Components**: Use `ComponentName` suffix (e.g., `ButtonComponent`, `DialogComponent`, `FilledIconComponent`)
- **Partials**: Use underscore prefix (e.g., `_trend_change.html.erb`, `_form_errors.html.erb`, `_sync_indicator.html.erb`)
- **Shared partials**: Place in `app/views/shared/` directory for reusable content
- **Context-specific partials**: Place in relevant controller view directory (e.g., `accounts/_account_sidebar_tabs.html.erb`)

View file

@ -1,4 +1,4 @@
ARG RUBY_VERSION=3.4.1
ARG RUBY_VERSION=3.4.4
FROM ruby:${RUBY_VERSION}-slim-bullseye
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
@ -10,6 +10,8 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
imagemagick \
iproute2 \
libpq-dev \
libyaml-dev \
libyaml-0-2 \
openssh-client \
postgresql-client \
vim

View file

@ -1,4 +1,15 @@
version: "3"
x-db-env: &db_env
POSTGRES_USER: postgres
POSTGRES_DB: postgres
POSTGRES_PASSWORD: postgres
x-rails-env: &rails_env
DB_HOST: db
HOST: "0.0.0.0"
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
BUNDLE_PATH: /bundle
REDIS_URL: redis://redis:6379/1
services:
app:
@ -16,32 +27,41 @@ services:
command: sleep infinity
environment:
DB_HOST: db
HOST: "0.0.0.0"
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
BUNDLE_PATH: /bundle
<<: *rails_env
depends_on:
- db
- redis
worker:
build:
context: ..
dockerfile: .devcontainer/Dockerfile
command: bundle exec sidekiq
restart: unless-stopped
environment:
<<: *rails_env
depends_on:
- redis
redis:
image: redis:latest
ports:
- "6379:6379"
restart: unless-stopped
volumes:
- redis-data:/data
db:
image: postgres:latest
restart: unless-stopped
volumes:
- postgres-data:/var/lib/postgresql/data
environment:
POSTGRES_USER: postgres
POSTGRES_DB: postgres
POSTGRES_PASSWORD: postgres
<<: *db_env
ports:
- "5432:5432"
volumes:
postgres-data:
redis-data:
bundle_cache:

View file

@ -1,20 +1,31 @@
# ================================ PLEASE READ ==========================================
# This file outlines all the possible environment variables supported by the Maybe app.
#
# This includes several features that are for our "hosted" version of Maybe, which most
# open-source contributors won't need.
# ================================ PLEASE READ ===========================================================
# This file outlines all the possible environment variables supported by the Maybe app for self hosting.
#
# If you are developing locally, you should be referencing `.env.local.example` instead.
# =======================================================================================
# If you're a developer setting up your local environment, please use `.env.local.example` instead.
# ========================================================================================================
# Required self-hosting vars
# --------------------------------------------------------------------------------------------------------
# Enables self hosting features (should be set to true unless you know what you're doing)
SELF_HOSTED=true
# Secret key used to encrypt credentials (https://api.rubyonrails.org/v7.1.3.2/classes/Rails/Application.html#method-i-secret_key_base)
# Has to be a random string, generated eg. by running `openssl rand -hex 64`
SECRET_KEY_BASE=secret-value
# Optional self-hosting vars
# --------------------------------------------------------------------------------------------------------
# Optional: Synth API Key for exchange rates + stock prices
# (you can also set this in your self-hosted settings page)
# Get it here: https://synthfinance.com/
SYNTH_API_KEY=
# Custom port config
# For users who have other applications listening at 3000, this allows them to set a value puma will listen to.
PORT=3000
# Exchange Rate & Stock Pricing API
# This is used to convert between different currencies in the app. In addition, it fetches global stock prices. We use Synth, which is a Maybe product. You can sign up for a free account at synthfinance.com.
SYNTH_API_KEY=
# SMTP Configuration
# This is only needed if you intend on sending emails from your Maybe instance (such as for password resets or email financial reports).
# Resend.com is a good option that offers a free tier for sending emails.
@ -37,60 +48,28 @@ POSTGRES_USER=postgres
# This is the domain that your Maybe instance will be hosted at. It is used to generate links in emails and other places.
APP_DOMAIN=
## Error and Performance Monitoring
# The app uses Sentry to monitor errors and performance. In reality, you likely don't need this unless you're deploying Maybe to many users.
SENTRY_DSN=
# If enabled, an invite code generated by `rake invites:create` is required to sign up as a new user.
# This is useful for controlling who can sign up for your Maybe instance.
REQUIRE_INVITE_CODE=false
# Enables self hosting features (should be set to true for most folks)
SELF_HOSTED=true
# The hosting platform used to deploy the app (e.g. "render")
# `localhost` (or unset) is used for local development and testing
HOSTING_PLATFORM=localhost
# Secret key used to encrypt credentials (https://api.rubyonrails.org/v7.1.3.2/classes/Rails/Application.html#method-i-secret_key_base)
# Has to be a random string, generated eg. by running `openssl rand -hex 64`
SECRET_KEY_BASE=secret-value
# Disable enforcing SSL connections
# DISABLE_SSL=true
# ======================================================================================================
# Upgrades Module - responsible for triggering upgrade alerts, prompts, and auto-upgrade functionality
# ======================================================================================================
#
# UPGRADES_ENABLED: Enables Upgrader class functionality.
# UPGRADES_MODE: Controls how the app will upgrade. `manual` means the user must manually upgrade the app. `auto` means the app will upgrade automatically (great for self-hosting)
# UPGRADES_TARGET: Controls what the app will upgrade to. `release` means the app will upgrade to the latest release. `commit` means the app will upgrade to the latest commit.
#
UPGRADES_ENABLED=false # unless editing the flow, you should keep this `false` locally in development
UPGRADES_MODE=manual # `manual` or `auto`
UPGRADES_TARGET=release # `release` or `commit`
# ======================================================================================================
# Git Repository Module - responsible for fetching latest commit data for upgrades
# ======================================================================================================
#
GITHUB_REPO_OWNER=maybe-finance
GITHUB_REPO_NAME=maybe
GITHUB_REPO_BRANCH=main
# Active Record Encryption Keys (Optional)
# These keys are used to encrypt sensitive data like API keys in the database.
# If not provided, they will be automatically generated based on your SECRET_KEY_BASE.
# You can generate your own keys by running: rails db:encryption:init
# ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=
# ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=
# ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=
# ======================================================================================================
# Active Storage Configuration - responsible for storing file uploads
# ======================================================================================================
#
# * Defaults to disk storage but you can also use Amazon S3, Google Cloud Storage, or Microsoft Azure Storage.
# * Defaults to disk storage but you can also use Amazon S3 or Cloudflare R2
# * Set the appropriate environment variables to use these services.
# * Ensure libvips is installed on your system for image processing - https://github.com/libvips/libvips
#
# Amazon S3
# ==========
# ACTIVE_STORAGE_SERVICE=amazon
# ACTIVE_STORAGE_SERVICE=amazon <- Enables Amazon S3 storage
# S3_ACCESS_KEY_ID=
# S3_SECRET_ACCESS_KEY=
# S3_REGION= # defaults to `us-east-1` if not set
@ -98,26 +77,9 @@ GITHUB_REPO_BRANCH=main
#
# Cloudflare R2
# =============
# ACTIVE_STORAGE_SERVICE=cloudflare
# ACTIVE_STORAGE_SERVICE=cloudflare <- Enables Cloudflare R2 storage
# CLOUDFLARE_ACCOUNT_ID=
# CLOUDFLARE_ACCESS_KEY_ID=
# CLOUDFLARE_SECRET_ACCESS_KEY=
# CLOUDFLARE_BUCKET=
# ======================================================================================================
# Billing Module - responsible for handling billing
# ======================================================================================================
#
STRIPE_PUBLISHABLE_KEY=
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=
# ======================================================================================================
# Plaid Configuration
# ======================================================================================================
#
PLAID_CLIENT_ID=
PLAID_SECRET=
PLAID_ENV=
PLAID_EU_CLIENT_ID=
PLAID_EU_SECRET=

View file

@ -1,8 +0,0 @@
SELF_HOSTED=false
SYNTH_API_KEY=fookey
# Set to true if you want SimpleCov reports generated
COVERAGE=false
# Set to true to run test suite serially
DISABLE_PARALLELIZATION=false

View file

@ -1,3 +1,5 @@
SELF_HOSTED=false
# ================
# Data Providers
# ---------------------------------------------------------------------------------

View file

@ -1,31 +1,61 @@
---
name: Bug report
about: Create a report to help us improve
about: Open a bug report when you experience broken functionality within the latest
version of the Maybe app
title: 'Bug: [Add descriptive title here]'
labels: ''
assignees: ''
---
**Where did this bug occur? (required)**
## Before you start (required)
- [ ] I am a self-hosted user reporting a bug from my self hosted app
- [ ] I am a user of Maybe's paid app
### General checklist
_Please note, if you are reporting a bug with sensitive data, please open an Intercom chat from within the app for help_
- [ ] I have removed personal / sensitive data from screenshots and logs
- [ ] I have searched [existing issues](https://github.com/maybe-finance/maybe/issues?q=is:issue) and [discussions](https://github.com/maybe-finance/maybe/discussions) to ensure this is not a duplicate issue
### How are you using Maybe?
- [ ] I am a paying Maybe customer (hosted version)
- Paying Maybe users can also open requests in Intercom (if there is sensitive info involved)
- [ ] I am a self-hosted user
### Self hoster checklist
_Paying, hosted users should delete this entire section._
If you are a self-hosted user, please complete all of the information below. Issues with incomplete information will be marked as `Needs Info` to help our small team prioritize bug fixes.
- Self hosted app commit SHA (find in user menu): [enter commit sha here]
- [ ] I have confirmed that my app's commit is the latest version of Maybe
- Where are you hosting?
- [ ] Render
- [ ] Docker Compose
- [ ] Umbrel
- [ ] Other (please specify)
---
## Bug description
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
### To Reproduce
Be as specific as possible so Maybe maintainers can quickly reproduce the bug you're experiencing.
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
### Expected behavior
**Screenshots / Recordings**
If applicable, add screenshots or short video recordings to help show the bug in more detail.
What is the intended behavior that you would expect?
### Screenshots and/or recordings
We highly recommend providing additional context with screenshots and/or screen recordings. This will _significantly_ improve the chances of the bug being addressed and fixed quickly.

View file

@ -7,15 +7,33 @@ assignees: ''
---
**PLEASE READ before opening an issue:**
## Before you start (required)
- Is this a feature request? Please [open a feature request discussion](https://github.com/maybe-finance/maybe/discussions/new?category=feature-requests).
- Do you need help or have a question? Please [open a discussion](https://github.com/maybe-finance/maybe/discussions/new/choose) or [join our Discord](https://link.maybe.co/discord) and post to the "help" channel.
### Is this a bug?
----------------------
A bug is _broken functionality_ of the app (i.e. it prevents you from using the app). For bugs, please use the ["Bug Report" template](https://github.com/maybe-finance/maybe/issues) instead.
**Is this issue related to a problem? Please describe.**
### Is this a bug with _sensitive info_?
**Describe the work that needs to be done to address this issue**
If you are a _paying_ Maybe user, you can open a support request in Intercom.
**Additional context**
### Is this a feature request?
A feature request is functionality that you would like that is not already on our [Roadmap](https://github.com/maybe-finance/maybe/wiki/Roadmap).
All feature requests should be opened in a [Feature request Discussion](https://github.com/maybe-finance/maybe/discussions/categories/feature-requests).
Be sure to search existing discussions prior to opening a new feature request.
### Is this related to Docker and/or hosting for self hosting?
If you are having a Docker configuration issue, please do not open a Github issue unless you've identified a bug in our Dockerfile. To get help with self hosting, there are several options:
- **First**: Read our [Docker hosting guide](https://github.com/maybe-finance/maybe/tree/main/docs/hosting/docker.md) and follow it step-by-step
- Open a [Docker Discussion](https://github.com/maybe-finance/maybe/discussions/categories/docker-compose-hosting)
---
## Issue description
If your issue does not fall into the categories above, please provide a **descriptive and complete** overview of your issue.

View file

@ -77,7 +77,10 @@ jobs:
timeout-minutes: 10
env:
PLAID_CLIENT_ID: foo
PLAID_SECRET: bar
DATABASE_URL: postgres://postgres:postgres@localhost:5432
REDIS_URL: redis://localhost:6379
RAILS_ENV: test
services:
@ -90,6 +93,12 @@ jobs:
- 5432:5432
options: --health-cmd="pg_isready" --health-interval=10s --health-timeout=5s --health-retries=3
redis:
image: redis
ports:
- 6379:6379
options: --health-cmd="redis-cli ping" --health-interval=10s --health-timeout=5s --health-retries=3
steps:
- name: Install packages
run: sudo apt-get update && sudo apt-get install --no-install-recommends -y google-chrome-stable curl libvips postgresql-client libpq-dev

View file

@ -1,6 +1,13 @@
name: Publish Docker image
on:
workflow_dispatch:
inputs:
ref:
description: 'Git ref (tag or commit SHA) to build'
required: true
type: string
default: 'main'
push:
tags:
- 'v*'
@ -33,6 +40,8 @@ jobs:
steps:
- name: Check out the repo
uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.ref || github.ref }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
@ -67,9 +76,10 @@ jobs:
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: ${{ startsWith(github.ref, 'refs/tags/v') && 'linux/amd64,linux/arm64,linux/arm/v7' || 'linux/amd64,linux/arm64' }}
platforms: 'linux/amd64,linux/arm64'
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: false
# https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#adding-a-description-to-multi-arch-images
outputs: type=image,name=target,annotation-index.org.opencontainers.image.description=A multi-arch Docker image for the Maybe Rails app
build-args: BUILD_COMMIT_SHA=${{ github.sha }}

44
.gitignore vendored
View file

@ -11,7 +11,6 @@
# Ignore all environment files (except templates).
/.env*
!/.env*.erb
!.env.test
!.env*.example
# Ignore all logfiles and tempfiles.
@ -63,6 +62,47 @@ gcp-storage-keyfile.json
coverage
.cursorrules
.cursor/rules/structure.mdc
.cursor/rules/agent.mdc
# Ignore node related files
node_modules
node_modules
compose.yml
plaid_test_accounts/
# Added by Claude Task Master
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
dev-debug.log
# Dependency directories
node_modules/
# Environment variables
.env
# Editor directories and files
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.roo*
# OS specific
# Task files
.taskmaster/
tasks.json
.taskmaster/tasks/
.taskmaster/reports/
.taskmaster/state.json
*.mcp.json
scripts/
.cursor/mcp.json
.taskmasterconfig
.windsurfrules
.cursor/rules/dev_workflow.mdc
.cursor/rules/taskmaster.mdc

View file

@ -1 +1 @@
3.4.1
3.4.4

273
CLAUDE.md Normal file
View file

@ -0,0 +1,273 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Common Development Commands
### Development Server
- `bin/dev` - Start development server (Rails, Sidekiq, Tailwind CSS watcher)
- `bin/rails server` - Start Rails server only
- `bin/rails console` - Open Rails console
### Testing
- `bin/rails test` - Run all tests
- `bin/rails test:db` - Run tests with database reset
- `bin/rails test:system` - Run system tests only (use sparingly - they take longer)
- `bin/rails test test/models/account_test.rb` - Run specific test file
- `bin/rails test test/models/account_test.rb:42` - Run specific test at line
### Linting & Formatting
- `bin/rubocop` - Run Ruby linter
- `npm run lint` - Check JavaScript/TypeScript code
- `npm run lint:fix` - Fix JavaScript/TypeScript issues
- `npm run format` - Format JavaScript/TypeScript code
- `bin/brakeman` - Run security analysis
### Database
- `bin/rails db:prepare` - Create and migrate database
- `bin/rails db:migrate` - Run pending migrations
- `bin/rails db:rollback` - Rollback last migration
- `bin/rails db:seed` - Load seed data
### Setup
- `bin/setup` - Initial project setup (installs dependencies, prepares database)
## Pre-Pull Request CI Workflow
ALWAYS run these commands before opening a pull request:
1. **Tests** (Required):
- `bin/rails test` - Run all tests (always required)
- `bin/rails test:system` - Run system tests (only when applicable, they take longer)
2. **Linting** (Required):
- `bin/rubocop -f github -a` - Ruby linting with auto-correct
- `bundle exec erb_lint ./app/**/*.erb -a` - ERB linting with auto-correct
3. **Security** (Required):
- `bin/brakeman --no-pager` - Security analysis
Only proceed with pull request creation if ALL checks pass.
## General Development Rules
### Authentication Context
- Use `Current.user` for the current user. Do NOT use `current_user`.
- Use `Current.family` for the current family. Do NOT use `current_family`.
### Development Guidelines
- Prior to generating any code, carefully read the project conventions and guidelines
- Ignore i18n methods and files. Hardcode strings in English for now to optimize speed of development
- Do not run `rails server` in your responses
- Do not run `touch tmp/restart.txt`
- Do not run `rails credentials`
- Do not automatically run migrations
## High-Level Architecture
### Application Modes
The Maybe app runs in two distinct modes:
- **Managed**: The Maybe team operates and manages servers for users (Rails.application.config.app_mode = "managed")
- **Self Hosted**: Users host the Maybe app on their own infrastructure, typically through Docker Compose (Rails.application.config.app_mode = "self_hosted")
### Core Domain Model
The application is built around financial data management with these key relationships:
- **User** → has many **Accounts** → has many **Transactions**
- **Account** types: checking, savings, credit cards, investments, crypto, loans, properties
- **Transaction** → belongs to **Category**, can have **Tags** and **Rules**
- **Investment accounts** → have **Holdings** → track **Securities** via **Trades**
### API Architecture
The application provides both internal and external APIs:
- Internal API: Controllers serve JSON via Turbo for SPA-like interactions
- External API: `/api/v1/` namespace with Doorkeeper OAuth and API key authentication
- API responses use Jbuilder templates for JSON rendering
- Rate limiting via Rack Attack with configurable limits per API key
### Sync & Import System
Two primary data ingestion methods:
1. **Plaid Integration**: Real-time bank account syncing
- `PlaidItem` manages connections
- `Sync` tracks sync operations
- Background jobs handle data updates
2. **CSV Import**: Manual data import with mapping
- `Import` manages import sessions
- Supports transaction and balance imports
- Custom field mapping with transformation rules
### Background Processing
Sidekiq handles asynchronous tasks:
- Account syncing (`SyncAccountsJob`)
- Import processing (`ImportDataJob`)
- AI chat responses (`CreateChatResponseJob`)
- Scheduled maintenance via sidekiq-cron
### Frontend Architecture
- **Hotwire Stack**: Turbo + Stimulus for reactive UI without heavy JavaScript
- **ViewComponents**: Reusable UI components in `app/components/`
- **Stimulus Controllers**: Handle interactivity, organized alongside components
- **Charts**: D3.js for financial visualizations (time series, donut, sankey)
- **Styling**: Tailwind CSS v4.x with custom design system
- Design system defined in `app/assets/tailwind/maybe-design-system.css`
- Always use functional tokens (e.g., `text-primary` not `text-white`)
- Prefer semantic HTML elements over JS components
- Use `icon` helper for icons, never `lucide_icon` directly
### Multi-Currency Support
- All monetary values stored in base currency (user's primary currency)
- Exchange rates fetched from Synth API
- `Money` objects handle currency conversion and formatting
- Historical exchange rates for accurate reporting
### Security & Authentication
- Session-based auth for web users
- API authentication via:
- OAuth2 (Doorkeeper) for third-party apps
- API keys with JWT tokens for direct API access
- Scoped permissions system for API access
- Strong parameters and CSRF protection throughout
### Testing Philosophy
- Comprehensive test coverage using Rails' built-in Minitest
- Fixtures for test data (avoid FactoryBot)
- Keep fixtures minimal (2-3 per model for base cases)
- VCR for external API testing
- System tests for critical user flows (use sparingly)
- Test helpers in `test/support/` for common scenarios
- Only test critical code paths that significantly increase confidence
- Write tests as you go, when required
### Performance Considerations
- Database queries optimized with proper indexes
- N+1 queries prevented via includes/joins
- Background jobs for heavy operations
- Caching strategies for expensive calculations
- Turbo Frames for partial page updates
### Development Workflow
- Feature branches merged to `main`
- Docker support for consistent environments
- Environment variables via `.env` files
- Lookbook for component development (`/lookbook`)
- Letter Opener for email preview in development
## Project Conventions
### Convention 1: Minimize Dependencies
- Push Rails to its limits before adding new dependencies
- Strong technical/business reason required for new dependencies
- Favor old and reliable over new and flashy
### Convention 2: Skinny Controllers, Fat Models
- Business logic in `app/models/` folder, avoid `app/services/`
- Use Rails concerns and POROs for organization
- Models should answer questions about themselves: `account.balance_series` not `AccountSeries.new(account).call`
### Convention 3: Hotwire-First Frontend
- **Native HTML preferred over JS components**
- Use `<dialog>` for modals, `<details><summary>` for disclosures
- **Leverage Turbo frames** for page sections over client-side solutions
- **Query params for state** over localStorage/sessions
- **Server-side formatting** for currencies, numbers, dates
- **Always use `icon` helper** in `application_helper.rb`, NEVER `lucide_icon` directly
### Convention 4: Optimize for Simplicity
- Prioritize good OOP domain design over performance
- Focus performance only on critical/global areas (avoid N+1 queries, mindful of global layouts)
### Convention 5: Database vs ActiveRecord Validations
- Simple validations (null checks, unique indexes) in DB
- ActiveRecord validations for convenience in forms (prefer client-side when possible)
- Complex validations and business logic in ActiveRecord
## TailwindCSS Design System
### Design System Rules
- **Always reference `app/assets/tailwind/maybe-design-system.css`** for primitives and tokens
- **Use functional tokens** defined in design system:
- `text-primary` instead of `text-white`
- `bg-container` instead of `bg-white`
- `border border-primary` instead of `border border-gray-200`
- **NEVER create new styles** in design system files without permission
- **Always generate semantic HTML**
## Component Architecture
### ViewComponent vs Partials Decision Making
**Use ViewComponents when:**
- Element has complex logic or styling patterns
- Element will be reused across multiple views/contexts
- Element needs structured styling with variants/sizes
- Element requires interactive behavior or Stimulus controllers
- Element has configurable slots or complex APIs
- Element needs accessibility features or ARIA support
**Use Partials when:**
- Element is primarily static HTML with minimal logic
- Element is used in only one or few specific contexts
- Element is simple template content
- Element doesn't need variants, sizes, or complex configuration
- Element is more about content organization than reusable functionality
**Component Guidelines:**
- Prefer components over partials when available
- Keep domain logic OUT of view templates
- Logic belongs in component files, not template files
### Stimulus Controller Guidelines
**Declarative Actions (Required):**
```erb
<!-- GOOD: Declarative - HTML declares what happens -->
<div data-controller="toggle">
<button data-action="click->toggle#toggle" data-toggle-target="button">Show</button>
<div data-toggle-target="content" class="hidden">Hello World!</div>
</div>
```
**Controller Best Practices:**
- Keep controllers lightweight and simple (< 7 targets)
- Use private methods and expose clear public API
- Single responsibility or highly related responsibilities
- Component controllers stay in component directory, global controllers in `app/javascript/controllers/`
- Pass data via `data-*-value` attributes, not inline JavaScript
## Testing Philosophy
### General Testing Rules
- **ALWAYS use Minitest + fixtures** (NEVER RSpec or factories)
- Keep fixtures minimal (2-3 per model for base cases)
- Create edge cases on-the-fly within test context
- Use Rails helpers for large fixture creation needs
### Test Quality Guidelines
- **Write minimal, effective tests** - system tests sparingly
- **Only test critical and important code paths**
- **Test boundaries correctly:**
- Commands: test they were called with correct params
- Queries: test output
- Don't test implementation details of other classes
### Testing Examples
```ruby
# GOOD - Testing critical domain business logic
test "syncs balances" do
Holding::Syncer.any_instance.expects(:sync_holdings).returns([]).once
assert_difference "@account.balances.count", 2 do
Balance::Syncer.new(@account, strategy: :forward).sync_balances
end
end
# BAD - Testing ActiveRecord functionality
test "saves balance" do
balance_record = Balance.new(balance: 100, currency: "USD")
assert balance_record.save
end
```
### Stubs and Mocks
- Use `mocha` gem
- Prefer `OpenStruct` for mock instances
- Only mock what's necessary

View file

@ -4,7 +4,7 @@ It means so much that you're interested in contributing to Maybe! Seriously. Tha
## House Rules
- Before contributing, familiarize yourself with our project conventions. You should read through our [Project Conventions Rule](https://github.com/maybe-finance/maybe/.cursor/rules/project-conventions.mdc), which is intended for LLMs, but is also an excellent primer on how we write code for Maybe.
- Before contributing, familiarize yourself with our project conventions. You should read through our [Project Conventions Rule](https://github.com/maybe-finance/maybe/.cursor/rules/project-conventions.mdc), which is intended for LLMs, but is also an excellent primer on how we write code for Maybe.
- While totally optional, consider using Cursor + VSCode as it will automatically apply our project conventions to your code via the `.cursor/rules` directory.
- Before contributing, please check if it already exists in [issues](https://github.com/maybe-finance/maybe/issues) or [PRs](https://github.com/maybe-finance/maybe/pulls)
- Given the speed at which we're moving on the codebase, we don't assign issues or "give" issues to anyone.

View file

@ -1,7 +1,7 @@
# syntax = docker/dockerfile:1
# Make sure RUBY_VERSION matches the Ruby version in .ruby-version and Gemfile
ARG RUBY_VERSION=3.4.1
ARG RUBY_VERSION=3.4.4
FROM registry.docker.com/library/ruby:$RUBY_VERSION-slim AS base
# Rails app lives here
@ -9,19 +9,21 @@ WORKDIR /rails
# Install base packages
RUN apt-get update -qq && \
apt-get install --no-install-recommends -y curl libvips postgresql-client git
apt-get install --no-install-recommends -y curl libvips postgresql-client libyaml-0-2
# Set production environment
ARG BUILD_COMMIT_SHA
ENV RAILS_ENV="production" \
BUNDLE_DEPLOYMENT="1" \
BUNDLE_PATH="/usr/local/bundle" \
BUNDLE_WITHOUT="development"
BUNDLE_WITHOUT="development" \
BUILD_COMMIT_SHA=${BUILD_COMMIT_SHA}
# Throw-away build stage to reduce size of final image
FROM base AS build
# Install packages needed to build gems
RUN apt-get install --no-install-recommends -y build-essential libpq-dev pkg-config
RUN apt-get install --no-install-recommends -y build-essential libpq-dev git pkg-config libyaml-dev
# Install application gems
COPY .ruby-version Gemfile Gemfile.lock ./

43
Gemfile
View file

@ -7,6 +7,7 @@ gem "rails", "~> 7.2.2"
# Drivers
gem "pg", "~> 1.5"
gem "redis", "~> 5.4"
# Deployment
gem "puma", ">= 5.0"
@ -18,30 +19,43 @@ gem "propshaft"
gem "tailwindcss-rails"
gem "lucide-rails", github: "maybe-finance/lucide-rails"
# Hotwire
# Hotwire + UI
gem "stimulus-rails"
gem "turbo-rails"
gem "view_component"
# Temporary pin to commit to fix crypto.randomUUID() errors. Revert this when the change has been released.
gem "hotwire_combobox", github: "josefarias/hotwire_combobox", ref: "b827048a8305e1115d5f96931ba1c9750d1e59fc"
# https://github.com/lookbook-hq/lookbook/issues/712
# TODO: Remove max version constraint when fixed
gem "lookbook", "2.3.11"
gem "hotwire_combobox"
# Background Jobs
gem "good_job"
gem "sidekiq"
gem "sidekiq-cron"
# Error logging
gem "stackprof"
# Monitoring
gem "vernier"
gem "rack-mini-profiler"
gem "sentry-ruby"
gem "sentry-rails"
gem "sentry-sidekiq"
gem "logtail-rails"
gem "skylight", groups: [ :production ]
# Active Storage
gem "aws-sdk-s3", "~> 1.177.0", require: false
gem "image_processing", ">= 1.2"
# Other
gem "ostruct"
gem "bcrypt", "~> 3.1"
gem "jwt"
gem "jbuilder"
# OAuth & API Security
gem "doorkeeper"
gem "rack-attack", "~> 6.6"
gem "faraday"
gem "faraday-retry"
gem "faraday-multipart"
@ -56,7 +70,15 @@ gem "stripe"
gem "intercom-rails"
gem "plaid"
gem "rotp", "~> 6.3"
gem "rqrcode", "~> 2.2"
gem "rqrcode", "~> 3.0"
gem "activerecord-import"
# State machines
gem "aasm"
gem "after_commit_everywhere", "~> 1.0"
# AI
gem "ruby-openai"
group :development, :test do
gem "debug", platforms: %i[mri windows]
@ -67,6 +89,10 @@ group :development, :test do
gem "dotenv-rails"
end
if ENV["BENCHMARKING_ENABLED"]
gem "dotenv-rails", groups: [ :production ]
end
group :development do
gem "hotwire-livereload"
gem "letter_opener"
@ -74,6 +100,9 @@ group :development do
gem "web-console"
gem "faker"
gem "benchmark-ips"
gem "stackprof"
gem "derailed_benchmarks"
gem "foreman"
end
group :test do

View file

@ -1,14 +1,3 @@
GIT
remote: https://github.com/josefarias/hotwire_combobox.git
revision: b827048a8305e1115d5f96931ba1c9750d1e59fc
ref: b827048a8305e1115d5f96931ba1c9750d1e59fc
specs:
hotwire_combobox (0.3.2)
platform_agent (>= 1.0.1)
rails (>= 7.0.7.2)
stimulus-rails (>= 1.2)
turbo-rails (>= 1.2)
GIT
remote: https://github.com/maybe-finance/lucide-rails.git
revision: 272e5fb8418ea458da3995d6abe0ba0ceee9c9f0
@ -19,6 +8,8 @@ GIT
GEM
remote: https://rubygems.org/
specs:
aasm (5.5.1)
concurrent-ruby (~> 1.0)
actioncable (7.2.2.1)
actionpack (= 7.2.2.1)
activesupport (= 7.2.2.1)
@ -72,6 +63,8 @@ GEM
activemodel (= 7.2.2.1)
activesupport (= 7.2.2.1)
timeout (>= 0.4.0)
activerecord-import (2.2.0)
activerecord (>= 4.2)
activestorage (7.2.2.1)
actionpack (= 7.2.2.1)
activejob (= 7.2.2.1)
@ -92,26 +85,31 @@ GEM
tzinfo (~> 2.0, >= 2.0.5)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
ast (2.4.2)
aws-eventstream (1.3.0)
aws-partitions (1.1043.0)
aws-sdk-core (3.217.0)
after_commit_everywhere (1.6.0)
activerecord (>= 4.2)
activesupport
ast (2.4.3)
aws-eventstream (1.4.0)
aws-partitions (1.1113.0)
aws-sdk-core (3.225.1)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
base64
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.97.0)
aws-sdk-core (~> 3, >= 3.216.0)
logger
aws-sdk-kms (1.104.0)
aws-sdk-core (~> 3, >= 3.225.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.177.0)
aws-sdk-core (~> 3, >= 3.210.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.11.0)
aws-sigv4 (1.12.0)
aws-eventstream (~> 1, >= 1.0.2)
base64 (0.2.0)
base64 (0.3.0)
bcrypt (3.1.20)
benchmark (0.4.0)
benchmark (0.4.1)
benchmark-ips (2.14.0)
better_html (2.1.1)
actionview (>= 6.0)
@ -120,11 +118,11 @@ GEM
erubi (~> 1.4)
parser (>= 2.4)
smart_properties
bigdecimal (3.1.9)
bigdecimal (3.2.2)
bindex (0.8.1)
bootsnap (1.18.4)
bootsnap (1.18.6)
msgpack (~> 1.2)
brakeman (7.0.0)
brakeman (7.0.2)
racc
builder (3.3.0)
capybara (3.40.0)
@ -141,22 +139,48 @@ GEM
chunky_png (1.4.0)
climate_control (1.2.0)
concurrent-ruby (1.3.5)
connection_pool (2.5.0)
connection_pool (2.5.3)
crack (1.0.0)
bigdecimal
rexml
crass (1.0.6)
csv (3.3.2)
cronex (0.15.0)
tzinfo
unicode (>= 0.4.4.5)
css_parser (1.21.1)
addressable
csv (3.3.5)
date (3.4.1)
debug (1.10.0)
debug (1.11.0)
irb (~> 1.10)
reline (>= 0.3.8)
derailed_benchmarks (2.2.1)
base64
benchmark-ips (~> 2)
bigdecimal
drb
get_process_mem
heapy (~> 0)
logger
memory_profiler (>= 0, < 2)
mini_histogram (>= 0.3.0)
mutex_m
ostruct
rack (>= 1)
rack-test
rake (> 10, < 14)
ruby-statistics (>= 4.0.1)
ruby2_keywords
thor (>= 0.19, < 2)
docile (1.4.1)
dotenv (3.1.7)
dotenv-rails (3.1.7)
dotenv (= 3.1.7)
doorkeeper (5.8.2)
railties (>= 5)
dotenv (3.1.8)
dotenv-rails (3.1.8)
dotenv (= 3.1.8)
railties (>= 6.1)
drb (2.2.1)
drb (2.2.3)
erb (5.0.1)
erb_lint (0.9.0)
activesupport
better_html (>= 2.0.1)
@ -167,48 +191,55 @@ GEM
erubi (1.13.1)
et-orbi (1.2.11)
tzinfo
faker (3.5.1)
event_stream_parser (1.0.0)
faker (3.5.2)
i18n (>= 1.8.11, < 2)
faraday (2.12.2)
faraday (2.13.2)
faraday-net_http (>= 2.0, < 3.5)
json
logger
faraday-multipart (1.1.0)
faraday-multipart (1.1.1)
multipart-post (~> 2.0)
faraday-net_http (3.4.0)
faraday-net_http (3.4.1)
net-http (>= 0.5.0)
faraday-retry (2.2.1)
faraday-retry (2.3.2)
faraday (~> 2.0)
ffi (1.17.1-aarch64-linux-gnu)
ffi (1.17.1-aarch64-linux-musl)
ffi (1.17.1-arm-linux-gnu)
ffi (1.17.1-arm-linux-musl)
ffi (1.17.1-arm64-darwin)
ffi (1.17.1-x86_64-darwin)
ffi (1.17.1-x86_64-linux-gnu)
ffi (1.17.1-x86_64-linux-musl)
ffi (1.17.2-aarch64-linux-gnu)
ffi (1.17.2-aarch64-linux-musl)
ffi (1.17.2-arm-linux-gnu)
ffi (1.17.2-arm-linux-musl)
ffi (1.17.2-arm64-darwin)
ffi (1.17.2-x86_64-darwin)
ffi (1.17.2-x86_64-linux-gnu)
ffi (1.17.2-x86_64-linux-musl)
foreman (0.88.1)
fugit (1.11.1)
et-orbi (~> 1, >= 1.2.11)
raabro (~> 1.4)
get_process_mem (1.0.0)
bigdecimal (>= 2.0)
ffi (~> 1.0)
globalid (1.2.1)
activesupport (>= 6.1)
good_job (4.9.0)
activejob (>= 6.1.0)
activerecord (>= 6.1.0)
concurrent-ruby (>= 1.3.1)
fugit (>= 1.11.0)
railties (>= 6.1.0)
thor (>= 1.0.0)
hashdiff (1.1.2)
hashdiff (1.2.0)
heapy (0.2.0)
thor
highline (3.1.2)
reline
hotwire-livereload (2.0.0)
actioncable (>= 7.0.0)
listen (>= 3.0.0)
railties (>= 7.0.0)
hotwire_combobox (0.4.0)
platform_agent (>= 1.0.1)
rails (>= 7.0.7.2)
stimulus-rails (>= 1.2)
turbo-rails (>= 1.2)
htmlbeautifier (1.4.3)
htmlentities (4.3.4)
i18n (1.14.7)
concurrent-ruby (~> 1.0)
i18n-tasks (1.0.14)
i18n-tasks (1.0.15)
activesupport (>= 4.0.2)
ast (>= 2.1.0)
erubi
@ -217,6 +248,7 @@ GEM
parser (>= 3.2.2.1)
rails-i18n
rainbow (>= 2.2.2, < 4.0)
ruby-progressbar (~> 1.8, >= 1.8.1)
terminal-table (>= 1.5.1)
image_processing (1.14.0)
mini_magick (>= 4.9.5, < 6)
@ -232,26 +264,30 @@ GEM
activesupport (> 4.0)
jwt (~> 2.0)
io-console (0.8.0)
irb (1.15.1)
irb (1.15.2)
pp (>= 0.6.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
jbuilder (2.13.0)
actionview (>= 5.0.0)
activesupport (>= 5.0.0)
jmespath (1.6.2)
json (2.10.1)
jwt (2.10.1)
json (2.12.2)
jwt (2.10.2)
base64
language_server-protocol (3.17.0.4)
launchy (3.1.0)
language_server-protocol (3.17.0.5)
launchy (3.1.1)
addressable (~> 2.8)
childprocess (~> 5.0)
logger (~> 1.6)
letter_opener (1.10.0)
launchy (>= 2.2, < 4)
lint_roller (1.1.0)
listen (3.9.0)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
logger (1.6.6)
logtail (0.1.15)
logger (1.7.0)
logtail (0.1.17)
msgpack (~> 1.0)
logtail-rack (0.2.6)
logtail (~> 0.1)
@ -262,9 +298,21 @@ GEM
logtail (~> 0.1, >= 0.1.14)
logtail-rack (~> 0.1)
railties (>= 5.0.0)
loofah (2.24.0)
loofah (2.24.1)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
lookbook (2.3.11)
activemodel
css_parser
htmlbeautifier (~> 1.3)
htmlentities (~> 4.3.4)
marcel (~> 1.0)
railties (>= 5.0)
redcarpet (~> 3.5)
rouge (>= 3.26, < 5.0)
view_component (>= 2.0)
yard (~> 0.9)
zeitwerk (~> 2.5)
mail (2.8.1)
mini_mime (>= 0.1.1)
net-imap
@ -272,53 +320,58 @@ GEM
net-smtp
marcel (1.0.4)
matrix (0.4.2)
mini_magick (5.1.2)
memory_profiler (1.1.0)
method_source (1.1.0)
mini_histogram (0.3.1)
mini_magick (5.2.0)
benchmark
logger
mini_mime (1.1.5)
minitest (5.25.4)
minitest (5.25.5)
mocha (2.7.1)
ruby2_keywords (>= 0.0.5)
msgpack (1.8.0)
multipart-post (2.4.1)
mutex_m (0.3.0)
net-http (0.6.0)
uri
net-imap (0.5.5)
net-imap (0.5.8)
date
net-protocol
net-pop (0.1.2)
net-protocol
net-protocol (0.2.2)
timeout
net-smtp (0.5.0)
net-smtp (0.5.1)
net-protocol
nio4r (2.7.4)
nokogiri (1.18.2-aarch64-linux-gnu)
nokogiri (1.18.8-aarch64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.2-aarch64-linux-musl)
nokogiri (1.18.8-aarch64-linux-musl)
racc (~> 1.4)
nokogiri (1.18.2-arm-linux-gnu)
nokogiri (1.18.8-arm-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.2-arm-linux-musl)
nokogiri (1.18.8-arm-linux-musl)
racc (~> 1.4)
nokogiri (1.18.2-arm64-darwin)
nokogiri (1.18.8-arm64-darwin)
racc (~> 1.4)
nokogiri (1.18.2-x86_64-darwin)
nokogiri (1.18.8-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.18.2-x86_64-linux-gnu)
nokogiri (1.18.8-x86_64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.2-x86_64-linux-musl)
nokogiri (1.18.8-x86_64-linux-musl)
racc (~> 1.4)
octokit (9.2.0)
octokit (10.0.0)
faraday (>= 1, < 3)
sawyer (~> 0.9)
pagy (9.3.3)
parallel (1.26.3)
parser (3.3.7.0)
ostruct (0.6.2)
pagy (9.3.5)
parallel (1.27.0)
parser (3.3.8.0)
ast (~> 2.4.1)
racc
pg (1.5.9)
plaid (36.0.0)
plaid (41.0.0)
faraday (>= 1.0.1, < 3.0)
faraday-multipart (>= 1.0.1, < 2.0)
platform_agent (1.0.1)
@ -327,24 +380,26 @@ GEM
pp (0.6.2)
prettyprint
prettyprint (0.2.0)
prism (1.3.0)
prism (1.4.0)
propshaft (1.1.0)
actionpack (>= 7.0.0)
activesupport (>= 7.0.0)
rack
railties (>= 7.0.0)
psych (5.2.3)
psych (5.2.6)
date
stringio
public_suffix (6.0.1)
public_suffix (6.0.2)
puma (6.6.0)
nio4r (~> 2.0)
raabro (1.4.0)
racc (1.8.1)
rack (3.1.10)
rack-mini-profiler (3.3.1)
rack (3.1.16)
rack-attack (6.7.0)
rack (>= 1.0, < 4)
rack-mini-profiler (4.0.0)
rack (>= 1.2.0)
rack-session (2.1.0)
rack-session (2.1.1)
base64 (>= 0.1.0)
rack (>= 3.0.0)
rack-test (2.2.0)
@ -365,7 +420,7 @@ GEM
activesupport (= 7.2.2.1)
bundler (>= 1.15.0)
railties (= 7.2.2.1)
rails-dom-testing (2.2.0)
rails-dom-testing (2.3.0)
activesupport (>= 5.0.0)
minitest
nokogiri (>= 1.6)
@ -387,61 +442,72 @@ GEM
thor (~> 1.0, >= 1.2.2)
zeitwerk (~> 2.6)
rainbow (3.1.1)
rake (13.2.1)
rake (13.3.0)
rb-fsevent (0.11.2)
rb-inotify (0.11.1)
ffi (~> 1.0)
rbs (3.8.1)
rbs (3.9.4)
logger
rdoc (6.12.0)
rdoc (6.14.2)
erb
psych (>= 4.0.0)
redcarpet (3.6.0)
redcarpet (3.6.1)
redis (5.4.0)
redis-client (>= 0.22.0)
redis-client (0.25.0)
connection_pool
regexp_parser (2.10.0)
reline (0.6.0)
reline (0.6.1)
io-console (~> 0.5)
rexml (3.4.0)
rexml (3.4.1)
rotp (6.3.0)
rqrcode (2.2.0)
rouge (4.5.2)
rqrcode (3.1.0)
chunky_png (~> 1.0)
rqrcode_core (~> 1.0)
rqrcode_core (1.2.0)
rubocop (1.71.0)
rqrcode_core (~> 2.0)
rqrcode_core (2.0.0)
rubocop (1.76.1)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
parallel (~> 1.10)
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.36.2, < 2.0)
rubocop-ast (>= 1.45.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.38.0)
parser (>= 3.3.1.0)
rubocop-minitest (0.36.0)
rubocop (>= 1.61, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0)
rubocop-performance (1.23.1)
rubocop (>= 1.48.1, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rails (2.29.1)
rubocop-ast (1.45.1)
parser (>= 3.3.7.2)
prism (~> 1.4)
rubocop-performance (1.25.0)
lint_roller (~> 1.1)
rubocop (>= 1.75.0, < 2.0)
rubocop-ast (>= 1.38.0, < 2.0)
rubocop-rails (2.32.0)
activesupport (>= 4.2.0)
lint_roller (~> 1.1)
rack (>= 1.1)
rubocop (>= 1.52.0, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rails-omakase (1.0.0)
rubocop
rubocop-minitest
rubocop-performance
rubocop-rails
ruby-lsp (0.23.9)
rubocop (>= 1.75.0, < 2.0)
rubocop-ast (>= 1.44.0, < 2.0)
rubocop-rails-omakase (1.1.0)
rubocop (>= 1.72)
rubocop-performance (>= 1.24)
rubocop-rails (>= 2.30)
ruby-lsp (0.24.1)
language_server-protocol (~> 3.17.0)
prism (>= 1.2, < 2.0)
rbs (>= 3, < 4)
rbs (>= 3, < 5)
sorbet-runtime (>= 0.5.10782)
ruby-lsp-rails (0.4.0)
ruby-lsp (>= 0.23.0, < 0.24.0)
ruby-lsp-rails (0.4.6)
ruby-lsp (>= 0.24.0, < 0.25.0)
ruby-openai (8.1.0)
event_stream_parser (>= 0.3.0, < 2.0.0)
faraday (>= 1)
faraday-multipart (>= 1)
ruby-progressbar (1.13.0)
ruby-vips (2.2.3)
ruby-statistics (4.1.0)
ruby-vips (2.2.4)
ffi (~> 1.12)
logger
ruby2_keywords (0.0.5)
@ -450,89 +516,112 @@ GEM
addressable (>= 2.3.5)
faraday (>= 0.17.3, < 3)
securerandom (0.4.1)
selenium-webdriver (4.28.0)
selenium-webdriver (4.34.0)
base64 (~> 0.2)
logger (~> 1.4)
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0)
sentry-rails (5.22.4)
sentry-rails (5.26.0)
railties (>= 5.0)
sentry-ruby (~> 5.22.4)
sentry-ruby (5.22.4)
sentry-ruby (~> 5.26.0)
sentry-ruby (5.26.0)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2)
sentry-sidekiq (5.26.0)
sentry-ruby (~> 5.26.0)
sidekiq (>= 3.0)
sidekiq (8.0.5)
connection_pool (>= 2.5.0)
json (>= 2.9.0)
logger (>= 1.6.2)
rack (>= 3.1.0)
redis-client (>= 0.23.2)
sidekiq-cron (2.3.0)
cronex (>= 0.13.0)
fugit (~> 1.8, >= 1.11.1)
globalid (>= 1.0.1)
sidekiq (>= 6.5.0)
simplecov (0.22.0)
docile (~> 1.1)
simplecov-html (~> 0.11)
simplecov_json_formatter (~> 0.1)
simplecov-html (0.13.1)
simplecov_json_formatter (0.1.4)
skylight (6.0.4)
activesupport (>= 5.2.0)
smart_properties (1.17.0)
sorbet-runtime (0.5.11813)
sorbet-runtime (0.5.12163)
stackprof (0.2.27)
stimulus-rails (1.3.4)
railties (>= 6.0.0)
stringio (3.1.3)
stripe (13.4.1)
tailwindcss-rails (4.0.0)
stringio (3.1.7)
stripe (15.3.0)
tailwindcss-rails (4.2.3)
railties (>= 7.0.0)
tailwindcss-ruby (~> 4.0)
tailwindcss-ruby (4.0.6)
tailwindcss-ruby (4.0.6-aarch64-linux-gnu)
tailwindcss-ruby (4.0.6-aarch64-linux-musl)
tailwindcss-ruby (4.0.6-arm64-darwin)
tailwindcss-ruby (4.0.6-x86_64-darwin)
tailwindcss-ruby (4.0.6-x86_64-linux-gnu)
tailwindcss-ruby (4.0.6-x86_64-linux-musl)
tailwindcss-ruby (4.1.8)
tailwindcss-ruby (4.1.8-aarch64-linux-gnu)
tailwindcss-ruby (4.1.8-aarch64-linux-musl)
tailwindcss-ruby (4.1.8-arm64-darwin)
tailwindcss-ruby (4.1.8-x86_64-darwin)
tailwindcss-ruby (4.1.8-x86_64-linux-gnu)
tailwindcss-ruby (4.1.8-x86_64-linux-musl)
terminal-table (4.0.0)
unicode-display_width (>= 1.1.1, < 4)
thor (1.3.2)
timeout (0.4.3)
turbo-rails (2.0.11)
actionpack (>= 6.0.0)
railties (>= 6.0.0)
turbo-rails (2.0.16)
actionpack (>= 7.1.0)
railties (>= 7.1.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
unicode (0.4.4.5)
unicode-display_width (3.1.4)
unicode-emoji (~> 4.0, >= 4.0.4)
unicode-emoji (4.0.4)
uri (1.0.2)
uri (1.0.3)
useragent (0.16.11)
vcr (6.3.1)
base64
vernier (1.8.0)
view_component (3.23.2)
activesupport (>= 5.2.0, < 8.1)
concurrent-ruby (~> 1)
method_source (~> 1.0)
web-console (4.2.1)
actionview (>= 6.0.0)
activemodel (>= 6.0.0)
bindex (>= 0.4.0)
railties (>= 6.0.0)
webmock (3.25.0)
webmock (3.25.1)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
websocket (1.2.11)
websocket-driver (0.7.7)
websocket-driver (0.8.0)
base64
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.7.1)
yard (0.9.37)
zeitwerk (2.7.3)
PLATFORMS
aarch64-linux
aarch64-linux-gnu
aarch64-linux-musl
arm-linux
arm-linux-gnu
arm-linux-musl
arm64-darwin
x86_64-darwin
x86_64-linux
x86_64-linux-gnu
x86_64-linux-musl
DEPENDENCIES
aasm
activerecord-import
after_commit_everywhere (~> 1.0)
aws-sdk-s3 (~> 1.177.0)
bcrypt (~> 3.1)
benchmark-ips
@ -542,43 +631,55 @@ DEPENDENCIES
climate_control
csv
debug
derailed_benchmarks
doorkeeper
dotenv-rails
erb_lint
faker
faraday
faraday-multipart
faraday-retry
good_job
foreman
hotwire-livereload
hotwire_combobox!
hotwire_combobox
i18n-tasks
image_processing (>= 1.2)
importmap-rails
inline_svg
intercom-rails
jbuilder
jwt
letter_opener
logtail-rails
lookbook (= 2.3.11)
lucide-rails!
mocha
octokit
ostruct
pagy
pg (~> 1.5)
plaid
propshaft
puma (>= 5.0)
rack-attack (~> 6.6)
rack-mini-profiler
rails (~> 7.2.2)
rails-settings-cached
redcarpet
redis (~> 5.4)
rotp (~> 6.3)
rqrcode (~> 2.2)
rqrcode (~> 3.0)
rubocop-rails-omakase
ruby-lsp-rails
ruby-openai
selenium-webdriver
sentry-rails
sentry-ruby
sentry-sidekiq
sidekiq
sidekiq-cron
simplecov
skylight
stackprof
stimulus-rails
stripe
@ -586,11 +687,13 @@ DEPENDENCIES
turbo-rails
tzinfo-data
vcr
vernier
view_component
web-console
webmock
RUBY VERSION
ruby 3.4.1p0
ruby 3.4.4p34
BUNDLED WITH
2.6.3
2.6.9

View file

@ -1,3 +1,3 @@
web: bundle exec ${DEBUG:+rdbg -O -n -c --} bin/rails server -b 0.0.0.0
css: bundle exec bin/rails tailwindcss:watch
worker: bundle exec good_job start
css: bundle exec bin/rails tailwindcss:watch 2>/dev/null
worker: bundle exec sidekiq

View file

@ -1,14 +1,11 @@
<img width="1440" alt="dashboard_mockup" src="https://github.com/maybe-finance/maybe/assets/35243/a7763d0e-a942-42db-bde7-eb8d28106917">
<sup><i>(Note: The image above is a mockup of what we're working towards. We're rapidly approaching the functionality shown, but not all of the parts are ready just yet.)</i></sup>
# Maybe: The OS for your personal finances
<img width="1190" alt="maybe_hero" src="https://github.com/user-attachments/assets/13fc5ef4-ce0f-4073-a163-9dbc3eb4c8e5" />
# Maybe: The personal finance app for everyone
<b>Get
involved: [Discord](https://link.maybe.co/discord) • [Website](https://maybefinance.com) • [Issues](https://github.com/maybe-finance/maybe/issues)</b>
_If you're looking for the previous React codebase, you can find it
at [maybe-finance/maybe-archive](https://github.com/maybe-finance/maybe-archive)._
## Backstory
We spent the better part of 2021/2022 building a personal finance + wealth
@ -27,11 +24,10 @@ and eventually offer a hosted version of the app for a small monthly fee.
## Maybe Hosting
There are 3 primary ways to use the Maybe app:
There are 2 primary ways to use the Maybe app:
1. Managed (easiest) - _coming soon..._
2. [One-click deploy](docs/hosting/one-click-deploy.md)
3. [Self-host with Docker](docs/hosting/docker.md)
1. Managed (easiest) - we're in alpha and release invites in our Discord
2. [Self-host with Docker](docs/hosting/docker.md)
## Contributing
@ -42,6 +38,14 @@ Once you've done that, please visit
our [contributing guide](https://github.com/maybe-finance/maybe/blob/main/CONTRIBUTING.md)
to get started!
### Performance Issues
With data-heavy apps, inevitably, there are performance issues. We've set up a public dashboard showing the problematic requests, along with the stacktraces to help debug them.
Any contributions that help improve performance are very much welcome.
https://oss.skylight.io/app/applications/XDpPIXEX52oi/recent/6h/endpoints
## Local Development Setup
**If you are trying to _self-host_ the Maybe app, stop here. You
@ -63,7 +67,7 @@ bin/setup
bin/dev
# Optionally, load demo data
rake demo_data:reset
rake demo_data:default
```
And visit http://localhost:3000 to see the app. You can use the following
@ -84,37 +88,10 @@ If you'd like multi-currency support, there are a few extra steps to follow.
### Setup Guides
#### Dev Container (optional)
This is 100% optional and meant for devs who don't want to worry about
installing requirements manually for their platform. You can
follow [this guide](https://code.visualstudio.com/docs/devcontainers/containers)
to learn more about Dev Containers.
If you run into `could not connect to server` errors, you may need to change
your `.env`'s `DB_HOST` environment variable value to `db` to point to the
Postgres container.
#### Mac
Please visit
our [Mac dev setup guide](https://github.com/maybe-finance/maybe/wiki/Mac-Dev-Setup-Guide).
#### Linux
Please visit
our [Linux dev setup guide](https://github.com/maybe-finance/maybe/wiki/Linux-Dev-Setup-Guide).
#### Windows
Please visit
our [Windows dev setup guide](https://github.com/maybe-finance/maybe/wiki/Windows-Dev-Setup-Guide).
### Testing Emails
In development, we use `letter_opener` to automatically open emails in your
browser. When an email sends locally, a new browser tab will open with a
preview.
- [Mac dev setup guide](https://github.com/maybe-finance/maybe/wiki/Mac-Dev-Setup-Guide)
- [Linux dev setup guide](https://github.com/maybe-finance/maybe/wiki/Linux-Dev-Setup-Guide)
- [Windows dev setup guide](https://github.com/maybe-finance/maybe/wiki/Windows-Dev-Setup-Guide)
- Dev containers - visit [this guide](https://code.visualstudio.com/docs/devcontainers/containers) to learn more
## Repo Activity

View file

@ -0,0 +1,71 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" fill="none">
<g filter="url(#filter0_i_2942_1229)">
<rect width="32" height="32" rx="10" fill="url(#paint0_linear_2942_1229)"/>
<rect width="32" height="32" rx="10" fill="white" fill-opacity="0.07" style="mix-blend-mode:plus-lighter"/>
</g>
<g filter="url(#filter1_ii_2942_1229)">
<rect x="1.75" y="1.75" width="28.5" height="28.5" rx="8" fill="url(#paint1_linear_2942_1229)"/>
</g>
<path d="M21.524 8.8489C21.9357 8.84115 22.2757 9.16862 22.2835 9.58033C22.3104 11.0111 22.3592 12.4355 22.4277 13.8639C22.4474 14.2752 22.1299 14.6246 21.7186 14.6443C21.3073 14.664 20.9579 14.3466 20.9382 13.9353C20.8691 12.4933 20.8198 11.0544 20.7926 9.60841C20.7848 9.1967 21.1123 8.85665 21.524 8.8489Z" fill="#141414"/>
<path d="M21.524 8.8489C21.9357 8.84115 22.2757 9.16862 22.2835 9.58033C22.3104 11.0111 22.3592 12.4355 22.4277 13.8639C22.4474 14.2752 22.1299 14.6246 21.7186 14.6443C21.3073 14.664 20.9579 14.3466 20.9382 13.9353C20.8691 12.4933 20.8198 11.0544 20.7926 9.60841C20.7848 9.1967 21.1123 8.85665 21.524 8.8489Z" fill="url(#paint2_linear_2942_1229)"/>
<path d="M15.4203 9.51724C15.6565 9.17999 15.5747 8.71506 15.2375 8.47877C14.9002 8.24249 14.4353 8.32434 14.199 8.66158C13.5874 9.53452 12.935 10.3942 12.2667 11.2747C12.0113 11.6113 11.7536 11.951 11.4949 12.2955C11.4948 12.2256 11.4945 12.1549 11.494 12.0833C11.4899 11.4759 11.4682 10.827 11.3511 10.1757C11.2781 9.77047 10.8905 9.50105 10.4852 9.57398C10.0799 9.64691 9.8105 10.0346 9.88343 10.4398C9.97721 10.9609 9.99884 11.5056 10.0029 12.0935C10.0039 12.2387 10.0037 12.3886 10.0036 12.5417C10.0032 12.9877 10.0028 13.4604 10.0303 13.919C10.0365 14.0218 10.0632 14.1185 10.1062 14.2053C9.39275 15.2317 8.72212 16.2913 8.16155 17.3909C8.15394 17.4058 8.14271 17.427 8.12867 17.4534C8.04301 17.6148 7.85275 17.9733 7.74077 18.3186C7.67701 18.5151 7.60594 18.7988 7.63944 19.0917C7.65733 19.2481 7.70803 19.4336 7.8297 19.6082C7.95607 19.7894 8.13073 19.9179 8.32815 19.9925C9.5403 20.4508 10.8812 20.4975 12.1498 20.4009C13.1799 20.3225 14.2197 20.1434 15.1461 19.9837C15.358 19.9472 15.5639 19.9117 15.7625 19.8787C16.1687 19.8111 16.4432 19.4271 16.3756 19.0209C16.3081 18.6147 15.924 18.3402 15.5178 18.4077C15.3021 18.4436 15.0842 18.4811 14.8647 18.5189C13.9419 18.6776 12.9891 18.8415 12.0367 18.914C10.998 18.9931 10.0276 18.956 9.18383 18.7076C9.25117 18.5246 9.34702 18.3419 9.42851 18.1866C9.45032 18.145 9.47115 18.1053 9.49008 18.0682C10.2409 16.5954 11.2184 15.1729 12.268 13.7535C12.6447 13.2441 13.035 12.7296 13.4266 12.2134C14.1092 11.3136 14.7956 10.4088 15.4203 9.51724Z" fill="#141414"/>
<path d="M15.4203 9.51724C15.6565 9.17999 15.5747 8.71506 15.2375 8.47877C14.9002 8.24249 14.4353 8.32434 14.199 8.66158C13.5874 9.53452 12.935 10.3942 12.2667 11.2747C12.0113 11.6113 11.7536 11.951 11.4949 12.2955C11.4948 12.2256 11.4945 12.1549 11.494 12.0833C11.4899 11.4759 11.4682 10.827 11.3511 10.1757C11.2781 9.77047 10.8905 9.50105 10.4852 9.57398C10.0799 9.64691 9.8105 10.0346 9.88343 10.4398C9.97721 10.9609 9.99884 11.5056 10.0029 12.0935C10.0039 12.2387 10.0037 12.3886 10.0036 12.5417C10.0032 12.9877 10.0028 13.4604 10.0303 13.919C10.0365 14.0218 10.0632 14.1185 10.1062 14.2053C9.39275 15.2317 8.72212 16.2913 8.16155 17.3909C8.15394 17.4058 8.14271 17.427 8.12867 17.4534C8.04301 17.6148 7.85275 17.9733 7.74077 18.3186C7.67701 18.5151 7.60594 18.7988 7.63944 19.0917C7.65733 19.2481 7.70803 19.4336 7.8297 19.6082C7.95607 19.7894 8.13073 19.9179 8.32815 19.9925C9.5403 20.4508 10.8812 20.4975 12.1498 20.4009C13.1799 20.3225 14.2197 20.1434 15.1461 19.9837C15.358 19.9472 15.5639 19.9117 15.7625 19.8787C16.1687 19.8111 16.4432 19.4271 16.3756 19.0209C16.3081 18.6147 15.924 18.3402 15.5178 18.4077C15.3021 18.4436 15.0842 18.4811 14.8647 18.5189C13.9419 18.6776 12.9891 18.8415 12.0367 18.914C10.998 18.9931 10.0276 18.956 9.18383 18.7076C9.25117 18.5246 9.34702 18.3419 9.42851 18.1866C9.45032 18.145 9.47115 18.1053 9.49008 18.0682C10.2409 16.5954 11.2184 15.1729 12.268 13.7535C12.6447 13.2441 13.035 12.7296 13.4266 12.2134C14.1092 11.3136 14.7956 10.4088 15.4203 9.51724Z" fill="url(#paint3_linear_2942_1229)"/>
<path d="M21.5709 21.534C21.6983 21.1424 21.484 20.7217 21.0924 20.5944C20.7008 20.467 20.2801 20.6813 20.1528 21.0729C19.9756 21.6179 19.7424 22.0319 19.4518 22.3169C19.1738 22.5896 18.8105 22.7774 18.2938 22.8258C17.5241 22.898 16.7434 22.5737 16.4029 22.0103C16.1898 21.6579 15.7315 21.545 15.3791 21.758C15.0267 21.971 14.9137 22.4294 15.1267 22.7818C15.8398 23.9614 17.2577 24.4207 18.4329 24.3105C19.2796 24.2311 19.9656 23.9017 20.496 23.3815C21.0138 22.8737 21.3481 22.2193 21.5709 21.534Z" fill="#141414"/>
<path d="M21.5709 21.534C21.6983 21.1424 21.484 20.7217 21.0924 20.5944C20.7008 20.467 20.2801 20.6813 20.1528 21.0729C19.9756 21.6179 19.7424 22.0319 19.4518 22.3169C19.1738 22.5896 18.8105 22.7774 18.2938 22.8258C17.5241 22.898 16.7434 22.5737 16.4029 22.0103C16.1898 21.6579 15.7315 21.545 15.3791 21.758C15.0267 21.971 14.9137 22.4294 15.1267 22.7818C15.8398 23.9614 17.2577 24.4207 18.4329 24.3105C19.2796 24.2311 19.9656 23.9017 20.496 23.3815C21.0138 22.8737 21.3481 22.2193 21.5709 21.534Z" fill="url(#paint4_linear_2942_1229)"/>
<defs>
<filter id="filter0_i_2942_1229" x="0" y="0" width="32" height="32" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>
<feGaussianBlur stdDeviation="0.49869"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_2942_1229"/>
</filter>
<filter id="filter1_ii_2942_1229" x="1.75" y="0.75" width="28.5" height="30.5" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="-1"/>
<feGaussianBlur stdDeviation="1"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.980392 0 0 0 0 0.309804 0 0 0 0 0.67451 0 0 0 0.2 0"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_2942_1229"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="1"/>
<feGaussianBlur stdDeviation="1"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.2 0 0 0 0 0.835294 0 0 0 0 1 0 0 0 0.15 0"/>
<feBlend mode="normal" in2="effect1_innerShadow_2942_1229" result="effect2_innerShadow_2942_1229"/>
</filter>
<linearGradient id="paint0_linear_2942_1229" x1="16" y1="0" x2="16" y2="32" gradientUnits="userSpaceOnUse">
<stop stop-color="#22CCEE"/>
<stop offset="0.274483" stop-color="#1570EF"/>
<stop offset="0.629793" stop-color="#6927DA"/>
<stop offset="1" stop-color="#F23E94"/>
</linearGradient>
<linearGradient id="paint1_linear_2942_1229" x1="16" y1="10.6562" x2="16" y2="30.25" gradientUnits="userSpaceOnUse">
<stop stop-color="#171717"/>
<stop offset="0.3" stop-color="#0B0B0B"/>
</linearGradient>
<linearGradient id="paint2_linear_2942_1229" x1="17.8739" y1="7.84186" x2="13.7328" y2="23.2967" gradientUnits="userSpaceOnUse">
<stop stop-color="#22CCEE"/>
<stop offset="0.274483" stop-color="#1570EF"/>
<stop offset="0.629793" stop-color="#6927DA"/>
<stop offset="1" stop-color="#F23E94"/>
</linearGradient>
<linearGradient id="paint3_linear_2942_1229" x1="17.8739" y1="7.84186" x2="13.7328" y2="23.2967" gradientUnits="userSpaceOnUse">
<stop stop-color="#22CCEE"/>
<stop offset="0.274483" stop-color="#1570EF"/>
<stop offset="0.629793" stop-color="#6927DA"/>
<stop offset="1" stop-color="#F23E94"/>
</linearGradient>
<linearGradient id="paint4_linear_2942_1229" x1="17.8739" y1="7.84186" x2="13.7328" y2="23.2967" gradientUnits="userSpaceOnUse">
<stop stop-color="#22CCEE"/>
<stop offset="0.274483" stop-color="#1570EF"/>
<stop offset="0.629793" stop-color="#6927DA"/>
<stop offset="1" stop-color="#F23E94"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 8.6 KiB

71
app/assets/images/ai.svg Normal file
View file

@ -0,0 +1,71 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" fill="none">
<g filter="url(#filter0_i_2942_1218)">
<rect width="32" height="32" rx="10" fill="url(#paint0_linear_2942_1218)"/>
<rect width="32" height="32" rx="10" fill="white" fill-opacity="0.07" style="mix-blend-mode:plus-lighter"/>
</g>
<g filter="url(#filter1_ii_2942_1218)">
<rect x="1.75" y="1.75" width="28.5" height="28.5" rx="8" fill="url(#paint1_linear_2942_1218)"/>
</g>
<path d="M21.524 8.8489C21.9357 8.84115 22.2757 9.16862 22.2835 9.58033C22.3104 11.0111 22.3592 12.4355 22.4277 13.8639C22.4474 14.2752 22.1299 14.6246 21.7186 14.6443C21.3073 14.664 20.9579 14.3466 20.9382 13.9353C20.8691 12.4933 20.8198 11.0544 20.7926 9.60841C20.7848 9.1967 21.1123 8.85665 21.524 8.8489Z" fill="#141414"/>
<path d="M21.524 8.8489C21.9357 8.84115 22.2757 9.16862 22.2835 9.58033C22.3104 11.0111 22.3592 12.4355 22.4277 13.8639C22.4474 14.2752 22.1299 14.6246 21.7186 14.6443C21.3073 14.664 20.9579 14.3466 20.9382 13.9353C20.8691 12.4933 20.8198 11.0544 20.7926 9.60841C20.7848 9.1967 21.1123 8.85665 21.524 8.8489Z" fill="url(#paint2_linear_2942_1218)"/>
<path d="M15.4203 9.51724C15.6565 9.17999 15.5747 8.71506 15.2375 8.47877C14.9002 8.24249 14.4353 8.32434 14.199 8.66158C13.5874 9.53452 12.935 10.3942 12.2667 11.2747C12.0113 11.6113 11.7536 11.951 11.4949 12.2955C11.4948 12.2256 11.4945 12.1549 11.494 12.0833C11.4899 11.4759 11.4682 10.827 11.3511 10.1757C11.2781 9.77047 10.8905 9.50105 10.4852 9.57398C10.0799 9.64691 9.8105 10.0346 9.88343 10.4398C9.97721 10.9609 9.99884 11.5056 10.0029 12.0935C10.0039 12.2387 10.0037 12.3886 10.0036 12.5417C10.0032 12.9877 10.0028 13.4604 10.0303 13.919C10.0365 14.0218 10.0632 14.1185 10.1062 14.2053C9.39275 15.2317 8.72212 16.2913 8.16155 17.3909C8.15394 17.4058 8.14271 17.427 8.12867 17.4534C8.04301 17.6148 7.85275 17.9733 7.74077 18.3186C7.67701 18.5151 7.60594 18.7988 7.63944 19.0917C7.65733 19.2481 7.70803 19.4336 7.8297 19.6082C7.95607 19.7894 8.13073 19.9179 8.32815 19.9925C9.5403 20.4508 10.8812 20.4975 12.1498 20.4009C13.1799 20.3225 14.2197 20.1434 15.1461 19.9837C15.358 19.9472 15.5639 19.9117 15.7625 19.8787C16.1687 19.8111 16.4432 19.4271 16.3756 19.0209C16.3081 18.6147 15.924 18.3402 15.5178 18.4077C15.3021 18.4436 15.0842 18.4811 14.8647 18.5189C13.9419 18.6776 12.9891 18.8415 12.0367 18.914C10.998 18.9931 10.0276 18.956 9.18383 18.7076C9.25117 18.5246 9.34702 18.3419 9.42851 18.1866C9.45032 18.145 9.47115 18.1053 9.49008 18.0682C10.2409 16.5954 11.2184 15.1729 12.268 13.7535C12.6447 13.2441 13.035 12.7296 13.4266 12.2134C14.1092 11.3136 14.7956 10.4088 15.4203 9.51724Z" fill="#141414"/>
<path d="M15.4203 9.51724C15.6565 9.17999 15.5747 8.71506 15.2375 8.47877C14.9002 8.24249 14.4353 8.32434 14.199 8.66158C13.5874 9.53452 12.935 10.3942 12.2667 11.2747C12.0113 11.6113 11.7536 11.951 11.4949 12.2955C11.4948 12.2256 11.4945 12.1549 11.494 12.0833C11.4899 11.4759 11.4682 10.827 11.3511 10.1757C11.2781 9.77047 10.8905 9.50105 10.4852 9.57398C10.0799 9.64691 9.8105 10.0346 9.88343 10.4398C9.97721 10.9609 9.99884 11.5056 10.0029 12.0935C10.0039 12.2387 10.0037 12.3886 10.0036 12.5417C10.0032 12.9877 10.0028 13.4604 10.0303 13.919C10.0365 14.0218 10.0632 14.1185 10.1062 14.2053C9.39275 15.2317 8.72212 16.2913 8.16155 17.3909C8.15394 17.4058 8.14271 17.427 8.12867 17.4534C8.04301 17.6148 7.85275 17.9733 7.74077 18.3186C7.67701 18.5151 7.60594 18.7988 7.63944 19.0917C7.65733 19.2481 7.70803 19.4336 7.8297 19.6082C7.95607 19.7894 8.13073 19.9179 8.32815 19.9925C9.5403 20.4508 10.8812 20.4975 12.1498 20.4009C13.1799 20.3225 14.2197 20.1434 15.1461 19.9837C15.358 19.9472 15.5639 19.9117 15.7625 19.8787C16.1687 19.8111 16.4432 19.4271 16.3756 19.0209C16.3081 18.6147 15.924 18.3402 15.5178 18.4077C15.3021 18.4436 15.0842 18.4811 14.8647 18.5189C13.9419 18.6776 12.9891 18.8415 12.0367 18.914C10.998 18.9931 10.0276 18.956 9.18383 18.7076C9.25117 18.5246 9.34702 18.3419 9.42851 18.1866C9.45032 18.145 9.47115 18.1053 9.49008 18.0682C10.2409 16.5954 11.2184 15.1729 12.268 13.7535C12.6447 13.2441 13.035 12.7296 13.4266 12.2134C14.1092 11.3136 14.7956 10.4088 15.4203 9.51724Z" fill="url(#paint3_linear_2942_1218)"/>
<path d="M21.5709 21.534C21.6983 21.1424 21.484 20.7217 21.0924 20.5944C20.7008 20.467 20.2801 20.6813 20.1528 21.0729C19.9756 21.6179 19.7424 22.0319 19.4518 22.3169C19.1738 22.5896 18.8105 22.7774 18.2938 22.8258C17.5241 22.898 16.7434 22.5737 16.4029 22.0103C16.1898 21.6579 15.7315 21.545 15.3791 21.758C15.0267 21.971 14.9137 22.4294 15.1267 22.7818C15.8398 23.9614 17.2577 24.4207 18.4329 24.3105C19.2796 24.2311 19.9656 23.9017 20.496 23.3815C21.0138 22.8737 21.3481 22.2193 21.5709 21.534Z" fill="#141414"/>
<path d="M21.5709 21.534C21.6983 21.1424 21.484 20.7217 21.0924 20.5944C20.7008 20.467 20.2801 20.6813 20.1528 21.0729C19.9756 21.6179 19.7424 22.0319 19.4518 22.3169C19.1738 22.5896 18.8105 22.7774 18.2938 22.8258C17.5241 22.898 16.7434 22.5737 16.4029 22.0103C16.1898 21.6579 15.7315 21.545 15.3791 21.758C15.0267 21.971 14.9137 22.4294 15.1267 22.7818C15.8398 23.9614 17.2577 24.4207 18.4329 24.3105C19.2796 24.2311 19.9656 23.9017 20.496 23.3815C21.0138 22.8737 21.3481 22.2193 21.5709 21.534Z" fill="url(#paint4_linear_2942_1218)"/>
<defs>
<filter id="filter0_i_2942_1218" x="0" y="0" width="32" height="32" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>
<feGaussianBlur stdDeviation="0.49869"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_2942_1218"/>
</filter>
<filter id="filter1_ii_2942_1218" x="1.75" y="0.861111" width="28.5" height="30.2778" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="-0.888889"/>
<feGaussianBlur stdDeviation="0.888889"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.980392 0 0 0 0 0.309804 0 0 0 0 0.67451 0 0 0 0.1 0"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_2942_1218"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="0.888889"/>
<feGaussianBlur stdDeviation="0.888889"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.2 0 0 0 0 0.835294 0 0 0 0 1 0 0 0 0.1 0"/>
<feBlend mode="normal" in2="effect1_innerShadow_2942_1218" result="effect2_innerShadow_2942_1218"/>
</filter>
<linearGradient id="paint0_linear_2942_1218" x1="16" y1="0" x2="16" y2="32" gradientUnits="userSpaceOnUse">
<stop stop-color="#22CCEE"/>
<stop offset="0.274483" stop-color="#1570EF"/>
<stop offset="0.629793" stop-color="#6927DA"/>
<stop offset="1" stop-color="#F23E94"/>
</linearGradient>
<linearGradient id="paint1_linear_2942_1218" x1="16" y1="10.6562" x2="16" y2="30.25" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="0.3" stop-color="#F7F7F7"/>
</linearGradient>
<linearGradient id="paint2_linear_2942_1218" x1="17.8739" y1="7.84186" x2="13.7328" y2="23.2967" gradientUnits="userSpaceOnUse">
<stop stop-color="#22CCEE"/>
<stop offset="0.274483" stop-color="#1570EF"/>
<stop offset="0.629793" stop-color="#6927DA"/>
<stop offset="1" stop-color="#F23E94"/>
</linearGradient>
<linearGradient id="paint3_linear_2942_1218" x1="17.8739" y1="7.84186" x2="13.7328" y2="23.2967" gradientUnits="userSpaceOnUse">
<stop stop-color="#22CCEE"/>
<stop offset="0.274483" stop-color="#1570EF"/>
<stop offset="0.629793" stop-color="#6927DA"/>
<stop offset="1" stop-color="#F23E94"/>
</linearGradient>
<linearGradient id="paint4_linear_2942_1218" x1="17.8739" y1="7.84186" x2="13.7328" y2="23.2967" gradientUnits="userSpaceOnUse">
<stop stop-color="#22CCEE"/>
<stop offset="0.274483" stop-color="#1570EF"/>
<stop offset="0.629793" stop-color="#6927DA"/>
<stop offset="1" stop-color="#F23E94"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 8.7 KiB

View file

@ -0,0 +1,37 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.75" y="0.75" width="18.5" height="18.5" rx="5.25"
class="gradient-fill"
fill="url(#paint0_linear_2046_1939)" />
<rect x="0.75" y="0.75" width="18.5" height="18.5" rx="5.25" stroke="currentColor" stroke-width="1.5" />
<path
d="M13.166 5.78146C13.4233 5.77662 13.6358 5.98129 13.6407 6.2386C13.6575 7.13281 13.688 8.02308 13.7308 8.91583C13.7431 9.1729 13.5447 9.39128 13.2876 9.40361C13.0306 9.41593 12.8122 9.21753 12.7999 8.96046C12.7567 8.05922 12.7259 7.1599 12.7089 6.25615C12.704 5.99883 12.9087 5.78631 13.166 5.78146Z"
fill="currentColor" />
<path
d="M9.35116 6.19917C9.49883 5.98839 9.44768 5.69781 9.2369 5.55013C9.02612 5.40246 8.73554 5.45361 8.58786 5.66439C8.20561 6.20997 7.79785 6.74728 7.3802 7.29762C7.22057 7.50796 7.05946 7.72025 6.89782 7.93558C6.89774 7.8919 6.89758 7.84771 6.89727 7.80294C6.89466 7.42332 6.88115 7.01776 6.8079 6.61074C6.76232 6.35744 6.52004 6.18906 6.26674 6.23464C6.01345 6.28022 5.84506 6.5225 5.89064 6.7758C5.94925 7.10149 5.96277 7.44189 5.9653 7.80935C5.96592 7.90008 5.96583 7.9938 5.96575 8.08947C5.96549 8.36824 5.96523 8.66365 5.98243 8.95029C5.98629 9.01454 6.00299 9.07497 6.02989 9.12923C5.58396 9.77068 5.16482 10.433 4.81446 11.1202C4.8097 11.1296 4.80269 11.1428 4.79392 11.1593C4.74038 11.2602 4.62146 11.4842 4.55147 11.7C4.51162 11.8229 4.46721 12.0002 4.48814 12.1832C4.49932 12.281 4.53101 12.3969 4.60706 12.506C4.68604 12.6193 4.7952 12.6996 4.91859 12.7462C5.67618 13.0326 6.51425 13.0618 7.30714 13.0015C7.95092 12.9525 8.60078 12.8405 9.17979 12.7407C9.31222 12.7179 9.44094 12.6957 9.56504 12.6751C9.81891 12.6329 9.99049 12.3928 9.94826 12.1389C9.90603 11.8851 9.66599 11.7135 9.41211 11.7557C9.27728 11.7782 9.14113 11.8016 9.00391 11.8252C8.42721 11.9244 7.83168 12.0269 7.2364 12.0722C6.58727 12.1216 5.98075 12.0984 5.45339 11.9432C5.49547 11.8288 5.55538 11.7146 5.60631 11.6175C5.61995 11.5915 5.63296 11.5667 5.64479 11.5435C6.11404 10.623 6.72498 9.73396 7.38096 8.84686C7.61644 8.52843 7.86038 8.20687 8.10509 7.8843C8.53175 7.3219 8.96074 6.75642 9.35116 6.19917Z"
fill="currentColor" />
<path
d="M13.1953 13.7096C13.2749 13.4649 13.141 13.2019 12.8962 13.1224C12.6515 13.0428 12.3886 13.1767 12.309 13.4215C12.1983 13.7621 12.0525 14.0208 11.8709 14.199C11.6971 14.3694 11.4701 14.4868 11.1471 14.517C10.6661 14.5621 10.1781 14.3594 9.96528 14.0074C9.83214 13.7871 9.54566 13.7165 9.32541 13.8496C9.10516 13.9828 9.03455 14.2693 9.16769 14.4895C9.61336 15.2268 10.4996 15.5138 11.2341 15.445C11.7632 15.3954 12.192 15.1895 12.5235 14.8644C12.8471 14.547 13.0561 14.1379 13.1953 13.7096Z"
fill="currentColor" />
<path
d="M13.166 5.78146C13.4233 5.77662 13.6358 5.98129 13.6407 6.2386C13.6575 7.13281 13.688 8.02308 13.7308 8.91583C13.7431 9.1729 13.5447 9.39128 13.2876 9.40361C13.0306 9.41593 12.8122 9.21753 12.7999 8.96046C12.7567 8.05922 12.7259 7.1599 12.7089 6.25615C12.704 5.99883 12.9087 5.78631 13.166 5.78146Z"
stroke="currentColor" stroke-width="0.3" stroke-linecap="round" />
<path
d="M9.35116 6.19917C9.49883 5.98839 9.44768 5.69781 9.2369 5.55013C9.02612 5.40246 8.73554 5.45361 8.58786 5.66439C8.20561 6.20997 7.79785 6.74728 7.3802 7.29762C7.22057 7.50796 7.05946 7.72025 6.89782 7.93558C6.89774 7.8919 6.89758 7.84771 6.89727 7.80294C6.89466 7.42332 6.88115 7.01776 6.8079 6.61074C6.76232 6.35744 6.52004 6.18906 6.26674 6.23464C6.01345 6.28022 5.84506 6.5225 5.89064 6.7758C5.94925 7.10149 5.96277 7.44189 5.9653 7.80935C5.96592 7.90008 5.96583 7.9938 5.96575 8.08947C5.96549 8.36824 5.96523 8.66365 5.98243 8.95029C5.98629 9.01454 6.00299 9.07497 6.02989 9.12923C5.58396 9.77068 5.16482 10.433 4.81446 11.1202C4.8097 11.1296 4.80269 11.1428 4.79392 11.1593C4.74038 11.2602 4.62146 11.4842 4.55147 11.7C4.51162 11.8229 4.46721 12.0002 4.48814 12.1832C4.49932 12.281 4.53101 12.3969 4.60706 12.506C4.68604 12.6193 4.7952 12.6996 4.91859 12.7462C5.67618 13.0326 6.51425 13.0618 7.30714 13.0015C7.95092 12.9525 8.60078 12.8405 9.17979 12.7407C9.31222 12.7179 9.44094 12.6957 9.56504 12.6751C9.81891 12.6329 9.99049 12.3928 9.94826 12.1389C9.90603 11.8851 9.66599 11.7135 9.41211 11.7557C9.27728 11.7782 9.14113 11.8016 9.00391 11.8252C8.42721 11.9244 7.83168 12.0269 7.2364 12.0722C6.58727 12.1216 5.98075 12.0984 5.45339 11.9432C5.49547 11.8288 5.55538 11.7146 5.60631 11.6175C5.61995 11.5915 5.63296 11.5667 5.64479 11.5435C6.11404 10.623 6.72498 9.73396 7.38096 8.84686C7.61644 8.52843 7.86038 8.20687 8.10509 7.8843C8.53175 7.3219 8.96074 6.75642 9.35116 6.19917Z"
stroke="currentColor" stroke-width="0.3" stroke-linecap="round" />
<path
d="M13.1953 13.7096C13.2749 13.4649 13.141 13.2019 12.8962 13.1224C12.6515 13.0428 12.3886 13.1767 12.309 13.4215C12.1983 13.7621 12.0525 14.0208 11.8709 14.199C11.6971 14.3694 11.4701 14.4868 11.1471 14.517C10.6661 14.5621 10.1781 14.3594 9.96528 14.0074C9.83214 13.7871 9.54566 13.7165 9.32541 13.8496C9.10516 13.9828 9.03455 14.2693 9.16769 14.4895C9.61336 15.2268 10.4996 15.5138 11.2341 15.445C11.7632 15.3954 12.192 15.1895 12.5235 14.8644C12.8471 14.547 13.0561 14.1379 13.1953 13.7096Z"
stroke="currentColor" stroke-width="0.3" stroke-linecap="round" />
<style>
[data-theme=dark] .gradient-fill {
fill: transparent;
}
</style>
<defs>
<linearGradient id="paint0_linear_2046_1939" x1="10" y1="6.25" x2="10" y2="20"
gradientUnits="userSpaceOnUse">
<stop stop-color="white" />
<stop offset="0.3" stop-color="#F7F7F7" />
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.6 KiB

View file

@ -0,0 +1,5 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M2.5 7.5H17.5M2.5 12.5H17.5M7.5 7.5V17.5M12.5 7.5V17.5M4.16667 2.5H15.8333C16.7538 2.5 17.5 3.24619 17.5 4.16667V15.8333C17.5 16.7538 16.7538 17.5 15.8333 17.5H4.16667C3.24619 17.5 2.5 16.7538 2.5 15.8333V4.16667C2.5 3.24619 3.24619 2.5 4.16667 2.5Z"
stroke="#737373" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>

After

Width:  |  Height:  |  Size: 468 B

View file

@ -1 +0,0 @@
/* Application styles */

View file

@ -8,10 +8,38 @@
@plugin "@tailwindcss/typography";
@plugin "@tailwindcss/forms";
@import "./simonweb_pickr.css";
@layer components {
.pcr-app{
position: static !important;
background: none !important;
box-shadow: none !important;
padding: 0 !important;
width: 100% !important;
}
.pcr-color-palette{
height: 12em !important;
}
.pcr-palette{
border-radius: 10px !important;
}
.pcr-palette:before{
border-radius: 10px !important;
}
.pcr-color-chooser{
height: 1.5em !important;
}
.pcr-picker{
height: 20px !important;
width: 20px !important;
}
}
.combobox {
.hw-combobox__main__wrapper,
.hw-combobox__input {
@apply w-full;
@apply bg-container text-primary w-full;
}
.hw-combobox__main__wrapper {
@ -25,6 +53,10 @@
.hw-combobox__label {
@apply block text-xs text-gray-500 peer-disabled:text-gray-400;
}
.hw-combobox__option {
@apply bg-container hover:bg-container-hover;
}
.hw_combobox__pagination__wrapper {
@apply h-px;
@ -42,18 +74,22 @@
/* Typography */
.prose {
@apply max-w-none;
@apply max-w-none text-primary;
a {
@apply text-link;
}
h2 {
@apply text-xl font-medium;
@apply text-xl font-medium text-primary;
}
h3 {
@apply text-lg font-medium;
@apply text-lg font-medium text-primary;
}
li {
@apply m-0;
@apply m-0 text-primary;
}
details {
@ -83,6 +119,30 @@
}
}
.prose--ai-chat {
@apply break-words;
p, li {
@apply text-sm text-primary;
}
scrollbar-width: thin;
scrollbar-color: rgba(156, 163, 175, 0.5) transparent;
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background-color: rgba(156, 163, 175, 0.5);
border-radius: 3px;
}
}
/* Custom scrollbar implementation for Windows browsers */
.windows {
::-webkit-scrollbar {

View file

@ -5,6 +5,14 @@
One-off styling (3rd party overrides, etc.) should be done in the application.css file.
*/
@import './maybe-design-system/background-utils.css';
@import './maybe-design-system/foreground-utils.css';
@import './maybe-design-system/text-utils.css';
@import './maybe-design-system/border-utils.css';
@import './maybe-design-system/component-utils.css';
@custom-variant theme-dark (&:where([data-theme=dark], [data-theme=dark] *));
@theme {
/* Font families */
--font-sans: 'Geist', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
@ -16,6 +24,12 @@
--color-success: var(--color-green-600);
--color-warning: var(--color-yellow-600);
--color-destructive: var(--color-red-600);
--color-shadow: --alpha(var(--color-black) / 6%);
/* Colors used in Stimulus controllers with SVGs (easier to define light/dark mode here than toggle within the controllers) */
/* See @layer base block below for dark mode overrides */
--budget-unused-fill: var(--color-gray-200);
--budget-unallocated-fill: var(--color-gray-50);
/* Gray scale */
--color-gray-25: #FAFAFA;
@ -215,201 +229,166 @@
--shadow-md: 0px 4px 8px -2px --alpha(var(--color-black) / 6%);
--shadow-lg: 0px 12px 16px -4px --alpha(var(--color-black) / 6%);
--shadow-xl: 0px 20px 24px -4px --alpha(var(--color-black) / 6%);
--animate-stroke-fill: stroke-fill 3s 300ms forwards;
@keyframes stroke-fill {
0% {
stroke-dashoffset: 43.9822971503;
}
100% {
stroke-dashoffset: 0;
}
}
}
/* Custom shadow borders used for surfaces / containers */
@utility shadow-border-xs {
box-shadow: var(--shadow-xs), 0px 0px 0px 1px var(--color-alpha-black-50);
}
@utility shadow-border-sm {
box-shadow: var(--shadow-sm), 0px 0px 0px 1px var(--color-alpha-black-50);
}
@utility shadow-border-md {
box-shadow: var(--shadow-md), 0px 0px 0px 1px var(--color-alpha-black-50);
}
@utility shadow-border-lg {
box-shadow: var(--shadow-lg), 0px 0px 0px 1px var(--color-alpha-black-50);
}
@utility shadow-border-xl {
box-shadow: var(--shadow-xl), 0px 0px 0px 1px var(--color-alpha-black-50);
}
/* Design system color utilities */
@utility text-primary {
@apply text-gray-900;
}
@utility text-secondary {
@apply text-gray-500;
}
@utility text-subdued {
@apply text-gray-400;
}
@utility text-link {
@apply text-blue-600;
}
@utility bg-surface {
@apply bg-gray-50;
}
@utility bg-surface-hover {
@apply bg-gray-100;
}
@utility bg-surface-inset {
@apply bg-gray-100;
}
@utility bg-surface-inset-hover {
@apply bg-gray-200;
}
@utility bg-container {
@apply bg-white;
}
@utility bg-container-hover {
@apply bg-gray-50;
}
@utility bg-container-inset {
@apply bg-gray-50;
}
@utility bg-container-inset-hover {
@apply bg-gray-100;
}
@utility bg-inverse {
@apply bg-gray-800;
}
@utility bg-inverse-hover {
@apply bg-gray-700;
}
@utility bg-overlay {
@apply bg-alpha-black-200;
}
@utility border-primary {
@apply border-alpha-black-300;
}
@utility border-secondary {
@apply border-alpha-black-200;
}
@utility border-tertiary {
@apply border-alpha-black-100;
}
@utility border-subdued {
@apply border-alpha-black-50;
/* Specific override for strong tags in prose under dark mode */
.prose:where([data-theme=dark], [data-theme=dark] *) strong {
color: theme(colors.white) !important;
}
@layer base {
form>button {
@apply cursor-pointer;
[data-theme="dark"] {
--color-success: var(--color-green-500);
--color-warning: var(--color-yellow-400);
--color-destructive: var(--color-red-400);
--color-shadow: --alpha(var(--color-white) / 8%);
/* Dark mode overrides for colors used in Stimulus controllers with SVGs */
--budget-unused-fill: var(--color-gray-500);
--budget-unallocated-fill: var(--color-gray-700);
--shadow-xs: 0px 1px 2px 0px --alpha(var(--color-white) / 8%);
--shadow-sm: 0px 1px 6px 0px --alpha(var(--color-white) / 8%);
--shadow-md: 0px 4px 8px -2px --alpha(var(--color-white) / 8%);
--shadow-lg: 0px 12px 16px -4px --alpha(var(--color-white) / 8%);
--shadow-xl: 0px 20px 24px -4px --alpha(var(--color-white) / 8%);
}
html {
padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom);
}
button {
@apply cursor-pointer focus-visible:outline-gray-900;
}
hr {
@apply text-gray-200;
}
/* We control the sizing through DialogComponent, so reset this value */
dialog:modal {
max-width: 100dvw;
max-height: 100dvh;
}
details>summary::-webkit-details-marker {
@apply hidden;
}
details>summary {
@apply list-none;
}
select[multiple="multiple"] {
@apply py-2 pr-2 space-y-0.5 overflow-y-auto;
}
select[multiple="multiple"] option {
@apply py-2 rounded-md;
}
select[multiple="multiple"] option:checked {
@apply after:content-['\2713'] bg-white after:text-gray-500 after:ml-2;
}
select[multiple="multiple"] option:active,
select[multiple="multiple"] option:focus {
@apply bg-white;
input[type='radio'] {
@apply border-gray-300 text-indigo-600 focus:ring-indigo-600;
/* Default light mode */
@variant theme-dark {
/* Dark mode radio button base and checked styles */
@apply border-gray-600 bg-gray-700 checked:bg-blue-500 focus:ring-blue-500 focus:ring-offset-gray-800;
}
}
}
@layer components {
/* Buttons */
.btn {
@apply px-3 py-2 rounded-lg text-sm font-medium cursor-pointer disabled:cursor-not-allowed focus:outline-gray-500;
}
.btn--primary {
@apply bg-gray-900 text-white hover:bg-gray-700 disabled:bg-gray-50 disabled:hover:bg-gray-50 disabled:text-gray-400;
}
.btn--secondary {
@apply bg-gray-50 hover:bg-gray-100 text-gray-900;
}
.btn--outline {
@apply border border-alpha-black-200 text-gray-900 hover:bg-gray-50 disabled:bg-gray-50 disabled:hover:bg-gray-50 disabled:text-gray-400;
}
.btn--ghost {
@apply border border-transparent text-gray-900 hover:bg-gray-100;
}
.btn--destructive {
@apply bg-red-500 text-white hover:bg-red-600 disabled:bg-red-50 disabled:hover:bg-red-50 disabled:text-red-400;
}
/* Forms */
.form-field {
@apply flex flex-col gap-1 relative px-3 py-2 rounded-md border bg-white border-alpha-black-100 shadow-xs w-full;
@apply focus-within:border-gray-900 focus-within:shadow-none focus-within:ring-4 focus-within:ring-gray-100;
@apply flex flex-col gap-1 relative px-3 py-2 rounded-md border bg-container border-secondary shadow-xs w-full;
@apply focus-within:border-secondary focus-within:shadow-none focus-within:ring-4 focus-within:ring-alpha-black-200;
@apply transition-all duration-300;
@variant theme-dark {
@apply focus-within:ring-alpha-white-300;
}
/* Add styles for multiple select within form fields */
select[multiple] {
@apply py-2 pr-2 space-y-0.5 overflow-y-auto;
option {
@apply py-2 rounded-md;
}
option:checked {
@apply after:content-['\2713'] bg-container-inset after:text-gray-500 after:ml-2;
}
option:active,
option:focus {
@apply bg-container-inset;
}
}
}
/* New form field structure components */
.form-field__header {
@apply flex items-center justify-between gap-2;
}
.form-field__body {
@apply flex flex-col gap-1;
}
.form-field__actions {
@apply flex items-center gap-1;
}
.form-field__label {
@apply block text-xs text-gray-500 peer-disabled:text-gray-400;
@apply block text-xs text-secondary peer-disabled:text-subdued;
}
.form-field__input {
@apply border-none bg-transparent text-sm opacity-100 w-full p-0;
@apply text-primary border-none bg-container text-sm opacity-100 w-full p-0;
@apply focus:opacity-100 focus:outline-hidden focus:ring-0;
@apply placeholder-shown:opacity-50;
@apply disabled:text-gray-400;
@apply disabled:text-subdued;
@apply text-ellipsis overflow-hidden whitespace-nowrap;
&select {
@apply pr-8;
@apply transition-opacity duration-300;
@apply placeholder:text-subdued;
@variant theme-dark {
&::-webkit-calendar-picker-indicator {
filter: invert(1);
cursor: pointer;
}
}
}
.form-field__radio {
@apply text-gray-900;
select.form-field__input {
@apply pr-10 appearance-none;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
background-position: right -0.15rem center;
background-repeat: no-repeat;
background-size: 1.25rem 1.25rem;
}
.form-field__radio {
@apply text-primary;
}
.form-field__submit {
@apply cursor-pointer rounded-lg bg-black p-3 text-center text-white hover:bg-gray-700;
@apply cursor-pointer rounded-lg bg-surface p-3 text-center text-white hover:bg-surface-hover;
}
/* Checkboxes */
.checkbox {
&[type='checkbox'] {
@apply rounded-sm;
@apply transition-colors duration-300;
}
}
@ -417,35 +396,51 @@
&[type='checkbox'] {
@apply border-alpha-black-200 checked:bg-gray-900 checked:ring-gray-900 focus:ring-gray-900 focus-visible:ring-gray-900 checked:hover:bg-gray-500;
}
&[type='checkbox']:disabled {
@apply cursor-not-allowed opacity-80 bg-gray-50 border-gray-200 checked:bg-gray-400 checked:ring-gray-400;
}
}
@variant theme-dark {
&[type='checkbox'] {
@apply ring-gray-900 checked:text-white;
background-color: var(--color-gray-600);
}
&[type='checkbox']:disabled {
@apply cursor-not-allowed opacity-80 ring-gray-600;
}
&[type='checkbox']:checked {
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e");
background-color: var(--color-gray-600);
}
}
}
.checkbox--dark {
&[type='checkbox'] {
@apply ring-gray-900 checked:text-white;
}
&[type='checkbox']:disabled {
@apply cursor-not-allowed opacity-80 ring-gray-600;
}
&[type='checkbox']:checked {
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='111827' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e");
}
}
/* Switches */
.switch {
@apply block bg-gray-100 w-9 h-5 rounded-full cursor-pointer;
@apply after:content-[''] after:block after:absolute after:top-0.5 after:left-0.5 after:bg-white after:w-4 after:h-4 after:rounded-full after:transition-transform after:duration-300 after:ease-in-out;
@apply peer-checked:bg-green-600 peer-checked:after:translate-x-4;
}
/* Tooltips */
.tooltip {
@apply hidden absolute;
}
.qrcode svg path {
fill: var(--color-black);
@variant theme-dark {
fill: var(--color-white);
}
}
}

View file

@ -0,0 +1,91 @@
@utility bg-surface {
@apply bg-gray-50;
@variant theme-dark {
@apply bg-black;
}
}
@utility bg-surface-hover {
@apply bg-gray-100;
@variant theme-dark {
@apply bg-gray-900;
}
}
@utility bg-surface-inset {
@apply bg-gray-100;
@variant theme-dark {
@apply bg-gray-800;
}
}
@utility bg-surface-inset-hover {
@apply bg-gray-200;
@variant theme-dark {
@apply bg-gray-800;
}
}
@utility bg-container {
@apply bg-white;
@variant theme-dark {
@apply bg-gray-900;
}
}
@utility bg-container-hover {
@apply bg-gray-50;
@variant theme-dark {
@apply bg-gray-800;
}
}
@utility bg-container-inset {
@apply bg-gray-50;
@variant theme-dark {
@apply bg-gray-800;
}
}
@utility bg-container-inset-hover {
@apply bg-gray-100;
@variant theme-dark {
@apply bg-gray-700;
}
}
@utility bg-inverse {
@apply bg-gray-800;
@variant theme-dark {
@apply bg-white;
}
}
@utility bg-inverse-hover {
@apply bg-gray-700;
@variant theme-dark {
@apply bg-gray-100;
}
}
@utility bg-overlay {
background-color: --alpha(var(--color-gray-100) / 50%);
@variant theme-dark {
background-color: var(--color-alpha-black-900);
}
}
@utility bg-loader {
@apply bg-surface-inset animate-pulse;
}

View file

@ -0,0 +1,92 @@
/* Custom shadow borders used for surfaces / containers */
@utility shadow-border-xs {
box-shadow: var(--shadow-xs), 0px 0px 0px 1px var(--color-alpha-black-50);
@variant theme-dark {
box-shadow: var(--shadow-xs), 0px 0px 0px 1px var(--color-alpha-white-50);
}
}
@utility shadow-border-sm {
box-shadow: var(--shadow-sm), 0px 0px 0px 1px var(--color-alpha-black-50);
@variant theme-dark {
box-shadow: var(--shadow-sm), 0px 0px 0px 1px var(--color-alpha-white-50);
}
}
@utility shadow-border-md {
box-shadow: var(--shadow-md), 0px 0px 0px 1px var(--color-alpha-black-50);
@variant theme-dark {
box-shadow: var(--shadow-md), 0px 0px 0px 1px var(--color-alpha-white-50);
}
}
@utility shadow-border-lg {
box-shadow: var(--shadow-lg), 0px 0px 0px 1px var(--color-alpha-black-50);
@variant theme-dark {
box-shadow: var(--shadow-lg), 0px 0px 0px 1px var(--color-alpha-white-50);
}
}
@utility shadow-border-xl {
box-shadow: var(--shadow-xl), 0px 0px 0px 1px var(--color-alpha-black-50);
@variant theme-dark {
box-shadow: var(--shadow-xl), 0px 0px 0px 1px var(--color-alpha-white-50);
}
}
@utility border-primary {
@apply border-alpha-black-300;
@variant theme-dark {
@apply border-alpha-white-400;
}
}
@utility border-secondary {
@apply border-alpha-black-200;
@variant theme-dark {
@apply border-alpha-white-300;
}
}
@utility border-tertiary {
@apply border-alpha-black-100;
@variant theme-dark {
@apply border-alpha-white-200;
}
}
@utility border-divider {
@apply border-tertiary;
}
@utility border-subdued {
@apply border-alpha-black-50;
@variant theme-dark {
@apply border-alpha-white-100;
}
}
@utility border-solid {
@apply border-black;
@variant theme-dark {
@apply border-white;
}
}
@utility border-destructive {
@apply border-red-500;
@variant theme-dark {
@apply border-red-400;
}
}

View file

@ -0,0 +1,109 @@
/* Button Backgrounds */
@utility button-bg-primary {
@apply bg-gray-900;
/* Maps to fg-primary light */
@variant theme-dark {
@apply bg-white;
/* Maps to fg-primary dark */
}
}
@utility button-bg-primary-hover {
@apply bg-gray-800;
/* Maps to fg-primary-variant light */
@variant theme-dark {
@apply bg-gray-50;
/* Maps to fg-primary-variant dark */
}
}
@utility button-bg-secondary {
@apply bg-gray-50; /* Maps to fg-secondary light */
@variant theme-dark {
@apply bg-gray-700; /* Maps to fg-secondary dark */
}
}
@utility button-bg-secondary-hover {
@apply bg-gray-100; /* Maps to fg-secondary-variant light */
@variant theme-dark {
@apply bg-gray-600; /* Maps to fg-secondary-variant dark */
}
}
@utility button-bg-disabled {
@apply bg-gray-50;
@variant theme-dark {
@apply bg-gray-700;
}
}
@utility button-bg-destructive {
@apply bg-red-500;
@variant theme-dark {
@apply bg-red-400;
}
}
@utility button-bg-destructive-hover {
@apply bg-red-600;
@variant theme-dark {
@apply bg-red-500;
}
}
@utility button-bg-ghost-hover {
@apply bg-gray-50;
@variant theme-dark {
@apply bg-gray-800 fg-inverse;
}
}
@utility button-bg-outline-hover {
@apply bg-gray-100;
@variant theme-dark {
@apply bg-gray-700;
}
}
/* Tab Styles */
@utility tab-item-active {
@apply bg-white;
@variant theme-dark {
@apply bg-gray-700;
}
}
@utility tab-item-hover {
@apply bg-gray-200;
@variant theme-dark {
@apply bg-gray-800;
}
}
@utility tab-bg-group {
@apply bg-gray-50;
@variant theme-dark {
@apply bg-alpha-black-700;
}
}
@utility bg-nav-indicator {
@apply bg-black;
@variant theme-dark {
@apply bg-white;
}
}

View file

@ -0,0 +1,63 @@
@utility fg-gray {
@apply text-gray-500;
@variant theme-dark {
@apply text-gray-400;
}
}
@utility fg-contrast {
@apply text-gray-400;
@variant theme-dark {
@apply text-gray-500;
}
}
@utility fg-inverse {
@apply text-white;
@variant theme-dark {
@apply text-gray-900;
}
}
@utility fg-primary {
@apply text-gray-900;
@variant theme-dark {
@apply text-white;
}
}
@utility fg-primary-variant {
@apply text-gray-800;
@variant theme-dark {
@apply text-gray-50;
}
}
@utility fg-secondary {
@apply text-gray-50;
@variant theme-dark {
@apply text-gray-700;
}
}
@utility fg-secondary-variant {
@apply text-gray-100;
@variant theme-dark {
@apply text-gray-600;
}
}
@utility fg-subdued {
@apply text-gray-400;
@variant theme-dark {
@apply text-gray-500;
}
}

View file

@ -0,0 +1,39 @@
@utility text-primary {
@apply text-gray-900;
@variant theme-dark {
@apply text-white;
}
}
@utility text-inverse {
@apply text-white;
@variant theme-dark {
@apply text-gray-900;
}
}
@utility text-secondary {
@apply text-gray-500;
@variant theme-dark {
@apply text-gray-400;
}
}
@utility text-subdued {
@apply text-gray-400;
@variant theme-dark {
@apply text-gray-600;
}
}
@utility text-link {
@apply text-blue-600;
@variant theme-dark {
@apply text-blue-500;
}
}

File diff suppressed because one or more lines are too long

View file

@ -1,4 +1,10 @@
module ApplicationCable
class Connection < ActionCable::Connection::Base
rescue_from StandardError, with: :report_error
private
def report_error(e)
Sentry.capture_exception(e)
end
end
end

View file

@ -0,0 +1,7 @@
<div class="<%= container_classes %>">
<%= helpers.icon icon_name, size: "sm", color: icon_color, class: "shrink-0" %>
<div class="flex-1 text-sm">
<%= message %>
</div>
</div>

View file

@ -0,0 +1,52 @@
class DS::Alert < DesignSystemComponent
def initialize(message:, variant: :info)
@message = message
@variant = variant
end
private
attr_reader :message, :variant
def container_classes
base_classes = "flex items-start gap-3 p-4 rounded-lg border"
variant_classes = case variant
when :info
"bg-blue-50 text-blue-700 border-blue-200 theme-dark:bg-blue-900/20 theme-dark:text-blue-400 theme-dark:border-blue-800"
when :success
"bg-green-50 text-green-700 border-green-200 theme-dark:bg-green-900/20 theme-dark:text-green-400 theme-dark:border-green-800"
when :warning
"bg-yellow-50 text-yellow-700 border-yellow-200 theme-dark:bg-yellow-900/20 theme-dark:text-yellow-400 theme-dark:border-yellow-800"
when :error, :destructive
"bg-red-50 text-red-700 border-red-200 theme-dark:bg-red-900/20 theme-dark:text-red-400 theme-dark:border-red-800"
end
"#{base_classes} #{variant_classes}"
end
def icon_name
case variant
when :info
"info"
when :success
"check-circle"
when :warning
"alert-triangle"
when :error, :destructive
"x-circle"
end
end
def icon_color
case variant
when :success
"success"
when :warning
"warning"
when :error, :destructive
"destructive"
else
"blue-600"
end
end
end

View file

@ -0,0 +1,13 @@
<%= container do %>
<% if icon && (icon_position != :right) %>
<%= helpers.icon(icon, size: size, color: icon_color, class: icon_classes) %>
<% end %>
<% unless icon_only? %>
<%= text %>
<% end %>
<% if icon && icon_position == :right %>
<%= helpers.icon(icon, size: size, color: icon_color) %>
<% end %>
<% end %>

View file

@ -0,0 +1,41 @@
# frozen_string_literal: true
# An extension to `button_to` helper. All options are passed through to the `button_to` helper with some additional
# options available.
class DS::Button < DS::Buttonish
attr_reader :confirm
def initialize(confirm: nil, **opts)
super(**opts)
@confirm = confirm
end
def container(&block)
if href.present?
button_to(href, **merged_opts, &block)
else
content_tag(:button, **merged_opts, &block)
end
end
private
def merged_opts
merged_opts = opts.dup || {}
extra_classes = merged_opts.delete(:class)
href = merged_opts.delete(:href)
data = merged_opts.delete(:data) || {}
if confirm.present?
data = data.merge(turbo_confirm: confirm.to_data_attribute)
end
if frame.present?
data = data.merge(turbo_frame: frame)
end
merged_opts.merge(
class: class_names(container_classes, extra_classes),
data: data
)
end
end

View file

@ -0,0 +1,156 @@
class DS::Buttonish < DesignSystemComponent
VARIANTS = {
primary: {
container_classes: "text-inverse bg-inverse hover:bg-inverse-hover disabled:bg-gray-500 theme-dark:disabled:bg-gray-400",
icon_classes: "fg-inverse"
},
secondary: {
container_classes: "text-primary bg-gray-50 theme-dark:bg-gray-700 hover:bg-gray-100 theme-dark:hover:bg-gray-600 disabled:bg-gray-200 theme-dark:disabled:bg-gray-600",
icon_classes: "fg-primary"
},
destructive: {
container_classes: "text-inverse bg-red-500 theme-dark:bg-red-400 hover:bg-red-600 theme-dark:hover:bg-red-500 disabled:bg-red-200 theme-dark:disabled:bg-red-600",
icon_classes: "fg-white"
},
outline: {
container_classes: "text-primary border border-secondary bg-transparent hover:bg-surface-hover",
icon_classes: "fg-gray"
},
outline_destructive: {
container_classes: "text-destructive border border-secondary bg-transparent hover:bg-gray-100 theme-dark:hover:bg-gray-700",
icon_classes: "fg-gray"
},
ghost: {
container_classes: "text-primary bg-transparent hover:bg-gray-100 theme-dark:hover:bg-gray-700",
icon_classes: "fg-gray"
},
icon: {
container_classes: "hover:bg-gray-100 theme-dark:hover:bg-gray-700",
icon_classes: "fg-gray"
},
icon_inverse: {
container_classes: "bg-inverse hover:bg-inverse-hover",
icon_classes: "fg-inverse"
}
}.freeze
SIZES = {
sm: {
container_classes: "px-2 py-1",
icon_container_classes: "inline-flex items-center justify-center w-8 h-8",
radius_classes: "rounded-md",
text_classes: "text-sm"
},
md: {
container_classes: "px-3 py-2",
icon_container_classes: "inline-flex items-center justify-center w-9 h-9",
radius_classes: "rounded-lg",
text_classes: "text-sm"
},
lg: {
container_classes: "px-4 py-3",
icon_container_classes: "inline-flex items-center justify-center w-10 h-10",
radius_classes: "rounded-xl",
text_classes: "text-base"
}
}.freeze
attr_reader :variant, :size, :href, :icon, :icon_position, :text, :full_width, :extra_classes, :frame, :opts
def initialize(variant: :primary, size: :md, href: nil, text: nil, icon: nil, icon_position: :left, full_width: false, frame: nil, **opts)
@variant = variant.to_s.underscore.to_sym
@size = size.to_sym
@href = href
@icon = icon
@icon_position = icon_position.to_sym
@text = text
@full_width = full_width
@extra_classes = opts.delete(:class)
@frame = frame
@opts = opts
end
def call
raise NotImplementedError, "Buttonish is an abstract class and cannot be instantiated directly."
end
def container_classes(override_classes = nil)
class_names(
"font-medium whitespace-nowrap",
merged_base_classes,
full_width ? "w-full justify-center" : nil,
container_size_classes,
size_data.dig(:text_classes),
variant_data.dig(:container_classes)
)
end
def container_size_classes
icon_only? ? size_data.dig(:icon_container_classes) : size_data.dig(:container_classes)
end
def icon_color
# Map variant to icon color for the icon helper
case variant
when :primary, :icon_inverse
:white
when :destructive, :outline_destructive
:destructive
else
:default
end
end
def icon_classes
class_names(
variant_data.dig(:icon_classes)
)
end
def icon_only?
variant.in?([ :icon, :icon_inverse ])
end
private
def variant_data
self.class::VARIANTS.dig(variant)
end
def size_data
self.class::SIZES.dig(size)
end
# Make sure that user can override common classes like `hidden`
def merged_base_classes
base_display_classes = "inline-flex items-center gap-1"
base_radius_classes = size_data.dig(:radius_classes)
extra_classes_list = (extra_classes || "").split
has_display_override = extra_classes_list.any? { |c| permitted_display_override_classes.include?(c) }
has_radius_override = extra_classes_list.any? { |c| permitted_radius_override_classes.include?(c) }
base_classes = []
unless has_display_override
base_classes << base_display_classes
end
unless has_radius_override
base_classes << base_radius_classes
end
class_names(
base_classes,
extra_classes
)
end
def permitted_radius_override_classes
[ "rounded-full" ]
end
def permitted_display_override_classes
[ "hidden", "flex" ]
end
end

View file

@ -0,0 +1,38 @@
<%= wrapper_element do %>
<%= tag.dialog class: "w-full h-full bg-transparent theme-dark:backdrop:bg-alpha-black-900 backdrop:bg-overlay #{drawer? ? "lg:p-3" : "lg:p-1"}", **merged_opts do %>
<%= tag.div class: dialog_outer_classes do %>
<%= tag.div class: dialog_inner_classes, data: { DS__dialog_target: "content" } do %>
<div class="grow overflow-y-auto py-4 space-y-4 flex flex-col">
<% if header? %>
<%= header %>
<% end %>
<% if body? %>
<div class="px-4 grow">
<%= body %>
<% if sections.any? %>
<div class="space-y-4">
<% sections.each do |section| %>
<%= section %>
<% end %>
</div>
<% end %>
</div>
<% end %>
<%# Optional, for customizing dialogs %>
<%= content %>
</div>
<% if actions? %>
<div class="flex items-center gap-2 justify-end p-4">
<% actions.each do |action| %>
<%= action %>
<% end %>
</div>
<% end %>
<% end %>
<% end %>
<% end %>
<% end %>

115
app/components/DS/dialog.rb Normal file
View file

@ -0,0 +1,115 @@
class DS::Dialog < DesignSystemComponent
renders_one :header, ->(title: nil, subtitle: nil, hide_close_icon: false, **opts, &block) do
content_tag(:header, class: "px-4 flex flex-col gap-2", **opts) do
title_div = content_tag(:div, class: "flex items-center justify-between gap-2") do
title = content_tag(:h2, title, class: class_names("font-medium text-primary", drawer? ? "text-lg" : "")) if title
close_icon = render DS::Button.new(variant: "icon", class: "ml-auto", icon: "x", tabindex: "-1", data: { action: "DS--dialog#close" }) unless hide_close_icon
safe_join([ title, close_icon ].compact)
end
subtitle = content_tag(:p, subtitle, class: "text-sm text-secondary") if subtitle
block_content = capture(&block) if block
safe_join([ title_div, subtitle, block_content ].compact)
end
end
renders_one :body
renders_many :actions, ->(cancel_action: false, **button_opts) do
merged_opts = if cancel_action
button_opts.merge(type: "button", data: { action: "DS--dialog#close" })
else
button_opts
end
render DS::Button.new(**merged_opts)
end
renders_many :sections, ->(title:, **disclosure_opts, &block) do
render DS::Disclosure.new(title: title, align: :right, **disclosure_opts) do
block.call
end
end
attr_reader :variant, :auto_open, :reload_on_close, :width, :disable_frame, :opts
VARIANTS = %w[modal drawer].freeze
WIDTHS = {
sm: "lg:max-w-[300px]",
md: "lg:max-w-[550px]",
lg: "lg:max-w-[700px]",
full: "lg:max-w-full"
}.freeze
def initialize(variant: "modal", auto_open: true, reload_on_close: false, width: "md", frame: nil, disable_frame: false, **opts)
@variant = variant.to_sym
@auto_open = auto_open
@reload_on_close = reload_on_close
@width = width.to_sym
@frame = frame
@disable_frame = disable_frame
@opts = opts
end
def frame
@frame || variant
end
# Caller must "opt-out" of using the default turbo-frame based on the variant
def wrapper_element(&block)
if disable_frame
content_tag(:div, &block)
else
content_tag("turbo-frame", id: frame, &block)
end
end
def dialog_outer_classes
variant_classes = if drawer?
"items-end justify-end"
else
"items-center justify-center"
end
class_names(
"flex h-full w-full",
variant_classes
)
end
def dialog_inner_classes
variant_classes = if drawer?
"lg:w-[550px] h-full"
else
class_names(
"max-h-full",
WIDTHS[width]
)
end
class_names(
"flex flex-col bg-container rounded-xl shadow-border-xs mx-3 lg:mx-0 w-full overflow-hidden",
variant_classes
)
end
def merged_opts
merged_opts = opts.dup
data = merged_opts.delete(:data) || {}
data[:controller] = [ "DS--dialog", "hotkey", data[:controller] ].compact.join(" ")
data[:DS__dialog_auto_open_value] = auto_open
data[:DS__dialog_reload_on_close_value] = reload_on_close
data[:action] = [ "mousedown->DS--dialog#clickOutside", data[:action] ].compact.join(" ")
data[:hotkey] = "esc:DS--dialog#close"
merged_opts[:data] = data
merged_opts
end
def drawer?
variant == :drawer
end
end

View file

@ -0,0 +1,33 @@
import { Controller } from "@hotwired/stimulus";
// Connects to data-controller="dialog"
export default class extends Controller {
static targets = ["content"]
static values = {
autoOpen: { type: Boolean, default: false },
reloadOnClose: { type: Boolean, default: false },
};
connect() {
if (this.element.open) return;
if (this.autoOpenValue) {
this.element.showModal();
}
}
// If the user clicks anywhere outside of the visible content, close the dialog
clickOutside(e) {
if (!this.contentTarget.contains(e.target)) {
this.close();
}
}
close() {
this.element.close();
if (this.reloadOnCloseValue) {
Turbo.visit(window.location.href);
}
}
}

View file

@ -0,0 +1,27 @@
<details class="group" <%= "open" if open %>>
<%= tag.summary class: class_names(
"px-3 py-2 rounded-xl cursor-pointer flex items-center justify-between bg-surface"
) do %>
<% if summary_content? %>
<%= summary_content %>
<% else %>
<div class="flex items-center gap-3">
<% if align == :left %>
<%= helpers.icon "chevron-right", class: "group-open:transform group-open:rotate-90" %>
<% end %>
<%= tag.span class: class_names("font-medium", align == :left ? "text-sm text-primary" : "text-xs uppercase text-secondary") do %>
<%= title %>
<% end %>
</div>
<% if align == :right %>
<%= helpers.icon "chevron-down", class: "group-open:transform group-open:rotate-180" %>
<% end %>
<% end %>
<% end %>
<div class="mt-2">
<%= content %>
</div>
</details>

View file

@ -0,0 +1,12 @@
class DS::Disclosure < DesignSystemComponent
renders_one :summary_content
attr_reader :title, :align, :open, :opts
def initialize(title: nil, align: "right", open: false, **opts)
@title = title
@align = align.to_sym
@open = open
@opts = opts
end
end

View file

@ -0,0 +1,8 @@
<%= tag.div style: transparent? ? container_styles : nil,
class: container_classes do %>
<% if icon %>
<%= helpers.icon(icon, size: icon_size, color: "current") %>
<% elsif text %>
<%= tag.span text.first, class: text_classes %>
<% end %>
<% end %>

View file

@ -0,0 +1,99 @@
class DS::FilledIcon < DesignSystemComponent
attr_reader :icon, :text, :hex_color, :size, :rounded, :variant
VARIANTS = %i[default text surface container inverse].freeze
SIZES = {
sm: {
container_size: "w-6 h-6",
container_radius: "rounded-md",
icon_size: "sm",
text_size: "text-xs"
},
md: {
container_size: "w-8 h-8",
container_radius: "rounded-lg",
icon_size: "md",
text_size: "text-xs"
},
lg: {
container_size: "w-9 h-9",
container_radius: "rounded-xl",
icon_size: "lg",
text_size: "text-sm"
}
}.freeze
def initialize(variant: :default, icon: nil, text: nil, hex_color: nil, size: "md", rounded: false)
@variant = variant.to_sym
@icon = icon
@text = text
@hex_color = hex_color
@size = size.to_sym
@rounded = rounded
end
def container_classes
class_names(
"flex justify-center items-center shrink-0",
size_classes,
radius_classes,
transparent? ? "border" : solid_bg_class
)
end
def icon_size
SIZES[size][:icon_size]
end
def text_classes
class_names(
"text-center font-medium uppercase",
SIZES[size][:text_size]
)
end
def container_styles
<<~STYLE.strip
background-color: #{transparent_bg_color};
border-color: #{transparent_border_color};
color: #{custom_fg_color};
STYLE
end
def transparent?
variant.in?(%i[default text])
end
private
def solid_bg_class
case variant
when :surface
"bg-surface-inset"
when :container
"bg-container-inset"
when :inverse
"bg-container"
end
end
def size_classes
SIZES[size][:container_size]
end
def radius_classes
rounded ? "rounded-full" : SIZES[size][:container_radius]
end
def custom_fg_color
hex_color || "var(--color-gray-500)"
end
def transparent_bg_color
"color-mix(in oklab, #{custom_fg_color} 10%, transparent)"
end
def transparent_border_color
"color-mix(in oklab, #{custom_fg_color} 10%, transparent)"
end
end

View file

@ -0,0 +1,13 @@
<%= link_to href, **merged_opts do %>
<% if icon && (icon_position != "right") %>
<%= helpers.icon(icon, size: size, color: icon_color) %>
<% end %>
<% unless icon_only? %>
<%= text %>
<% end %>
<% if icon && icon_position == "right" %>
<%= helpers.icon(icon, size: size, color: icon_color) %>
<% end %>
<% end %>

31
app/components/DS/link.rb Normal file
View file

@ -0,0 +1,31 @@
# An extension to `link_to` helper. All options are passed through to the `link_to` helper with some additional
# options available.
class DS::Link < DS::Buttonish
attr_reader :frame
VARIANTS = VARIANTS.reverse_merge(
default: {
container_classes: "",
icon_classes: "fg-gray"
}
).freeze
def merged_opts
merged_opts = opts.dup || {}
data = merged_opts.delete(:data) || {}
if frame
data = data.merge(turbo_frame: frame)
end
merged_opts.merge(
class: class_names(container_classes, extra_classes),
data: data
)
end
private
def container_size_classes
super unless variant == :default
end
end

View file

@ -0,0 +1,27 @@
<%= tag.div data: { controller: "DS--menu", DS__menu_placement_value: placement, DS__menu_offset_value: offset, testid: testid } do %>
<% if variant == :icon %>
<%= render DS::Button.new(variant: "icon", icon: icon_vertical ? "more-vertical" : "more-horizontal", data: { DS__menu_target: "button" }) %>
<% elsif variant == :button %>
<%= button %>
<% elsif variant == :avatar %>
<button data-DS--menu-target="button">
<div class="w-9 h-9 cursor-pointer">
<%= render "settings/user_avatar", avatar_url: avatar_url, initials: initials %>
</div>
</button>
<% end %>
<div data-DS--menu-target="content" class="px-2 lg:px-0 max-w-full hidden z-50">
<div class="mx-auto min-w-[200px] shadow-border-xs bg-container rounded-lg">
<%= header %>
<%= tag.div class: class_names("py-1" => !no_padding) do %>
<% items.each do |item| %>
<%= item %>
<% end %>
<%= custom_content %>
<% end %>
</div>
</div>
<% end %>

38
app/components/DS/menu.rb Normal file
View file

@ -0,0 +1,38 @@
# frozen_string_literal: true
class DS::Menu < DesignSystemComponent
attr_reader :variant, :avatar_url, :initials, :placement, :offset, :icon_vertical, :no_padding, :testid
renders_one :button, ->(**button_options, &block) do
options_with_target = button_options.merge(data: { DS__menu_target: "button" })
if block
content_tag(:button, **options_with_target, &block)
else
DS::Button.new(**options_with_target)
end
end
renders_one :header, ->(&block) do
content_tag(:div, class: "border-b border-tertiary", &block)
end
renders_one :custom_content
renders_many :items, DS::MenuItem
VARIANTS = %i[icon button avatar].freeze
def initialize(variant: "icon", avatar_url: nil, initials: nil, placement: "bottom-end", offset: 12, icon_vertical: false, no_padding: false, testid: nil)
@variant = variant.to_sym
@avatar_url = avatar_url
@initials = initials
@placement = placement
@offset = offset
@icon_vertical = icon_vertical
@no_padding = no_padding
@testid = testid
raise ArgumentError, "Invalid variant: #{@variant}" unless VARIANTS.include?(@variant)
end
end

View file

@ -0,0 +1,12 @@
<% if variant == :divider %>
<%= render "shared/ruler", classes: "my-1" %>
<% else %>
<div class="px-1">
<%= wrapper do %>
<% if icon %>
<%= helpers.icon(icon, color: destructive? ? :destructive : :default) %>
<% end %>
<%= tag.span(text, class: text_classes) %>
<% end %>
</div>
<% end %>

View file

@ -0,0 +1,62 @@
class DS::MenuItem < DesignSystemComponent
VARIANTS = %i[link button divider].freeze
attr_reader :variant, :text, :icon, :href, :method, :destructive, :confirm, :frame, :opts
def initialize(variant:, text: nil, icon: nil, href: nil, method: :post, destructive: false, confirm: nil, frame: nil, **opts)
@variant = variant.to_sym
@text = text
@icon = icon
@href = href
@method = method.to_sym
@destructive = destructive
@confirm = confirm
@frame = frame
@opts = opts
raise ArgumentError, "Invalid variant: #{@variant}" unless VARIANTS.include?(@variant)
end
def wrapper(&block)
if variant == :button
button_to href, method: method, class: container_classes, **merged_opts, &block
elsif variant == :link
link_to href, class: container_classes, **merged_opts, &block
else
nil
end
end
def text_classes
[
"text-sm",
destructive? ? "text-destructive" : "text-primary"
].join(" ")
end
def destructive?
method == :delete || destructive
end
private
def container_classes
[
"flex items-center gap-2 p-2 rounded-md w-full",
destructive? ? "hover:bg-red-tint-5 theme-dark:hover:bg-red-tint-10" : "hover:bg-container-hover"
].join(" ")
end
def merged_opts
merged_opts = opts.dup || {}
data = merged_opts.delete(:data) || {}
if confirm.present?
data = data.merge(turbo_confirm: confirm.to_data_attribute)
end
if frame.present?
data = data.merge(turbo_frame: frame)
end
merged_opts.merge(data: data)
end
end

12
app/components/DS/tab.rb Normal file
View file

@ -0,0 +1,12 @@
class DS::Tab < DesignSystemComponent
attr_reader :id, :label
def initialize(id:, label:)
@id = id
@label = label
end
def call
content
end
end

View file

@ -0,0 +1,18 @@
<%= tag.div data: {
controller: "DS--tabs",
testid: testid,
DS__tabs_session_key_value: session_key,
DS__tabs_url_param_key_value: url_param_key,
DS__tabs_nav_btn_active_class: active_btn_classes,
DS__tabs_nav_btn_inactive_class: inactive_btn_classes
} do %>
<% if unstyled? %>
<%= content %>
<% else %>
<%= nav %>
<% panels.each do |panel| %>
<%= panel %>
<% end %>
<% end %>
<% end %>

66
app/components/DS/tabs.rb Normal file
View file

@ -0,0 +1,66 @@
class DS::Tabs < DesignSystemComponent
renders_one :nav, ->(classes: nil) do
DS::Tabs::Nav.new(
active_tab: active_tab,
active_btn_classes: active_btn_classes,
inactive_btn_classes: inactive_btn_classes,
btn_classes: base_btn_classes,
classes: unstyled? ? classes : class_names(nav_container_classes, classes)
)
end
renders_many :panels, ->(tab_id:, &block) do
content_tag(
:div,
class: ("hidden" unless tab_id == active_tab),
data: { id: tab_id, DS__tabs_target: "panel" },
&block
)
end
VARIANTS = {
default: {
active_btn_classes: "bg-white theme-dark:bg-gray-700 text-primary shadow-sm",
inactive_btn_classes: "text-secondary hover:bg-surface-inset-hover",
base_btn_classes: "w-full inline-flex justify-center items-center text-sm font-medium px-2 py-1 rounded-md transition-colors duration-200",
nav_container_classes: "flex bg-surface-inset p-1 rounded-lg mb-4"
}
}
attr_reader :active_tab, :url_param_key, :session_key, :variant, :testid
def initialize(active_tab:, url_param_key: nil, session_key: nil, variant: :default, active_btn_classes: "", inactive_btn_classes: "", testid: nil)
@active_tab = active_tab
@url_param_key = url_param_key
@session_key = session_key
@variant = variant.to_sym
@active_btn_classes = active_btn_classes
@inactive_btn_classes = inactive_btn_classes
@testid = testid
end
def active_btn_classes
unstyled? ? @active_btn_classes : VARIANTS.dig(variant, :active_btn_classes)
end
def inactive_btn_classes
unstyled? ? @inactive_btn_classes : VARIANTS.dig(variant, :inactive_btn_classes)
end
private
def unstyled?
variant == :unstyled
end
def base_btn_classes
unless unstyled?
VARIANTS.dig(variant, :base_btn_classes)
end
end
def nav_container_classes
unless unstyled?
VARIANTS.dig(variant, :nav_container_classes)
end
end
end

View file

@ -0,0 +1,29 @@
class DS::Tabs::Nav < DesignSystemComponent
erb_template <<~ERB
<%= tag.nav class: classes do %>
<% btns.each do |btn| %>
<%= btn %>
<% end %>
<% end %>
ERB
renders_many :btns, ->(id:, label:, classes: nil, &block) do
content_tag(
:button, label, id: id,
type: "button",
class: class_names(btn_classes, id == active_tab ? active_btn_classes : inactive_btn_classes, classes),
data: { id: id, action: "DS--tabs#show", DS__tabs_target: "navBtn" },
&block
)
end
attr_reader :active_tab, :classes, :active_btn_classes, :inactive_btn_classes, :btn_classes
def initialize(active_tab:, classes: nil, active_btn_classes: nil, inactive_btn_classes: nil, btn_classes: nil)
@active_tab = active_tab
@classes = classes
@active_btn_classes = active_btn_classes
@inactive_btn_classes = inactive_btn_classes
@btn_classes = btn_classes
end
end

View file

@ -0,0 +1,11 @@
class DS::Tabs::Panel < DesignSystemComponent
attr_reader :tab_id
def initialize(tab_id:)
@tab_id = tab_id
end
def call
content
end
end

View file

@ -0,0 +1,57 @@
import { Controller } from "@hotwired/stimulus";
// Connects to data-controller="tabs--components"
export default class extends Controller {
static classes = ["navBtnActive", "navBtnInactive"];
static targets = ["panel", "navBtn"];
static values = { sessionKey: String, urlParamKey: String };
show(e) {
const btn = e.target.closest("button");
const selectedTabId = btn.dataset.id;
this.navBtnTargets.forEach((navBtn) => {
if (navBtn.dataset.id === selectedTabId) {
navBtn.classList.add(...this.navBtnActiveClasses);
navBtn.classList.remove(...this.navBtnInactiveClasses);
} else {
navBtn.classList.add(...this.navBtnInactiveClasses);
navBtn.classList.remove(...this.navBtnActiveClasses);
}
});
this.panelTargets.forEach((panel) => {
if (panel.dataset.id === selectedTabId) {
panel.classList.remove("hidden");
} else {
panel.classList.add("hidden");
}
});
if (this.urlParamKeyValue) {
const url = new URL(window.location.href);
url.searchParams.set(this.urlParamKeyValue, selectedTabId);
window.history.replaceState({}, "", url);
}
// Update URL with the selected tab
if (this.sessionKeyValue) {
this.#updateSessionPreference(selectedTabId);
}
}
#updateSessionPreference(selectedTabId) {
fetch("/current_session", {
method: "PUT",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"X-CSRF-Token": document.querySelector('[name="csrf-token"]').content,
Accept: "application/json",
},
body: new URLSearchParams({
"current_session[tab_key]": this.sessionKeyValue,
"current_session[tab_value]": selectedTabId,
}).toString(),
});
}
}

View file

@ -0,0 +1,5 @@
<div class="relative inline-block select-none">
<%= hidden_field_tag name, unchecked_value, id: nil %>
<%= check_box_tag name, checked_value, checked, class: "sr-only peer", disabled: disabled, id: id, **opts %>
<%= label_tag name, "&nbsp;".html_safe, class: label_classes, for: id %>
</div>

View file

@ -0,0 +1,26 @@
class DS::Toggle < DesignSystemComponent
attr_reader :id, :name, :checked, :disabled, :checked_value, :unchecked_value, :opts
def initialize(id:, name: nil, checked: false, disabled: false, checked_value: "1", unchecked_value: "0", **opts)
@id = id
@name = name
@checked = checked
@disabled = disabled
@checked_value = checked_value
@unchecked_value = unchecked_value
@opts = opts
end
def label_classes
class_names(
"block w-9 h-5 cursor-pointer",
"rounded-full bg-gray-100 theme-dark:bg-gray-700",
"transition-colors duration-300",
"after:content-[''] after:block after:bg-white after:absolute after:rounded-full",
"after:top-0.5 after:left-0.5 after:w-4 after:h-4",
"after:transition-transform after:duration-300 after:ease-in-out",
"peer-checked:bg-green-600 peer-checked:after:translate-x-4",
"peer-disabled:opacity-70 peer-disabled:cursor-not-allowed"
)
end
end

View file

@ -0,0 +1,58 @@
<div id="<%= dom_id(account, :chart) %>" class="bg-container shadow-border-xs rounded-xl space-y-2">
<div class="flex justify-between flex-col-reverse lg:flex-row gap-2 px-4 pt-4 mb-2">
<div class="space-y-2 w-full">
<div class="flex items-center gap-1">
<%= tag.p title, class: "text-sm font-medium text-secondary" %>
<% if account.investment? %>
<%= render "investments/value_tooltip", balance: account.balance_money, holdings: holdings_value_money, cash: account.cash_balance_money %>
<% end %>
</div>
<div class="flex flex-row gap-2 items-baseline">
<%= tag.p view_balance_money.format, class: "text-primary text-3xl font-medium truncate" %>
<% if converted_balance_money %>
<%= tag.p converted_balance_money.format, class: "text-sm font-medium text-secondary" %>
<% end %>
</div>
</div>
<%= form_with url: account_path(account), method: :get, data: { controller: "auto-submit-form" } do |form| %>
<div class="flex items-center gap-2">
<% if account.investment? %>
<%= form.select :chart_view,
[["Total value", "balance"], ["Holdings", "holdings_balance"], ["Cash", "cash_balance"]],
{ selected: view },
class: "bg-container border border-secondary rounded-lg text-sm pr-7 cursor-pointer text-primary focus:outline-hidden focus:ring-0",
data: { "auto-submit-form-target": "auto" } %>
<% end %>
<%= form.select :period,
Period.as_options,
{ selected: period.key },
data: { "auto-submit-form-target": "auto" },
class: "bg-container border border-secondary rounded-lg px-3 py-2 text-sm pr-7 cursor-pointer text-primary focus:outline-hidden focus:ring-0" %>
</div>
<% end %>
</div>
<%= turbo_frame_tag dom_id(@account, :chart_details) do %>
<div class="px-4">
<%= render partial: "shared/trend_change", locals: { trend: trend, comparison_label: period.comparison_label } %>
</div>
<div class="h-64 pb-4">
<% if series.any? %>
<div
id="lineChart"
class="w-full h-full"
data-controller="time-series-chart"
data-time-series-chart-data-value="<%= series.to_json %>"></div>
<% else %>
<div class="w-full h-full flex items-center justify-center">
<p class="text-secondary text-sm">No data available</p>
</div>
<% end %>
</div>
<% end %>
</div>

View file

@ -0,0 +1,72 @@
class UI::Account::Chart < ApplicationComponent
attr_reader :account
def initialize(account:, period: nil, view: nil)
@account = account
@period = period
@view = view
end
def period
@period ||= Period.last_30_days
end
def holdings_value_money
account.balance_money - account.cash_balance_money
end
def view_balance_money
case view
when "balance"
account.balance_money
when "holdings_balance"
holdings_value_money
when "cash_balance"
account.cash_balance_money
end
end
def title
case account.accountable_type
when "Investment", "Crypto"
case view
when "balance"
"Total account value"
when "holdings_balance"
"Holdings value"
when "cash_balance"
"Cash value"
end
when "Property", "Vehicle"
"Estimated #{account.accountable_type.humanize.downcase} value"
when "CreditCard", "OtherLiability"
"Debt balance"
when "Loan"
"Remaining principal balance"
else
"Balance"
end
end
def foreign_currency?
account.currency != account.family.currency
end
def converted_balance_money
return nil unless foreign_currency?
account.balance_money.exchange_to(account.family.currency, fallback_rate: 1)
end
def view
@view ||= "balance"
end
def series
account.balance_series(period: period, view: view)
end
def trend
series.trend
end
end

View file

@ -0,0 +1,29 @@
<%= turbo_stream_from account %>
<%= turbo_frame_tag dom_id(account, :container) do %>
<%= tag.div class: "space-y-4 pb-32" do %>
<%= render "accounts/show/header", account: account, title: title, subtitle: subtitle %>
<%= render UI::Account::Chart.new(account: account, period: chart_period, view: chart_view) %>
<div class="min-h-[800px]" data-testid="account-details">
<% if tabs.count > 1 %>
<%= render DS::Tabs.new(active_tab: active_tab, url_param_key: "tab") do |tabs_container| %>
<% tabs_container.with_nav(classes: "max-w-fit") do |nav| %>
<% tabs.each do |tab| %>
<% nav.with_btn(id: tab, label: tab.to_s.humanize, classes: "px-6") %>
<% end %>
<% end %>
<% tabs.each do |tab| %>
<% tabs_container.with_panel(tab_id: tab) do %>
<%= render tab_partial_name(tab), account: account %>
<% end %>
<% end %>
<% end %>
<% else %>
<%= render tab_partial_name(tabs.first), account: account %>
<% end %>
</div>
<% end %>
<% end %>

View file

@ -0,0 +1,45 @@
class UI::AccountPage < ApplicationComponent
attr_reader :account, :chart_view, :chart_period
def initialize(account:, chart_view: nil, chart_period: nil, active_tab: nil)
@account = account
@chart_view = chart_view
@chart_period = chart_period
@active_tab = active_tab
end
def title
account.name
end
def subtitle
return nil unless account.property?
account.property.address
end
def active_tab
tabs.find { |tab| tab == @active_tab&.to_sym } || tabs.first
end
def tabs
case account.accountable_type
when "Investment"
[ :activity, :holdings ]
when "Property", "Vehicle", "Loan"
[ :activity, :overview ]
else
[ :activity ]
end
end
def tab_partial_name(tab)
case tab
when :activity
"accounts/show/activity"
when :holdings, :overview
# Accountable is responsible for implementing the partial in the correct folder
"#{account.accountable_type.downcase.pluralize}/tabs/#{tab}"
end
end
end

View file

@ -0,0 +1,4 @@
class ApplicationComponent < ViewComponent::Base
# These don't work as expected with helpers.turbo_frame_tag, etc., so we include them here
include Turbo::FramesHelper, Turbo::StreamsHelper
end

View file

@ -0,0 +1,2 @@
class DesignSystemComponent < ViewComponent::Base
end

View file

@ -1,37 +0,0 @@
class Account::TradesController < ApplicationController
include EntryableResource
permitted_entryable_attributes :id, :qty, :price
private
def build_entry
Account::TradeBuilder.new(create_entry_params)
end
def create_entry_params
params.require(:account_entry).permit(
:account_id, :date, :amount, :currency, :qty, :price, :ticker, :type, :transfer_account_id
).tap do |params|
account_id = params.delete(:account_id)
params[:account] = Current.family.accounts.find(account_id)
end
end
def update_entry_params
return entry_params unless entry_params[:entryable_attributes].present?
update_params = entry_params
update_params = update_params.merge(entryable_type: "Account::Trade")
qty = update_params[:entryable_attributes][:qty]
price = update_params[:entryable_attributes][:price]
if qty.present? && price.present?
qty = update_params[:nature] == "inflow" ? -qty.to_d : qty.to_d
update_params[:entryable_attributes][:qty] = qty
update_params[:amount] = qty * price.to_d
end
update_params.except(:nature)
end
end

View file

@ -1,22 +0,0 @@
class Account::TransactionCategoriesController < ApplicationController
def update
@entry = Current.family.entries.account_transactions.find(params[:transaction_id])
@entry.update!(entry_params)
respond_to do |format|
format.html { redirect_back_or_to account_transaction_path(@entry) }
format.turbo_stream do
render turbo_stream: turbo_stream.replace(
"category_menu_account_transaction_#{@entry.account_transaction_id}",
partial: "categories/menu",
locals: { transaction: @entry.account_transaction }
)
end
end
end
private
def entry_params
params.require(:account_entry).permit(:entryable_type, entryable_attributes: [ :id, :category_id ])
end
end

View file

@ -1,37 +0,0 @@
class Account::TransactionsController < ApplicationController
include EntryableResource
permitted_entryable_attributes :id, :category_id, :merchant_id, { tag_ids: [] }
def bulk_delete
destroyed = Current.family.entries.destroy_by(id: bulk_delete_params[:entry_ids])
destroyed.map(&:account).uniq.each(&:sync_later)
redirect_back_or_to transactions_url, notice: t(".success", count: destroyed.count)
end
def bulk_edit
end
def bulk_update
updated = Current.family
.entries
.where(id: bulk_update_params[:entry_ids])
.bulk_update!(bulk_update_params)
redirect_back_or_to transactions_url, notice: t(".success", count: updated)
end
private
def bulk_delete_params
params.require(:bulk_delete).permit(entry_ids: [])
end
def bulk_update_params
params.require(:bulk_update).permit(:date, :notes, :category_id, :merchant_id, entry_ids: [])
end
def search_params
params.fetch(:q, {})
.permit(:start_date, :end_date, :search, :amount, :amount_operator, accounts: [], account_ids: [], categories: [], merchants: [], types: [], tags: [])
end
end

View file

@ -1,3 +0,0 @@
class Account::ValuationsController < ApplicationController
include EntryableResource
end

View file

@ -2,16 +2,24 @@ class AccountableSparklinesController < ApplicationController
def show
@accountable = Accountable.from_type(params[:accountable_type]&.classify)
@series = Rails.cache.fetch(cache_key) do
family.accounts.active
.where(accountable_type: @accountable.name)
.balance_series(
currency: family.currency,
favorable_direction: @accountable.favorable_direction
)
end
etag_key = cache_key
render layout: false
# Use HTTP conditional GET so the client receives 304 Not Modified when possible.
if stale?(etag: etag_key, last_modified: family.latest_sync_completed_at)
@series = Rails.cache.fetch(etag_key, expires_in: 24.hours) do
builder = Balance::ChartSeriesBuilder.new(
account_ids: account_ids,
currency: family.currency,
period: Period.last_30_days,
favorable_direction: @accountable.favorable_direction,
interval: "1 day"
)
builder.balance_series
end
render layout: false
end
end
private
@ -19,7 +27,15 @@ class AccountableSparklinesController < ApplicationController
Current.family
end
def accountable
Accountable.from_type(params[:accountable_type]&.classify)
end
def account_ids
family.accounts.visible.where(accountable_type: accountable.name).pluck(:id)
end
def cache_key
family.build_cache_key("#{@accountable.name}_sparkline")
family.build_cache_key("#{@accountable.name}_sparkline", invalidate_on_data_updates: true)
end
end

View file

@ -1,5 +1,6 @@
class AccountsController < ApplicationController
before_action :set_account, only: %i[sync chart sparkline]
before_action :set_account, only: %i[sync sparkline toggle_active show destroy]
include Periodable
def index
@manual_accounts = family.accounts.manual.alphabetically
@ -8,6 +9,15 @@ class AccountsController < ApplicationController
render layout: "settings"
end
def show
@chart_view = params[:chart_view] || "balance"
@tab = params[:tab]
@q = params.fetch(:q, {}).permit(:search)
entries = @account.entries.search(@q).reverse_chronological
@pagy, @entries = pagy(entries, limit: params[:per_page] || "10")
end
def sync
unless @account.syncing?
@account.sync_later
@ -16,22 +26,35 @@ class AccountsController < ApplicationController
redirect_to account_path(@account)
end
def chart
render layout: "application"
end
def sparkline
render layout: false
etag_key = @account.family.build_cache_key("#{@account.id}_sparkline", invalidate_on_data_updates: true)
# Short-circuit with 304 Not Modified when the client already has the latest version.
# We defer the expensive series computation until we know the content is stale.
if stale?(etag: etag_key, last_modified: @account.family.latest_sync_completed_at)
@sparkline_series = @account.sparkline_series
render layout: false
end
end
def sync_all
unless family.syncing?
family.sync_later
def toggle_active
if @account.active?
@account.disable!
elsif @account.disabled?
@account.enable!
end
redirect_to accounts_path
end
def destroy
if @account.linked?
redirect_to account_path(@account), alert: "Cannot delete a linked account"
else
@account.destroy_later
redirect_to accounts_path, notice: "Account scheduled for deletion"
end
end
private
def family
Current.family

View file

@ -0,0 +1,59 @@
# frozen_string_literal: true
class Api::V1::AccountsController < Api::V1::BaseController
include Pagy::Backend
# Ensure proper scope authorization for read access
before_action :ensure_read_scope
def index
# Test with Pagy pagination
family = current_resource_owner.family
accounts_query = family.accounts.visible.alphabetically
# Handle pagination with Pagy
@pagy, @accounts = pagy(
accounts_query,
page: safe_page_param,
limit: safe_per_page_param
)
@per_page = safe_per_page_param
# Rails will automatically use app/views/api/v1/accounts/index.json.jbuilder
render :index
rescue => e
Rails.logger.error "AccountsController error: #{e.message}"
Rails.logger.error e.backtrace.join("\n")
render json: {
error: "internal_server_error",
message: "Error: #{e.message}"
}, status: :internal_server_error
end
private
def ensure_read_scope
authorize_scope!(:read)
end
def safe_page_param
page = params[:page].to_i
page > 0 ? page : 1
end
def safe_per_page_param
per_page = params[:per_page].to_i
# Default to 25, max 100
case per_page
when 1..100
per_page
else
25
end
end
end

View file

@ -0,0 +1,210 @@
module Api
module V1
class AuthController < BaseController
include Invitable
skip_before_action :authenticate_request!
skip_before_action :check_api_key_rate_limit
skip_before_action :log_api_access
def signup
# Check if invite code is required
if invite_code_required? && params[:invite_code].blank?
render json: { error: "Invite code is required" }, status: :forbidden
return
end
# Validate invite code if provided
if params[:invite_code].present? && !InviteCode.exists?(token: params[:invite_code]&.downcase)
render json: { error: "Invalid invite code" }, status: :forbidden
return
end
# Validate password
password_errors = validate_password(params[:user][:password])
if password_errors.any?
render json: { errors: password_errors }, status: :unprocessable_entity
return
end
# Validate device info
unless valid_device_info?
render json: { error: "Device information is required" }, status: :bad_request
return
end
user = User.new(user_signup_params)
# Create family for new user
family = Family.new
user.family = family
user.role = :admin
if user.save
# Claim invite code if provided
InviteCode.claim!(params[:invite_code]) if params[:invite_code].present?
# Create device and OAuth token
device = create_or_update_device(user)
token_response = create_oauth_token_for_device(user, device)
render json: token_response.merge(
user: {
id: user.id,
email: user.email,
first_name: user.first_name,
last_name: user.last_name
}
), status: :created
else
render json: { errors: user.errors.full_messages }, status: :unprocessable_entity
end
end
def login
user = User.find_by(email: params[:email])
if user&.authenticate(params[:password])
# Check MFA if enabled
if user.otp_required?
unless params[:otp_code].present? && user.verify_otp?(params[:otp_code])
render json: {
error: "Two-factor authentication required",
mfa_required: true
}, status: :unauthorized
return
end
end
# Validate device info
unless valid_device_info?
render json: { error: "Device information is required" }, status: :bad_request
return
end
# Create device and OAuth token
device = create_or_update_device(user)
token_response = create_oauth_token_for_device(user, device)
render json: token_response.merge(
user: {
id: user.id,
email: user.email,
first_name: user.first_name,
last_name: user.last_name
}
)
else
render json: { error: "Invalid email or password" }, status: :unauthorized
end
end
def refresh
# Find the refresh token
refresh_token = params[:refresh_token]
unless refresh_token.present?
render json: { error: "Refresh token is required" }, status: :bad_request
return
end
# Find the access token associated with this refresh token
access_token = Doorkeeper::AccessToken.by_refresh_token(refresh_token)
if access_token.nil? || access_token.revoked?
render json: { error: "Invalid refresh token" }, status: :unauthorized
return
end
# Create new access token
new_token = Doorkeeper::AccessToken.create!(
application: access_token.application,
resource_owner_id: access_token.resource_owner_id,
expires_in: 30.days.to_i,
scopes: access_token.scopes,
use_refresh_token: true
)
# Revoke old access token
access_token.revoke
# Update device last seen
user = User.find(access_token.resource_owner_id)
device = user.mobile_devices.find_by(device_id: params[:device][:device_id])
device&.update_last_seen!
render json: {
access_token: new_token.plaintext_token,
refresh_token: new_token.plaintext_refresh_token,
token_type: "Bearer",
expires_in: new_token.expires_in,
created_at: new_token.created_at.to_i
}
end
private
def user_signup_params
params.require(:user).permit(:email, :password, :first_name, :last_name)
end
def validate_password(password)
errors = []
if password.blank?
errors << "Password can't be blank"
return errors
end
errors << "Password must be at least 8 characters" if password.length < 8
errors << "Password must include both uppercase and lowercase letters" unless password.match?(/[A-Z]/) && password.match?(/[a-z]/)
errors << "Password must include at least one number" unless password.match?(/\d/)
errors << "Password must include at least one special character" unless password.match?(/[!@#$%^&*(),.?":{}|<>]/)
errors
end
def valid_device_info?
device = params[:device]
return false if device.nil?
required_fields = %w[device_id device_name device_type os_version app_version]
required_fields.all? { |field| device[field].present? }
end
def create_or_update_device(user)
# Handle both string and symbol keys
device_data = params[:device].permit(:device_id, :device_name, :device_type, :os_version, :app_version)
device = user.mobile_devices.find_or_initialize_by(device_id: device_data[:device_id])
device.update!(device_data.merge(last_seen_at: Time.current))
device
end
def create_oauth_token_for_device(user, device)
# Create OAuth application for this device if needed
oauth_app = device.create_oauth_application!
# Revoke any existing tokens for this device
device.revoke_all_tokens!
# Create new access token with 30-day expiration
access_token = Doorkeeper::AccessToken.create!(
application: oauth_app,
resource_owner_id: user.id,
expires_in: 30.days.to_i,
scopes: "read_write",
use_refresh_token: true
)
{
access_token: access_token.plaintext_token,
refresh_token: access_token.plaintext_refresh_token,
token_type: "Bearer",
expires_in: access_token.expires_in,
created_at: access_token.created_at.to_i
}
end
end
end
end

View file

@ -0,0 +1,279 @@
# frozen_string_literal: true
class Api::V1::BaseController < ApplicationController
include Doorkeeper::Rails::Helpers
# Skip regular session-based authentication for API
skip_authentication
# Skip CSRF protection for API endpoints
skip_before_action :verify_authenticity_token
# Skip onboarding requirements for API endpoints
skip_before_action :require_onboarding_and_upgrade
# Force JSON format for all API requests
before_action :force_json_format
# Use our custom authentication that supports both OAuth and API keys
before_action :authenticate_request!
before_action :check_api_key_rate_limit
before_action :log_api_access
# Override Doorkeeper's default behavior to return JSON instead of redirecting
def doorkeeper_unauthorized_render_options(error: nil)
{ json: { error: "unauthorized", message: "Access token is invalid, expired, or missing" } }
end
# Error handling for common API errors
rescue_from ActiveRecord::RecordNotFound, with: :handle_not_found
rescue_from Doorkeeper::Errors::DoorkeeperError, with: :handle_unauthorized
rescue_from ActionController::ParameterMissing, with: :handle_bad_request
private
# Force JSON format for all API requests
def force_json_format
request.format = :json
end
# Authenticate using either OAuth or API key
def authenticate_request!
return if authenticate_oauth
return if authenticate_api_key
render_unauthorized unless performed?
end
# Try OAuth authentication first
def authenticate_oauth
return false unless request.headers["Authorization"].present?
# Manually verify the token (bypassing doorkeeper_authorize! which had scope issues)
token_string = request.authorization&.split(" ")&.last
access_token = Doorkeeper::AccessToken.by_token(token_string)
# Check token validity and scope (read_write includes read access)
has_sufficient_scope = access_token&.scopes&.include?("read") || access_token&.scopes&.include?("read_write")
unless access_token && !access_token.expired? && has_sufficient_scope
render_json({ error: "unauthorized", message: "Access token is invalid, expired, or missing required scope" }, status: :unauthorized)
return false
end
# Set the doorkeeper_token for compatibility
@_doorkeeper_token = access_token
if doorkeeper_token&.resource_owner_id
@current_user = User.find_by(id: doorkeeper_token.resource_owner_id)
# If user doesn't exist, the token is invalid (user was deleted)
unless @current_user
Rails.logger.warn "API OAuth Token Invalid: Access token resource_owner_id #{doorkeeper_token.resource_owner_id} does not exist"
render_json({ error: "unauthorized", message: "Access token is invalid - user not found" }, status: :unauthorized)
return false
end
else
Rails.logger.warn "API OAuth Token Invalid: Access token missing resource_owner_id"
render_json({ error: "unauthorized", message: "Access token is invalid - missing resource owner" }, status: :unauthorized)
return false
end
@authentication_method = :oauth
setup_current_context_for_api
true
rescue Doorkeeper::Errors::DoorkeeperError => e
Rails.logger.warn "API OAuth Error: #{e.message}"
false
end
# Try API key authentication
def authenticate_api_key
api_key_value = request.headers["X-Api-Key"]
return false unless api_key_value
@api_key = ApiKey.find_by_value(api_key_value)
return false unless @api_key && @api_key.active?
@current_user = @api_key.user
@api_key.update_last_used!
@authentication_method = :api_key
@rate_limiter = ApiRateLimiter.new(@api_key)
setup_current_context_for_api
true
end
# Check rate limits for API key authentication
def check_api_key_rate_limit
return unless @authentication_method == :api_key && @rate_limiter
if @rate_limiter.rate_limit_exceeded?
usage_info = @rate_limiter.usage_info
render_rate_limit_exceeded(usage_info)
return false
end
# Increment request count for successful API key requests
@rate_limiter.increment_request_count!
# Add rate limit headers to response
add_rate_limit_headers(@rate_limiter.usage_info)
end
# Render rate limit exceeded response
def render_rate_limit_exceeded(usage_info)
response.headers["X-RateLimit-Limit"] = usage_info[:rate_limit].to_s
response.headers["X-RateLimit-Remaining"] = "0"
response.headers["X-RateLimit-Reset"] = usage_info[:reset_time].to_s
response.headers["Retry-After"] = usage_info[:reset_time].to_s
Rails.logger.warn "API Rate Limit Exceeded: API Key #{@api_key.name} (User: #{@current_user.email}) - #{usage_info[:current_count]}/#{usage_info[:rate_limit]} requests"
render_json({
error: "rate_limit_exceeded",
message: "Rate limit exceeded. Try again in #{usage_info[:reset_time]} seconds.",
details: {
limit: usage_info[:rate_limit],
current: usage_info[:current_count],
reset_in_seconds: usage_info[:reset_time]
}
}, status: :too_many_requests)
end
# Add rate limit headers to successful responses
def add_rate_limit_headers(usage_info)
response.headers["X-RateLimit-Limit"] = usage_info[:rate_limit].to_s
response.headers["X-RateLimit-Remaining"] = usage_info[:remaining].to_s
response.headers["X-RateLimit-Reset"] = usage_info[:reset_time].to_s
end
# Render unauthorized response
def render_unauthorized
render_json({ error: "unauthorized", message: "Access token or API key is invalid, expired, or missing" }, status: :unauthorized)
end
# Returns the user that owns the access token or API key
def current_resource_owner
@current_user
end
# Get current scopes from either authentication method
def current_scopes
case @authentication_method
when :oauth
doorkeeper_token&.scopes&.to_a || []
when :api_key
@api_key&.scopes || []
else
[]
end
end
# Check if the current authentication has the required scope
# Implements hierarchical scope checking where read_write includes read access
def authorize_scope!(required_scope)
scopes = current_scopes
case required_scope.to_s
when "read"
# Read access requires either "read" or "read_write" scope
has_access = scopes.include?("read") || scopes.include?("read_write")
when "write"
# Write access requires "read_write" scope
has_access = scopes.include?("read_write")
else
# For any other scope, check exact match (backward compatibility)
has_access = scopes.include?(required_scope.to_s)
end
unless has_access
Rails.logger.warn "API Insufficient Scope: User #{current_resource_owner&.email} attempted to access #{required_scope} but only has #{scopes}"
render_json({ error: "insufficient_scope", message: "This action requires the '#{required_scope}' scope" }, status: :forbidden)
return false
end
true
end
# Consistent JSON response method
def render_json(data, status: :ok)
render json: data, status: status
end
# Error handlers
def handle_not_found(exception)
Rails.logger.warn "API Record Not Found: #{exception.message}"
render_json({ error: "record_not_found", message: "The requested resource was not found" }, status: :not_found)
end
def handle_unauthorized(exception)
Rails.logger.warn "API Unauthorized: #{exception.message}"
render_json({ error: "unauthorized", message: "Access token is invalid or expired" }, status: :unauthorized)
end
def handle_bad_request(exception)
Rails.logger.warn "API Bad Request: #{exception.message}"
render_json({ error: "bad_request", message: "Required parameters are missing or invalid" }, status: :bad_request)
end
# Log API access for monitoring and debugging
def log_api_access
return unless current_resource_owner
auth_info = case @authentication_method
when :oauth
"OAuth Token"
when :api_key
"API Key: #{@api_key.name}"
else
"Unknown"
end
Rails.logger.info "API Request: #{request.method} #{request.path} - User: #{current_resource_owner.email} (Family: #{current_resource_owner.family_id}) - Auth: #{auth_info}"
end
# Family-based access control helper (to be used by subcontrollers)
def ensure_current_family_access(resource)
return unless resource.respond_to?(:family_id)
unless resource.family_id == current_resource_owner.family_id
Rails.logger.warn "API Forbidden: User #{current_resource_owner.email} attempted to access resource from family #{resource.family_id}"
render_json({ error: "forbidden", message: "Access denied to this resource" }, status: :forbidden)
return false
end
true
end
# Manual doorkeeper_token accessor for compatibility with manual token verification
def doorkeeper_token
@_doorkeeper_token
end
# Set up Current context for API requests since we don't use session-based auth
def setup_current_context_for_api
# For API requests, we need to create a minimal session-like object
# or find/create an actual session for this user to make Current.user work
if @current_user
# Try to find an existing session for this user, or create a temporary one
session = @current_user.sessions.first
if session
Current.session = session
else
# Create a temporary session for this API request
# This won't be persisted but will allow Current.user to work
session = @current_user.sessions.build(
user_agent: request.user_agent,
ip_address: request.ip
)
Current.session = session
end
end
end
# Check if AI features are enabled for the current user
def require_ai_enabled
unless current_resource_owner&.ai_enabled?
render_json({ error: "feature_disabled", message: "AI features are not enabled for this user" }, status: :forbidden)
end
end
end

View file

@ -0,0 +1,84 @@
# frozen_string_literal: true
class Api::V1::ChatsController < Api::V1::BaseController
include Pagy::Backend
before_action :require_ai_enabled
before_action :ensure_read_scope, only: [ :index, :show ]
before_action :ensure_write_scope, only: [ :create, :update, :destroy ]
before_action :set_chat, only: [ :show, :update, :destroy ]
def index
@pagy, @chats = pagy(Current.user.chats.ordered, items: 20)
end
def show
return unless @chat
@pagy, @messages = pagy(@chat.messages.ordered, items: 50)
end
def create
@chat = Current.user.chats.build(title: chat_params[:title])
if @chat.save
if chat_params[:message].present?
@message = @chat.messages.build(
content: chat_params[:message],
type: "UserMessage",
ai_model: chat_params[:model] || "gpt-4"
)
if @message.save
AssistantResponseJob.perform_later(@message)
render :show, status: :created
else
@chat.destroy
render json: { error: "Failed to create initial message", details: @message.errors.full_messages }, status: :unprocessable_entity
end
else
render :show, status: :created
end
else
render json: { error: "Failed to create chat", details: @chat.errors.full_messages }, status: :unprocessable_entity
end
end
def update
return unless @chat
if @chat.update(update_chat_params)
render :show
else
render json: { error: "Failed to update chat", details: @chat.errors.full_messages }, status: :unprocessable_entity
end
end
def destroy
return unless @chat
@chat.destroy
head :no_content
end
private
def ensure_read_scope
authorize_scope!(:read)
end
def ensure_write_scope
authorize_scope!(:write)
end
def set_chat
@chat = Current.user.chats.find(params[:id])
rescue ActiveRecord::RecordNotFound
render json: { error: "Chat not found" }, status: :not_found
end
def chat_params
params.permit(:title, :message, :model)
end
def update_chat_params
params.permit(:title)
end
end

View file

@ -0,0 +1,55 @@
# frozen_string_literal: true
class Api::V1::MessagesController < Api::V1::BaseController
before_action :require_ai_enabled
before_action :ensure_write_scope, only: [ :create, :retry ]
before_action :set_chat
def create
@message = @chat.messages.build(
content: message_params[:content],
type: "UserMessage",
ai_model: message_params[:model] || "gpt-4"
)
if @message.save
AssistantResponseJob.perform_later(@message)
render :show, status: :created
else
render json: { error: "Failed to create message", details: @message.errors.full_messages }, status: :unprocessable_entity
end
end
def retry
last_message = @chat.messages.ordered.last
if last_message&.type == "AssistantMessage"
new_message = @chat.messages.create!(
type: "AssistantMessage",
content: "",
ai_model: last_message.ai_model
)
AssistantResponseJob.perform_later(new_message)
render json: { message: "Retry initiated", message_id: new_message.id }, status: :accepted
else
render json: { error: "No assistant message to retry" }, status: :unprocessable_entity
end
end
private
def ensure_write_scope
authorize_scope!(:write)
end
def set_chat
@chat = Current.user.chats.find(params[:chat_id])
rescue ActiveRecord::RecordNotFound
render json: { error: "Chat not found" }, status: :not_found
end
def message_params
params.permit(:content, :model)
end
end

View file

@ -0,0 +1,47 @@
# frozen_string_literal: true
# Test controller for API V1 Base Controller functionality
# This controller is only used for testing the base controller behavior
class Api::V1::TestController < Api::V1::BaseController
def index
render_json({ message: "test_success", user: current_resource_owner&.email })
end
def not_found
# Trigger RecordNotFound error for testing error handling
raise ActiveRecord::RecordNotFound, "Test record not found"
end
def family_access
# Test family-based access control
# Create a mock resource that belongs to a different family
mock_resource = OpenStruct.new(family_id: 999) # Different family ID
# Check family access - if it returns false, it already rendered the error
if ensure_current_family_access(mock_resource)
# If we get here, access was allowed
render_json({ family_id: current_resource_owner.family_id })
end
end
def scope_required
# Test scope authorization - require write scope
return unless authorize_scope!("write")
render_json({
message: "scope_authorized",
scopes: current_scopes,
required_scope: "write"
})
end
def multiple_scopes_required
# Test read scope requirement
return unless authorize_scope!("read")
render_json({
message: "read_scope_authorized",
scopes: current_scopes
})
end
end

View file

@ -0,0 +1,327 @@
# frozen_string_literal: true
class Api::V1::TransactionsController < Api::V1::BaseController
include Pagy::Backend
# Ensure proper scope authorization for read vs write access
before_action :ensure_read_scope, only: [ :index, :show ]
before_action :ensure_write_scope, only: [ :create, :update, :destroy ]
before_action :set_transaction, only: [ :show, :update, :destroy ]
def index
family = current_resource_owner.family
transactions_query = family.transactions.visible
# Apply filters
transactions_query = apply_filters(transactions_query)
# Apply search
transactions_query = apply_search(transactions_query) if params[:search].present?
# Include necessary associations for efficient queries
transactions_query = transactions_query.includes(
{ entry: :account },
:category, :merchant, :tags,
transfer_as_outflow: { inflow_transaction: { entry: :account } },
transfer_as_inflow: { outflow_transaction: { entry: :account } }
).reverse_chronological
# Handle pagination with Pagy
@pagy, @transactions = pagy(
transactions_query,
page: safe_page_param,
limit: safe_per_page_param
)
# Make per_page available to the template
@per_page = safe_per_page_param
# Rails will automatically use app/views/api/v1/transactions/index.json.jbuilder
render :index
rescue => e
Rails.logger.error "TransactionsController#index error: #{e.message}"
Rails.logger.error e.backtrace.join("\n")
render json: {
error: "internal_server_error",
message: "Error: #{e.message}"
}, status: :internal_server_error
end
def show
# Rails will automatically use app/views/api/v1/transactions/show.json.jbuilder
render :show
rescue => e
Rails.logger.error "TransactionsController#show error: #{e.message}"
Rails.logger.error e.backtrace.join("\n")
render json: {
error: "internal_server_error",
message: "Error: #{e.message}"
}, status: :internal_server_error
end
def create
family = current_resource_owner.family
# Validate account_id is present
unless transaction_params[:account_id].present?
render json: {
error: "validation_failed",
message: "Account ID is required",
errors: [ "Account ID is required" ]
}, status: :unprocessable_entity
return
end
account = family.accounts.find(transaction_params[:account_id])
@entry = account.entries.new(entry_params_for_create)
if @entry.save
@entry.sync_account_later
@entry.lock_saved_attributes!
@entry.transaction.lock_attr!(:tag_ids) if @entry.transaction.tags.any?
@transaction = @entry.transaction
render :show, status: :created
else
render json: {
error: "validation_failed",
message: "Transaction could not be created",
errors: @entry.errors.full_messages
}, status: :unprocessable_entity
end
rescue => e
Rails.logger.error "TransactionsController#create error: #{e.message}"
Rails.logger.error e.backtrace.join("\n")
render json: {
error: "internal_server_error",
message: "Error: #{e.message}"
}, status: :internal_server_error
end
def update
if @entry.update(entry_params_for_update)
@entry.sync_account_later
@entry.lock_saved_attributes!
@entry.transaction.lock_attr!(:tag_ids) if @entry.transaction.tags.any?
@transaction = @entry.transaction
render :show
else
render json: {
error: "validation_failed",
message: "Transaction could not be updated",
errors: @entry.errors.full_messages
}, status: :unprocessable_entity
end
rescue => e
Rails.logger.error "TransactionsController#update error: #{e.message}"
Rails.logger.error e.backtrace.join("\n")
render json: {
error: "internal_server_error",
message: "Error: #{e.message}"
}, status: :internal_server_error
end
def destroy
@entry.destroy!
@entry.sync_account_later
render json: {
message: "Transaction deleted successfully"
}, status: :ok
rescue => e
Rails.logger.error "TransactionsController#destroy error: #{e.message}"
Rails.logger.error e.backtrace.join("\n")
render json: {
error: "internal_server_error",
message: "Error: #{e.message}"
}, status: :internal_server_error
end
private
def set_transaction
family = current_resource_owner.family
@transaction = family.transactions.find(params[:id])
@entry = @transaction.entry
rescue ActiveRecord::RecordNotFound
render json: {
error: "not_found",
message: "Transaction not found"
}, status: :not_found
end
def ensure_read_scope
authorize_scope!(:read)
end
def ensure_write_scope
authorize_scope!(:write)
end
def apply_filters(query)
# Account filtering
if params[:account_id].present?
query = query.joins(:entry).where(entries: { account_id: params[:account_id] })
end
if params[:account_ids].present?
account_ids = Array(params[:account_ids])
query = query.joins(:entry).where(entries: { account_id: account_ids })
end
# Category filtering
if params[:category_id].present?
query = query.where(category_id: params[:category_id])
end
if params[:category_ids].present?
category_ids = Array(params[:category_ids])
query = query.where(category_id: category_ids)
end
# Merchant filtering
if params[:merchant_id].present?
query = query.where(merchant_id: params[:merchant_id])
end
if params[:merchant_ids].present?
merchant_ids = Array(params[:merchant_ids])
query = query.where(merchant_id: merchant_ids)
end
# Date range filtering
if params[:start_date].present?
query = query.joins(:entry).where("entries.date >= ?", Date.parse(params[:start_date]))
end
if params[:end_date].present?
query = query.joins(:entry).where("entries.date <= ?", Date.parse(params[:end_date]))
end
# Amount filtering
if params[:min_amount].present?
min_amount = params[:min_amount].to_f
query = query.joins(:entry).where("entries.amount >= ?", min_amount)
end
if params[:max_amount].present?
max_amount = params[:max_amount].to_f
query = query.joins(:entry).where("entries.amount <= ?", max_amount)
end
# Tag filtering
if params[:tag_ids].present?
tag_ids = Array(params[:tag_ids])
query = query.joins(:tags).where(tags: { id: tag_ids })
end
# Transaction type filtering (income/expense)
if params[:type].present?
case params[:type].downcase
when "income"
query = query.joins(:entry).where("entries.amount < 0")
when "expense"
query = query.joins(:entry).where("entries.amount > 0")
end
end
query
end
def apply_search(query)
search_term = "%#{params[:search]}%"
query.joins(:entry)
.left_joins(:merchant)
.where(
"entries.name ILIKE ? OR entries.notes ILIKE ? OR merchants.name ILIKE ?",
search_term, search_term, search_term
)
end
def transaction_params
params.require(:transaction).permit(
:account_id, :date, :amount, :name, :description, :notes, :currency,
:category_id, :merchant_id, :nature, tag_ids: []
)
end
def entry_params_for_create
entry_params = {
name: transaction_params[:name] || transaction_params[:description],
date: transaction_params[:date],
amount: calculate_signed_amount,
currency: transaction_params[:currency] || current_resource_owner.family.currency,
notes: transaction_params[:notes],
entryable_type: "Transaction",
entryable_attributes: {
category_id: transaction_params[:category_id],
merchant_id: transaction_params[:merchant_id],
tag_ids: transaction_params[:tag_ids] || []
}
}
entry_params.compact
end
def entry_params_for_update
entry_params = {
name: transaction_params[:name] || transaction_params[:description],
date: transaction_params[:date],
notes: transaction_params[:notes],
entryable_attributes: {
id: @entry.entryable_id,
category_id: transaction_params[:category_id],
merchant_id: transaction_params[:merchant_id],
tag_ids: transaction_params[:tag_ids]
}.compact_blank
}
# Only update amount if provided
if transaction_params[:amount].present?
entry_params[:amount] = calculate_signed_amount
end
entry_params.compact
end
def calculate_signed_amount
amount = transaction_params[:amount].to_f
nature = transaction_params[:nature]
case nature&.downcase
when "income", "inflow"
-amount.abs # Income is negative
when "expense", "outflow"
amount.abs # Expense is positive
else
amount # Use as provided
end
end
def safe_page_param
page = params[:page].to_i
page > 0 ? page : 1
end
def safe_per_page_param
per_page = params[:per_page].to_i
case per_page
when 1..100
per_page
else
25 # Default
end
end
end

View file

@ -0,0 +1,38 @@
class Api::V1::UsageController < Api::V1::BaseController
# GET /api/v1/usage
def show
return unless authorize_scope!(:read)
case @authentication_method
when :api_key
usage_info = @rate_limiter.usage_info
render_json({
api_key: {
name: @api_key.name,
scopes: @api_key.scopes,
last_used_at: @api_key.last_used_at,
created_at: @api_key.created_at
},
rate_limit: {
tier: usage_info[:tier],
limit: usage_info[:rate_limit],
current_count: usage_info[:current_count],
remaining: usage_info[:remaining],
reset_in_seconds: usage_info[:reset_time],
reset_at: Time.current + usage_info[:reset_time].seconds
}
})
when :oauth
# For OAuth, we don't track detailed usage yet, but we can return basic info
render_json({
authentication_method: "oauth",
message: "Detailed usage tracking is available for API key authentication"
})
else
render_json({
error: "invalid_authentication_method",
message: "Unable to determine usage information"
}, status: :bad_request)
end
end
end

View file

@ -1,27 +1,15 @@
class ApplicationController < ActionController::Base
include Onboardable, Localize, AutoSync, Authentication, Invitable, SelfHostable, StoreLocation, Impersonatable
include RestoreLayoutPreferences, Onboardable, Localize, AutoSync, Authentication, Invitable,
SelfHostable, StoreLocation, Impersonatable, Breadcrumbable,
FeatureGuardable, Notifiable
include Pagy::Backend
helper_method :require_upgrade?, :subscription_pending?
before_action :detect_os
before_action :set_default_chat
before_action :set_active_storage_url_options
private
def require_upgrade?
return false if self_hosted?
return false unless Current.session
return false if Current.family.subscribed?
return false if subscription_pending? || request.path == settings_billing_path
return false if Current.family.active_accounts_count <= 3
true
end
def subscription_pending?
subscribed_at = Current.session.subscribed_at
subscribed_at.present? && subscribed_at <= Time.current && subscribed_at > 1.hour.ago
end
def detect_os
user_agent = request.user_agent
@os = case user_agent
@ -33,4 +21,18 @@ class ApplicationController < ActionController::Base
else ""
end
end
# By default, we show the user the last chat they interacted with
def set_default_chat
@last_viewed_chat = Current.user&.last_viewed_chat
@chat = @last_viewed_chat
end
def set_active_storage_url_options
ActiveStorage::Current.url_options = {
protocol: request.protocol,
host: request.host,
port: request.optional_port
}
end
end

View file

@ -11,14 +11,14 @@ class BudgetCategoriesController < ApplicationController
if params[:id] == BudgetCategory.uncategorized.id
@budget_category = @budget.uncategorized_budget_category
@recent_transactions = @recent_transactions.where(account_transactions: { category_id: nil })
@recent_transactions = @recent_transactions.where(transactions: { category_id: nil })
else
@budget_category = Current.family.budget_categories.find(params[:id])
@recent_transactions = @recent_transactions.joins("LEFT JOIN categories ON categories.id = account_transactions.category_id")
@recent_transactions = @recent_transactions.joins("LEFT JOIN categories ON categories.id = transactions.category_id")
.where("categories.id = ? OR categories.parent_id = ?", @budget_category.category.id, @budget_category.category.id)
end
@recent_transactions = @recent_transactions.order("account_entries.date DESC, ABS(account_entries.amount) DESC").take(3)
@recent_transactions = @recent_transactions.order("entries.date DESC, ABS(entries.amount) DESC").take(3)
end
def update

View file

@ -25,6 +25,7 @@ class BudgetsController < ApplicationController
end
private
def budget_create_params
params.require(:budget).permit(:start_date)
end

View file

@ -56,8 +56,13 @@ class CategoriesController < ApplicationController
redirect_back_or_to categories_path, notice: t(".success")
end
def destroy_all
Current.family.categories.destroy_all
redirect_back_or_to categories_path, notice: "All categories deleted"
end
def bootstrap
Current.family.categories.bootstrap_defaults
Current.family.categories.bootstrap!
redirect_back_or_to categories_path, notice: t(".success")
end

View file

@ -0,0 +1,65 @@
class ChatsController < ApplicationController
include ActionView::RecordIdentifier
before_action :set_chat, only: [ :show, :edit, :update, :destroy ]
def index
@chat = nil # override application_controller default behavior of setting @chat to last viewed chat
@chats = Current.user.chats.order(created_at: :desc)
end
def show
set_last_viewed_chat(@chat)
end
def new
@chat = Current.user.chats.new(title: "New chat #{Time.current.strftime("%Y-%m-%d %H:%M")}")
end
def create
@chat = Current.user.chats.start!(chat_params[:content], model: chat_params[:ai_model])
set_last_viewed_chat(@chat)
redirect_to chat_path(@chat, thinking: true)
end
def edit
end
def update
@chat.update!(chat_params)
respond_to do |format|
format.html { redirect_back_or_to chat_path(@chat), notice: "Chat updated" }
format.turbo_stream { render turbo_stream: turbo_stream.replace(dom_id(@chat, :title), partial: "chats/chat_title", locals: { chat: @chat }) }
end
end
def destroy
@chat.destroy
clear_last_viewed_chat
redirect_to chats_path, notice: "Chat was successfully deleted"
end
def retry
@chat.retry_last_message!
redirect_to chat_path(@chat, thinking: true)
end
private
def set_chat
@chat = Current.user.chats.find(params[:id])
end
def set_last_viewed_chat(chat)
Current.user.update!(last_viewed_chat: chat)
end
def clear_last_viewed_chat
Current.user.update!(last_viewed_chat: nil)
end
def chat_params
params.require(:chat).permit(:title, :content, :ai_model)
end
end

View file

@ -2,10 +2,10 @@ module AccountableResource
extend ActiveSupport::Concern
included do
include ScrollFocusable
include Periodable
before_action :set_account, only: [ :show, :edit, :update, :destroy ]
before_action :set_link_token, only: :new
before_action :set_account, only: [ :show, :edit, :update ]
before_action :set_link_options, only: :new
end
class_methods do
@ -23,12 +23,11 @@ module AccountableResource
end
def show
@chart_view = params[:chart_view] || "balance"
@q = params.fetch(:q, {}).permit(:search)
entries = @account.entries.search(@q).reverse_chronological
set_focused_record(entries, params[:focused_record_id])
@pagy, @entries = pagy(entries, limit: params[:per_page] || "10", params: ->(params) { params.except(:focused_record_id) })
@pagy, @entries = pagy(entries, limit: params[:per_page] || "10")
end
def edit
@ -36,48 +35,39 @@ module AccountableResource
def create
@account = Current.family.accounts.create_and_sync(account_params.except(:return_to))
@account.lock_saved_attributes!
redirect_to account_params[:return_to].presence || @account, notice: t("accounts.create.success", type: accountable_type.name.underscore.humanize)
end
def update
@account.update_with_sync!(account_params.except(:return_to))
redirect_back_or_to @account, notice: t("accounts.update.success", type: accountable_type.name.underscore.humanize)
end
# Handle balance update if provided
if account_params[:balance].present?
result = @account.set_current_balance(account_params[:balance].to_d)
unless result.success?
@error_message = result.error_message
render :edit, status: :unprocessable_entity
return
end
@account.sync_later
end
def destroy
@account.destroy_later
redirect_to accounts_path, notice: t("accounts.destroy.success", type: accountable_type.name.underscore.humanize)
# Update remaining account attributes
update_params = account_params.except(:return_to, :balance, :currency)
unless @account.update(update_params)
@error_message = @account.errors.full_messages.join(", ")
render :edit, status: :unprocessable_entity
return
end
@account.lock_saved_attributes!
redirect_back_or_to account_path(@account), notice: t("accounts.update.success", type: accountable_type.name.underscore.humanize)
end
private
def set_link_token
@us_link_token = Current.family.get_link_token(
webhooks_url: plaid_us_webhooks_url,
redirect_url: accounts_url,
accountable_type: accountable_type.name,
region: :us
)
if Current.family.eu?
@eu_link_token = Current.family.get_link_token(
webhooks_url: plaid_eu_webhooks_url,
redirect_url: accounts_url,
accountable_type: accountable_type.name,
region: :eu
)
end
end
def plaid_us_webhooks_url
return webhooks_plaid_url if Rails.env.production?
ENV.fetch("DEV_WEBHOOKS_URL", root_url.chomp("/")) + "/webhooks/plaid"
end
def plaid_eu_webhooks_url
return webhooks_plaid_eu_url if Rails.env.production?
ENV.fetch("DEV_WEBHOOKS_URL", root_url.chomp("/")) + "/webhooks/plaid_eu"
def set_link_options
@show_us_link = Current.family.can_connect_plaid_us?
@show_eu_link = Current.family.can_connect_plaid_eu?
end
def accountable_type
@ -90,7 +80,7 @@ module AccountableResource
def account_params
params.require(:account).permit(
:name, :is_active, :balance, :subtype, :currency, :accountable_type, :return_to,
:name, :balance, :subtype, :currency, :accountable_type, :return_to,
accountable_attributes: self.class.permitted_accountable_attributes
)
end

View file

@ -4,11 +4,13 @@ module Authentication
included do
before_action :set_request_details
before_action :authenticate_user!
before_action :set_sentry_user
end
class_methods do
def skip_authentication(**options)
skip_before_action :authenticate_user!, **options
skip_before_action :set_sentry_user, **options
end
end
@ -26,7 +28,13 @@ module Authentication
end
def find_session_by_cookie
Session.find_by(id: cookies.signed[:session_token])
cookie_value = cookies.signed[:session_token]
if cookie_value.present?
Session.find_by(id: cookie_value)
else
nil
end
end
def create_session_for(user)
@ -43,4 +51,17 @@ module Authentication
Current.user_agent = request.user_agent
Current.ip_address = request.ip
end
def set_sentry_user
return unless defined?(Sentry) && ENV["SENTRY_DSN"].present?
if Current.user
Sentry.set_user(
id: Current.user.id,
email: Current.user.email,
username: Current.user.display_name,
ip_address: Current.ip_address
)
end
end
end

View file

@ -1,19 +1,22 @@
module AutoSync
extend ActiveSupport::Concern
included do
before_action :sync_family, if: :family_needs_auto_sync?
end
# included do
# before_action :sync_family, if: :family_needs_auto_sync?
# end
private
def sync_family
Current.family.update!(last_synced_at: Time.current)
Current.family.sync_later
end
def family_needs_auto_sync?
return false unless Current.family.present?
return false unless Current.family&.accounts&.active&.any?
return false if (Current.family.last_sync_created_at&.to_date || 1.day.ago) >= Date.current
return false unless Current.family.auto_sync_on_login
(Current.family.last_synced_at&.to_date || 1.day.ago) < Date.current
Rails.logger.info "Auto-syncing family #{Current.family.id}, last sync was #{Current.family.last_sync_created_at}"
true
end
end

View file

@ -0,0 +1,13 @@
module Breadcrumbable
extend ActiveSupport::Concern
included do
before_action :set_breadcrumbs
end
private
# The default, unless specific controller or action explicitly overrides
def set_breadcrumbs
@breadcrumbs = [ [ "Home", root_path ], [ controller_name.titleize, nil ] ]
end
end

View file

@ -2,14 +2,9 @@ module EntryableResource
extend ActiveSupport::Concern
included do
before_action :set_entry, only: %i[show update destroy]
end
include StreamExtensions, ActionView::RecordIdentifier
class_methods do
def permitted_entryable_attributes(*attrs)
@permitted_entryable_attributes = attrs if attrs.any?
@permitted_entryable_attributes ||= [ :id ]
end
before_action :set_entry, only: %i[show update destroy]
end
def show
@ -21,49 +16,16 @@ module EntryableResource
@entry = Current.family.entries.new(
account: account,
currency: account ? account.currency : Current.family.currency,
entryable: entryable_type.new
entryable: entryable
)
end
def create
@entry = build_entry
if @entry.save
@entry.sync_account_later
flash[:notice] = t("account.entries.create.success")
respond_to do |format|
format.html { redirect_back_or_to account_path(@entry.account) }
redirect_target_url = request.referer || account_path(@entry.account)
format.turbo_stream { render turbo_stream: turbo_stream.action(:redirect, redirect_target_url) }
end
else
render :new, status: :unprocessable_entity
end
raise NotImplementedError, "Entryable resources must implement #create"
end
def update
if @entry.update(update_entry_params)
@entry.sync_account_later
respond_to do |format|
format.html { redirect_back_or_to account_path(@entry.account), notice: t("account.entries.update.success") }
format.turbo_stream do
render turbo_stream: [
turbo_stream.replace(
"header_account_entry_#{@entry.id}",
partial: "#{entryable_type.name.underscore.pluralize}/header",
locals: { entry: @entry }
),
turbo_stream.replace("account_entry_#{@entry.id}", partial: "account/entries/entry", locals: { entry: @entry })
]
end
end
else
render :show, status: :unprocessable_entity
end
raise NotImplementedError, "Entryable resources must implement #update"
end
def destroy
@ -71,58 +33,15 @@ module EntryableResource
@entry.destroy!
@entry.sync_account_later
flash[:notice] = t("account.entries.destroy.success")
respond_to do |format|
format.html { redirect_back_or_to account_path(account) }
redirect_target_url = request.referer || account_path(@entry.account)
format.turbo_stream { render turbo_stream: turbo_stream.action(:redirect, redirect_target_url) }
end
redirect_back_or_to account_path(account), notice: t("account.entries.destroy.success")
end
private
def entryable_type
permitted_entryable_types = %w[Account::Transaction Account::Valuation Account::Trade]
klass = params[:entryable_type] || "Account::#{controller_name.classify}"
klass.constantize if permitted_entryable_types.include?(klass)
def entryable
controller_name.classify.constantize.new
end
def set_entry
@entry = Current.family.entries.find(params[:id])
end
def build_entry
Current.family.entries.new(create_entry_params)
end
def update_entry_params
prepared_entry_params
end
def create_entry_params
prepared_entry_params.merge({
entryable_type: entryable_type.name,
entryable_attributes: entry_params[:entryable_attributes] || {}
})
end
def prepared_entry_params
default_params = entry_params.except(:nature)
default_params = default_params.merge(entryable_type: entryable_type.name) if entry_params[:entryable_attributes].present?
if entry_params[:nature].present? && entry_params[:amount].present?
signed_amount = entry_params[:nature] == "inflow" ? -entry_params[:amount].to_d : entry_params[:amount].to_d
default_params = default_params.merge(amount: signed_amount)
end
default_params
end
def entry_params
params.require(:account_entry).permit(
:account_id, :name, :enriched_name, :date, :amount, :currency, :excluded, :notes, :nature,
entryable_attributes: self.class.permitted_entryable_attributes
)
end
end

View file

@ -0,0 +1,23 @@
# Simple feature guard that renders a 403 Forbidden status with a message
# when the feature is disabled.
#
# Example:
#
# class MessagesController < ApplicationController
# guard_feature unless: -> { Current.user.ai_enabled? }
# end
#
module FeatureGuardable
extend ActiveSupport::Concern
class_methods do
def guard_feature(**options)
before_action :guard_feature, **options
end
end
private
def guard_feature
render plain: "Feature disabled: #{controller_name}##{action_name}", status: :forbidden
end
end

View file

@ -0,0 +1,56 @@
module Notifiable
extend ActiveSupport::Concern
included do
helper_method :render_flash_notifications
helper_method :flash_notification_stream_items
end
private
def render_flash_notifications
notifications = flash.flat_map { |type, data| resolve_notifications(type, data) }.compact
view_context.safe_join(
notifications.map { |notification| view_context.render(**notification) }
)
end
def flash_notification_stream_items
items = flash.flat_map do |type, data|
notifications = resolve_notifications(type, data)
if type == "cta"
notifications.map { |notification| turbo_stream.replace("cta", **notification) }
else
notifications.map { |notification| turbo_stream.append("notification-tray", **notification) }
end
end.compact
# If rendering flash notifications via stream, we mark them as used to avoid
# them being rendered again on the next page load
flash.clear
items
end
def resolve_cta(cta)
case cta[:type]
when "category_rule"
{ partial: "rules/category_rule_cta", locals: { cta: } }
end
end
def resolve_notifications(type, data)
case type
when "alert"
[ { partial: "shared/notifications/alert", locals: { message: data } } ]
when "cta"
[ resolve_cta(data) ]
when "notice"
messages = Array(data)
messages.map { |message| { partial: "shared/notifications/notice", locals: { message: message } } }
else
[]
end
end
end

Some files were not shown because too many files have changed in this diff Show more