formatDate was parsing UTC timestamps in local time, shifting dates
back a day in western timezones. Strip the time portion before parsing
so dates display correctly.
Revert interest calculation from daily accrual back to standard monthly
compounding (rate/12) with per-payment rounding. This matches how
mortgage servicers calculate interest and produces correct amortization
schedules.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The change tracker-based approaches (RemoveRange, Clear) both caused
DbUpdateConcurrencyException when deleting amortization entries.
Use ExecuteDeleteAsync to delete directly in the database, bypassing
the change tracker entirely.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the term months field with a maturity date picker - users know
when their loan ends, not the month count. Compute termMonths from the
two dates. Rename "Origination Date" to "First Payment Date" and adjust
the amortization schedule so payment #1 falls on the entered date.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
RemoveRange + collection replacement caused EF Core to attempt a
double-delete on amortization entries. Use Clear() on the tracked
collection instead, letting EF handle orphan deletion in a single pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The amortization schedule and payoff calculator were using rate/12 for
monthly interest, which assumes 30.42 days per period and overestimates
interest. Switch to daily accrual (rate/365 * actual days) to match how
lenders actually compute interest between payment dates.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The current account balance is not the origination balance, so defaulting
it into the Original Balance field is misleading. Leave it at 0 so users
enter the actual loan origination amount.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Parse date string components directly and use Date.UTC() to avoid
local timezone interpretation that shifts dates by a day in western
timezones when using new Date() with date-only strings.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fetch loan data (mortgage/student) from Plaid's Liabilities API during sync
and auto-create LoanDetail records. For accounts without liabilities data,
pre-populate the setup form with balance, interest rate, and smart term
defaults. Also fixes maturity date computation in the frontend form.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use clean payee name instead of raw bank string when a payee match is found
during import. When payees are created/renamed or aliases added, automatically
update or link matching unlinked transactions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds a Status select to the add/edit transaction dialog that defaults to
the current value when editing, so cleared transactions stay cleared.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Settings > AI tab now includes context size slider (2048–131072) with VRAM
estimate and a chatbot name field. ChatService reads both dynamically instead
of hardcoding num_ctx=16384 and "Purrse AI". Chat panel FAB and empty state
use mdi-cat icon to match the rest of the app.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When the AI updates a category (or any tool result), the transactions
page now automatically reloads. ChatPanel triggers a refresh counter
in the usePageContext composable when the response includes a tool
result, and TransactionsView watches that counter to reload data.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Ollama defaults to 2048 tokens which is far too small for page context
data + 15 tool definitions + conversation history. Added num_ctx=16384
via the options field on all Ollama requests. With Qwen at ~4GB and
7GB VRAM free this fits comfortably.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The AI was claiming to update categories without actually calling the
update tool — it answered from page context as if the change was made.
Restructured the system prompt to clearly separate read-only questions
(can answer from page data) from mutations (MUST call tools). The AI
is now told to never claim a change was made without calling the
appropriate tool.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When the model returns empty content with page context present, the
15 tool definitions likely overwhelm the model's attention. Now on
empty response, the request is retried with tools stripped so the
model focuses on answering from the page data. Also added diagnostic
logging for page context receipt and empty response details.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The null-coalescing operator only caught null, not empty/whitespace
responses. When Ollama returns empty content (common when the model
hesitates between tool calling and text), the empty string was saved
as the assistant message. Now uses IsNullOrWhiteSpace to catch both.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The nudge forced the AI to call a tool even when the answer was
already in the page context (e.g. Sarku transactions visible on the
Transactions page). This caused the model to pick an unrelated tool.
Now the nudge also checks for page context — if the current page has
data attached, the model is allowed to answer from it directly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When the AI searched for transactions on a specific date like
"2026-02-11", ParseDateArg produced 2026-02-11T00:00:00Z (midnight).
The query "Date <= midnight" excluded any transaction stored with a
time component later that day. Now date-only end dates are bumped to
23:59:59.999 so the full day is covered. Applied to search_transactions,
get_spending_by_category, and get_income_vs_expense tools.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two issues caused the AI to lose context in multi-turn conversations:
1. Page context was a system message buried before 20 conversation
messages. Now it's injected directly into the user's message so
the model sees the data right next to the question.
2. Prior tool results were replayed with role="tool", which many
Ollama models ignore in conversation history. Now they're replayed
as assistant messages with a [Tool Result] prefix so the model
reliably reads them.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The tool-use nudge fired on every first response without tools, even
when the conversation already had tool results the AI could reference.
This forced the AI to make a new (broader) tool call instead of
interpreting a short follow-up like "2026-02-11" in context of the
prior exchange. Now the nudge only fires when there are no prior tool
results in the conversation. Also added a system prompt rule telling
the AI to interpret ambiguous follow-ups in context.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tool results are rendered as rich tables/charts in the UI, but the
system prompt was telling the AI to list transaction details and
"summarize tool results naturally", causing it to repeat the same
data in text. Updated the prompt to explain that tool results are
already visible and the AI should give a brief insight instead.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The LLM summary for update_transaction_category was missing the
transaction ID, so when the user asked to undo a change the AI had
to guess which record from the page context and often picked the
wrong one. Now the summary includes the exact ID and the old category
name, giving the AI everything it needs to reverse the change.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move the classify-uncategorized endpoint from synchronous to async.
The controller now queues a ClassificationJob to an unbounded channel
and returns 202 Accepted. A new BackgroundService processes jobs and
pushes results (or errors) to the user via SignalR. The frontend
listens for the AiClassificationComplete event and surfaces it in the
notification system.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the standalone /chat page with a bottom-right floating widget
available on every page. The panel sends page context with each message
so the AI knows what the user is viewing. On the Transactions page,
the full visible transaction list (with filters) is serialized into
the context so the AI can answer questions about on-screen entries.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
PayeeName and SearchText filters used Contains() which translates
to case-sensitive LIKE in PostgreSQL. Switch to EF.Functions.ILike
for case-insensitive matching so searching for "frantech" finds
"Buyvm Frantech" and "vultr" finds "Vultr.com".
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Log model name, tool count, raw response preview, and parsed
tool_call count at INFO level. Broaden the retry nudge to trigger
whenever no tools are called on the first attempt, not just when
code is detected.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The 401 interceptor was re-triggering itself when the refresh call
also returned 401, causing repeated requests before eventually
logging out. Add guards to skip refresh for the refresh endpoint
itself and prevent any request from retrying more than once.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Restructure the system prompt as explicit rules to more forcefully
prevent smaller models from generating code. Add a server-side
safety net that strips fenced code blocks from assistant responses
before returning them to the user.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds a lightweight docker:cli sidecar that prunes dangling images
every 24 hours to prevent untagged image buildup from redeployments.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Smaller models sometimes output Python/JS code instead of invoking
tools. Add explicit instruction to never generate code and always
call the provided function tools directly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Integrate 6 new tool chains into the chat service: get_budget,
get_budget_summary, get_loan_details, calculate_loan_payoff,
list_payees, and get_scheduled_transactions. Also raises the
search_transactions page_size cap from 25 to 50 and the upcoming
bills display from 5 to 10.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Three new tools: list_categories shows all categories with type and
parent, create_category adds a new one with optional parent nesting,
delete_category removes non-system categories by name. Frontend renders
category tables, success alerts for creation, and warning alerts for
deletion.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ParseDateArg now applies DateTime.SpecifyKind(Utc) since TryParse
returns Unspecified kind. Also fixed the spending report fallback
start date to use the DateTimeKind.Utc constructor overload.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Conversational AI chat that uses Ollama's native tool-calling API to query
transactions, spending reports, account balances, dashboard summaries, and
update transaction categories through natural language. Includes persistent
conversation history, a full-page chat UI with sidebar, and rich inline
rendering of tool results (tables, alerts, cards).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Full-stack profile section: DisplayName column on User model, profile CRUD
endpoints on AuthController, and complete Settings > Profile tab UI with
profile editing, password change, account info, and account deletion.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Cache SimpleFIN API responses for 60 seconds so multiple accounts in
the same sync cycle reuse a single HTTP call instead of hitting the
API per-account. Pre-load existing transactions by FitId in one batch
query to skip per-duplicate FindAsync calls during memo backfill.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Update duplicate handling during bank sync to populate the memo field
on existing transactions when it's empty. Fix raw SQL table name from
quoted "Transactions" to lowercase "transactions" matching the actual
PostgreSQL schema.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
v-data-table-server re-emits update:options when items/totalCount
change, creating a loop that resets to page 1. Guard against
redundant fetches by skipping when page and pageSize haven't
changed. Remove duplicate loadTransactions from onMounted since
the table's initial update:options event handles the first load.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use the raw Description field as memo so the full bank text
(e.g. "Card purchase BUYVM VPS* FRANTECH 11 MY.FRANTECH.C WY
02-08-2026") is always visible. The cleaned Payee field is still
used for the payee name and matching.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
One-time migration: UPDATE all transactions that have a FitId
(bank-synced) and Status=Uncleared to Status=Cleared. Tracked
in data_migrations table so it only runs once.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Both SimpleFIN and Plaid only return posted transactions (SimpleFIN
filters out pending, Plaid returns settled). These have already
cleared at the bank so they should be marked Cleared on import.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace v-data-table with v-data-table-server so page changes
fetch from the API instead of being limited to the first 50
results. Filters reset to page 1; page/size changes trigger a
new API call with the correct page and pageSize parameters.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Make the entire header clickable to toggle rail mode so users
can expand the drawer after collapsing it
- Center the cat icon in rail mode via scoped CSS
- Add tooltips on all nav items and logout when in rail mode
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Resets LastSyncAt on all linked accounts so the next sync
re-fetches the full 90-day transaction window. Duplicate
detection prevents re-importing existing transactions.
- POST /api/bank-sync/connections/{id}/reset-sync endpoint
- Re-sync button shown only on SimpleFIN connection cards
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Npgsql requires DateTimeKind.Utc for timestamp with time zone
columns. This fixes errors in transaction create/edit, budgets,
reports, loans, scheduled transactions, and file imports.
Changes:
- TransactionService: SpecifyKind on request.Date for create,
update, and transfer
- BudgetService: UTC kind on all new DateTime(year, month, 1)
- ReportService: UTC kind on endOfMonth; rewrite spending-by-
category GroupBy to avoid untranslatable navigation properties
- LoanService: UTC kind on payoff scenario date construction
- ScheduledTransactionService: UTC kind on SemiMonthly date
- ImportService: SpecifyKind on parsed.Date when persisting
- OFX/CSV/QIF parsers: AssumeUniversal + AdjustToUniversal on
all date parsing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
EF Core cannot translate GroupBy with a navigation property
(Category.Name) joined via nullable FK into SQL. Split into two
queries: group by CategoryId server-side, then look up category
names in a separate query.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>