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

Compare commits

...

274 commits
v0.5.0 ... 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
1035 changed files with 38560 additions and 11226 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

@ -11,13 +11,11 @@ alwaysApply: true
- 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 under any circumstance do the following:
- 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
- Ignore i18n methods and files. Hardcode strings in English for now to optimize speed of development.
- Do not automatically run migrations

View file

@ -53,6 +53,7 @@ This codebase adopts a "skinny controller, fat models" convention. Furthermore,
- 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.
@ -65,56 +66,8 @@ All code should maximize readability and simplicity.
- Example 1: be mindful of loading large data payloads in global layouts
- Example 2: Avoid N+1 queries
### Convention 5: Use Minitest + Fixtures for testing, minimize fixtures
Due to the open-source nature of this project, we have chosen Minitest + Fixtures for testing to maximize familiarity and predictability.
- 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 [forward_calculator_test.rb](mdc:test/models/account/balance/forward_calculator_test.rb)
- Take a minimal approach to testing—only test the absolutely critical code paths that will significantly increase developer confidence
#### Convention 5a: 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
Account::Holding::Syncer.any_instance.expects(:sync_holdings).returns([]).once
@account.expects(:start_date).returns(2.days.ago.to_date)
Account::Balance::ForwardCalculator.any_instance.expects(:calculate).returns(
[
Account::Balance.new(date: 1.day.ago.to_date, balance: 1000, cash_balance: 1000, currency: "USD"),
Account::Balance.new(date: Date.current, balance: 1000, cash_balance: 1000, currency: "USD")
]
)
assert_difference "@account.balances.count", 2 do
Account::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 = Account::Balance.new(balance: 100, currency: "USD")
assert balance_record.save
end
```
### Convention 6: 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

View file

@ -55,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
@ -115,10 +115,10 @@ The most important type of sync is the account sync. It is orchestrated by the
- Auto-matches transfer records for the account
- 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/account/holding.rb), which uses [base_calculator.rb](mdc:app/models/account/holding/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
@ -126,7 +126,7 @@ 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
@ -247,10 +247,3 @@ class ConcreteProvider < Provider
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

@ -15,8 +15,8 @@ The codebase uses TailwindCSS v4.x (the newest version) with a custom design sys
- 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-primary`
- 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 generate semantic HTML
- 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

@ -51,6 +51,14 @@ APP_DOMAIN=
# Disable enforcing SSL connections
# DISABLE_SSL=true
# 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
# ======================================================================================================

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

39
.gitignore vendored
View file

@ -68,4 +68,41 @@ coverage
# Ignore node related files
node_modules
compose.yml
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,7 +9,7 @@ WORKDIR /rails
# Install base packages
RUN apt-get update -qq && \
apt-get install --no-install-recommends -y curl libvips postgresql-client
apt-get install --no-install-recommends -y curl libvips postgresql-client libyaml-0-2
# Set production environment
ARG BUILD_COMMIT_SHA
@ -23,7 +23,7 @@ ENV RAILS_ENV="production" \
FROM base AS build
# Install packages needed to build gems
RUN apt-get install --no-install-recommends -y build-essential libpq-dev git 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 ./

30
Gemfile
View file

@ -19,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"
# 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 "sidekiq"
gem "sidekiq-cron"
# Error logging
# 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"
@ -57,9 +70,13 @@ 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"
@ -72,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"
@ -79,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

