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:

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:


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:

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:

Outcomes:


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:

Outcomes:


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:

Files Added:

Files Modified:

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:


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:

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:

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:

Outcomes:


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:

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:

Outcomes:


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:

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:

Outcomes:


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:

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:


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:

Files Modified:

New Component: ComboBox Unlike SearchableSelect (which only allows selection from a list), ComboBox:

Implementation Details:

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:


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:

Files Modified:

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:

Outcomes:


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:

Files Modified:

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:


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:

  1. Import ID format mismatch: BB: vs BUDGETBUDDY:
  2. Tests were tautological - they tested the wrong format against the wrong format
  3. Dangerous fallback in Api.fs: if ID mapping failed, ALL transactions were marked as duplicates

Files Added:

Files Modified:

Files Deleted:

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:


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:

Files Modified:

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:

  1. NavigateTo page triggers Cmd.navigate (changes URL)
  2. URL change fires router.onUrlChanged
  3. UrlChanged segments updates CurrentPage and triggers page-specific load commands

Features:

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:


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:

Files Modified:

Rationale: Users can rapidly change categories while reviewing transactions, which caused unnecessary API calls to the server. The debouncing implementation:

Design Decisions:

Outcomes:


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:

Files Modified:

Rationale: Page headers were implemented inline in each view with slight variations. The PageHeader component:

Design Decisions:

Outcomes:


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:

Files Added:

Files Modified:

Rationale: The RemoteData type is used throughout the frontend for async data fetching. Helper functions provide:

Design Decisions:

Outcomes:


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:

Rationale: The Dashboard Sync button used inline Tailwind styles with custom shadow values that weren’t reusable. Moving these to the Design System ensures:

Outcomes:


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:

Files Modified:

Rationale: The Frontend Architecture Review identified inconsistent error handling across the application. This component standardizes error displays with:

Outcomes:


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:

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:

Outcomes:


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:

Files Modified:

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:

Outcomes:

Notes:


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:

Verified Existing Implementations:

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:


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:

Design Changes:

  1. Before: Dashboard with 3 stats cards + quick action card + 5-item history list
  2. After: Single centered “Start Sync” button with glow effect, last sync info below (date + transaction count)
  3. 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:


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:

Files Modified:

UX Changes:

  1. Disabled buttons: Now visually distinct with 50% opacity and cursor-not-allowed
  2. Validation message: Orange text under button shows “Bitte ausfüllen: Field1, Field2” when fields are missing
  3. Required field markers: Red asterisk (*) on all required fields (consistent across all forms)
  4. Consistent pattern: Form.submitButton used 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:


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:

Files Modified:

UX Flow:

  1. User saves Comdirect credentials and Account-ID
  2. “Test Connection” button appears (only if credentials saved)
  3. Clicking starts TAN authentication → orange waiting UI
  4. User confirms Push-TAN in Comdirect app
  5. Clicking “I’ve Confirmed the TAN” completes validation
  6. 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:


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:

Solution:

  1. Generate a stable encryption key: openssl rand -base64 32
  2. Add to .env: BUDGETBUDDY_ENCRYPTION_KEY=<your-key>
  3. 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:


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:

Files Modified:

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:


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:

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:


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:

Implementation Details:

Rationale: Users with many transactions needed a way to quickly skip/unskip batches instead of clicking individual skip buttons.

Outcomes:


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:

Link Formats:

Rationale: Users frequently need to match Amazon transactions to specific orders. The deep link eliminates the need to search through order history manually.

Outcomes:


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:

  1. Falsches project.yml Format: Die Konfiguration verwendete language_servers: fsharp: command: ... statt dem erwarteten languages: - fsharp
  2. Fehlende Solution-Datei: fsautocomplete benötigt eine .sln-Datei, die im Projekt fehlte

Files Added:

Files Modified:

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:

Outcomes:


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:

Files Modified:

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:


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:

  1. View Issue: No dedicated view for FetchingTransactions status - only a generic loading view was shown
  2. State Issue: The confirmTan API call runs synchronously (TAN confirm + fetch + rules + duplicate detection), so the client never saw the intermediate FetchingTransactions status

