Skip to content

Conversation

@jjmata
Copy link
Collaborator

@jjmata jjmata commented Aug 15, 2025

Issues pending:

  • Add remaining AI prompts/make DIVs collapsible.
  • Add read-only display of model in use per prompt (gpt-4 vs. gpt-4.1 vs. gpt-4.1-mini)
  • API Key doesn't mask after first time/message claims it does. Change one or the other.
  • Data Import/Export should move from under /settings/profile to /imports
  • Add other guides (plaid.md, chats.md, etc.) to /settings/guides

This is where we are going:

Screenshot 2025-08-19 at 1 23 05 AM

Summary by cubic

Reorganized the Settings navigation by adding an Advanced section to group admin-level options. Updated translations for English, Norwegian Bokmål, and Turkish.

  • Refactors
    • Added an Advanced section between Transactions and Other.
    • Moved API Keys, Self Hosting (conditional), and SimpleFin into Advanced.
    • Left Billing under General; no route or permission changes.

Summary by CodeRabbit

  • New Features

    • Admin exports on Import/Export page with delete capability.
    • AI Prompts settings page exposing prompt instructions and model info.
    • Guides page with onboarding documentation.
  • Improvements

    • Redesigned imports and exports UIs: status badges, delete/download controls, inline import actions, reorganized settings layout and breadcrumbs.
    • API Key flows relabeled (Save API Key) and layout simplified.
    • Settings menu now opens Accounts.
  • Localization

    • Added/updated translations for exports, imports, and settings.

@coderabbitai
Copy link

coderabbitai bot commented Aug 15, 2025

Walkthrough

Adds admin-facing exports to the Imports page with lazy loading and delete; introduces Settings pages for AI Prompts and Guides; reorganizes Settings navigation and labels; changes OpenAI model defaults and exposes prompt instructions; updates FamilyExport attachment purge behavior, routes, views, locales, and tests.

Changes

