[WIP] [AI] Account icon fetch and display (stacked revisions)#7743
[WIP] [AI] Account icon fetch and display (stacked revisions)#7743StephenBrown2 wants to merge 7 commits intoactualbudget:masteractualbudget/actual:masterfrom
Conversation
✅ Deploy Preview for actualbudget ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
👋 Hello contributor! We would love to review your PR! Before we can do that, please make sure:
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 }); |
📝 WalkthroughWalkthroughThis 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. ChangesAccount and Bank Icons
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (5)
upcoming-release-notes/test-budget-sync-disabled.md (1)
6-6: ⚡ Quick winConsider removing internal implementation terms from the release note.
The message can stay user-facing by dropping internals like
cloudFileIdand 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 winConsider using
Pick<GoCardlessInstitution, 'name' | 'logo'>instead of an inline structural type.The inline
{ name: string; logo?: string }duplicates structure already described byGoCardlessInstitution. UsingPickties the hydrated subset to the canonical type and prevents silent drift ifGoCardlessInstitutionevolves (e.g.,logobecomes a richer object).Note also the inconsistency:
GoCardlessInstitution.logois required (string), but the hydrated version makes it optional. If hydration can genuinely omit the logo,GoCardlessInstitution.logoshould probably also be made optional for consistency; if it cannot, thenlogohere 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
logocan be absent after hydration, also updateGoCardlessInstitution:- 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
connectortype duplicates the exportedPluggyAiConnector.The inline anonymous type is structurally identical to
PluggyAiConnectorfrom@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
useCallbackin new file violates the React Compiler auto-memoization guideline.
EmojiTabis new code;handleEmojishould 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 Compilerauto-memoization in desktop-client; omit manualuseCallback,useMemo, andReact.memowhen 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 inuseEffect.Each render while
Pickerordatais null registers a new.then()handler. When the module resolves, all registered handlers fire, callingsetPicker/setDataredundantly. 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
⛔ Files ignored due to path filters (1)
yarn.lockis excluded by!**/yarn.lock,!**/*.lock
📒 Files selected for processing (30)
packages/desktop-client/package.jsonpackages/desktop-client/src/components/Modals.tsxpackages/desktop-client/src/components/accounts/Header.tsxpackages/desktop-client/src/components/accounts/icon-picker/IconPickerModal.tsxpackages/desktop-client/src/components/accounts/icon-picker/normalizeIcon.test.tspackages/desktop-client/src/components/accounts/icon-picker/normalizeIcon.tspackages/desktop-client/src/components/banksync/useBuiltInBankSyncProviders.tspackages/desktop-client/src/components/mobile/accounts/AccountPage.tsxpackages/desktop-client/src/components/mobile/accounts/AccountsPage.tsxpackages/desktop-client/src/components/modals/AccountMenuModal.tsxpackages/desktop-client/src/components/sidebar/Account.tsxpackages/desktop-client/src/modals/modalsSlice.tspackages/loot-core/migrations/1778162298000_account_bank_icons.sqlpackages/loot-core/src/mocks/index.tspackages/loot-core/src/server/accounts/app.tspackages/loot-core/src/server/accounts/link.tspackages/loot-core/src/server/budgetfiles/app.tspackages/loot-core/src/server/db/index.tspackages/loot-core/src/server/db/types/index.tspackages/loot-core/src/types/models/account.tspackages/loot-core/src/types/models/gocardless.tspackages/loot-core/src/types/models/pluggyai.tspackages/sync-server/src/app-favicon.test.tspackages/sync-server/src/app-favicon.tspackages/sync-server/src/app-pluggyai/app-pluggyai.jspackages/sync-server/src/app-pluggyai/pluggyai-service.jspackages/sync-server/src/app.tsupcoming-release-notes/account-icons-sidebar.mdupcoming-release-notes/auto-bank-icons-on-link.mdupcoming-release-notes/test-budget-sync-disabled.md
| 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)`, | ||
| ); |
There was a problem hiding this comment.
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.
| 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; |
There was a problem hiding this comment.
🧩 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 20Repository: 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.tsRepository: actualbudget/actual
Length of output: 1494
🏁 Script executed:
# Find fetchFaviconViaProxy definition and implementation
rg -n "fetchFaviconViaProxy" --type ts --type js -A 15Repository: 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 2Repository: 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.tsRepository: 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.
| 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/); | ||
| }); |
There was a problem hiding this comment.
🧩 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.tsRepository: 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.
| 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}`); | ||
| } |
There was a problem hiding this comment.
🧩 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 || trueRepository: actualbudget/actual
Length of output: 1886
🏁 Script executed:
sed -n '54,66p' packages/sync-server/src/app-favicon.tsRepository: 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. |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
|
🤖 Auto-generated Release Notes Hey @StephenBrown2! I've automatically created a release notes file based on CodeRabbit's analysis: Category: Features 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. |
This comment has been minimized.
This comment has been minimized.
Add favicon proxy image-by-URL mode with tests, merge Pluggy connector metadata for logos, persist institution website on new banks, and best-effort icon fetch for GoCardless, Pluggy, and SimpleFin without blocking link.
1ae377e to
4fb3f6c
Compare
|
/update-vrt |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
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:
Domain-name bypass (line 65):
ipaddr.isValidreturnstrueonly when the string is a valid IPv4 or IPv6 address, so any hostname string (e.g.attacker.comresolving to127.0.0.1) falls through toreturn trueand is fetched without restriction.Redirect bypass (lines 74–75):
fetchWithTimeoutuses barefetch()with noredirect: 'manual'option, so a public domain can redirect to a private IP and the response body is consumed without revalidating the finalres.url.Possible mitigations (pick at least one):
- Resolve the hostname to its IP(s) via
dns.promises.lookupand run each resolved IP throughisHostnameAllowedbefore callingfetch.- Set
redirect: 'manual'and re-validate theLocationheader (and resolved IP of the redirect target) before following.- After
fetchresolves, parseres.urland re-runisHostnameAllowedon 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 touseEffectCalling
Promise.all([import(...), import(...)])directly inside the render function (outside anyuseEffect) is a side effect in render. While module-import caching prevents duplicate network requests, there are two practical concerns:
- React Strict Mode double-invokes renders, scheduling multiple
.thencallbacks.- If either import fails,
Pickeranddatastaynullforever — the component is permanently stuck in "Loading…" with no error surfaced to the user.♻️ Proposed refactor
Add
useEffectto the React import (alongside the earlieruseRef/useStatechange):-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
⛔ Files ignored due to path filters (1)
yarn.lockis excluded by!**/yarn.lock,!**/*.lock
📒 Files selected for processing (30)
packages/desktop-client/package.jsonpackages/desktop-client/src/components/Modals.tsxpackages/desktop-client/src/components/accounts/Header.tsxpackages/desktop-client/src/components/accounts/icon-picker/IconPickerModal.tsxpackages/desktop-client/src/components/accounts/icon-picker/normalizeIcon.test.tspackages/desktop-client/src/components/accounts/icon-picker/normalizeIcon.tspackages/desktop-client/src/components/banksync/useBuiltInBankSyncProviders.tspackages/desktop-client/src/components/mobile/accounts/AccountPage.tsxpackages/desktop-client/src/components/mobile/accounts/AccountsPage.tsxpackages/desktop-client/src/components/modals/AccountMenuModal.tsxpackages/desktop-client/src/components/sidebar/Account.tsxpackages/desktop-client/src/modals/modalsSlice.tspackages/loot-core/migrations/1778162298000_account_bank_icons.sqlpackages/loot-core/src/mocks/index.tspackages/loot-core/src/server/accounts/app.tspackages/loot-core/src/server/accounts/link.tspackages/loot-core/src/server/budgetfiles/app.tspackages/loot-core/src/server/db/index.tspackages/loot-core/src/server/db/types/index.tspackages/loot-core/src/types/models/account.tspackages/loot-core/src/types/models/gocardless.tspackages/loot-core/src/types/models/pluggyai.tspackages/sync-server/src/app-favicon.test.tspackages/sync-server/src/app-favicon.tspackages/sync-server/src/app-pluggyai/app-pluggyai.jspackages/sync-server/src/app-pluggyai/pluggyai-service.jspackages/sync-server/src/app.tsupcoming-release-notes/account-icons-sidebar.mdupcoming-release-notes/auto-bank-icons-on-link.mdupcoming-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'; |
There was a problem hiding this comment.
🛠️ 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
|
VRT tests ❌ failed. View the test report. To update the VRT screenshots, comment |
|
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)? |
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. |
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):
ysu→svx→vqs→xsn→wmm→wqzNOTE: 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 treatsTEST_BUDGET_IDthe same as demo budgets: syncing is forced disabled in non-testNODE_ENV, matching existing demo behavior.Related issue(s)
None linked (bugfix / polish discovered alongside icon work).
Testing
fileIdsync toast.Checklist
upcoming-release-notes/test-budget-sync-disabled.md)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:
1778162298000_account_bank_icons.sqladds the new columns.AccountEntity(and DB row types) includewebsite,icon, and deriveddisplayIcon/displayWebsite(fromCOALESCE(account.*, bank.*)in the accounts query).Related issue(s)
Relates to account/bank icon feature work (no issue number captured here).
Testing
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
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) forapp-favicontests./faviconwith a known-good institution URL when server is running with auth as required.Checklist
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 accounticon/website.bank-update— update bankicon/website.favicon-fetch— server-side call through the sync-server favicon proxy (with clear errors when no server / proxy issues).getAccountsmaps the new DB fields ontoAccountEntity. 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).Checklist
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, ReduxmodalsSliceupdates, 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
yarn workspace @actual-app/webtests / typecheck as appropriate for touched packages.Checklist
upcoming-release-notes/account-icons-sidebar.md)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:
imagemode (query param andsource: 'image') so curated logos can be pulled by URL as well as by institution website.tryAutoFetchBankIcon(new in loot-core): fetches via proxy, persists on the bank row when empty, never overwrites user-set icons, logs failures only.useBuiltInBankSyncProvidersupdated for connector-aware institution naming and URLs.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
institution.logo), Pluggy (connectorimageUrl/ CDN), and confirm bank icon appears when server is configured; confirm link still succeeds if fetch fails or is slow.imagemode changes.Checklist
upcoming-release-notes/auto-bank-icons-on-link.md)Bundle Stats
View detailed bundle stats
desktop-client
Total
Changeset
node_modules/@emoji-mart/data/sets/15/native.jsonnode_modules/emoji-mart/dist/module.jssrc/components/accounts/icon-picker/IconPickerModal.tsxsrc/components/accounts/icon-picker/normalizeIcon.tsnode_modules/@emoji-mart/react/dist/module.jshome/runner/work/actual/actual/packages/component-library/src/icons/v1/Photo.tsxsrc/components/mobile/accounts/AccountPage.tsxsrc/components/sidebar/Account.tsxsrc/components/modals/AccountMenuModal.tsxsrc/components/mobile/accounts/AccountsPage.tsxsrc/components/accounts/Header.tsxsrc/components/banksync/useBuiltInBankSyncProviders.tssrc/components/Modals.tsxpackage.jsonView detailed bundle breakdown
Added
Removed
No assets were removed
Bigger
Smaller
Unchanged
loot-core
Total
Changeset
home/runner/work/actual/actual/packages/loot-core/src/server/accounts/link.tshome/runner/work/actual/actual/packages/loot-core/src/server/accounts/app.tshome/runner/work/actual/actual/packages/loot-core/src/server/db/index.tshome/runner/work/actual/actual/packages/loot-core/src/server/budgetfiles/app.tsView detailed bundle breakdown
Added
Removed
Bigger
No assets were bigger
Smaller
No assets were smaller
Unchanged
No assets were unchanged
api
Total
Changeset
home/runner/work/actual/actual/packages/loot-core/src/server/accounts/link.tshome/runner/work/actual/actual/packages/loot-core/src/server/accounts/app.tshome/runner/work/actual/actual/packages/loot-core/src/server/db/index.tshome/runner/work/actual/actual/packages/loot-core/src/server/budgetfiles/app.tsView detailed bundle breakdown
Added
No assets were added
Removed
No assets were removed
Bigger
Smaller
No assets were smaller
Unchanged
cli
Total
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
crdt
Total
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