Solution:

Files 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:


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:

  1. BudgetBuddy’s pre-import detection (Reference/ImportId/Fuzzy matching BEFORE sending to YNAB)
  2. YNAB’s rejection (when YNAB rejects during import due to duplicate import_id)

Key Changes:

  1. Domain Types Extended (src/Shared/Domain.fs)
    • Added DuplicateDetectionDetails record with diagnostic fields:
      • TransactionReference, ReferenceFoundInYnab, ImportIdFoundInYnab
      • FuzzyMatchDate, FuzzyMatchAmount, FuzzyMatchPayee
    • Updated DuplicateStatus to include details in all variants
    • Added YnabImportStatus type (NotAttempted YnabImported RejectedByYnab)
    • Added YnabImportStatus field to SyncTransaction
  2. Detection Logic Updated (src/Server/DuplicateDetection.fs)
    • detectDuplicate now returns full diagnostic details
    • All three checks (Reference, ImportId, Fuzzy) are run and results captured
  3. API Updated (src/Server/Api.fs)
    • importToYnab now sets YnabImportStatus on each transaction after YNAB response
    • forceImportDuplicates also sets YnabImported status
  4. 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 ynabRejected for transactions rejected during import

Files Added:

Files Modified:

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:

Outcomes:


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:

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:

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:


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:

  1. Renamed filter types for clarity:
    • ReadyTransactionsCategorizedTransactions
    • PendingTransactionsUncategorizedTransactions
    • DuplicateTransactionsConfirmedDuplicates
  2. Fixed count logic:
    • Categorized: Has CategoryId AND not Skipped/Imported
    • Uncategorized: No CategoryId AND not Skipped/Imported
    • ConfirmedDuplicates: Only ConfirmedDuplicate status (not PossibleDuplicate)
  3. Updated Stats labels: “Ready” → “Categorized”, “Pending” → “Uncategorized”
  4. Improved Duplicate Banner: Now only shows ConfirmedDuplicates with better German explanation
  5. Removed redundant “Transaktionen ohne Kategorie” warning banner (info now in Uncategorized stat)

Files Modified:

Outcomes:


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:

  1. Added TransactionFilter discriminated union type with 5 filter options:
    • AllTransactions, ReadyTransactions, PendingTransactions, SkippedTransactions, DuplicateTransactions
  2. Added ActiveFilter field to the SyncFlow Model
  3. Added SetFilter message to Msg type
  4. Extended Stats component with OnClick and IsActive props for interactivity
  5. Added filterTransactions helper function to filter by status
  6. Stats cards now show active state with teal ring highlight
  7. Filter indicator shows “X of Y transactions” with “Show all” link
  8. Empty state when filter matches no transactions

Files Modified:

Outcomes:


2025-12-09 21:00 - UI: Breitere Selectbox & elegante Memo-Anzeige

What I did:

  1. Category-Selectbox von w-52 (208px) auf w-72 (288px) verbreitert
  2. Memo-Row neu gestaltet mit Glassmorphism-Stil passend zum Design-System

Memo-Row Redesign:

Files Modified:

Outcomes:


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:

Implementierung:

  1. Neue Hilfsfunktion categoryText: Sucht Kategorienamen aus vorberechneten Options
  2. Bedingte Render-Logik: if tx.Status = Skipped then Text else Selectbox
  3. Angewendet auf Mobile und Desktop Layout

Files Modified:

Outcomes:


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:

Lösung Phase 1 (Category Options vorberechnen):

Lösung Phase 2 (Optimistic UI):

Files Modified:

Performance-Ergebnis: | Metrik | Vorher | Nachher | Verbesserung | |——–|——–|———|————–| | Dropdown öffnen | 15.700ms | 18ms | 872x |

Outcomes:


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:

Lösung:

  1. Aktionen nach vorne verschoben: Jetzt zwischen Status-Indikatoren und Kategorie-Dropdown
  2. Feste Container-Breite: w-16 für Aktions-Container → stabile Layouts
  3. Platzhalter: Wenn Create Rule Button nicht aktiv, wird ein unsichtbarer Platzhalter gerendert
  4. Link-Styling: Teal-Farbe + External-Link-Icon für erkennbare Links

Neues Layout (Desktop):

[▶] [●] [⚠] | [Skip][Rule] | [Category ▾] | [Payee 🔗] | [Date] | [Amount]

Files Modified:

Outcomes:


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:

Files Added:

Files Modified:

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:

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:


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:

  1. Betrag-Formatierung: Fable transpiliert :F2 zu %P(F2) statt .toFixed(2) - jetzt explizites Rounding
  2. Status-Farben: Skipped-Transaktionen jetzt immer grau (statt rot bei Duplicates)
  3. NeedsAttention gleiche Farbe: Amazon/PayPal Transaktionen haben jetzt dieselbe Farbe wie andere Uncategorized
  4. Mobile Actions: Buttons auf Mobile immer sichtbar (nicht hover-only)
  5. Größere Dropdown-Options: Kategorie-Auswahl mit größerem Touch-Target
  6. Expand-Feature: Chevron-Icon zum Anzeigen von Memo-Text

Files Modified:

Layout-Änderung Desktop:

[▶/▼] [●] [Kategorie-Dropdown] [Payee...] [Datum] [Betrag] [Actions]

Prioritäten für Farben:

  1. Skipped → immer grau (opacity-50)
  2. ConfirmedDuplicate → rot (nur wenn nicht skipped)
  3. PossibleDuplicate → orange pulsierend (nur wenn nicht skipped)
  4. Pending/NeedsAttention → orange (gleiche Farbe!)
  5. AutoCategorized → teal
  6. ManualCategorized/Imported → grün

Outcomes:


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:

Design-Änderungen:

Layout-Struktur:

Desktop: [●] [Category-Dropdown] [Payee...] [Date] [Amount] [Actions]
Mobile:  Line 1: [●] [Category-Dropdown] [Amount]
         Line 2: [Payee ...] [Date] [Actions]

Touch-Optimierung:

Outcomes:


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:

Technische Details:

Outcomes:


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:

Files Modified:

Technische Details:

Outcomes:


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:

Files Modified:

Technische Details:

Bug-Fix: Scroll-Verhalten in Modals:

Outcomes:


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:

Lösung:

Files Modified:

Technische Details:

Outcomes:


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:

Technische Details:

Outcomes:


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:

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:

Outcomes:


2025-12-07 19:50 - Increased Memo Limit to 300 + Whitespace Compression

What I did:

  1. Increased memo character limit from 200 to 300 (testing if YNAB accepts it)
  2. Added whitespace compression: multiple spaces/tabs/newlines become single space

Why:

Files Modified:

Outcomes:


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: " at the end of the memo.

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: " at the end of YNAB memos to match transactions.

The Fix:

  1. Created new buildMemoWithReference function that:
    • Appends “, Ref: " to the memo
    • If total > 200 chars: truncates from the BEGINNING (preserving the reference)
    • Format when truncated: “…, Ref: "
  2. Kept simple truncateSplitMemo for split transaction memos (they don’t need a reference since the parent transaction has one)

Files Modified:

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:


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:

  1. All transactions are now imported as “uncleared” instead of “cleared”
  2. Transactions without categories can now be imported (appear in YNAB’s Uncategorized view)

Why:

Files Modified:

UI Changes:

Outcomes:


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:

Files Modified:

Technical Notes:

Test Summary:

Outcomes:


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:

  1. User clicks “I’ve Confirmed” → ConfirmTan message sent
  2. Backend receives request, immediately changes session status to FetchingTransactions
  3. Button remains active (no loading state)
  4. User clicks again → second ConfirmTan sent
  5. Backend validates session is in AwaitingTan state, but it’s already FetchingTransactions → 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:

Outcomes:


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:

  1. User clicks “Import to YNAB”
  2. YNAB responds with duplicates
  3. Toast shows “Imported X transaction(s). Y already exist in YNAB.”
  4. Button appears: “Re-import Y Duplicate(s)”
  5. User clicks → transactions are sent with new UUIDs
  6. Success!

Files Modified:

Outcomes:

Final Cleanup (17:45):


2025-12-07 - Fix YNAB False Success Reports + Duplicate Handling

What I did: Fixed two related bugs:

  1. The app reported successful transaction transfer even when YNAB rejected them as duplicates
  2. 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:

  1. New type TransactionCreateResult in YnabClient.fs:
    • CreatedCount: int - actual number of created transactions
    • DuplicateImportIds: string list - import IDs that were rejected
  2. Updated Api.fs import logic:
    • Parse duplicate_import_ids from YNAB response
    • Only mark transactions as Imported if they weren’t in the duplicates list
    • Duplicate transactions keep their original status (not marked as imported)
  3. Added logging to see YNAB request/response for debugging

Files Modified:

Tests Added:

Outcomes:


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:

  1. Stale reference bug in completeSession()
  2. JSON encoding bug using Encode.int64 instead of Encode.int

Neither would have regressed if the original code had proper tests.

Files Modified:

Key Points Added:

  1. Every bug fix MUST include a regression test
  2. Write failing test FIRST, then fix
  3. Test should include comment explaining what bug it prevents
  4. 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:

  1. Changed Amount field from int64 to int in YnabTransactionRequest and YnabSubtransactionRequest types
  2. Changed Encode.int64 to Encode.int in encoders
  3. Changed int64 to int in amount conversion code
  4. Added regression tests to verify amounts are serialized as JSON numbers

Files Modified:

Outcomes:

Test Coverage Gap Identified: There were no tests verifying the JSON format sent to YNAB. The bug existed because:

  1. Tests only verified data transformations, not the actual HTTP request body format
  2. 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:

  1. It matched on currentSession.Value and captured state
  2. Called updateSessionCounts() which updated currentSession.Value with correct counts
  3. Then used the OLD state.Session (with 0 counts) to create the completed session
  4. 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:

Outcomes:


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:

  1. Defined proper F# record types YnabTransactionRequest and YnabSubtransactionRequest
  2. Created manual encoders encodeTransaction and encodeSubtransaction using Encode.object
  3. Replaced Encode.Auto.generateEncoder() with Encode.list (ynabTransactions |> List.map encodeTransaction)

Files Modified:

Outcomes:


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:

  1. LoadCategories in SyncFlow/State.fs did nothing (Cmd.none) - it was marked as “simplified for now”
  2. The parent Client/State.fs intercepted LoadCategories but only loaded categories if Settings were already loaded (race condition)
  3. If Settings weren’t loaded yet when navigating to SyncFlow, categories never loaded

Solution:

  1. Made LoadCategories in SyncFlow self-sufficient - it now fetches settings first to get the budget ID, then loads categories from YNAB API
  2. Removed the parent interception - LoadCategories is now passed through to the child component

Files Modified:

Outcomes:


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:

  1. Checkbox selection (Select All/None)
  2. 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:

  1. Removed selection UI entirely - no more checkboxes, Select All/None buttons, or selection count badge
  2. Added Unskip functionality - skipped transactions can now be restored
  3. Auto-skip confirmed duplicates - transactions with ConfirmedDuplicate status are automatically skipped during sync
  4. Simplified import logic - all non-skipped transactions with categories are imported

Files Added:

Files Modified:

Files Deleted:

Rationale:

New Import Logic:

Outcomes:


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:

Files Modified:

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:


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:

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:

  1. Visual flickering as the list briefly shows loading state
  2. Unnecessary network requests
  3. 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:


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:

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:

  1. F# modules initialize by dependency graph, not by open order
  2. lazy is required for configuration that tests need to override
  3. In-Memory SQLite needs a shared connection kept alive
  4. do before open in test files allows setting env vars before module init

Outcomes:


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:

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:

Rationale: The API already returns the updated/created Rule, so reloading the entire list was wasteful. Local state updates are:

Outcomes:


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:

Files Modified:

Files Deleted:

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: