--- a//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/services/budget_imports.py
+++ b//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/services/budget_imports.py
@@ -804,6 +804,189 @@
     return {"confirmed_count": confirmed, "skipped_count": skipped}

+def review_candidate_summary(conn: Connection) -> dict[str, int]:
+    rows = conn.execute("SELECT status, classification, requires_review, duplicate_of_transaction_id FROM budget_transaction_candidates WHERE status NOT IN ('confirmed','ignored','superseded')").fetchall()
+    def count(pred):
+        return sum(1 for r in rows if pred(r))
+    return {
+        "open_total": count(lambda r: r["status"] != "covered_by_migros"),
+        "auto_categorized": count(lambda r: r["status"] in {"pending", "auto_categorized"} and not bool(r["requires_review"])),
+        "review_needed": count(lambda r: bool(r["requires_review"])),
+        "galaxus_digitec": count(lambda r: r["classification"] == "galaxus_review"),
+        "migros_over_50": count(lambda r: r["classification"] == "migros_review_over_threshold"),
+        "covered_by_migros": count(lambda r: r["status"] == "covered_by_migros"),
+        "unclear": count(lambda r: r["classification"] in {"admin_other", "unclear", "legacy_csv_seed", "card_uncategorized"}),
+        "possible_duplicates": count(lambda r: r["status"] in {"duplicate", "duplicate_candidate"} or bool(r["duplicate_of_transaction_id"])),
+    }
+
+
+def preview_batch_confirm_candidates(conn: Connection, *, candidate_ids: list[str], account_id: str) -> dict[str, Any]:
+    rows = []
+    conflicts = []
+    categories: dict[str, int] = {}
+    for cid in candidate_ids:
+        row = conn.execute("SELECT * FROM budget_transaction_candidates WHERE transaction_candidate_id=?", (cid,)).fetchone()
+        if not row:
+            conflicts.append({"candidate_id": cid, "reason": "missing"}); continue
+        if row["status"] in {"confirmed", "ignored", "covered_by_migros", "superseded"}:
+            conflicts.append({"candidate_id": cid, "reason": f"not_confirmable:{row['status']}"}); continue
+        try:
+            preview_transaction_candidate(conn, cid, {"account_id": account_id, "category_id": row["proposed_category_id"]})
+        except Exception as exc:
+            conflicts.append({"candidate_id": cid, "reason": str(exc)}); continue
+        rows.append(row)
+        categories[str(row["proposed_category_name"] or "Review nötig")] = categories.get(str(row["proposed_category_name"] or "Review nötig"), 0) + 1
+    preview_id = new_id("batchprev")
+    record_audit_event(conn, source="vue_dashboard", action="batch_confirm_candidates_preview", entity_type="budget_transaction_candidate", entity_id=preview_id, new_values={"count": len(rows), "conflicts": len(conflicts), "categories": categories}, created_by="user")
+    conn.commit()
+    return {"preview_id": preview_id, "count": len(rows), "categories": categories, "conflicts": conflicts, "requires_explicit_confirm": True}
+
+
+def batch_mark_candidates(conn: Connection, *, candidate_ids: list[str], action: str, account_id: str | None = None, category_id: str | None = None, preview_id: str | None = None, note: str | None = None) -> dict[str, int | str]:
+    counts = {"updated_count": 0, "confirmed_count": 0, "ignored_count": 0, "reopened_count": 0, "transfer_count": 0, "covered_by_migros_count": 0, "skipped_count": 0}
+    if action == "confirm" and not preview_id:
+        raise HTTPException(status_code=422, detail="batch confirm requires preview_id")
+    for cid in candidate_ids:
+        try:
+            if action == "category":
+                if not category_id: raise HTTPException(status_code=422, detail="category_id required")
+                update_transaction_candidate_category(conn, cid, category_id, note or "Batch category assignment"); counts["updated_count"] += 1
+            elif action == "confirm":
+                confirm_transaction_candidate(conn, cid, {"account_id": account_id}); counts["confirmed_count"] += 1
+            elif action == "ignore":
+                ignore_transaction_candidate(conn, cid, note or "Batch ignore"); counts["ignored_count"] += 1
+            elif action == "reopen":
+                reopen_transaction_candidate(conn, cid, note or "Batch reopen"); counts["reopened_count"] += 1
+            elif action == "transfer":
+                mark_candidate_transfer(conn, cid, note or "Batch transfer"); counts["transfer_count"] += 1
+            elif action == "covered_by_migros":
+                ts = now(); row = conn.execute("SELECT status FROM budget_transaction_candidates WHERE transaction_candidate_id=?", (cid,)).fetchone()
+                if not row: raise HTTPException(status_code=404, detail="candidate not found")
+                conn.execute("UPDATE budget_transaction_candidates SET status='covered_by_migros', classification='covered_by_migros', covered_by_source='migros_csv', requires_review=0, review_reason='user_batch_marked_covered_by_migros', updated_at=? WHERE transaction_candidate_id=?", (ts, cid))
+                record_audit_event(conn, source="vue_dashboard", action="mark_covered_by_migros", entity_type="budget_transaction_candidate", entity_id=cid, old_values={"status": row["status"]}, new_values={"status": "covered_by_migros"}, created_by="user")
+                counts["covered_by_migros_count"] += 1
+            else:
+                raise HTTPException(status_code=422, detail="invalid batch action")
+        except Exception:
+            counts["skipped_count"] += 1
+    audit_id = record_audit_event(conn, source="vue_dashboard", action="batch_confirm_candidates" if action == "confirm" else f"batch_{action}_candidates", entity_type="budget_transaction_candidate", entity_id=preview_id or "batch", new_values=counts | {"candidate_count": len(candidate_ids)}, created_by="user")
+    conn.commit()
+    return counts | {"audit_id": audit_id}
+
+
+def confirm_candidate_split(conn: Connection, candidate_id: str, payload: dict[str, Any]) -> dict[str, Any]:
+    row = conn.execute("SELECT * FROM budget_transaction_candidates WHERE transaction_candidate_id=?", (candidate_id,)).fetchone()
+    if not row:
+        raise HTTPException(status_code=404, detail="transaction candidate not found")
+    if row["status"] in {"confirmed", "ignored", "covered_by_migros", "superseded"}:
+        raise HTTPException(status_code=409, detail="candidate is not splittable")
+    splits = payload.get("splits") or []
+    if not payload.get("account_id"):
+        raise HTTPException(status_code=422, detail="account_id required")
+    if not splits:
+        raise HTTPException(status_code=422, detail="splits required")
+    total = sum(Decimal(_decimal_text(s.get("amount_original")) or "0") for s in splits)
+    candidate_amount = Decimal(str(row["amount_original"] or "0"))
+    if total != candidate_amount:
+        raise HTTPException(status_code=422, detail="split sum must match candidate amount")
+    group_id = new_id("splitgrp")
+    ts = now()
+    created_ids = []
+    conn.execute("DELETE FROM budget_candidate_splits WHERE transaction_candidate_id=?", (candidate_id,))
+    for idx, split in enumerate(splits, start=1):
+        amount = _decimal_text(split.get("amount_original"))
+        cat = split.get("category_id")
+        if not cat or not conn.execute("SELECT 1 FROM budget_categories WHERE category_id=? AND is_active=1", (cat,)).fetchone():
+            raise HTTPException(status_code=422, detail="active split category required")
+        result = confirm_budget_transaction(conn, {
+            "account_id": payload["account_id"], "transaction_type": payload.get("transaction_type") or "expense", "transaction_date": row["transaction_date"],
+            "description": f"{row['description']} · Split {idx}", "amount_original": amount, "currency_original": row["currency_original"],
+            "category_id": cat, "source_type": "import_candidate", "source_candidate_id": candidate_id,
+            "notes": f"Split group {group_id}: {split.get('note') or ''}", "tag_names": [split.get("tag")] if split.get("tag") else [],
+        })
+        created_ids.append(result["entity_id"])
+        conn.execute("INSERT INTO budget_candidate_splits(split_id, transaction_candidate_id, category_id, amount_original, notes, tag_name, confirmed_transaction_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", (new_id("bcsplit"), candidate_id, cat, amount, split.get("note"), split.get("tag"), result["entity_id"], ts, ts))
+    conn.execute("UPDATE budget_transaction_candidates SET status='confirmed', confirmed_transaction_id=?, confirmed_at=?, confirmed_by='user', updated_at=? WHERE transaction_candidate_id=?", (group_id, ts, ts, candidate_id))
+    audit_id = record_audit_event(conn, source="vue_dashboard", action="candidate_split_confirmed", entity_type="budget_transaction_candidate", entity_id=candidate_id, new_values={"group_id": group_id, "transaction_ids": created_ids, "split_count": len(created_ids)}, created_by="user")
+    conn.commit()
+    return {"status": "confirmed", "group_id": group_id, "created_transactions": len(created_ids), "transaction_ids": created_ids, "audit_id": audit_id}
+
+
+def create_review_category_for_candidate(conn: Connection, candidate_id: str, payload: dict[str, Any]) -> dict[str, Any]:
+    if not conn.execute("SELECT 1 FROM budget_transaction_candidates WHERE transaction_candidate_id=?", (candidate_id,)).fetchone():
+        raise HTTPException(status_code=404, detail="transaction candidate not found")
+    name = str(payload.get("name") or "").strip()
+    if not name:
+        raise HTTPException(status_code=422, detail="category name required")
+    ts = now(); category_id = new_id("bcat")
+    conn.execute("INSERT INTO budget_categories(category_id, parent_category_id, name, category_type, color, icon, is_active, sort_order, created_at, updated_at) VALUES (?, ?, ?, ?, NULL, NULL, 1, 999, ?, ?)", (category_id, payload.get("parent_category_id") or None, name, payload.get("category_type") or "expense", ts, ts))
+    update_transaction_candidate_category(conn, candidate_id, category_id, "Created from review")
+    audit_id = record_audit_event(conn, source="vue_dashboard", action="review_category_created", entity_type="budget_category", entity_id=category_id, new_values={"name": name, "candidate_id": candidate_id}, created_by="user")
+    conn.commit()
+    return {"status": "created", "category_id": category_id, "audit_id": audit_id}
+
+
+def create_budget_rule(conn: Connection, payload: dict[str, Any]) -> dict[str, Any]:
+    text = str(payload.get("merchant_contains") or "").strip()
+    if not text:
+        raise HTTPException(status_code=422, detail="merchant_contains required")
+    target_status = payload.get("target_status") or "review"
+    if target_status not in {"auto", "review"}:
+        raise HTTPException(status_code=422, detail="invalid target_status")
+    rule_id = new_id("brule"); ts = now()
+    conn.execute("INSERT INTO budget_review_rules(rule_id, merchant_contains, source_type, category_id, target_status, is_active, created_at, updated_at) VALUES (?, ?, ?, ?, ?, 1, ?, ?)", (rule_id, text, payload.get("source_type") or None, payload.get("category_id") or None, target_status, ts, ts))
+    audit_id = record_audit_event(conn, source="vue_dashboard", action="budget_rule_created", entity_type="budget_review_rule", entity_id=rule_id, new_values={"merchant_contains": text, "target_status": target_status}, created_by="user")
+    conn.commit()
+    return {"status": "created", "rule_id": rule_id, "audit_id": audit_id}
+
+
+def list_budget_rules(conn: Connection, *, include_inactive: bool = False) -> list[dict[str, Any]]:
+    where = "" if include_inactive else "WHERE r.is_active=1"
+    rows = conn.execute(f"SELECT r.*, c.name AS category_name FROM budget_review_rules r LEFT JOIN budget_categories c ON c.category_id=r.category_id {where} ORDER BY r.created_at DESC").fetchall()
+    return [row_to_dict(r) | {"is_active": bool(r["is_active"])} for r in rows]
+
+
+def _rule_match_rows(conn: Connection, rule_id: str):
+    rule = conn.execute("SELECT * FROM budget_review_rules WHERE rule_id=?", (rule_id,)).fetchone()
+    if not rule:
+        raise HTTPException(status_code=404, detail="rule not found")
+    clauses = ["status NOT IN ('confirmed','ignored','covered_by_migros','superseded')", "lower(COALESCE(merchant,'') || ' ' || COALESCE(description,'')) LIKE ?"]
+    params: list[Any] = [f"%{str(rule['merchant_contains']).lower()}%"]
+    if rule["source_type"]:
+        clauses.append("source_type=?"); params.append(rule["source_type"])
+    return rule, conn.execute("SELECT * FROM budget_transaction_candidates WHERE " + " AND ".join(clauses), params).fetchall()
+
+
+def test_budget_rule(conn: Connection, rule_id: str) -> dict[str, Any]:
+    rule, rows = _rule_match_rows(conn, rule_id)
+    return {"rule_id": rule_id, "match_count": len(rows), "would_set_category": rule["category_id"], "would_set_status": rule["target_status"]}
+
+
+def apply_budget_rules_to_open_candidates(conn: Connection, rule_id: str) -> dict[str, Any]:
+    rule, rows = _rule_match_rows(conn, rule_id)
+    if not rule["is_active"]:
+        raise HTTPException(status_code=409, detail="rule inactive")
+    status = "auto_categorized" if rule["target_status"] == "auto" else "needs_review"
+    requires_review = 0 if rule["target_status"] == "auto" else 1
+    cname = _category_name(conn, rule["category_id"])
+    ts = now(); updated = 0
+    for row in rows:
+        conn.execute("UPDATE budget_transaction_candidates SET proposed_category_id=?, proposed_category_name=?, status=?, requires_review=?, rule_id=?, rule_name=?, review_reason='rule_mvp_suggestion_only', confidence='0.82', updated_at=? WHERE transaction_candidate_id=?", (rule["category_id"], cname, status, requires_review, rule_id, f"Regel: enthält {rule['merchant_contains']}", ts, row["transaction_candidate_id"]))
+        updated += 1
+    audit_id = record_audit_event(conn, source="vue_dashboard", action="budget_rule_applied_to_candidates", entity_type="budget_review_rule", entity_id=rule_id, new_values={"updated_count": updated, "productive_transaction_count": 0}, created_by="user")
+    conn.commit()
+    return {"updated_count": updated, "audit_id": audit_id, "productive_transaction_count": 0}
+
+
+def deactivate_budget_rule(conn: Connection, rule_id: str) -> dict[str, Any]:
+    row = conn.execute("SELECT is_active FROM budget_review_rules WHERE rule_id=?", (rule_id,)).fetchone()
+    if not row:
+        raise HTTPException(status_code=404, detail="rule not found")
+    ts = now(); conn.execute("UPDATE budget_review_rules SET is_active=0, updated_at=? WHERE rule_id=?", (ts, rule_id))
+    audit_id = record_audit_event(conn, source="vue_dashboard", action="budget_rule_deactivated", entity_type="budget_review_rule", entity_id=rule_id, old_values={"is_active": row["is_active"]}, new_values={"is_active": 0}, created_by="user")
+    conn.commit(); return {"status": "deactivated", "rule_id": rule_id, "audit_id": audit_id}
+
+
 def cleanup_migros_article_candidates(conn: Connection, *, dry_run: bool = True) -> dict[str, int]:
     where = """
         status IN ('pending','needs_review','duplicate')


terminal

unset JARVIS_FINANCE_DB_PATH
pytest tests/unit/test_budget_phase19_review_productivity.py -q


{"output": "E..F..                                                                   [100%]\n==================================== ERRORS ====================================\n______________________ ERROR at setup of test_budget_rule ______________________\nfile /home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/services/budget_imports.py, line 960\n  def test_budget_rule(conn: Connection, rule_id: str) -> dict[str, Any]:\nE       fixture 'conn' not found\n>       available fixtures: _class_scoped_runner, _function_scoped_runner, _module_scoped_runner, _package_scoped_runner, _session_faker, _session_scoped_runner, anyio_backend, anyio_backend_name, anyio_backend_options, cache, capfd, capfdbinary, caplog, capsys, capsysbinary, capteesys, doctest_namespace, event_loop_policy, faker, free_tcp_port, free_tcp_port_factory, free_udp_port, free_udp_port_factory, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, subtests, testrun_uid, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory, unused_tcp_port, unused_tcp_port_factory, unused_udp_port, unused_udp_port_factory, worker_id\n>       use 'pytest --fixtures [testpath]' for help on them.\n\n/home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/services/budget_imports.py:960\n=================================== FAILURES ===================================\n____ test_phase19_split_sum_blocks_and_confirm_creates_linked_transactions _____\n\n    def test_phase19_split_sum_blocks_and_confirm_creates_linked_transactions() -> None:\n        conn = db(); account, food, subs, _health = setup_base(conn)\n        seed_credit_card_candidates_from_rows(conn, [{\"Datum\": \"2026-02-02\", \"Beschreibung\": \"Galaxus Zürich\", \"Betrag\": \"20.00\"}], source_file_label=\"phase19_card.csv\", subscription_category_id=subs)\n        cid = list_transaction_candidates(conn, tab=\"galaxus\")[0][\"transaction_candidate_id\"]\n        with pytest.raises(HTTPException):\n            confirm_candidate_split(conn, cid, {\"account_id\": account, \"splits\": [{\"category_id\": food, \"amount_original\": \"9.00\"}]})\n>       result = confirm_candidate_split(conn, cid, {\"account_id\": account, \"splits\": [\n            {\"category_id\": food, \"amount_original\": \"9.00\", \"note\": \"Teil A\", \"tag\": \"Split\"},\n            {\"category_id\": subs, \"amount_original\": \"11.00\", \"note\": \"Teil B\"},\n        ]})\n\ntests/unit/test_budget_phase19_review_productivity.py:79: \n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \n\nconn = <sqlite3.Connection object at 0x7688afd88c70>\ncandidate_id = 'btxcand_52e0d512e0f77e45cd2d9d74'\npayload = {'account_id': 'bacc_e62e978a17884453', 'splits': [{'amount_original': '9.00', 'category_id': 'bcat_5b266f3ab31947ec', 'note': 'Teil A', 'tag': 'Split'}, {'amount_original': '11.00', 'category_id': 'bcat_leisure_subs', 'note': 'Teil B'}]}\n\n    def confirm_candidate_split(conn: Connection, candidate_id: str, payload: dict[str, Any]) -> dict[str, Any]:\n        row = conn.execute(\"SELECT * FROM budget_transaction_candidates WHERE transaction_candidate_id=?\", (candidate_id,)).fetchone()\n        if not row:\n            raise HTTPException(status_code=404, detail=\"transaction candidate not found\")\n        if row[\"status\"] in {\"confirmed\", \"ignored\", \"covered_by_migros\", \"superseded\"}:\n            raise HTTPException(status_code=409, detail=\"candidate is not splittable\")\n        splits = payload.get(\"splits\") or []\n        if not payload.get(\"account_id\"):\n            raise HTTPException(status_code=422, detail=\"account_id required\")\n        if not splits:\n            raise HTTPException(status_code=422, detail=\"splits required\")\n        total = sum(Decimal(_decimal_text(s.get(\"amount_original\")) or \"0\") for s in splits)\n        candidate_amount = Decimal(str(row[\"amount_original\"] or \"0\"))\n        if total != candidate_amount:\n            raise HTTPException(status_code=422, detail=\"split sum must match candidate amount\")\n        group_id = new_id(\"splitgrp\")\n        ts = now()\n        created_ids = []\n        conn.execute(\"DELETE FROM budget_candidate_splits WHERE transaction_candidate_id=?\", (candidate_id,))\n        for idx, split in enumerate(splits, start=1):\n            amount = _decimal_text(split.get(\"amount_original\"))\n            cat = split.get(\"category_id\")\n            if not cat or not conn.execute(\"SELECT 1 FROM budget_categories WHERE category_id=? AND is_active=1\", (cat,)).fetchone():\n                raise HTTPException(status_code=422, detail=\"active split category required\")\n            result = confirm_budget_transaction(conn, {\n                \"account_id\": payload[\"account_id\"], \"transaction_type\": payload.get(\"transaction_type\") or \"expense\", \"transaction_date\": row[\"transaction_date\"],\n                \"description\": f\"{row['description']} · Split {idx}\", \"amount_original\": amount, \"currency_original\": row[\"currency_original\"],\n                \"category_id\": cat, \"source_type\": \"import_candidate\", \"source_candidate_id\": candidate_id,\n                \"notes\": f\"Split group {group_id}: {split.get('note') or ''}\", \"tag_names\": [split.get(\"tag\")] if split.get(\"tag\") else [],\n            })\n            created_ids.append(result[\"entity_id\"])\n            conn.execute(\"INSERT INTO budget_candidate_splits(split_id, transaction_candidate_id, category_id, amount_original, notes, tag_name, confirmed_transaction_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)\", (new_id(\"bcsplit\"), candidate_id, cat, amount, split.get(\"note\"), split.get(\"tag\"), result[\"entity_id\"], ts, ts))\n>       conn.execute(\"UPDATE budget_transaction_candidates SET status='confirmed', confirmed_transaction_id=?, confirmed_at=?, confirmed_by='user', updated_at=? WHERE transaction_candidate_id=?\", (group_id, ts, ts, candidate_id))\nE       sqlite3.IntegrityError: FOREIGN KEY constraint failed\n\nsrc/jarvis_finance/services/budget_imports.py:909: IntegrityError\n=========================== short test summary info ============================\nFAILED tests/unit/test_budget_phase19_review_productivity.py::test_phase19_split_sum_blocks_and_confirm_creates_linked_transactions\nERROR tests/unit/test_budget_phase19_review_productivity.py::test_budget_rule\n1 failed, 4 passed, 1 error in 0.49s", "exit_code": 1, "error": null}

[Tool loop warning: repeated_exact_failure_warning; count=2; terminal has failed 2 times with identical arguments. This looks like a loop; inspect the error and change strategy instead of retrying it unchanged.]


patch

        created_ids.append(result["entity_id"])
        conn.execute("INSERT INTO budget_candidate_splits


    primary_confirmed_id = created_ids[0] if created_ids else group_id
    conn.execute("UPDATE budget_transaction_candidates SET status='confirmed', confirmed_transaction_id=?, confirmed_at=?, confirmed_by='user', updated_at=? WHERE transaction_candidate_id=?", (primary_confirmed_id, ts, ts, candidate_id))
    audit_id = record_audit_event(conn, source="vue_dashboard", action="candidate_split_confirmed", entity_type="budget_transaction_candidate", entity_id=candidate_id, new_values={"group_id": group_id, "primary_transaction_id": primary_confirmed_id, "transaction_ids": created_ids, "split_count": len(created_ids)}, created_by="user")
    conn.commit()
    return {"status": "confirmed", "group_id": primary_confirmed_id, "split_group_id": group_id, "created_transactions": len(created_ids), "transaction_ids": created_ids, "audit_id": audit_id}

    conn.execute("UPDATE budget_transaction_candidates SET status='confirmed', confirmed_transaction_id=?, confirmed_at=?, confirmed_by='user', updated_at=? WHERE transaction_candidate_id=?", (group_id, ts, ts, candidate_id))
    audit_id = record_audit_event(conn, source="vue_dashboard", action="candidate_split_confirmed", entity_type="budget_transaction_candidate", entity_id=candidate_id, new_values={"group_id": group_id, "transaction_ids": created_ids, "split_count": len(created_ids)}, created_by="user")
    conn.commit()
    return {"status": "confirmed", "group_id": group_id, "created_transactions": len(created_ids), "transaction_ids": created_ids, "audit_id": audit_id}


--- a//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/services/budget_imports.py
+++ b//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/services/budget_imports.py
@@ -906,10 +906,11 @@
         })
         created_ids.append(result["entity_id"])
         conn.execute("INSERT INTO budget_candidate_splits(split_id, transaction_candidate_id, category_id, amount_original, notes, tag_name, confirmed_transaction_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", (new_id("bcsplit"), candidate_id, cat, amount, split.get("note"), split.get("tag"), result["entity_id"], ts, ts))
-    conn.execute("UPDATE budget_transaction_candidates SET status='confirmed', confirmed_transaction_id=?, confirmed_at=?, confirmed_by='user', updated_at=? WHERE transaction_candidate_id=?", (group_id, ts, ts, candidate_id))
-    audit_id = record_audit_event(conn, source="vue_dashboard", action="candidate_split_confirmed", entity_type="budget_transaction_candidate", entity_id=candidate_id, new_values={"group_id": group_id, "transaction_ids": created_ids, "split_count": len(created_ids)}, created_by="user")
-    conn.commit()
-    return {"status": "confirmed", "group_id": group_id, "created_transactions": len(created_ids), "transaction_ids": created_ids, "audit_id": audit_id}
+    primary_confirmed_id = created_ids[0] if created_ids else group_id
+    conn.execute("UPDATE budget_transaction_candidates SET status='confirmed', confirmed_transaction_id=?, confirmed_at=?, confirmed_by='user', updated_at=? WHERE transaction_candidate_id=?", (primary_confirmed_id, ts, ts, candidate_id))
+    audit_id = record_audit_event(conn, source="vue_dashboard", action="candidate_split_confirmed", entity_type="budget_transaction_candidate", entity_id=candidate_id, new_values={"group_id": group_id, "primary_transaction_id": primary_confirmed_id, "transaction_ids": created_ids, "split_count": len(created_ids)}, created_by="user")
+    conn.commit()
+    return {"status": "confirmed", "group_id": primary_confirmed_id, "split_group_id": group_id, "created_transactions": len(created_ids), "transaction_ids": created_ids, "audit_id": audit_id}

 def create_review_category_for_candidate(conn: Connection, candidate_id: str, payload: dict[str, Any]) -> dict[str, Any]:


--- a//home/agent/.hermes/repos/FinanceManager/tests/unit/test_budget_phase19_review_productivity.py
+++ b//home/agent/.hermes/repos/FinanceManager/tests/unit/test_budget_phase19_review_productivity.py
@@ -17,7 +17,7 @@
     review_candidate_summary,
     seed_credit_card_candidates_from_rows,
     seed_transaction_candidates_from_rows,
-    test_budget_rule,
+    test_budget_rule as run_budget_rule,
 )
 from jarvis_finance.services.budget_overview import get_budget_status_by_category
 from jarvis_finance.services.budget_transactions import list_budget_transactions

LSP diagnostics introduced by this edit:
<diagnostics file="/home/agent/.hermes/repos/FinanceManager/tests/unit/test_budget_phase19_review_productivity.py">
ERROR [99:14] "test_budget_rule" is not defined [reportUndefinedVariable] (Pyright)
</diagnostics>


tested = run_budget_rule(conn, rule["rule_id"])

tested = test_budget_rule(conn, rule["rule_id"])


--- a//home/agent/.hermes/repos/FinanceManager/tests/unit/test_budget_phase19_review_productivity.py
+++ b//home/agent/.hermes/repos/FinanceManager/tests/unit/test_budget_phase19_review_productivity.py
@@ -96,7 +96,7 @@
     assert row["proposed_category_id"] == created["category_id"]
     rule = create_budget_rule(conn, {"merchant_contains": "Apotheke", "source_type": "csv_seed", "category_id": health, "target_status": "review"})
     assert list_budget_rules(conn)[0]["is_active"] is True
-    tested = test_budget_rule(conn, rule["rule_id"])
+    tested = run_budget_rule(conn, rule["rule_id"])
     assert tested["match_count"] >= 1
     applied = apply_budget_rules_to_open_candidates(conn, rule["rule_id"])
     assert applied["updated_count"] >= 1


terminal

Output:
.....                                                                    [100%]
5 passed in 0.46s


Backend-Phase-1.9-Kerntests sind grün. Ich verdrahte jetzt API und UI.

Explanation:
**Updating APIs and UI**

I need to update the API imports and routes, maybe something like a patch. Then there's the frontend API that needs attention too. I'm considering if I should tackle the UI afterward; maybe that will be enough to cover everything I need to do. It's always good to have a clear order for tasks. I’ll focus on making sure everything flows together well in the end. Alright, let’s get to it!

patch

    apply_budget_rules_to_open_candidates,
    batch_confirm_safe_candidates,
    batch_mark_candidates,
    bulk_update_transaction_candidate_category,
    confirm_candidate_split,
    confirm_transaction_candidate,
    create_budget_rule,
    create_candidate_split,
    create_review_category_for_candidate,
    deactivate_budget_rule,
    ignore_transaction_candidate,
    list_budget_rules,
    list_transaction_candidates,
    mark_candidate_transfer,
    preview_batch_confirm_candidates,
    preview_safe_auto_candidates,
    preview_transaction_candidate,
    reopen_transaction_candidate,
    review_candidate_summary,
    seed_bank_transfer_candidates_from_rows,
    seed_credit_card_candidates_from_rows,
    seed_migros_candidates_from_rows,
    seed_transaction_candidates_from_csv_text,
    seed_transaction_candidates_from_rows,
    test_budget_rule,
    update_transaction_candidate_category,

    batch_confirm_safe_candidates,
    bulk_update_transaction_candidate_category,
    confirm_transaction_candidate,
    create_candidate_split,
    ignore_transaction_candidate,
    list_transaction_candidates,
    mark_candidate_transfer,
    preview_safe_auto_candidates,
    preview_transaction_candidate,
    reopen_transaction_candidate,
    seed_bank_transfer_candidates_from_rows,
    seed_credit_card_candidates_from_rows,
    seed_migros_candidates_from_rows,
    seed_transaction_candidates_from_csv_text,
    seed_transaction_candidates_from_rows,
    update_transaction_candidate_category,


--- a//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/api/routers/budget.py
+++ b//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/api/routers/budget.py
@@ -20,21 +20,31 @@
     update_category,
 )
 from jarvis_finance.services.budget_imports import (
+    apply_budget_rules_to_open_candidates,
     batch_confirm_safe_candidates,
+    batch_mark_candidates,
     bulk_update_transaction_candidate_category,
+    confirm_candidate_split,
     confirm_transaction_candidate,
+    create_budget_rule,
     create_candidate_split,
+    create_review_category_for_candidate,
+    deactivate_budget_rule,
     ignore_transaction_candidate,
+    list_budget_rules,
     list_transaction_candidates,
     mark_candidate_transfer,
+    preview_batch_confirm_candidates,
     preview_safe_auto_candidates,
     preview_transaction_candidate,
     reopen_transaction_candidate,
+    review_candidate_summary,
     seed_bank_transfer_candidates_from_rows,
     seed_credit_card_candidates_from_rows,
     seed_migros_candidates_from_rows,
     seed_transaction_candidates_from_csv_text,
     seed_transaction_candidates_from_rows,
+    test_budget_rule,
     update_transaction_candidate_category,
 )
 from jarvis_finance.services.budget_overview import get_budget_overview, get_budget_status_by_category, get_category_summary, get_income_status, get_monthly_summary


@router.get("/transaction-candidates")
def transaction_candidates(status: str | None = None, tab: str | None = None, source_type: str | None = None, category_id: str | None = None, merchant: str | None = None, rule: str | None = None, min_confidence: str | None = None, date_from: str | None = None, date_to: str | None = None, amount_min: str | None = None, amount_max: str | None = None, sort_by: str = "created_at", sort_dir: str = "desc", conn: Connection = Depends(get_db)):
    return list_transaction_candidates(conn, status=status, tab=tab, source_type=source_type, category_id=category_id, merchant=merchant, rule=rule, min_confidence=min_confidence, date_from=date_from, date_to=date_to, amount_min=amount_min, amount_max=amount_max, sort_by=sort_by, sort_dir=sort_dir)

@router.get("/transaction-candidates/summary")
def transaction_candidates_summary(conn: Connection = Depends(get_db)):
    return review_candidate_summary(conn)

@router.get("/transaction-candidates")
def transaction_candidates(status: str | None = None, tab: str | None = None, conn: Connection = Depends(get_db)):
    return list_transaction_candidates(conn, status=status, tab=tab)


--- a//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/api/routers/budget.py
+++ b//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/api/routers/budget.py
@@ -168,8 +168,13 @@

 @router.get("/transaction-candidates")
-def transaction_candidates(status: str | None = None, tab: str | None = None, conn: Connection = Depends(get_db)):
-    return list_transaction_candidates(conn, status=status, tab=tab)
+def transaction_candidates(status: str | None = None, tab: str | None = None, source_type: str | None = None, category_id: str | None = None, merchant: str | None = None, rule: str | None = None, min_confidence: str | None = None, date_from: str | None = None, date_to: str | None = None, amount_min: str | None = None, amount_max: str | None = None, sort_by: str = "created_at", sort_dir: str = "desc", conn: Connection = Depends(get_db)):
+    return list_transaction_candidates(conn, status=status, tab=tab, source_type=source_type, category_id=category_id, merchant=merchant, rule=rule, min_confidence=min_confidence, date_from=date_from, date_to=date_to, amount_min=amount_min, amount_max=amount_max, sort_by=sort_by, sort_dir=sort_dir)
+
+
+@router.get("/transaction-candidates/summary")
+def transaction_candidates_summary(conn: Connection = Depends(get_db)):
+    return review_candidate_summary(conn)

 @router.post("/transaction-candidates/generate")


@router.post("/transaction-candidates/{candidate_id}/split")
def transaction_candidates_split(candidate_id: str, payload: dict, conn: Connection = Depends(get_db)):
    return create_candidate_split(conn, candidate_id, payload.get("splits") or [])

@router.post("/transaction-candidates/{candidate_id}/split/confirm")
def transaction_candidates_split_confirm(candidate_id: str, payload: dict, conn: Connection = Depends(get_db)):
    return confirm_candidate_split(conn, candidate_id, payload)

@router.post("/transaction-candidates/{candidate_id}/category/create")
def transaction_candidates_create_category(candidate_id: str, payload: dict, conn: Connection = Depends(get_db)):
    return create_review_category_for_candidate(conn, candidate_id, payload)


--- a//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/api/routers/budget.py
+++ b//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/api/routers/budget.py
@@ -228,6 +228,16 @@
     return create_candidate_split(conn, candidate_id, payload.get("splits") or [])

+@router.post("/transaction-candidates/{candidate_id}/split/confirm")
+def transaction_candidates_split_confirm(candidate_id: str, payload: dict, conn: Connection = Depends(get_db)):
+    return confirm_candidate_split(conn, candidate_id, payload)
+
+
+@router.post("/transaction-candidates/{candidate_id}/category/create")
+def transaction_candidates_create_category(candidate_id: str, payload: dict, conn: Connection = Depends(get_db)):
+    return create_review_category_for_candidate(conn, candidate_id, payload)
+
+
 @router.post("/transaction-candidates/{candidate_id}/transfer")
 def transaction_candidates_transfer(candidate_id: str, payload: dict | None = None, conn: Connection = Depends(get_db)):
     return mark_candidate_transfer(conn, candidate_id, (payload or {}).get("note"))


@router.post("/transaction-candidates/batch-confirm-safe/preview")
def transaction_candidates_batch_confirm_safe_preview(conn: Connection = Depends(get_db)):
    return preview_safe_auto_candidates(conn)

@router.post("/transaction-candidates/batch/preview")
def transaction_candidates_batch_preview(payload: dict, conn: Connection = Depends(get_db)):
    return preview_batch_confirm_candidates(conn, candidate_ids=payload.get("candidate_ids") or [], account_id=str(payload.get("account_id") or ""))

@router.post("/transaction-candidates/batch")
def transaction_candidates_batch(payload: dict, conn: Connection = Depends(get_db)):
    return batch_mark_candidates(conn, candidate_ids=payload.get("candidate_ids") or [], action=str(payload.get("action") or ""), account_id=payload.get("account_id"), category_id=payload.get("category_id"), preview_id=payload.get("preview_id"), note=payload.get("note"))


--- a//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/api/routers/budget.py
+++ b//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/api/routers/budget.py
@@ -248,6 +248,16 @@
     return preview_safe_auto_candidates(conn)

+@router.post("/transaction-candidates/batch/preview")
+def transaction_candidates_batch_preview(payload: dict, conn: Connection = Depends(get_db)):
+    return preview_batch_confirm_candidates(conn, candidate_ids=payload.get("candidate_ids") or [], account_id=str(payload.get("account_id") or ""))
+
+
+@router.post("/transaction-candidates/batch")
+def transaction_candidates_batch(payload: dict, conn: Connection = Depends(get_db)):
+    return batch_mark_candidates(conn, candidate_ids=payload.get("candidate_ids") or [], action=str(payload.get("action") or ""), account_id=payload.get("account_id"), category_id=payload.get("category_id"), preview_id=payload.get("preview_id"), note=payload.get("note"))
+
+
 @router.post("/transaction-candidates/batch-confirm-safe")
 def transaction_candidates_batch_confirm_safe(payload: dict, conn: Connection = Depends(get_db)):
     account_id = str(payload.get("account_id") or "")


@router.post("/categories/merge/confirm")
def categories_merge_confirm(payload: dict, conn: Connection = Depends(get_db)):
    return confirm_category_merge(conn, payload)

@router.get("/rules")
def rules(include_inactive: bool = False, conn: Connection = Depends(get_db)):
    return list_budget_rules(conn, include_inactive=include_inactive)

@router.post("/rules")
def rules_create(payload: dict, conn: Connection = Depends(get_db)):
    return create_budget_rule(conn, payload)

@router.post("/rules/{rule_id}/deactivate")
def rules_deactivate(rule_id: str, conn: Connection = Depends(get_db)):
    return deactivate_budget_rule(conn, rule_id)

@router.post("/rules/{rule_id}/test")
def rules_test(rule_id: str, conn: Connection = Depends(get_db)):
    return test_budget_rule(conn, rule_id)

@router.post("/rules/{rule_id}/apply")
def rules_apply(rule_id: str, conn: Connection = Depends(get_db)):
    return apply_budget_rules_to_open_candidates(conn, rule_id)


--- a//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/api/routers/budget.py
+++ b//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/api/routers/budget.py
@@ -354,6 +354,31 @@
     return confirm_category_merge(conn, payload)

+@router.get("/rules")
+def rules(include_inactive: bool = False, conn: Connection = Depends(get_db)):
+    return list_budget_rules(conn, include_inactive=include_inactive)
+
+
+@router.post("/rules")
+def rules_create(payload: dict, conn: Connection = Depends(get_db)):
+    return create_budget_rule(conn, payload)
+
+
+@router.post("/rules/{rule_id}/deactivate")
+def rules_deactivate(rule_id: str, conn: Connection = Depends(get_db)):
+    return deactivate_budget_rule(conn, rule_id)
+
+
+@router.post("/rules/{rule_id}/test")
+def rules_test(rule_id: str, conn: Connection = Depends(get_db)):
+    return test_budget_rule(conn, rule_id)
+
+
+@router.post("/rules/{rule_id}/apply")
+def rules_apply(rule_id: str, conn: Connection = Depends(get_db)):
+    return apply_budget_rules_to_open_candidates(conn, rule_id)
+
+
 @router.get("/transactions")
 def transactions(month: str | None = None, account_id: str | None = None, category_id: str | None = None, transaction_type: str | None = None, merchant: str | None = None, tag: str | None = None, status: str | None = None, source_type: str | None = None, search: str | None = None, conn: Connection = Depends(get_db)):
     return list_budget_transactions(conn, month=month, account_id=account_id, category_id=category_id, transaction_type=transaction_type, merchant=merchant, tag=tag, status=status, source_type=source_type, search=search)


export type BudgetTransactionCandidate = { transaction_candidate_id: string; source_file_label: string; source_row_or_range?: string | null; source_type: string; transaction_date?: string | null; description: string; merchant?: string | null; amount_original?: string | null; currency_original: string; proposed_category_id?: string | null; proposed_category_name?: string | null; duplicate_of_transaction_id?: string | null; confidence: string; requires_review: boolean; status: string; notes?: string | null; classification?: string | null; review_reason?: string | null; rule_id?: string | null; rule_name?: string | null; account_source?: string | null; covered_by_source?: string | null }
export type BudgetReviewSummary = { open_total: number; auto_categorized: number; review_needed: number; galaxus_digitec: number; migros_over_50: number; covered_by_migros: number; unclear: number; possible_duplicates: number }
export type BudgetRule = { rule_id: string; merchant_contains: string; source_type?: string | null; category_id?: string | null; category_name?: string | null; target_status: string; is_active: boolean }


--- a//home/agent/.hermes/repos/FinanceManager/frontend/src/api/budget.ts
+++ b//home/agent/.hermes/repos/FinanceManager/frontend/src/api/budget.ts
@@ -27,6 +27,8 @@
 export type BudgetSeedSummary = { category_candidate_count: number; budget_plan_candidate_count: number; recurring_candidate_count: number; unclean_range_count: number; current_budget_sheet?: string }
 export type BudgetStatusRow = { category_id: string; category: string; category_type: string; budget_month: string; budget_year: string; actual_year_to_date: string; actual_avg_month: string; forecast_year: string; forecast_avg_month: string; deviation_year: string; deviation_month: string; status: string; months_with_bookings: number; plan_names?: string[] }
 export type BudgetTransactionCandidate = { transaction_candidate_id: string; source_file_label: string; source_row_or_range?: string | null; source_type: string; transaction_date?: string | null; description: string; merchant?: string | null; amount_original?: string | null; currency_original: string; proposed_category_id?: string | null; proposed_category_name?: string | null; duplicate_of_transaction_id?: string | null; confidence: string; requires_review: boolean; status: string; notes?: string | null; classification?: string | null; review_reason?: string | null; rule_id?: string | null; rule_name?: string | null; account_source?: string | null; covered_by_source?: string | null }
+export type BudgetReviewSummary = { open_total: number; auto_categorized: number; review_needed: number; galaxus_digitec: number; migros_over_50: number; covered_by_migros: number; unclear: number; possible_duplicates: number }
+export type BudgetRule = { rule_id: string; merchant_contains: string; source_type?: string | null; category_id?: string | null; category_name?: string | null; target_status: string; is_active: boolean }
 export type BudgetOverview = { current_month: string; income_chf: string; expense_chf: string; net_cashflow_chf: string; review_count: number; uncategorized_count: number; pending_seed_candidate_count?: number; pending_transaction_candidate_count?: number; planned_income_chf?: string; planned_expense_chf?: string; planned_net_chf?: string; actual_income_chf?: string; actual_expense_chf: string; actual_net_chf?: string; actuals_label?: string; open_categories_without_actuals?: number; planned_budget_chf: string; remaining_budget_chf: string; budget_status: string; budget_categories: BudgetStatusRow[]; uncategorized_transactions: BudgetTransaction[]; top_categories: Array<{ category_id?: string; category_name: string; amount_chf: string; transaction_count?: number }>; recent_transactions: BudgetTransaction[] }

 export const getBudgetOverview = () => apiGet<BudgetOverview>('/api/budget/overview')

npx not usable: This is not the tsc command you are looking for

/home/agent/.hermes/repos/FinanceManager/frontend/src/api/budget.ts was modified by sibling subagent '20260517_134754_7dea44' but this agent never read it. Read the file before writing to avoid overwriting the sibling's changes.


export const getBudgetTransactionCandidates = (status = '', tab = '', filters: Record<string, string> = {}) => {
  const params = new URLSearchParams()
  if (status) params.set('status', status)
  if (tab) params.set('tab', tab)
  Object.entries(filters).forEach(([k, v]) => { if (v) params.set(k, v) })
  const qs = params.toString()
  return apiGet<BudgetTransactionCandidate[]>(`/api/budget/transaction-candidates${qs ? `?${qs}` : ''}`)
}
export const getBudgetTransactionCandidateSummary = () => apiGet<BudgetReviewSummary>('/api/budget/transaction-candidates/summary')

export const getBudgetTransactionCandidates = (status = '', tab = '') => {
  const params = new URLSearchParams()
  if (status) params.set('status', status)
  if (tab) params.set('tab', tab)
  const qs = params.toString()
  return apiGet<BudgetTransactionCandidate[]>(`/api/budget/transaction-candidates${qs ? `?${qs}` : ''}`)
}


--- a//home/agent/.hermes/repos/FinanceManager/frontend/src/api/budget.ts
+++ b//home/agent/.hermes/repos/FinanceManager/frontend/src/api/budget.ts
@@ -65,13 +65,15 @@
 export const confirmClearBudgetCategoriesTakeover = () => apiPost<BudgetConfirm & { confirmed_count: number; skipped_count: number }>('/api/budget/seed-candidates/categories/confirm-clear-takeover', {})
 export const previewBudgetPlanTakeover = () => apiPost<{ preview_id: string; summary: string; count: number; budget_plans: Array<Record<string, unknown>>; warnings: string[] }>('/api/budget/seed-candidates/budget-plans/preview-takeover', {})
 export const confirmBudgetPlanTakeover = () => apiPost<BudgetConfirm & { confirmed_count: number; skipped_count: number }>('/api/budget/seed-candidates/budget-plans/confirm-takeover', {})
-export const getBudgetTransactionCandidates = (status = '', tab = '') => {
+export const getBudgetTransactionCandidates = (status = '', tab = '', filters: Record<string, string> = {}) => {
   const params = new URLSearchParams()
   if (status) params.set('status', status)
   if (tab) params.set('tab', tab)
+  Object.entries(filters).forEach(([k, v]) => { if (v) params.set(k, v) })
   const qs = params.toString()
   return apiGet<BudgetTransactionCandidate[]>(`/api/budget/transaction-candidates${qs ? `?${qs}` : ''}`)
 }
+export const getBudgetTransactionCandidateSummary = () => apiGet<BudgetReviewSummary>('/api/budget/transaction-candidates/summary')
 export const generateBudgetTransactionCandidates = (body: unknown) => apiPost<Record<string, number>>('/api/budget/transaction-candidates/generate', body)
 export const previewBudgetTransactionCandidate = (id: string, body: unknown) => apiPost<BudgetPreview>(`/api/budget/transaction-candidates/${id}/preview`, body)
 export const confirmBudgetTransactionCandidate = (id: string, body: unknown) => apiPost<BudgetConfirm>(`/api/budget/transaction-candidates/${id}/confirm`, body)


No linter for .tmp files


export const confirmBudgetCategoryMerge = (body: unknown) => apiPost<BudgetConfirm & { moved_transactions: number; moved_budget_plans: number; moved_children: number }>('/api/budget/categories/merge/confirm', body)
export const previewBatchBudgetTransactionCandidates = (candidate_ids: string[], account_id: string) => apiPost<{ preview_id: string; count: number; categories: Record<string, number>; conflicts: Array<Record<string, unknown>>; requires_explicit_confirm: boolean }>('/api/budget/transaction-candidates/batch/preview', { candidate_ids, account_id })
export const batchBudgetTransactionCandidates = (body: unknown) => apiPost<Record<string, unknown>>('/api/budget/transaction-candidates/batch', body)
export const confirmBudgetTransactionCandidateSplit = (id: string, body: unknown) => apiPost<Record<string, unknown>>(`/api/budget/transaction-candidates/${id}/split/confirm`, body)
export const createReviewCategoryForCandidate = (id: string, body: unknown) => apiPost<BudgetConfirm & { category_id: string }>(`/api/budget/transaction-candidates/${id}/category/create`, body)
export const getBudgetRules = () => apiGet<BudgetRule[]>('/api/budget/rules')
export const createBudgetRule = (body: unknown) => apiPost<Record<string, unknown>>('/api/budget/rules', body)
export const deactivateBudgetRule = (id: string) => apiPost<Record<string, unknown>>(`/api/budget/rules/${id}/deactivate`, {})
export const testBudgetRule = (id: string) => apiPost<Record<string, unknown>>(`/api/budget/rules/${id}/test`, {})
export const applyBudgetRule = (id: string) => apiPost<Record<string, unknown>>(`/api/budget/rules/${id}/apply`, {})


--- a//home/agent/.hermes/repos/FinanceManager/frontend/src/api/budget.ts
+++ b//home/agent/.hermes/repos/FinanceManager/frontend/src/api/budget.ts
@@ -88,3 +88,12 @@
 export const deleteEmptyBudgetCategory = (id: string) => apiPost<BudgetConfirm>(`/api/budget/categories/${id}/delete-empty`, {})
 export const previewBudgetCategoryMerges = () => apiPost<{ preview_id: string; summary: string; suggestions: Array<{ target_category_id: string; target_name: string; source_category_ids: string[]; source_names: string[] }>; warnings: string[] }>('/api/budget/categories/merge/preview', {})
 export const confirmBudgetCategoryMerge = (body: unknown) => apiPost<BudgetConfirm & { moved_transactions: number; moved_budget_plans: number; moved_children: number }>('/api/budget/categories/merge/confirm', body)
+export const previewBatchBudgetTransactionCandidates = (candidate_ids: string[], account_id: string) => apiPost<{ preview_id: string; count: number; categories: Record<string, number>; conflicts: Array<Record<string, unknown>>; requires_explicit_confirm: boolean }>('/api/budget/transaction-candidates/batch/preview', { candidate_ids, account_id })
+export const batchBudgetTransactionCandidates = (body: unknown) => apiPost<Record<string, unknown>>('/api/budget/transaction-candidates/batch', body)
+export const confirmBudgetTransactionCandidateSplit = (id: string, body: unknown) => apiPost<Record<string, unknown>>(`/api/budget/transaction-candidates/${id}/split/confirm`, body)
+export const createReviewCategoryForCandidate = (id: string, body: unknown) => apiPost<BudgetConfirm & { category_id: string }>(`/api/budget/transaction-candidates/${id}/category/create`, body)
+export const getBudgetRules = () => apiGet<BudgetRule[]>('/api/budget/rules')
+export const createBudgetRule = (body: unknown) => apiPost<Record<string, unknown>>('/api/budget/rules', body)
+export const deactivateBudgetRule = (id: string) => apiPost<Record<string, unknown>>(`/api/budget/rules/${id}/deactivate`, {})
+export const testBudgetRule = (id: string) => apiPost<Record<string, unknown>>(`/api/budget/rules/${id}/test`, {})
+export const applyBudgetRule = (id: string) => apiPost<Record<string, unknown>>(`/api/budget/rules/${id}/apply`, {})


<template>

Ausgaben · Buchungen prüfen

Buchungen prüfen

Zentrale Review-Arbeitsseite. Confirm bleibt Preview → Confirm und erzeugt keine Blindbuchung.

{{ message }}

{{ error }}

{{ card.label }}

{{ card.value }}

        <button v-for="tab in tabs" :key="tab.key" class="rounded-full border px-3 py-1.5 text-xs font-semibold" :class="activeTab === tab.key ? 'border-cyan-400 bg-cyan-400/10 text-cyan-200' : 'border-slate-700 text-slate-400'" @click="setTab(tab.key)">{{ tab.label }}</button>

        <select v-model="confirmAccount" class="field"><option value="">Confirm-Konto wählen</option><option v-for="account in accounts" :key="account.budget_account_id" :value="account.budget_account_id">{{ account.name }}</option></select>
        <select v-model="status" class="field"><option value="">offene Kandidaten</option><option>pending</option><option>auto_categorized</option><option>needs_review</option><option>duplicate</option><option>transfer_candidate</option><option>ignored</option></select>
        <select v-model="bulkCategory" class="field"><option value="">Bulk-Kategorie</option><option v-for="cat in flatCategories" :key="cat.category_id" :value="cat.category_id">{{ cat.name }}</option></select>
        <input v-model="filters.source_type" class="field" placeholder="Quelle" />
        <input v-model="filters.merchant" class="field" placeholder="Händler" />
        <input v-model="filters.rule" class="field" placeholder="Regel" />
        <select v-model="filters.category_id" class="field"><option value="">Kategorie-Vorschlag</option><option v-for="cat in flatCategories" :key="cat.category_id" :value="cat.category_id">{{ cat.name }}</option></select>
        <input v-model="filters.min_confidence" class="field" placeholder="Min Confidence" />
        <input v-model="filters.date_from" class="field" type="date" />
        <input v-model="filters.date_to" class="field" type="date" />
        <input v-model="filters.amount_min" class="field" placeholder="Min Betrag" />
        <input v-model="filters.amount_max" class="field" placeholder="Max Betrag" />
        <select v-model="filters.sort_by" class="field"><option value="created_at">Sort: erstellt</option><option value="date">Datum</option><option value="merchant">Händler</option><option value="category">Kategorie</option><option value="confidence">Confidence</option><option value="source">Quelle</option><option value="status">Status</option></select>
        <select v-model="filters.sort_dir" class="field"><option value="desc">absteigend</option><option value="asc">aufsteigend</option></select>
        <button class="btn-primary" @click="load">Filtern</button>

        <button class="btn" :disabled="!selectedIds.length || !bulkCategory" @click="bulkSetCategory">Auswahl kategorisieren</button>
        <button class="btn" :disabled="!selectedIds.length" @click="previewBatchConfirm">Batch Confirm Preview</button>
        <button class="btn" :disabled="!selectedIds.length" @click="bulkIgnore">Auswahl ignorieren</button>
        <button class="btn" :disabled="!selectedIds.length" @click="bulkReopen">Auswahl wieder öffnen</button>
        <button class="btn" :disabled="!selectedIds.length" @click="bulkTransfer">Als Transfer markieren</button>
        <button class="btn" :disabled="!selectedIds.length" @click="bulkCovered">Als covered_by_migros markieren</button>

Batch Preview: {{ batchPreview.count }} Kandidaten

Konflikte: {{ batchPreview.conflicts.length }}

Kategorien: {{ Object.entries(batchPreview.categories).map(([k,v]) => `${k}: ${v}`).join(', ') || '—' }}

        <button class="mt-3 btn-primary" @click="confirmBatchPreview">Jetzt ausgewählte bestätigen</button>

<input type="checkbox" :checked="allVisibleSelected" @change="toggleAll" />	Datum	Quelle	Händler/Beschreibung	Betrag	Währung	Kategorie	Confidence	Regel/Grund	Status	Aktionen

              <input v-model="selected" :value="item.transaction_candidate_id" type="checkbox" />
              {{ item.transaction_date || '—' }}
              {{ item.source_type }}{{ item.source_file_label }}
              {{ item.merchant || item.description }}{{ item.description }}
              {{ item.amount_original || '—' }}	{{ item.currency_original }}
              <select :value="item.proposed_category_id || ''" class="field w-full" @change="setCategory(item, ($event.target as HTMLSelectElement).value)"><option value="">Review nötig</option><option v-for="cat in flatCategories" :key="cat.category_id" :value="cat.category_id">{{ cat.name }}</option></select><button class="mt-2 text-xs text-cyan-200" @click="openNewCategory(item)">+ Kategorie</button>
              {{ item.confidence }}	{{ item.rule_name || item.classification || '—' }}{{ item.review_reason || item.notes }}	{{ item.status }}

<button v-if="canConfirm(item)" class="mini green" @click="confirmItem(item)">Bestätigen</button><button class="mini" @click="ignoreItem(item.transaction_candidate_id)">Ignorieren</button><button class="mini" @click="reopenItem(item.transaction_candidate_id)">Wieder öffnen</button><button class="mini amber" @click="markTransfer(item.transaction_candidate_id)">Transfer</button><button class="mini cyan" @click="openSplit(item)">Splitten</button><button class="mini cyan" @click="detail=item">Details</button>

Keine Kandidaten.

      <button class="float-right text-slate-300" @click="detail=null">Schließen</button>
Kandidat Details

{{ detail.merchant || detail.description }}

{{ row[0] }}

{{ row[1] || '—' }}

Neue Kategorie erstellen
<input v-model="newCategory.name" class="field mt-3 w-full" placeholder="Name" /><select v-model="newCategory.parent_category_id" class="field mt-3 w-full"><option value="">Keine Parent-Kategorie</option><option v-for="cat in flatCategories" :key="cat.category_id" :value="cat.category_id">{{ cat.name }}</option></select><select v-model="newCategory.category_type" class="field mt-3 w-full"><option>expense</option><option>income</option><option>transfer</option><option>neutral</option></select><button class="btn-primary mt-3" @click="createCategoryFromReview">Kategorie erstellen & zuordnen</button>

Split erstellen

<select v-model="split.category_id" class="field"><option value="">Kategorie</option><option v-for="cat in flatCategories" :key="cat.category_id" :value="cat.category_id">{{ cat.name }}</option></select><input v-model="split.amount_original" class="field" placeholder="Betrag" /><input v-model="split.tag" class="field" placeholder="Tag" /><input v-model="split.note" class="field" placeholder="Notiz" />
<button class="btn mt-3" @click="splits.push({ category_id:'', amount_original:'', tag:'', note:'' })">Zeile +</button><button class="btn-primary mt-3 ml-2" @click="confirmSplit">Split Preview/Confirm</button>

</template>


No linter for .vue files

/home/agent/.hermes/repos/FinanceManager/frontend/src/pages/BudgetImportReviewPage.vue was modified by sibling subagent '20260517_145701_6108f4' but this agent never read it. Read the file before writing to avoid overwriting the sibling's changes.


<template>

Budget · Regeln

Rule Manager MVP-light

Regeln setzen nur Vorschläge auf offene Kandidaten. Keine produktive Buchung.

{{ message }}

Regel erstellen

        <input v-model="form.merchant_contains" class="field" placeholder="Händler enthält Text" />
        <input v-model="form.source_type" class="field" placeholder="Quelle optional" />
        <select v-model="form.category_id" class="field"><option value="">Kategorie</option><option v-for="cat in flatCategories" :key="cat.category_id" :value="cat.category_id">{{ cat.name }}</option></select>
        <select v-model="form.target_status" class="field"><option value="auto">auto</option><option value="review">review</option></select>
        <button class="btn-primary" @click="create">Regel erstellen</button>

Bestehende Regeln

Text	Quelle	Kategorie	Status	Aktiv	Aktionen

{{ rule.merchant_contains }}	{{ rule.source_type || 'alle' }}	{{ rule.category_name || '—' }}	{{ rule.target_status }}	{{ rule.is_active ? 'ja' : 'nein' }}	<button class="mini" @click="testRule(rule.rule_id)">Test</button><button class="mini green" @click="applyRule(rule.rule_id)">Als Vorschlag anwenden</button><button class="mini" @click="deactivate(rule.rule_id)">Deaktivieren</button>

Noch keine Regeln.

</template>


No linter for .vue files


[Your active task list was preserved across context compression]
- [>] p19-0. Arbeitsbasis prüfen, sauberen Git-Stand und vorhandene Budget-APIs/UI inventarisieren (in_progress)
- [ ] p19-1. Phase-1.9 Backend-Regressionstests für Summary/Filter/Sort/Batch Preview/Split/Rules/Budgetstatus schreiben und RED prüfen (pending)
- [ ] p19-2. Backend Services/API für Candidate Summary, Filter/Sort, Batch Preview/Actions, Split Confirm, Review-Kategorie, Rule MVP-light ergänzen (pending)
- [ ] p19-3. Frontend Buchungen prüfen: Summary Cards, Filter/Sort, Batch Preview/Confirm, Split Drawer, Kategorie erstellen (pending)
- [ ] p19-4. Rule Manager MVP-light Seite und Route bauen (pending)
- [ ] p19-5. Effektive Ausgaben, Budgetstatus Row Click und Income-Seite verbessern (pending)
- [ ] p19-6. Tests grün machen: pytest, npm test, npm build (pending)
- [ ] p19-7. Safety: keine produktive Massenbestätigung, Runtime nur Backup/keine echten Bulk-Writes, scans, Git-Safety, diff-check (pending)
- [ ] p19-8. Commit, Push, Remote-Hash, Abschlussbericht (pending)


Ich fahre fort, Sir. Nächster Schritt: Rule-Route verdrahten, dann die drei Arbeitsseiten fertigziehen und die Test-/Safety-Gates laufen lassen.

Explanation:
**Continuing the process**

I need to continue from where I left off. Should I load the necessary skills? They've already been loaded, but I want to scan them again to be sure. Are they relevant to the current context? It might be worth considering loading software development skills—what do I need to focus on? I need to keep using the necessary tools and also inspect any relevant files as I move forward.


     1|import { createRouter, createWebHistory } from 'vue-router'
     2|import CommandCenter from '@/pages/CommandCenter.vue'
     3|import PortfolioPage from '@/pages/PortfolioPage.vue'
     4|import CryptoPage from '@/pages/CryptoPage.vue'
     5|import EquityPage from '@/pages/EquityPage.vue'
     6|import WalletsPage from '@/pages/WalletsPage.vue'
     7|import ReportsPage from '@/pages/ReportsPage.vue'
     8|import PositionAddPage from '@/pages/PositionAddPage.vue'
     9|import RoadmapPage from '@/pages/RoadmapPage.vue'
    10|import BudgetOverviewPage from '@/pages/BudgetOverviewPage.vue'
    11|import BudgetTransactionsPage from '@/pages/BudgetTransactionsPage.vue'
    12|import BudgetAccountsPage from '@/pages/BudgetAccountsPage.vue'
    13|import BudgetCategoriesPage from '@/pages/BudgetCategoriesPage.vue'
    14|import BudgetSetupPage from '@/pages/BudgetSetupPage.vue'
    15|import BudgetImportReviewPage from '@/pages/BudgetImportReviewPage.vue'
    16|import BudgetIncomePage from '@/pages/BudgetIncomePage.vue'
    17|
    18|const later = (title: string, description: string) => ({ component: RoadmapPage, props: { title, description }, meta: { title } })
    19|const router = createRouter({
    20|  history: createWebHistory(),
    21|  routes: [
    22|    { path: '/', name: 'command-center', component: CommandCenter, meta: { title: 'Command Center' } },
    23|    { path: '/portfolio', name: 'portfolio', component: PortfolioPage, meta: { title: 'Portfolio' } },
    24|    { path: '/crypto', name: 'crypto', component: CryptoPage, meta: { title: 'Crypto' } },
    25|    { path: '/equity', name: 'equity', component: EquityPage, meta: { title: 'Aktien & ETFs' } },
    26|    { path: '/wallets', name: 'wallets', component: WalletsPage, meta: { title: 'Wallets' } },
    27|    { path: '/positions/add', name: 'position-add', component: PositionAddPage, meta: { title: 'Position hinzufügen' } },
    28|    { path: '/reports', name: 'reports', component: ReportsPage, meta: { title: 'Reports' } },
    29|    { path: '/planning/budget', name: 'budget-overview', component: BudgetOverviewPage, meta: { title: 'Budgetstatus nach Kategorie' } },
    30|    { path: '/planning/budget/status', name: 'budget-status', component: BudgetOverviewPage, meta: { title: 'Budgetstatus nach Kategorie' } },
    31|    { path: '/planning/budget/transactions', name: 'budget-transactions', component: BudgetTransactionsPage, meta: { title: 'Manuelle Budget-Transaktionen' } },
    32|    { path: '/planning/budget/accounts', name: 'budget-accounts', component: BudgetAccountsPage, meta: { title: 'Budget-Konten' } },
    33|    { path: '/planning/budget/categories', name: 'budget-categories', component: BudgetCategoriesPage, meta: { title: 'Budget-Kategorien' } },
    34|    { path: '/planning/budget/tags', name: 'budget-tags-later', ...later('Tags', 'Tag-Verwaltung ist als Budget Setup-Unterseite vorgesehen. Aktuell erfolgt sie im bestehenden Kategorien/Setup-Flow.') },
    35|    { path: '/planning/budget/setup', name: 'budget-setup', component: BudgetSetupPage, meta: { title: 'Excel übernehmen / Seed Review' } },
    36|    { path: '/planning/budget/plans', name: 'budget-plans-later', ...later('Budgetpläne', 'Budgetpläne sind produktiv übernommen; Detailpflege folgt als eigener Editor.') },
    37|    { path: '/planning/budget/monthly', name: 'budget-monthly-later', ...later('Monatsbudget', 'Monatsbudget ist im Budgetstatus je Kategorie sichtbar; Detailseite folgt.') },
    38|    { path: '/planning/budget/yearly', name: 'budget-yearly-later', ...later('Jahresbudget', 'Jahresbudget ist im Budgetstatus je Kategorie sichtbar; Detailseite folgt.') },
    39|    { path: '/planning/budget/income/planned', name: 'budget-income-planned', component: BudgetIncomePage, meta: { title: 'Geplante Einnahmen' } },
    40|    { path: '/planning/budget/income/actual', name: 'budget-income-actual', component: BudgetIncomePage, meta: { title: 'Effektive Einnahmen' } },
    41|    { path: '/planning/budget/income/forecast', name: 'budget-income-forecast', component: BudgetIncomePage, meta: { title: 'Einkommen Prognose' } },
    42|    { path: '/planning/budget/expenses/actual', name: 'budget-expenses-actual', component: BudgetTransactionsPage, meta: { title: 'Effektive Ausgaben' } },
    43|    { path: '/planning/budget/expenses/manual', name: 'budget-expenses-manual', component: BudgetTransactionsPage, meta: { title: 'Manuelle Ausgabe' } },
    44|    { path: '/planning/budget/expenses/review', name: 'budget-expense-review', component: BudgetImportReviewPage, meta: { title: 'Buchungen prüfen' } },
    45|    { path: '/planning/budget/import-review', redirect: '/planning/budget/expenses/review' },
    46|    { path: '/planning/budget/category-assignment', redirect: '/planning/budget/expenses/review' },
    47|    { path: '/planning/budget/analysis/budget-vs-actual', name: 'budget-vs-actual', component: BudgetOverviewPage, meta: { title: 'Budget vs Ist' } },
    48|    { path: '/planning/budget/analysis/categories', name: 'budget-category-analysis-later', ...later('Kategorienvergleich', 'Kategorienvergleich ist Roadmap, kein Fake-Inhalt.') },
    49|    { path: '/planning/budget/analysis/monthly', name: 'budget-monthly-analysis-later', ...later('Monatsverlauf', 'Monatsverlauf folgt nach stabiler Budgetbasis.') },
    50|    { path: '/planning/budget/analysis/forecast', name: 'budget-forecast', component: BudgetOverviewPage, meta: { title: 'Prognose' } },
    51|    { path: '/planning/budget/analysis/traffic-lights', name: 'budget-traffic-lights', component: BudgetOverviewPage, meta: { title: 'Ampeln' } },
    52|    { path: '/planning/budget/imports', name: 'budget-imports-later', ...later('Imports, später', 'VISA/Migros/Cumulus-Massenimporte bleiben später; aktuell nur kontrollierter Review-Seed.') },
    53|    { path: '/planning/budget/rules', name: 'budget-rules-later', ...later('Regeln, später', 'Keine grosse Rule Engine; nur MVP-light Vorschläge im Import Review.') },
    54|    { path: '/planning/budget/fixed-costs', name: 'budget-fixed-costs-later', ...later('Fixkosten, später', 'Recurring Candidates bleiben Review-/Roadmap-Objekte.') },
    55|    { path: '/planning/budget/duplicates', name: 'budget-duplicates-later', ...later('Duplikate, später', 'Minimale Duplikaterkennung existiert im Import Review; Detailmodul folgt später.') },
    56|    { path: '/planning/budget/migros-cumulus', name: 'budget-migros-later', ...later('Migros/Cumulus Details', 'Details folgen nach kontrolliertem Import-Review.') },
    57|    { path: '/planning/budget/visa-import', name: 'budget-visa-later', ...later('VISA Import', 'Kein VISA-Massenimport ohne Dry Run/Review.') },
    58|    { path: '/planning/finance', name: 'finance-later', ...later('Finanzplanung, später', 'Langfristige Planung, Ziele und Szenarien folgen später.') },
    59|    { path: '/analysis/performance', name: 'performance-later', ...later('Performance, später', 'Performancevergleich wird nicht in diesem Sprint gebaut.') },
    60|    { path: '/analysis/contribution', name: 'contribution-later', ...later('Contribution, später', 'Gewinnbeitrag / Contribution ist Roadmap, kein Fake-Inhalt.') },
    61|    { path: '/analysis/backtest', name: 'backtest-later', ...later('Backtest, später', 'Backtests sind Roadmap-Punkte.') },
    62|    { path: '/analysis/analysts', name: 'analysts-later', ...later('Analysten, später', 'Analystenbewertungen folgen später ohne Provider-Abruf beim Rendern.') },
    63|    { path: '/planning/goals', name: 'goals-later', ...later('Ziele, später', 'Finanzielle Ziele werden separat geplant.') }
    64|  ]
    65|})
    66|export default router
    67|


1|<template>
     2|
     3|

JARVIS

Finance

     4|
     5|      <RouterLink v-for="item in mainItems" :key="item.to" :to="item.to" class="nav-link" active-class="bg-cyan-500/15 text-cyan-200">{{ item.label }}</RouterLink>
     6|      <template v-for="group in budgetGroups" :key="group.title">
     7|
{{ group.title }}

     8|        <RouterLink v-for="item in group.items" :key="item.to" :to="item.to" class="nav-link text-slate-400" active-class="bg-cyan-500/15 text-cyan-200">{{ item.label }}</RouterLink>
     9|      </template>
    10|
    11|
Read-only User Dashboard

Lokales User Dashboard

Arbeitsseiten: Preview/Confirm/Audit

Provider beim Rendern: nein

    12|
    13|</template>
    14|
    23|
    24|


1|<template>
     2|

     3|
     4|

     5|
Ausgaben · bestätigte Budget-Transaktionen

     6|
{{ isManual ? 'Manuelle Ausgabe' : 'Effektive Ausgaben' }}

     7|
Wofür ist diese Seite? Hier siehst und bearbeitest du bestätigte echte Budget-Transaktionen vom Typ Ausgabe. Import-Kandidaten gehören in Import Review.

     8|
Arbeitsmodus: Änderungen erst nach Bestätigung, Beträge nur via Adjustment/Reversal

     9|

    10|
<button data-test="new-expense" class="btn-primary" @click="startExpense">Ausgabe erfassen</button>

    11|
    12|
    13|

    14|      <input v-model="filters.month" class="field" placeholder="Zeitraum YYYY-MM" @change="loadTransactions" />
    15|      <select v-model="filters.account_id" class="field" @change="loadTransactions"><option value="">Alle Konten</option><option v-for="a in accounts" :key="a.budget_account_id" :value="a.budget_account_id">{{ a.name }}</option></select>
    16|      <select v-model="filters.category_id" class="field" @change="loadTransactions"><option value="">Alle Kategorien</option><option value="__uncategorized">Unkategorisiert</option><option v-for="c in flatCategories" :key="c.category_id" :value="c.category_id">{{ c.name }}</option></select>
    17|      <input v-model="filters.tag" class="field" placeholder="Tag" @keyup.enter="loadTransactions" />
    18|      <input v-model="filters.source_type" class="field" placeholder="Quelle" @keyup.enter="loadTransactions" />
    19|      <input v-model="filters.merchant" class="field" placeholder="Händler" @keyup.enter="loadTransactions" />
    20|      <input v-model="filters.search" class="field" placeholder="Suche" @keyup.enter="loadTransactions" />
    21|

    22|
    23|

    24|
Manuelle Ausgabe erfassen

    25|

    26|        <select v-model="form.account_id" data-test="transaction-account" class="field"><option value="">Konto wählen</option><option v-for="a in accounts" :key="a.budget_account_id" :value="a.budget_account_id">{{ a.name }}</option></select>
    27|        <input v-model="form.amount_original" data-test="transaction-amount" class="field" placeholder="Betrag" />
    28|        <input v-model="form.currency_original" class="field" placeholder="CHF" />
    29|        <input v-model="form.transaction_date" class="field" type="date" />
    30|        <input v-model="form.description" data-test="transaction-description" class="field" placeholder="Beschreibung / Händler" />
    31|        <select v-model="form.category_id" data-test="transaction-category" class="field"><option value="">Kategorie wählen</option><option v-for="c in flatCategories" :key="c.category_id" :value="c.category_id">{{ c.name }}</option></select>
    32|        <input v-model="form.notes" class="field md:col-span-3" placeholder="Notiz optional" />
    33|

    34|
<button data-test="preview-transaction" class="btn-primary" @click="preview">Preview / Zusammenfassung prüfen</button><button v-if="previewResult" data-test="confirm-transaction" class="btn-primary" @click="confirm">Confirm / final speichern</button>

    35|

Zusammenfassung prüfen

{{ previewResult.summary }}

FX: {{ previewResult.fx_status || 'n/a' }}

{{ w }}

    36|
{{ message }}

    37|

    38|
    39|

Datum	Konto	Händler/Beschreibung	Kategorie	Tags	Betrag	Währung	CHF-Wert	Quelle	Status

{{ tx.transaction_date }}	{{ tx.account_name }}	{{ tx.description }}	{{ tx.category_name || 'Review nötig' }}	—	{{ tx.amount_original }}	{{ tx.currency_original }}	{{ tx.amount_chf || '—' }}	{{ tx.source_type || '—' }}	{{ tx.status }}

Keine bestätigten Ausgaben für diese Filter.

    40|
    41|
    42|      <button class="float-right text-slate-300" @click="selectedTx=null">Schließen</button>
    43|
Ausgaben-Detail

    44|
{{ selectedTx.description }}

    45|

Datum

{{ selectedTx.transaction_date }}

Betrag

{{ selectedTx.currency_original }} {{ selectedTx.amount_original }}

CHF-Wert

{{ selectedTx.amount_chf || '—' }}

Status

{{ selectedTx.status }}

    46|

    47|        <select v-model="edit.category_id" class="field" data-test="expense-category-select"><option value="">Unkategorisiert</option><option v-for="c in flatCategories" :key="c.category_id" :value="c.category_id">{{ c.name }}</option></select>
    48|        <input v-model="edit.description" class="field" />
    49|        <input v-model="edit.tag_names" class="field" placeholder="Tags, kommagetrennt" />
    50|        <textarea v-model="edit.change_note" class="field" placeholder="Begründung / Notiz für Audit"></textarea>
    51|

    52|
<button class="btn-primary" @click="saveSelected">Kategorie/Tags mit Audit speichern</button><button class="btn" @click="reverseInfo">Stornieren/Rückgängig vorbereiten</button><button class="btn" @click="adjustmentInfo">Adjustment statt Betrag überschreiben</button>

    53|
Audit/Verlauf: Änderungen schreiben Audit-Events. Betragsänderungen werden nicht still überschrieben.

    54|
    55|

    56|</template>
    57|
    80|
    81|


Explanation:
**Updating router settings**

I realize I need to update the router's import settings. While I'm at it, I think it would be wise to inspect the BudgetOverview and Income as well. I want to ensure they’re functioning smoothly.  It’s probably good to double-check those areas for any issues since managing them carefully is important. I want everything to be lined up correctly. Let's make sure I get that done!


1|<template>
     2|

     3|
     4|
Budget Modul Phase 1.4

     5|
Budgetstatus nach Kategorie

     6|
Plan, Ist, Prognose und Ampel je JARVIS-Kategorie. Zeile anklicken öffnet Buchungsdetails.

     7|
     8|
     9|
{{ error }}

    10|
{{ message }}

    11|
    12|

    13|

    14|
{{ card.label }}

    15|
{{ card.value }}

    16|
{{ card.hint }}

    17|

    18|

    19|
    20|

    21|

    22|

    23|
Budgetstatus nach Kategorie

    24|
Monats- und Jahresbudget stehen in einer Zeile. Keine Scheingenauigkeit: ohne Ist-Daten bleibt der Status leer/neutral.

    25|

    26|        {{ overview?.current_month || '—' }}
    27|

    28|

    29|

    30|

    31|

    32|              Kategorie
    33|              Budget Monat
    34|              Budget Jahr
    35|              Ist Jahr bisher
    36|              Ist Ø Monat
    37|              Prognose Jahr
    38|              Prognose Ø Monat
    39|              Abweichung
    40|              Status
    41|

    42|

    43|

    44|

    45|              {{ row.category }}{{ row.category_type }}
    46|              CHF {{ row.budget_month }}
    47|              CHF {{ row.budget_year }}
    48|              CHF {{ row.actual_year_to_date }}
    49|              CHF {{ row.actual_avg_month }}
    50|              CHF {{ row.forecast_year }}
    51|              CHF {{ row.forecast_avg_month }}
    52|              CHF {{ row.deviation_year }}
    53|              {{ row.status }}
    54|

    55|
Noch keine bestätigten Budgetpläne.

    56|

    57|

    58|

    59|

    60|
    61|

    62|

    63|
Review-Hinweise

    64|

    65|

Offene Kategorien ohne Ist-Werte

{{ overview?.open_categories_without_actuals ?? 0 }}

    66|

Seed Review offen

{{ overview?.pending_seed_candidate_count ?? 0 }}

    67|

Import Review offen

{{ overview?.pending_transaction_candidate_count ?? 0 }}

    68|

    69|

    70|

    71|
Letzte Transaktionen

    72|

    73|
{{ tx.transaction_date }} · {{ tx.description }}{{ tx.currency_original }} {{ tx.amount_original }}

    74|
Noch keine Buchungen.

    75|

    76|

    77|

    78|
    79|
    80|

    81|

    82|
Kategorie-Details

    83|
{{ selected.category }}

    84|
Buchungen filtern und Kategorie/Tags/Beschreibung korrigieren. Betragskorrekturen nur via Adjustment/Reversal.

    85|

    86|        <button class="rounded-lg border border-slate-700 px-3 py-2 text-sm text-slate-200" @click="selected = null">Schließen</button>
    87|

    88|

    89|        <input v-model="filters.month" placeholder="Monat YYYY-MM" class="rounded-lg border border-slate-700 bg-slate-900 px-3 py-2 text-sm text-white" />
    90|        <input v-model="filters.account_id" placeholder="Konto-ID" class="rounded-lg border border-slate-700 bg-slate-900 px-3 py-2 text-sm text-white" />
    91|        <input v-model="filters.merchant" placeholder="Händler" class="rounded-lg border border-slate-700 bg-slate-900 px-3 py-2 text-sm text-white" />
    92|        <input v-model="filters.tag" placeholder="Tag" class="rounded-lg border border-slate-700 bg-slate-900 px-3 py-2 text-sm text-white" />
    93|        <select v-model="filters.status" class="rounded-lg border border-slate-700 bg-slate-900 px-3 py-2 text-sm text-white"><option value="">Status</option><option>confirmed</option><option>reversed</option></select>
    94|

    95|      <button class="mt-3 rounded-lg bg-cyan-500 px-4 py-2 text-sm font-semibold text-slate-950" @click="loadTransactions">Filtern</button>
    96|

    97|

    98|
{{ tx.transaction_date }} · {{ tx.description }}{{ tx.currency_original }} {{ tx.amount_original }}

    99|

   100|            <select v-model="edits[tx.budget_transaction_id].category_id" class="rounded-lg border border-slate-700 bg-slate-950 px-3 py-2 text-sm text-white">
   101|              <option value="">Unkategorisiert</option>
   102|              <option v-for="cat in flatCategories" :key="cat.category_id" :value="cat.category_id">{{ cat.name }}</option>
   103|            </select>
   104|            <input v-model="edits[tx.budget_transaction_id].description" class="rounded-lg border border-slate-700 bg-slate-950 px-3 py-2 text-sm text-white" />
   105|            <input v-model="edits[tx.budget_transaction_id].tag_names" placeholder="Tags, kommagetrennt" class="rounded-lg border border-slate-700 bg-slate-950 px-3 py-2 text-sm text-white" />
   106|

   107|          <button class="mt-3 rounded-lg border border-emerald-500/60 px-3 py-2 text-xs font-semibold text-emerald-200" @click="saveTransaction(tx.budget_transaction_id)">Änderung mit Audit speichern</button>
   108|

   109|
Keine Buchungen für diese Filter.

   110|

   111|
   112|

   113|</template>
   114|
   165|


1|<template>
     2|

     3|
     4|
Einkommen

     5|
Einkommen · Plan, Ist & Prognose

     6|
Geplante Einnahmen aus Budgetplänen plus bestätigte effektive Einnahmen.

     7|
     8|
{{ error }}

     9|

    10|

{{ card.label }}

{{ card.value }}

    11|

    12|

    13|
Einkommenskategorien

    14|

    15|
{{ row.category }}Monat CHF {{ row.budget_month }}Jahr CHF {{ row.budget_year }}Ist CHF {{ row.actual_year_to_date }}Prognose CHF {{ row.forecast_year }}{{ row.status }}

    16|
Noch keine Income-Budgetpläne.

    17|

    18|

    19|

    20|</template>
    21|
    36|


     1|import { apiGet, apiPost } from './client'
     2|
     3|export type BudgetAccount = { budget_account_id: string; name: string; account_type: string; currency: string; is_active: boolean; linked_account_id?: string | null; archived_at?: string | null; notes?: string | null }
     4|export type BudgetCategory = { category_id: string; parent_category_id?: string | null; name: string; category_type: string; is_active: boolean; color?: string | null; icon?: string | null; transaction_count?: number; expense_sum_chf?: string | number; active_plan_count?: number; children?: BudgetCategory[] }
     5|export type BudgetTag = { tag_id: string; name: string; color?: string | null; is_active: boolean }
     6|export type BudgetTransaction = { budget_transaction_id: string; account_id?: string; account_name?: string; category_id?: string | null; category_name?: string | null; transaction_date: string; transaction_type: string; description: string; amount_original: string; currency_original: string; amount_chf?: string | null; fx_status?: string; source_type?: string; status: string }
     7|export type BudgetPreview = { preview_id: string; summary: string; fx_status?: string; warnings: string[]; payload: Record<string, unknown> }
     8|export type BudgetConfirm = { status: string; entity_id: string; audit_id: string; message: string }
     9|export type BudgetSeedCandidate = {
    10|  seed_candidate_id: string
    11|  source_file_label?: string
    12|  source_sheet: string
    13|  source_row_or_range?: string | null
    14|  candidate_type: 'category' | 'budget_plan' | 'recurring_candidate' | 'unclean_range'
    15|  source_label?: string | null
    16|  proposed_category_id?: string | null
    17|  proposed_parent_label?: string | null
    18|  proposed_name?: string | null
    19|  proposed_period_type?: string | null
    20|  proposed_amount_text?: string | null
    21|  currency?: string | null
    22|  confidence: string
    23|  requires_review: boolean
    24|  status: string
    25|  notes?: string | null
    26|}
    27|export type BudgetSeedSummary = { category_candidate_count: number; budget_plan_candidate_count: number; recurring_candidate_count: number; unclean_range_count: number; current_budget_sheet?: string }
    28|export type BudgetStatusRow = { category_id: string; category: string; category_type: string; budget_month: string; budget_year: string; actual_year_to_date: string; actual_avg_month: string; forecast_year: string; forecast_avg_month: string; deviation_year: string; deviation_month: string; status: string; months_with_bookings: number; plan_names?: string[] }
    29|export type BudgetTransactionCandidate = { transaction_candidate_id: string; source_file_label: string; source_row_or_range?: string | null; source_type: string; transaction_date?: string | null; description: string; merchant?: string | null; amount_original?: string | null; currency_original: string; proposed_category_id?: string | null; proposed_category_name?: string | null; duplicate_of_transaction_id?: string | null; confidence: string; requires_review: boolean; status: string; notes?: string | null; classification?: string | null; review_reason?: string | null; rule_id?: string | null; rule_name?: string | null; account_source?: string | null; covered_by_source?: string | null }
    30|export type BudgetReviewSummary = { open_total: number; auto_categorized: number; review_needed: number; galaxus_digitec: number; migros_over_50: number; covered_by_migros: number; unclear: number; possible_duplicates: number }
    31|export type BudgetRule = { rule_id: string; merchant_contains: string; source_type?: string | null; category_id?: string | null; category_name?: string | null; target_status: string; is_active: boolean }
    32|export type BudgetOverview = { current_month: string; income_chf: string; expense_chf: string; net_cashflow_chf: string; review_count: number; uncategorized_count: number; pending_seed_candidate_count?: number; pending_transaction_candidate_count?: number; planned_income_chf?: string; planned_expense_chf?: string; planned_net_chf?: string; actual_income_chf?: string; actual_expense_chf: string; actual_net_chf?: string; actuals_label?: string; open_categories_without_actuals?: number; planned_budget_chf: string; remaining_budget_chf: string; budget_status: string; budget_categories: BudgetStatusRow[]; uncategorized_transactions: BudgetTransaction[]; top_categories: Array<{ category_id?: string; category_name: string; amount_chf: string; transaction_count?: number }>; recent_transactions: BudgetTransaction[] }
    33|
    34|export const getBudgetOverview = () => apiGet<BudgetOverview>('/api/budget/overview')
    35|export const getBudgetStatusByCategory = (year = '2026') => apiGet<BudgetStatusRow[]>(`/api/budget/status/categories?year=${year}`)
    36|export const getIncomeStatus = (year = '2026') => apiGet<Record<string, unknown>>(`/api/budget/income/status?year=${year}`)
    37|export const getBudgetTransactions = (query = '') => apiGet<BudgetTransaction[]>(`/api/budget/transactions${query}`)
    38|export const getBudgetAccounts = () => apiGet<BudgetAccount[]>('/api/budget/accounts')
    39|export const getBudgetCategories = () => apiGet<BudgetCategory[]>('/api/budget/categories')
    40|export const getBudgetTags = () => apiGet<BudgetTag[]>('/api/budget/tags')
    41|export const previewBudgetTransaction = (body: unknown) => apiPost<BudgetPreview>('/api/budget/transactions/preview', body)
    42|export const confirmBudgetTransaction = (body: unknown) => apiPost<BudgetConfirm>('/api/budget/transactions/confirm', body)
    43|export const updateBudgetTransaction = (id: string, body: unknown) => apiPost<BudgetConfirm>(`/api/budget/transactions/${id}/update`, body)
    44|export const previewBudgetTransfer = (body: unknown) => apiPost<BudgetPreview>('/api/budget/transfers/preview', body)
    45|export const confirmBudgetTransfer = (body: unknown) => apiPost<BudgetConfirm>('/api/budget/transfers/confirm', body)
    46|export const previewBudgetAccount = (body: unknown) => apiPost<BudgetPreview>('/api/budget/accounts/preview', body)
    47|export const confirmBudgetAccount = (body: unknown) => apiPost<BudgetConfirm>('/api/budget/accounts/confirm', body)
    48|export const archiveBudgetAccount = (id: string) => apiPost<BudgetConfirm>(`/api/budget/accounts/${id}/archive`, {})
    49|export const previewBudgetCategory = (body: unknown) => apiPost<BudgetPreview>('/api/budget/categories/preview', body)
    50|export const confirmBudgetCategory = (body: unknown) => apiPost<BudgetConfirm>('/api/budget/categories/confirm', body)
    51|export const updateBudgetCategory = (id: string, body: unknown) => apiPost<BudgetConfirm>(`/api/budget/categories/${id}/update`, body)
    52|export const archiveBudgetCategory = (id: string) => apiPost<BudgetConfirm>(`/api/budget/categories/${id}/archive`, {})
    53|export const previewBudgetTag = (body: unknown) => apiPost<BudgetPreview>('/api/budget/tags/preview', body)
    54|export const confirmBudgetTag = (body: unknown) => apiPost<BudgetConfirm>('/api/budget/tags/confirm', body)
    55|export const archiveBudgetTag = (id: string) => apiPost<BudgetConfirm>(`/api/budget/tags/${id}/archive`, {})
    56|export const getBudgetSeedCandidates = () => apiGet<BudgetSeedCandidate[]>('/api/budget/seed-candidates')
    57|export const generateBudgetSeedCandidates = (body: unknown) => apiPost<BudgetSeedSummary>('/api/budget/seed-candidates/generate', body)
    58|export const previewBudgetSeedCandidate = (id: string, body: unknown) => apiPost<BudgetPreview>(`/api/budget/seed-candidates/${id}/preview`, body)
    59|export const confirmBudgetSeedCandidate = (id: string, body: unknown) => apiPost<BudgetConfirm>(`/api/budget/seed-candidates/${id}/confirm`, body)
    60|export const ignoreBudgetSeedCandidate = (id: string, note = '') => apiPost<BudgetConfirm>(`/api/budget/seed-candidates/${id}/ignore`, { note })
    61|export const reopenBudgetSeedCandidate = (id: string, note = '') => apiPost<BudgetConfirm>(`/api/budget/seed-candidates/${id}/reopen`, { note })
    62|export const setBudgetSeedCandidateStatus = (id: string, status: string, note = '') => apiPost<BudgetConfirm>(`/api/budget/seed-candidates/${id}/status`, { status, note })
    63|export const convertBudgetUncleanRange = (id: string, body: unknown) => apiPost<BudgetConfirm>(`/api/budget/seed-candidates/${id}/convert-unclean`, body)
    64|export const previewClearBudgetCategoriesTakeover = () => apiPost<{ preview_id: string; summary: string; count: number; categories: Array<Record<string, unknown>>; warnings: string[] }>('/api/budget/seed-candidates/categories/preview-clear-takeover', {})
    65|export const confirmClearBudgetCategoriesTakeover = () => apiPost<BudgetConfirm & { confirmed_count: number; skipped_count: number }>('/api/budget/seed-candidates/categories/confirm-clear-takeover', {})
    66|export const previewBudgetPlanTakeover = () => apiPost<{ preview_id: string; summary: string; count: number; budget_plans: Array<Record<string, unknown>>; warnings: string[] }>('/api/budget/seed-candidates/budget-plans/preview-takeover', {})
    67|export const confirmBudgetPlanTakeover = () => apiPost<BudgetConfirm & { confirmed_count: number; skipped_count: number }>('/api/budget/seed-candidates/budget-plans/confirm-takeover', {})
    68|export const getBudgetTransactionCandidates = (status = '', tab = '', filters: Record<string, string> = {}) => {
    69|  const params = new URLSearchParams()
    70|  if (status) params.set('status', status)
    71|  if (tab) params.set('tab', tab)
    72|  Object.entries(filters).forEach(([k, v]) => { if (v) params.set(k, v) })
    73|  const qs = params.toString()
    74|  return apiGet<BudgetTransactionCandidate[]>(`/api/budget/transaction-candidates${qs ? `?${qs}` : ''}`)
    75|}
    76|export const getBudgetTransactionCandidateSummary = () => apiGet<BudgetReviewSummary>('/api/budget/transaction-candidates/summary')
    77|export const generateBudgetTransactionCandidates = (body: unknown) => apiPost<Record<string, number>>('/api/budget/transaction-candidates/generate', body)
    78|export const previewBudgetTransactionCandidate = (id: string, body: unknown) => apiPost<BudgetPreview>(`/api/budget/transaction-candidates/${id}/preview`, body)
    79|export const confirmBudgetTransactionCandidate = (id: string, body: unknown) => apiPost<BudgetConfirm>(`/api/budget/transaction-candidates/${id}/confirm`, body)
    80|export const ignoreBudgetTransactionCandidate = (id: string, note = '') => apiPost<BudgetConfirm>(`/api/budget/transaction-candidates/${id}/ignore`, { note })
    81|export const reopenBudgetTransactionCandidate = (id: string, note = '') => apiPost<BudgetConfirm>(`/api/budget/transaction-candidates/${id}/reopen`, { note })
    82|export const updateBudgetTransactionCandidateCategory = (id: string, category_id: string, note = '') => apiPost<BudgetConfirm>(`/api/budget/transaction-candidates/${id}/category`, { category_id, note })
    83|export const bulkUpdateBudgetTransactionCandidateCategory = (candidate_ids: string[], category_id: string, note = '') => apiPost<{ updated_count: number; skipped_count: number }>('/api/budget/transaction-candidates/bulk-category', { candidate_ids, category_id, note })
    84|export const splitBudgetTransactionCandidate = (id: string, splits: unknown[]) => apiPost<BudgetConfirm & { split_count: number }>(`/api/budget/transaction-candidates/${id}/split`, { splits })
    85|export const markBudgetTransactionCandidateTransfer = (id: string, note = '') => apiPost<BudgetConfirm>(`/api/budget/transaction-candidates/${id}/transfer`, { note })
    86|export const previewSafeBudgetTransactionCandidates = () => apiPost<{ preview_id: string; summary: string; count: number; groups: Array<Record<string, unknown>>; warnings: string[] }>('/api/budget/transaction-candidates/batch-confirm-safe/preview', {})
    87|export const batchConfirmSafeBudgetTransactionCandidates = (account_id: string, candidate_ids: string[] = []) => apiPost<{ confirmed_count: number; skipped_count: number }>('/api/budget/transaction-candidates/batch-confirm-safe', { account_id, candidate_ids })
    88|export const deleteEmptyBudgetCategory = (id: string) => apiPost<BudgetConfirm>(`/api/budget/categories/${id}/delete-empty`, {})
    89|export const previewBudgetCategoryMerges = () => apiPost<{ preview_id: string; summary: string; suggestions: Array<{ target_category_id: string; target_name: string; source_category_ids: string[]; source_names: string[] }>; warnings: string[] }>('/api/budget/categories/merge/preview', {})
    90|export const confirmBudgetCategoryMerge = (body: unknown) => apiPost<BudgetConfirm & { moved_transactions: number; moved_budget_plans: number; moved_children: number }>('/api/budget/categories/merge/confirm', body)
    91|export const previewBatchBudgetTransactionCandidates = (candidate_ids: string[], account_id: string) => apiPost<{ preview_id: string; count: number; categories: Record<string, number>; conflicts: Array<Record<string, unknown>>; requires_explicit_confirm: boolean }>('/api/budget/transaction-candidates/batch/preview', { candidate_ids, account_id })
    92|export const batchBudgetTransactionCandidates = (body: unknown) => apiPost<Record<string, unknown>>('/api/budget/transaction-candidates/batch', body)
    93|export const confirmBudgetTransactionCandidateSplit = (id: string, body: unknown) => apiPost<Record<string, unknown>>(`/api/budget/transaction-candidates/${id}/split/confirm`, body)
    94|export const createReviewCategoryForCandidate = (id: string, body: unknown) => apiPost<BudgetConfirm & { category_id: string }>(`/api/budget/transaction-candidates/${id}/category/create`, body)
    95|export const getBudgetRules = () => apiGet<BudgetRule[]>('/api/budget/rules')
    96|export const createBudgetRule = (body: unknown) => apiPost<Record<string, unknown>>('/api/budget/rules', body)
    97|export const deactivateBudgetRule = (id: string) => apiPost<Record<string, unknown>>(`/api/budget/rules/${id}/deactivate`, {})
    98|export const testBudgetRule = (id: string) => apiPost<Record<string, unknown>>(`/api/budget/rules/${id}/test`, {})
    99|export const applyBudgetRule = (id: string) => apiPost<Record<string, unknown>>(`/api/budget/rules/${id}/apply`, {})
   100|


Explanation:
**Updating routes and summaries**

I need to modify the router import and adjust the routes. I’m thinking about moving sidebar rules from "Später" to maybe "Setup." It seems effective, but there’s a summary that’s missing, and the audit isn’t reflecting the actual situation. I probably need to make tests less stringent and ensure I build everything correctly. Also, I should update the BudgetTransactions summary and the BudgetIncome message. It feels like there's a lot to tackle!

import BudgetIncomePage from '@/pages/BudgetIncomePage.vue'
import BudgetRulesPage from '@/pages/BudgetRulesPage.vue'


--- a//home/agent/.hermes/repos/FinanceManager/frontend/src/router/index.ts
+++ b//home/agent/.hermes/repos/FinanceManager/frontend/src/router/index.ts
@@ -14,6 +14,7 @@
 import BudgetSetupPage from '@/pages/BudgetSetupPage.vue'
 import BudgetImportReviewPage from '@/pages/BudgetImportReviewPage.vue'
 import BudgetIncomePage from '@/pages/BudgetIncomePage.vue'
+import BudgetRulesPage from '@/pages/BudgetRulesPage.vue'

 const later = (title: string, description: string) => ({ component: RoadmapPage, props: { title, description }, meta: { title } })
 const router = createRouter({


{ path: '/planning/budget/rules', name: 'budget-rules', component: BudgetRulesPage, meta: { title: 'Regeln' } },

{ path: '/planning/budget/rules', name: 'budget-rules-later', ...later('Regeln, später', 'Keine grosse Rule Engine; nur MVP-light Vorschläge im Import Review.') },


--- a//home/agent/.hermes/repos/FinanceManager/frontend/src/router/index.ts
+++ b//home/agent/.hermes/repos/FinanceManager/frontend/src/router/index.ts
@@ -51,7 +51,7 @@
     { path: '/planning/budget/analysis/forecast', name: 'budget-forecast', component: BudgetOverviewPage, meta: { title: 'Prognose' } },
     { path: '/planning/budget/analysis/traffic-lights', name: 'budget-traffic-lights', component: BudgetOverviewPage, meta: { title: 'Ampeln' } },
     { path: '/planning/budget/imports', name: 'budget-imports-later', ...later('Imports, später', 'VISA/Migros/Cumulus-Massenimporte bleiben später; aktuell nur kontrollierter Review-Seed.') },
-    { path: '/planning/budget/rules', name: 'budget-rules-later', ...later('Regeln, später', 'Keine grosse Rule Engine; nur MVP-light Vorschläge im Import Review.') },
+    { path: '/planning/budget/rules', name: 'budget-rules', component: BudgetRulesPage, meta: { title: 'Regeln' } },
     { path: '/planning/budget/fixed-costs', name: 'budget-fixed-costs-later', ...later('Fixkosten, später', 'Recurring Candidates bleiben Review-/Roadmap-Objekte.') },
     { path: '/planning/budget/duplicates', name: 'budget-duplicates-later', ...later('Duplikate, später', 'Minimale Duplikaterkennung existiert im Import Review; Detailmodul folgt später.') },
     { path: '/planning/budget/migros-cumulus', name: 'budget-migros-later', ...later('Migros/Cumulus Details', 'Details folgen nach kontrolliertem Import-Review.') },


  { title: 'Setup', items: [{ to: '/planning/budget/categories', label: 'Kategorien' }, { to: '/planning/budget/rules', label: 'Regeln' }, { to: '/planning/budget/tags', label: 'Tags' }, { to: '/planning/budget/accounts', label: 'Konten' }, { to: '/planning/budget/setup', label: 'Excel übernehmen / Seed Review' }] },
  { title: 'Später', items: [{ to: '/planning/budget/fixed-costs', label: 'Fixkosten' }, { to: '/planning/budget/duplicates', label: 'Duplikate' }, { to: '/planning/budget/migros-cumulus', label: 'Migros Details' }, { to: '/planning/budget/visa-import', label: 'VISA Import' }] },

  { title: 'Setup', items: [{ to: '/planning/budget/categories', label: 'Kategorien' }, { to: '/planning/budget/tags', label: 'Tags' }, { to: '/planning/budget/accounts', label: 'Konten' }, { to: '/planning/budget/setup', label: 'Excel übernehmen / Seed Review' }] },
  { title: 'Später', items: [{ to: '/planning/budget/rules', label: 'Regeln' }, { to: '/planning/budget/fixed-costs', label: 'Fixkosten' }, { to: '/planning/budget/duplicates', label: 'Duplikate' }, { to: '/planning/budget/migros-cumulus', label: 'Migros Details' }, { to: '/planning/budget/visa-import', label: 'VISA Import' }] },