@ -8,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)
@ -61,7 +63,7 @@ GEM
activemodel (= 7.2.2.1)
activesupport (= 7.2.2.1)
timeout (>= 0.4.0)
activerecord-import (2.1.0)
activerecord-import (2.2.0)
activerecord (>= 4.2)
activestorage (7.2.2.1)
actionpack (= 7.2.2.1)
@ -83,28 +85,31 @@ GEM
tzinfo (~> 2.0, >= 2.0.5)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
after_commit_everywhere (1.6.0)
activerecord (>= 4.2)
activesupport
ast (2.4.3)
aws-eventstream (1.3.2)
aws-partitions (1.1073.0)
aws-sdk-core (3.221.0)
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)
logger
aws-sdk-kms (1.99.0)
aws-sdk-core (~> 3, >= 3.216.0)
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)
@ -113,9 +118,9 @@ 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.2)
racc
@ -134,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.3)
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)
@ -158,30 +189,41 @@ GEM
rubocop (>= 1)
smart_properties
erubi (1.13.1)
et-orbi (1.2.11)
tzinfo
event_stream_parser (1.0.0)
faker (3.5.1)
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.3.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)
hashdiff (1.1.2)
hashdiff (1.2.0)
heapy (0.2.0)
thor
highline (3.1.2)
reline
hotwire-livereload (2.0.0)
@ -193,6 +235,8 @@ GEM
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.15)
@ -220,15 +264,18 @@ 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.2)
jwt (2.10.1)
json (2.12.2)
jwt (2.10.2)
base64
language_server-protocol (3.17.0.4)
language_server-protocol (3.17.0.5)
launchy (3.1.1)
addressable (~> 2.8)
childprocess (~> 5.0)
@ -251,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
@ -261,6 +320,9 @@ GEM
net-smtp
marcel (1.0.4)
matrix (0.4.2)
memory_profiler (1.1.0)
method_source (1.1.0)
mini_histogram (0.3.1)
mini_magick (5.2.0)
benchmark
logger
@ -270,9 +332,10 @@ GEM
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.6)
net-imap (0.5.8)
date
net-protocol
net-pop (0.1.2)
@ -282,32 +345,33 @@ GEM
net-smtp (0.5.1)
net-protocol
nio4r (2.7.4)
nokogiri (1.18.6-aarch64-linux-gnu)
nokogiri (1.18.8-aarch64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.6-aarch64-linux-musl)
nokogiri (1.18.8-aarch64-linux-musl)
racc (~> 1.4)
nokogiri (1.18.6-arm-linux-gnu)
nokogiri (1.18.8-arm-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.6-arm-linux-musl)
nokogiri (1.18.8-arm-linux-musl)
racc (~> 1.4)
nokogiri (1.18.6-arm64-darwin)
nokogiri (1.18.8-arm64-darwin)
racc (~> 1.4)
nokogiri (1.18.6-x86_64-darwin)
nokogiri (1.18.8-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.18.6-x86_64-linux-gnu)
nokogiri (1.18.8-x86_64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.6-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.4)
parallel (1.26.3)
parser (3.3.7.2)
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.1.0)
plaid (41.0.0)
faraday (>= 1.0.1, < 3.0)
faraday-multipart (>= 1.0.1, < 2.0)
platform_agent (1.0.1)
@ -322,17 +386,20 @@ GEM
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.12)
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)
@ -353,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)
@ -375,29 +442,31 @@ 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.9.1)
rbs (3.9.4)
logger
rdoc (6.13.0)
rdoc (6.14.2)
erb
psych (>= 4.0.0)
redcarpet (3.6.1)
redis (5.4.0)
redis-client (>= 0.22.0)
redis-client (0.24.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.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.74.0)
rqrcode_core (~> 2.0)
rqrcode_core (2.0.0)
rubocop (1.76.1)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
@ -405,38 +474,40 @@ GEM
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.38.0, < 2.0)
rubocop-ast (>= 1.45.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.41.0)
rubocop-ast (1.45.1)
parser (>= 3.3.7.2)
rubocop-performance (1.24.0)
prism (~> 1.4)
rubocop-performance (1.25.0)
lint_roller (~> 1.1)
rubocop (>= 1.72.1, < 2.0)
rubocop (>= 1.75.0, < 2.0)
rubocop-ast (>= 1.38.0, < 2.0)
rubocop-rails (2.30.3)
rubocop-rails (2.32.0)
activesupport (>= 4.2.0)
lint_roller (~> 1.1)
rack (>= 1.1)
rubocop (>= 1.72.1, < 2.0)
rubocop-ast (>= 1.38.0, < 2.0)
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.23.12)
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)
@ -445,58 +516,67 @@ GEM
addressable (>= 2.3.5)
faraday (>= 0.17.3, < 3)
securerandom (0.4.1)
selenium-webdriver (4.31.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.23.0)
sentry-rails (5.26.0)
railties (>= 5.0)
sentry-ruby (~> 5.23.0)
sentry-ruby (5.23.0)
sentry-ruby (~> 5.26.0)
sentry-ruby (5.26.0)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2)
sentry-sidekiq (5.23.0)
sentry-ruby (~> 5.23.0)
sentry-sidekiq (5.26.0)
sentry-ruby (~> 5.26.0)
sidekiq (>= 3.0)
sidekiq (8.0.2)
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.11953)
sorbet-runtime (0.5.12163)
stackprof (0.2.27)
stimulus-rails (1.3.4)
railties (>= 6.0.0)
stringio (3.1.5)
stripe (14.0.0)
tailwindcss-rails (4.2.1)
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.15)
tailwindcss-ruby (4.0.15-aarch64-linux-gnu)
tailwindcss-ruby (4.0.15-aarch64-linux-musl)
tailwindcss-ruby (4.0.15-arm64-darwin)
tailwindcss-ruby (4.0.15-x86_64-darwin)
tailwindcss-ruby (4.0.15-x86_64-linux-gnu)
tailwindcss-ruby (4.0.15-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.13)
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)
@ -504,7 +584,11 @@ GEM
useragent (0.16.11)
vcr (6.3.1)
base64
vernier (1.7.0)
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)
@ -515,13 +599,14 @@ GEM
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.2)
yard (0.9.37)
zeitwerk (2.7.3)
PLATFORMS
aarch64-linux-gnu
@ -534,7 +619,9 @@ PLATFORMS
x86_64-linux-musl
DEPENDENCIES
aasm
activerecord-import
after_commit_everywhere (~> 1.0)
aws-sdk-s3 (~> 1.177.0)
bcrypt (~> 3.1)
benchmark-ips
@ -544,12 +631,15 @@ DEPENDENCIES
climate_control
csv
debug
derailed_benchmarks
doorkeeper
dotenv-rails
erb_lint
faker
faraday
faraday-multipart
faraday-retry
foreman
hotwire-livereload
hotwire_combobox
i18n-tasks
@ -557,24 +647,28 @@ DEPENDENCIES
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
@ -583,7 +677,10 @@ DEPENDENCIES
sentry-ruby
sentry-sidekiq
sidekiq
sidekiq-cron
simplecov
skylight
stackprof
stimulus-rails
stripe
tailwindcss-rails
@ -591,11 +688,12 @@ DEPENDENCIES
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
css: bundle exec bin/rails tailwindcss:watch 2>/dev/null
worker: bundle exec sidekiq

