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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .cursor/rules/project-conventions.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ This rule serves as high-level documentation for how you should write code in th
- TailwindCSS for styles
- Lucide Icons for icons
- OpenAI for AI chat
- Database: PostgreSQL
- Jobs: Sidekiq + Redis
- Database: Sqlite3
- Jobs: SolidQueue
- External
- Payments: Stripe
- User bank data syncing: Plaid
Expand Down
6 changes: 3 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Common Development Commands

### Development Server
- `bin/dev` - Start development server (Rails, Sidekiq, Tailwind CSS watcher)
- `bin/dev` - Start development server (Rails, Solid queue, Tailwind CSS watcher)
- `bin/rails server` - Start Rails server only
- `bin/rails console` - Open Rails console

Expand Down Expand Up @@ -126,11 +126,11 @@ Provider support notes:
- Lunchflow: does not currently store pending metadata.

### Background Processing
Sidekiq handles asynchronous tasks:
SolidQueue handles asynchronous tasks:
- Account syncing (`SyncJob`)
- Import processing (`ImportJob`)
- AI chat responses (`AssistantResponseJob`)
- Scheduled maintenance via sidekiq-cron
- Scheduled maintenance via SolidQueue

### Frontend Architecture
- **Hotwire Stack**: Turbo + Stimulus for reactive UI without heavy JavaScript
Expand Down
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,6 @@ The instructions below are for developers to get started with contributing to th
### Requirements

- See `.ruby-version` file for required Ruby version
- PostgreSQL >9.3 (latest stable version recommended)
- Redis > 5.4 (latest stable version recommended)