Cohort / File(s) Summary
Import/Export consolidation
app/controllers/imports_controller.rb, app/views/imports/index.html.erb, app/views/imports/_import.html.erb, app/views/imports/_empty.html.erb, config/locales/views/imports/*
Imports index restructured into settings_section layout; admin-only Exports section added (lazy turbo_frame). Import item actions refactored to inline buttons/links; empty state button removed. Import/Export-related locale keys added/edited.
Family Exports CRUD and UI
app/controllers/family_exports_controller.rb, app/views/family_exports/_list.html.erb, config/routes.rb, config/locales/views/family_exports/*, test/controllers/family_exports_controller_test.rb
Adds destroy action and route; before_action extended. Export list UI redesigned with status badges and delete/download controls. Redirects changed to imports_path. i18n for statuses added. Controller and controller tests updated for delete behavior and redirects.
Settings navigation and labels
app/helpers/settings_helper.rb, app/views/settings/_settings_nav.html.erb, config/locales/views/settings/*, app/views/users/_user_menu.html.erb, app/controllers/settings/api_keys_controller.rb, app/views/settings/profiles/show.html.erb, test/system/settings_test.rb
SETTINGS_ORDER and settings nav view reorganized (General, Transactions, Advanced, More); labels renamed (e.g., Profile Info, API Key, Import/Export); Guides and AI Prompts added; user menu Settings link now targets accounts_path. Profile settings removed admin import/export block. Tests updated to match nav.
Settings: AI Prompts
app/controllers/settings/ai_prompts_controller.rb, app/views/settings/ai_prompts/show.html.erb, config/routes.rb, config/locales/views/settings/en.yml
New Settings::AiPromptsController#show, route, view rendering prompt instructions and model info in collapsible sections, and translations added. @assistant_config populated from Assistant.config_for.
Settings: Guides
app/controllers/settings/guides_controller.rb, app/views/settings/guides/show.html.erb, docs/onboarding/guide.md, config/routes.rb
New Guides settings page and route; controller reads and renders docs/onboarding/guide.md as HTML in view. Onboarding guide markdown file added.
OpenAI provider defaults and prompts
app/models/provider/openai.rb, app/models/provider/openai/auto_categorizer.rb, app/models/provider/openai/auto_merchant_detector.rb, app/views/settings/ai_prompts/show.html.erb
Public methods default model to empty string; AutoCategorizer and AutoMerchantDetector add DEFAULT_MODEL and use `model.presence
API Keys UI updates
app/views/settings/api_keys/new.html.erb, app/views/settings/api_keys/show.html.erb, config/locales/views/settings/api_keys/en.yml, test/system/settings/api_keys_test.rb
API Keys views reworded and rearranged; button label changed to “Save API Key” and no-key title to “API Key”. System tests updated to match copy and flows.
Attachments and model tests
app/models/family_export.rb, test/models/family_export_test.rb
has_one_attached :export_file now uses dependent: :purge_later. Model tests expanded to cover attachments, filename generation, downloadable? logic, status transitions, ordering, and purge behavior.
Locales and small UI tweaks
config/locales/views/*, app/views/family_exports/_list.html.erb, app/views/settings/profiles/show.html.erb, app/views/users/_user_menu.html.erb
Multiple locale files added/updated for imports, family_exports, settings; family_exports list copy/styling updated; user menu and profile view tweaks applied.

Sequence Diagram(s)

sequenceDiagram
  actor Admin
  participant Browser
  participant ImportsCtrl as ImportsController
  participant ImportsView as Imports View
  participant TurboFrame as Turbo Frame "family_exports"
  participant FamilyExportsCtrl as FamilyExportsController

  Admin->>Browser: GET /imports
  Browser->>ImportsCtrl: request
  ImportsCtrl-->>Browser: renders index with @imports (+ @exports for admins)
  Browser->>ImportsView: render page
  ImportsView->>TurboFrame: lazy load /family_exports
  TurboFrame->>FamilyExportsCtrl: GET /family_exports (index)
  FamilyExportsCtrl-->>TurboFrame: renders exports list (statuses, actions)

  rect rgba(200,255,200,0.12)
    Admin->>Browser: Click "New Export" (modal)
    Browser->>FamilyExportsCtrl: GET/POST new/create
    FamilyExportsCtrl-->>Browser: redirect to /imports
  end

  rect rgba(255,230,190,0.12)
    Admin->>Browser: Click delete on export
    Browser->>FamilyExportsCtrl: DELETE /family_exports/:id
    FamilyExportsCtrl-->>Browser: destroy, purge attachment (async), redirect to /imports with notice
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

A rabbit hops through exports bright,
Guides and prompts now in my sight.
Models fallback, nav rearranged,
Keys renamed and views exchanged.
I nibble tests and tidy code — hooray! 🥕

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch jjmata/admin-ux-for-llm-prompts

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@matthieuEv matthieuEv Aug 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the order, i think it would be better to have:

  • Profil Info
  • Preferences
  • Security
  • Accounts
  • Bank Sync

This order look a lot more like some other settings, with the "User related" settings in first

Also the SETTINGS_ORDER do not seems to be used in the Settings Section Sidebar, i think it would be best to use one var to manage this, for readability

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so also maybe changing the object like:

SETTINGS_ORDER = [
  "GENERAL" : [
    { name: "Profile Info", path: :settings_profile_path },
    { name: "Preferences", path: :settings_preferences_path },
    { name: "Accounts", path: :accounts_path },
    { name: "Bank Sync", path: :settings_bank_sync_path },
    { name: "Security", path: :settings_security_path },
  ],
  "TRANSACTIONS" : [
    { name: "Billing", path: :settings_billing_path, condition: :not_self_hosted? },
    { name: "Categories", path: :categories_path },
    { name: "Tags", path: :tags_path },
    { name: "Rules", path: :rules_path },
    { name: "Merchants", path: :family_merchants_path },
  ],
  "ADVANCED" : [
    { name: "AI Prompts", path: :settings_ai_prompts_path },
    { name: "API Key", path: :settings_api_key_path },
    { name: "Self-Hosting", path: :settings_hosting_path, condition: :self_hosted? },
    { name: "Imports", path: :imports_path },
    { name: "SimpleFin", path: :simplefin_items_path },
  ],
  "MORE" : [
    { name: "Guides", path: :settings_guides_path },
    { name: "What's new", path: :changelog_path },
    { name: "Feedback", path: :feedback_path }
  ]
]

{ name: "API Key", path: :settings_api_key_path },
{ name: "Self-Hosting", path: :settings_hosting_path, condition: :self_hosted? },
{ name: "Imports", path: :imports_path },
{ name: "SimpleFin", path: :simplefin_items_path },

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why having a SimpleFin section as it is already in the Bank Sync?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The plan is for this to go away indeed, once #104 goes in we can take of it (just wanted to avoid the merge conflict!)

@matthieuEv
Copy link

For the OpenAI key, why not allow the user to update his key in the Self-Hosting setting?

@matthieuEv
Copy link

For the AI prompt part, it would be great to be able to expand or not the "Provider", and also to update the prompt inside the settings

@jjmata
Copy link
Collaborator Author

jjmata commented Aug 18, 2025

For the OpenAI key, why not allow the user to update his key in the Self-Hosting setting?

Yeah. That's a great way to do it and would be easy to implement as-is (feel free to PR it!) Only thing that slowed me down is that is doesn't solve the bigger problem: make model/provider configurable.

Even OpenRouter support might be a better solution that gets you both and some observability!

@jjmata
Copy link
Collaborator Author

jjmata commented Aug 18, 2025

For the AI prompt part, it would be great to be able to expand or not the "Provider", and also to update the prompt inside the settings

Yeah, that is my goal. I've had versions that collapse the DIVs but then I realized the design was not consistent with other Settings UI (just added the provider section yesterday to prepare for multiple prompt "families" by provider)

One could actually argue that even within a provider the prompts are specific to a model "family" (reasoning model vs. not?) But the complication was starting to grow in my mind (the persistence layer will have to support multiple versions of the prompt and get read at runtime, right?)

So anyway, yes ... feel free to work on my branch if you have ideas. :-) The thoughts you had are very similar to the ones I put in the first comment already yesterday as "subtask reminders" for myself.

@jjmata
Copy link
Collaborator Author

jjmata commented Aug 19, 2025

Thinking of leaving these for a later time:

  • API Key doesn't mask after first time/message claims it does. Change one or the other.
  • Add other guides (plaid.md, chats.md, etc.) to /settings/guides

They are "unrelated" to the task at hand (LLM configs) and the bulk of what I set out to do is done.

@jjmata jjmata requested a review from matthieuEv August 19, 2025 08:23
@jjmata jjmata self-assigned this Aug 19, 2025
@jjmata jjmata added the enhancement New feature or request label Aug 19, 2025
@jjmata jjmata added this to the v0.7.0 milestone Aug 19, 2025
@jjmata jjmata marked this pull request as ready for review August 19, 2025 08:24
Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 36 files

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/views/settings/api_keys/show.html.erb (1)

111-126: Plain API key is exposed outside the one-time reveal flow

The current-API-key branch renders @current_api_key.plain_key, which defeats “show once then mask” and risks leakage (DOM, screenshots, logs). Show the real key only in the newly created flow; otherwise mask or omit.

Minimal safe patch to mask the key and example in non-new flows:

-            <code id="api-key-display" class="font-mono text-sm text-primary break-all" data-clipboard-target="source"><%= @current_api_key.plain_key %></code>
+            <code id="api-key-display" class="font-mono text-sm text-primary break-all" data-clipboard-target="source">••••••••••••</code>
@@
-          curl -H "X-Api-Key: <%= @current_api_key.plain_key %>" <%= request.base_url %>/api/v1/accounts
+          curl -H "X-Api-Key: <API_KEY>" <%= request.base_url %>/api/v1/accounts

Follow-ups:

  • If available, replace the placeholder with a masked hint (e.g., last 4 chars) from a helper or model attribute, not the plain key.
  • Ensure the controller action sets headers to prevent caching on the reveal page:
# app/controllers/settings/api_keys_controller.rb
def show
  # ...
  response.headers["Cache-Control"] = "no-store"
end

I can wire up a masked_api_key(@current_api_key) helper and locales if you want.

Also applies to: 128-134, 136-148

♻️ Duplicate comments (2)
app/helpers/settings_helper.rb (2)

2-25: Model sections explicitly instead of relying on comment blocks

If the UI groups items into “General”, “Transactions”, “Advanced”, and “More”, encode that structure. This reduces drift between the sidebar and footer and makes reordering easier from one source of truth.

Sketch:

SETTINGS_SECTIONS = {
  "GENERAL" => [
    { name_key: "settings.nav.accounts", path: :accounts_path },
    # ...
  ],
  "TRANSACTIONS" => [ ... ],
  "ADVANCED" => [ ... ],
  "MORE" => [ ... ]
}

Then derive both the sidebar and the flattened sequence (for previous/next) from this constant.


19-20: Avoid duplication: “SimpleFin” appears both under Bank Sync and Advanced

You acknowledged this earlier will be removed after #104. Keeping both temporarily may confuse users.

If #104 is imminent, consider hiding the Advanced “SimpleFin” item behind the same condition as “Bank Sync” or temporarily removing it to reduce duplication.

🧹 Nitpick comments (42)
app/controllers/settings/api_keys_controller.rb (3)

11-12: Localize breadcrumb label instead of hardcoding "API Key".

Use I18n so the breadcrumb aligns with locales (and the recent singularization work).

-      [ "API Key", nil ]
+      [ t("settings.settings_nav.api_key_label"), nil ]

32-39: Wrap temporary revocation + create in a transaction to avoid partial state on failure.

Current flow revokes keys with update_column, then manually restores on failure. A transaction lets you avoid the explicit restore and reduces risk from mid-request failures.

-    # Temporarily revoke existing keys for validation to pass
-    existing_keys = Current.user.api_keys.active
-    existing_keys.each { |key| key.update_column(:revoked_at, Time.current) }
-
-    if @api_key.save
-      flash[:notice] = "Your API key has been created successfully"
-      redirect_to settings_api_key_path
-    else
-      # Restore existing keys if new key creation failed
-      existing_keys.each { |key| key.update_column(:revoked_at, nil) }
-      render :new, status: :unprocessable_entity
-    end
+    ActiveRecord::Base.transaction do
+      # Temporarily revoke existing keys for validation to pass
+      existing_keys = Current.user.api_keys.active
+      existing_keys.update_all(revoked_at: Time.current)
+
+      if @api_key.save
+        flash[:notice] = I18n.t("settings.api_keys.create.success")
+        redirect_to settings_api_key_path and return
+      else
+        # Transaction rollback will revert the temporary revocations
+        raise ActiveRecord::Rollback
+      end
+    end
+    # Only reached if save failed
+    flash.now[:alert] = I18n.t("settings.api_keys.create.failure")
+    render :new, status: :unprocessable_entity

Optional: add row locks if you want stronger guarantees under concurrent requests:

ActiveRecord::Base.transaction do
  existing_keys = Current.user.api_keys.active.lock # SELECT ... FOR UPDATE
  existing_keys.update_all(revoked_at: Time.current)
  ...
end

33-47: Localize flash messages for consistency with the rest of Settings.

Hardcoded English strings bypass your locales and will be inconsistent for non-English users.

-      flash[:notice] = "Your API key has been created successfully"
+      flash[:notice] = I18n.t("settings.api_keys.create.success")
...
-      flash[:notice] = "API key has been revoked successfully"
+      flash[:notice] = I18n.t("settings.api_keys.destroy.success")
...
-      flash[:alert] = "Failed to revoke API key"
+      flash[:alert] = I18n.t("settings.api_keys.destroy.failure")

Follow up: add the corresponding keys under config/locales/views/settings/{en,nb,tr}.yml.

config/locales/views/imports/nb.yml (1)

62-62: Capitalization change to "Ny Import" looks intentional.

If you follow Title Case for action buttons across locales, this is consistent. If not, double-check against your Norwegian style guide.

docs/onboarding/guide.md (2)

35-37: Normalize callout syntax: use [!NOTE] (uppercase) for consistency

GitHub-flavored callouts are conventionally uppercase. Using [!NOTE] aligns with [!IMPORTANT] and [!TIP] used elsewhere and avoids renderer quirks.

Apply this diff:

-> [!Note]
+> [!NOTE]

39-44: Avoid broken in-app links for Guides until routing is ready

The link to /docs/hosting/plaid.md will likely 404 inside the app’s Guides page since the controller currently renders a single hardcoded doc. Until multiple guides are routed, prefer a stable external URL or mark as WIP without a link.

Option A (stable link to GitHub):

-> - [**Plaid**](/docs/hosting/plaid.md)
+> - [**Plaid**](https://github.com/we-promise/sure/blob/main/docs/hosting/plaid.md)

Option B: I can help wire up dynamic guide routing (slug-based) and a markdown link rewriter so relative /docs links map to settings/guides/:slug. Want me to draft that?

app/helpers/settings_helper.rb (2)

53-57: Guard against nil in concat to prevent template errors

adjacent_setting can return nil; concat(nil) can be brittle depending on Rails version. Use safe_join with compact to avoid errors and keep markup clean.

Apply this diff:

   content_tag :div, class: "hidden md:flex flex-row justify-between gap-4" do
-    concat(previous_setting)
-    concat(next_setting)
+    safe_join([previous_setting, next_setting].compact)
   end
@@
   content_tag :div, class: "md:hidden flex flex-col gap-4" do
-    concat(previous_setting)
-    concat(next_setting)
+    safe_join([previous_setting, next_setting].compact)
   end

Also applies to: 63-67


2-25: Localize navigation labels for i18n consistency

Labels like “Accounts”, “Bank Sync”, “Guides”, “What’s new”, “Feedback” are hardcoded English. Since the app ships with multiple locales, consider moving these to locale files and storing a key instead of a string in SETTINGS_ORDER. Resolve the display text via I18n where rendered.

Example approach:

  • Change entries to { name_key: "settings.nav.accounts", path: :accounts_path }
  • In views/helpers, render t(entry[:name_key]) instead of entry[:name]
app/views/users/_user_menu.html.erb (1)

33-33: Confirm landing destination for “Settings”

Link now points to accounts_path, which is outside the settings layout. If the intent is to send users into the Settings flow, consider a Settings page (e.g., Profile Info) instead; otherwise, this is fine given the new IA.

Optional tweak (drop unused param unless Accounts uses it):

-<% menu.with_item(variant: "link", text: "Settings", icon: "settings", href: accounts_path(return_to: request.fullpath)) %>
+<% menu.with_item(variant: "link", text: "Settings", icon: "settings", href: accounts_path) %>
app/controllers/settings/profiles_controller.rb (2)

8-11: Localize breadcrumb labels

Hardcoded “Home” and “Profile Info” break i18n. Use translation keys so the breadcrumb follows the selected locale.

Apply this diff:

-    @breadcrumbs = [
-      [ "Home", root_path ],
-      [ "Profile Info", nil ]
-    ]
+    @breadcrumbs = [
+      [ t("breadcrumbs.home"), root_path ],
+      [ t("settings.profiles.show.breadcrumb"), nil ]
+    ]

You’ll need to add these keys in locale files if they don’t exist yet.


29-35: Localize success and error flash messages in destroy

Two messages are hardcoded in English whereas others already use I18n.

Apply this diff:

-    if @user.destroy
-      # Also destroy the invitation associated with this user for this family
-      Current.family.invitations.find_by(email: @user.email)&.destroy
-      flash[:notice] = "Member removed successfully."
-    else
-      flash[:alert] = "Failed to remove member."
-    end
+    if @user.destroy
+      # Also destroy the invitation associated with this user for this family
+      Current.family.invitations.find_by(email: @user.email)&;destroy
+      flash[:notice] = t("settings.profiles.destroy.member_removed")
+    else
+      flash[:alert] = t("settings.profiles.destroy.failed")
+    end

And add to locales, for example:

en:
  settings:
    profiles:
      destroy:
        member_removed: "Member removed successfully."
        failed: "Failed to remove member."
app/views/settings/api_keys/new.html.erb (2)

3-3: Confirm that settings_section handles title: nil cleanly (no empty header space).

If the helper supports a “hide title” flag, prefer that over passing nil; otherwise, consider pulling the title from I18n for consistency with the rest of the API key pages.

Would you like me to switch this to an I18n-backed title (e.g., t("settings.api_keys.new.title"))?


54-57: Align the button label with I18n and the rest of the flow.

The UI now says “Save API Key” while locales under settings.api_keys.new still expose “Create API Key”. To avoid drift, wire the button label to I18n and add a dedicated key for “Save API Key”.

Apply this diff here:

-      <%= render DS::Button.new(
-        text: "Save API Key",
+      <%= render DS::Button.new(
+        text: t("settings.api_keys.new.save_key"),
         variant: "primary",
         type: "submit"
       ) %>

And add the missing locale key in config/locales/views/settings/api_keys/en.yml (see my comment there). Additionally, consider switching the page title to I18n as well:

<%= content_for :page_title, t("settings.api_keys.new.title") %>
app/controllers/imports_controller.rb (2)

2-2: include SettingsHelper appears unused in the controller.

Helpers are typically for views. Since no helper methods are invoked here, consider removing to keep the controller surface minimal.

Apply this diff:

-  include SettingsHelper

17-20: Use I18n for breadcrumb labels.

Hardcoded strings make localization harder. Prefer I18n so Settings layout can render localized crumbs.

Apply this diff:

-    @breadcrumbs = [
-      [ "Home", root_path ],
-      [ "Import/Export", imports_path ]
-    ]
+    @breadcrumbs = [
+      [ t("common.home"), root_path ],
+      [ t("imports.breadcrumbs.index"), imports_path ]
+    ]

Ensure these keys exist (or add them) in the locales:

  • common.home
  • imports.breadcrumbs.index
app/views/settings/guides/show.html.erb (2)

1-1: Prefer I18n for the page title.

Use a locale key to match navigation labels and enable translations.

Apply this diff:

-<%= content_for :page_title, "Guides" %>
+<%= content_for :page_title, t("settings.guides.title") %>

3-5: Avoid html_safe; use semantic HTML and sanitize Markdown output.

Even if the content source is trusted, sanitizing is safer and aligns with best practices. Also prefer semantic <article> over a generic <div>.

Apply this diff:

-<div class="bg-container rounded-xl shadow-border-xs p-4 prose prose-sm max-w-none">
-  <%= @guide_content.html_safe %>
-</div>
+<article class="bg-container rounded-xl shadow-border-xs p-4 prose prose-sm max-w-none">
+  <%= sanitize(@guide_content) %>
+<article>
config/locales/views/settings/api_keys/en.yml (1)

44-61: Add a translation for the “Save API Key” button to avoid hardcoded strings in the view.

The view uses “Save API Key”, but the new section only has “Create API Key” keys. Add a dedicated key and update the view to use it.

Apply this diff:

       new:
         title: "Create API Key"
         create_new_key: "Create New API Key"
         description: "Configure your new API key with a descriptive name and appropriate permissions."
         name_label: "API Key Name"
         name_placeholder: "e.g., Production App, Analytics Dashboard"
         name_help: "Choose a descriptive name to help you identify this key's purpose."
         permissions_label: "Permissions"
         permissions_help: "Select the permissions your API key needs. You can always create a new key with different permissions."
+        save_key: "Save API Key"
         scope_details:
           read_accounts: "View account information, balances, and account-level data"

Then wire it in the view as suggested in my comment on app/views/settings/api_keys/new.html.erb.

config/routes.rb (1)

66-67: Nit: consider singular resource naming for consistency

You’re using singular resources under settings (e.g., api_key) but pluralized names for singular endpoints: ai_prompts and guides. Not functionally wrong, but renaming to ai_prompt and guide would align with Rails conventions and existing settings resources.

Apply if desired:

-    resource :ai_prompts, only: :show
-    resource :guides, only: :show
+    resource :ai_prompt, only: :show
+    resource :guide, only: :show

Note: This changes route helpers (settings_ai_prompt_path, settings_guide_path) and requires updating nav, controllers, and tests accordingly.

app/controllers/settings/ai_prompts_controller.rb (1)

4-11: Breadcrumbs and Current usage are fine; consider trimming unused ivars

@family is set but not referenced by the view (per the AI summary). If unused, remove to keep controller lean.

-    @family = Current.family
app/controllers/settings/guides_controller.rb (1)

9-17: Handle missing guide file and reuse the existing markdown helper

  • File.read will 500 if docs/onboarding/guide.md is missing.
  • You already have ApplicationHelper#markdown with safer defaults (hard_wrap, link rel attributes, etc.). Reuse it to avoid duplicating renderer config and to keep link attributes consistent.

Apply this diff:

-    markdown = Redcarpet::Markdown.new(Redcarpet::Render::HTML,
-      autolink: true,
-      tables: true,
-      fenced_code_blocks: true,
-      strikethrough: true,
-      superscript: true
-    )
-    @guide_content = markdown.render(File.read(Rails.root.join("docs/onboarding/guide.md")))
+    source_path = Rails.root.join("docs/onboarding/guide.md")
+    begin
+      raw = File.read(source_path)
+    rescue Errno::ENOENT
+      raw = "# Guides\n\nThe onboarding guide is missing at #{source_path}."
+    end
+    @guide_content = helpers.markdown(raw)
app/controllers/family_exports_controller.rb (1)

36-39: Consider guarding deletion + add Turbo Stream response for smoother UX

Two small improvements:

  • Prevent deleting in-progress exports (can avoid job errors/orphans).
  • Support Turbo Stream redirect to match create/index patterns.

Proposed change:

 def destroy
-  @export.destroy
-  redirect_to imports_path, notice: "Export deleted successfully"
+  if @export.status != "completed"
+    return redirect_to imports_path, alert: "Export can only be deleted after completion"
+  end
+
+  @export.destroy
+  respond_to do |format|
+    format.html        { redirect_to imports_path, notice: "Export deleted successfully" }
+    format.turbo_stream { stream_redirect_to imports_path, notice: "Export deleted successfully" }
+  end
 end
test/controllers/family_exports_controller_test.rb (1)

74-84: Happy path delete covered; consider adding a guard-path test if you enforce “completed-only” delete

If you adopt the controller guard to block deleting non-completed exports, add a test asserting no deletion and a suitable alert for processing/pending exports.

app/models/provider/openai/auto_merchant_detector.rb (2)

2-3: DEFAULT_MODEL introduced — good; optionally centralize or document per-concept defaults

The local DEFAULT_MODEL fallback is reasonable. If you intend different defaults per concept (categorizer vs merchant detector), keep as-is; otherwise consider centralizing under Provider::Openai to avoid drift.


31-70: Tighten JSON schema to enforce “1 result per transaction” and URL shape

The instructions ask for 1 result per transaction and apex domains. Add schema constraints to reduce LLM latitude:

  • merchants: enforce minItems/maxItems = transactions.size and uniqueItems: true.
  • business_url: add a basic domain pattern (still allow null).

Example changes (outside this hunk, in json_schema):

def json_schema
  {
    type: "object",
    properties: {
      merchants: {
        type: "array",
        description: "An array of auto-detected merchant businesses for each transaction",
        minItems: transactions.size,
        maxItems: transactions.size,
        uniqueItems: true,
        items: {
          type: "object",
          properties: {
            transaction_id: {
              type: "string",
              description: "The internal ID of the original transaction",
              enum: transactions.map { |t| t[:id] }
            },
            business_name: {
              type: ["string", "null"],
              description: "The detected business name of the transaction, or `null` if uncertain"
            },
            business_url: {
              type: ["string", "null"],
              description: "The URL of the detected business (apex domain only), or `null` if uncertain",
              pattern: '^[a-z0-9.-]+\.[a-z]{2,}$'
            }
          },
          required: ["transaction_id", "business_name", "business_url"],
          additionalProperties: false
        }
      }
    },
    required: ["merchants"],
    additionalProperties: false
  }
end

Optionally, post-normalize the URL to strip protocol and “www.” to reduce reliance on prompt adherence:

def normalize_domain(value)
  return nil if value.nil?
  host = value.to_s.downcase.strip.sub(%r{\Ahttps?://}i, "").split("/").first
  host&.sub(/\Awww\./, "")
end
app/models/provider/openai.rb (2)

17-17: Deferring model default to helpers is fine; log the resolved model for observability

Right now, logs will contain the raw param (possibly empty string). Consider resolving the model here for logging parity with what the helper actually used:

Example (outside this line):

def auto_categorize(transactions: [], user_categories: [], model: "")
  with_provider_response do
    raise Error, "Too many transactions to auto-categorize. Max is 25 per request." if transactions.size > 25

    selected_model = model.presence || AutoCategorizer::DEFAULT_MODEL

    result = AutoCategorizer.new(
      client,
      model: selected_model,
      transactions: transactions,
      user_categories: user_categories
    ).auto_categorize

    log_langfuse_generation(
      name: "auto_categorize",
      model: selected_model,
      input: { transactions: transactions, user_categories: user_categories },
      output: result.map(&:to_h)
    )

    result
  end
end

39-39: Same for merchants: resolve-and-log the effective model

Mirror the pattern for auto_detect_merchants to keep logs accurate:

def auto_detect_merchants(transactions: [], user_merchants: [], model: "")
  with_provider_response do
    raise Error, "Too many transactions to auto-detect merchants. Max is 25 per request." if transactions.size > 25

    selected_model = model.presence || AutoMerchantDetector::DEFAULT_MODEL

    result = AutoMerchantDetector.new(
      client,
      model: selected_model,
      transactions: transactions,
      user_merchants: user_merchants
    ).auto_detect_merchants

    log_langfuse_generation(
      name: "auto_detect_merchants",
      model: selected_model,
      input: { transactions: transactions, user_merchants: user_merchants },
      output: result.map(&:to_h)
    )

    result
  end
end
test/system/settings_test.rb (1)

31-35: Make path assertions resilient to query params

Some settings pages may append query params (e.g., filter or pagination), which can cause flaky failures. Recommend ignoring query params for all iterations.

Apply this diff:

-        assert_current_path path
+        assert_current_path path, ignore_query: true
app/views/settings/_settings_nav.html.erb (1)

23-32: Localize “SimpleFin” label for consistency with other items

All other labels use translations; “SimpleFin” is a hardcoded string. Use a locale key for consistency and i18n.

Apply this diff:

-      { label: "SimpleFin", path: simplefin_items_path, icon: "building-2" }
+      { label: t(".simplefin_label"), path: simplefin_items_path, icon: "building-2" }

Follow-up: add settings.settings_nav.simplefin_label to en/nb/tr locale files.

I can add the locale keys across locales if you want.

app/views/imports/_import.html.erb (4)

43-50: Revert button icon color mismatches the button color

The button uses orange text, but the icon is forced to destructive (red). Make the icon inherit the parent color for consistency.

Apply this diff:

-      <%= icon "rotate-ccw", class: "w-5 h-5 text-destructive" %>
+      <%= icon "rotate-ccw", class: "w-5 h-5", color: "current" %>

56-63: Avoid duplicating destructive color on icon; inherit from parent

The delete button sets both the parent and the icon to destructive. Inherit color from the parent for cleaner styling.

Apply this diff:

-        <%= icon "trash-2", class: "w-5 h-5 text-destructive" %>
+        <%= icon "trash-2", class: "w-5 h-5", color: "current" %>

43-50: Add accessible labels to icon-only buttons

Icon-only controls should expose aria-labels for screen readers.

Apply this diff:

-      <%= button_to revert_import_path(import),
+      <%= button_to revert_import_path(import),
           method: :put,
-          class: "flex items-center gap-2 text-orange-500 hover:text-orange-600",
+          class: "flex items-center gap-2 text-orange-500 hover:text-orange-600",
+          aria: { label: t(".revert_action_label", default: "Revert import") },
           data: { 
             turbo_confirm: "This will delete transactions that were imported, but you will still be able to review and re-import your data at any time."
           } do %>

And for the delete and view controls:

-      <%= button_to import_path(import),
+      <%= button_to import_path(import),
           method: :delete,
           class: "flex items-center gap-2 text-destructive hover:text-destructive-hover",
+          aria: { label: t(".delete_action_label", default: "Delete import") },
           data: { 
             turbo_confirm: CustomConfirm.for_resource_deletion("import")
           } do %>

...

-    <%= link_to import_path(import),
+    <%= link_to import_path(import),
         class: "flex items-center gap-2 text-primary hover:text-primary-hover" do %>
+      <span class="sr-only"><%= t(".view_action_label", default: "View import") %></span>

Follow-up: add translation keys if desired instead of defaults.


46-48: Prefer localized confirm message over hardcoded copy

Use i18n for the confirm text to enable translation and easier copy updates.

Apply this diff:

-          data: { 
-            turbo_confirm: "This will delete transactions that were imported, but you will still be able to review and re-import your data at any time."
-          } do %>
+          data: {
+            turbo_confirm: t(".revert_confirm", default: "This will delete transactions that were imported, but you will still be able to review and re-import your data at any time.")
+          } do %>

I can add the locale key to the appropriate en.yml if you want.

config/locales/views/settings/en.yml (1)

90-90: Standardize on api_keys_label for the API Key label

The only code reference is the plural key in

  • app/views/settings/_settings_nav.html.erb:27 (t(".api_keys_label"))

The singular api_key_label isn’t used in any view calls—only present in locale files—so let’s consolidate to avoid drift:

• Remove api_key_label from all locales:

  • config/locales/views/settings/en.yml (line 90)
  • config/locales/views/settings/tr.yml (line 70)
  • config/locales/views/settings/nb.yml (line 72)

• Add api_keys_label translations in tr.yml and nb.yml matching the en.yml value, e.g.:

api_keys_label: API Anahtarı  # tr.yml
api_keys_label: API-nøkkel   # nb.yml

I can follow up with a cleanup PR to apply these changes and update other locales.

app/models/provider/openai/auto_categorizer.rb (1)

55-64: Minor: place constants at the top for discoverability

AutoCategorization alias nested under private is fine functionally (Ruby ignores private for constants), but moving constants (DEFAULT_MODEL, AutoCategorization) together near the top enhances readability.

I can follow up with a small refactor if you prefer.

app/views/family_exports/_list.html.erb (3)

12-14: Localize date/time instead of hardcoded strftime

Use Rails’ l(...) (I18n) so dates respect locale and time zone.

-              <p class="text-sm font-medium text-primary">Export from <%= export.created_at.strftime("%B %d, %Y at %I:%M %p") %></p>
+              <p class="text-sm font-medium text-primary">
+                <%= t(".export_from", date: l(export.created_at, format: :long)) %>
+              </p>

Follow-up: Add export_from: to config/locales/views/family_exports/*.yml, e.g. export_from: "Export from %{date}".


31-35: Externalize “Exporting…” to I18n

Keep strings consistent with the rest of the view.

-              <span class="text-sm">Exporting...</span>
+              <span class="text-sm"><%= t(".exporting") %></span>

Follow-up: Add exporting: to config/locales/views/family_exports/*.yml.


42-44: Confirm messages should use I18n

Hardcoded English confirm messages break localization.

-                    turbo_confirm: "Are you sure you want to delete this export? This action cannot be undone.",
+                    turbo_confirm: t(".confirm_delete_completed"),
@@
-                    turbo_confirm: "Are you sure you want to delete this failed export?",
+                    turbo_confirm: t(".confirm_delete_failed"),

Follow-up: Add confirm_delete_completed and confirm_delete_failed locale keys.

Also applies to: 64-66

app/views/imports/index.html.erb (1)

24-28: Improve accessibility for the loading spinner

Add aria-live and an SR-only label for screen reader users while the frame lazy-loads.

-        <%= turbo_frame_tag "family_exports", src: family_exports_path, loading: :lazy do %>
-          <div class="mt-4 text-center text-secondary py-8">
+        <%= turbo_frame_tag "family_exports", src: family_exports_path, loading: :lazy do %>
+          <div class="mt-4 text-center text-secondary py-8" aria-live="polite" aria-busy="true">
             <div class="animate-spin inline-block h-4 w-4 border-2 border-secondary border-t-transparent rounded-full"></div>
+            <span class="sr-only"><%= t(".loading_exports") %></span>
           </div>
         <% end %>

Follow-up: Add loading_exports: to the imports locale file.

app/views/settings/api_keys/show.html.erb (1)

3-4: Localize static strings (headers, labels, button texts)

Several UI strings are hardcoded. Move them to I18n to match the rest of Settings and existing tests.

Illustrative changes:

-    <h1 class="text-primary text-xl font-medium">API Key Created Successfully</h1>
+    <h1 class="text-primary text-xl font-medium"><%= t(".created.title") %></h1>
@@
-    <h1 class="text-primary text-xl font-medium">Your API Key</h1>
+    <h1 class="text-primary text-xl font-medium"><%= t(".current.title") %></h1>

Apply similarly for:

  • “Copy API Key”, “How to use your API key”, “Continue to API Key Settings”
  • “Create New Key”, “Permissions”, “Active”, “Revoke Key”
  • “API Key”, “Create API Key”, feature bullet points

I can generate a full diff plus en/nb/tr locale entries if helpful.

Also applies to: 59-66, 90-93, 96-107, 111-126, 128-134, 136-146, 151-157, 175-196

app/views/settings/ai_prompts/show.html.erb (2)

14-16: Replace raw color class with design token

Use functional tokens (from maybe-design-system.css) instead of raw bg-gray-25 per guidelines.

-            <div class="w-9 h-9 rounded-full bg-gray-25 flex justify-center items-center">
+            <div class="w-9 h-9 rounded-full bg-container-inset flex justify-center items-center">

Apply to all three section icons.

Also applies to: 42-44, 70-72


29-33: Keep domain logic out of views; avoid instantiating service objects in ERB

  • Rendering constants and creating provider instances in the view couples presentation to provider internals and risks side effects/boot costs.
  • Push these into the controller (or a helper), and pass plain data to the view.

Example controller setup:

# app/controllers/settings/ai_prompts_controller.rb
def show
  @assistant_config = Assistant.config_for(OpenStruct.new(user: Current.user))
  @available_models = Provider::Openai::MODELS
  @categorizer_model = Provider::Openai::AutoCategorizer::DEFAULT_MODEL
  @merchant_model   = Provider::Openai::AutoMerchantDetector::DEFAULT_MODEL
  @categorizer_instructions = @assistant_config[:auto_categorizer]&.instructions || Provider::Openai::AutoCategorizer.new(nil).instructions
  @merchant_instructions    = @assistant_config[:auto_merchant]&.instructions || Provider::Openai::AutoMerchantDetector.new(nil, model: "", transactions: [], user_merchants: []).instructions
end

Then in the view, reference @available_models, @categorizer_model, and the precomputed instruction strings.

Also applies to: 57-60, 85-88

Comment on lines +36 to +39
def destroy
@export.destroy
redirect_to imports_path, notice: "Export deleted successfully"
end
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify attachments are purged on destroy (avoid orphaned blobs/storage costs)

If FamilyExport has an attached export_file, ensure destroying the record purges the blob (via has_one_attached :export_file, dependent: :purge_later or a destroy callback). If not configured, the blob can linger.

Run this to check and confirm the model setup:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate FamilyExport model and inspect attachment + destroy callbacks
fd -a -t f 'family_export.rb' | while read -r file; do
  echo "==> Inspecting $file"
  rg -n -C3 'has_one_attached\s+:export_file' "$file" || true
  rg -n -C2 'after_destroy|before_destroy|purge_later|purge' "$file" || true
done

Length of output: 570


Ensure export_file attachments are purged on destroy

Active Storage won’t automatically delete blobs unless you configure it. Right now, in app/models/family_export.rb you have:

has_one_attached :export_file

Without a dependent: option or a callback, deleting a FamilyExport record will leave its blob orphaned, incurring ongoing storage costs. Please update as follows:

• In app/models/family_export.rb

-  has_one_attached :export_file
+  has_one_attached :export_file, dependent: :purge_later

• Alternatively, if you prefer callbacks:

after_destroy_commit :purge_export_file

private

def purge_export_file
  export_file.purge_later
end

This ensures any attached blob is enqueued for deletion when the record is destroyed.

🤖 Prompt for AI Agents
In app/controllers/family_exports_controller.rb around lines 36-39: the destroy
action deletes the FamilyExport record but leaves its Active Storage blob
orphaned; update app/models/family_export.rb to ensure attachments are purged on
destroy by either adding the dependent option to the attachment (use
has_one_attached :export_file, dependent: :purge_later) or adding an
after_destroy_commit callback that calls export_file.purge_later (or purge) in a
private method so the blob is enqueued for deletion when the record is
destroyed.

[ "AI Prompts", nil ]
]
@family = Current.family
@assistant_config = Assistant.config_for(OpenStruct.new(user: Current.user))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Missing require for OpenStruct may raise NameError in production

OpenStruct is in Ruby’s stdlib but not auto-required. Without require "ostruct", constants may be unavailable depending on boot order.

Apply this diff:

+require "ostruct"
 class Settings::AiPromptsController < ApplicationController
   layout "settings"

Alternatively (no stdlib dependency):

-    @assistant_config = Assistant.config_for(OpenStruct.new(user: Current.user))
+    ChatContext = Struct.new(:user)
+    @assistant_config = Assistant.config_for(ChatContext.new(Current.user))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@assistant_config = Assistant.config_for(OpenStruct.new(user: Current.user))
require "ostruct"
class Settings::AiPromptsController < ApplicationController
layout "settings"
def show
@assistant_config = Assistant.config_for(OpenStruct.new(user: Current.user))
end
end
🤖 Prompt for AI Agents
In app/controllers/settings/ai_prompts_controller.rb around line 10, the code
uses OpenStruct but does not require the stdlib, which can raise NameError in
some boot orders; either add require "ostruct" at the top of the file (or in an
initializer) so OpenStruct is always loaded, or refactor to avoid the stdlib
dependency (e.g., replace OpenStruct.new(user: Current.user) with a simple PORO
or Struct.new(:user).new(Current.user)) to ensure the constant is available in
production.

end

def auto_categorize(transactions: [], user_categories: [], model: "gpt-4.1-mini")
def auto_categorize(transactions: [], user_categories: [], model: "")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

MODELS vs DEFAULT_MODEL mismatch — ensure supported models include the default

UI now shows DEFAULT_MODEL = "gpt-4.1-mini" at the concept level, but Provider::Openai::MODELS only lists "gpt-4.1". This can confuse users and invalidate supports_model? checks.

  • Either add "gpt-4.1-mini" to MODELS, or clarify that MODELS is “selectable” while concept defaults may diverge.

Quick check + follow-up:

Also applies to: 39-39


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show where MODELS is referenced and if 'mini' is included anywhere
rg -n -C2 '\bMODELS\b|gpt-4\.1-mini|supports_model\?' --type rb

# Show where DEFAULT_MODEL is used in views to understand UX impact
fd -a -t f 'show.html.erb' | xargs rg -n -C2 'DEFAULT_MODEL|Provider::Openai::MODELS' || true

Length of output: 112


I’ll retry the search more broadly to locate the constants and their usage:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show definition of MODELS in the OpenAI provider
rg -n 'MODELS\s*=' --glob 'app/models/provider/openai.rb'

# Show definition of DEFAULT_MODEL anywhere in the repo
rg -n 'DEFAULT_MODEL\s*=' .

# Find any literal “gpt-4.1-mini” mentions
rg -n 'gpt-4\.1-mini' .

# Look in all view templates for DEFAULT_MODEL or Provider::Openai::MODELS
rg -n -C2 'DEFAULT_MODEL|Provider::Openai::MODELS' --glob '*.html.erb'

Length of output: 3444


Ensure “gpt-4.1-mini” is included in the supported MODELS
The defaults in your auto-categorizer and auto-merchant detector classes use DEFAULT_MODEL = "gpt-4.1-mini", but in app/models/provider/openai.rb:

# line 7
MODELS = %w[gpt-4.1]

This mismatch will:

  • Omit “gpt-4.1-mini” from the UI’s selectable list (Provider::Openai::MODELS.join(", ") in app/views/settings/ai_prompts/show.html.erb lines 29, 57, and 85).
  • Cause supports_model? to reject the default variant.

Actions:

  • Update the MODELS constant to include the mini variant:
    # app/models/provider/openai.rb
    - MODELS = %w[gpt-4.1]
    + MODELS = %w[gpt-4.1 gpt-4.1-mini]
  • If the selector list must remain distinct, add a comment/docs to clarify that defaults may diverge from “selectable” models.
🤖 Prompt for AI Agents
In app/models/provider/openai.rb around line 17, the MODELS constant currently
lacks "gpt-4.1-mini" causing the default model to be unsupported and missing
from UI selectors; update the MODELS array to include "gpt-4.1-mini" (e.g., add
it alongside "gpt-4.1"), and optionally add a brief comment noting that defaults
(used by auto-categorizer/auto-merchant) may include variants not shown in the
selectable list so maintainers know the divergence.

Comment on lines 3 to 18
settings:
ai_prompts:
show:
page_title: AI Prompts
openai_label: OpenAI
prompt_instructions: Prompt Instructions
main_system_prompt:
title: Main System Prompt
subtitle: Core instructions that define how the AI assistant behaves in all chat conversations
transaction_categorizer:
title: Transaction Categorizer
subtitle: AI automatically categorizes your transactions based on your defined categories
merchant_detector:
title: Merchant Detector
subtitle: AI identifies and enriches transaction data with merchant information
billings:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Ensure nb/tr locales include the new/renamed keys

To avoid missing translation warnings, verify parity for:

  • settings.ai_prompts.*
  • settings.settings_nav.advanced_section_title, ai_prompts_label, guides_label, imports_label, profile_label/api_key(s)_label
  • profiles.show.page_title/profile_title renames

Also applies to: 86-113


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

pip install --quiet pyyaml > /dev/null

python - <<'PY'
import os, sys, yaml, glob

def flatten(prefix, obj):
    out = {}
    if isinstance(obj, dict):
        for k, v in obj.items():
            out.update(flatten(f"{prefix}.{k}" if prefix else k, v))
    else:
        out[prefix] = True
    return out

def load_yml(path):
    with open(path, 'r', encoding='utf-8') as f:
        return yaml.safe_load(f)

en_file = "config/locales/views/settings/en.yml"
if not os.path.exists(en_file):
    print("en.yml not found at config/locales/views/settings/en.yml", file=sys.stderr)
    sys.exit(1)

en = load_yml(en_file) or {}
en_keys = set(k for k in flatten("", en).keys() if k.startswith("en.settings."))

locale_files = glob.glob("config/locales/views/settings/*.yml") + glob.glob("config/locales/views/settings/**/*.yml")
missing = {}

