Let imports opt out of AI classification

Classification preferences gains "Classify newly imported transactions with
AI", stored as classify_on_import in config.toml and on by default, so existing
configurations keep their behaviour. It covers CSV imports and bank
synchronization alike.

With it off, no import path contacts the provider: classification falls to the
new provider-free rules path, where an opted-in merchant rule still applies its
category and tags, an alias match still attaches its merchant, and everything
else arrives on the editable fallback without a provenance error that would
suggest the provider had failed. Analyse remains available on demand.
This commit is contained in:
Lars Nolden
2026-09-11 18:10:05 +02:00
parent dc767799bc
commit dece0d5b79
10 changed files with 217 additions and 30 deletions
+10
View File
@@ -630,6 +630,7 @@ function ImportForm({
{prepared && (
<ImportReview
prepared={prepared}
state={state}
acceptState={acceptState}
onError={onError}
close={() => {
@@ -647,16 +648,20 @@ function ImportForm({
// only obvious against real records.
function ImportReview({
prepared,
state,
acceptState,
onError,
close,
}: {
prepared: PreparedImport;
state: State;
acceptState: (state: State, message?: string) => void;
onError: (error: string) => void;
close: () => void;
}) {
const [busy, setBusy] = useState(false);
const classifying =
state.settings.classify_on_import && state.status.ai_configured;
const discard = () => {
// Free the server's prepared statement; an expiring one is harmless.
void request("/api/import/cancel", { id: prepared.id }).catch(() => {});
@@ -744,6 +749,11 @@ function ImportReview({
{prepared.samples.length} of {prepared.records} records, including the
largest amount and both directions of money.
</p>
<p className="muted small">
{classifying
? "After importing, these transactions are classified with AI. Turn that off under Settings → Classification preferences."
: "AI classification of imports is off, so these transactions arrive unclassified and editable. Enabled merchant rules still apply."}
</p>
</div>
<div className="form-actions">
<button
+22 -1
View File
@@ -16,6 +16,9 @@ export function Settings({ state, mutate }: { state: State; mutate: Mutate }) {
const [includeAmount, setIncludeAmount] = useState(
state.settings.include_amount,
);
const [classifyOnImport, setClassifyOnImport] = useState(
state.settings.classify_on_import,
);
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
const [rebuild, setRebuild] = useState(false);
@@ -334,7 +337,11 @@ export function Settings({ state, mutate }: { state: State; mutate: Mutate }) {
try {
await mutate(
"/api/settings",
{ model: model.trim(), include_amount: includeAmount },
{
model: model.trim(),
include_amount: includeAmount,
classify_on_import: classifyOnImport,
},
"Classification preferences saved",
);
} catch (err) {
@@ -366,6 +373,20 @@ export function Settings({ state, mutate }: { state: State; mutate: Mutate }) {
Disabled by default for privacy. Enabling this shares the amount
with the configured AI provider to help classification.
</p>
<label className="checkbox">
<input
type="checkbox"
checked={classifyOnImport}
onChange={(e) => setClassifyOnImport(e.target.checked)}
/>
Classify newly imported transactions with AI
</label>
<p className="muted small">
Applies to CSV imports and bank synchronization. When off, no
import contacts your AI provider: enabled merchant rules still
classify, and everything else arrives unclassified and editable.
AI classification Analyse is unaffected.
</p>
<button
className="button primary"
disabled={busy || credentialsBusy}
+5 -1
View File
@@ -93,7 +93,11 @@ export interface State {
banking_configured: boolean;
ai_configured: boolean;
};
settings: { model: string; include_amount: boolean };
settings: {
model: string;
include_amount: boolean;
classify_on_import: boolean;
};
sessions: { session_id: string; valid_until: string; accounts: Account[] }[];
}
export interface Total {
+1 -1
View File
@@ -382,7 +382,7 @@ function App() {
)}
{page === "settings" && (
<Settings
key={`${state.settings.model}-${state.settings.include_amount}`}
key={`${state.settings.model}-${state.settings.include_amount}-${state.settings.classify_on_import}`}
state={state}
mutate={mutate}
/>