Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

[WIP] [AI] Account icon fetch and display (stacked revisions)#7743

Open
StephenBrown2 wants to merge 7 commits intoactualbudget:masteractualbudget/actual:masterfrom
StephenBrown2:account-iconsStephenBrown2/actual:account-iconsCopy head branch name to clipboard
Open

[WIP] [AI] Account icon fetch and display (stacked revisions)#7743
StephenBrown2 wants to merge 7 commits intoactualbudget:masteractualbudget/actual:masterfrom
StephenBrown2:account-iconsStephenBrown2/actual:account-iconsCopy head branch name to clipboard

Conversation

@StephenBrown2
Copy link
Copy Markdown
Contributor

@StephenBrown2 StephenBrown2 commented May 7, 2026

Stacked PR set for account icon fetch and display

I really liked the feature added by the extension shown off in Discord: https://discord.com/channels/937901803608096828/1276822891060527134/1500590218133110784 so I decided to vibe a built-in solution itself. The benefit of this is, bank-sync gives automatic icons for synced accounts.

I have reviewed all the changes that were made by Cursor, but it's enough that the maintainers probably won't want to merge it all at once.

Once this stack merges, you can set an icon for an account from the emoji picker, from an upload, or, when a sync server is configured, from a favicon fetch that runs through that server's proxy so the web build is not doing cross-origin image loads under the usual COEP/CSP constraints. After you finish linking SimpleFIN, GoCardless, or Pluggy, we try to fill a bank-level icon from the institution site or the logo URL the connector returns; a bad or slow fetch never blocks the link.

Test budgets also stop trying to sync when a server is configured, same as demo budgets, which removes the stray invalid fileId errors on local-only files.

In jj the stack is the revset ysu::wqz from oldest to tip; each section below is written like its own PR for review.

Stack (base → tip): ysusvxvqsxsnwmmwqz

NOTE: Given @MatissJanis comment in #7731 (comment) , I don't expect this to be merged, but, like the sidebar grouping PR, I hope this can serve as a start or inspiration for what can be done.


Revision 1 — ysu (9b765b8421bc)

Title: [AI] Disable sync for test budgets when a sync server is configured

Description

Test budgets (created via Create test file) are local-only and never receive a cloudFileId. When a sync server was configured, syncing was still enabled for those files, so mutations could surface an invalid fileId sync error toast. This change treats TEST_BUDGET_ID the same as demo budgets: syncing is forced disabled in non-test NODE_ENV, matching existing demo behavior.

Related issue(s)

None linked (bugfix / polish discovered alongside icon work).

Testing

  • With a sync server configured, create a test budget file, perform a mutation that previously triggered sync, and confirm no invalid fileId sync toast.
  • Confirm demo budgets still do not attempt sync in the same setup.

Checklist

  • Release notes added (upcoming-release-notes/test-budget-sync-disabled.md)
  • No obvious regressions in affected areas (budget open / sync gating)
  • Self-review has been performed - I understand what each change in the code does and why it is needed

Revision 2 — svx (a7d0adf65cdf)

Title: [AI] Add account and bank website/icon schema and DB display fields

Description

Adds persistence and read-model support for per-account and per-bank website and icon fields:

  • Migration 1778162298000_account_bank_icons.sql adds the new columns.
  • AccountEntity (and DB row types) include website, icon, and derived displayIcon / displayWebsite (from COALESCE(account.*, bank.*) in the accounts query).

Related issue(s)

Relates to account/bank icon feature work (no issue number captured here).

Testing

  • Apply migration on a dev DB; open budget; confirm accounts-get / DB layer still loads and no SQL errors.
  • yarn typecheck (from repo root) after this slice in isolation if exported to a branch.

Checklist

  • Release notes added at this layer (user-facing notes land in later revisions; schema-only may be optional)
  • No obvious regressions in affected areas (account list queries)
  • Self-review has been performed - I understand what each change in the code does and why it is needed

Revision 3 — vqs (57cb54337802)

Title: [AI] Add sync-server /favicon proxy for account bank icons

Description

Implements a sync-server HTTP endpoint that fetches remote favicons (with tests), returning JSON suitable for embedding as a base64 data URL in the client. This avoids browser COEP/CSP constraints that block arbitrary cross-origin images in the web app while keeping icons usable offline once fetched.

Related issue(s)

Supports manual and automatic bank/account icon flows that depend on a configured sync server.

Testing

  • yarn workspace @actual-app/sync-server test (or project-equivalent) for app-favicon tests.
  • Manual: hit /favicon with a known-good institution URL when server is running with auth as required.

Checklist

  • Release notes added (see link above) — may be covered by later user-facing PR text
  • No obvious regressions in affected areas (server boot, existing routes)
  • Self-review has been performed - I understand what each change in the code does and why it is needed

Revision 4 — xsn (89a59713b8cd)

Title: [AI] Add loot-core account icon handlers and favicon-fetch RPC

Description

Loot-core gains isolated mutators so rename flows do not clobber icon metadata:

  • account-set-icon — update account icon / website.
  • bank-update — update bank icon / website.
  • favicon-fetch — server-side call through the sync-server favicon proxy (with clear errors when no server / proxy issues).

getAccounts maps the new DB fields onto AccountEntity. Mocks include the new account fields for tests.

Related issue(s)

Builds on Revision 2 (schema/types) and Revision 3 (proxy endpoint).

Testing

  • yarn workspace @actual-app/core run test (or targeted account/db tests if present).
  • Smoke: call new handlers from the app shell where applicable.

Checklist

  • Release notes added (see link above)
  • No obvious regressions in affected areas (account CRUD, getAccounts)
  • Self-review has been performed - I understand what each change in the code does and why it is needed

Revision 5 — wmm (2292c9ea80db)

Title: [AI] Add account icon picker UI (modal, sidebar, mobile) and dependencies

Description

Desktop client UI for choosing account icons: upload, emoji, and favicon (when sync server available). Icons show in the sidebar, account header, mobile account surfaces, and related modals. Adds IconPickerModal, normalizeIcon (+ unit tests), modal wiring, Redux modalsSlice updates, and dependency / lockfile updates for the picker experience.

Release notes

Account icons: each account in the sidebar (and on mobile) can now show a small icon to the left of its name. Pick from an uploaded image, an emoji, or — when connected to a sync server — an auto-fetched bank favicon. Icons can also be set on a bank to apply to all of its accounts. The auto-favicon mode requires a sync server because Actual's COEP/CSP security headers (needed for SQL.js's SharedArrayBuffer) block cross-origin images in pure browser installs; the sync server fetches and embeds the favicon as a base64 data URL so it works offline.

Related issue(s)

Depends on Revisions 2–4 for data and RPC support.

Testing

  • Open icon picker from sidebar / account menu / mobile flows; set emoji, upload, and (with server) favicon paths.
  • yarn workspace @actual-app/web tests / typecheck as appropriate for touched packages.

Checklist

  • Release notes added (upcoming-release-notes/account-icons-sidebar.md)
  • No obvious regressions in affected areas (sidebar, mobile accounts, modals)
  • Self-review has been performed - I understand what each change in the code does and why it is needed

Revision 6 — wqz (1ae377ea7a54)

Title: [AI] Auto-fetch bank icons when linking synced accounts

Description

Best-effort automatic bank icons after linking synced accounts, without blocking the link flow:

  • Extends the favicon proxy with an image mode (query param and source: 'image') so curated logos can be pulled by URL as well as by institution website.
  • Pluggy.ai connector path merges connector metadata (name, image URL, institution URL) for use when linking.
  • GoCardless / Pluggy / SimpleFin paths pass institution website or logo URL into tryAutoFetchBankIcon (new in loot-core): fetches via proxy, persists on the bank row when empty, never overwrites user-set icons, logs failures only.
  • Types and useBuiltInBankSyncProviders updated for connector-aware institution naming and URLs.
  • Minor formatting in IconPickerModal (button props one line).

Release notes

Bank icons are now fetched automatically when you link a synced account. SimpleFin hands us the institution's website, which we run through the favicon fetcher. GoCardless and Pluggy.ai hand us a curated institution logo, which we embed directly (Pluggy: from the connector hosted on its CDN; GoCardless: from institution.logo). The icon shows up on the bank record so it applies to every account at that institution. Requires a connected sync server (same constraint as the manual icon picker). Best-effort: a failed or slow fetch never blocks linking, and existing user-set icons are never overwritten.

Related issue(s)

Builds on Revisions 3–5 (proxy, RPC, UI). Same sync-server constraint as manual favicon fetch.

Testing

  • Link accounts via SimpleFin (website → favicon), GoCardless (institution.logo), Pluggy (connector imageUrl / CDN), and confirm bank icon appears when server is configured; confirm link still succeeds if fetch fails or is slow.
  • Re-run sync-server tests after image mode changes.

Checklist

  • Release notes added (upcoming-release-notes/auto-bank-icons-on-link.md)
  • No obvious regressions in affected areas (bank linking, Pluggy connector payload)
  • Self-review has been performed - I understand what each change in the code does and why it is needed

Bundle Stats

Bundle Files count Total bundle size % Changed
desktop-client 34 → 37 13.93 MB → 14.57 MB (+650.52 kB) +4.56%
loot-core 1 5.27 MB → 5.28 MB (+3.87 kB) +0.07%
api 2 3.9 MB → 3.9 MB (+3.77 kB) +0.09%
cli 1 7.97 MB 0%
crdt 1 43.7 kB 0%
View detailed bundle stats

desktop-client

Total

Files count Total bundle size % Changed
34 → 37 13.93 MB → 14.57 MB (+650.52 kB) +4.56%
Changeset
File Δ Size
node_modules/@emoji-mart/data/sets/15/native.json 🆕 +501.2 kB 0 B → 501.2 kB
node_modules/emoji-mart/dist/module.js 🆕 +120.76 kB 0 B → 120.76 kB
src/components/accounts/icon-picker/IconPickerModal.tsx 🆕 +19.79 kB 0 B → 19.79 kB
src/components/accounts/icon-picker/normalizeIcon.ts 🆕 +2.82 kB 0 B → 2.82 kB
node_modules/@emoji-mart/react/dist/module.js 🆕 +544 B 0 B → 544 B
home/runner/work/actual/actual/packages/component-library/src/icons/v1/Photo.tsx 🆕 +497 B 0 B → 497 B
src/components/mobile/accounts/AccountPage.tsx 📈 +1.17 kB (+12.87%) 9.07 kB → 10.24 kB
src/components/sidebar/Account.tsx 📈 +1.34 kB (+10.60%) 12.66 kB → 14 kB
src/components/modals/AccountMenuModal.tsx 📈 +644 B (+6.65%) 9.46 kB → 10.09 kB
src/components/mobile/accounts/AccountsPage.tsx 📈 +777 B (+3.77%) 20.11 kB → 20.87 kB
src/components/accounts/Header.tsx 📈 +720 B (+2.51%) 28.01 kB → 28.72 kB
src/components/banksync/useBuiltInBankSyncProviders.ts 📈 +126 B (+1.41%) 8.74 kB → 8.86 kB
src/components/Modals.tsx 📈 +129 B (+1.00%) 12.56 kB → 12.69 kB
package.json 📈 +88 B (+0.96%) 8.98 kB → 9.06 kB
View detailed bundle breakdown

Added

Asset File Size % Changed
static/js/native.js 0 B → 501.2 kB (+501.2 kB) -
static/js/module.js 0 B → 121.29 kB (+121.29 kB) -
static/js/react.js 0 B → 17.2 kB (+17.2 kB) -

Removed
No assets were removed

Bigger

Asset File Size % Changed
static/js/index.js 1.93 MB → 1.96 MB (+25.62 kB) +1.29%
static/js/narrow.js 364.41 kB → 366.34 kB (+1.93 kB) +0.53%
static/js/Value.js 4.94 MB → 4.94 MB (+497 B) +0.01%

Smaller

Asset File Size % Changed
static/js/theme.js 31.67 kB → 14.46 kB (-17.2 kB) -54.32%

Unchanged

Asset File Size % Changed
static/js/BackgroundImage.js 121.09 kB 0%
static/js/FormulaEditor.js 962.55 kB 0%
static/js/ReportRouter.js 1.22 MB 0%
static/js/ScheduleEditForm.js 145.68 kB 0%
static/js/TransactionEdit.js 189.54 kB 0%
static/js/TransactionList.js 85.81 kB 0%
static/js/alerts.js 800.08 kB 0%
static/js/bankSyncUtils.js 54.1 kB 0%
static/js/ca.js 187.91 kB 0%
static/js/client.js 451.37 kB 0%
static/js/da.js 101.38 kB 0%
static/js/de.js 170.55 kB 0%
static/js/en-GB.js 8.2 kB 0%
static/js/en.js 185.11 kB 0%
static/js/es.js 179 kB 0%
static/js/extends.js 520.63 kB 0%
static/js/fr.js 178.9 kB 0%
static/js/indexeddb-main-thread-worker-e59fee74.js 13.46 kB 0%
static/js/it.js 165.01 kB 0%
static/js/nb-NO.js 148.31 kB 0%
static/js/nl.js 106.47 kB 0%
static/js/pl.js 86.52 kB 0%
static/js/pt-BR.js 189.58 kB 0%
static/js/resize-observer.js 18.06 kB 0%
static/js/th.js 174.76 kB 0%
static/js/uk.js 207.75 kB 0%
static/js/useFormatList.js 4.96 kB 0%
static/js/wide.js 453 B 0%
static/js/workbox-window.prod.es5.js 7.33 kB 0%
static/js/zh-Hans.js 117.91 kB 0%

loot-core

Total

Files count Total bundle size % Changed
1 5.27 MB → 5.28 MB (+3.87 kB) +0.07%
Changeset
File Δ Size
home/runner/work/actual/actual/packages/loot-core/src/server/accounts/link.ts 📈 +121 B (+29.73%) 407 B → 528 B
home/runner/work/actual/actual/packages/loot-core/src/server/accounts/app.ts 📈 +3.58 kB (+16.32%) 21.96 kB → 25.54 kB
home/runner/work/actual/actual/packages/loot-core/src/server/db/index.ts 📈 +146 B (+0.69%) 20.53 kB → 20.68 kB
home/runner/work/actual/actual/packages/loot-core/src/server/budgetfiles/app.ts 📈 +25 B (+0.22%) 11.03 kB → 11.06 kB
View detailed bundle breakdown

Added

Asset File Size % Changed
kcab.worker.B1lRkS2-.js 0 B → 5.28 MB (+5.28 MB) -

Removed

Asset File Size % Changed
kcab.worker.DbAkUAmB.js 5.27 MB → 0 B (-5.27 MB) -100%

Bigger
No assets were bigger

Smaller
No assets were smaller

Unchanged
No assets were unchanged


api

Total

Files count Total bundle size % Changed
2 3.9 MB → 3.9 MB (+3.77 kB) +0.09%
Changeset
File Δ Size
home/runner/work/actual/actual/packages/loot-core/src/server/accounts/link.ts 📈 +120 B (+30.30%) 396 B → 516 B
home/runner/work/actual/actual/packages/loot-core/src/server/accounts/app.ts 📈 +3.48 kB (+16.28%) 21.4 kB → 24.88 kB
home/runner/work/actual/actual/packages/loot-core/src/server/db/index.ts 📈 +146 B (+0.71%) 20.08 kB → 20.22 kB
home/runner/work/actual/actual/packages/loot-core/src/server/budgetfiles/app.ts 📈 +25 B (+0.23%) 10.67 kB → 10.7 kB
View detailed bundle breakdown

Added
No assets were added

Removed
No assets were removed

Bigger

Asset File Size % Changed
index.js 3.9 MB → 3.9 MB (+3.77 kB) +0.09%

Smaller
No assets were smaller

Unchanged

Asset File Size % Changed
models.js 0 B 0%

cli

Total

Files count Total bundle size % Changed
1 7.97 MB 0%
View detailed bundle breakdown

Added
No assets were added

Removed
No assets were removed

Bigger
No assets were bigger

Smaller
No assets were smaller

Unchanged

Asset File Size % Changed
cli.js 7.97 MB 0%

crdt

Total

Files count Total bundle size % Changed
1 43.7 kB 0%
View detailed bundle breakdown

Added
No assets were added

Removed
No assets were removed

Bigger
No assets were bigger

Smaller
No assets were smaller

Unchanged

Asset File Size % Changed
index.js 43.7 kB 0%

@actual-github-bot actual-github-bot Bot changed the title [AI] Account icon fetch and display (stacked revisions) [WIP] [AI] Account icon fetch and display (stacked revisions) May 7, 2026
@netlify
Copy link
Copy Markdown

netlify Bot commented May 7, 2026

Deploy Preview for actualbudget ready!

Name Link
🔨 Latest commit 1b4ac87
🔍 Latest deploy log https://app.netlify.com/projects/actualbudget/deploys/69fd38e708e46600082b2b96
😎 Deploy Preview https://deploy-preview-7743.demo.actualbudget.org
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai coderabbitai Bot added the contains DB migrations Pull request contains a migration that changes the database label May 7, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 7, 2026

👋 Hello contributor!

We would love to review your PR! Before we can do that, please make sure:

  • ✅ All CI checks pass
  • ✅ The PR is moved from draft to open (if applicable)
  • ✅ The "[WIP]" prefix is removed from the PR title
  • ✅ All CodeRabbit code review comments are resolved (if you disagree with anything - reply to the bot with your reasoning so we can read through it). The bot will eventually approve the PR.

We do this to reduce the TOIL the core contributor team has to go through for each PR and to allow for speedy reviews and merges.

For more information, please see our Contributing Guide.

const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
try {
return await fetch(url, { ...options, signal: controller.signal });
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 7, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds comprehensive account and bank icon support to Actual. Users can set custom icons via upload, emoji, or auto-fetched favicons, with icons inherited from bank to accounts and displayed across desktop/mobile sidebars and headers. The sync server provides favicon fetching with SSRF protection. Icons auto-populate during bank account linking when unavailable, and test budgets now disable sync like demo budgets.

Changes

Account and Bank Icons

Layer / File(s) Summary
Database Schema and Type Definitions
packages/loot-core/migrations/*, packages/loot-core/src/server/db/types/index.ts, packages/loot-core/src/types/models/account.ts, packages/loot-core/src/types/models/gocardless.ts, packages/loot-core/src/types/models/pluggyai.ts, packages/loot-core/src/mocks/index.ts
Database migration adds website and icon columns to accounts and banks tables. TypeScript types updated across account entities, sync server models, and test mocks to include icon, website, displayIcon, and displayWebsite fields.
Icon Normalization Utilities
packages/desktop-client/src/components/accounts/icon-picker/normalizeIcon.ts
Canvas-based utilities normalize uploaded images and emoji into 64×64 PNG data URLs with size validation. Includes normalizeImageToDataUrl, emojiToDataUrl, and toDataUrl exports plus error handling via IconNormalizationError.
Favicon Fetching Service
packages/sync-server/src/app-favicon.ts
New Express module discovers and fetches favicons with SSRF protection (blocks private IPs), 32 KB size limit, content-type whitelist, and fallback strategies (homepage <link> tag, /favicon.ico, DuckDuckGo). Exports fetchFaviconForUrl and fetchImageForUrl.
Server Handlers and Auto-Fetch
packages/loot-core/src/server/accounts/app.ts, packages/loot-core/src/server/accounts/link.ts
RPC handlers account-set-icon, bank-update, and favicon-fetch manage icon persistence. tryAutoFetchBankIcon best-effort fetches and stores bank icons during GoCardless/SimpleFin/PluggyAI linking without blocking. Updated findOrCreateBank to accept optional website parameter.
Pluggy AI Sync Server Integration
packages/sync-server/src/app-pluggyai/app-pluggyai.js, packages/sync-server/src/app-pluggyai/pluggyai-service.js
Pluggy account aggregation refactored to concurrently fetch item decoration via new getItemById service method. Accounts decorated with connector metadata (name, image URL, website) when item data available; failures per item logged but do not block.
Icon Picker Modal
packages/desktop-client/src/components/accounts/icon-picker/IconPickerModal.tsx, packages/desktop-client/package.json
Three-tab React modal for favicon (requires sync server), upload, and emoji icon selection with live preview, scope toggle (account vs. bank), and "Clear"/"Apply" actions. Uses @emoji-mart/react and emoji-mart for emoji picker.
Modal Routing
packages/desktop-client/src/components/Modals.tsx, packages/desktop-client/src/modals/modalsSlice.ts
account-icon-picker modal routed through dispatcher. account-menu modal extended with optional onEditIcon callback.
Account Menu Enhancement
packages/desktop-client/src/components/modals/AccountMenuModal.tsx
Adds "Edit icon" button alongside "Edit notes" in account context menu.
Desktop UI Components
packages/desktop-client/src/components/sidebar/Account.tsx, packages/desktop-client/src/components/accounts/Header.tsx
Sidebar and header account rows now display account icons with context-menu "Edit icon" action. AccountIcon and AccountHeaderIcon components render displayIcon or fallback to null.
Mobile UI Components
packages/desktop-client/src/components/mobile/accounts/AccountPage.tsx, packages/desktop-client/src/components/mobile/accounts/AccountsPage.tsx
Mobile account page header and account list items render icons. Components mirror desktop icon display with displayIcon fallback.
Bank Sync Client Provider Updates
packages/desktop-client/src/components/banksync/useBuiltInBankSyncProviders.ts
PluggyAI external account transformation now passes optional connector metadata (name, image, website) to sync server and maps orgId accordingly.
Test Budget Sync Behavior
packages/loot-core/src/server/budgetfiles/app.ts
Test budgets now disable sync mode on load, matching demo budget behavior.
Icon Normalization Tests
packages/desktop-client/src/components/accounts/icon-picker/normalizeIcon.test.ts
Vitest suite validates data URL formatting, emoji rendering, and size-cap enforcement with mocked canvas.
Favicon Service Tests
packages/sync-server/src/app-favicon.test.ts, packages/sync-server/src/app.ts
Comprehensive Vitest suite covering session validation, URL parsing, favicon discovery, fallbacks, SSRF guards, and size enforcement. Favicon handlers mounted at /favicon route.
Release Notes
upcoming-release-notes/account-icons-sidebar.md, upcoming-release-notes/auto-bank-icons-on-link.md, upcoming-release-notes/test-budget-sync-disabled.md
Feature documentation describing user-configurable icons, bank-wide inheritance, sync-server requirement for favicon fetching, auto-fetch on linking, and test budget sync behavior.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

"🐰 I painted tiny badges in the night,
From emoji gleam to favicon light,
Sidebars sparkle, headers too—
Accounts wear icons, bright and new,
A little hop for UI delight."

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title '[WIP] [AI] Account icon fetch and display (stacked revisions)' clearly describes the main change: adding account icon functionality with emoji, upload, and favicon options across the application.
Description check ✅ Passed The description thoroughly explains the feature set, stack structure, and implementation details across six revisions with clear objectives, testing guidance, and release notes for each change.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@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

🧹 Nitpick comments (5)
upcoming-release-notes/test-budget-sync-disabled.md (1)

6-6: ⚡ Quick win

Consider removing internal implementation terms from the release note.

The message can stay user-facing by dropping internals like cloudFileId and mutation mechanics.

Proposed wording
-Test budgets (created via `Create test file`) no longer attempt to sync when a sync server is configured, matching the existing behavior of demo budgets. Previously any mutation in a test budget would trigger an "invalid fileId" sync error toast because test budgets are local-only and never get a `cloudFileId`.
+Test budgets created via `Create test file` no longer try to sync when a sync server is configured, matching demo budget behavior and preventing the previous “invalid fileId” sync error toast.

Based on learnings: For files in upcoming-release-notes/*.md, keep release notes concise: ideally one sentence and non-technical content unless the category is Maintenance.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@upcoming-release-notes/test-budget-sync-disabled.md` at line 6, The release
note in upcoming-release-notes/test-budget-sync-disabled.md exposes internal
implementation terms ("cloudFileId" and mutation/sync mechanics); update the
single-sentence note to be concise and user-facing by removing internal terms
and technical details (e.g., say: "Test budgets created via 'Create test file'
no longer attempt to sync when a sync server is configured, matching demo
budgets."), keeping it non-technical and fitting the Maintenance guideline for
that folder.
packages/loot-core/src/types/models/gocardless.ts (1)

79-80: ⚡ Quick win

Consider using Pick<GoCardlessInstitution, 'name' | 'logo'> instead of an inline structural type.

The inline { name: string; logo?: string } duplicates structure already described by GoCardlessInstitution. Using Pick ties the hydrated subset to the canonical type and prevents silent drift if GoCardlessInstitution evolves (e.g., logo becomes a richer object).

Note also the inconsistency: GoCardlessInstitution.logo is required (string), but the hydrated version makes it optional. If hydration can genuinely omit the logo, GoCardlessInstitution.logo should probably also be made optional for consistency; if it cannot, then logo here should be required too.

♻️ Proposed refactor
-  /** Institution object from the sync-server after hydration. */
-  institution: { name: string; logo?: string };
+  /** Institution object from the sync-server after hydration. */
+  institution: Pick<GoCardlessInstitution, 'name' | 'logo'>;

If logo can be absent after hydration, also update GoCardlessInstitution:

-  logo: string;
+  logo?: string;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/loot-core/src/types/models/gocardless.ts` around lines 79 - 80,
Replace the inline hydrated institution type with a Pick of the canonical type
to avoid duplication: change the property type for institution to
Pick<GoCardlessInstitution, 'name' | 'logo'> and then reconcile the optionality
of logo between GoCardlessInstitution and the hydrated version—if hydration can
omit logo, make GoCardlessInstitution.logo optional; otherwise make the Pick
contain a required logo—so update either GoCardlessInstitution or the
institution field accordingly to keep them consistent.
packages/desktop-client/src/components/banksync/useBuiltInBankSyncProviders.ts (1)

49-54: 💤 Low value

connector type duplicates the exported PluggyAiConnector.

The inline anonymous type is structurally identical to PluggyAiConnector from @actual-app/core/types/models/pluggyai. Reusing it avoids silent drift if fields are added to the canonical type later.

♻️ Proposed refactor
+import type { PluggyAiConnector } from '@actual-app/core/types/models/pluggyai';

 type PluggyAiAccount = {
   // ...
-  /** Optional: sync-server merges Item.connector onto each account when the Item fetch succeeds. */
-  connector?: {
-    name?: string;
-    imageUrl?: string;
-    institutionUrl?: string;
-  };
+  /** Optional: sync-server merges Item.connector onto each account when the Item fetch succeeds. */
+  connector?: PluggyAiConnector;
 };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/desktop-client/src/components/banksync/useBuiltInBankSyncProviders.ts`
around lines 49 - 54, The inline anonymous type for "connector" duplicates the
exported PluggyAiConnector; replace the inline type with the canonical
PluggyAiConnector type from `@actual-app/core/types/models/pluggyai` by importing
PluggyAiConnector and changing the connector?: { ... } declaration to
connector?: PluggyAiConnector (ensure the import is added/used in
useBuiltInBankSyncProviders.ts and update any optional/nullable handling to
match the imported type).
packages/desktop-client/src/components/accounts/icon-picker/IconPickerModal.tsx (2)

539-560: ⚡ Quick win

useCallback in new file violates the React Compiler auto-memoization guideline.

EmojiTab is new code; handleEmoji should be a plain function and let the compiler handle memoization.

♻️ Proposed fix
-  const handleEmoji = useCallback(
-    (value: string) => {
-      setEmoji(value);
-      try {
-        if (!value.trim()) {
-          setPreview(null);
-          return;
-        }
-        const dataUrl = emojiToDataUrl(value);
-        setPreview(dataUrl);
-        setError(null);
-      } catch (err) {
-        const message =
-          err instanceof IconNormalizationError
-            ? err.message
-            : t('Failed to render emoji');
-        setError(message);
-        setPreview(null);
-      }
-    },
-    [setError, setPreview, t],
-  );
+  const handleEmoji = (value: string) => {
+    setEmoji(value);
+    try {
+      if (!value.trim()) {
+        setPreview(null);
+        return;
+      }
+      const dataUrl = emojiToDataUrl(value);
+      setPreview(dataUrl);
+      setError(null);
+    } catch (err) {
+      const message =
+        err instanceof IconNormalizationError
+          ? err.message
+          : t('Failed to render emoji');
+      setError(message);
+      setPreview(null);
+    }
+  };

As per coding guidelines: "Use React Compiler auto-memoization in desktop-client; omit manual useCallback, useMemo, and React.memo when adding or refactoring code."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/desktop-client/src/components/accounts/icon-picker/IconPickerModal.tsx`
around lines 539 - 560, Remove the manual useCallback wrapper around handleEmoji
in IconPickerModal (the handler defined in EmojiTab); make handleEmoji a plain
function that calls setEmoji, uses emojiToDataUrl, and catches
IconNormalizationError to call setError and setPreview as currently implemented,
and keep the same dependencies (setEmoji, setPreview, setError, t) referenced
directly so the React Compiler can auto-memoize it.

614-636: ⚡ Quick win

FullEmojiPicker: dynamic imports triggered on every render, not in useEffect.

Each render while Picker or data is null registers a new .then() handler. When the module resolves, all registered handlers fire, calling setPicker/setData redundantly. In React StrictMode (development double-invoke) this triggers the import twice on mount.

♻️ Proposed fix
 function FullEmojiPicker({ onPick }: { onPick: (emoji: string) => void }) {
   const [Picker, setPicker] = useState<ComponentType<{
     data: unknown;
     onEmojiSelect: (e: { native?: string }) => void;
     autoFocus?: boolean;
     perLine?: number;
   }> | null>(null);
   const [data, setData] = useState<unknown>(null);

-  if (!Picker || !data) {
-    void Promise.all([
-      import('@emoji-mart/react'),
-      import('@emoji-mart/data'),
-    ]).then(([picker, d]) => {
-      setPicker(() => picker.default);
-      setData(d.default);
-    });
-    return (
-      <Text style={{ color: theme.pageTextSubdued, fontSize: 12 }}>
-        <Trans>Loading emoji picker…</Trans>
-      </Text>
-    );
-  }
+  useEffect(() => {
+    void Promise.all([
+      import('@emoji-mart/react'),
+      import('@emoji-mart/data'),
+    ]).then(([picker, d]) => {
+      setPicker(() => picker.default);
+      setData(d.default);
+    });
+  }, []);

+  if (!Picker || !data) {
+    return (
+      <Text style={{ color: theme.pageTextSubdued, fontSize: 12 }}>
+        <Trans>Loading emoji picker…</Trans>
+      </Text>
+    );
+  }

   return (
     <Picker ...
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/desktop-client/src/components/accounts/icon-picker/IconPickerModal.tsx`
around lines 614 - 636, FullEmojiPicker currently triggers dynamic imports on
every render because the Promise.all call is placed outside a lifecycle hook,
causing multiple .then handlers and redundant setPicker/setData calls (and
double import in StrictMode); move the dynamic import logic into a useEffect
inside FullEmojiPicker that runs only when Picker or data are null (e.g.,
useEffect with dependency [Picker, data] or a single-run effect that sets state
via setPicker and setData), and ensure you guard against setting state after
unmount by tracking a mounted flag or cancelling if needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@packages/desktop-client/src/components/accounts/icon-picker/normalizeIcon.ts`:
- Around line 11-16: The checkSize function currently compares MAX_BASE64_BYTES
against base64.length (encoded chars); change it to compute the decoded payload
byte length and compare that to MAX_BASE64_BYTES instead. In checkSize, extract
the base64 payload as before, decode it to bytes (e.g., using
Buffer.from(base64, 'base64') or equivalent) and use the resulting byte length
for the comparison; if it exceeds MAX_BASE64_BYTES, throw the same
IconNormalizationError with the decoded byte count in the message to preserve
current behavior and diagnostics.

In `@packages/loot-core/migrations/1778162298000_account_bank_icons.sql`:
- Around line 1-9: The server path stores auto-fetched icons without size
validation in tryAutoFetchBankIcon (which calls fetchFaviconViaProxy) before the
db.update call that writes dataUrl; add the same MAX_BASE64_BYTES check used on
the client: after receiving result.base64 and result.contentType, compute the
byte length of the decoded base64 (e.g. bytes = Math.floor(result.base64.length
* 3 / 4) - padding) or convert MAX_BASE64_BYTES to an equivalent base64 length
and compare, and only construct and persist dataUrl (via db.update in
tryAutoFetchBankIcon) if it is <= MAX_BASE64_BYTES; if it exceeds the limit,
skip storing the icon (or store null/empty) and log a warning. Ensure you
reference the shared MAX_BASE64_BYTES constant (import or reuse the client
constant) so the server limit matches the client-side limit.

In `@packages/sync-server/src/app-favicon.test.ts`:
- Around line 70-75: Add a new test in
packages/sync-server/src/app-favicon.test.ts that mirrors the existing URL-path
test but uses the domain query parameter to exercise the alternative code path
(the implementation at line 251 accepts either `url` or `domain`). In the new
`it` block use `request(app).get('/')` with `?domain=example.com`, then assert
the same expected outcomes as the URL-case tests (e.g., status code and response
body/headers) so the domain branch is covered; reference the existing test style
using `request` and `app` to match assertions.

In `@packages/sync-server/src/app-favicon.ts`:
- Around line 88-96: The SSRF check is bypassed because
isHostnameAllowed(parsed.hostname) only permits non-IP hostnames and
fetchWithTimeout follows redirects; fix by resolving parsed.hostname to its
IP(s) and validating each IP against your allowed-ranges policy before fetching
(use DNS lookup in the favicon retrieval flow that uses parsed), and/or call
fetchWithTimeout with redirects disabled (redirect: 'manual') and then
revalidate the final response.url/Location header (and any resolved IP for the
redirect target) before reading response body; update the code paths around
parsed (URL) validation, isHostnameAllowed usage, and the fetchWithTimeout
response handling to enforce IP validation or redirect revalidation and throw
FaviconError on violations.

In `@upcoming-release-notes/account-icons-sidebar.md`:
- Line 6: The release note in upcoming-release-notes/account-icons-sidebar.md is
too long and too technical for the "Features" category; replace the paragraph
with a single, concise, user-facing sentence describing the feature (e.g., that
accounts can show an icon from an uploaded image, an emoji, or a bank favicon)
and remove COEP/CSP/SharedArrayBuffer implementation details, moving any
technical explanation to developer/docs rather than the release note.

In `@upcoming-release-notes/auto-bank-icons-on-link.md`:
- Line 6: The release note text ("Bank icons are now fetched automatically when
you link a synced account...") is too long and implementation-heavy for
upcoming-release-notes; shorten it to a single non-technical sentence and remove
CDN/field/connector details so it passes CI. Replace the paragraph with a
concise one-liner such as: "Bank icons are now fetched automatically when you
link a synced account (requires a connected sync server)." and drop any internal
implementation notes or examples.

---

Nitpick comments:
In
`@packages/desktop-client/src/components/accounts/icon-picker/IconPickerModal.tsx`:
- Around line 539-560: Remove the manual useCallback wrapper around handleEmoji
in IconPickerModal (the handler defined in EmojiTab); make handleEmoji a plain
function that calls setEmoji, uses emojiToDataUrl, and catches
IconNormalizationError to call setError and setPreview as currently implemented,
and keep the same dependencies (setEmoji, setPreview, setError, t) referenced
directly so the React Compiler can auto-memoize it.
- Around line 614-636: FullEmojiPicker currently triggers dynamic imports on
every render because the Promise.all call is placed outside a lifecycle hook,
causing multiple .then handlers and redundant setPicker/setData calls (and
double import in StrictMode); move the dynamic import logic into a useEffect
inside FullEmojiPicker that runs only when Picker or data are null (e.g.,
useEffect with dependency [Picker, data] or a single-run effect that sets state
via setPicker and setData), and ensure you guard against setting state after
unmount by tracking a mounted flag or cancelling if needed.

In
`@packages/desktop-client/src/components/banksync/useBuiltInBankSyncProviders.ts`:
- Around line 49-54: The inline anonymous type for "connector" duplicates the
exported PluggyAiConnector; replace the inline type with the canonical
PluggyAiConnector type from `@actual-app/core/types/models/pluggyai` by importing
PluggyAiConnector and changing the connector?: { ... } declaration to
connector?: PluggyAiConnector (ensure the import is added/used in
useBuiltInBankSyncProviders.ts and update any optional/nullable handling to
match the imported type).

In `@packages/loot-core/src/types/models/gocardless.ts`:
- Around line 79-80: Replace the inline hydrated institution type with a Pick of
the canonical type to avoid duplication: change the property type for
institution to Pick<GoCardlessInstitution, 'name' | 'logo'> and then reconcile
the optionality of logo between GoCardlessInstitution and the hydrated
version—if hydration can omit logo, make GoCardlessInstitution.logo optional;
otherwise make the Pick contain a required logo—so update either
GoCardlessInstitution or the institution field accordingly to keep them
consistent.

In `@upcoming-release-notes/test-budget-sync-disabled.md`:
- Line 6: The release note in
upcoming-release-notes/test-budget-sync-disabled.md exposes internal
implementation terms ("cloudFileId" and mutation/sync mechanics); update the
single-sentence note to be concise and user-facing by removing internal terms
and technical details (e.g., say: "Test budgets created via 'Create test file'
no longer attempt to sync when a sync server is configured, matching demo
budgets."), keeping it non-technical and fitting the Maintenance guideline for
that folder.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 98d99b34-bb3e-41a1-b9a5-2dcba196fd76

📥 Commits

Reviewing files that changed from the base of the PR and between 1f4f706 and 1ae377e.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (30)
  • packages/desktop-client/package.json
  • packages/desktop-client/src/components/Modals.tsx
  • packages/desktop-client/src/components/accounts/Header.tsx
  • packages/desktop-client/src/components/accounts/icon-picker/IconPickerModal.tsx
  • packages/desktop-client/src/components/accounts/icon-picker/normalizeIcon.test.ts
  • packages/desktop-client/src/components/accounts/icon-picker/normalizeIcon.ts
  • packages/desktop-client/src/components/banksync/useBuiltInBankSyncProviders.ts
  • packages/desktop-client/src/components/mobile/accounts/AccountPage.tsx
  • packages/desktop-client/src/components/mobile/accounts/AccountsPage.tsx
  • packages/desktop-client/src/components/modals/AccountMenuModal.tsx
  • packages/desktop-client/src/components/sidebar/Account.tsx
  • packages/desktop-client/src/modals/modalsSlice.ts
  • packages/loot-core/migrations/1778162298000_account_bank_icons.sql
  • packages/loot-core/src/mocks/index.ts
  • packages/loot-core/src/server/accounts/app.ts
  • packages/loot-core/src/server/accounts/link.ts
  • packages/loot-core/src/server/budgetfiles/app.ts
  • packages/loot-core/src/server/db/index.ts
  • packages/loot-core/src/server/db/types/index.ts
  • packages/loot-core/src/types/models/account.ts
  • packages/loot-core/src/types/models/gocardless.ts
  • packages/loot-core/src/types/models/pluggyai.ts
  • packages/sync-server/src/app-favicon.test.ts
  • packages/sync-server/src/app-favicon.ts
  • packages/sync-server/src/app-pluggyai/app-pluggyai.js
  • packages/sync-server/src/app-pluggyai/pluggyai-service.js
  • packages/sync-server/src/app.ts
  • upcoming-release-notes/account-icons-sidebar.md
  • upcoming-release-notes/auto-bank-icons-on-link.md
  • upcoming-release-notes/test-budget-sync-disabled.md

Comment on lines +11 to +16
function checkSize(dataUrl: string): string {
const base64 = dataUrl.split(',')[1] ?? '';
if (base64.length > MAX_BASE64_BYTES) {
throw new IconNormalizationError(
`Icon is too large after normalization (${base64.length} bytes > ${MAX_BASE64_BYTES} bytes)`,
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

MAX_BASE64_BYTES is enforced as character count, not decoded byte size

base64.length measures encoded chars, so this currently allows payloads larger than the intended byte cap (roughly +33%). If this limit is meant to be raw bytes, compute decoded size before comparing.

Suggested fix
 function checkSize(dataUrl: string): string {
   const base64 = dataUrl.split(',')[1] ?? '';
-  if (base64.length > MAX_BASE64_BYTES) {
+  const padding =
+    base64.endsWith('==') ? 2 : base64.endsWith('=') ? 1 : 0;
+  const decodedBytes = Math.floor((base64.length * 3) / 4) - padding;
+  if (decodedBytes > MAX_BASE64_BYTES) {
     throw new IconNormalizationError(
-      `Icon is too large after normalization (${base64.length} bytes > ${MAX_BASE64_BYTES} bytes)`,
+      `Icon is too large after normalization (${decodedBytes} bytes > ${MAX_BASE64_BYTES} bytes)`,
     );
   }
   return dataUrl;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/desktop-client/src/components/accounts/icon-picker/normalizeIcon.ts`
around lines 11 - 16, The checkSize function currently compares MAX_BASE64_BYTES
against base64.length (encoded chars); change it to compute the decoded payload
byte length and compare that to MAX_BASE64_BYTES instead. In checkSize, extract
the base64 payload as before, decode it to bytes (e.g., using
Buffer.from(base64, 'base64') or equivalent) and use the resulting byte length
for the comparison; if it exceeds MAX_BASE64_BYTES, throw the same
IconNormalizationError with the decoded byte count in the message to preserve
current behavior and diagnostics.

Comment on lines +1 to +9
BEGIN TRANSACTION;

ALTER TABLE banks ADD COLUMN website TEXT;
ALTER TABLE banks ADD COLUMN icon TEXT;

ALTER TABLE accounts ADD COLUMN website TEXT;
ALTER TABLE accounts ADD COLUMN icon TEXT;

COMMIT;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for size/length/MAX_BASE64_BYTES checks near the server-side icon storage paths
rg -n "MAX_BASE64_BYTES\|icon.*length\|length.*icon\|byteLength\|size.*icon\|icon.*size" \
  --type ts --type js \
  -g '!*.test.*' \
  -g '!node_modules' \
  -A 2 -B 2

echo "---"
# Find tryAutoFetchBankIcon definition
rg -n "tryAutoFetchBankIcon" --type ts --type js -A 20

Repository: actualbudget/actual

Length of output: 6792


🏁 Script executed:

# Get the complete tryAutoFetchBankIcon function (from line 216 onwards)
sed -n '216,260p' packages/loot-core/src/server/accounts/app.ts

Repository: actualbudget/actual

Length of output: 1494


🏁 Script executed:

# Find fetchFaviconViaProxy definition and implementation
rg -n "fetchFaviconViaProxy" --type ts --type js -A 15

Repository: actualbudget/actual

Length of output: 4051


🏁 Script executed:

# Search for MAX_BASE64_BYTES definition to understand the limit
rg -n "MAX_BASE64_BYTES" --type ts --type js -B 2 -A 2

Repository: actualbudget/actual

Length of output: 3148


🏁 Script executed:

# Get the complete fetchFaviconViaProxy function implementation
sed -n '145,215p' packages/loot-core/src/server/accounts/app.ts

Repository: actualbudget/actual

Length of output: 1941


Add server-side size validation for auto-fetched bank icons.

The tryAutoFetchBankIcon function (line 216-244) fetches icons via fetchFaviconViaProxy and stores them directly to the database without size validation. The result base64 is stored as-is at line 239-240:

const dataUrl = `data:${result.contentType};base64,${result.base64}`;
await db.update('banks', { id: bankId, icon: dataUrl });

Client-side icon normalization enforces MAX_BASE64_BYTES (16 KB) for user uploads and emojis, but the server-side auto-fetch path bypasses this entirely. Large favicons from the proxy (e.g., high-resolution bank logos from external sources) will be persisted without a cap, inflating the database and all sync payloads that include account rows.

Add the same size check to fetchFaviconViaProxy or tryAutoFetchBankIcon before storing, using a consistent limit with the client-side MAX_BASE64_BYTES.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/loot-core/migrations/1778162298000_account_bank_icons.sql` around
lines 1 - 9, The server path stores auto-fetched icons without size validation
in tryAutoFetchBankIcon (which calls fetchFaviconViaProxy) before the db.update
call that writes dataUrl; add the same MAX_BASE64_BYTES check used on the
client: after receiving result.base64 and result.contentType, compute the byte
length of the decoded base64 (e.g. bytes = Math.floor(result.base64.length * 3 /
4) - padding) or convert MAX_BASE64_BYTES to an equivalent base64 length and
compare, and only construct and persist dataUrl (via db.update in
tryAutoFetchBankIcon) if it is <= MAX_BASE64_BYTES; if it exceeds the limit,
skip storing the icon (or store null/empty) and log a warning. Ensure you
reference the shared MAX_BASE64_BYTES constant (import or reuse the client
constant) so the server limit matches the client-side limit.

Comment on lines +70 to +75
it('returns 400 when no url/domain/image is provided', async () => {
const res = await request(app).get('/');

expect(res.statusCode).toBe(400);
expect(res.body.error).toMatch(/Missing url, domain, or image/);
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether app-favicon.ts has a 'domain' query parameter code path
rg -n "domain" packages/sync-server/src/app-favicon.ts

Repository: actualbudget/actual

Length of output: 227


Add test coverage for the domain query parameter.

The implementation (line 251) accepts either url or domain as query parameters, but the test suite only covers the url parameter path. Add at least one test exercising ?domain=... to verify this alternative code path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/sync-server/src/app-favicon.test.ts` around lines 70 - 75, Add a new
test in packages/sync-server/src/app-favicon.test.ts that mirrors the existing
URL-path test but uses the domain query parameter to exercise the alternative
code path (the implementation at line 251 accepts either `url` or `domain`). In
the new `it` block use `request(app).get('/')` with `?domain=example.com`, then
assert the same expected outcomes as the URL-case tests (e.g., status code and
response body/headers) so the domain branch is covered; reference the existing
test style using `request` and `app` to match assertions.

Comment on lines +88 to +96
const parsed = new URL(url);
if (!isHostnameAllowed(parsed.hostname)) {
throw new FaviconError(`Blocked hostname: ${parsed.hostname}`, 403);
}

const res = await fetchWithTimeout(url);
if (!res.ok) {
throw new FaviconError(`HTTP ${res.status} fetching ${url}`);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify current protections are hostname-string-only and redirect-unaware.

set -euo pipefail

echo "1) Show SSRF hostname check and where it's used:"
rg -n -C3 'function isHostnameAllowed|isHostnameAllowed\(' packages/sync-server/src/app-favicon.ts

echo
echo "2) Show fetch call options (look for missing redirect control and post-fetch URL/IP re-check):"
rg -n -C3 'fetchWithTimeout|fetch\(' packages/sync-server/src/app-favicon.ts

echo
echo "3) Confirm no DNS resolution-based validation exists in this file:"
rg -n -C2 'dns\.|lookup\(|resolve\(' packages/sync-server/src/app-favicon.ts || true

Repository: actualbudget/actual

Length of output: 1886


🏁 Script executed:

sed -n '54,66p' packages/sync-server/src/app-favicon.ts

Repository: actualbudget/actual

Length of output: 351


SSRF guard is bypassable for domain names and HTTP redirects

The check only validates literal IP addresses; any domain name (including one resolving to private/loopback IPs) is allowed. Additionally, fetch() follows redirects automatically without revalidation, allowing a public domain to redirect to a private IP. Both vectors undermine the SSRF protection.

Code details

The isHostnameAllowed function returns true for all non-IP hostnames (line 65), and fetchWithTimeout has no redirect control. A domain like attacker.com that resolves to 127.0.0.1 would pass the check at line 89, then be fetched without DNS validation or redirect constraints.

Fix by either: (1) resolving hostname to IP(s) and validating them, (2) disabling redirects with redirect: 'manual', or (3) revalidating the final response URL before reading response bytes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/sync-server/src/app-favicon.ts` around lines 88 - 96, The SSRF check
is bypassed because isHostnameAllowed(parsed.hostname) only permits non-IP
hostnames and fetchWithTimeout follows redirects; fix by resolving
parsed.hostname to its IP(s) and validating each IP against your allowed-ranges
policy before fetching (use DNS lookup in the favicon retrieval flow that uses
parsed), and/or call fetchWithTimeout with redirects disabled (redirect:
'manual') and then revalidate the final response.url/Location header (and any
resolved IP for the redirect target) before reading response body; update the
code paths around parsed (URL) validation, isHostnameAllowed usage, and the
fetchWithTimeout response handling to enforce IP validation or redirect
revalidation and throw FaviconError on violations.

authors: [actualbot]
---

Account icons: each account in the sidebar (and on mobile) can now show a small icon to the left of its name. Pick from an uploaded image, an emoji, or — when connected to a sync server — an auto-fetched bank favicon. Icons can also be set on a bank to apply to all of its accounts. The auto-favicon mode requires a sync server because Actual's COEP/CSP security headers (needed for SQL.js's SharedArrayBuffer) block cross-origin images in pure browser installs; the sync server fetches and embeds the favicon as a base64 data URL so it works offline.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Release note is too long and overly technical for the Features category.

The CI length check will likely flag this, and the COEP/CSP/SharedArrayBuffer explanation belongs in documentation rather than a release note. A concise, user-facing version would be appropriate.

✏️ Suggested trim
-Account icons: each account in the sidebar (and on mobile) can now show a small icon to the left of its name. Pick from an uploaded image, an emoji, or — when connected to a sync server — an auto-fetched bank favicon. Icons can also be set on a bank to apply to all of its accounts. The auto-favicon mode requires a sync server because Actual's COEP/CSP security headers (needed for SQL.js's SharedArrayBuffer) block cross-origin images in pure browser installs; the sync server fetches and embeds the favicon as a base64 data URL so it works offline.
+Account icons: accounts and banks can now display a custom icon (uploaded image, emoji, or auto-fetched favicon) in the sidebar and on mobile. Favicon auto-fetching requires a sync server.

Based on learnings: "For files in upcoming-release-notes/*.md, keep release notes concise: ideally one sentence and non-technical content unless the category is Maintenance. A CI check enforces the length, and longer notes will be flagged."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@upcoming-release-notes/account-icons-sidebar.md` at line 6, The release note
in upcoming-release-notes/account-icons-sidebar.md is too long and too technical
for the "Features" category; replace the paragraph with a single, concise,
user-facing sentence describing the feature (e.g., that accounts can show an
icon from an uploaded image, an emoji, or a bank favicon) and remove
COEP/CSP/SharedArrayBuffer implementation details, moving any technical
explanation to developer/docs rather than the release note.

authors: [actualbot]
---

Bank icons are now fetched automatically when you link a synced account. SimpleFin hands us the institution's website, which we run through the favicon fetcher. GoCardless and Pluggy.ai hand us a curated institution logo, which we embed directly (Pluggy: from the connector hosted on its CDN; GoCardless: from `institution.logo`). The icon shows up on the bank record so it applies to every account at that institution. Requires a connected sync server (same constraint as the manual icon picker). Best-effort: a failed or slow fetch never blocks linking, and existing user-set icons are never overwritten.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Release note is too long and technical — will likely fail the CI length check.

The learning for this repo states release notes should be ideally one sentence and non-technical for non-Maintenance categories. This entry is a long multi-sentence paragraph with implementation details (CDN paths, specific field names, etc.) that belongs in the PR description rather than the changelog. Consider trimming to something like:

Bank icons are now fetched automatically when you link a synced account (requires a connected sync server).

Based on learnings: "For files in upcoming-release-notes/*.md, keep release notes concise: ideally one sentence and non-technical content unless the category is Maintenance. A CI check enforces the length, and longer notes will be flagged."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@upcoming-release-notes/auto-bank-icons-on-link.md` at line 6, The release
note text ("Bank icons are now fetched automatically when you link a synced
account...") is too long and implementation-heavy for upcoming-release-notes;
shorten it to a single non-technical sentence and remove CDN/field/connector
details so it passes CI. Replace the paragraph with a concise one-liner such as:
"Bank icons are now fetched automatically when you link a synced account
(requires a connected sync server)." and drop any internal implementation notes
or examples.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 7, 2026

🤖 Auto-generated Release Notes

Hey @StephenBrown2! I've automatically created a release notes file based on CodeRabbit's analysis:

Category: Features
Summary: Add support for customizable account and bank icons, including auto-fetching favicons.
File: upcoming-release-notes/7743.md

If you're happy with this release note, you can add it to your pull request. If not, you'll need to add your own before a maintainer can review your change.

@github-actions

This comment has been minimized.

@StephenBrown2
Copy link
Copy Markdown
Contributor Author

/update-vrt

@coderabbitai coderabbitai Bot removed the contains DB migrations Pull request contains a migration that changes the database label May 8, 2026
@github-actions

This comment has been minimized.

Copy link
Copy Markdown
Contributor

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

♻️ Duplicate comments (1)
packages/sync-server/src/app-favicon.ts (1)

54-66: ⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

⚠️ SSRF guard is still bypassable via domain names and HTTP redirects (unresolved from prior review)

Two attack vectors remain open:

  1. Domain-name bypass (line 65): ipaddr.isValid returns true only when the string is a valid IPv4 or IPv6 address, so any hostname string (e.g. attacker.com resolving to 127.0.0.1) falls through to return true and is fetched without restriction.

  2. Redirect bypass (lines 74–75): fetchWithTimeout uses bare fetch() with no redirect: 'manual' option, so a public domain can redirect to a private IP and the response body is consumed without revalidating the final res.url.

Possible mitigations (pick at least one):

  • Resolve the hostname to its IP(s) via dns.promises.lookup and run each resolved IP through isHostnameAllowed before calling fetch.
  • Set redirect: 'manual' and re-validate the Location header (and resolved IP of the redirect target) before following.
  • After fetch resolves, parse res.url and re-run isHostnameAllowed on the final URL's hostname (catches redirects to literal-IP addresses, but not DNS-based rebinding).

Also applies to: 68-79, 88-96

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/sync-server/src/app-favicon.ts` around lines 54 - 66, The SSRF check
in isHostnameAllowed is insufficient because it only blocks literal IPs and
allows domain names and redirects; update the fetch flow (function
fetchWithTimeout and any call sites that call isHostnameAllowed) to (a) resolve
the request hostname via dns.promises.lookup (or lookupAll) and run each
returned IP through isHostnameAllowed before making the request, and (b) set
fetch option redirect: 'manual' so you can validate any Location header (resolve
that location's hostname via DNS and re-run isHostnameAllowed) before following
redirects; additionally, after any final fetch that returns a response, parse
res.url and re-run isHostnameAllowed on that final hostname as a last check to
catch literal-IP redirects.
🧹 Nitpick comments (1)
packages/desktop-client/src/components/accounts/icon-picker/IconPickerModal.tsx (1)

614-636: ⚡ Quick win

FullEmojiPicker: dynamic imports triggered in render body — move to useEffect

Calling Promise.all([import(...), import(...)]) directly inside the render function (outside any useEffect) is a side effect in render. While module-import caching prevents duplicate network requests, there are two practical concerns:

  1. React Strict Mode double-invokes renders, scheduling multiple .then callbacks.
  2. If either import fails, Picker and data stay null forever — the component is permanently stuck in "Loading…" with no error surfaced to the user.
♻️ Proposed refactor

Add useEffect to the React import (alongside the earlier useRef/useState change):

-import { useRef, useState } from 'react';
+import { useEffect, useRef, useState } from 'react';

Then in FullEmojiPicker:

 function FullEmojiPicker({ onPick }: { onPick: (emoji: string) => void }) {
   const [Picker, setPicker] = useState<...>(null);
   const [data, setData] = useState<unknown>(null);
+  const [loadError, setLoadError] = useState(false);

-  if (!Picker || !data) {
-    void Promise.all([
-      import('@emoji-mart/react'),
-      import('@emoji-mart/data'),
-    ]).then(([picker, d]) => {
-      setPicker(() => picker.default);
-      setData(d.default);
-    });
-    return (
-      <Text style={{ color: theme.pageTextSubdued, fontSize: 12 }}>
-        <Trans>Loading emoji picker…</Trans>
-      </Text>
-    );
-  }
+  useEffect(() => {
+    void Promise.all([
+      import('@emoji-mart/react'),
+      import('@emoji-mart/data'),
+    ])
+      .then(([picker, d]) => {
+        setPicker(() => picker.default);
+        setData(d.default);
+      })
+      .catch(() => setLoadError(true));
+  }, []);
+
+  if (loadError) {
+    return (
+      <Text style={{ color: theme.errorText, fontSize: 12 }}>
+        <Trans>Failed to load emoji picker</Trans>
+      </Text>
+    );
+  }
+  if (!Picker || !data) {
+    return (
+      <Text style={{ color: theme.pageTextSubdued, fontSize: 12 }}>
+        <Trans>Loading emoji picker…</Trans>
+      </Text>
+    );
+  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/desktop-client/src/components/accounts/icon-picker/IconPickerModal.tsx`
around lines 614 - 636, FullEmojiPicker currently triggers dynamic imports in
the render body (setting Picker and data), which should be moved into a
useEffect to avoid render-side effects and duplicate/uncaught promises; update
FullEmojiPicker to run Promise.all([import('@emoji-mart/react'),
import('@emoji-mart/data')]) inside a useEffect with an empty dependency array,
call setPicker(() => picker.default) and setData(d.default) from the effect, and
add error handling (e.g., set an error state or fallback flag) so the component
can render an error message instead of staying stuck on "Loading…"; keep the
existing Picker and data state variables and ensure the effect is
canceled/guarded against updates after unmount if necessary.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@packages/desktop-client/src/components/accounts/icon-picker/IconPickerModal.tsx`:
- Line 1: Remove manual memoization: delete the useCallback import and the
useCallback wrapper around the EmojiTab handler(s) in IconPickerModal — locate
the EmojiTab function/component and any callback definitions currently wrapped
with useCallback (and their dependency arrays) and replace them with plain
functions or arrow functions (e.g., export/declare EmojiTab without
useCallback). Also remove useCallback from the import list at the top of the
file and any other occurrences in the 539-560 region so the React Compiler
handles memoization automatically.

---

Duplicate comments:
In `@packages/sync-server/src/app-favicon.ts`:
- Around line 54-66: The SSRF check in isHostnameAllowed is insufficient because
it only blocks literal IPs and allows domain names and redirects; update the
fetch flow (function fetchWithTimeout and any call sites that call
isHostnameAllowed) to (a) resolve the request hostname via dns.promises.lookup
(or lookupAll) and run each returned IP through isHostnameAllowed before making
the request, and (b) set fetch option redirect: 'manual' so you can validate any
Location header (resolve that location's hostname via DNS and re-run
isHostnameAllowed) before following redirects; additionally, after any final
fetch that returns a response, parse res.url and re-run isHostnameAllowed on
that final hostname as a last check to catch literal-IP redirects.

---

Nitpick comments:
In
`@packages/desktop-client/src/components/accounts/icon-picker/IconPickerModal.tsx`:
- Around line 614-636: FullEmojiPicker currently triggers dynamic imports in the
render body (setting Picker and data), which should be moved into a useEffect to
avoid render-side effects and duplicate/uncaught promises; update
FullEmojiPicker to run Promise.all([import('@emoji-mart/react'),
import('@emoji-mart/data')]) inside a useEffect with an empty dependency array,
call setPicker(() => picker.default) and setData(d.default) from the effect, and
add error handling (e.g., set an error state or fallback flag) so the component
can render an error message instead of staying stuck on "Loading…"; keep the
existing Picker and data state variables and ensure the effect is
canceled/guarded against updates after unmount if necessary.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d2f5a3a5-1c4b-4be6-9a20-c3beb7e905a8

📥 Commits

Reviewing files that changed from the base of the PR and between 1ae377e and 4fb3f6c.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (30)
  • packages/desktop-client/package.json
  • packages/desktop-client/src/components/Modals.tsx
  • packages/desktop-client/src/components/accounts/Header.tsx
  • packages/desktop-client/src/components/accounts/icon-picker/IconPickerModal.tsx
  • packages/desktop-client/src/components/accounts/icon-picker/normalizeIcon.test.ts
  • packages/desktop-client/src/components/accounts/icon-picker/normalizeIcon.ts
  • packages/desktop-client/src/components/banksync/useBuiltInBankSyncProviders.ts
  • packages/desktop-client/src/components/mobile/accounts/AccountPage.tsx
  • packages/desktop-client/src/components/mobile/accounts/AccountsPage.tsx
  • packages/desktop-client/src/components/modals/AccountMenuModal.tsx
  • packages/desktop-client/src/components/sidebar/Account.tsx
  • packages/desktop-client/src/modals/modalsSlice.ts
  • packages/loot-core/migrations/1778162298000_account_bank_icons.sql
  • packages/loot-core/src/mocks/index.ts
  • packages/loot-core/src/server/accounts/app.ts
  • packages/loot-core/src/server/accounts/link.ts
  • packages/loot-core/src/server/budgetfiles/app.ts
  • packages/loot-core/src/server/db/index.ts
  • packages/loot-core/src/server/db/types/index.ts
  • packages/loot-core/src/types/models/account.ts
  • packages/loot-core/src/types/models/gocardless.ts
  • packages/loot-core/src/types/models/pluggyai.ts
  • packages/sync-server/src/app-favicon.test.ts
  • packages/sync-server/src/app-favicon.ts
  • packages/sync-server/src/app-pluggyai/app-pluggyai.js
  • packages/sync-server/src/app-pluggyai/pluggyai-service.js
  • packages/sync-server/src/app.ts
  • upcoming-release-notes/account-icons-sidebar.md
  • upcoming-release-notes/auto-bank-icons-on-link.md
  • upcoming-release-notes/test-budget-sync-disabled.md
✅ Files skipped from review due to trivial changes (9)
  • packages/sync-server/src/app.ts
  • packages/loot-core/src/server/db/types/index.ts
  • packages/desktop-client/src/components/Modals.tsx
  • upcoming-release-notes/test-budget-sync-disabled.md
  • packages/loot-core/migrations/1778162298000_account_bank_icons.sql
  • upcoming-release-notes/account-icons-sidebar.md
  • packages/loot-core/src/types/models/account.ts
  • upcoming-release-notes/auto-bank-icons-on-link.md
  • packages/desktop-client/src/components/accounts/icon-picker/normalizeIcon.ts
🚧 Files skipped from review as they are similar to previous changes (16)
  • packages/desktop-client/package.json
  • packages/loot-core/src/server/budgetfiles/app.ts
  • packages/loot-core/src/types/models/gocardless.ts
  • packages/loot-core/src/mocks/index.ts
  • packages/desktop-client/src/modals/modalsSlice.ts
  • packages/desktop-client/src/components/accounts/Header.tsx
  • packages/loot-core/src/server/accounts/link.ts
  • packages/desktop-client/src/components/mobile/accounts/AccountsPage.tsx
  • packages/desktop-client/src/components/modals/AccountMenuModal.tsx
  • packages/desktop-client/src/components/banksync/useBuiltInBankSyncProviders.ts
  • packages/sync-server/src/app-pluggyai/app-pluggyai.js
  • packages/loot-core/src/types/models/pluggyai.ts
  • packages/desktop-client/src/components/sidebar/Account.tsx
  • packages/sync-server/src/app-pluggyai/pluggyai-service.js
  • packages/loot-core/src/server/accounts/app.ts
  • packages/desktop-client/src/components/mobile/accounts/AccountPage.tsx

@@ -0,0 +1,647 @@
import { useCallback, useRef, useState } from 'react';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Remove manual useCallback — React Compiler handles memoization automatically

useCallback is explicitly imported and used in EmojiTab, which violates the desktop-client guideline. React Compiler auto-memoizes this function; the wrapper and dependency array are redundant.

♻️ Proposed fix
-import { useCallback, useRef, useState } from 'react';
+import { useRef, useState } from 'react';
-  const handleEmoji = useCallback(
-    (value: string) => {
-      setEmoji(value);
-      try {
-        if (!value.trim()) {
-          setPreview(null);
-          return;
-        }
-        const dataUrl = emojiToDataUrl(value);
-        setPreview(dataUrl);
-        setError(null);
-      } catch (err) {
-        const message =
-          err instanceof IconNormalizationError
-            ? err.message
-            : t('Failed to render emoji');
-        setError(message);
-        setPreview(null);
-      }
-    },
-    [setError, setPreview, t],
-  );
+  const handleEmoji = (value: string) => {
+    setEmoji(value);
+    try {
+      if (!value.trim()) {
+        setPreview(null);
+        return;
+      }
+      const dataUrl = emojiToDataUrl(value);
+      setPreview(dataUrl);
+      setError(null);
+    } catch (err) {
+      const message =
+        err instanceof IconNormalizationError
+          ? err.message
+          : t('Failed to render emoji');
+      setError(message);
+      setPreview(null);
+    }
+  };

As per coding guidelines: "Use React Compiler auto-memoization in desktop-client; omit manual useCallback, useMemo, and React.memo when adding or refactoring code."

Also applies to: 539-560

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/desktop-client/src/components/accounts/icon-picker/IconPickerModal.tsx`
at line 1, Remove manual memoization: delete the useCallback import and the
useCallback wrapper around the EmojiTab handler(s) in IconPickerModal — locate
the EmojiTab function/component and any callback definitions currently wrapped
with useCallback (and their dependency arrays) and replace them with plain
functions or arrow functions (e.g., export/declare EmojiTab without
useCallback). Also remove useCallback from the import list at the top of the
file and any other occurrences in the 539-560 region so the React Compiler
handles memoization automatically.

Auto-generated by VRT workflow

PR: actualbudget#7743
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 8, 2026

VRT tests ❌ failed. View the test report.

To update the VRT screenshots, comment /update-vrt on this PR. The VRT update operation takes about 50 minutes.

@PuddleOfFat
Copy link
Copy Markdown

I’m a little lost in the technical jargon, but would users using a local-only offline file (no bank syncing) still have the ability to get bank favicons? Like how 1Password automatically fetches account favicons when one enters a url for the account details?

@StephenBrown2
Copy link
Copy Markdown
Contributor Author

StephenBrown2 commented May 8, 2026

I’m a little lost in the technical jargon, but would users using a local-only offline file (no bank syncing) still have the ability to get bank favicons? Like how 1Password automatically fetches account favicons when one enters a url for the account details?

Yes and no. Unfortunately, for the favicon fetch to work, it must be done on the backend, due to how the security settings are for the frontend to never request any external resources. This does not require you to use bank sync, but rather just to have a sync server running, which the desktop app already does, and if you're running a server, is what you've got as well.

However, you can grab the favicon from the website yourself and upload it to Actual with this, and it'll get stored and stay offline.

@PuddleOfFat
Copy link
Copy Markdown

I’m a little lost in the technical jargon, but would users using a local-only offline file (no bank syncing) still have the ability to get bank favicons? Like how 1Password automatically fetches account favicons when one enters a url for the account details?

Yes and no. Unfortunately, for the favicon fetch to work, it must be done on the backend, due to how the security settings are for the frontend to never request any external resources. This does not require you to use bank sync, but rather just to have a sync server running, which the desktop app already does, and if you're running a server, is what you've got as well.

However, you can grab the favicon from the website yourself and upload it to Actual with this, and it'll get stored and stay offline.

Okay so then one cannot just give it a url and it will automatically fetch the favicon? Or are you saying this method would only be possible on the backend (for those who are not running any server and just using a local-only file)?

@StephenBrown2
Copy link
Copy Markdown
Contributor Author

Okay so then one cannot just give it a url and it will automatically fetch the favicon? Or are you saying this method would only be possible on the backend (for those who are not running any server and just using a local-only file)?

This should work with all supported methods of running Actual.

If you are a) Running the desktop (Electron) app or b) Connected to an Actual server (i.e. running in Docker or the cloud), then you can just give it a url and it will automatically fetch the favicon.

If you are running the web app without a server component, you're probably building from source, and would need to start a server briefly and connect to it to get that to work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

Morty Proxy This is a proxified and sanitized view of the page, visit original site.