### Getting Started
```sh
Expand Down
2 changes: 1 addition & 1 deletion app/channels/application_cable/connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ class Connection < ActionCable::Connection::Base

private
def report_error(e)
Sentry.capture_exception(e)
Rails.error.report(error)
end
end
end
9 changes: 6 additions & 3 deletions app/controllers/categories_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ class CategoriesController < ApplicationController

def index
@categories = Current.family.categories.alphabetically

render layout: "settings"
end

Expand All @@ -15,8 +14,12 @@ def new
end

def create
@category = Current.family.categories.new(category_params)

params = if category_params[:parent_id].blank?
category_params.except(:parent_id)
else
category_params
end
@category = Current.family.categories.new(params)
if @category.save
@transaction.update(category_id: @category.id) if @transaction

Expand Down
1 change: 1 addition & 0 deletions app/controllers/concerns/onboardable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ def redirectable_path?(path)
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?("/jobs")
return false if path.starts_with?("/api") # Exclude API endpoints from onboarding redirects

[
Expand Down
27 changes: 0 additions & 27 deletions app/controllers/concerns/self_hostable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ module SelfHostable

included do
helper_method :self_hosted?, :self_hosted_first_login?

prepend_before_action :verify_self_host_config
end

private
Expand All @@ -15,29 +13,4 @@ def self_hosted?
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 Sure 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
15 changes: 0 additions & 15 deletions app/controllers/pages_controller.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
class PagesController < ApplicationController
include Periodable

skip_authentication only: %i[redis_configuration_error privacy terms]
before_action :ensure_intro_guest!, only: :intro

def dashboard
if Current.user&.ui_layout_intro?
redirect_to chats_path and return
Expand Down Expand Up @@ -62,18 +59,6 @@ def feedback
render layout: "settings"
end

def redis_configuration_error
render layout: "blank"
end

def privacy
render layout: "blank"
end

def terms
render layout: "blank"
end

private
def preferences_params
prefs = params.require(:preferences)
Expand Down
53 changes: 24 additions & 29 deletions app/controllers/webhooks_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,57 +3,52 @@ class WebhooksController < ApplicationController
skip_authentication

def plaid
webhook_body = request.body.read
plaid_verification_header = request.headers["Plaid-Verification"]
Rails.error.handle(fallback: -> {
render json: { error: "Invalid webhook:" }, status: :bad_request
}) do
webhook_body = request.body.read
plaid_verification_header = request.headers["Plaid-Verification"]

client = Provider::Registry.plaid_provider_for_region(:us)
client = Provider::Registry.plaid_provider_for_region(:us)

client.validate_webhook!(plaid_verification_header, webhook_body)
client.validate_webhook!(plaid_verification_header, webhook_body)

PlaidItem::WebhookProcessor.new(webhook_body).process
PlaidItem::WebhookProcessor.new(webhook_body).process

render json: { received: true }, status: :ok
rescue => error
Sentry.capture_exception(error)
Rails.logger.error("Webhook error: #{error.class} - #{error.message}")
render json: { error: "Invalid webhook" }, status: :bad_request
render json: { received: true }, status: :ok
end
end

def plaid_eu
webhook_body = request.body.read
plaid_verification_header = request.headers["Plaid-Verification"]
Rails.error.handle(fallback: -> {
render json: { error: "Invalid webhook" }, status: :bad_request
}) do
webhook_body = request.body.read
plaid_verification_header = request.headers["Plaid-Verification"]

client = Provider::Registry.plaid_provider_for_region(:eu)
client = Provider::Registry.plaid_provider_for_region(:eu)

client.validate_webhook!(plaid_verification_header, webhook_body)
client.validate_webhook!(plaid_verification_header, webhook_body)

PlaidItem::WebhookProcessor.new(webhook_body).process
PlaidItem::WebhookProcessor.new(webhook_body).process

render json: { received: true }, status: :ok
rescue => error
Sentry.capture_exception(error)
Rails.logger.error("Webhook error: #{error.class} - #{error.message}")
render json: { error: "Invalid webhook" }, status: :bad_request
render json: { received: true }, status: :ok
end
end

def stripe
stripe_provider = Provider::Registry.get_provider(:stripe)

begin
Rails.error.handle(JSON::ParserError, Stripe::SignatureVerificationError, fallback: -> {
Rails.logger.error "Stripe webhook processing error"
head :bad_request
}) do
webhook_body = request.body.read
sig_header = request.env["HTTP_STRIPE_SIGNATURE"]

stripe_provider.process_webhook_later(webhook_body, sig_header)

head :ok
rescue JSON::ParserError => error
Sentry.capture_exception(error)
Rails.logger.error "JSON parser error: #{error.message}"
head :bad_request
rescue Stripe::SignatureVerificationError => error
Sentry.capture_exception(error)
Rails.logger.error "Stripe signature verification error: #{error.message}"
head :bad_request
end
end
end
57 changes: 57 additions & 0 deletions app/jobs/seed_dump_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
class SeedDumpJob < ApplicationJob
queue_as :scheduled

def perform
return if Rails.env.development?

models = [
"Setting",
"ActiveStorage::VariantRecord",
"ActiveStorage::Attachment",
"ActiveStorage::Blob",
"Security::Price",
"Rule::Condition",
"Rule::Action",
"TransactionImport",
"Import::Row",
"Valuation",
"User",
"Transfer",
"Transaction",
"Trade",
"Tagging",
"Sync",
"Session",
"Security",
"Rule",
"RejectedTransfer",
"Investment",
"Holding",
"Family",
"ExchangeRate",
"Entry",
"Depository",
"DataEnrichment",
"Crypto",
"CreditCard",
"Chat",
"BudgetCategory",
"Budget",
"Balance",
"Account",
"Tag",
"Category",
"Import::TagMapping",
"Import::CategoryMapping",
"ToolCall::Function",
"UserMessage",
"AssistantMessage"
].join(",")
SeedDump.dump_using_environment({
"IMPORT" => true,
"FILE" => "db/seeds/db_restore.rb",
"EXCLUDE" => "",
"MODELS" => models
})
end
end
21 changes: 10 additions & 11 deletions app/models/sync.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,10 @@ def clean
end

def perform
Rails.logger.tagged("Sync", id, syncable_type, syncable_id) do
# This can happen on server restarts or if Sidekiq enqueues a duplicate job
Rails.event.tagged("Sync", id, syncable_type, syncable_id) do
# This can happen on server restarts or if ~~Sidekiq~~ SolidQueue enqueues a duplicate job
unless may_start?
Rails.logger.warn("Sync #{id} is not in a valid state (#{aasm.from_state}) to start. Skipping sync.")
Rails.event.notify("Sync", { id: id, syncable_type: syncable_type, syncable_id: syncable_id, message: "not in a valid state to start" })
return
end

Expand All @@ -88,6 +88,7 @@ def perform
start!

begin
Rails.event.notify("Performing sync", { syncable_type: syncable_type, syncable_id: syncable.id })
syncable.perform_sync(self)
rescue => e
fail!
Expand Down Expand Up @@ -160,27 +161,25 @@ def all_children_finalized?
end

def perform_post_sync
Rails.logger.info("Performing post-sync for #{syncable_type} (#{syncable.id})")
Rails.event.notify("Performing post-sync", { syncable_type: syncable_type, syncable_id: syncable.id })
syncable.perform_post_sync
syncable.broadcast_sync_complete
rescue => e
Rails.logger.error("Error performing post-sync for #{syncable_type} (#{syncable.id}): #{e.message}")
Rails.event.notify("Error performing post-sync", { syncable_type: syncable_type, syncable_id: syncable.id, error: e.message })
report_error(e)
end

def report_error(error)
Sentry.capture_exception(error) do |scope|
scope.set_tags(sync_id: id)
end
Rails.error.report(error, context: { sync_id: id })
end

def report_warnings
todays_sync_count = syncable.syncs.where(created_at: Date.current.all_day).count

if todays_sync_count > 10
Sentry.capture_exception(
if todays_sync_count > 100
Rails.error.report(
Error.new("#{syncable_type} (#{syncable.id}) has exceeded 10 syncs today (count: #{todays_sync_count})"),
level: :warning
context: { count: todays_sync_count, syncable_type: syncable_type, syncable_id: syncable.id }
)
end
end
Expand Down
2 changes: 1 addition & 1 deletion app/views/impersonation_sessions/_super_admin_bar.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<span class="text-inverse font-semibold uppercase">Super Admin</span>
</div>
<div>
<%= link_to "Jobs", sidekiq_web_url, class: "text-white underline hover:text-gray-100" %>
<%= link_to "Jobs", "/jobs", class: "text-white underline hover:text-gray-100" %>
</div>

<div class="flex items-center space-x-2 px-2 py-2 text-white">
Expand Down
Loading
Loading