View file

@ -1,7 +1,7 @@
<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>
@ -24,7 +24,7 @@ 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) - we're in alpha and release invites in our Discord
2. [Self-host with Docker](docs/hosting/docker.md)
@ -38,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
@ -59,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

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

View file

@ -1,85 +1,71 @@
<svg width="62" height="68" viewBox="0 0 62 68" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_f_7620_90382)">
<path d="M15.0109 27.3668C14.8138 11.2848 17.2087 15.4884 28.5797 15.5133L32.8675 15.5228C44.2383 15.5478 46.7179 11.3549 46.9149 27.4368L46.9891 33.5015C47.1861 49.5834 44.7913 53.0249 33.4205 52.9999L29.1325 52.9906C17.7617 52.9656 15.2823 49.5134 15.0852 33.4315L15.0109 27.3668Z" fill="url(#paint0_linear_7620_90382)" fill-opacity="0.15"/>
</g>
<g filter="url(#filter1_i_7620_90382)">
<rect x="15" y="13" width="32" height="32" rx="10.6667" fill="url(#paint1_linear_7620_90382)"/>
<rect x="15" y="13" width="32" height="32" rx="10.6667" fill="white" fill-opacity="0.07" style="mix-blend-mode:plus-lighter"/>
</g>
<g filter="url(#filter2_ii_7620_90382)">
<rect x="16.7773" y="14.7778" width="28.4444" height="28.4444" rx="8.88889" fill="url(#paint2_linear_7620_90382)"/>
<path d="M36.1921 22.073C36.6039 22.0652 36.9439 22.3927 36.9517 22.8044C36.9786 24.2352 37.0273 25.6596 37.0958 27.088C37.1155 27.4993 36.7981 27.8487 36.3868 27.8684C35.9755 27.8881 35.6261 27.5707 35.6063 27.1594C35.5372 25.7174 35.488 24.2785 35.4607 22.8325C35.453 22.4208 35.7804 22.0807 36.1921 22.073Z" fill="#141414"/>
<path d="M36.1921 22.073C36.6039 22.0652 36.9439 22.3927 36.9517 22.8044C36.9786 24.2352 37.0273 25.6596 37.0958 27.088C37.1155 27.4993 36.7981 27.8487 36.3868 27.8684C35.9755 27.8881 35.6261 27.5707 35.6063 27.1594C35.5372 25.7174 35.488 24.2785 35.4607 22.8325C35.453 22.4208 35.7804 22.0807 36.1921 22.073Z" fill="url(#paint3_linear_7620_90382)"/>
<path d="M30.0884 22.7413C30.3247 22.4041 30.2428 21.9392 29.9056 21.7029C29.5684 21.4666 29.1034 21.5484 28.8671 21.8857C28.2555 22.7586 27.6031 23.6183 26.9349 24.4988C26.6795 24.8354 26.4217 25.1751 26.1631 25.5196C26.1629 25.4497 26.1627 25.379 26.1622 25.3074C26.158 24.7 26.1364 24.0511 26.0192 23.3998C25.9463 22.9946 25.5586 22.7251 25.1533 22.7981C24.7481 22.871 24.4787 23.2587 24.5516 23.6639C24.6454 24.185 24.667 24.7297 24.671 25.3176C24.672 25.4628 24.6719 25.6127 24.6718 25.7658C24.6714 26.2118 24.6709 26.6845 24.6985 27.1431C24.7046 27.2459 24.7313 27.3426 24.7744 27.4294C24.0609 28.4557 23.3903 29.5154 22.8297 30.615C22.8221 30.6299 22.8109 30.6511 22.7968 30.6775C22.7112 30.8389 22.5209 31.1974 22.4089 31.5427C22.3452 31.7392 22.2741 32.0229 22.3076 32.3158C22.3255 32.4722 22.3762 32.6577 22.4979 32.8323C22.6242 33.0135 22.7989 33.142 22.9963 33.2166C24.2085 33.6749 25.5494 33.7216 26.818 33.625C27.848 33.5466 28.8878 33.3675 29.8142 33.2078C30.0261 33.1713 30.2321 33.1358 30.4306 33.1028C30.8368 33.0352 31.1113 32.6512 31.0438 32.245C30.9762 31.8388 30.5921 31.5643 30.1859 31.6318C29.9702 31.6677 29.7524 31.7052 29.5328 31.743C28.6101 31.9017 27.6572 32.0656 26.7048 32.1381C25.6662 32.2172 24.6958 32.1801 23.852 31.9317C23.9193 31.7487 24.0152 31.566 24.0967 31.4107C24.1185 31.3691 24.1393 31.3294 24.1582 31.2923C24.909 29.8195 25.8865 28.397 26.9361 26.9776C27.3129 26.4681 27.7032 25.9536 28.0947 25.4375C28.7774 24.5377 29.4637 23.6329 30.0884 22.7413Z" fill="#141414"/>
<path d="M30.0884 22.7413C30.3247 22.4041 30.2428 21.9392 29.9056 21.7029C29.5684 21.4666 29.1034 21.5484 28.8671 21.8857C28.2555 22.7586 27.6031 23.6183 26.9349 24.4988C26.6795 24.8354 26.4217 25.1751 26.1631 25.5196C26.1629 25.4497 26.1627 25.379 26.1622 25.3074C26.158 24.7 26.1364 24.0511 26.0192 23.3998C25.9463 22.9946 25.5586 22.7251 25.1533 22.7981C24.7481 22.871 24.4787 23.2587 24.5516 23.6639C24.6454 24.185 24.667 24.7297 24.671 25.3176C24.672 25.4628 24.6719 25.6127 24.6718 25.7658C24.6714 26.2118 24.6709 26.6845 24.6985 27.1431C24.7046 27.2459 24.7313 27.3426 24.7744 27.4294C24.0609 28.4557 23.3903 29.5154 22.8297 30.615C22.8221 30.6299 22.8109 30.6511 22.7968 30.6775C22.7112 30.8389 22.5209 31.1974 22.4089 31.5427C22.3452 31.7392 22.2741 32.0229 22.3076 32.3158C22.3255 32.4722 22.3762 32.6577 22.4979 32.8323C22.6242 33.0135 22.7989 33.142 22.9963 33.2166C24.2085 33.6749 25.5494 33.7216 26.818 33.625C27.848 33.5466 28.8878 33.3675 29.8142 33.2078C30.0261 33.1713 30.2321 33.1358 30.4306 33.1028C30.8368 33.0352 31.1113 32.6512 31.0438 32.245C30.9762 31.8388 30.5921 31.5643 30.1859 31.6318C29.9702 31.6677 29.7524 31.7052 29.5328 31.743C28.6101 31.9017 27.6572 32.0656 26.7048 32.1381C25.6662 32.2172 24.6958 32.1801 23.852 31.9317C23.9193 31.7487 24.0152 31.566 24.0967 31.4107C24.1185 31.3691 24.1393 31.3294 24.1582 31.2923C24.909 29.8195 25.8865 28.397 26.9361 26.9776C27.3129 26.4681 27.7032 25.9536 28.0947 25.4375C28.7774 24.5377 29.4637 23.6329 30.0884 22.7413Z" fill="url(#paint4_linear_7620_90382)"/>
<path d="M36.2391 34.7581C36.3664 34.3664 36.1522 33.9458 35.7606 33.8185C35.369 33.6911 34.9483 33.9054 34.821 34.297C34.6438 34.842 34.4106 35.256 34.12 35.541C33.8419 35.8137 33.4787 36.0015 32.9619 36.0499C32.1922 36.1221 31.4116 35.7978 31.071 35.2344C30.858 34.882 30.3996 34.7691 30.0472 34.9821C29.6948 35.1951 29.5818 35.6535 29.7949 36.0059C30.5079 37.1855 31.9259 37.6448 33.1011 37.5346C33.9477 37.4552 34.6338 37.1258 35.1641 36.6056C35.6819 36.0978 36.0163 35.4433 36.2391 34.7581Z" fill="#141414"/>
<path d="M36.2391 34.7581C36.3664 34.3664 36.1522 33.9458 35.7606 33.8185C35.369 33.6911 34.9483 33.9054 34.821 34.297C34.6438 34.842 34.4106 35.256 34.12 35.541C33.8419 35.8137 33.4787 36.0015 32.9619 36.0499C32.1922 36.1221 31.4116 35.7978 31.071 35.2344C30.858 34.882 30.3996 34.7691 30.0472 34.9821C29.6948 35.1951 29.5818 35.6535 29.7949 36.0059C30.5079 37.1855 31.9259 37.6448 33.1011 37.5346C33.9477 37.4552 34.6338 37.1258 35.1641 36.6056C35.6819 36.0978 36.0163 35.4433 36.2391 34.7581Z" fill="url(#paint5_linear_7620_90382)"/>
</g>
<defs>
<filter id="filter0_f_7620_90382" x="0.937778" y="0.937778" width="60.1244" height="66.1244" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="7.03111" result="effect1_foregroundBlur_7620_90382"/>
</filter>
<filter id="filter1_i_7620_90382" x="15" y="13" 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_7620_90382"/>
</filter>
<filter id="filter2_ii_7620_90382" x="16.7773" y="13.8889" width="28.4453" height="30.2222" 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_7620_90382"/>
<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_7620_90382" result="effect2_innerShadow_7620_90382"/>
</filter>
<linearGradient id="paint0_linear_7620_90382" x1="30.1185" y1="16.1417" x2="33.7041" y2="53.1754" 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_7620_90382" x1="31" y1="13" x2="31" y2="45" 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="paint2_linear_7620_90382" x1="30.9996" y1="23.6667" x2="30.9996" y2="43.2222" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="0.3" stop-color="#F7F7F7"/>
</linearGradient>
<linearGradient id="paint3_linear_7620_90382" x1="32.5419" y1="21.0645" x2="28.4008" y2="36.5193" 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_7620_90382" x1="32.5419" y1="21.0645" x2="28.4008" y2="36.5193" 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="paint5_linear_7620_90382" x1="32.5419" y1="21.0645" x2="28.4008" y2="36.5193" 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>
<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>

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

Before After
Before After

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

@ -20,7 +20,6 @@
}
.pcr-color-palette{
height: 12em !important;
width: 21.5rem !important;
}
.pcr-palette{
border-radius: 10px !important;
@ -40,7 +39,7 @@
.combobox {
.hw-combobox__main__wrapper,
.hw-combobox__input {
@apply w-full;
@apply bg-container text-primary w-full;
}
.hw-combobox__main__wrapper {
@ -54,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;
@ -71,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 {
@ -165,5 +172,4 @@
&::-webkit-scrollbar-thumb:hover {
background: #a6a6a6;
}
}
/* The following Markdown CSS has been removed as requested */
}

View file

@ -5,6 +5,12 @@
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 {
@ -18,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;
@ -217,264 +229,48 @@
--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;
@variant theme-dark {
@apply text-white;
}
}
@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;
}
}
@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-800;
}
}
@utility bg-surface-inset {
@apply bg-gray-100;
@variant theme-dark {
@apply bg-gray-900;
}
}
@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: rgba(var(--color-gray-100), 0.5);
@variant theme-dark {
background-color: var(--color-alpha-black-900);
}
}
@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-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;
}
}
/* Foreground Colors */
@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;
}
/* 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 {
[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;
}
@ -483,17 +279,24 @@
@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;
}
input[type='radio'] {
@apply border-gray-300 text-indigo-600 focus:ring-indigo-600; /* Default light mode */
@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;
@ -502,66 +305,12 @@
}
@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;
@apply transition-all duration-300;
}
.btn--primary {
@apply button-bg-primary text-white disabled:text-gray-400;
@apply hover:button-bg-primary-hover;
@apply disabled:button-bg-disabled disabled:hover:button-bg-disabled;
@variant theme-dark {
@apply button-bg-primary fg-primary;
@apply hover:button-bg-primary-hover;
@apply disabled:button-bg-disabled disabled:hover:button-bg-disabled;
}
}
.btn--secondary {
@apply button-bg-secondary text-primary;
@apply hover:button-bg-secondary-hover;
@variant theme-dark {
@apply button-bg-secondary text-white;
@apply hover:button-bg-secondary-hover;
}
}
.btn--outline {
@apply border border-alpha-black-200 text-primary disabled:button-bg-disabled disabled:hover:button-bg-disabled disabled:text-gray-400;
@apply hover:button-bg-outline-hover;
@variant theme-dark {
@apply border-alpha-white-300 text-white disabled:button-bg-disabled disabled:hover:button-bg-disabled disabled:text-gray-600;
@apply hover:button-bg-outline-hover;
}
}
.btn--ghost {
@apply border border-transparent text-primary hover:button-bg-ghost-hover;
@variant theme-dark {
@apply fg-primary hover:button-bg-ghost-hover;
}
}
.btn--destructive {
@apply button-bg-destructive text-white hover:button-bg-destructive-hover disabled:button-bg-disabled disabled:hover:button-bg-disabled disabled:text-red-400;
@variant theme-dark {
@apply button-bg-destructive text-white hover:button-bg-destructive-hover disabled:button-bg-disabled disabled:hover:button-bg-disabled;
}
}
/* Forms */
.form-field {
@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;
}
@ -569,43 +318,68 @@
/* 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-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-subdued;
@apply text-ellipsis overflow-hidden whitespace-nowrap;
@apply transition-opacity duration-300;
&select {
@apply pr-8;
@apply placeholder:text-subdued;
@variant theme-dark {
&::-webkit-calendar-picker-indicator {
filter: invert(1);
cursor: pointer;
}
}
}
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-surface p-3 text-center text-white hover:bg-surface-hover;
}
@ -622,142 +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;
@apply after:transition-transform after:duration-300 after:ease-in-out;
@apply peer-checked:bg-green-600 peer-checked:after:translate-x-4;
@apply transition-colors duration-300;
}
/* Tooltips */
.tooltip {
@apply hidden absolute;
}
}
@layer utilities {
/* Specific override for strong tags in prose under dark mode */
.prose:where([data-theme=dark], [data-theme=dark] *) strong {
color: theme(colors.white) !important;
}
}
/* 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;
}
}
@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;
.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;
}
}

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, :manual_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,5 @@
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
@ -9,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
@ -17,23 +26,35 @@ class AccountsController < ApplicationController
redirect_to account_path(@account)
end
def chart
@chart_view = params[:chart_view] || "balance"
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,28 +1,15 @@
class ApplicationController < ActionController::Base
include Onboardable, Localize, AutoSync, Authentication, Invitable, SelfHostable, StoreLocation, Impersonatable, Breadcrumbable, FeatureGuardable
include Pagy::Backend
include RestoreLayoutPreferences, Onboardable, Localize, AutoSync, Authentication, Invitable,
SelfHostable, StoreLocation, Impersonatable, Breadcrumbable,
FeatureGuardable, Notifiable
helper_method :require_upgrade?, :subscription_pending?
include Pagy::Backend
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
@ -40,4 +27,12 @@ class ApplicationController < ActionController::Base
@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

@ -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

@ -1,8 +1,6 @@
class ChatsController < ApplicationController
include ActionView::RecordIdentifier
guard_feature unless: -> { Current.user.ai_enabled? }
before_action :set_chat, only: [ :show, :edit, :update, :destroy ]
def index

View file

@ -2,10 +2,10 @@ module AccountableResource
extend ActiveSupport::Concern
included do
include ScrollFocusable, Periodable
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
@ -27,9 +27,7 @@ module AccountableResource
@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
@ -37,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
@ -91,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

@ -1,20 +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 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

@ -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,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

View file

@ -2,16 +2,36 @@ module Onboardable
extend ActiveSupport::Concern
included do
before_action :redirect_to_onboarding, if: :needs_onboarding?
before_action :require_onboarding_and_upgrade
end
private
def redirect_to_onboarding
redirect_to onboarding_path
# First, we require onboarding, then once that's complete, we require an upgrade for non-subscribed users.
def require_onboarding_and_upgrade
return unless Current.user
return unless redirectable_path?(request.path)
if Current.user.needs_onboarding?
redirect_to onboarding_path
elsif Current.family.needs_subscription?
redirect_to trial_onboarding_path
elsif Current.family.upgrade_required?
redirect_to upgrade_subscription_path
end
end
def needs_onboarding?
Current.user && Current.user.onboarded_at.blank? &&
!%w[/users /onboarding /sessions].any? { |path| request.path.start_with?(path) }
def redirectable_path?(path)
return false if path.starts_with?("/settings")
return false if path.starts_with?("/subscription")
return false if path.starts_with?("/onboarding")
return false if path.starts_with?("/users")
return false if path.starts_with?("/api") # Exclude API endpoints from onboarding redirects
[
new_registration_path,
new_session_path,
new_password_reset_path,
new_email_confirmation_path
].exclude?(path)
end
end

View file

@ -0,0 +1,24 @@
module RestoreLayoutPreferences
extend ActiveSupport::Concern
included do
before_action :restore_active_tabs
end
private
def restore_active_tabs
last_selected_tab = Current.session&.get_preferred_tab("account_sidebar_tab") || "asset"
@account_group_tab = account_group_tab_param || last_selected_tab
end
def valid_account_group_tabs
%w[asset liability all]
end
def account_group_tab_param
param_value = params[:account_sidebar_tab]
return nil unless param_value.in?(valid_account_group_tabs)
param_value
end
end

View file

@ -1,21 +0,0 @@
module ScrollFocusable
extend ActiveSupport::Concern
def set_focused_record(record_scope, record_id, default_per_page: 10)
return unless record_id.present?
@focused_record = record_scope.find_by(id: record_id)
record_index = record_scope.pluck(:id).index(record_id)
return unless record_index
page_of_focused_record = (record_index / (params[:per_page]&.to_i || default_per_page)) + 1
if params[:page]&.to_i != page_of_focused_record
(
redirect_to(url_for(page: page_of_focused_record, focused_record_id: record_id))
)
end
end
end

View file

@ -3,6 +3,8 @@ module SelfHostable
included do
helper_method :self_hosted?, :self_hosted_first_login?
prepend_before_action :verify_self_host_config
end
private
@ -13,4 +15,29 @@ module SelfHostable
def self_hosted_first_login?
self_hosted? && User.count.zero?
end
def verify_self_host_config
return unless self_hosted?
# Special handling for Redis configuration error page
if controller_name == "pages" && action_name == "redis_configuration_error"
# If Redis is now working, redirect to home
if redis_connected?
redirect_to root_path, notice: "Redis is now configured properly! You can now setup your Maybe application."
end
return
end
unless redis_connected?
redirect_to redis_configuration_error_path
end
end
def redis_connected?
Redis.new.ping
true
rescue Redis::CannotConnectError
false
end
end

View file

@ -0,0 +1,20 @@
module StreamExtensions
extend ActiveSupport::Concern
def stream_redirect_to(path, notice: nil, alert: nil)
custom_stream_redirect(path, notice: notice, alert: alert)
end
def stream_redirect_back_or_to(path, notice: nil, alert: nil)
custom_stream_redirect(path, redirect_back: true, notice: notice, alert: alert)
end
private
def custom_stream_redirect(path, redirect_back: false, notice: nil, alert: nil)
flash[:notice] = notice if notice.present?
flash[:alert] = alert if alert.present?
redirect_target_url = redirect_back ? request.referer : path
render turbo_stream: turbo_stream.action(:redirect, redirect_target_url)
end
end

View file

@ -0,0 +1,22 @@
class CookieSessionsController < ApplicationController
def update
save_kv_to_session(
cookie_session_params[:tab_key],
cookie_session_params[:tab_value]
)
redirect_back_or_to root_path
end
private
def cookie_session_params
params.require(:cookie_session).permit(:tab_key, :tab_value)
end
def save_kv_to_session(key, value)
raise "Key must be a string" unless key.is_a?(String)
raise "Value must be a string" unless value.is_a?(String)
session["custom_#{key}"] = value
end
end

View file

@ -0,0 +1,14 @@
class CurrentSessionsController < ApplicationController
def update
if session_params[:tab_key].present? && session_params[:tab_value].present?
Current.session.set_preferred_tab(session_params[:tab_key], session_params[:tab_value])
end
head :ok
end
private
def session_params
params.require(:current_session).permit(:tab_key, :tab_value)
end
end

View file

@ -0,0 +1,53 @@
class FamilyMerchantsController < ApplicationController
before_action :set_merchant, only: %i[edit update destroy]
def index
@breadcrumbs = [ [ "Home", root_path ], [ "Merchants", nil ] ]
@family_merchants = Current.family.merchants.alphabetically
render layout: "settings"
end
def new
@family_merchant = FamilyMerchant.new(family: Current.family)
end
def create
@family_merchant = FamilyMerchant.new(merchant_params.merge(family: Current.family))
if @family_merchant.save
respond_to do |format|
format.html { redirect_to family_merchants_path, notice: t(".success") }
format.turbo_stream { render turbo_stream: turbo_stream.action(:redirect, family_merchants_path) }
end
else
render :new, status: :unprocessable_entity
end
end
def edit
end
def update
@family_merchant.update!(merchant_params)
respond_to do |format|
format.html { redirect_to family_merchants_path, notice: t(".success") }
format.turbo_stream { render turbo_stream: turbo_stream.action(:redirect, family_merchants_path) }
end
end
def destroy
@family_merchant.destroy!
redirect_to family_merchants_path, notice: t(".success")
end
private
def set_merchant
@family_merchant = Current.family.merchants.find(params[:id])
end
def merchant_params
params.require(:family_merchant).permit(:name, :color)
end
end

View file

@ -1,4 +1,4 @@
class Account::HoldingsController < ApplicationController
class HoldingsController < ApplicationController
before_action :set_holding, only: %i[show destroy]
def index

View file

@ -36,7 +36,9 @@ class Import::ConfigurationsController < ApplicationController
:currency_col_label,
:date_format,
:number_format,
:signage_convention
:signage_convention,
:amount_type_strategy,
:amount_type_inflow_value,
)
end
end

View file

@ -6,6 +6,13 @@ class Import::UploadsController < ApplicationController
def show
end
def sample_csv
send_data @import.csv_template.to_csv,
filename: "#{@import.type.underscore.split('_').first}_sample.csv",
type: "text/csv",
disposition: "attachment"
end
def update
if csv_valid?(csv_str)
@import.account = Current.family.accounts.find_by(id: params.dig(:import, :account_id))

View file

@ -5,6 +5,8 @@ class ImportsController < ApplicationController
@import.publish_later
redirect_to import_path(@import), notice: "Your import has started in the background."
rescue Import::MaxRowCountExceededError
redirect_back_or_to import_path(@import), alert: "Your import exceeds the maximum row count of #{@import.max_row_count}."
end
def index

View file

@ -2,6 +2,6 @@ class LoansController < ApplicationController
include AccountableResource
permitted_accountable_attributes(
:id, :rate_type, :interest_rate, :term_months
:id, :rate_type, :interest_rate, :term_months, :initial_balance
)
end

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