for lf in locale_files:
    if lf.endswith("/en.yml"): continue
    data = load_yml(lf) or {}
    if not isinstance(data, dict): continue
    root = next(iter(data.keys()), None)
    if not root or root == "en": continue
    keys = set(k for k in flatten("", data).keys() if k.startswith(f"{root}.settings."))
    missing_keys = sorted([k.replace("en.", f"{root}.") for k in en_keys if k.replace("en.", f"{root}.") not in keys])
    if missing_keys:
        missing[lf] = missing_keys

if missing:
    print("Missing translation keys per file:")
    for lf, keys in missing.items():
        print(f"\n{lf}")
        for k in keys:
            print(f"  - {k}")
else:
    print("All locales under config/locales/views/settings/ contain the settings.* keys present in en.yml.")
PY

Length of output: 27568


Add missing translation keys for AI Prompts and Settings Navigation in nb/tr locales

The parity check shows that the following locale files are missing the new keys introduced under settings.* in config/locales/views/settings/en.yml:

  • config/locales/views/settings/nb.yml
  • config/locales/views/settings/tr.yml
  • config/locales/views/settings/api_keys/nb.yml
  • config/locales/views/settings/api_keys/tr.yml
  • config/locales/views/settings/hostings/nb.yml
  • config/locales/views/settings/hostings/tr.yml
  • config/locales/views/settings/securities/nb.yml
  • config/locales/views/settings/securities/tr.yml

