diff --git a/OPERATIONS.txt b/OPERATIONS.txt index 09240a7..5ffe40d 100644 --- a/OPERATIONS.txt +++ b/OPERATIONS.txt @@ -172,7 +172,9 @@ response records high, medium or low confidence. Imports never auto-apply a low-confidence category: the row keeps the kind-specific unclassified category with merchant and confidence recorded. Analyse previews show the low-confidence suggestion unselected for review. Transactions exposes a -Needs review filter for low-confidence or fallback rows. A model-proposed +Needs review filter for low-confidence or fallback rows, and a +classification filter over how each row was classified: manually, by AI, +by a merchant rule, by transfer matching, or not at all. A model-proposed merchant name is dropped (the row keeps its validated category and tags) when it is identifier-shaped, longer than 100 characters, or contains control or format code points such as bidirectional overrides and diff --git a/README.md b/README.md index 2377b85..1f9ea86 100644 --- a/README.md +++ b/README.md @@ -423,7 +423,7 @@ Bank synchronization and recognized N26, ING, and Kontist CSV imports do **not** **Classify newly imported transactions with AI** under **Classification preferences** controls whether importing contacts the provider at all. It covers CSV imports and bank synchronization, is on by default, and is stored as `classify_on_import` in `config.toml`. With it off, no import makes a provider request: enabled merchant rules still classify, and everything else arrives unclassified and editable without a failure that would suggest the provider was unreachable. **AI classification → Analyse** still works on demand, so you can review a batch deliberately instead of on every import. -AI classification sends only identifier-redacted text: the transaction's own IDs, account identifiers and labels, payment references, labeled or IBAN-attached BICs, and configured private names are removed, while merchant and counterparty text remains available for recognition. Classification responses carry `high`, `medium`, or `low` confidence. Imports never auto-apply a low-confidence category — the row stays on the kind-appropriate unclassified category with the merchant link and confidence recorded — while **Analyse** previews show the low-confidence suggestion unselected for review, and **Transactions → Needs review** lists both. +AI classification sends only identifier-redacted text: the transaction's own IDs, account identifiers and labels, payment references, labeled or IBAN-attached BICs, and configured private names are removed, while merchant and counterparty text remains available for recognition. Classification responses carry `high`, `medium`, or `low` confidence. Imports never auto-apply a low-confidence category — the row stays on the kind-appropriate unclassified category with the merchant link and confidence recorded — while **Analyse** previews show the low-confidence suggestion unselected for review, and **Transactions → Needs review** lists both. Transactions also filters by classification status — manual, AI, merchant rule, transfer match, or unclassified — matching the labels its Source column shows. Classification choices retain their names, paths, hints, and aliases, but use short request-local references such as `c1`, `m1`, and `t1` instead of long database IDs. Merchant defaults and applicable classification history use the same references. Every eligible category, merchant, and tag remains available; responses are mapped back to canonical IDs and validated locally. diff --git a/web/src/Transactions.tsx b/web/src/Transactions.tsx index 765108d..612ce9b 100644 --- a/web/src/Transactions.tsx +++ b/web/src/Transactions.tsx @@ -41,6 +41,26 @@ const EVENTS: Record = { corporate_action: "Corporate action", position_transfer: "Position transfer", }; +// Human labels for classification provenance sources; the filter options and +// the Source column speak the same language. Both fallback shapes — the +// import-time "unclassified" error record and the plain sign-based +// "fallback" — read as Unclassified. +const CLASSIFICATIONS: Record = { + manual: "Manual", + openrouter: "AI", + rule: "Merchant rule", + transfer_match: "Transfer match", + fallback: "Unclassified", + unclassified: "Unclassified", +}; +const CLASSIFICATION_FILTERS: [string, string][] = [ + ["manual", "Manual"], + ["openrouter", "AI"], + ["rule", "Merchant rule"], + ["transfer_match", "Transfer match"], + ["unclassified", "Unclassified"], +]; + // A corporate action or a position transfer moves shares between holdings and // settles no money at all, so its zero amount is a fact and not a gap. function positionOnly(investment?: Investment): boolean { @@ -92,6 +112,7 @@ export function Transactions({ }) { const [query, setQuery] = useState(""); const [needsReview, setNeedsReview] = useState(false); + const [status, setStatus] = useState(""); const [editing, setEditing] = useState(null); const [page, setPage] = useState(0); const filtered = useMemo(() => { @@ -122,6 +143,12 @@ export function Transactions({ (e.kind === "income" ? "cat_income_unclassified" : "cat_expenses_unclassified")) && + (!status || + (status === "unclassified" + ? ["fallback", "unclassified", ""].includes( + e.classification.source || "", + ) + : e.classification.source === status)) && (!filter.tag_id || e.tag_ids.includes(filter.tag_id)) && (!filter.merchant_id || e.merchant_id === filter.merchant_id) && (!query || @@ -134,7 +161,7 @@ export function Transactions({ b.facts.booking_date.localeCompare(a.facts.booking_date) || a.facts.id.localeCompare(b.facts.id), ); - }, [data, filter, query, needsReview]); + }, [data, filter, query, needsReview, status]); const currentPage = Math.min( page, Math.max(0, Math.ceil(filtered.length / 40) - 1), @@ -181,6 +208,22 @@ export function Transactions({ /> Needs review + Click a transaction to edit @@ -284,7 +327,8 @@ export function Transactions({ - {e.classification.source} + {CLASSIFICATIONS[e.classification.source] ?? + e.classification.source} {e.classification.error && ( diff --git a/web/src/styles.css b/web/src/styles.css index 69f092d..35f827c 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -744,6 +744,16 @@ main { .search input::placeholder { color: #9aa6b3; } +.toolbar-select { + height: 35px; + font-size: 11px; + padding: 0 9px; + border: 1px solid #dbe2ea; + border-radius: 5px; + background: #fff; + color: #46596a; + flex-shrink: 0; +} .table-scroll { overflow-x: auto; }