Development Diary
This diary tracks the development progress of BudgetBuddy.
2026-05-19 - Fix: Restore “force re-import” UI trigger lost in mobile redesign
What I did:
Beim mobile-first Redesign (2026-03) wurde der UI-Trigger für ForceImportDuplicates versehentlich entfernt — das Handler-Pendant und der Server-Endpoint blieben aber bestehen. Folge: wenn der User Transaktionen in YNAB löscht und sie in BB erneut importieren will, schlägt der Import fehl (“Imported 0 transaction(s). 2 already exist in YNAB”), weil YNABs import_id-Historie auch nach Delete bestehen bleibt — und es gibt keinen Knopf, um den Force-Re-Import mit neuer UUID anzustoßen.
Neues Banner direkt unter dem bestehenden “X Duplikate”-Info-Banner: wird gerendert, wenn model.DuplicateTransactionIds nach einem fehlgeschlagenen Import nicht leer ist. Zeigt Anzahl + Button “Erneut importieren →” der ForceImportDuplicates dispatcht (nutzt das vorhandene Backend-Pipeline mit forceNewImportId=true → neue UUIDs umgehen YNABs Duplikat-Check).
Files Modified:
src/Client/Components/SyncFlow/Views/TransactionList.fs— Neues Warning-Banner mit Inline-Button nach Duplikat-Info-Banner.src/Client/styles.css—.info-banner.warning,.info-banner-icon.warning,.info-banner-text,.info-banner-action(orange Token-Farbe, kein hardcoded Hex außer rgba auf Neon-Orange-Token).src/Client/Components/SyncFlow/State.fs— Misleading Kommentar “UI trigger removed during mobile-first redesign” entfernt.
Rationale:
YNAB löscht import_id-Historie nicht beim Delete einer Transaktion — der einzige Weg zum Re-Import ist eine neue UUID. Backend-Logik dafür existiert bereits (forceImportDuplicates API-Endpoint, YnabClient.createTransactions ... forceNewImportId=true); nur die UI-Lücke wurde wieder geschlossen.
Outcomes:
- Build: PASSED (
dotnet build,npx vite build) - Tests: 467 passed, 6 skipped, 0 failed.
2026-05-19 - Fix: Extract merchant from memo for Comdirect card payments
What I did:
Comdirect’s API liefert für Visa-Debitkarte-/Kartenzahlungen weder remitter.holderName noch creditor.holderName — der Händlername steht ausschließlich im remittanceInfo (Memo). Dadurch wurde Payee = None gesetzt und der YNAB-Upload fiel auf payee_name = "Unknown" zurück. YNAB ordnete daraufhin diese Transaktionen via Payee-Renaming-Rules/Account-Name-Match automatisch als „Transfer to Bar” ein und ignorierte die zugewiesene Kategorie.
Neuer Fallback in transactionDecoder: wenn beides (remitter, creditor) fehlt UND das Memo Kartenzahlung oder Visa-Debitkarte enthält, wird der Text vor dem ersten Komma als Payee extrahiert (z.B. „Software Outlet BV”, „Google Play Apps”).
Files Modified:
src/Server/ComdirectClient.fs— Zwei neueinternal-FunktionenisCardPaymentMemoundextractCardMerchant. Decoder nutzt den Memo-Fallback nur wenn weder remitter noch creditor gesetzt ist (SEPA-Transaktionen unverändert).src/Tests/ComdirectDecoderTests.fs— 10 neue Tests: 3 fürisCardPaymentMemo, 7 fürextractCardMerchant(inkl. Edge Cases: leeres Memo, führendes Komma, kein Komma, Whitespace-Trimmung).
Rationale: Regressionsschutz gegen den YNAB-Auto-Transfer-Bug. Der Fix wirkt nur auf neue Imports — bestehende, fälschlich als „Transfer to Bar” angelegte YNAB-Transaktionen müssen manuell in YNAB korrigiert werden (Payee-Renaming-Rule entfernen + Transfer-Verknüpfung über „Make this a regular transaction” lösen).
Side-Effects:
- DuplicateDetection: künftige Kartenzahlungen sind per Payee fuzzy-matchbar (war vorher nur Datum/Betrag).
- RulesEngine: Rules, die auf Payee matchen, können jetzt für Kartenzahlungen greifen — beabsichtigte Verbesserung.
Outcomes:
- Build: PASSED
- Tests: 467 passed, 6 skipped (.env-abhängige Integration-Tests), 0 failed.
2026-02-28 - Fix: Skip future-dated transactions before YNAB import
What I did: Fixed a bug where Comdirect pre-notified transactions (Vormerkbuchungen) with future booking dates caused YNAB to reject the entire import batch with “date must not be in the future or over 5 years ago”. Transactions with booking dates after today are now automatically filtered out before sending to YNAB.
Root Cause: Comdirect returns transactions with future booking dates (e.g., a standing order for March 2nd visible on February 28th). YNAB’s API rejects any transaction with a future date, and since transactions are sent as a batch, the entire batch of 38 transactions failed.
Files Modified:
src/Server/YnabClient.fs- Added future-date filter increateTransactionswith logging for skipped transactionssrc/Tests/YnabClientTests.fs- Added 2 regression tests: future-dated transactions are filtered out, today’s date is accepted
Outcomes:
- Build: ✅
- Tests: 410/410 passed
- Import works again, future-dated transactions are silently skipped
2026-02-23 - Amazon Order-ID Category Suggestion Feature
What I did: Implemented automatic category suggestions for Amazon transactions based on Order ID matching. When a transaction with a specific Amazon Order ID has been categorized (either in YNAB history or in the current session), other transactions with the same Order ID automatically receive the same category as a suggestion.
Two sources of suggestions:
- Source B (YNAB History): Existing YNAB transactions with categories are scanned for Amazon Order IDs during sync initialization. Matching Order IDs propagate categories to new uncategorized transactions.
- Source A (In-Session): When a user manually categorizes a transaction, other transactions with the same Amazon Order ID in the current session immediately receive the same category.
Files Added:
src/Server/OrderIdMatcher.fs- New pure-logic module withbuildYnabOrderIdMap,applySuggestions, andpropagateInSessionfunctionssrc/Tests/OrderIdMatcherTests.fs- Comprehensive tests for all three OrderIdMatcher functions
Files Modified:
src/Shared/Domain.fs- AddedCategoryId/CategoryNametoYnabTransaction,SuggestedByOrderIdtoSyncTransactionsrc/Shared/Api.fs- ChangedcategorizeTransactionreturn type fromSyncResult<SyncTransaction>toSyncResult<SyncTransaction list>(first = categorized, rest = propagated)src/Server/YnabClient.fs- ExtendedtransactionDecoderto decodecategory_idandcategory_namefrom YNAB APIsrc/Server/RulesEngine.fs- MadeamazonOrderIdPatternandextractAmazonOrderIdpublic, addedextractAmazonOrderIdFromTexthelper, addedSuggestedByOrderId = NonetoclassifyTransactionssrc/Server/Server.fsproj- AddedOrderIdMatcher.fscompile includesrc/Server/Api.fs- Integrated OrderIdMatcher in sync flow (after duplicate detection) and incategorizeTransaction(in-session propagation), addedSuggestedByOrderId = Noneto manual categorize and bulk categorizesrc/Server/Persistence.fs- AddedSuggestedByOrderId = Noneto session restore recordsrc/Client/Components/SyncFlow/Types.fs- ChangedTransactionCategorizedmsg to acceptSyncTransaction listsrc/Client/Components/SyncFlow/State.fs- Updated handler to process list of updated transactions, primary tx tracked for manual categorizationsrc/Client/Components/SyncFlow/Views/TransactionRow.fs- Added purple “Bestellung” badge for Order-ID suggestions, purple status dot color for suggested transactionssrc/Tests/Tests.fsproj- AddedOrderIdMatcherTests.fscompile includesrc/Tests/DuplicateDetectionTests.fs- AddedSuggestedByOrderId = NoneandCategoryId/CategoryNameto record constructionssrc/Tests/SplitTransactionTests.fs- AddedSuggestedByOrderId = Noneto record constructionssrc/Tests/YnabClientTests.fs- AddedSuggestedByOrderId = Noneto record constructionssrc/Tests/SyncSessionManagerTests.fs- AddedSuggestedByOrderId = Noneto record constructionsrc/Tests/PersistenceTypeConversionTests.fs- AddedSuggestedByOrderId = Noneto record construction
Rationale: Amazon transactions frequently come in multiple parts (partial deliveries, returns) with the same Order ID. Manually categorizing each one is tedious. This feature automates the process by propagating categories between transactions sharing the same Order ID.
Outcomes:
- Build: ✅
- Tests: 399/399 passed (6 integration tests skipped)
- Issues: None
2025-12-31 - Created standards/ Directory for Reusable F# Blueprint
What I did:
Created a comprehensive standards/ directory structure to reorganize documentation from docs/ into reusable, AI-agent-optimized standards for new F# full-stack projects. This enables minimal context window usage with maximum impact when using Claude Code or other AI agents.
Files Added:
standards/README.md- Main entry point with task-based and skill-based navigationstandards/global/architecture.md- Tech stack, core principles, project structurestandards/global/development-workflow.md- 9-step development order, bug fix protocol, diary formatstandards/global/claude-tools.md- Serena MCP tool usage (MANDATORY for F# code)standards/global/quick-reference.md- Copy-paste ready code templatesstandards/global/anti-patterns.md- Critical production bugs and lessons learnedstandards/global/learnings.md- Why this stack works, proven patternsstandards/shared/types.md- Domain type design, value objects, discriminated unionsstandards/shared/api-contracts.md- Fable.Remoting patterns, command/query separationstandards/shared/validation.md- Field validators, error accumulationstandards/backend/overview.md- Giraffe HttpHandler basics, Program.fs setupstandards/backend/api-implementation.md- Fable.Remoting implementation, orchestrationstandards/backend/domain-logic.md- Pure business logic, event sourcing, no I/Ostandards/backend/persistence-sqlite.md- SQLite config, Dapper CRUD, OptionHandlerstandards/backend/persistence-files.md- JSON persistence, event sourcing filesstandards/backend/error-handling.md- Result types, async patternsstandards/frontend/overview.md- MVU architecture, core conceptsstandards/frontend/state-management.md- Model, Msg, update patterns, optimistic UIstandards/frontend/view-patterns.md- Component organization, list rendering, RemoteDatastandards/frontend/remotedata.md- RemoteData type, helpers, usage patternsstandards/frontend/routing.md- Feliz.Router patterns, page definitionstandards/testing/overview.md- Expecto basics, test organizationstandards/testing/domain-tests.md- Testing pure functions, event replaystandards/testing/api-tests.md- API endpoint testing, validationstandards/testing/persistence-tests.md- In-memory SQLite, USE_MEMORY_DBstandards/testing/frontend-tests.md- Elmish update testing, state transitionsstandards/testing/property-tests.md- FsCheck property-based testingstandards/deployment/docker.md- Multi-stage builds, Dockerfile patternsstandards/deployment/docker-compose.md- Stack configuration, environment setupstandards/deployment/tailscale.md- Tailscale sidecar setup, ACLsstandards/deployment/production.md- Production config, health checks, monitoring
Rationale:
The existing docs/ directory (~220KB across 18 files) was comprehensive but project-specific and not optimized for AI agent consumption. By extracting and reorganizing the content into focused, self-contained standards files, future projects can benefit from:
- Minimal context usage: Each file is concise (typically <100 lines) and self-contained
- Task-based navigation: README maps tasks directly to relevant files
- Skill integration: Maps Claude Code skills to primary documentation files
- Reusable patterns: Excludes project-specific content (milestones, design system)
- Consistent structure: Every file follows template (Overview, When to Use, Patterns, Checklist, See Also)
Structure:
standards/
├── README.md # Navigation hub
├── global/ # Architecture, workflow, tools (6 files)
├── shared/ # Types, API contracts, validation (3 files)
├── backend/ # API, domain, persistence, errors (6 files)
├── frontend/ # MVU, state, views, RemoteData (5 files)
├── testing/ # Domain, API, persistence, frontend, property tests (6 files)
└── deployment/ # Docker, compose, Tailscale, production (4 files)
Content Sources:
docs/00-ARCHITECTURE.md→global/architecture.md,backend/overview.md,frontend/overview.mddocs/01-PROJECT-SETUP.md→ Excluded (project-specific)docs/02-FRONTEND-GUIDE.md→frontend/state-management.md,frontend/view-patterns.md,frontend/routing.mddocs/03-BACKEND-GUIDE.md→backend/api-implementation.md,backend/domain-logic.mddocs/04-SHARED-TYPES.md→shared/types.md,shared/api-contracts.mddocs/05-PERSISTENCE.md→backend/persistence-sqlite.md,backend/persistence-files.mddocs/06-TESTING.md→testing/*filesdocs/07-BUILD-DEPLOY.md→deployment/docker.md,deployment/docker-compose.md,deployment/production.mddocs/08-TAILSCALE-INTEGRATION.md→deployment/tailscale.mddocs/09-QUICK-REFERENCE.md→global/quick-reference.mddocs/LEARNINGS.md→global/learnings.md,global/anti-patterns.mdCLAUDE.md→global/claude-tools.md,global/development-workflow.md
Outcomes:
- Build: N/A (documentation only)
- Tests: N/A
- Issues: None
- Total files created: 31 (1 README + 30 content files)
- Total lines: ~2,800 lines across all standards files
2025-12-29 - Restored Amazon Order Links in Sync Flow
What I did: Fixed a regression where Amazon Order Links (ExternalLinks) were lost when the Payee ComboBox was added. The links are now visible again next to the payee field.
Files Modified:
src/Client/Components/SyncFlow/Views/TransactionRow.fs:- Added
externalLinkButtonhelper function to render external link icons - Mobile Layout:
- Skipped transactions: Payee text is now a clickable teal link (when ExternalLink available)
- Active transactions: External link icon appears next to the ComboBox
- Desktop Layout:
- Same logic as mobile
- Increased payee column width from
w-48tow-52to accommodate the icon
- Added
Rationale: The previous commit (b44542f) that added transfer payees to the ComboBox accidentally removed the ExternalLinks rendering. Users could no longer click on Amazon transactions to see the original order. The backend was still calculating the links correctly, they just weren’t being displayed.
Technical Details:
- ExternalLinks (Amazon order deep links, PayPal activity links) are still generated in
RulesEngine.fs - For skipped transactions: Payee text itself becomes a clickable link with external icon
- For active transactions: Separate icon button appears next to the editable ComboBox
- Only the first ExternalLink is displayed (most transactions only have one)
Outcomes:
- Build: ✅
- Tests: 377 passed, 6 skipped
- Issues: None
2025-12-29 - Added Transfer-Payees to Sync Flow ComboBox
What I did: Implemented grouped payee options in the Sync Flow ComboBox, allowing users to select Transfer Payees (for transfers between YNAB accounts) in addition to regular payees. Transfers are now displayed in a separate group at the top of the dropdown with a “Transfers” header, followed by regular “Payees”.
Files Modified:
src/Client/DesignSystem/Input.fs:- Moved
ComboBoxOptiontype definition beforeComboBoxProps(F# type ordering requirement) - Updated
ComboBoxcomponent to properly handleComboBoxOption listinstead of tuple list - Added support for disabled items (section headers) - rendered as non-clickable, uppercase, grey text
- Updated keyboard navigation to skip disabled items (section headers)
- Updated filtering to preserve section headers when their group has matching items
- Added
comboBoxGroupedhelper function for directComboBoxOption listusage - Fixed
comboBoxhelper to convert tuples toComboBoxOptionusingtoComboBoxOptions
- Moved
src/Client/Components/SyncFlow/Views/TransactionList.fs:- Changed
payeeOptionsfrom excluding transfers to including them - Added grouping: Transfers first (sorted by name), then Payees (sorted by name)
- Uses
Input.sectionHeaderfor group headers
- Changed
src/Client/Components/SyncFlow/Views/TransactionRow.fs:- Updated type signature to accept
Input.ComboBoxOption listinstead of tuple list - Changed
Input.comboBoxcalls toInput.comboBoxGrouped
- Updated type signature to accept
Rationale: Users needed the ability to categorize bank transactions as transfers between accounts (e.g., “Transfer : Tagesgeld ING”). Previously, transfer payees were filtered out, making it impossible to select them. The grouped dropdown provides clear visual separation between transfer and regular payees.
Technical Details:
- Section headers use
IsDisabled = trueinComboBoxOption - Keyboard navigation (ArrowUp/ArrowDown) automatically skips disabled items
- Search filtering preserves headers if any of their children match
- YNAB recognizes the “Transfer : Account Name” format and processes it as a transfer
Outcomes:
- Build: ✅
- Tests: 377 passed, 6 skipped
- Issues: None
2025-12-28 14:00 - Fixed Payee Loading Bug
What I did: Fixed a bug where payees weren’t being loaded in the Sync Flow, causing the ComboBox dropdown to show no suggestions.
Root Cause:
In src/Client/State.fs, the syncFlowCmd returned from SyncFlow.State.init() was not included in the parent’s Cmd.batch. This meant the LoadPayees (and LoadCategories) commands were never dispatched.
Files Modified:
src/Client/State.fs- AddedCmd.map SyncFlowMsg syncFlowCmdto the init Cmd.batch
The Bug:
// BEFORE (broken):
let cmd = Cmd.batch [
Cmd.map DashboardMsg dashboardCmd
Cmd.map SettingsMsg settingsCmd
initialPageCmd
]
// AFTER (fixed):
let cmd = Cmd.batch [
Cmd.map DashboardMsg dashboardCmd
Cmd.map SettingsMsg settingsCmd
Cmd.map SyncFlowMsg syncFlowCmd // <-- was missing!
initialPageCmd
]
How it was found:
Added debug logging throughout the LoadPayees flow. Discovered that LoadCategories was being dispatched but LoadPayees was not - even though both were in the same Cmd.batch in SyncFlow.State.init(). This pointed to the parent not dispatching the SyncFlow commands at all.
Outcomes:
- Build: ✅
- Payee ComboBox now shows YNAB payee suggestions correctly
2025-12-28 - Added Payee Select Field to Sync Flow
What I did: Added an editable Payee field to the transaction row in the Sync Flow. Users can now select from existing YNAB payees via a searchable dropdown OR enter custom payee text. The payee is saved as a PayeeOverride and sent to YNAB when importing transactions.
Files Added:
- None (new component added inline in existing file)
Files Modified:
src/Shared/Domain.fs- AddedYnabPayeeIdandYnabPayeetypessrc/Shared/Api.fs- AddedgetPayeesmethod toYnabApiinterfacesrc/Server/YnabClient.fs- AddedpayeeDecoderandgetPayeesfunction for YNAB APIsrc/Server/Api.fs- ImplementedgetPayeesinynabApisrc/Client/DesignSystem/Input.fs- Added newComboBoxcomponent (text input with dropdown suggestions)src/Client/Components/SyncFlow/Types.fs- AddedPayees,PendingPayeeVersionsto Model; addedLoadPayees,PayeesLoaded,SetPayeeOverride,CommitPayeeChangemessagessrc/Client/Components/SyncFlow/State.fs- Added handlers for payee loading and editing with debouncingsrc/Client/Components/SyncFlow/Views/TransactionList.fs- AddedpayeeOptionscomputation and passing totransactionRowsrc/Client/Components/SyncFlow/Views/TransactionRow.fs- Replaced static payee display with editable ComboBox
New Component: ComboBox Unlike SearchableSelect (which only allows selection from a list), ComboBox:
- Has an always-editable text input
- Shows filtered suggestions from YNAB payees
- Allows custom text input (not just selection)
- Uses the same styling and keyboard navigation as SearchableSelect
Implementation Details:
- Payees loaded at session start via
LoadPayeescommand - Transfer payees filtered out (only regular payees shown)
- Debouncing applied to payee changes (same pattern as category changes)
- Both category and payee changes use the same API endpoint (
categorizeTransaction) - Desktop: Payee ComboBox appears next to Category (w-48)
- Mobile: Payee ComboBox appears on second row
Rationale: User requested the ability to see and edit payees in the Sync Flow, similar to how they can edit categories. The YNAB payee list provides autocomplete suggestions while still allowing custom payee names.
Outcomes:
- Build: ✅ (2 pre-existing warnings about deprecated Modal component)
- Tests: 377/377 passed (6 skipped - integration tests)
- Issues: None
2025-12-16 16:45 - Added “To be imported” Filter on Sync Page
What I did: Added a new filter “To be imported” (ToBeImported) to the Sync Transactions page. This filter shows only transactions that will actually be imported to YNAB (not skipped and not already imported).
Files Added:
- None
Files Modified:
src/Client/Components/SyncFlow/Types.fs- AddedToBeImportedcase toTransactionFilterdiscriminated unionsrc/Client/Components/SyncFlow/State.fs- Added filter logic forToBeImportedinfilterTransactionsfunctionsrc/Client/Components/SyncFlow/Views/TransactionList.fs- Added filter logic, count calculation, and new stat cardsrc/Client/DesignSystem/Stats.fs- AddedgridFiveColfunction for 5-column responsive grid layout
Rationale: User requested a filter to see only transactions that will be imported. The existing filters (Total, Categorized, Uncategorized, Skipped) didn’t provide a direct way to see which transactions are queued for import.
Implementation Details:
- Filter logic:
tx.Status <> Skipped && tx.Status <> Imported - New stat card with Teal accent color between “Total” and “Categorized”
- Responsive 5-column grid: 2 cols (mobile), 3 cols (tablet), 5 cols (desktop)
Outcomes:
- Build: ✅ (2 pre-existing warnings)
- Tests: 377/377 passed (6 skipped - integration tests)
- Issues: None
2025-12-16 - Fixed Import ID Slash Parsing Bug (Critical, Follow-up)
What I did:
Fixed a follow-up bug in the Import ID duplicate detection where Comdirect transaction IDs containing “/” were not being matched correctly. The previous fix addressed format consistency between generateImportId and matchesImportId, but Api.fs still had incorrect parsing logic.
Root Cause:
In Api.fs line 958, when parsing YNAB’s duplicate_import_ids response:
let cleanId = txIdPart.Split('/') |> Array.head // BUG: stripped "/33825" suffix
This incorrectly split Comdirect IDs like 3I2C21XS1ZXDAP9P/33825 at the “/” and only kept 3I2C21XS1ZXDAP9P. But the local transaction IDs still had the full ID with the “/33825” part, so they never matched.
Symptoms:
- Import always showed “0 imported” even when transactions existed
- Force Import option never appeared (because
ynabRejectedcount was always 0) - Docker logs showed:
[WARNING] Could not map 41 duplicate import IDs to transaction IDs
Files Modified:
src/Server/Api.fs- Removed the erroneousSplit('/')that stripped the Comdirect ID suffixsrc/Tests/DuplicateDetectionTests.fs- Added 2 regression tests for Comdirect IDs with slashes
Rationale:
Comdirect transaction IDs have the format {base}/{number} where the “/” and number are integral parts of the ID. The previous “fix” assumed “/” was an old format suffix that could be stripped, but it’s actually part of the current Comdirect ID format.
Outcomes:
- Build: ✅
- Tests: 377/377 passed (+2 new regression tests)
- Issues: None remaining
2025-12-16 - Fixed Force Import Duplicate Bug (Critical)
What I did:
Fixed a critical bug where Force Import caused transactions to be imported multiple times to YNAB. The root cause was a format mismatch between Import ID generation and detection - YnabClient.fs generated BB:{txId} format but DuplicateDetection.fs searched for BUDGETBUDDY:{txId}: format. This meant matchesByImportId NEVER matched anything, causing the fallback logic to mark ALL transactions as duplicates.
Root Cause:
- Import ID format mismatch:
BB:vsBUDGETBUDDY: - Tests were tautological - they tested the wrong format against the wrong format
- Dangerous fallback in Api.fs: if ID mapping failed, ALL transactions were marked as duplicates
Files Added:
- None
Files Modified:
src/Shared/Domain.fs- AddedImportIdPrefixconstant,generateImportIdandmatchesImportIdhelper functionssrc/Server/YnabClient.fs- Changed to useDomain.generateImportIdandDomain.ImportIdPrefixsrc/Server/DuplicateDetection.fs- ChangedmatchesByImportIdto useDomain.matchesImportIdsrc/Server/Api.fs- Removed dangerous fallback that marked ALL transactions as duplicatessrc/Tests/DuplicateDetectionTests.fs- Fixed tests to useDomain.generateImportIdinstead of hardcoded formatsrc/Tests/YnabClientTests.fs- Fixed all tautological tests:importIdGenerationTests: Replaced with meaningful tests that callDomain.generateImportIdandDomain.matchesImportIdtransactionConversionTests: Changed to useDomain.generateImportIdinstead of hardcoded wrong formatpropertyBasedTests: Changed to useDomain.generateImportIdwith proper TransactionId types
Files Deleted:
- None
Rationale: The bug caused users to see duplicate transactions in YNAB when using Force Import. By centralizing the Import ID format in a constant, we prevent future format mismatches and ensure tests always use the same format as production code.
Outcomes:
- Build: Success
- Tests: 375/375 passed
- Issues: None remaining
2025-12-15 - URL-Based Routing with Feliz.Router
What I did:
Implemented hash-based URL routing using Feliz.Router to enable browser back/forward navigation and deep linking. Users can now navigate directly to pages via URLs like #/sync, #/rules, #/settings.
Files Added:
src/Tests/RoutingTests.fs- 14 unit tests for URL parsing and roundtrip verification
Files Modified:
src/Client/Client.fsproj- Added Feliz.Router 4.0.0 package referencesrc/Client/Types.fs- AddedRoutingmodule with URL parsing functions:parseUrl: Converts URL segments toPagetypetoUrlSegments: ConvertsPageto URL segmentscurrentPage: Gets current page from browser URL
src/Client/State.fs:- Added
open Feliz.Routerimport - Added
UrlChanged of string listmessage - Updated
initto parse initial URL (enables deep linking) - Changed
NavigateToto trigger URL change viaCmd.navigate - Added
UrlChangedhandler for actual state changes
- Added
src/Client/View.fs:- Added
open Feliz.Routerimport - Wrapped view with
React.routercomponent - Added
router.onUrlChangedhandler
- Added
src/Tests/Tests.fsproj- Added RoutingTests.fs to compilation
URL Schema:
| URL | Page |
|—–|——|
| #/ | Dashboard |
| #/sync | SyncFlow |
| #/rules | Rules |
| #/settings | Settings |
| #/invalid | Dashboard (fallback) |
Architecture: The URL is now the single source of truth for navigation:
NavigateTo pagetriggersCmd.navigate(changes URL)- URL change fires
router.onUrlChanged UrlChanged segmentsupdatesCurrentPageand triggers page-specific load commands
Features:
- Browser back/forward buttons work correctly
- Deep linking (e.g., opening
#/settingsdirectly) - All existing navigation patterns still work
- Guard against redundant updates if page hasn’t changed
Rationale: Before this change, navigation was purely in-memory state. Users couldn’t bookmark pages, share URLs, or use browser history. With Feliz.Router, the app now behaves like a standard web application with proper URL routing.
Outcomes:
- Build: ✅ (0 errors, 2 warnings - unrelated Modal.fs deprecation)
- Tests: 371/377 passed (6 skipped integration tests)
- 14 new routing tests added
2025-12-15 - Frontend Architecture: Category Selection Debouncing (Milestone 8)
What I did: Completed Milestone 8 from the Frontend Architecture Improvement plan. Implemented debouncing for category selection to reduce server load when users rapidly change categories.
Files Added:
src/Client/Debounce.fs- Generic debounce utilities for Elmish:delayedfunction to create delayed commands with custom delaydelayedDefaultfunction with 400ms default delay- Version-based approach that works with Elmish architecture
Files Modified:
src/Client/Client.fsproj- Added Debounce.fs to compilationsrc/Client/Components/SyncFlow/Types.fs:- Added
PendingCategoryVersions: Map<TransactionId, int>to Model - Added
CommitCategoryChange of TransactionId * YnabCategoryId option * intmessage
- Added
src/Client/Components/SyncFlow/State.fs:- Added
open Clientimport - Refactored
CategorizeTransactionto use version-based debouncing - Added
CommitCategoryChangehandler that only commits if version is current - Updated
initto includePendingCategoryVersions = Map.empty
- Added
src/Client/Components/SyncFlow/Views/TransactionRow.fs:- Added
isPendingSave: boolparameter totransactionRowfunction - Added orange pulsing dot indicator when save is pending
- Added
src/Client/Components/SyncFlow/Views/TransactionList.fs:- Updated
transactionRowcall to compute and passisPendingSave
- Updated
docs/Frontend-Architecture-milestones.md- Marked Milestone 8 as complete
Rationale: Users can rapidly change categories while reviewing transactions, which caused unnecessary API calls to the server. The debouncing implementation:
- Uses version tracking to ensure only the latest change triggers an API call
- Maintains optimistic UI updates for instant feedback
- Shows a visual indicator that the change is pending
- Works within the Elmish architecture without external state or timers
Design Decisions:
- 400ms delay balances responsiveness with server load reduction
- Version-based approach prevents race conditions naturally
- Orange pulsing dot provides clear pending state feedback
- Debounce module is generic and reusable for other use cases
Outcomes:
- Build: ✅ (0 errors, 2 warnings - unrelated Modal.fs deprecation)
- Tests: 357/357 passed (6 skipped)
- Issues: None
2025-12-15 - Frontend Architecture: PageHeader Design System Component (Milestone 7)
What I did:
Completed Milestone 7 from the Frontend Architecture Improvement plan. Created a reusable PageHeader Design System component for consistent page layouts across the application.
Files Added:
src/Client/DesignSystem/PageHeader.fs- PageHeader component with:-
TitleStylediscriminated union (StandardGradient) Propsrecord with Title, Subtitle, Actions, TitleStyleviewfunction for full customization- Convenience functions:
simple,withSubtitle,gradient,gradientWithSubtitle,withActions,gradientWithActions
-
Files Modified:
src/Client/Client.fsproj- Added PageHeader.fs to compilationsrc/Client/Components/Settings/View.fs- Replaced inlinepageHeaderfunction withPageHeader.withActionssrc/Client/Components/Rules/View.fs- AddedrulesHeaderActionshelper, replaced inline header withPageHeader.gradientWithActionssrc/Client/Components/SyncFlow/View.fs- Replaced inline header withPageHeader.withActionsdocs/FRONTEND-ARCHITECTURE-MILESTONES.md- Marked Milestone 7 as complete
Rationale: Page headers were implemented inline in each view with slight variations. The PageHeader component:
- Provides consistent responsive layout (stack on mobile, row on desktop)
- Supports both standard and gradient title styles
- Handles action button placement uniformly
- Includes
animate-fade-infor smooth appearance - Reduces code duplication across Settings, Rules, and SyncFlow views
Design Decisions:
- Dashboard view was skipped (uses centered layout without traditional header)
- Added
TitleStyleto support gradient titles used in Rules page - Actions passed as ReactElement list for maximum flexibility
- Subtitle is optional (string option) to support both with/without subtitle cases
Outcomes:
- Build: ✅ (0 errors, 2 warnings - unrelated Modal.fs deprecation)
- Tests: 357/357 passed (6 skipped)
- Issues: None
2025-12-15 - Frontend Architecture: RemoteData Helper Module (Milestone 6)
What I did:
Completed Milestone 6 from the Frontend Architecture Improvement plan. Created a comprehensive RemoteData helper module with 17 utility functions for common operations on the RemoteData<'T> type, along with 63 unit tests.
Files Modified:
src/Client/Types.fs- AddedRemoteDatamodule with[<RequireQualifiedAccess>]attribute containing:- Core:
map,bind,isLoading,isSuccess,isFailure,isNotAsked - Extraction:
toOption,withDefault,toError - Error handling:
mapError,recover,recoverWith - Combining:
map2 - Folding:
fold - Conversion:
fromResult,fromOption,fromOptionWithError
- Core:
Files Added:
src/Tests/RemoteDataTests.fs- 63 unit tests covering all helper functions with tests for all four RemoteData cases (Success, Loading, NotAsked, Failure)
Files Modified:
src/Tests/Tests.fsproj- AddedRemoteDataTests.fsand Client project reference
Rationale: The RemoteData type is used throughout the frontend for async data fetching. Helper functions provide:
- Cleaner composition with
map,bind,map2 - Quick state checks with
isLoading,isSuccess, etc. - Safe extraction with
toOption,withDefault - Error recovery patterns with
recover,recoverWith - Conversion utilities for interop with
ResultandOption
Design Decisions:
- Module placed in
Types.fsrather than separate file to avoid compilation order issues - Used
[<RequireQualifiedAccess>]to require explicitRemoteData.prefix - Added Client reference to Tests.fsproj - Client compiles on .NET so this works
- Existing code analyzed but not refactored - explicit match expressions are already readable
Outcomes:
- Build: ✅ (0 errors, 2 warnings - unrelated)
- Tests: 357/357 passed (6 skipped) - 63 new RemoteData tests
- Issues: None
2025-12-16 00:15 - Frontend Architecture: Dashboard Hero Button Design System (Milestone 5)
What I did:
Completed Milestone 5 from the Frontend Architecture Improvement plan. Integrated the Dashboard Sync button into the Design System by creating reusable Button.hero variants and replacing inline styles with Design System components.
Files Modified:
src/Client/DesignSystem/Tokens.fs- Added large glow effect variants to theGlowsmodule:orangeLg/orangeHoverLg- Large orange glow for hero buttonstealLg/tealHoverLg- Large teal glow for secondary hero actionsgreenLg/greenHoverLg- Large green glow for success actions
src/Client/DesignSystem/Button.fs- Added Hero Button section with four variants:hero- Large CTA button with prominent orange glowheroWithIcon- Hero button with icon before textheroLoading- Hero button with loading spinner stateheroTeal- Teal variant for secondary prominent actions
-
src/Client/Components/Dashboard/View.fs- Replaced 17-line inline-styledsyncButtonwith single-lineButton.heroWithIcon "Start Sync" (Icons.sync MD Primary) onNavigateToSync CLAUDE.md- Added hero button documentation to the Button Examples section
Rationale: The Dashboard Sync button used inline Tailwind styles with custom shadow values that weren’t reusable. Moving these to the Design System ensures:
- Consistent hero button styling across the application
- Reusable glow effects for other prominent CTAs
- Easier maintenance of visual effects in one place
Outcomes:
- Build: ✅ (0 errors, 2 warnings - unrelated to this change)
- Tests: 294/294 passed (6 skipped)
- Issues: None
2025-12-15 23:45 - Frontend Architecture: ErrorDisplay Design System Komponente (Milestone 4)
What I did:
Completed Milestone 4 from the Frontend Architecture Improvement plan. Created a new ErrorDisplay Design System component providing standardized error displays across the application with consistent styling and behavior.
Files Added:
src/Client/DesignSystem/ErrorDisplay.fs- New Design System component with multiple error display variants:inline'/inlineWithIcon- Compact inline errors for form validationcard/cardWithTitle/cardCompact- Card-based errors with optional retry buttonshero/heroSimple- Large hero-style errors for major operations (like sync failures)fullPage/fullPageWithAction- Full-page error states for critical failuresforRemoteData/simple/warning- Convenience functions
Files Modified:
src/Client/Client.fsproj- AddedErrorDisplay.fsto compilation (after Button.fs)src/Client/Components/SyncFlow/Views/StatusViews.fs- Replaced inlineerrorViewwithErrorDisplay.herosrc/Client/Components/SyncFlow/Views/TransactionList.fs- Replaced inline error withErrorDisplay.cardCompactsrc/Client/Components/Settings/View.fs- Replaced 3 inline error displays withErrorDisplay.cardCompactsrc/Client/Components/Rules/View.fs- Replaced inline error withErrorDisplay.cardWithTitle
Rationale: The Frontend Architecture Review identified inconsistent error handling across the application. This component standardizes error displays with:
- Consistent visual design using the neon color palette
- ARIA roles for accessibility (
role="alert") - Optional retry functionality
- Multiple variants for different contexts (inline, card, hero, full-page)
Outcomes:
- Build: ✅ (0 errors, 2 warnings - unrelated to this change)
- Tests: 294/294 passed (6 skipped)
- Issues: None
2025-12-15 23:00 - Frontend Architecture: Rules Form State Konsolidierung (Milestone 3)
What I did:
Completed Milestone 3 from the Frontend Architecture Improvement plan. Consolidated the 9 separate form fields in the Rules Model into a dedicated RuleFormState record type, improving code organization and maintainability.
Files Modified:
src/Client/Components/Rules/Types.fs- AddedRuleFormStaterecord type andRuleFormStatemodule withemptyandfromRulehelper functions. UpdatedModeltype to useForm: RuleFormStateinstead of 10 separate fields.src/Client/Components/Rules/State.fs- RemovedemptyRuleFormfunction (nowRuleFormState.empty), updatedinitandupdatefunctions to use the new consolidated form state.src/Client/Components/Rules/View.fs- Updated all form field accesses frommodel.RuleFormFieldNametomodel.Form.FieldNameandmodel.RuleSavingtomodel.Form.IsSaving.
Before (10 separate fields):
type Model = {
RuleFormName: string
RuleFormPattern: string
RuleFormPatternType: PatternType
RuleFormTargetField: TargetField
RuleFormCategoryId: YnabCategoryId option
RuleFormPayeeOverride: string
RuleFormEnabled: bool
RuleFormTestInput: string
RuleFormTestResult: string option
RuleSaving: bool
// ... other fields
}
After (1 consolidated form state):
type RuleFormState = {
Name: string
Pattern: string
PatternType: PatternType
TargetField: TargetField
CategoryId: YnabCategoryId option
PayeeOverride: string
Enabled: bool
TestInput: string
TestResult: string option
IsSaving: bool
}
type Model = {
Form: RuleFormState
// ... other fields
}
Rationale:
- Reduces cognitive load by grouping related form state
- Helper functions (
empty,fromRule) reduce duplication and make code cleaner - Consistent naming pattern (
model.Form.X) improves readability - Easier to add new form fields in the future
Outcomes:
- Build: ✅
- Tests: 294/294 passed (6 skipped integration tests)
- No functional changes (purely structural refactoring)
2025-12-15 22:00 - Frontend Architecture: SyncFlow View Modularisierung (Milestone 2)
What I did: Completed Milestone 2 from the Frontend Architecture Improvement plan. Split the large SyncFlow/View.fs (1700+ lines) into smaller, maintainable modules. Created a new Views/ subfolder with focused components.
Files Added:
src/Client/Components/SyncFlow/Views/StatusViews.fs- Contains:tanWaitingView,fetchingView,loadingView,errorView,completedView,startSyncView(~350 lines)src/Client/Components/SyncFlow/Views/InlineRuleForm.fs- Contains:inlineRuleFormwith pattern type conversions (~200 lines)src/Client/Components/SyncFlow/Views/TransactionRow.fs- Contains:transactionRow,statusDot,duplicateIndicator,expandChevron,skipToggleIcon,createRuleButton,memoRow,duplicateDebugInfo, helper functions (~450 lines)src/Client/Components/SyncFlow/Views/TransactionList.fs- Contains:transactionListView,filterTransactions(~310 lines)
Files Modified:
src/Client/Components/SyncFlow/View.fs- Reduced from ~1700 to ~90 lines, now only contains compositionsrc/Client/Client.fsproj- Added new files in correct compilation orderdocs/FRONTEND-ARCHITECTURE-MILESTONES.md- Marked Milestone 2 as complete
Module Dependencies:
StatusViews.fs → Types, Shared.Domain, DesignSystem
InlineRuleForm.fs → Types, Shared.Domain, DesignSystem
TransactionRow.fs → Types, Shared.Domain, DesignSystem, InlineRuleForm
TransactionList.fs → Types, Shared.Domain, DesignSystem, TransactionRow
View.fs → Types, Shared.Domain, DesignSystem, StatusViews, TransactionList
Rationale: Large monolithic view files are difficult to maintain, navigate, and understand. Splitting into focused modules:
- Improves code organization and readability
- Makes it easier to find and modify specific components
- Enables better code reuse
- Reduces cognitive load when working on specific features
Outcomes:
- Build: ✅
- Tests: 294/294 passed (6 skipped integration tests)
- No functional changes (purely structural refactoring)
Notes:
SplitEditor.fswas not created - no split transaction UI exists in codebaseCategorySelector.fswas not created separately - integrated intoTransactionRow.fsusingInput.searchableSelect
2025-12-15 21:15 - Frontend Architecture: React Key Props (Milestone 1)
What I did: Completed Milestone 1 from the Frontend Architecture Improvement plan. Added React key props to all list renderings to ensure efficient React reconciliation and prevent potential rendering issues.
Files Modified:
src/Client/Components/Settings/View.fs- Addedprop.keyto Budget dropdown options (line 150-155), Addedprop.keyto Account dropdown options (line 194-199)src/Client/Components/Rules/View.fs- Addedprop.keyto Skeleton loader items (line 570-574)docs/FRONTEND-ARCHITECTURE-MILESTONES.md- Marked Milestone 1 as complete with summary
Verified Existing Implementations:
- SyncFlow/View.fs: Transaction list already had proper keys via
prop.key idpattern - Rules/View.fs: Rule list already had proper keys via
prop.key (string id)pattern
Pattern Applied:
// Before
for bwa in budgets do
Html.option [ prop.value id; prop.text bwa.Budget.Name ]
// After
for bwa in budgets do
let (YnabBudgetId id) = bwa.Budget.Id
Html.option [ prop.key id; prop.value id; prop.text bwa.Budget.Name ]
Rationale: React keys help React identify which items have changed, been added, or removed. Without keys, React may re-render the entire list unnecessarily or cause subtle UI bugs when items are reordered.
Outcomes:
- Build: ✅
- Tests: 294/294 passed (6 skipped integration tests)
- No functional changes (structural improvement only)
2025-12-15 18:30 - Refactor: Minimalistisches Dashboard Redesign
What I did: Completely redesigned the Dashboard to be minimalistic and focused. Removed the stats grid (Last Sync, Total Imported, Sync Sessions cards), removed the Recent Activity history list, and replaced everything with a single centered “Start Sync” button with last sync information below it.
Files Modified:
src/Client/Components/Dashboard/Types.fs- Simplified model:RecentSessions: RemoteData<SyncSession list>→LastSession: RemoteData<SyncSession option>, renamed messages accordinglysrc/Client/Components/Dashboard/State.fs- Updated init and update functions to useLoadLastSessioninstead ofLoadRecentSessions, now fetches only the most recent session viagetSyncHistory 1src/Client/Components/Dashboard/View.fs- Complete rewrite: removedstatsSection,historyItem,historySection,pageHeader, keptwarningAlertfor config warnings, new centered layout with large sync button and last sync infosrc/Client/State.fs- Updated navigation handler to useLoadLastSessioninstead ofLoadRecentSessions
Design Changes:
- Before: Dashboard with 3 stats cards + quick action card + 5-item history list
- After: Single centered “Start Sync” button with glow effect, last sync info below (date + transaction count)
- Config warnings remain visible when YNAB/Comdirect not configured
Rationale: User feedback: Dashboard statistics were not actionable. The history list couldn’t be clicked, and the numbers (total imported, sync sessions count) provided no real value. A minimalistic dashboard that focuses on the main action (starting a sync) is more useful.
Outcomes:
- Build: ✅
- Tests: 294/294 passed (6 skipped integration tests)
- Visual: Clean, focused dashboard with prominent sync button
2025-12-15 16:00 - Feature: Verbessertes Formular-Handling mit Validierungsfeedback
What I did: Implemented consistent form validation UX across all forms. Disabled buttons now have clear visual distinction (50% opacity), and a validation message appears under the button showing which required fields are missing. All required fields are now marked with a red asterisk.
Files Added:
src/Client/DesignSystem/Form.fs- New form validation component withsubmitButton,submitButtonWithIcon, andsubmitButtonSecondaryfunctions
Files Modified:
src/Client/DesignSystem/Button.fs- Added disabled styling (disabled:opacity-50 disabled:cursor-not-allowed disabled:shadow-none)src/Client/Client.fsproj- Added Form.fs to compilationsrc/Client/Components/Settings/View.fs- Changed YNAB Token + all 5 Comdirect fields to useInput.groupRequired, replaced buttons withForm.submitButtonsrc/Client/Components/Rules/View.fs- Replaced footer button withForm.submitButtonsrc/Client/Components/SyncFlow/View.fs- Added required marker to Rule Name, replaced button withForm.submitButton, changed Pattern asterisk to neon-red for consistency
UX Changes:
- Disabled buttons: Now visually distinct with 50% opacity and cursor-not-allowed
- Validation message: Orange text under button shows “Bitte ausfüllen: Field1, Field2” when fields are missing
- Required field markers: Red asterisk (*) on all required fields (consistent across all forms)
- Consistent pattern:
Form.submitButtonused in Settings, Rules modal, and Inline rule form
Rationale: User feedback indicated that disabled buttons had no visual distinction and no feedback about why they were disabled. This made forms confusing to use.
Outcomes:
- Build: ✅
- Tests: 294/294 passed (6 skipped integration tests)
- Visual: Buttons clearly distinguishable, validation feedback visible
2025-12-15 - Feature: Comdirect Connection Test in Settings
What I did:
Implemented Comdirect connection test in Settings. The “Test Connection” button initiates the full OAuth + Push-TAN flow to validate credentials. Originally planned to include account discovery via /api/banking/v1/accounts, but discovered that Comdirect doesn’t provide a public accounts endpoint (returns 404 for all endpoint variants).
Architectural Decision: Account-ID remains a manual input field. The connection test validates that credentials are correct through the TAN flow, but users must enter their Account-ID manually.
Files Added:
- None
Files Modified:
src/Shared/Domain.fs- AddedComdirectAccounttype andComdirectAccountIdwrapper type (unused but kept for future)src/Shared/Api.fs- ExtendedSettingsApiwithtestComdirectConnectionandconfirmComdirectTanfunctionssrc/Server/ComdirectClient.fs- AddedaccountDecoder,accountsDecoder, andgetAccountsfunction (unused, kept for future)src/Server/ComdirectAuthSession.fs- AddedfetchAccountsfunction (unused, kept for future)src/Server/Api.fs- Implemented connection test and TAN confirmation endpointssrc/Client/Components/Settings/Types.fs- AddedComdirectConnectionValidandComdirectAuthPendingto Modelsrc/Client/Components/Settings/State.fs- Added init fields and update handlers for connection testsrc/Client/Components/Settings/View.fs- Added TAN flow UI with success/error display
UX Flow:
- User saves Comdirect credentials and Account-ID
- “Test Connection” button appears (only if credentials saved)
- Clicking starts TAN authentication → orange waiting UI
- User confirms Push-TAN in Comdirect app
- Clicking “I’ve Confirmed the TAN” completes validation
- Green success message: “Credentials verified successfully!”
Rationale: Originally a high-priority backlog item for account discovery. Simplified to credential validation after discovering Comdirect doesn’t expose an accounts API.
Outcomes:
- Build: ✅
- Tests: 294/294 passed (6 skipped integration tests)
- Backlog item updated to reflect actual implementation
2025-12-13 19:45 - Fix: Encrypted Settings Lost After Docker Rebuild
What I did:
Fixed critical bug where encrypted settings (YNAB token, Comdirect credentials) were lost after every docker-compose up -d --build. The root cause was that the encryption key was derived from Environment.MachineName, which changes with each Docker container rebuild.
Root Cause Analysis:
// In Persistence.fs - getEncryptionKey()
let machineKey = Environment.MachineName + "BudgetBuddy2025"
Docker containers get a new hostname on each rebuild → different encryption key → previously encrypted settings become unreadable.
Files Modified:
docker-compose.yml- AddedBUDGETBUDDY_ENCRYPTION_KEYenvironment variableREADME.md- Added documentation for the encryption key setup.env- Added generated encryption key (not committed)
Solution:
- Generate a stable encryption key:
openssl rand -base64 32 - Add to
.env:BUDGETBUDDY_ENCRYPTION_KEY=<your-key> - Reference in docker-compose.yml:
BUDGETBUDDY_ENCRYPTION_KEY=${BUDGETBUDDY_ENCRYPTION_KEY}
Important: After applying this fix, existing encrypted settings are lost and must be re-entered. The encryption key in .env must be kept secret and backed up.
Outcomes:
- Build: ✅
- Tests: N/A (configuration fix)
- Settings now persist across Docker rebuilds
2025-12-12 14:45 - Script: deploy-rules.sh for Live Database Import
What I did:
Created automation script to import categorization rules from rules.yml to the live Docker database. The script handles stopping/starting the container and sets the correct DATA_DIR environment variable.
Files Added:
scripts/deploy-rules.sh- Bash script that:- Accepts budget name and optional
--clearflag - Stops the Docker container
- Runs
import-rules.fsxwithDATA_DIR=~/my_apps/budgetbuddy - Restarts the container and waits for health check
- Supports
--listto show available YNAB budgets
- Accepts budget name and optional
Files Modified:
CLAUDE.md- Added documentation for the deploy-rules script under “Quick Commands”
Rationale: Manual rule imports to the live database required multiple steps (stop app, set DATA_DIR, run script, start app). This script automates the entire process with a single command.
Usage:
./scripts/deploy-rules.sh --list # List available budgets
./scripts/deploy-rules.sh "My Budget" # Add new rules
./scripts/deploy-rules.sh "My Budget" --clear # Clear all & reimport
Outcomes:
- Build: N/A (script only)
- Tests: N/A
- Successfully imported 55 rules to live database
2025-12-12 10:30 - Bugfix: Database not initialized in Docker
What I did:
Fixed critical bug where database tables were not created when running in Docker. The initializeDatabase() function was defined but never called at server startup.
Files Modified:
src/Server/Program.fs- Added call toPersistence.initializeDatabase()at server startup (replacedPersistence.ensureDataDir()which only created the directory but not the tables)
Rationale:
When deploying via Docker, the database file was created but without any tables. All API calls failed with “no such table: settings” errors. The initializeDatabase() function creates all required tables (rules, settings, sync_sessions, sync_transactions) with CREATE TABLE IF NOT EXISTS.
Root Cause:
Persistence.ensureDataDir() only creates the /app/data directory. The actual table creation in initializeDatabase() was never called - it was only invoked in tests.
Outcomes:
- Build: ✅
- Tests: 294/294 passed
- Database tables will now be created automatically on first server start
2025-12-11 20:45 - Feature: Skip All / Unskip All Buttons
What I did: Added “Skip All” and “Unskip All” buttons to the Sync Flow transaction review. These buttons allow users to quickly skip or unskip all visible transactions based on the currently active filter.
Files Modified:
src/Client/Components/SyncFlow/Types.fs- Added new Msg types:SkipAllVisible: Triggers bulk skip of visible transactionsUnskipAllVisible: Triggers bulk unskip of visible transactionsBulkSkipCompleted: Handles completion of individual skip operationsBulkUnskipCompleted: Handles completion of individual unskip operations
src/Client/Components/SyncFlow/State.fs- Added:filterTransactionshelper (duplicated from View.fs for state logic)- Handler for
SkipAllVisible: Gets visible non-skipped transactions, applies optimistic UI update, sends parallel API calls - Handler for
UnskipAllVisible: Gets visible skipped transactions, restores status, sends parallel API calls - Handlers for
BulkSkipCompleted/BulkUnskipCompleted: Silent success, rollback on error
src/Client/Components/SyncFlow/View.fs- Added to action bar:- “Skip All (N)” ghost button - shows count, only visible when there are skippable transactions
- “Unskip All (N)” ghost button with green icon - shows count, only visible when there are unskippable transactions
- Buttons respect the active filter (All, Categorized, Uncategorized, Skipped, Confirmed Duplicates)
Implementation Details:
- Optimistic UI: Local state updates immediately for responsive feel
- Parallel API calls: Each transaction skip/unskip is sent in parallel for performance
- Filter-aware: Buttons only affect visible transactions based on current filter
- Error handling: On any error, reloads all transactions from server (rollback)
- Dynamic counts: Button text shows exact number of affected transactions
Rationale: Users with many transactions needed a way to quickly skip/unskip batches instead of clicking individual skip buttons.
Outcomes:
- Build: ✅
- Tests: 294/294 passed
- No new tests needed (UI-only feature using existing skip/unskip API)
2025-12-11 18:30 - Feature: Amazon Order-ID Deep Links
What I did: Implemented deep linking for Amazon transactions. When a transaction memo contains an Amazon order ID (format: ABC-1234567-1234567), the link now goes directly to the specific order details page instead of the generic order history.
Reference Implementation: Based on the YNAB Amazon Linker browser extension (https://github.com/rommsen/ynab-amazon-linker).
Files Modified:
src/Server/RulesEngine.fs- Added:amazonOrderIdPattern: Regex pattern\b([A-Z0-9]{3}-\d{7}-\d{7})\bto match order IDsextractAmazonOrderId: Function to extract order ID from transaction text (payee + memo)- Updated
generateAmazonLink: Now returns deep link to order-details if ID found, else fallback to order-history
src/Tests/RulesEngineTests.fs- Added/updated tests:- “Amazon order ID in memo generates deep link”
- “Amazon order ID in payee generates deep link”
- “Amazon without order ID generates history link”
- Updated existing tests to work with new label format
Link Formats:
- With Order ID:
https://www.amazon.de/gp/your-account/order-details?ie=UTF8&orderID={orderId}(Label: “Bestellung {orderId}”) - Without Order ID:
https://www.amazon.de/gp/your-account/order-history(Label: “Amazon Orders”)
Rationale: Users frequently need to match Amazon transactions to specific orders. The deep link eliminates the need to search through order history manually.
Outcomes:
- Build: ✅
- Tests: 293/293 passed (added 3 new tests)
- No frontend changes needed (ExternalLinks already rendered correctly)
2025-12-11 16:45 - Setup: Serena MCP für F# Code-Analyse
What I did: Konfigurierte Serena MCP (Model Context Protocol) für semantische F#-Code-Analyse. Serena ermöglicht symbolbasiertes Navigieren, Refactoring und intelligente Code-Bearbeitung.
Problem: Serena MCP startete nicht korrekt - der F# Language Server (fsautocomplete) konnte nicht initialisiert werden.
Root Causes:
- Falsches project.yml Format: Die Konfiguration verwendete
language_servers: fsharp: command: ...statt dem erwartetenlanguages: - fsharp - Fehlende Solution-Datei:
fsautocompletebenötigt eine.sln-Datei, die im Projekt fehlte
Files Added:
BudgetBuddy.sln- Solution-Datei mit allen 4 Projekten (Shared, Client, Server, Tests).serena/memories/project_overview.md- Projektübersicht.serena/memories/tech_stack.md- Technologie-Stack.serena/memories/codebase_structure.md- Codebase-Struktur.serena/memories/code_style.md- Code-Stil und Konventionen.serena/memories/suggested_commands.md- Entwicklungs-Commands.serena/memories/task_completion.md- Task-Completion Checklist
Files Modified:
.serena/project.yml- Korrigiertes Format: ```yaml project_name: BudgetBuddy languages:- fsharp ignored_paths:
- node_modules
- .git
- bin
- obj
- .fable
- dist ```
Rationale:
Serena bietet semantische Code-Analyse über Language Server Protocol (LSP). Für F# wird fsautocomplete verwendet, das bereits als globales dotnet tool installiert war. Die Konfiguration musste nur angepasst werden.
Key Learnings:
- Serena erwartet einfaches
languages: - fsharpFormat (nicht verschachtelteslanguage_servers:) - F# Language Server benötigt zwingend eine
.sln-Datei .serena/Ordner sollte eingecheckt werden (außer/cache)- Nach Konfigurationsänderungen muss Claude Code komplett neu gestartet werden
Outcomes:
- Build: ✅
- Serena: ✅ Funktioniert - kann F#-Symbole analysieren
- Onboarding: ✅ 6 Memory-Dateien erstellt
2025-12-11 16:30 - Fix: JSON-Fehlermeldung bei frühem Import-Klick
What I did: Fixed the bug where Comdirect JSON error responses were displayed as raw JSON instead of user-friendly messages. When clicking “I’ve Confirmed” before actually confirming the TAN in the banking app, users previously saw:
Network error (HTTP 400): {"code":"TAN_UNGUELTIG","messages":[{"severity":"INFO","key":"PUSHTAN_ANGEFORDERT","message":"TAN-Freigabe über die App wurde noch nicht erteilt.",...}]}
Now they see the clean message:
TAN-Freigabe über die App wurde noch nicht erteilt.
Files Added:
- None
Files Modified:
src/Server/Api.fs:- Added
parseComdirectErrorJsonfunction (lines 68-110) that parses Comdirect’s structured JSON error format - Extracts the
messagefield from themessagesarray - Provides predefined messages for known error codes (TAN_UNGUELTIG, SESSION_EXPIRED, UNAUTHORIZED)
- Updated
comdirectErrorToStringto use the parser for NetworkError cases - Also improved error messages for other ComdirectError variants
- Added
src/Client/Components/SyncFlow/State.fs:- Updated
syncErrorToStringforComdirectAuthFailedto pass through the reason directly (no redundant prefix) - Backend now provides user-friendly messages, so frontend just displays them
- Updated
src/Tests/ComdirectClientTests.fs:- Added 11 regression tests for
parseComdirectErrorJson - Tests cover: TAN_UNGUELTIG with German message, multiple messages, missing fields, unknown codes, predefined codes, invalid JSON, empty input
- Added 11 regression tests for
Rationale:
Comdirect API returns errors in a structured JSON format with a code field and messages array. Previously, this JSON was passed through as-is, making it unreadable for users. The parser now extracts meaningful messages.
Outcomes:
- Build: ✅
- Tests: 45/46 Comdirect tests pass (1 skipped integration test)
- Backlog updated: Bug marked as completed
2025-12-11 15:35 - Fix: Sync Flow Progress Indicator for Fetching Step
What I did: Fixed the sync flow progress indicator to show the correct step when fetching transactions. The issue had two parts:
- View Issue: No dedicated view for
FetchingTransactionsstatus - only a generic loading view was shown - State Issue: The
confirmTanAPI call runs synchronously (TAN confirm + fetch + rules + duplicate detection), so the client never saw the intermediateFetchingTransactionsstatus
Solution:
- Added
fetchingViewcomponent showing the progress indicator with “Fetch” as active step - Added optimistic UI update in State.fs: When user clicks “I’ve Confirmed”, we immediately set the local session status to
FetchingTransactionsbefore the API call starts
Files Modified:
src/Client/Components/SyncFlow/View.fs:- Added
fetchingViewcomponent (lines 163-238) that shows:- Animated sync icon with neon glow
- “Fetching Transactions” title and description
- Progress indicator showing: Connected ✓ → TAN ✓ → Fetch (active with spinner)
- Updated main view to use
fetchingView ()forFetchingTransactionsstatus
- Added
src/Client/Components/SyncFlow/State.fs:- Modified
ConfirmTanhandler to optimistically update session status toFetchingTransactions - This ensures the UI shows the fetching view immediately while the API call runs
- Modified
Rationale:
The confirmTan backend API runs all operations synchronously, so the intermediate status was never visible to the client. Optimistic UI update solves this by showing the expected state immediately.
Outcomes:
- Build: Server ✓, Client ✓
- Tests: Not applicable (UI-only change)
- Issues: None
2025-12-11 - Feature: Transparent Duplicate Detection with Debug Info
What I did: Implemented a major improvement to the duplicate detection workflow to provide full transparency into how duplicates are detected. The system now clearly distinguishes between two separate mechanisms:
- BudgetBuddy’s pre-import detection (Reference/ImportId/Fuzzy matching BEFORE sending to YNAB)
- YNAB’s rejection (when YNAB rejects during import due to duplicate import_id)
Key Changes:
- Domain Types Extended (
src/Shared/Domain.fs)- Added
DuplicateDetectionDetailsrecord with diagnostic fields:- TransactionReference, ReferenceFoundInYnab, ImportIdFoundInYnab
- FuzzyMatchDate, FuzzyMatchAmount, FuzzyMatchPayee
- Updated
DuplicateStatusto include details in all variants -
Added YnabImportStatustype (NotAttemptedYnabImported RejectedByYnab) - Added
YnabImportStatusfield toSyncTransaction
- Added
- Detection Logic Updated (
src/Server/DuplicateDetection.fs)detectDuplicatenow returns full diagnostic details- All three checks (Reference, ImportId, Fuzzy) are run and results captured
- API Updated (
src/Server/Api.fs)importToYnabnow setsYnabImportStatuson each transaction after YNAB responseforceImportDuplicatesalso setsYnabImportedstatus
- UI Improvements (
src/Client/Components/SyncFlow/View.fs)- Debug Info Panel: Always visible when transaction expanded, shows:
- Reference and whether found in YNAB
- ImportId status (New vs Exists)
- Fuzzy match details if applicable
- YNAB import result with “BudgetBuddy missed this!” warning if applicable
- Separate Banners:
- Teal banner: “X pre-detected duplicates (BudgetBuddy)” [Pre-Import badge]
- Red banner: “X rejected by YNAB” [Post-Import badge] with Force Re-import button
- Count now includes
ynabRejectedfor transactions rejected during import
- Debug Info Panel: Always visible when transaction expanded, shows:
Files Added:
- None (all changes to existing files)
Files Modified:
src/Shared/Domain.fs- New types: DuplicateDetectionDetails, YnabRejectionReason, YnabImportStatussrc/Server/DuplicateDetection.fs- detectDuplicate returns diagnostic detailssrc/Server/Persistence.fs- Updated SyncTransaction creation with new fieldssrc/Server/RulesEngine.fs- Updated SyncTransaction creation with new fieldssrc/Server/Api.fs- importToYnab and forceImportDuplicates set YnabImportStatussrc/Client/Components/SyncFlow/View.fs- Debug info panel, separate banners, updated countssrc/Tests/*.fs- Updated all tests to use new DuplicateStatus format with details
Rationale: Users were confused about why some transactions showed “Möchtest du X reimportieren?” because the two duplicate detection systems were not clearly distinguished. Now:
- Pre-import detection is clearly labeled and auto-skips confirmed duplicates
- Post-import rejections from YNAB are shown separately with explanation
- Each transaction shows exactly why BudgetBuddy made its detection decision
Outcomes:
- Build: All projects compile successfully
- Tests: 279/285 passed (6 integration tests skipped due to missing env)
- Issues: None
2025-12-11 - Bugfix: Force Re-import Button erschien vor YNAB-Import
What I did: Fixed a bug where the “Re-import X Duplicate(s)” button appeared in the action bar BEFORE any import to YNAB had been attempted. The button was incorrectly counting all categorized, non-skipped, non-imported transactions instead of only YNAB-rejected transactions.
Bug Details:
- The action bar had a fallback logic that counted transactions where:
Status <> Imported && Status <> Skipped && (CategoryId.IsSome || Splits.IsSome)
- This was wrong because it showed the button for transactions that were simply ready to import, not rejected by YNAB
Fix:
Changed the duplicateCount calculation to only count transactions with YnabImportStatus = RejectedByYnab _:
let ynabRejectedCount =
match model.SyncTransactions with
| Success transactions ->
transactions
|> List.filter (fun tx ->
match tx.YnabImportStatus with
| RejectedByYnab _ -> true
| _ -> false)
|> List.length
| _ -> 0
Files Modified:
src/Client/Components/SyncFlow/View.fs- Fixed action bar button logic (lines 1199-1218)
Rationale:
The “Re-import Rejected” button should only appear AFTER a YNAB import has been attempted AND YNAB has rejected some transactions. Before import, YnabImportStatus = NotAttempted for all transactions, so the count is 0 and the button is hidden.
Outcomes:
- Build: ✅
- Tests: 279/285 passed (6 skipped integration tests)
- Issues: None - button now only appears when appropriate
2025-12-09 23:00 - Refactor: Stats Filter Semantics and Duplicate Banner
What I did: Refactored the Stats-Filter system for clearer semantics and improved the Duplicate Banner.
Changes:
- Renamed filter types for clarity:
ReadyTransactions→CategorizedTransactionsPendingTransactions→UncategorizedTransactionsDuplicateTransactions→ConfirmedDuplicates
- Fixed count logic:
- Categorized: Has CategoryId AND not Skipped/Imported
- Uncategorized: No CategoryId AND not Skipped/Imported
- ConfirmedDuplicates: Only
ConfirmedDuplicatestatus (notPossibleDuplicate)
- Updated Stats labels: “Ready” → “Categorized”, “Pending” → “Uncategorized”
- Improved Duplicate Banner: Now only shows ConfirmedDuplicates with better German explanation
- Removed redundant “Transaktionen ohne Kategorie” warning banner (info now in Uncategorized stat)
Files Modified:
src/Client/Components/SyncFlow/Types.fs- Renamed TransactionFilter casessrc/Client/Components/SyncFlow/View.fs- Updated filterTransactions, count logic, labels, banner
Outcomes:
- Build: ✅
- Tests: 279/285 passed (6 skipped integration tests)
- UI: Clearer semantics, less clutter
2025-12-09 22:15 - Feature: Clickable Stats Filters for Transaction List
What I did: Implemented clickable filter functionality for the Stats cards on the Sync Transactions page. Users can now click on Total, Ready, Pending, Skipped, or the Duplicates banner to filter the transaction list.
Implementation:
- Added
TransactionFilterdiscriminated union type with 5 filter options:- AllTransactions, ReadyTransactions, PendingTransactions, SkippedTransactions, DuplicateTransactions
- Added
ActiveFilterfield to the SyncFlow Model - Added
SetFiltermessage to Msg type - Extended Stats component with
OnClickandIsActiveprops for interactivity - Added
filterTransactionshelper function to filter by status - Stats cards now show active state with teal ring highlight
- Filter indicator shows “X of Y transactions” with “Show all” link
- Empty state when filter matches no transactions
Files Modified:
src/Client/Components/SyncFlow/Types.fs- Added TransactionFilter DU, ActiveFilter field, SetFilter messagesrc/Client/Components/SyncFlow/State.fs- Initialized ActiveFilter, added SetFilter handlersrc/Client/Components/SyncFlow/View.fs- Added filterTransactions, clickable Stats, filter UIsrc/Client/DesignSystem/Stats.fs- Added OnClick and IsActive props to StatProps
Outcomes:
- Build: ✅
- Tests: 279/285 passed (6 skipped integration tests)
- UX: Users can quickly filter to see only pending, ready, skipped, or duplicate transactions
2025-12-09 21:00 - UI: Breitere Selectbox & elegante Memo-Anzeige
What I did:
- Category-Selectbox von
w-52(208px) aufw-72(288px) verbreitert - Memo-Row neu gestaltet mit Glassmorphism-Stil passend zum Design-System
Memo-Row Redesign:
- Glassmorphism:
bg-base-200/30 backdrop-blur-sm border border-white/5 - Abgerundete Karte mit Margin statt volle Breite
- Icon-Badge mit teal Akzent
- Uppercase Label “MEMO” + relaxed Text-Spacing
Files Modified:
src/Client/Components/SyncFlow/View.fsmemoRow: Komplett neu gestaltet (Zeile 484-512)- Desktop Layout:
w-52→w-72(Zeile 649)
Outcomes:
- Build: ✅
- Selectbox: Kategorienamen besser lesbar
- Memo: Passt jetzt zum glassmorphism Design-System
2025-12-09 20:45 - Performance: Skipped Transactions ohne Selectbox
What I did: Weitere Performance-Optimierung: Skipped Transactions rendern keine interaktive Selectbox mehr, sondern nur noch einen statischen Kategorienamen als Text.
Rationale:
- Die searchableSelect-Komponente ist teuer zu rendern (Event-Handler, State, DOM-Nodes)
- Skipped Transactions brauchen keine Interaktion - sie werden nicht importiert
- Bei 50% skipped = 50% weniger Selectboxen zu rendern
Implementierung:
- Neue Hilfsfunktion
categoryText: Sucht Kategorienamen aus vorberechneten Options - Bedingte Render-Logik:
if tx.Status = Skipped then Text else Selectbox - Angewendet auf Mobile und Desktop Layout
Files Modified:
src/Client/Components/SyncFlow/View.fscategoryText: Neue Hilfsfunktion für Text-Lookup- Mobile Layout (Zeile 533-551): Bedingte Selectbox/Text
- Desktop Layout (Zeile 631-649): Bedingte Selectbox/Text
Outcomes:
- Build: ✅
- Tests: Nicht geändert (reine UI-Optimierung)
- Performance: Weniger DOM-Nodes, schnelleres Rendering
2025-12-09 20:15 - Performance: Category Dropdown 872x schneller
What I did: Massive Performance-Optimierung der Category-Select-Dropdowns auf der Transaction-Seite. Das Öffnen eines Dropdowns dauerte vorher 15,7 Sekunden - jetzt nur noch 18ms.
Root Cause: Die Category-Liste wurde für jede der 193 Transaktionszeilen neu berechnet:
- 193 Transaktionen × 160 Kategorien = 30.880 String-Operationen pro Render
- Jedes Mal wurde
List.mapmit String-Interpolation aufgerufen
Lösung Phase 1 (Category Options vorberechnen):
categoryOptionswird einmal vor der Transaktion-Liste berechnet- Als Parameter an
transactionRowübergeben stattcategories - Reduziert Berechnungen von 30.880 auf 160 pro Render
Lösung Phase 2 (Optimistic UI):
ManuallyCategorizedIdswird sofort im Model aktualisiert (vor API-Call)- “Create Rule” Button erscheint jetzt sofort nach Kategorisierung
Files Modified:
src/Client/Components/SyncFlow/View.fstransactionRowFunktion: Signatur geändert, nimmtcategoryOptionsstattcategories- Mobile/Desktop Category-Selects: Nutzen jetzt vorberechnete Options
transactionListView: BerechnetcategoryOptionseinmal vor der Schleife
src/Client/Components/SyncFlow/State.fsCategorizeTransactionHandler: Optimistisches Update vonManuallyCategorizedIds
Performance-Ergebnis: | Metrik | Vorher | Nachher | Verbesserung | |——–|——–|———|————–| | Dropdown öffnen | 15.700ms | 18ms | 872x |
Outcomes:
- Build: ✅
- Tests: Nicht geändert (UI-only Optimierung)
- Performance: Drastisch verbessert
2025-12-09 18:45 - UI: Transaktionsliste Aktionen sichtbar machen & Links verbessern
What I did: Verbesserung der Transaktionslisten-UI: Aktions-Buttons (Skip, Create Rule) sind jetzt immer sichtbar statt nur bei Hover, und externe Links (Amazon, PayPal) sind klar als Links erkennbar.
Probleme vorher:
- Hover-Aktionen waren unsichtbar aber nahmen Platz ein → Geldbeträge nicht bündig
- Create Rule Button erschien/verschwand → Layout-Shift
- Amazon/PayPal-Links sahen aus wie normaler Text
Lösung:
- Aktionen nach vorne verschoben: Jetzt zwischen Status-Indikatoren und Kategorie-Dropdown
- Feste Container-Breite:
w-16für Aktions-Container → stabile Layouts - Platzhalter: Wenn Create Rule Button nicht aktiv, wird ein unsichtbarer Platzhalter gerendert
- Link-Styling: Teal-Farbe + External-Link-Icon für erkennbare Links
Neues Layout (Desktop):
[▶] [●] [⚠] | [Skip][Rule] | [Category ▾] | [Payee 🔗] | [Date] | [Amount]
Files Modified:
src/Client/Components/SyncFlow/View.fscreateRuleButton: Gibt jetzt Platzhalter-div stattHtml.nonezurück- Desktop-Layout: Aktionen nach vorne verschoben,
opacity-0entfernt, feste Breite - Mobile-Layout: Gleiche Änderungen für Konsistenz
- External Links: Teal-Farbe +
Icons.externalLinkIcon
Outcomes:
- Build: ✅
- Tests: 279/279 passed
- UI: Aktionen immer sichtbar, Beträge bündig, Links erkennbar
2025-12-09 16:30 - Feature: Regel aus Zuweisungs-Dialog erstellen
What I did: Implementierung eines neuen Features, das es ermöglicht, direkt beim manuellen Kategorisieren einer Transaktion eine Regel zu erstellen. Nach dem Kategorisieren erscheint ein “Create Rule” Button, der ein Inline-Formular expandiert.
Key Features:
- Button erscheint nach manueller Kategorisierung einer Transaktion
- Inline-Formular expandiert unter der Transaktion (kein Modal-Unterbrechung)
- Pre-filled: Pattern (Payee), Kategorie, auto-generierter Rule Name
- Default: Contains-Regel (umschaltbar zu Exact/Regex)
- TargetField wählbar: Combined (default), Payee only, Memo only
- Auto-Apply: Neue Regel wird sofort auf andere pending Transaktionen angewandt
- Responsive: Gestackt auf Mobile, Grid auf Desktop
Files Added:
- Keine neuen Dateien
Files Modified:
src/Client/Components/SyncFlow/Types.fs- Neuer Type:
InlineRuleFormStatefür Formular-State - Model erweitert um
InlineRuleFormundManuallyCategorizedIds - 12 neue Msg-Varianten für Inline-Rule-Creation Flow
- Neuer Type:
src/Client/Components/SyncFlow/State.fsinit()erweitert mit neuen Model-FeldernTransactionCategorizedtrackt nun manuell kategorisierte IDs- Neue Helper-Funktionen:
matchesRule,rulesErrorToString - Handler für alle neuen Messages: OpenInlineRuleForm, CloseInlineRuleForm, UpdateInlineRule*, SaveInlineRule, InlineRuleSaved, ApplyNewRuleToTransactions, TransactionsUpdatedByRule
src/Client/Components/SyncFlow/View.fs- Neue Komponente:
createRuleButton(Icon-Button für manuell kategorisierte Transaktionen) - Neue Komponente:
inlineRuleForm(responsive Formular mit Name, Pattern, Type, TargetField) transactionRowakzeptiert nunInlineRuleFormundManuallyCategorizedIdsParameter- Mobile + Desktop: createRuleButton in Actions-Bereich integriert
- Inline-Formular wird unter Transaktion angezeigt wenn aktiv
- Neue Komponente:
UI-Flow:
1. User kategorisiert Transaktion → [✓] ManualCategorized
2. "Create Rule" Button erscheint (🔧-Icon)
3. Klick → Inline-Formular expandiert
4. Pre-filled: Pattern="REWE", Name="Auto: REWE", Category=ausgewählte
5. User kann anpassen: PatternType, TargetField
6. "Create Rule" → API-Call → Toast "Rule created!"
7. Auto-Apply auf andere pending Transaktionen mit passendem Pattern
Responsive Layout:
- Mobile: Alle Felder vertikal gestackt, Buttons full-width
- Desktop: 12-Column Grid (Pattern: 6 cols, Type: 3 cols, TargetField: 3 cols)
Rationale: Dieses Feature ermöglicht einen schnelleren Workflow beim Kategorisieren von Transaktionen. Statt in den Rules-Bereich zu wechseln, kann der User direkt aus dem Kontext heraus eine Regel erstellen.
Outcomes:
- Build: ✅ (Server + Client)
- Tests: 279/279 passed, 6 skipped
- Keine neuen Tests hinzugefügt (RulesEngine bereits umfassend getestet)
2025-12-09 - Fix: UI-Verbesserungen für kompakte Transaktionsliste
What I did: Mehrere UI-Probleme behoben, die nach dem initialen Redesign der Transaktionsliste aufgefallen sind:
- Betrag-Formatierung: Fable transpiliert
:F2zu%P(F2)statt.toFixed(2)- jetzt explizites Rounding - Status-Farben: Skipped-Transaktionen jetzt immer grau (statt rot bei Duplicates)
- NeedsAttention gleiche Farbe: Amazon/PayPal Transaktionen haben jetzt dieselbe Farbe wie andere Uncategorized
- Mobile Actions: Buttons auf Mobile immer sichtbar (nicht hover-only)
- Größere Dropdown-Options: Kategorie-Auswahl mit größerem Touch-Target
- Expand-Feature: Chevron-Icon zum Anzeigen von Memo-Text
Files Modified:
src/Client/DesignSystem/Money.fsformatAmountnutzt jetztSystem.Math.Round(float absAmount, 2).ToString("0.00")statt:F2- Kommentar hinzugefügt, der das Fable-Transpilations-Problem erklärt
src/Client/Components/SyncFlow/View.fsstatusDot: Skipped hat jetzt Priorität über DuplicateStatusstatusDot: NeedsAttention nutzt jetztbg-neon-orange(wie Pending)getRowStateClasses: Skipped-Check vor Duplicate-Check- Mobile Actions:
md:opacity-0 md:group-hover:opacity-100stattopacity-0 group-hover:opacity-100 - Neue
expandChevronFunktion für Memo-Expand-Icon - Neue
memoRowFunktion für expandierbaren Memo-Text transactionRowakzeptiert jetztexpandedIds: Set<TransactionId>Parameter- Layout enthält jetzt Chevron-Icon links vom Status-Dot
src/Client/DesignSystem/Input.fs- Dropdown-Options:
px-4 py-3 text-basestattpx-3 py-2 text-sm - Max-Höhe:
max-h-80stattmax-h-60
- Dropdown-Options:
src/Client/Components/SyncFlow/Types.fs- Model erweitert um
ExpandedTransactionIds: Set<TransactionId> - Neue Message:
ToggleTransactionExpand of TransactionId
- Model erweitert um
src/Client/Components/SyncFlow/State.fsinit()erweitert mitExpandedTransactionIds = Set.empty- Handler für
ToggleTransactionExpand: Toggle Set-Membership
Layout-Änderung Desktop:
[▶/▼] [●] [Kategorie-Dropdown] [Payee...] [Datum] [Betrag] [Actions]
Prioritäten für Farben:
- Skipped → immer grau (opacity-50)
- ConfirmedDuplicate → rot (nur wenn nicht skipped)
- PossibleDuplicate → orange pulsierend (nur wenn nicht skipped)
- Pending/NeedsAttention → orange (gleiche Farbe!)
- AutoCategorized → teal
- ManualCategorized/Imported → grün
Outcomes:
- Build: ✅
- Tests: 279/279 passed, 6 skipped
- Beträge werden korrekt formatiert:
-25.99 EUR - Skipped Duplicates sind grau
- Mobile Actions immer sichtbar
- Memo-Text durch Expand abrufbar
2025-12-09 - Redesign: Kompakte Transaktionsliste im Import-Flow
What I did: Komplettes Redesign der Transaktions-Import-UI von unstrukturierten Karten (~100-120px Höhe) zu einer kompakten, scanbaren Liste (~44px Desktop, ~72px Mobile). Das neue Design folgt dem Mobile-First Ansatz aus dem Design System und stellt die Kategorie als zentrales Interaktionselement in den Fokus.
Files Modified:
src/Client/Components/SyncFlow/View.fs- Neue Helper-Funktionen:
statusDot,getRowStateClasses,duplicateIndicator,skipToggleIcon,externalLinkIcon,formatDateCompact - Neue Komponente:
transactionRowmit Mobile/Desktop responsive Layout - Container von
space-y-3auf Card-Style mitborder-white/5 divide-yumgestellt - Alte
transactionCardFunktion als DEPRECATED markiert (noch vorhanden für Referenz)
- Neue Helper-Funktionen:
Design-Änderungen:
- Status-Anzeige: Von großen Badges zu kleinen farbigen Dots (8px)
- Grün: Kategorisiert (manual/auto)
- Teal: Auto-kategorisiert
- Orange: Pending
- Rot: Duplicate
- Grau: Skipped
- Pink (pulsierend): Needs Attention
- Duplicate-Handling: Inline-Banner entfernt, stattdessen Tooltip auf Icon
- Kategorie-Auswahl: Prominent links positioniert (FOKUS)
- Actions: Hover-only auf Desktop, immer sichtbar auf Mobile
Layout-Struktur:
Desktop: [●] [Category-Dropdown] [Payee...] [Date] [Amount] [Actions]
Mobile: Line 1: [●] [Category-Dropdown] [Amount]
Line 2: [Payee ...] [Date] [Actions]
Touch-Optimierung:
- Skip-Button:
min-w-[44px] min-h-[44px]für Touch-Targets - Kategorie-Dropdown nutzt existierenden
searchableSelectmit Touch-Support
Outcomes:
- Build: ✅
- Tests: 279/279 passed, 6 skipped
- ~2.5x mehr Transaktionen auf Desktop sichtbar
- ~1.5x mehr Transaktionen auf Mobile sichtbar
- Klarer Scan-Pfad: Status → Kategorie → Details → Betrag
2025-12-09 - Feature: Inline-Bestätigung für Rule-Löschen
What I did: Schutz vor versehentlichem Löschen von Rules durch Inline-Bestätigung (MVU-konform). Beim Klick auf das Trash-Icon erscheint ein roter “Löschen?”-Button, der nach 3 Sekunden automatisch zurück zum normalen Icon wechselt.
Files Modified:
src/Client/Components/Rules/Types.fs- Neues Model-Feld:
ConfirmingDeleteRuleId: RuleId option - Neue Messages:
ConfirmDeleteRule of RuleId,CancelConfirmDelete
- Neues Model-Feld:
src/Client/Components/Rules/State.fsinit()erweitert mitConfirmingDeleteRuleId = None- Handler für
ConfirmDeleteRule: Setzt State + startet 3s Timeout-Cmd - Handler für
CancelConfirmDelete: Setzt State zurück DeleteRulesetzt ebenfallsConfirmingDeleteRuleId = None
src/Client/Components/Rules/View.fsruleRowbekommtmodelals ersten Parameter- Delete-Button zeigt konditionell: Trash-Icon oder roten “Löschen?”-Button
- Button hat
animate-pulsefür visuelle Aufmerksamkeit
Technische Details:
- Vollständig MVU-konform: Kein lokaler React-State, alle Änderungen über Messages
- Timeout via
Cmd.OfAsync.performmitAsync.Sleep 3000 - Nur eine Rule kann gleichzeitig im Confirm-Modus sein
Outcomes:
- Build: ✅
- Tests: 279/279 passed, 6 skipped
- Versehentliches Löschen wird durch Zwei-Klick-Mechanismus verhindert
2025-12-09 - Feature: Einzeilige Rules-Darstellung + Legende
What I did: Rules-Darstellung von mehrzeiliger Card-Ansicht auf kompakte einzeilige Zeilen umgestellt. Die neue Darstellung zeigt alle wichtigen Informationen in einer Zeile: Toggle, Pattern-Type-Icon, Name, Pfeil, Kategorie, und Aktions-Buttons. Zusätzlich wurde eine Legende für die Pattern-Type-Icons hinzugefügt.
Files Added:
- Keine neuen Dateien
Files Modified:
src/Client/Components/Rules/View.fs- Neue
patternTypeIconFunktion für kompakte Pattern-Type-Anzeige (Regex:.*, Contains:~, Exact:=) ruleCardersetzt durch kompakteruleRowKomponente- Single-Line Layout:
[Toggle] [PatternIcon] [Name...] → [Category...] [Edit][Delete] - Pattern-Type-Icon auf kleinen Screens versteckt (
hidden sm:block) - Actions auf Desktop nur bei Hover sichtbar (
sm:opacity-0 sm:group-hover:opacity-100) - Name und Pattern als Tooltip verfügbar
- Spacing reduziert:
space-y-1.5stattgap-3 - Legende im Info-Tip hinzugefügt: Zeigt alle Pattern-Types mit Icons (
~ Contains,= Exact,.* Regex)
- Neue
Technische Details:
- Pattern-Type als kompaktes Icon-Badge: farbcodiert (Purple=Regex, Teal=Contains, Green=Exact)
- Flexbox-Layout mit truncate für Überlauf-Handling
- Responsive: Auf Mobile immer Actions sichtbar, auf Desktop nur bei Hover
- Tooltip zeigt vollständigen Namen und Pattern bei Hover über Namen
- Legende responsive: Auf Desktop neben Info-Text, auf Mobile darunter
Outcomes:
- Build: ✅
- Tests: 279/279 passed, 6 skipped
- Rules nehmen jetzt deutlich weniger Platz ein (ca. 1/3 der vorherigen Höhe)
- Mehr Rules auf einen Blick sichtbar
- Pattern-Type-Icons sind durch Legende erklärt
2025-12-08 - Feature: Suchbare Kategorie-Selectboxen mit Keyboard-Navigation
What I did: Neue SearchableSelect Komponente implementiert, die eine durchsuchbare Dropdown-Liste für Kategorien bereitstellt. Beim Öffnen erscheint ein Textfeld mit Auto-Fokus, das die Kategorien mit “contains”-Logik filtert (case-insensitive Suche in der Mitte des Namens möglich). Vollständige Keyboard-Navigation hinzugefügt.
Files Added:
- Keine neuen Dateien
Files Modified:
src/Client/DesignSystem/Input.fs- Neue
SearchableSelectPropsType-Definition - Neue
SearchableSelectReact-Komponente mit:- Click-outside Detection zum Schließen
- Auto-Focus auf Suchfeld beim Öffnen
- Case-insensitive Contains-Filter
- Vollständige Keyboard-Navigation:
- ⬆️⬇️ Arrow Up/Down zum Navigieren
- ↵ Enter zum Auswählen
- ⎋ Escape zum Schließen
- ⇥ Tab zum Schließen
- Wrap-around an Liste-Enden
- Visuelles Highlighting der aktuell hervorgehobenen Option
- Auto-Scroll zur hervorgehobenen Option (nur bei Tastatur-Navigation)
- Mouse-Hover aktualisiert auch den Highlight-Index
- Neue
searchableSelectHelper-Funktion - SVG-Warnungen behoben (Html.svg → Svg.svg)
- Neue
src/Client/Components/SyncFlow/View.fsselectWithPlaceholderersetzt durchsearchableSelectfür Kategorie-Auswahl
src/Client/Components/Rules/View.fsselectWithPlaceholderersetzt durchsearchableSelectfür Kategorie-Auswahl
Technische Details:
- React Hooks:
useStatefür isOpen/searchText/highlightedIndex/isKeyboardNav,useReffür Container/Input/List - useEffect für Click-outside-Detection, Auto-Focus, und Auto-Scroll
- Filter:
label.ToLowerInvariant().Contains(searchLower) - Index 0 = Clear/Placeholder Option, Index 1+ = gefilterte Optionen
- Dropdown zeigt “No matches found” bei leerer Ergebnisliste
data-option-indexAttribut für DOM-Abfrage beim Scrolling
Bug-Fix: Scroll-Verhalten in Modals:
- Problem: Mouse-Hover triggerte
scrollIntoView(), was das gesamte Modal/Fenster scrollte - Lösung: Neuer
isKeyboardNavState unterscheidet zwischen Maus- und Tastatur-Navigation - Auto-Scroll nur bei Tastatur-Navigation via manuelles
list.scrollTop(scrollt nur innerhalb der Dropdown-Liste) setHighlightFromMouseHelper setztisKeyboardNav = falsebei Mouse-Events
Outcomes:
- Build: ✅
- Tests: 279/279 passed, 6 skipped
- Beide Kategorie-Selectboxen sind jetzt durchsuchbar und per Tastatur navigierbar
- Maus-Scrollen funktioniert ohne unerwünschtes Seiten-/Modal-Scrolling
2025-12-08 - Fix: Langsame Kategorie-Auswahl (Optimistisches UI)
What I did: Bug behoben, bei dem die Auswahl einer Kategorie in der Selectbox fast eine Sekunde dauerte. Die Ursache war, dass das UI auf die Backend-Antwort gewartet hat, bevor die Kategorie-Auswahl angezeigt wurde (“pessimistisches UI”). Jetzt wird das Model sofort lokal aktualisiert (optimistisches UI) und der API-Call läuft im Hintergrund.
Problem:
- Bei
CategorizeTransactionwurde das Model NICHT aktualisiert, nur ein API-Call gestartet - Erst bei
TransactionCategorized(nach ~500-1000ms) wurde das Model aktualisiert - Benutzer musste auf jede Kategorie-Auswahl warten
Lösung:
- Optimistisches UI: Model wird sofort lokal aktualisiert mit der neuen Kategorie
- API-Call läuft im Hintergrund
- Bei Fehler: Rollback durch Neuladen der Transaktionen
Files Modified:
src/Client/Components/SyncFlow/State.fsCategorizeTransactionaktualisiert jetzt sofort das lokale ModelTransactionCategorized (Error _)löst jetztLoadTransactionsaus für Rollback
Technische Details:
- Die Kategorie-Auswahl aktualisiert jetzt sofort:
CategoryIdundCategoryNameStatuszuManualCategorized(oderPendingbei Leer-Auswahl)Splitswird aufNonegesetzt
- Bei Fehler werden Transaktionen vom Server neu geladen (konsistenter Zustand)
Outcomes:
- Build: ✅
- Tests: 279/279 passed, 6 skipped
- Kategorie-Auswahl ist jetzt sofort sichtbar (keine Verzögerung mehr)
2025-12-07 22:45 - Entferne Comdirect Zeilennummern-Präfixe aus Memos
What I did: Comdirect sendet den Verwendungszweck (remittanceInfo) mit Zeilennummern-Präfixen wie “01”, “02”, etc. Diese erschienen in der UI vor jedem Memo (z.B. “01REWE…” statt “REWE…”). Eine Regex-Funktion hinzugefügt, die diese Präfixe beim Parsen entfernt.
Files Modified:
src/Server/ComdirectClient.fs- Neue FunktionremoveLineNumberPrefixeshinzugefügt und beim Parsen vonremittanceInfoangewendetsrc/Server/Server.fsproj-InternalsVisibleTofür Tests hinzugefügtsrc/Tests/ComdirectDecoderTests.fs- 10 Unit-Tests für die neue Funktion hinzugefügt
Technische Details:
- Regex-Pattern:
(^|\n)\d{2}(?=[A-Za-zÄÖÜäöüß])- matcht 2-stellige Zahlen am Anfang oder nach Zeilenumbruch, gefolgt von Buchstaben - Unterstützt deutsche Umlaute
- Trim() entfernt übrige Leerzeichen
- Funktion ist
internalum direktes Unit-Testing zu ermöglichen
Outcomes:
- Build: ✅
- Tests: 279/279 passed, 6 skipped (integration tests) - 10 neue Tests
- Memos erscheinen jetzt ohne “01”-Präfix
2025-12-07 21:15 - Legacy Rules Import Script
What I did:
Created a reusable F# script to import categorization rules from the legacy rules.yml file into the BudgetBuddy database.
Files Added:
scripts/import-rules.fsx- Import script that:- Parses YAML rules (match/category pairs)
- Fetches YNAB categories to match names to IDs
- Inserts rules into SQLite database
- Supports
--listflag to show available budgets - Supports budget selection by name parameter
Usage:
dotnet fsi scripts/import-rules.fsx --list # List budgets
dotnet fsi scripts/import-rules.fsx "My Budget" # Import with specific budget
dotnet fsi scripts/import-rules.fsx --clear "My Budget" # Clear all + reimport
Features:
- Category matching: exact name, GroupName/Name format, case-insensitive
- Duplicate detection: skips rules that already exist
- Clear reporting: shows imported/skipped counts and missing categories
Outcomes:
- Successfully imported 55 of 56 rules from rules.yml
- 1 category not found in YNAB: “Krankenversicherung Lena Debeka”
- Script is reusable for future database resets
2025-12-07 19:50 - Increased Memo Limit to 300 + Whitespace Compression
What I did:
- Increased memo character limit from 200 to 300 (testing if YNAB accepts it)
- Added whitespace compression: multiple spaces/tabs/newlines become single space
Why:
- Old GitHub issue (2019) claimed 100-char limit, but that may be outdated
- Testing with 300 to see actual limit
- Whitespace compression saves characters for actual content
Files Modified:
src/Server/YnabClient.fs:- Added
memoLimit = 300constant - Added
compressWhitespacefunction using regex\s+→ single space - Applied compression before truncation
- Added
src/Tests/DuplicateDetectionTests.fs:- Updated limit from 200 to 300
- Added 3 new tests for whitespace compression
src/Tests/YnabClientTests.fs:- Updated memo tests to reflect new behavior
Outcomes:
- Build: ✅ success
- Tests: 269 passed (6 skipped integration tests)
2025-12-07 19:40 - Fixed Memo Truncation Breaking Duplicate Detection
What I did:
Fixed a critical bug where memos were being truncated from the END, causing the Comdirect reference to be cut off. This broke duplicate detection because the reference is appended as “, Ref:
Root Cause:
The old truncateMemo function truncated long memos to 197 characters + “…” = 200 chars (YNAB limit). But the reference was NOT being appended to the memo at all when sending to YNAB! The duplicate detection system (DuplicateDetection.fs) expects to find “Ref:
The Fix:
- Created new
buildMemoWithReferencefunction that:- Appends “, Ref:
" to the memo - If total > 200 chars: truncates from the BEGINNING (preserving the reference)
- Format when truncated: “…
, Ref: "
- Appends “, Ref:
- Kept simple
truncateSplitMemofor split transaction memos (they don’t need a reference since the parent transaction has one)
Files Modified:
src/Server/YnabClient.fs:- Replaced
truncateMemowithbuildMemoWithReference(memo, reference) - Added
truncateSplitMemofor split transactions (line 220-225) - Added
buildMemoWithReferencefunction (lines 227-246) - Updated memo construction to include reference (line 355)
- Updated split memo to use
truncateSplitMemo(line 313)
- Replaced
src/Tests/DuplicateDetectionTests.fs:- Added 6 regression tests for memo with reference behavior (lines 460-568)
- Tests verify reference is always extractable after truncation
Why This Matters: Without the reference in the YNAB memo, transactions imported to YNAB could not be detected as duplicates on subsequent syncs. This would lead to duplicate transactions in YNAB.
Outcomes:
- Build: ✅ success
- Tests: 266 passed (6 skipped integration tests)
- All regression tests for memo truncation pass
2025-12-07 - YNAB Import: Uncleared Transactions + Optional Categories
What I did: Implemented two high-priority features from the backlog that allow more flexible YNAB imports:
- All transactions are now imported as “uncleared” instead of “cleared”
- Transactions without categories can now be imported (appear in YNAB’s Uncategorized view)
Why:
- Uncleared: Users can review and manually clear transactions in YNAB after import
- Optional categories: Supports YNAB’s native uncategorized workflow - users can categorize later in YNAB
Files Modified:
src/Server/YnabClient.fs:- Changed
Cleared = "cleared"toCleared = "uncleared"(line 342) - Removed category requirement from filter (lines 309-313)
- Handle
CategoryId = Nonecase without throwing (lines 354-361)
- Changed
src/Server/Api.fs:- Updated import filter to include
Pendingstatus (lines 835-843) - Renamed
categorizedvariable totoImportfor clarity - Updated all references throughout the function
- Updated import filter to include
src/Client/Components/SyncFlow/View.fs:- Import button now enabled for any non-skipped, non-imported transaction (lines 410-418)
- Added warning banner for uncategorized transactions (lines 402-435)
src/Tests/YnabClientTests.fs:- Added test “transaction is encoded with cleared=uncleared”
- Added test “uncategorized transaction is encoded without category_id”
- Updated “uncategorized transactions pass filter” test (previously verified filter-out)
- Updated existing tests to use “uncleared” instead of “cleared”
UI Changes:
- Warning banner appears when importing uncategorized transactions
- Shows count: “X Transaktion(en) ohne Kategorie”
- Subtitle: “Diese werden als ‘Uncategorized’ in YNAB importiert.”
Outcomes:
- Build: ✅ success
- Tests: 260/266 passed (6 skipped integration tests)
- Both features working as expected
2025-12-07 - Add Comprehensive Tests for SyncSessionManager
What I did: Implemented comprehensive test coverage for the SyncSessionManager module, which previously had ZERO test coverage. This was identified as a critical gap by the QA milestone reviewer.
Files Added:
src/Tests/SyncSessionManagerTests.fs- Comprehensive tests covering:- Session Lifecycle Tests (11 tests): startNewSession, getCurrentSession, completeSession, failSession, clearSession, updateSessionStatus, updateSession, and error handling
- Transaction Operations Tests (14 tests): addTransactions, getTransactions, getTransaction, updateTransaction, updateTransactions, getStatusCounts, updateSessionCounts
- Session Validation Tests (7 tests): validateSession, validateSessionStatus with various scenarios
- Edge Cases and Integration Tests (6 tests): workflow simulation, state transitions, transaction overwrites
Files Modified:
src/Tests/Tests.fsproj- Added SyncSessionManagerTests.fs to compilation
Technical Notes:
- Used
testSequencedfor all test lists because SyncSessionManager uses global mutable state (currentSession : SessionState option ref) - Each test calls
resetSession()at start to ensure test isolation - Tests verify actual behavior, not tautologies (e.g., verifying that completeSession sets both status AND timestamp)
Test Summary:
- Session Lifecycle Tests: 11 tests
- Transaction Operations Tests: 14 tests
- Session Validation Tests: 7 tests
- Edge Cases and Integration: 6 tests
- Total new tests: 38
Outcomes:
- Build: success
- Tests: 258/264 passed (6 skipped integration tests that require .env credentials)
- All SyncSessionManager functionality now has test coverage
2025-12-07 - Fix TAN Confirmation Double-Click Bug
What I did: Fixed a race condition bug where clicking the “I’ve Confirmed” button twice during TAN confirmation caused an error toast “Invalid session state. Expected: AwaitingTan, Actual: FetchingTransactions”. The issue was that the button had no loading state, so users would click again thinking nothing happened.
Root Cause:
- User clicks “I’ve Confirmed” →
ConfirmTanmessage sent - Backend receives request, immediately changes session status to
FetchingTransactions - Button remains active (no loading state)
- User clicks again → second
ConfirmTansent - Backend validates session is in
AwaitingTanstate, but it’s alreadyFetchingTransactions→ Error
Fix:
Added IsTanConfirming: bool flag to track when TAN confirmation is in progress. The button now shows a loading state and subsequent clicks are ignored.
Files Modified:
src/Client/Components/SyncFlow/Types.fs:- Added
IsTanConfirming: boolto Model
- Added
src/Client/Components/SyncFlow/State.fs:- Initialize
IsTanConfirming = falseininit - Set
IsTanConfirming = truewhenConfirmTanis dispatched (ignore if already true) - Reset
IsTanConfirming = falsewhenTanConfirmedis received (success or error)
- Initialize
src/Client/Components/SyncFlow/View.fs:- Updated
tanWaitingViewto acceptisConfirming: boolparameter - Button shows “Importing…” with loading spinner when confirming
- Button shows normal “I’ve Confirmed” when idle
- Updated
Outcomes:
- Build: success
- Tests: 220/227 passed (1 unrelated failure in Persistence Type Conversions test, 6 skipped integration tests)
- Button now shows loading state, preventing double-clicks
2025-12-07 - Add Force Re-Import for YNAB Duplicates
What I did: Implemented a complete “Force Re-Import” feature for transactions that YNAB rejects as duplicates. This solves the problem where deleted transactions in YNAB can’t be re-imported because YNAB remembers the import_id forever.
Flow:
- User clicks “Import to YNAB”
- YNAB responds with duplicates
- Toast shows “Imported X transaction(s). Y already exist in YNAB.”
- Button appears: “Re-import Y Duplicate(s)”
- User clicks → transactions are sent with new UUIDs
- Success!
Files Modified:
src/Server/YnabClient.fs:- Added
forceNewImportId: boolparameter tocreateTransactions - Normal import uses Transaction-ID based import_id (duplicate protection)
- Force import uses new UUID (bypasses YNAB’s duplicate detection)
- Added
src/Shared/Api.fs:- Added
ImportResulttype withCreatedCountandDuplicateTransactionIds - Changed
importToYnabreturn type frominttoImportResult - Added new
forceImportDuplicatesendpoint
- Added
src/Server/Api.fs:- Updated
importToYnabto returnImportResultwith duplicate transaction IDs - Implemented
forceImportDuplicatesendpoint usingforceNewImportId = true
- Updated
src/Client/Components/SyncFlow/Types.fs:- Added
DuplicateTransactionIds: TransactionId listto Model - Added
ForceImportDuplicatesandForceImportCompletedmessages
- Added
src/Client/Components/SyncFlow/State.fs:- Handle
ImportCompletedwithImportResult - Handle
ForceImportDuplicatesandForceImportCompleted
- Handle
src/Client/Components/SyncFlow/View.fs:- Added “Re-import X Duplicate(s)” button when duplicates exist
Outcomes:
- Build: ✅ (Server + Client)
- Tests: 221/221 passed
- Feature working end-to-end
Final Cleanup (17:45):
- Removed all debug
printfnstatements fromApi.fs - Verified all tests still pass after cleanup
2025-12-07 - Fix YNAB False Success Reports + Duplicate Handling
What I did: Fixed two related bugs:
- The app reported successful transaction transfer even when YNAB rejected them as duplicates
- Duplicate transactions were still marked as “Imported” in the UI
Problem 1 - False Success Count:
The createTransactions function returned the count of sent transactions instead of actually created ones.
Problem 2 - Incorrect Status Marking:
Even when YNAB rejected transactions as duplicates, they were marked as Imported in the local state because the code blindly marked all categorized transactions as imported.
Solution:
- New type
TransactionCreateResultinYnabClient.fs:CreatedCount: int- actual number of created transactionsDuplicateImportIds: string list- import IDs that were rejected
- Updated
Api.fsimport logic:- Parse
duplicate_import_idsfrom YNAB response - Only mark transactions as
Importedif they weren’t in the duplicates list - Duplicate transactions keep their original status (not marked as imported)
- Parse
- Added logging to see YNAB request/response for debugging
Files Modified:
src/Server/YnabClient.fs:- Added
TransactionCreateResulttype - Changed
createTransactionsreturn type frominttoTransactionCreateResult - Added request/response logging
- Parse and return duplicate import IDs
- Added
src/Server/Api.fs:- Parse duplicate import IDs to match against transaction IDs
- Only mark actually-created transactions as
Imported - Duplicates retain their original status
src/Tests/YnabClientTests.fs:- Added 4 regression tests for response parsing
Tests Added:
correctly counts created transactions from YNAB responsecorrectly identifies duplicate transactions from YNAB responsehandles response with all transactions rejected as duplicateshandles response missing duplicate_import_ids field
Outcomes:
- Build: ✅
- Tests: 221/221 passed (6 integration tests skipped)
- UI now correctly shows actual imported count
- Duplicate transactions are no longer falsely marked as imported
2025-12-08 01:15 - Add Mandatory Bug Fix Protocol to CLAUDE.md
What I did: Added a new “Bug Fix Protocol (MANDATORY)” section to CLAUDE.md that requires regression tests for every bug fix.
Motivation: Two bugs were fixed today that could have been caught earlier with proper tests:
- Stale reference bug in
completeSession() - JSON encoding bug using
Encode.int64instead ofEncode.int
Neither would have regressed if the original code had proper tests.
Files Modified:
CLAUDE.md- Added “Bug Fix Protocol (MANDATORY)” section with:- 5-step process for fixing bugs
- Test requirements for bug fixes
- Example test comment format
- Explanation of why this matters
Key Points Added:
- Every bug fix MUST include a regression test
- Write failing test FIRST, then fix
- Test should include comment explaining what bug it prevents
- Run full test suite to verify no regressions
2025-12-08 01:00 - Fix YNAB Transactions Not Being Created (JSON Encoding Bug)
What I did: Fixed a critical bug where transactions were not actually being created in YNAB despite the API appearing to succeed.
Problem:
Encode.int64 in Thoth.Json.Net serializes 64-bit integers as strings (because JavaScript can’t handle 64-bit integers):
"amount": "-50250" // WRONG - string with quotes
But YNAB API expects a number:
"amount": -50250 // CORRECT - number without quotes
The YNAB API was likely rejecting the transactions silently or misinterpreting the amount.
Solution:
- Changed
Amountfield fromint64tointinYnabTransactionRequestandYnabSubtransactionRequesttypes - Changed
Encode.int64toEncode.intin encoders - Changed
int64tointin amount conversion code - Added regression tests to verify amounts are serialized as JSON numbers
Files Modified:
src/Server/YnabClient.fs- Changed int64 to int for Amount fields and encoderssrc/Tests/YnabClientTests.fs- Added 2 new tests for JSON encoding verification
Outcomes:
- Build: ✅
- Tests: 217/217 passed (2 new tests)
- YNAB transactions should now be created correctly
Test Coverage Gap Identified: There were no tests verifying the JSON format sent to YNAB. The bug existed because:
- Tests only verified data transformations, not the actual HTTP request body format
- No integration test with YNAB API mocking
2025-12-08 00:30 - Fix Sync Complete Screen Showing Wrong Import Counts
What I did: Fixed a bug where the “Sync Complete” screen always showed 0 IMPORTED and 0 SKIPPED even when transactions were successfully imported to YNAB.
Problem:
In SyncSessionManager.fs, the completeSession() function had a stale reference bug:
- It matched on
currentSession.Valueand capturedstate - Called
updateSessionCounts()which updatedcurrentSession.Valuewith correct counts - Then used the OLD
state.Session(with 0 counts) to create the completed session - This overwrote the updated counts with 0
Solution:
Re-read currentSession.Value after calling updateSessionCounts() to get the session with the updated ImportedCount and SkippedCount.
Files Modified:
src/Server/SyncSessionManager.fs- FixedcompleteSession()to re-read session state afterupdateSessionCounts()
Outcomes:
- Build: ✅
- Tests: 215/215 passed
- Sync Complete screen should now show correct import/skipped counts
2025-12-07 23:45 - Fix YNAB JSON Serialization Error for Transactions
What I did: Fixed a bug where syncing transactions to YNAB failed with “Could not determine JSON object type for type <>f__AnonymousType…”.
Problem:
The createTransactions function in YnabClient.fs used F# anonymous types ({| ... |}) that were cast to obj and passed to Encode.Auto.generateEncoder(). Thoth.Json cannot automatically serialize anonymous types.
Solution:
- Defined proper F# record types
YnabTransactionRequestandYnabSubtransactionRequest - Created manual encoders
encodeTransactionandencodeSubtransactionusingEncode.object - Replaced
Encode.Auto.generateEncoder()withEncode.list (ynabTransactions |> List.map encodeTransaction)
Files Modified:
src/Server/YnabClient.fs- Added record types and manual encoders, refactoredcreateTransactionsto use them
Outcomes:
- Build: ✅
- Tests: 215/215 passed
- YNAB sync should now work correctly
2025-12-07 22:15 - Fix SyncFlow Category Loading Race Condition
What I did: Fixed a bug where categories could not be selected in the SyncFlow transaction review. The category dropdown was always empty because categories were never loaded.
Problem:
LoadCategoriesinSyncFlow/State.fsdid nothing (Cmd.none) - it was marked as “simplified for now”- The parent
Client/State.fsinterceptedLoadCategoriesbut only loaded categories if Settings were already loaded (race condition) - If Settings weren’t loaded yet when navigating to SyncFlow, categories never loaded
Solution:
- Made
LoadCategoriesin SyncFlow self-sufficient - it now fetches settings first to get the budget ID, then loads categories from YNAB API - Removed the parent interception -
LoadCategoriesis now passed through to the child component
Files Modified:
src/Client/Components/SyncFlow/State.fs- ImplementedLoadCategorieshandler to fetch settings and categories independentlysrc/Client/State.fs- Removed special handling forLoadCategories, now passes message to child
Outcomes:
- Build: ✅
- Category selection now works in SyncFlow
- Skip button confirmed working
2025-12-07 18:30 - Simplify Transaction Import Flow (Remove Selection, Add Unskip)
What I did: Simplified the transaction import flow by removing the confusing checkbox selection mechanism and adding proper Skip/Unskip functionality with auto-skip for confirmed duplicates.
Problem: The previous UI had two conflicting mechanisms:
- Checkbox selection (Select All/None)
- Skip button per transaction
This was confusing because the frontend validated based on selection, but the backend ignored selection entirely and just imported all categorized transactions. Users didn’t know which transactions would actually be imported.
Solution:
- Removed selection UI entirely - no more checkboxes, Select All/None buttons, or selection count badge
- Added Unskip functionality - skipped transactions can now be restored
- Auto-skip confirmed duplicates - transactions with
ConfirmedDuplicatestatus are automatically skipped during sync - Simplified import logic - all non-skipped transactions with categories are imported
Files Added:
- None
Files Modified:
src/Shared/Api.fs- AddedunskipTransactionAPI endpointsrc/Server/Api.fs- Added auto-skip for ConfirmedDuplicate, implementedunskipTransactionendpointsrc/Client/Components/SyncFlow/Types.fs- RemovedSelectedTransactionsfrom Model, removed selection Msg types, addedUnskipTransactionandTransactionUnskippedsrc/Client/Components/SyncFlow/State.fs- Removed selection handlers, added Unskip handlerssrc/Client/Components/SyncFlow/View.fs- Removed checkbox, Select All/None buttons, selection badge; updatedcanImportlogic; added Skip/Unskip toggle buttonsrc/Client/DesignSystem/Icons.fs- Addedundoicon for Unskip button
Files Deleted:
- None
Rationale:
- The dual selection+skip mechanism was confusing and the frontend/backend were inconsistent
- Auto-skipping confirmed duplicates reduces user effort and prevents accidental duplicate imports
- Unskip allows users to correct mistakes or import transactions that were auto-skipped
New Import Logic:
- Import all transactions where:
Status != SkippedCategoryId.IsSome
Outcomes:
- Build: ✅
- Tests: 215/215 passed (6 skipped - integration tests)
- UI is now simpler and more intuitive
2025-12-07 17:00 - Fix Modal Animation Flicker
What I did: Fixed the Rules Modal flickering issue where a brief flash of the modal at full opacity appeared before the animation started.
Root Cause:
The CSS animation used animation-fill-mode: forwards, which only preserves the END state of the animation. The element was briefly visible at full opacity before the animation started (which sets opacity to 0), causing a flicker.
Solution:
Changed animation-fill-mode from forwards to both:
backwards: Applies initial animation styles (opacity: 0, scale: 0.95) BEFORE the animation startsforwards: Preserves final animation styles after it endsboth: Does both - element is invisible from the start, then smoothly animates in
Files Modified:
src/Client/styles.css:- Changed
.animate-scale-infromforwardstoboth
- Changed
src/Client/DesignSystem/Modal.fs:- Removed animation from backdrop (only modal content animates now)
- Added
ModalInternalReact component withuseRefto track animation state (prevents re-animation on re-renders)
Technical Details:
/* Before (flickered) */
.animate-scale-in {
animation: scaleIn 0.3s var(--transition-spring) forwards;
}
/* After (no flicker) */
.animate-scale-in {
animation: scaleIn 0.3s var(--transition-spring) both;
}
Rationale:
With forwards only, the browser renders the element at its default state (visible), then starts the animation which sets opacity to 0, causing a brief flash. With both, the initial animation keyframe values are applied immediately, so the element starts invisible.
Outcomes:
- Build: ✅
- Tests: All passing
- Modal opens smoothly without flicker
2025-12-07 15:00 - Fix Frontend Flicker: Local State Updates & Refresh Buttons
What I did: Fixed the frontend flickering issue where the entire list would reload from the server after every mutation (categorize, skip, toggle, save). Now mutations update the local state directly, eliminating unnecessary API calls and providing a smoother UX. Also added manual refresh buttons to all pages.
Files Modified:
src/Client/Components/SyncFlow/State.fs:TransactionCategorized: Now updates local list instead ofCmd.ofMsg LoadTransactionsTransactionSkipped: Now updates local list instead of reloadBulkCategorized: Now updates multiple transactions locally using MapSplitsSaved: Now updates local list and closes modal without reloadSplitCleared: Now updates local list without reload
src/Client/Components/Settings/State.fs:YnabTokenSaved: Updates local state with saved tokenComdirectCredentialsSaved: Updates local state with saved credentialsSyncSettingsSaved: Updates local state with saved sync settingsDefaultBudgetSet: Now takes (budgetId, Result) and updates local stateDefaultAccountSet: Now takes (accountId, Result) and updates local state
src/Client/Components/Settings/Types.fs:- Changed
DefaultBudgetSetto includeYnabBudgetIdfor local updates - Changed
DefaultAccountSetto includeYnabAccountIdfor local updates
- Changed
src/Client/Components/Dashboard/View.fs:- Added refresh button in page header (dispatches LoadRecentSessions, LoadCurrentSession, LoadSettings)
src/Client/Components/Rules/View.fs:- Added refresh button in page header (dispatches LoadRules)
src/Client/Components/SyncFlow/View.fs:- Added refresh button in page header (dispatches LoadTransactions, LoadCurrentSession)
- Optimized stats calculation: Replaced 4x List.filter with single List.fold
src/Client/Components/Settings/View.fs:- Added refresh button in page header (dispatches LoadSettings)
src/Client/DesignSystem/Button.fs:- Added
Title: string optionfield toButtonPropsfor tooltip/accessibility - Updated
defaultPropswithTitle = None - Updated
viewfunction to renderprop.titlewhen Title is set
- Added
Rationale: The MVU (Model-View-Update) pattern with Virtual DOM should only re-render changed parts of the UI. Reloading entire lists from the server after every mutation causes:
- Visual flickering as the list briefly shows loading state
- Unnecessary network requests
- Poor perceived performance
Since the API already returns the updated object(s) after mutations, we can use these to update the local state directly.
Outcomes:
- Build: ✅
- Tests: 215/215 passed
- No more flickering when categorizing, skipping, or editing transactions
- No more flickering when saving settings
- Manual refresh available via icon button on each page
2025-12-07 11:30 - Documentation: Test Isolation Patterns for Future Projects
What I did: Updated project documentation and Claude Code skills to capture the knowledge about F# test isolation patterns, particularly around environment variables and lazy loading for database configuration.
Files Modified:
.claude/skills/fsharp-persistence/SKILL.md:- Replaced simple static connection string with lazy-loading pattern
- Added “Why Lazy Loading?” section explaining F# module initialization
- Added “Connection Management for Tests” section about In-Memory SQLite lifecycle
- Updated Best Practices with test isolation requirements
- Updated Verification Checklist
.claude/skills/fsharp-tests/SKILL.md:- Updated Program.fs section with
USE_MEMORY_DBenvironment variable setup - Added “Test Files Using Persistence” section with
dobeforeopenpattern - Added “Testing Persistence (In-Memory SQLite)” section with complete example
- Added “F# Module Initialization Gotchas” section
- Updated Best Practices with test isolation dos/don’ts
- Updated Verification Checklist
- Updated Program.fs section with
docs/05-PERSISTENCE.md:- Replaced simple connection string pattern with full lazy-loading example
- Added “Why Lazy Loading?” section
- Added “Connection Management for In-Memory SQLite” section
- Added Best Practices items 11-13 for test isolation
docs/06-TESTING.md:- Completely rewrote “Testing Persistence (with In-Memory DB)” section
- Added “Setting Up Test Mode” with Main.fs and test file examples
- Added “Why This Pattern Works” explanation
- Added “Common Pitfalls” section with wrong/correct examples
- Added “Verifying Test Isolation” with verification commands
- Added Best Practices items 11-13 for test isolation
Rationale: The experience with the 236 test rules in production taught valuable lessons about F# module initialization order and test isolation. This knowledge should be preserved for future projects so the same mistakes aren’t repeated.
Key Lessons Documented:
- F# modules initialize by dependency graph, not by
openorder lazyis required for configuration that tests need to override- In-Memory SQLite needs a shared connection kept alive
dobeforeopenin test files allows setting env vars before module init
Outcomes:
- Build: ✅
- All documentation updated with complete patterns
- Future projects will have clear guidance on test isolation
2025-12-07 - Fix: Rules list no longer reloads on every change
What I did: Fixed a performance and UX issue where toggling a rule’s enabled state, saving, or deleting a rule caused the entire Rules list to reload from the server.
Problem:
- Every change to a single rule triggered
Cmd.ofMsg LoadRules - This caused unnecessary network traffic
- Visible UI flickering due to RemoteData going to Loading state
- Poor user experience
Root Cause:
The MVU update handlers for RuleToggled, RuleSaved, and RuleDeleted all dispatched LoadRules instead of updating the local state with the returned data.
Files Modified:
src/Client/Components/Rules/Types.fs:- Changed
RuleDeleted of Result<unit, RulesError>toRuleDeleted of Result<RuleId, RulesError>to pass the deleted rule’s ID
- Changed
src/Client/Components/Rules/State.fs:RuleToggled: Now uses the returnedupdatedRuleto update the local state viaList.mapRuleSaved: Now uses the returnedsavedRule- adds to list for new rules, replaces for updatesDeleteRule: Modified to passruleIdthrough the success resultRuleDeleted: Now filters out the deleted rule locally viaList.filter
src/Client/Components/Rules/View.fs:- Added
prop.keyto rule list items for efficient React reconciliation
- Added
Rationale: The API already returns the updated/created Rule, so reloading the entire list was wasteful. Local state updates are:
- Faster (no network round-trip)
- Smoother (no UI flickering)
- More efficient (single item update vs full list fetch)
Outcomes:
- Build: ✅
- Tests: 215/215 passed (6 skipped integration tests)
- Toggling a rule no longer causes list reload
- Creating/updating a rule updates in-place
- Deleting a rule removes it immediately from UI
2025-12-07 - Fix: Tests no longer write to Production Database
What I did: Fixed a critical bug where persistence tests were writing test data (Rules, Sessions, Transactions) to the production database. Implemented in-memory SQLite support for tests.
Problem:
PersistenceTypeConversionTests.fswas inserting test Rules into the production SQLite database- Every test run added 6 new “Test Rule” entries
- Accumulated 236 duplicate test rules in production
Files Modified:
CLAUDE.md- Added anti-pattern: “Tests writing to production database - NEVER write tests that persist data to the production database. Always use in-memory SQLite”src/Server/Persistence.fs:- Added lazy-loading for DB configuration (
dbConfig = lazy (...)) - Added
USE_MEMORY_DBenvironment variable support - When
USE_MEMORY_DB=true, usesData Source=:memory:;Mode=Memory;Cache=Shared - Shared connection for in-memory mode (keeps DB alive across operations)
- Changed all
use conn = getConnection()tolet conn = getConnection()to prevent disposing shared connection
- Added lazy-loading for DB configuration (
src/Tests/PersistenceTypeConversionTests.fs:- Added
Environment.SetEnvironmentVariable("USE_MEMORY_DB", "true")at module initialization (beforeopen Persistence)
- Added
src/Tests/Main.fs:- Also sets
USE_MEMORY_DB=truefor safety
- Also sets
Files Deleted:
src/Tests/TestSetup.fs- Removed (approach didn’t work due to F# module initialization order)
Rationale: Tests must NEVER write to production databases. The in-memory SQLite approach provides complete isolation - each test run starts fresh, and no data persists after tests complete.
Outcomes:
- Build: ✅
- Tests: 215/215 passed (6 skipped integration tests)
- Production DB: ✅ Verified unchanged after test runs