Please add the missing key groups to each file:

  • settings.ai_prompts.show:

    • page_title
    • openai_label
    • prompt_instructions
    • main_system_prompt.title
    • main_system_prompt.subtitle
    • transaction_categorizer.title
    • transaction_categorizer.subtitle
    • merchant_detector.title
    • merchant_detector.subtitle
  • settings.settings_nav:

    • advanced_section_title
    • ai_prompts_label
    • api_key_label / api_keys_label
    • guides_label
    • imports_label
    • bank_sync_label
  • settings.preferences.show:

    • default_account_order
  • profiles.show renames:

    • page_title → profile_title

Ensuring these keys exist in nb and tr locales will prevent missing translation warnings.

🤖 Prompt for AI Agents
In config/locales/views/settings/en.yml around lines 3 to 18, new translation
groups under settings.ai_prompts.show and new settings keys were added;
replicate these exact key structures into the following locale files:
config/locales/views/settings/nb.yml, config/locales/views/settings/tr.yml,
config/locales/views/settings/api_keys/nb.yml,
config/locales/views/settings/api_keys/tr.yml,
config/locales/views/settings/hostings/nb.yml,
config/locales/views/settings/hostings/tr.yml,
config/locales/views/settings/securities/nb.yml, and
config/locales/views/settings/securities/tr.yml by adding
settings.ai_prompts.show with page_title, openai_label, prompt_instructions,
main_system_prompt.title and .subtitle, transaction_categorizer.title and
.subtitle, and merchant_detector.title and .subtitle; also add
settings.settings_nav with advanced_section_title, ai_prompts_label,
api_key_label (and api_keys_label where appropriate), guides_label,
imports_label, and bank_sync_label; add
settings.preferences.show.default_account_order; and in profiles.show rename
page_title to profile_title (remove or replace page_title) so the nb and tr
files mirror the en keys and prevent missing translation warnings.

Comment on lines +21 to +22
Once Sure is installed, open a browser and navigate to [localhost:3000](http://localhost:3000/sessions/new).<br />
You will see the **login page** (pictured below). Since we do not have an account yet, click on **Sign Up** to begin.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix misleading link label vs destination URL

The anchor text says “localhost:3000” but links to “/sessions/new”. Make the label and target consistent to avoid confusing readers.

Apply this diff:

-Once Sure is installed, open a browser and navigate to [localhost:3000](http://localhost:3000/sessions/new).<br />
+Once Sure is installed, open a browser and navigate to http://localhost:3000/sessions/new.<br />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Once Sure is installed, open a browser and navigate to [localhost:3000](http://localhost:3000/sessions/new).<br />
You will see the **login page** (pictured below). Since we do not have an account yet, click on **Sign Up** to begin.
Once Sure is installed, open a browser and navigate to http://localhost:3000/sessions/new.<br />
You will see the **login page** (pictured below). Since we do not have an account yet, click on **Sign Up** to begin.
🤖 Prompt for AI Agents
In docs/onboarding/guide.md around lines 21 to 22, the anchor text displays
"localhost:3000" but the href points to "/sessions/new", causing a mismatch;
update the anchor so its label and target are consistent — either change the
href to "http://localhost:3000" (or "http://localhost:3000/sessions/new" if you
want to include the path) to match the visible "localhost:3000" text, or change
the visible text to "localhost:3000/sessions/new" or "Sign In" to match the
existing "/sessions/new" link target.

@matthieuEv
Copy link

For the OpenAI key, why not allow the user to update his key in the Self-Hosting setting?

Yeah. That's a great way to do it and would be easy to implement as-is (feel free to PR it!) Only thing that slowed me down is that is doesn't solve the bigger problem: make model/provider configurable.

Even OpenRouter support might be a better solution that gets you both and some observability!

Is there already a PR implementing OpenRouter or can i start one?

@jjmata
Copy link
Collaborator Author

jjmata commented Aug 21, 2025

Is there already a PR implementing OpenRouter or can i start one?

No OpenRouter work has been started yet that I know of, no. Go for it! Please make sure you maintain LangFuse observability if you do that one! See #86 for details.

The one issue I see with OpenRouter is that the "naive" implementation (just use the OpenAI-compatible endpoint) assumes that all models/providers work the same. To do it right you have to implement your own model/provider/openrouter and implement it to be generalizable.

@jjmata
Copy link
Collaborator Author

jjmata commented Aug 22, 2025

Thanks for the review, @matthieuEv ... this is good to move things forward for now.

@jjmata
Copy link
Collaborator Author

jjmata commented Aug 22, 2025

Actually, let me add some export tests ...

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (7)
app/models/family_export.rb (1)

4-4: No custom Active Storage queues detected

I searched your config/initializers and config/environments directories for any config.active_storage.queues settings and found none. As a result, purge_later jobs will run on the default queue and could contend with your export-generation jobs.

To isolate purge jobs onto their own queue, consider:

  • Defining a dedicated purge queue in an initializer, for example:

    # config/initializers/active_storage.rb
    Rails.application.config.active_storage.queues.purge = :active_storage_purge
  • Updating your background processor’s configuration (e.g., Sidekiq, Resque) to include the active_storage_purge queue with appropriate concurrency settings.

Let me know if you’d like to add this configuration or need further verification.

test/controllers/family_exports_controller_test.rb (4)

74-84: Happy path delete covered; consider adding a cross-family authorization test.

You cover non-admin denial, but an additional check that an admin from another family cannot delete exports they don’t own would harden authorization.

Would you like me to draft a test for “admin of Family A cannot delete Family B’s export”?


85-101: Assert the purge job is enqueued when deleting with an attachment.

Since the model uses dependent: :purge_later, tighten the test to ensure a purge job is queued.

Apply this diff inside the test to also assert the job enqueue:

@@
-    assert export.export_file.attached?
-    assert_difference "@family.family_exports.count", -1 do
-      delete family_export_path(export)
-    end
+    assert export.export_file.attached?
+    assert_difference "@family.family_exports.count", -1 do
+      assert_enqueued_with(job: ActiveStorage::PurgeJob) do
+        delete family_export_path(export)
+      end
+    end

102-118: Mirror purge-job assertion for failed exports.

Same rationale as above; failed exports with files should enqueue a purge on delete.

@@
-    assert export.export_file.attached?
-    assert_difference "@family.family_exports.count", -1 do
-      delete family_export_path(export)
-    end
+    assert export.export_file.attached?
+    assert_difference "@family.family_exports.count", -1 do
+      assert_enqueued_with(job: ActiveStorage::PurgeJob) do
+        delete family_export_path(export)
+      end
+    end

119-142: Optionally also verify blob purge by performing jobs.

If you want to go beyond attachment removal and assert the blob is purged, you can perform enqueued jobs and check the blob record too.

@@
-    # Delete the export
-    delete family_export_path(export)
+    # Delete the export and run purge jobs
+    blob_id = export.export_file.blob.id
+    perform_enqueued_jobs do
+      assert_enqueued_with(job: ActiveStorage::PurgeJob) do
+        delete family_export_path(export)
+      end
+    end
@@
-    # Verify the Active Storage attachment is also gone
-    # Note: Active Storage purges files asynchronously with `dependent: :purge_later`
-    # In tests, we can check that the attachment record is gone
-    assert_not ActiveStorage::Attachment.exists?(file_id)
+    # Verify the Active Storage attachment and blob are gone
+    assert_not ActiveStorage::Attachment.exists?(file_id)
+    assert_not ActiveStorage::Blob.exists?(blob_id)
test/models/family_export_test.rb (2)

37-74: Reduce duplication across downloadable? cases (optional).

Several tests repeat attach/update patterns. A table-driven subtest or helper could cut duplication and speed the suite.

Example shape:

{
  ["completed", true]  => true,
  ["pending",   true]  => false,
  ["completed", false] => false,
  ["failed",    true]  => false
}.each do |(status, with_file), expected|
  subtest_name = "downloadable? #{status} #{with_file ? 'with' : 'without'} file"
  test subtest_name do
    @export.update!(status:)
    @export.export_file.attach(io: StringIO.new("x"), filename: "t.zip", content_type: "application/zip") if with_file
    assert_equal expected, @export.downloadable?
  end
end

76-100: Also assert that a purge job is enqueued and executed.

Since the model now purges asynchronously, it’s useful to assert both enqueue and effect. You’re already capturing blob_id.

@@
-    # Destroy the export
-    @export.destroy!
+    # Destroy the export and run purge jobs
+    perform_enqueued_jobs do
+      assert_enqueued_with(job: ActiveStorage::PurgeJob) do
+        @export.destroy!
+      end
+    end
@@
-    # Verify the Active Storage attachment is gone
-    assert_not ActiveStorage::Attachment.exists?(file_id)
+    # Verify the Active Storage attachment and blob are gone
+    assert_not ActiveStorage::Attachment.exists?(file_id)
+    assert_not ActiveStorage::Blob.exists?(blob_id)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 022d81b and 419eeba.

📒 Files selected for processing (3)
  • app/models/family_export.rb (1 hunks)
  • test/controllers/family_exports_controller_test.rb (2 hunks)
  • test/models/family_export_test.rb (1 hunks)
🧰 Additional context used
📓 Path-based instructions (8)
{app,lib}/**/*.{rb,erb}

📄 CodeRabbit inference engine (CLAUDE.md)

{app,lib}/**/*.{rb,erb}: Use Current.user for the authenticated user; do not use current_user
Use Current.family for the current family; do not use current_family

Files:

  • app/models/family_export.rb
{app/**/*.rb,app/**/*.erb}

📄 CodeRabbit inference engine (.cursor/rules/general-rules.mdc)

{app/**/*.rb,app/**/*.erb}: Use Current.user for the current user; do not use current_user in Rails code and templates
Use Current.family for the current family; do not use current_family in Rails code and templates

Files:

  • app/models/family_export.rb
app/models/**/{*,**/*.rb}

📄 CodeRabbit inference engine (.cursor/rules/project-conventions.mdc)

Use Rails concerns and POROs organized around model traits in app/models and app/models/concerns

Files:

  • app/models/family_export.rb
app/models/**/*.rb

📄 CodeRabbit inference engine (.cursor/rules/project-conventions.mdc)

app/models/**/*.rb: Models should answer questions about themselves (e.g., Account#balance_series) rather than external service objects
Use ActiveRecord validations for complex validations and business logic (may mirror DB constraints for UX)

Domain models should not call Provider::Registry directly; instead, add a Provided concern within the model’s namespace to choose providers and expose convenience methods (e.g., app/models//provided.rb).

Files:

  • app/models/family_export.rb
{app/helpers/**/*.rb,app/models/**/*.rb,app/views/**/*.html.erb}

📄 CodeRabbit inference engine (.cursor/rules/project-conventions.mdc)

Format currencies, numbers, and dates server-side (helpers/models) and pass formatted values to Stimulus for display only

Files:

  • app/models/family_export.rb
{app/models/**/*.rb,app/controllers/**/*.rb}

📄 CodeRabbit inference engine (.cursor/rules/project-conventions.mdc)

Avoid N+1 queries

Files:

  • app/models/family_export.rb
test/**/*_test.rb

📄 CodeRabbit inference engine (CLAUDE.md)

test/**/*_test.rb: Use VCR for external API testing
Use the mocha gem for stubs and mocks in tests

Files:

  • test/models/family_export_test.rb
  • test/controllers/family_exports_controller_test.rb
test/**/*.rb

📄 CodeRabbit inference engine (.cursor/rules/testing.mdc)

test/**/*.rb: Write tests with Minitest; do not use RSpec in this codebase
Use fixtures for test data; do not use factories
For tests needing many records, use Rails helpers to create them and inline the creation in the test (e.g., helpers in test/support)
Test boundaries correctly: for query methods, assert returned output; for command methods, assert the command was invoked with correct params
Do not test one class’s implementation details in another class’s test suite
Use the mocha gem for stubs and mocks
Prefer OpenStruct for simple mock instances, or define a small mock class for complex cases
Only mock what’s necessary; don’t mock return values you don’t assert on

Files:

  • test/models/family_export_test.rb
  • test/controllers/family_exports_controller_test.rb
🧬 Code graph analysis (1)
test/models/family_export_test.rb (3)
test/controllers/family_exports_controller_test.rb (1)
  • setup (3-153)
test/jobs/family_data_export_job_test.rb (1)
  • setup (3-32)
app/models/family_export.rb (2)
  • filename (15-17)
  • downloadable? (19-21)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: ci / test
🔇 Additional comments (6)
test/controllers/family_exports_controller_test.rb (3)

36-37: Redirect target updated correctly to Imports.

The redirect to imports_path after creating an export matches the PR’s navigation changes. Looks good.


70-72: Consistent UX for incomplete downloads.

Redirecting incomplete downloads back to imports_path with a clear alert keeps the flow coherent with the new location.


143-152: Non-admin delete denial looks good.

Covers the negative path and keeps users scoped correctly.

test/models/family_export_test.rb (3)

4-7: Solid test setup.

Fixtures + a fresh export per test keeps things isolated and readable.


29-35: Deterministic filename test is on point.

Freezing time to validate the timestamped name avoids flakiness.


115-130: Ordered scope test reads well and is robust.

Backdating via update_column makes intent explicit and avoids time helpers leakage between tests.

@jjmata jjmata merged commit d054cd0 into main Aug 22, 2025
6 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Sep 23, 2025
@coderabbitai coderabbitai bot mentioned this pull request Oct 7, 2025
2 tasks
This was referenced Oct 28, 2025
@jjmata jjmata deleted the jjmata/admin-ux-for-llm-prompts branch October 30, 2025 23:49
@coderabbitai coderabbitai bot mentioned this pull request Nov 7, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants