def _load_flags(row: Any) -> list[str]:
    try:
        return [str(x) for x in json.loads(row["quality_flags_json"] or "[]")]
    except Exception:
        return []

def _store_flags(flags: list[str]) -> str:
    return _json(sorted({f for f in flags if f}))

def _without_flags(flags: list[str], remove: set[str]) -> list[str]:
    return [f for f in flags if f not in remove]

def _require_note(note: str | None, message: str = "Review metadata change requires a note") -> str:
    if not note or not note.strip():
        raise ValueError(message)
    return note.strip()

def validate_isin(value: str) -> str:
    isin = (value or "").strip().upper().replace(" ", "")
    if not re.fullmatch(r"[A-Z]{2}[A-Z0-9]{9}[0-9]", isin):
        raise ValueError("Invalid ISIN format")
    return isin

def calculate_review_item_readiness(row: Any) -> str:
    status = str(row["review_status"] or "open")
    if status == "ignored":
        return "ignored"
    flags = set(_load_flags(row))
    asset_class = str(row["detected_asset_class"] or "other")
    if status == "blocked" or "aggregate_only" in flags or "missing_instrument_details" in flags:
        return "blocked"
    if asset_class in {"equity", "etf"}:
        has_identifier = bool(row["isin"]) or (bool(row["ticker"]) and bool(row["exchange"]) and int(row["ticker_exchange_confirmed"] or 0) == 1)
        required = [
            bool(row["source_platform"]),
            bool(row["mapped_account_id"]),
            asset_class in {"equity", "etf"},
            has_identifier,
            bool(row["detected_currency"]),
            (int(row["snapshot_date_confirmed"] or 0) == 1),
            (int(row["detected_quantity_present"] or 0) == 1),
            int(row["reviewer_confirmed"] or 0) == 1,
        ]
        blocking_flags = {"missing_isin", "missing_ticker", "aggregate_only", "missing_instrument_details", "missing_fx"}
        if all(required) and not (flags & blocking_flags):
            return "ready_for_import"
        return "review_needed" if has_identifier or bool(row["mapped_account_id"]) else "not_ready"
    if asset_class == "cash":
        required = [bool(row["mapped_account_id"]), bool(row["detected_currency"]), int(row["snapshot_date_confirmed"] or 0) == 1, int(row["detected_market_value_present"] or 0) == 1, int(row["reviewer_confirmed"] or 0) == 1]
        return "ready_for_import" if all(required) else "review_needed"
    return "not_ready"

def refresh_review_item_readiness(conn: Connection, review_item_id: str) -> str:
    row = conn.execute("SELECT * FROM broker_import_review_items WHERE review_item_id=?", (review_item_id,)).fetchone()
    if not row:
        raise ValueError("review item not found")
    readiness = calculate_review_item_readiness(row)
    conn.execute("UPDATE broker_import_review_items SET import_readiness_status=?, account_mapping_status=CASE WHEN mapped_account_id IS NULL OR mapped_account_id='' THEN 'missing' ELSE 'mapped' END, updated_at=? WHERE review_item_id=?", (readiness, utc_now(), review_item_id))
    return readiness

def _audit_review_change(conn: Connection, *, review_item_id: str, action: str, old: dict[str, Any], new: dict[str, Any], note: str, created_by: str = "manual_review") -> str:
    return record_audit_event(conn, source="manual_review_queue", action=action, entity_type="broker_import_review_item", entity_id=review_item_id, old_values=old, new_values=new, user_text_note=note, confirmed=True, created_by=created_by)

def update_review_item_isin(conn: Connection, *, review_item_id: str, isin: str, note: str, created_by: str = "manual_review") -> str:
    note = _require_note(note)
    isin = validate_isin(isin)
    row = conn.execute("SELECT * FROM broker_import_review_items WHERE review_item_id=?", (review_item_id,)).fetchone()
    if not row: raise ValueError("review item not found")
    flags = _without_flags(_load_flags(row), {"missing_isin", "name_only_not_auto_mapped"})
    conn.execute("UPDATE broker_import_review_items SET isin=?, quality_flags_json=?, reviewer_note=?, updated_at=? WHERE review_item_id=?", (isin, _store_flags(flags), note, utc_now(), review_item_id))
    readiness = refresh_review_item_readiness(conn, review_item_id)
    return _audit_review_change(conn, review_item_id=review_item_id, action="update_isin", old={"isin": row["isin"], "quality_flags": _load_flags(row)}, new={"isin": isin, "quality_flags": flags, "readiness": readiness}, note=note, created_by=created_by)

def update_review_item_ticker_exchange(conn: Connection, *, review_item_id: str, ticker: str | None = None, exchange: str | None = None, note: str, confirm_ticker_exchange: bool = False, created_by: str = "manual_review") -> str:
    note = _require_note(note)
    row = conn.execute("SELECT * FROM broker_import_review_items WHERE review_item_id=?", (review_item_id,)).fetchone()
    if not row: raise ValueError("review item not found")
    new_ticker = (ticker or row["ticker"] or "").strip().upper() or None
    new_exchange = (exchange or row["exchange"] or "").strip().upper() or None
    flags = _load_flags(row)
    if new_ticker:
        flags = _without_flags(flags, {"missing_ticker"})
    if new_ticker and not new_exchange and "ticker_without_exchange" not in flags:
        flags.append("ticker_without_exchange")
    if new_ticker and new_exchange:
        flags = _without_flags(flags, {"ticker_without_exchange"})
    confirmed = 1 if (confirm_ticker_exchange and new_ticker and new_exchange) else int(row["ticker_exchange_confirmed"] or 0)
    conn.execute("UPDATE broker_import_review_items SET ticker=?, exchange=?, ticker_exchange_confirmed=?, quality_flags_json=?, reviewer_note=?, updated_at=? WHERE review_item_id=?", (new_ticker, new_exchange, confirmed, _store_flags(flags), note, utc_now(), review_item_id))
    readiness = refresh_review_item_readiness(conn, review_item_id)
    return _audit_review_change(conn, review_item_id=review_item_id, action="update_ticker_exchange", old={"ticker": row["ticker"], "exchange": row["exchange"]}, new={"ticker": new_ticker, "exchange": new_exchange, "ticker_exchange_confirmed": confirmed, "readiness": readiness}, note=note, created_by=created_by)

def confirm_review_item_instrument_mapping(conn: Connection, *, review_item_id: str, instrument_id: str | None = None, note: str, confirm_ticker_exchange: bool = False, created_by: str = "manual_review") -> str:
    note = _require_note(note)
    row = conn.execute("SELECT * FROM broker_import_review_items WHERE review_item_id=?", (review_item_id,)).fetchone()
    if not row: raise ValueError("review item not found")
    if not row["isin"] and not (row["ticker"] and row["exchange"] and confirm_ticker_exchange):
        raise ValueError("Name-only rows cannot be marked as mapped")
    if not instrument_id:
        instrument_id = _first_matching_instrument(conn, isin=row["isin"], ticker=row["ticker"], exchange=row["exchange"], currency=row["detected_currency"])
    if not instrument_id:
        raise ValueError("instrument_id required when no existing instrument matches")
    flags = _without_flags(_load_flags(row), {"missing_isin", "missing_ticker", "ticker_without_exchange", "name_only_not_auto_mapped"})
    conn.execute("UPDATE broker_import_review_items SET mapped_instrument_id=?, review_status='mapped', ticker_exchange_confirmed=?, reviewer_confirmed=1, quality_flags_json=?, reviewer_note=?, updated_at=? WHERE review_item_id=?", (instrument_id, 1 if confirm_ticker_exchange else int(row["ticker_exchange_confirmed"] or 0), _store_flags(flags), note, utc_now(), review_item_id))
    readiness = refresh_review_item_readiness(conn, review_item_id)
    return _audit_review_change(conn, review_item_id=review_item_id, action="confirm_instrument_mapping", old={"review_status": row["review_status"], "mapped_instrument_id": row["mapped_instrument_id"]}, new={"review_status": "mapped", "mapped_instrument_id": instrument_id, "readiness": readiness}, note=note, created_by=created_by)

def confirm_review_item_account_mapping(conn: Connection, *, review_item_id: str, account_id: str, note: str, created_by: str = "manual_review") -> str:
    note = _require_note(note)
    row = conn.execute("SELECT * FROM broker_import_review_items WHERE review_item_id=?", (review_item_id,)).fetchone()
    acct = conn.execute("SELECT account_id, account_type FROM accounts WHERE account_id=? AND is_active=1", (account_id,)).fetchone()
    if not row: raise ValueError("review item not found")
    if not acct: raise ValueError("account not found")
    if row["detected_asset_class"] == "cash" and acct["account_type"] not in {"cash", "reserve", "other"}:
        raise ValueError("cash review items must map to cash/reserve/other accounts")
    if row["detected_asset_class"] in {"equity", "etf"} and acct["account_type"] == "cash":
        raise ValueError("brokerage review items must not map to cash-only accounts")
    conn.execute("UPDATE broker_import_review_items SET mapped_account_id=?, account_mapping_status='mapped', reviewer_note=?, updated_at=? WHERE review_item_id=?", (account_id, note, utc_now(), review_item_id))
    readiness = refresh_review_item_readiness(conn, review_item_id)
    return _audit_review_change(conn, review_item_id=review_item_id, action="confirm_account_mapping", old={"mapped_account_id": row["mapped_account_id"]}, new={"mapped_account_id": account_id, "readiness": readiness}, note=note, created_by=created_by)

def confirm_review_item_snapshot_date(conn: Connection, *, review_item_id: str, note: str, created_by: str = "manual_review") -> str:
    note = _require_note(note)
    row = conn.execute("SELECT * FROM broker_import_review_items WHERE review_item_id=?", (review_item_id,)).fetchone()
    if not row: raise ValueError("review item not found")
    conn.execute("UPDATE broker_import_review_items SET snapshot_date_confirmed=1, reviewer_note=?, updated_at=? WHERE review_item_id=?", (note, utc_now(), review_item_id))
    readiness = refresh_review_item_readiness(conn, review_item_id)
    return _audit_review_change(conn, review_item_id=review_item_id, action="confirm_snapshot_date", old={"snapshot_date_confirmed": row["snapshot_date_confirmed"]}, new={"snapshot_date_confirmed": 1, "readiness": readiness}, note=note, created_by=created_by)

def ignore_review_item(conn: Connection, *, review_item_id: str, note: str, created_by: str = "manual_review") -> str:
    note = _require_note(note, "Ignore requires a note")
    row = conn.execute("SELECT * FROM broker_import_review_items WHERE review_item_id=?", (review_item_id,)).fetchone()
    if not row: raise ValueError("review item not found")
    conn.execute("UPDATE broker_import_review_items SET review_status='ignored', import_readiness_status='ignored', reviewer_note=?, updated_at=? WHERE review_item_id=?", (note, utc_now(), review_item_id))
    return _audit_review_change(conn, review_item_id=review_item_id, action="ignore_review_item", old={"review_status": row["review_status"]}, new={"review_status": "ignored", "readiness": "ignored"}, note=note, created_by=created_by)

def mark_review_item_resolved(conn: Connection, *, review_item_id: str, note: str, created_by: str = "manual_review") -> str:
    note = _require_note(note)
    row = conn.execute("SELECT * FROM broker_import_review_items WHERE review_item_id=?", (review_item_id,)).fetchone()
    if not row: raise ValueError("review item not found")
    readiness = calculate_review_item_readiness(row)
    if readiness not in {"ready_for_import", "ignored"}:
        raise ValueError("Review item cannot be resolved until mandatory issues are solved")
    conn.execute("UPDATE broker_import_review_items SET review_status='resolved', import_readiness_status=?, reviewer_note=?, updated_at=? WHERE review_item_id=?", (readiness, note, utc_now(), review_item_id))
    return _audit_review_change(conn, review_item_id=review_item_id, action="mark_resolved", old={"review_status": row["review_status"]}, new={"review_status": "resolved", "readiness": readiness}, note=note, created_by=created_by)

def get_broker_review_items(conn: Connection, *, status: str | None = None, source_platform: str | None = None, dry_run_id: str | None = None, quality_flag: str | None = None, asset_class: str | None = None, currency: str | None = None, account_mapping_status: str | None = None, readiness_status: str | None = None, limit: int = 100) -> list[dict[str, str]]:
    clauses=[]; params=[]
    for col,val in [("review_status",status),("source_platform",source_platform),("dry_run_id",dry_run_id),("detected_asset_class",asset_class),("detected_currency",currency),("account_mapping_status",account_mapping_status),("import_readiness_status",readiness_status)]:
        if val:
            clauses.append(f"{col}=?"); params.append(val)
    if quality_flag:
        clauses.append("quality_flags_json LIKE ?"); params.append(f'%"{quality_flag}"%')
    where = "WHERE " + " AND ".join(clauses) if clauses else ""
    params.append(limit)
    rows = conn.execute(f"""
        SELECT review_item_id AS item_id, dry_run_id, source_platform, source_file_type,
               source_row_ref, source_label, normalized_name, detected_asset_class,
               detected_currency, detected_quantity_present, detected_market_value_present,
               isin, ticker, exchange, mapped_instrument_id, mapped_account_id,
               account_mapping_status, quality_flags_json, review_status, import_readiness_status,
               reviewer_confirmed, snapshot_date_confirmed, ticker_exchange_confirmed,
               reviewer_note, created_at, updated_at
        FROM broker_import_review_items {where}
        ORDER BY created_at DESC LIMIT ?
        """, tuple(params)).fetchall()
    return [{k: "" if row[k] is None else str(row[k]) for k in row.keys()} for row in rows]

def get_review_item_detail(conn: Connection, review_item_id: str) -> dict[str, Any]:
    row = conn.execute("SELECT * FROM broker_import_review_items WHERE review_item_id=?", (review_item_id,)).fetchone()
    if not row: raise ValueError("review item not found")
    audit = conn.execute("SELECT timestamp, action, entity_type, entity_id, user_text_note, created_by FROM audit_log WHERE entity_id=? ORDER BY timestamp DESC", (review_item_id,)).fetchall()
    return {"item": {k: "" if row[k] is None else str(row[k]) for k in row.keys()}, "audit_history": [{k: "" if a[k] is None else str(a[k]) for k in a.keys()} for a in audit]}

def get_dry_run_sessions(conn: Connection, *, limit: int = 50) -> list[dict[str, str]]:
    rows = conn.execute("""
        SELECT d.dry_run_id, d.source_platform, d.source_file_type, d.created_at, d.snapshot_date_status,
               d.rows_total, d.candidate_positions, d.candidate_cash_rows, d.warnings_count, d.errors_count,
               COALESCE(d.session_status,'active') AS session_status, COALESCE(d.is_current,0) AS is_current,
               SUM(CASE WHEN r.review_item_id IS NOT NULL THEN 1 ELSE 0 END) AS review_items,
               SUM(CASE WHEN r.import_readiness_status='ready_for_import' THEN 1 ELSE 0 END) AS ready_for_import,
               SUM(CASE WHEN r.review_status='blocked' OR r.import_readiness_status='blocked' THEN 1 ELSE 0 END) AS blocked,
               SUM(CASE WHEN r.review_status='ignored' THEN 1 ELSE 0 END) AS ignored,
               SUM(CASE WHEN r.review_status='open' THEN 1 ELSE 0 END) AS open
        FROM broker_import_dry_runs d
        LEFT JOIN broker_import_review_items r ON r.dry_run_id=d.dry_run_id
        GROUP BY d.dry_run_id
        ORDER BY d.created_at DESC LIMIT ?
    """, (limit,)).fetchall()
    return [{k: "" if row[k] is None else str(row[k]) for k in row.keys()} for row in rows]

def update_dry_run_session_status(conn: Connection, *, dry_run_id: str, action: str, note: str, created_by: str = "manual_review") -> str:
    note = _require_note(note)
    row = conn.execute("SELECT * FROM broker_import_dry_runs WHERE dry_run_id=?", (dry_run_id,)).fetchone()
    if not row: raise ValueError("dry run not found")
    now=utc_now()
    if action == "archive":
        conn.execute("UPDATE broker_import_dry_runs SET session_status='archived', is_current=0, archived_at=?, notes=? WHERE dry_run_id=?", (now, note, dry_run_id))
        new={"session_status":"archived"}
    elif action == "discard":
        conn.execute("UPDATE broker_import_dry_runs SET session_status='discarded', is_current=0, discarded_at=?, notes=? WHERE dry_run_id=?", (now, note, dry_run_id))
        new={"session_status":"discarded"}
    elif action == "mark_current":
        conn.execute("UPDATE broker_import_dry_runs SET is_current=0 WHERE source_platform=?", (row["source_platform"],))
        conn.execute("UPDATE broker_import_dry_runs SET session_status='active', is_current=1, notes=? WHERE dry_run_id=?", (note, dry_run_id))
        new={"session_status":"active","is_current":1}
    else:
        raise ValueError("unsupported dry run session action")
    return record_audit_event(conn, source="manual_review_queue", action=f"dry_run_{action}", entity_type="broker_import_dry_run", entity_id=dry_run_id, old_values={"session_status": row["session_status"] if "session_status" in row.keys() else "active", "is_current": row["is_current"] if "is_current" in row.keys() else 0}, new_values=new, user_text_note=note, confirmed=True, created_by=created_by)

def get_review_readiness_counts(conn: Connection) -> list[dict[str, str]]:
    rows=conn.execute("SELECT import_readiness_status AS readiness_status, COUNT(*) AS count FROM broker_import_review_items GROUP BY import_readiness_status ORDER BY import_readiness_status").fetchall()
    return [{"readiness_status": row["readiness_status"] or "not_ready", "count": str(row["count"])} for row in rows]

def get_ready_for_import_by_source(conn: Connection) -> list[dict[str, str]]:
    rows=conn.execute("SELECT source_platform, SUM(CASE WHEN import_readiness_status='ready_for_import' THEN 1 ELSE 0 END) AS ready_for_import, COUNT(*) AS total FROM broker_import_review_items GROUP BY source_platform ORDER BY source_platform").fetchall()
    return [{"source_platform": row["source_platform"], "ready_for_import": str(row["ready_for_import"] or 0), "total": str(row["total"])} for row in rows]

def assert_productive_import_disabled() -> bool:
    return True


terminal

set -euo pipefail
cd /home/agent/.hermes/repos/FinanceManager
cat /tmp/b4_append.py >> src/jarvis_finance/imports/broker_mapping.py
PYTHONPATH=src python -m py_compile src/jarvis_finance/imports/broker_mapping.py src/jarvis_finance/storage/migrations.py


from pathlib import Path
p=Path('/home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/data.py')
s=p.read_text()
s=s.replace('def get_broker_dry_runs(conn: Connection, limit: int = 20) -> list[dict[str, str]]:\n    return rowdicts(conn.execute(\n        """\n        SELECT source_platform, source_file_type, detected_snapshot_date, snapshot_date_status,\n               rows_total, candidate_positions, candidate_cash_rows, mapped_positions,\n               blocked_positions, warnings_count, errors_count, quality_flags_json, created_at\n        FROM broker_import_dry_runs ORDER BY created_at DESC LIMIT ?\n        """, (limit,)\n    ).fetchall())\n', 'def get_broker_dry_runs(conn: Connection, limit: int = 20) -> list[dict[str, str]]:\n    from jarvis_finance.imports.broker_mapping import get_dry_run_sessions\n\n    return get_dry_run_sessions(conn, limit=limit)\n')
s=s.replace('def get_broker_review_items(conn: Connection, limit: int = 100) -> list[dict[str, str]]:\n    from jarvis_finance.imports.broker_mapping import get_broker_review_items as _items\n\n    return _items(conn, status=None, limit=limit)\n', '''def get_broker_review_items(conn: Connection, limit: int = 100, **filters) -> list[dict[str, str]]:\n    from jarvis_finance.imports.broker_mapping import get_broker_review_items as _items\n\n    return _items(conn, limit=limit, **filters)\n\n\ndef get_review_readiness_counts(conn: Connection) -> list[dict[str, str]]:\n    from jarvis_finance.imports.broker_mapping import get_review_readiness_counts as _counts\n\n    return _counts(conn)\n\n\ndef get_ready_for_import_by_source(conn: Connection) -> list[dict[str, str]]:\n    from jarvis_finance.imports.broker_mapping import get_ready_for_import_by_source as _counts\n\n    return _counts(conn)\n''')
# insert additional DQ checks before return items
s=s.replace('    for flag_row in get_review_quality_flag_counts(conn):\n        count = int(flag_row["open_items"] or 0)\n        items.append({"check": f"Review Flag: {flag_row[\'quality_flag\']}", "count": str(count), "severity": "warnung" if count else "ok"})\n    return items\n', '''    for flag_row in get_review_quality_flag_counts(conn):\n        count = int(flag_row["open_items"] or 0)\n        items.append({"check": f"Review Flag: {flag_row['quality_flag']}", "count": str(count), "severity": "warnung" if count else "ok"})\n    for ready in get_review_readiness_counts(conn):\n        count = int(ready["count"] or 0)\n        sev = "ok" if ready["readiness_status"] in {"ready_for_import", "ignored"} else ("warnung" if count else "ok")\n        items.append({"check": f"Review Readiness: {ready['readiness_status']}", "count": str(count), "severity": sev})\n    for source in get_ready_for_import_by_source(conn):\n        if int(source["total"] or 0) and int(source["ready_for_import"] or 0) == 0:\n            items.append({"check": f"Quelle ohne ready_for_import: {source['source_platform']}", "count": source["total"], "severity": "warnung"})\n    missing_snapshot = conn.execute("SELECT COUNT(*) AS c FROM broker_import_dry_runs WHERE snapshot_date_status='missing'").fetchone()\n    count = int(missing_snapshot["c"] or 0) if missing_snapshot else 0\n    items.append({"check": "Dry Runs ohne Snapshot Date", "count": str(count), "severity": "warnung" if count else "ok"})\n    return items\n''')
p.write_text(s)
print('data patched')


Output: data patched


from __future__ import annotations

from jarvis_finance.dashboard import data
from jarvis_finance.dashboard.components.tables import show_table

PAGE_TITLE = "Manual Review Queue"

QUALITY_FLAGS = [
    "missing_isin", "missing_ticker", "snapshot_only", "cost_basis_uncertain",
    "market_value_legacy", "missing_fx", "aggregate_only", "missing_instrument_details",
    "cash_snapshot_candidate", "needs_manual_review",
]

def _none_if_all(value: str) -> str | None:
    return None if value in {"Alle", ""} else value

def render(st, conn) -> None:
    st.title("Manual Review Queue")
    st.caption("Phase B4: Runtime-Review-Metadaten bearbeiten; Portfolio-/Ledger-/Snapshot-Writes bleiben blockiert.")

    sessions = data.get_broker_dry_runs(conn, limit=100)
    dry_run_ids = [s["dry_run_id"] for s in sessions if s.get("dry_run_id")]

    st.subheader("Filter")
    c1, c2, c3 = st.columns(3)
    with c1:
        source = _none_if_all(st.selectbox("Quelle", ["Alle", "PostFinance", "True Wealth", "Raiffeisen"]))
        status = _none_if_all(st.selectbox("Status", ["Alle", "open", "blocked", "mapped", "ignored", "resolved"]))
        asset_class = _none_if_all(st.selectbox("Assetklasse", ["Alle", "equity", "etf", "cash", "other"]))
    with c2:
        dry_run_id = _none_if_all(st.selectbox("Dry Run Session", ["Alle", *dry_run_ids]))
        quality_flag = _none_if_all(st.selectbox("Quality Flag", ["Alle", *QUALITY_FLAGS]))
        currency = st.text_input("Währung", value="").strip().upper() or None
    with c3:
        account_mapping_status = _none_if_all(st.selectbox("Account Mapping", ["Alle", "missing", "mapped"]))
        readiness = _none_if_all(st.selectbox("Import Readiness", ["Alle", "not_ready", "review_needed", "ready_for_import", "ignored", "blocked"]))

    items = data.get_broker_review_items(
        conn,
        limit=500,
        source_platform=source,
        dry_run_id=dry_run_id,
        status=status,
        quality_flag=quality_flag,
        asset_class=asset_class,
        currency=currency,
        account_mapping_status=account_mapping_status,
        readiness_status=readiness,
    )
    st.subheader("Review Items")
    show_table(st, items)

    st.subheader("Detailansicht")
    if items:
        selected = st.selectbox("Review Item", [i["item_id"] for i in items])
        from jarvis_finance.imports.broker_mapping import get_review_item_detail
        detail = get_review_item_detail(conn, selected)
        st.write(detail["item"])
        st.caption("Audit History")
        show_table(st, detail["audit_history"])
    else:
        st.info("Keine Review Items für diesen Filter.")

    st.subheader("Dry Run Sessions")
    show_table(st, sessions)
    st.caption("Aktionen vorbereitet: archivieren, verwerfen, als aktuell markieren. Kein produktiver Import.")

    st.subheader("Kontrollierte Review-Metadaten-Aktionen")
    st.info("Review-Metadaten schreiben ist nach expliziter Bestätigung erlaubt; Portfolio-/Ledger-/Snapshot-Writes bleiben blockiert.")
    st.button("ISIN speichern", disabled=True)
    st.button("Ticker/Exchange speichern", disabled=True)
    st.button("Instrument Mapping bestätigen", disabled=True)
    st.button("Account Mapping bestätigen", disabled=True)
    st.button("Ignore with note", disabled=True)
    st.button("Mark resolved", disabled=True)
    st.button("Produktiven Initial Snapshot committen", disabled=True)


from __future__ import annotations

from jarvis_finance.dashboard import data
from jarvis_finance.dashboard.components.tables import show_table

PAGE_TITLE = "Data Quality Center"

def render(st, conn) -> None:
    st.title("Data Quality Center")
    st.caption("Alerts, Preise, FX, ISIN, Parser-/Review-Flags, Readiness und Safe-Mode-Prüfungen.")
    st.subheader("Data Quality Checks")
    show_table(st, data.get_data_quality(conn))
    st.subheader("Review Items nach Quality Flag")
    show_table(st, data.get_review_quality_flag_counts(conn))
    st.subheader("Import Readiness")
    show_table(st, data.get_review_readiness_counts(conn))
    st.subheader("Ready for Import je Quelle")
    show_table(st, data.get_ready_for_import_by_source(conn))
    st.subheader("Alerts")
    show_table(st, data.get_alerts(conn))
    st.subheader("Sichere Aktionen")
    st.button("Data Quality Recheck", disabled=True)
    st.button("Resolve Fixed Alerts", disabled=True)
    st.button("Review Items aktualisieren", disabled=True)
    st.button("Produktiven Import starten", disabled=True)
    st.info("Links zur Review Queue können über die Navigation/Filter genutzt werden; produktive Commit-Aktionen bleiben deaktiviert.")


patch

st.caption("Phase B2: synthetische Parser-Dry-Runs, Review Items und Quality Flags — kein produktiver Aktien-/ETF-/Cash-Import.")

st.caption("Phase B4: Dry-Run-Sessions, Review Queue und Readiness — kein produktiver Aktien-/ETF-/Cash-Import.")


--- a//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/pages/page_14_import_wizard.py
+++ b//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/pages/page_14_import_wizard.py
@@ -9,7 +9,7 @@

 def render(st, conn) -> None:
     st.title("Import Wizard")
-    st.caption("Phase B2: synthetische Parser-Dry-Runs, Review Items und Quality Flags — kein produktiver Aktien-/ETF-/Cash-Import.")
+    st.caption("Phase B4: Dry-Run-Sessions, Review Queue und Readiness — kein produktiver Aktien-/ETF-/Cash-Import.")

     st.info("Modus: Read-only. Produktive Commit-Aktionen sind standardmäßig deaktiviert.")
     st.warning("Echte Broker-/Bankdateien nur Runtime/Temp verarbeiten. Keine Dateien ins Repo kopieren.")


     1|from __future__ import annotations
     2|
     3|from jarvis_finance.storage.database import connect_memory
     4|from jarvis_finance.storage.migrations import apply_migrations, get_schema_version
     5|from jarvis_finance.storage.schema import REQUIRED_TABLES
     6|
     7|
     8|def test_db_schema_can_be_created() -> None:
     9|    conn = connect_memory()
    10|    apply_migrations(conn)
    11|    tables = {row["name"] for row in conn.execute("SELECT name FROM sqlite_master WHERE type='table'")}
    12|    assert set(REQUIRED_TABLES).issubset(tables)
    13|
    14|
    15|def test_schema_version_recorded() -> None:
    16|    conn = connect_memory()
    17|    apply_migrations(conn)
    18|    assert get_schema_version(conn) == 5
    19|
    20|
    21|def test_decimal_sensitive_columns_use_text_affinity() -> None:
    22|    conn = connect_memory()
    23|    apply_migrations(conn)
    24|    tx_cols = {row["name"]: row["type"] for row in conn.execute("PRAGMA table_info(transactions)").fetchall()}
    25|    crypto_cols = {row["name"]: row["type"] for row in conn.execute("PRAGMA table_info(crypto_holdings)").fetchall()}
    26|    instrument_cols = {row["name"] for row in conn.execute("PRAGMA table_info(instruments)").fetchall()}
    27|    assert tx_cols["quantity"].upper() == "TEXT"
    28|    assert tx_cols["fx_rate_to_chf"].upper() == "TEXT"
    29|    assert crypto_cols["quantity"].upper() == "TEXT"
    30|    assert {"position_category", "ter", "distribution_policy", "index_name", "fund_domicile", "benchmark"}.issubset(instrument_cols)
    31|
    32|
    33|def test_broker_bank_mapping_tables_exist_with_required_columns() -> None:
    34|    conn = connect_memory()
    35|    apply_migrations(conn)
    36|    instrument_mapping_cols = {row["name"] for row in conn.execute("PRAGMA table_info(instrument_mappings)").fetchall()}
    37|    account_mapping_cols = {row["name"] for row in conn.execute("PRAGMA table_info(platform_account_mappings)").fetchall()}
    38|    dry_run_cols = {row["name"] for row in conn.execute("PRAGMA table_info(broker_import_dry_runs)").fetchall()}
    39|
    40|    assert {"source_name", "source_platform", "source_label", "normalized_name", "isin", "ticker", "exchange", "currency", "asset_class", "instrument_id", "mapping_status", "confidence", "quality_flags_json"}.issubset(instrument_mapping_cols)
    41|    assert {"source_platform", "source_account_label", "normalized_platform", "normalized_account_name", "internal_platform_id", "internal_account_id", "account_type", "currency", "mapping_status"}.issubset(account_mapping_cols)
    42|    assert {"source_platform", "source_file_type", "source_filename_hash", "detected_snapshot_date", "snapshot_date_status", "candidate_positions", "candidate_cash_rows", "mapped_positions", "blocked_positions", "quality_flags_json", "summary_json"}.issubset(dry_run_cols)
    43|    review_cols = {row["name"] for row in conn.execute("PRAGMA table_info(broker_import_review_items)").fetchall()}
    44|    assert {"review_item_id", "dry_run_id", "source_platform", "source_row_ref", "row_hash", "source_label", "normalized_name", "detected_asset_class", "detected_currency", "quality_flags_json", "review_status"}.issubset(review_cols)
    45|


--- a//home/agent/.hermes/repos/FinanceManager/tests/unit/test_schema.py
+++ b//home/agent/.hermes/repos/FinanceManager/tests/unit/test_schema.py
@@ -15,7 +15,7 @@
 def test_schema_version_recorded() -> None:
     conn = connect_memory()
     apply_migrations(conn)
-    assert get_schema_version(conn) == 5
+    assert get_schema_version(conn) == 6

 def test_decimal_sensitive_columns_use_text_affinity() -> None:


    assert {"source_platform", "source_file_type", "source_filename_hash", "detected_snapshot_date", "snapshot_date_status", "candidate_positions", "candidate_cash_rows", "mapped_positions", "blocked_positions", "quality_flags_json", "summary_json", "session_status", "is_current"}.issubset(dry_run_cols)
    review_cols = {row["name"] for row in conn.execute("PRAGMA table_info(broker_import_review_items)").fetchall()}
    assert {"review_item_id", "dry_run_id", "source_platform", "source_row_ref", "row_hash", "source_label", "normalized_name", "detected_asset_class", "detected_currency", "quality_flags_json", "review_status", "import_readiness_status", "reviewer_confirmed", "snapshot_date_confirmed", "ticker_exchange_confirmed", "account_mapping_status"}.issubset(review_cols)

    assert {"source_platform", "source_file_type", "source_filename_hash", "detected_snapshot_date", "snapshot_date_status", "candidate_positions", "candidate_cash_rows", "mapped_positions", "blocked_positions", "quality_flags_json", "summary_json"}.issubset(dry_run_cols)
    review_cols = {row["name"] for row in conn.execute("PRAGMA table_info(broker_import_review_items)").fetchall()}
    assert {"review_item_id", "dry_run_id", "source_platform", "source_row_ref", "row_hash", "source_label", "normalized_name", "detected_asset_class", "detected_currency", "quality_flags_json", "review_status"}.issubset(review_cols)


--- a//home/agent/.hermes/repos/FinanceManager/tests/unit/test_schema.py
+++ b//home/agent/.hermes/repos/FinanceManager/tests/unit/test_schema.py
@@ -39,6 +39,6 @@

     assert {"source_name", "source_platform", "source_label", "normalized_name", "isin", "ticker", "exchange", "currency", "asset_class", "instrument_id", "mapping_status", "confidence", "quality_flags_json"}.issubset(instrument_mapping_cols)
     assert {"source_platform", "source_account_label", "normalized_platform", "normalized_account_name", "internal_platform_id", "internal_account_id", "account_type", "currency", "mapping_status"}.issubset(account_mapping_cols)
-    assert {"source_platform", "source_file_type", "source_filename_hash", "detected_snapshot_date", "snapshot_date_status", "candidate_positions", "candidate_cash_rows", "mapped_positions", "blocked_positions", "quality_flags_json", "summary_json"}.issubset(dry_run_cols)
+    assert {"source_platform", "source_file_type", "source_filename_hash", "detected_snapshot_date", "snapshot_date_status", "candidate_positions", "candidate_cash_rows", "mapped_positions", "blocked_positions", "quality_flags_json", "summary_json", "session_status", "is_current"}.issubset(dry_run_cols)
     review_cols = {row["name"] for row in conn.execute("PRAGMA table_info(broker_import_review_items)").fetchall()}
-    assert {"review_item_id", "dry_run_id", "source_platform", "source_row_ref", "row_hash", "source_label", "normalized_name", "detected_asset_class", "detected_currency", "quality_flags_json", "review_status"}.issubset(review_cols)
+    assert {"review_item_id", "dry_run_id", "source_platform", "source_row_ref", "row_hash", "source_label", "normalized_name", "detected_asset_class", "detected_currency", "quality_flags_json", "review_status", "import_readiness_status", "reviewer_confirmed", "snapshot_date_confirmed", "ticker_exchange_confirmed", "account_mapping_status"}.issubset(review_cols)


Explanation:
**Updating import functions**

I need to append some tests, and I think a direct call to create_review_item at a lower level would be easier. So, I should import the necessary functions for that. Right now, my import line isn’t quite enough and needs to expand. It's a good opportunity to double-check all the imports to make sure I have everything I need for the tests to run smoothly.

from jarvis_finance.imports.broker_mapping import assert_write_enabled, confirm_review_item_mapping, get_broker_review_items, run_broker_import_dry_run

from jarvis_finance.imports.broker_mapping import (
    assert_productive_import_disabled,
    assert_write_enabled,
    confirm_review_item_account_mapping,
    confirm_review_item_instrument_mapping,
    confirm_review_item_snapshot_date,
    confirm_review_item_mapping,
    create_broker_import_review_item,
    get_broker_review_items,
    get_dry_run_sessions,
    ignore_review_item,
    mark_review_item_resolved,
    run_broker_import_dry_run,
    update_dry_run_session_status,
    update_review_item_isin,
    update_review_item_ticker_exchange,
)


--- a//home/agent/.hermes/repos/FinanceManager/tests/unit/test_broker_parser_dry_run_pipeline.py
+++ b//home/agent/.hermes/repos/FinanceManager/tests/unit/test_broker_parser_dry_run_pipeline.py
@@ -4,7 +4,23 @@
 from pathlib import Path

 from jarvis_finance.dashboard import data as dashboard_data
-from jarvis_finance.imports.broker_mapping import assert_write_enabled, confirm_review_item_mapping, get_broker_review_items, run_broker_import_dry_run
+from jarvis_finance.imports.broker_mapping import (
+    assert_productive_import_disabled,
+    assert_write_enabled,
+    confirm_review_item_account_mapping,
+    confirm_review_item_instrument_mapping,
+    confirm_review_item_snapshot_date,
+    confirm_review_item_mapping,
+    create_broker_import_review_item,
+    get_broker_review_items,
+    get_dry_run_sessions,
+    ignore_review_item,
+    mark_review_item_resolved,
+    run_broker_import_dry_run,
+    update_dry_run_session_status,
+    update_review_item_isin,
+    update_review_item_ticker_exchange,
+)
 from jarvis_finance.imports.broker_parsers import parse_postfinance_docx, parse_raiffeisen_xlsx, parse_true_wealth_docx
 from jarvis_finance.storage.database import connect_memory
 from jarvis_finance.storage.migrations import apply_migrations, utc_now


def seed_instrument(c, *, instrument_id='inst-1', asset_class='etf', isin='CH0000000001', ticker='METF', exchange='SIX', currency='CHF'):
    c.execute("INSERT INTO instruments(instrument_id, asset_class, name, ticker, isin, exchange, currency, created_at) VALUES (?,?,?,?,?,?,?,?)", (instrument_id, asset_class, 'Mapped ETF', ticker, isin, exchange, currency, utc_now()))

def seed_accounts(c):
    c.execute("INSERT INTO platforms(platform_id, name, platform_type, created_at) VALUES ('pf','PostFinance','broker',?)", (utc_now(),))
    c.execute("INSERT INTO accounts(account_id, platform_id, account_name, account_type, currency, created_at) VALUES ('acct-broker','pf','Brokerage','brokerage','CHF',?)", (utc_now(),))
    c.execute("INSERT INTO accounts(account_id, platform_id, account_name, account_type, currency, created_at) VALUES ('acct-cash','pf','Cash','cash','CHF',?)", (utc_now(),))

def seed_review_item(c, *, source='PostFinance', asset_class='etf', currency='CHF', flags=None, quantity=True, market_value=True, status='open'):
    dry = run_broker_import_dry_run(c, parse_postfinance_docx.__globals__['ParsedBrokerFile'] if False else None, source_filename='x') if False else None
    c.execute("INSERT INTO broker_import_dry_runs(dry_run_id, source_name, source_platform, source_file_label, source_file_type, source_hash, source_filename_hash, detected_sections_json, rows_total, quality_flags_json, summary_json, created_at, snapshot_date_status) VALUES ('dry-1', ?, ?, '[redacted]', 'docx', 'h', 'h', '[]', 1, '{}', '{}', ?, 'missing')", (source, source, utc_now()))
    return create_broker_import_review_item(c, dry_run_id='dry-1', source_platform=source, source_file_type='docx', source_row_ref='row-1', source_label='Synthetic Label', detected_asset_class=asset_class, detected_currency=currency, detected_quantity_present=quantity, detected_market_value_present=market_value, quality_flags=flags or ['missing_isin', 'missing_ticker', 'snapshot_only', 'cost_basis_uncertain'], review_status=status)

def seed_instrument(c):
    c.execute("INSERT INTO instruments(instrument_id, asset_class, name, ticker, isin, exchange, currency, created_at) VALUES ('inst-1','etf','Mapped ETF','METF','CH0000000001','SIX','CHF',?)", (utc_now(),))


--- a//home/agent/.hermes/repos/FinanceManager/tests/unit/test_broker_parser_dry_run_pipeline.py
+++ b//home/agent/.hermes/repos/FinanceManager/tests/unit/test_broker_parser_dry_run_pipeline.py
@@ -63,8 +63,20 @@
     return c

-def seed_instrument(c):
-    c.execute("INSERT INTO instruments(instrument_id, asset_class, name, ticker, isin, exchange, currency, created_at) VALUES ('inst-1','etf','Mapped ETF','METF','CH0000000001','SIX','CHF',?)", (utc_now(),))
+def seed_instrument(c, *, instrument_id='inst-1', asset_class='etf', isin='CH0000000001', ticker='METF', exchange='SIX', currency='CHF'):
+    c.execute("INSERT INTO instruments(instrument_id, asset_class, name, ticker, isin, exchange, currency, created_at) VALUES (?,?,?,?,?,?,?,?)", (instrument_id, asset_class, 'Mapped ETF', ticker, isin, exchange, currency, utc_now()))
+
+
+def seed_accounts(c):
+    c.execute("INSERT INTO platforms(platform_id, name, platform_type, created_at) VALUES ('pf','PostFinance','broker',?)", (utc_now(),))
+    c.execute("INSERT INTO accounts(account_id, platform_id, account_name, account_type, currency, created_at) VALUES ('acct-broker','pf','Brokerage','brokerage','CHF',?)", (utc_now(),))
+    c.execute("INSERT INTO accounts(account_id, platform_id, account_name, account_type, currency, created_at) VALUES ('acct-cash','pf','Cash','cash','CHF',?)", (utc_now(),))
+
+
+def seed_review_item(c, *, source='PostFinance', asset_class='etf', currency='CHF', flags=None, quantity=True, market_value=True, status='open'):
+    dry = run_broker_import_dry_run(c, parse_postfinance_docx.__globals__['ParsedBrokerFile'] if False else None, source_filename='x') if False else None
+    c.execute("INSERT INTO broker_import_dry_runs(dry_run_id, source_name, source_platform, source_file_label, source_file_type, source_hash, source_filename_hash, detected_sections_json, rows_total, quality_flags_json, summary_json, created_at, snapshot_date_status) VALUES ('dry-1', ?, ?, '[redacted]', 'docx', 'h', 'h', '[]', 1, '{}', '{}', ?, 'missing')", (source, source, utc_now()))
+    return create_broker_import_review_item(c, dry_run_id='dry-1', source_platform=source, source_file_type='docx', source_row_ref='row-1', source_label='Synthetic Label', detected_asset_class=asset_class, detected_currency=currency, detected_quantity_present=quantity, detected_market_value_present=market_value, quality_flags=flags or ['missing_isin', 'missing_ticker', 'snapshot_only', 'cost_basis_uncertain'], review_status=status)

 def test_synthetic_postfinance_parser_recognizes_candidates_and_quality_flags(tmp_path: Path) -> None:


def seed_review_item(c, *, source='PostFinance', asset_class='etf', currency='CHF', flags=None, quantity=True, market_value=True, status='open'):
    c.execute("INSERT INTO broker_import_dry_runs(dry_run_id, source_name, source_platform, source_file_label, source_file_type, source_hash, source_filename_hash, detected_sections_json, rows_total, quality_flags_json, summary_json, created_at, snapshot_date_status) VALUES ('dry-1', ?, ?, '[redacted]', 'docx', 'h', 'h', '[]', 1, '{}', '{}', ?, 'missing')", (source, source, utc_now()))

def seed_review_item(c, *, source='PostFinance', asset_class='etf', currency='CHF', flags=None, quantity=True, market_value=True, status='open'):
    dry = run_broker_import_dry_run(c, parse_postfinance_docx.__globals__['ParsedBrokerFile'] if False else None, source_filename='x') if False else None
    c.execute("INSERT INTO broker_import_dry_runs(dry_run_id, source_name, source_platform, source_file_label, source_file_type, source_hash, source_filename_hash, detected_sections_json, rows_total, quality_flags_json, summary_json, created_at, snapshot_date_status) VALUES ('dry-1', ?, ?, '[redacted]', 'docx', 'h', 'h', '[]', 1, '{}', '{}', ?, 'missing')", (source, source, utc_now()))


--- a//home/agent/.hermes/repos/FinanceManager/tests/unit/test_broker_parser_dry_run_pipeline.py
+++ b//home/agent/.hermes/repos/FinanceManager/tests/unit/test_broker_parser_dry_run_pipeline.py
@@ -74,7 +74,6 @@

 def seed_review_item(c, *, source='PostFinance', asset_class='etf', currency='CHF', flags=None, quantity=True, market_value=True, status='open'):
-    dry = run_broker_import_dry_run(c, parse_postfinance_docx.__globals__['ParsedBrokerFile'] if False else None, source_filename='x') if False else None
     c.execute("INSERT INTO broker_import_dry_runs(dry_run_id, source_name, source_platform, source_file_label, source_file_type, source_hash, source_filename_hash, detected_sections_json, rows_total, quality_flags_json, summary_json, created_at, snapshot_date_status) VALUES ('dry-1', ?, ?, '[redacted]', 'docx', 'h', 'h', '[]', 1, '{}', '{}', ?, 'missing')", (source, source, utc_now()))
     return create_broker_import_review_item(c, dry_run_id='dry-1', source_platform=source, source_file_type='docx', source_row_ref='row-1', source_label='Synthetic Label', detected_asset_class=asset_class, detected_currency=currency, detected_quantity_present=quantity, detected_market_value_present=market_value, quality_flags=flags or ['missing_isin', 'missing_ticker', 'snapshot_only', 'cost_basis_uncertain'], review_status=status)


Explanation:
**Appending tests and issues**

I'm considering how to append tests. I need to think about the seed_review duplicate dry-1 issue, especially if there are multiple entries in the same connection. For the filters, maybe I should use the run function in conjunction with files. It seems like appending everything could help streamline the process. Alright, I’ll keep these details in mind as I move forward!

def test_b4_review_queue_filters_by_source_dry_run_and_quality_flag(tmp_path: Path) -> None:
    c = conn()
    p1 = tmp_path / "pf.docx"; _docx(p1, [["Produkt", "Menge", "Währung"], ["ETF A", "Menge", "CHF"]])
    p2 = tmp_path / "tw.docx"; _docx(p2, [["Position", "Währung"], ["ETF B", "USD"]])
    s1 = run_broker_import_dry_run(c, parse_postfinance_docx(p1), source_filename=p1.name)
    s2 = run_broker_import_dry_run(c, parse_true_wealth_docx(p2), source_filename=p2.name)
    assert len(get_broker_review_items(c, source_platform="PostFinance")) == 1
    assert len(get_broker_review_items(c, dry_run_id=s2["dry_run_id"])) == 1
    assert len(get_broker_review_items(c, quality_flag="missing_fx")) == 1

def test_b4_isin_ticker_exchange_updates_write_audit_and_name_only_not_mapped() -> None:
    c = conn(); item = seed_review_item(c)
    update_review_item_isin(c, review_item_id=item, isin="CH0000000001", note="synthetic isin")
    update_review_item_ticker_exchange(c, review_item_id=item, ticker="METF", note="synthetic ticker")
    row = c.execute("SELECT * FROM broker_import_review_items WHERE review_item_id=?", (item,)).fetchone()
    assert row["isin"] == "CH0000000001"
    assert row["ticker"] == "METF"
    assert "missing_isin" not in row["quality_flags_json"]
    assert row["review_status"] == "open"
    assert c.execute("SELECT COUNT(*) AS n FROM audit_log WHERE entity_id=?", (item,)).fetchone()["n"] == 2

def test_b4_ticker_without_exchange_stays_review_needed() -> None:
    c = conn(); item = seed_review_item(c)
    update_review_item_ticker_exchange(c, review_item_id=item, ticker="METF", note="ticker only")
    row = c.execute("SELECT import_readiness_status, quality_flags_json FROM broker_import_review_items WHERE review_item_id=?", (item,)).fetchone()
    assert row["import_readiness_status"] in {"not_ready", "review_needed"}
    assert "ticker_without_exchange" in row["quality_flags_json"]

def test_b4_instrument_mapping_with_isin_can_be_ready_for_import_for_equity() -> None:
    c = conn(); seed_instrument(c, asset_class="equity"); seed_accounts(c); item = seed_review_item(c, asset_class="equity")
    update_review_item_isin(c, review_item_id=item, isin="CH0000000001", note="isin")
    confirm_review_item_account_mapping(c, review_item_id=item, account_id="acct-broker", note="account")
    confirm_review_item_snapshot_date(c, review_item_id=item, note="snapshot confirmed")
    confirm_review_item_instrument_mapping(c, review_item_id=item, instrument_id="inst-1", note="instrument")
    row = c.execute("SELECT import_readiness_status FROM broker_import_review_items WHERE review_item_id=?", (item,)).fetchone()
    assert row["import_readiness_status"] == "ready_for_import"

def test_b4_ready_for_import_for_etf_and_account_mapping_audit() -> None:
    c = conn(); seed_instrument(c); seed_accounts(c); item = seed_review_item(c, asset_class="etf")
    update_review_item_isin(c, review_item_id=item, isin="CH0000000001", note="isin")
    audit = confirm_review_item_account_mapping(c, review_item_id=item, account_id="acct-broker", note="account")
    confirm_review_item_snapshot_date(c, review_item_id=item, note="snapshot")
    confirm_review_item_instrument_mapping(c, review_item_id=item, instrument_id="inst-1", note="instrument")
    assert audit
    assert c.execute("SELECT import_readiness_status FROM broker_import_review_items WHERE review_item_id=?", (item,)).fetchone()["import_readiness_status"] == "ready_for_import"

def test_b4_ready_for_import_for_cash() -> None:
    c = conn(); seed_accounts(c); item = seed_review_item(c, asset_class="cash", flags=["cash_snapshot_candidate"], quantity=False, market_value=True)
    confirm_review_item_account_mapping(c, review_item_id=item, account_id="acct-cash", note="cash account")
    confirm_review_item_snapshot_date(c, review_item_id=item, note="snapshot")
    # cash has no instrument mapping; reviewer confirmation is modelled by explicit metadata confirmation here
    c.execute("UPDATE broker_import_review_items SET reviewer_confirmed=1 WHERE review_item_id=?", (item,))
    from jarvis_finance.imports.broker_mapping import refresh_review_item_readiness
    assert refresh_review_item_readiness(c, item) == "ready_for_import"

def test_b4_ignore_requires_note_and_mark_resolved_requires_ready() -> None:
    c = conn(); item = seed_review_item(c)
    try:
        ignore_review_item(c, review_item_id=item, note="")
    except ValueError:
        pass
    else:
        raise AssertionError("ignore without note accepted")
    ignored_audit = ignore_review_item(c, review_item_id=item, note="synthetic ignore")
    assert ignored_audit
    item2 = seed_review_item(c, source="True Wealth")
    try:
        mark_review_item_resolved(c, review_item_id=item2, note="too early")
    except ValueError:
        pass
    else:
        raise AssertionError("resolved before ready accepted")

def test_b4_aggregate_only_remains_blocked_for_instrument_import() -> None:
    c = conn(); item = seed_review_item(c, source="Raiffeisen", asset_class="other", flags=["aggregate_only", "missing_instrument_details"], status="blocked")
    row = c.execute("SELECT import_readiness_status, review_status FROM broker_import_review_items WHERE review_item_id=?", (item,)).fetchone()
    from jarvis_finance.imports.broker_mapping import refresh_review_item_readiness
    assert refresh_review_item_readiness(c, item) == "blocked"

def test_b4_dry_run_session_archive_discard_current_writes_audit(tmp_path: Path) -> None:
    c = conn()
    p = tmp_path / "pf.docx"; _docx(p, [["Produkt", "Menge", "Währung"], ["ETF A", "Menge", "CHF"]])
    summary = run_broker_import_dry_run(c, parse_postfinance_docx(p), source_filename=p.name)
    update_dry_run_session_status(c, dry_run_id=summary["dry_run_id"], action="mark_current", note="current")
    update_dry_run_session_status(c, dry_run_id=summary["dry_run_id"], action="archive", note="archive")
    sessions = get_dry_run_sessions(c)
    assert sessions[0]["session_status"] == "archived"
    assert c.execute("SELECT COUNT(*) AS n FROM audit_log WHERE entity_id=?", (summary["dry_run_id"],)).fetchone()["n"] == 2

def test_b4_data_quality_readiness_counts_and_productive_import_disabled() -> None:
    c = conn(); item = seed_review_item(c)
    rows = {r["check"]: r["count"] for r in dashboard_data.get_data_quality(c)}
    assert "Review Readiness: not_ready" in rows
    assert assert_productive_import_disabled() is True
    assert c.execute("SELECT COUNT(*) AS n FROM positions_snapshot").fetchone()["n"] == 0


terminal

cd /home/agent/.hermes/repos/FinanceManager && cat /tmp/b4_tests_append.py >> tests/unit/test_broker_parser_dry_run_pipeline.py && PYTHONPATH=src pytest tests/unit/test_broker_parser_dry_run_pipeline.py tests/unit/test_schema.py -q


Output:
.................F.......                                                [100%]
=================================== FAILURES ===================================
________ test_b4_ignore_requires_note_and_mark_resolved_requires_ready _________

    def test_b4_ignore_requires_note_and_mark_resolved_requires_ready() -> None:
        c = conn(); item = seed_review_item(c)
        try:
            ignore_review_item(c, review_item_id=item, note="")
        except ValueError:
            pass
        else:
            raise AssertionError("ignore without note accepted")
        ignored_audit = ignore_review_item(c, review_item_id=item, note="synthetic ignore")
        assert ignored_audit
>       item2 = seed_review_item(c, source="True Wealth")
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

tests/unit/test_broker_parser_dry_run_pipeline.py:270:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

c = <sqlite3.Connection object at 0x7697615a58a0>

    def seed_review_item(c, *, source='PostFinance', asset_class='etf', currency='CHF', flags=None, quantity=True, market_value=True, status='open'):
>       c.execute("INSERT INTO broker_import_dry_runs(dry_run_id, source_name, source_platform, source_file_label, source_file_type, source_hash, source_filename_hash, detected_sections_json, rows_total, quality_flags_json, summary_json, created_at, snapshot_date_status) VALUES ('dry-1', ?, ?, '[redacted]', 'docx', 'h', 'h', '[]', 1, '{}', '{}', ?, 'missing')", (source, source, utc_now()))
E       sqlite3.IntegrityError: UNIQUE constraint failed: broker_import_dry_runs.dry_run_id

tests/unit/test_broker_parser_dry_run_pipeline.py:77: IntegrityError
=========================== short test summary info ============================
FAILED tests/unit/test_broker_parser_dry_run_pipeline.py::test_b4_ignore_requires_note_and_mark_resolved_requires_ready
1 failed, 24 passed in 0.26s


patch

import zipfile
import uuid
from pathlib import Path


--- a//home/agent/.hermes/repos/FinanceManager/tests/unit/test_broker_parser_dry_run_pipeline.py
+++ b//home/agent/.hermes/repos/FinanceManager/tests/unit/test_broker_parser_dry_run_pipeline.py
@@ -1,6 +1,7 @@
 from __future__ import annotations

 import zipfile
+import uuid
 from pathlib import Path

 from jarvis_finance.dashboard import data as dashboard_data

/home/agent/.hermes/repos/FinanceManager/tests/unit/test_broker_parser_dry_run_pipeline.py was modified since you last read it on disk (external edit or unrecorded writer). Re-read the file before writing.


def seed_review_item(c, *, source='PostFinance', asset_class='etf', currency='CHF', flags=None, quantity=True, market_value=True, status='open'):
    dry_id = f"dry-{uuid.uuid4()}"
    c.execute("INSERT INTO broker_import_dry_runs(dry_run_id, source_name, source_platform, source_file_label, source_file_type, source_hash, source_filename_hash, detected_sections_json, rows_total, quality_flags_json, summary_json, created_at, snapshot_date_status) VALUES (?, ?, ?, '[redacted]', 'docx', 'h', 'h', '[]', 1, '{}', '{}', ?, 'missing')", (dry_id, source, source, utc_now()))
    return create_broker_import_review_item(c, dry_run_id=dry_id, source_platform=source, source_file_type='docx', source_row_ref='row-1', source_label='Synthetic Label', detected_asset_class=asset_class, detected_currency=currency, detected_quantity_present=quantity, detected_market_value_present=market_value, quality_flags=flags or ['missing_isin', 'missing_ticker', 'snapshot_only', 'cost_basis_uncertain'], review_status=status)

def seed_review_item(c, *, source='PostFinance', asset_class='etf', currency='CHF', flags=None, quantity=True, market_value=True, status='open'):
    c.execute("INSERT INTO broker_import_dry_runs(dry_run_id, source_name, source_platform, source_file_label, source_file_type, source_hash, source_filename_hash, detected_sections_json, rows_total, quality_flags_json, summary_json, created_at, snapshot_date_status) VALUES ('dry-1', ?, ?, '[redacted]', 'docx', 'h', 'h', '[]', 1, '{}', '{}', ?, 'missing')", (source, source, utc_now()))
    return create_broker_import_review_item(c, dry_run_id='dry-1', source_platform=source, source_file_type='docx', source_row_ref='row-1', source_label='Synthetic Label', detected_asset_class=asset_class, detected_currency=currency, detected_quantity_present=quantity, detected_market_value_present=market_value, quality_flags=flags or ['missing_isin', 'missing_ticker', 'snapshot_only', 'cost_basis_uncertain'], review_status=status)


--- a//home/agent/.hermes/repos/FinanceManager/tests/unit/test_broker_parser_dry_run_pipeline.py
+++ b//home/agent/.hermes/repos/FinanceManager/tests/unit/test_broker_parser_dry_run_pipeline.py
@@ -75,8 +75,9 @@

 def seed_review_item(c, *, source='PostFinance', asset_class='etf', currency='CHF', flags=None, quantity=True, market_value=True, status='open'):
-    c.execute("INSERT INTO broker_import_dry_runs(dry_run_id, source_name, source_platform, source_file_label, source_file_type, source_hash, source_filename_hash, detected_sections_json, rows_total, quality_flags_json, summary_json, created_at, snapshot_date_status) VALUES ('dry-1', ?, ?, '[redacted]', 'docx', 'h', 'h', '[]', 1, '{}', '{}', ?, 'missing')", (source, source, utc_now()))
-    return create_broker_import_review_item(c, dry_run_id='dry-1', source_platform=source, source_file_type='docx', source_row_ref='row-1', source_label='Synthetic Label', detected_asset_class=asset_class, detected_currency=currency, detected_quantity_present=quantity, detected_market_value_present=market_value, quality_flags=flags or ['missing_isin', 'missing_ticker', 'snapshot_only', 'cost_basis_uncertain'], review_status=status)
+    dry_id = f"dry-{uuid.uuid4()}"
+    c.execute("INSERT INTO broker_import_dry_runs(dry_run_id, source_name, source_platform, source_file_label, source_file_type, source_hash, source_filename_hash, detected_sections_json, rows_total, quality_flags_json, summary_json, created_at, snapshot_date_status) VALUES (?, ?, ?, '[redacted]', 'docx', 'h', 'h', '[]', 1, '{}', '{}', ?, 'missing')", (dry_id, source, source, utc_now()))
+    return create_broker_import_review_item(c, dry_run_id=dry_id, source_platform=source, source_file_type='docx', source_row_ref='row-1', source_label='Synthetic Label', detected_asset_class=asset_class, detected_currency=currency, detected_quantity_present=quantity, detected_market_value_present=market_value, quality_flags=flags or ['missing_isin', 'missing_ticker', 'snapshot_only', 'cost_basis_uncertain'], review_status=status)

 def test_synthetic_postfinance_parser_recognizes_candidates_and_quality_flags(tmp_path: Path) -> None:


terminal

cd /home/agent/.hermes/repos/FinanceManager && PYTHONPATH=src pytest tests/unit/test_broker_parser_dry_run_pipeline.py tests/unit/test_schema.py -q


Output:
.........................                                                [100%]
25 passed in 0.22s


Explanation:
**Testing migration issues**

I need to test whether the old `confirm_review_item_mapping` is failing due to new columns. It seems `create_review_item` doesn't set readiness, which might affect aggregate tests. I should verify that the runtime applies migration and check status counts. There's also a chance the migration version is bumped, but the existing schema lacks session columns. I have to run the migration, but first, I need to compile everything to ensure readiness.

todo

Code-Struktur Review Queue/Dry Run/Data Quality/Audit inspizieren

B4 Schema/Mapping-Actions/Readiness/Session Management implementieren

Dashboard Seiten Filter/Details/Data Quality/Safe Mode erweitern

Tests für B4 ergänzen und produktive Daten-Unverändertheit prüfen


Compile, pytest, Git-Safety, Commit und Push


terminal

set -euo pipefail
cd /home/agent/.hermes/repos/FinanceManager
PYTHONPATH=src python -m jarvis_finance.cli.main migrate
PYTHONPATH=src python - <<'PY'
import json
from jarvis_finance.config.settings import load_settings
from jarvis_finance.storage.database import connect
from jarvis_finance.storage.migrations import apply_migrations
from jarvis_finance.imports.broker_mapping import refresh_review_item_readiness, get_ready_for_import_by_source
conn=connect(load_settings().db_path); apply_migrations(conn)
for row in conn.execute('SELECT review_item_id FROM broker_import_review_items').fetchall():
    refresh_review_item_readiness(conn, row['review_item_id'])
conn.commit()
status_counts={}
for row in conn.execute('SELECT source_platform, review_status, import_readiness_status, COUNT(*) AS n FROM broker_import_review_items GROUP BY source_platform, review_status, import_readiness_status').fetchall():
    status_counts.setdefault(row['source_platform'], []).append({'status':row['review_status'],'readiness':row['import_readiness_status'],'count':row['n']})
productive={t: conn.execute(f'SELECT COUNT(*) AS n FROM {t}').fetchone()['n'] for t in ['positions_snapshot','transactions']}
print(json.dumps({'status_counts':status_counts,'ready_by_source':get_ready_for_import_by_source(conn),'productive_counts':productive}, sort_keys=True))
PY


Output:
SCHEMA_VERSION=6
{"productive_counts": {"positions_snapshot": 0, "transactions": 0}, "ready_by_source": [{"ready_for_import": "0", "source_platform": "PostFinance", "total": "34"}, {"ready_for_import": "0", "source_platform": "Raiffeisen", "total": "7"}, {"ready_for_import": "0", "source_platform": "True Wealth", "total": "18"}], "status_counts": {"PostFinance": [{"count": 33, "readiness": "not_ready", "status": "open"}, {"count": 1, "readiness": "review_needed", "status": "open"}], "Raiffeisen": [{"count": 4, "readiness": "blocked", "status": "blocked"}, {"count": 3, "readiness": "review_needed", "status": "open"}], "True Wealth": [{"count": 17, "readiness": "not_ready", "status": "open"}, {"count": 1, "readiness": "review_needed", "status": "open"}]}}


set -euo pipefail
cd /home/agent/.hermes/repos/FinanceManager
PYTHONPATH=src python -m compileall -q src tests
PYTHONPATH=src pytest -q
rm -rf .pytest_cache src/jarvis_finance/**/__pycache__ tests/**/__pycache__
PYTHONPATH=src python -m jarvis_finance.cli.main git-safety-scan .
git status --short --branch


Output:
.........F.............................................................. [ 46%]
........................................................................ [ 92%]
...........                                                              [100%]
=================================== FAILURES ===================================
__ test_import_wizard_manual_review_and_data_quality_pages_import_and_render ___

    def test_import_wizard_manual_review_and_data_quality_pages_import_and_render() -> None:
        c = conn()
        create_instrument_mapping(c, source_platform="PostFinance", source_label="Unknown ETF", asset_class="etf", currency="USD")
        st = FakeStreamlit()
        for module in [page_14_import_wizard, page_15_manual_review_queue, page_16_data_quality_center]:
>           module.render(st, c)

tests/unit/test_broker_mapping_import_wizard.py:122:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

st = <test_broker_mapping_import_wizard.FakeStreamlit object at 0x7d8e946e7390>
conn = <sqlite3.Connection object at 0x7d8e949ccc70>

    def render(st, conn) -> None:
        st.title("Manual Review Queue")
        st.caption("Phase B4: Runtime-Review-Metadaten bearbeiten; Portfolio-/Ledger-/Snapshot-Writes bleiben blockiert.")

        sessions = data.get_broker_dry_runs(conn, limit=100)
        dry_run_ids = [s["dry_run_id"] for s in sessions if s.get("dry_run_id")]

        st.subheader("Filter")
        c1, c2, c3 = st.columns(3)
        with c1:
            source = _none_if_all(st.selectbox("Quelle", ["Alle", "PostFinance", "True Wealth", "Raiffeisen"]))
            status = _none_if_all(st.selectbox("Status", ["Alle", "open", "blocked", "mapped", "ignored", "resolved"]))
            asset_class = _none_if_all(st.selectbox("Assetklasse", ["Alle", "equity", "etf", "cash", "other"]))
        with c2:
            dry_run_id = _none_if_all(st.selectbox("Dry Run Session", ["Alle", *dry_run_ids]))
            quality_flag = _none_if_all(st.selectbox("Quality Flag", ["Alle", *QUALITY_FLAGS]))
>           currency = st.text_input("Währung", value="").strip().upper() or None
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E           AttributeError: 'NoneType' object has no attribute 'strip'

src/jarvis_finance/dashboard/pages/page_15_manual_review_queue.py:35: AttributeError
=========================== short test summary info ============================
FAILED tests/unit/test_broker_mapping_import_wizard.py::test_import_wizard_manual_review_and_data_quality_pages_import_and_render
1 failed, 154 passed in 5.00s


patch

        currency_value = st.text_input("Währung", value="")
        currency = (currency_value or "").strip().upper() or None

currency = st.text_input("Währung", value="").strip().upper() or None


--- a//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/pages/page_15_manual_review_queue.py
+++ b//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/pages/page_15_manual_review_queue.py
@@ -32,7 +32,8 @@
     with c2:
         dry_run_id = _none_if_all(st.selectbox("Dry Run Session", ["Alle", *dry_run_ids]))
         quality_flag = _none_if_all(st.selectbox("Quality Flag", ["Alle", *QUALITY_FLAGS]))
-        currency = st.text_input("Währung", value="").strip().upper() or None
+        currency_value = st.text_input("Währung", value="")
+        currency = (currency_value or "").strip().upper() or None
     with c3:
         account_mapping_status = _none_if_all(st.selectbox("Account Mapping", ["Alle", "missing", "mapped"]))
         readiness = _none_if_all(st.selectbox("Import Readiness", ["Alle", "not_ready", "review_needed", "ready_for_import", "ignored", "blocked"]))


terminal

Output:
........................................................................ [ 46%]
........................................................................ [ 92%]
...........                                                              [100%]
155 passed in 4.87s
GIT_SAFETY_OK
## main...origin/main
 M src/jarvis_finance/dashboard/data.py
 M src/jarvis_finance/dashboard/pages/page_14_import_wizard.py
 M src/jarvis_finance/dashboard/pages/page_15_manual_review_queue.py
 M src/jarvis_finance/dashboard/pages/page_16_data_quality_center.py
 M src/jarvis_finance/imports/broker_mapping.py
 M src/jarvis_finance/storage/migrations.py
 M tests/unit/test_broker_parser_dry_run_pipeline.py
 M tests/unit/test_schema.py


Explanation:
**Running git commands**

I’m thinking about running `git diff` to compare changes, but first, I need to finalize some counts based on the latest runtime updates. It’s interesting to consider whether I actually need real files for this. I want to ensure safety is up to par before I commit these changes. Once that’s settled, I’ll go ahead and commit and push everything to the repository.

set -euo pipefail
cd /home/agent/.hermes/repos/FinanceManager
git diff --stat
git add src/jarvis_finance/dashboard/data.py src/jarvis_finance/dashboard/pages/page_14_import_wizard.py src/jarvis_finance/dashboard/pages/page_15_manual_review_queue.py src/jarvis_finance/dashboard/pages/page_16_data_quality_center.py src/jarvis_finance/imports/broker_mapping.py src/jarvis_finance/storage/migrations.py tests/unit/test_broker_parser_dry_run_pipeline.py tests/unit/test_schema.py
git commit -m "feat: harden broker review queue mapping workflow"
PYTHONPATH=src python -m compileall -q src tests
PYTHONPATH=src pytest -q
rm -rf .pytest_cache src/jarvis_finance/**/__pycache__ tests/**/__pycache__
PYTHONPATH=src python -m jarvis_finance.cli.main git-safety-scan .
git status --short --branch


Output:
src/jarvis_finance/dashboard/data.py               |  37 +++-
 .../dashboard/pages/page_14_import_wizard.py       |   2 +-
 .../dashboard/pages/page_15_manual_review_queue.py |  71 +++++-
 .../dashboard/pages/page_16_data_quality_center.py |  12 +-
 src/jarvis_finance/imports/broker_mapping.py       | 242 +++++++++++++++++++++
 src/jarvis_finance/storage/migrations.py           |  15 +-
 tests/unit/test_broker_parser_dry_run_pipeline.py  | 141 +++++++++++-
 tests/unit/test_schema.py                          |   6 +-
 8 files changed, 495 insertions(+), 31 deletions(-)
[main 1c5c3a0] feat: harden broker review queue mapping workflow
 8 files changed, 495 insertions(+), 31 deletions(-)
........................................................................ [ 46%]
........................................................................ [ 92%]
...........                                                              [100%]
155 passed in 5.08s
GIT_SAFETY_OK
## main...origin/main [voraus 1]


set -euo pipefail
cd /home/agent/.hermes/repos/FinanceManager
python - <<'PY'
import re, subprocess, sys
from pathlib import Path
repo=Path('/home/agent/.hermes/repos/FinanceManager')
text=Path.home().joinpath('jarvis_runtime/finance-system/secrets/Github_token_FinanceManager.csv').read_text()
m=re.search(r'(github_pat_[A-Za-z0-9_]+|ghp_[A-Za-z0-9_]+|gho_[A-Za-z0-9_]+)', text)
if not m:
    print('TOKEN_PARSE=failed'); sys.exit(12)
token=m.group(1); auth_url=f'https://{token}@github.com/Gamexgit/FinanceManager.git'
def run(args, check=True): return subprocess.run(args, cwd=repo, text=True, capture_output=True, check=check)
try:
    run(['git','fetch',auth_url,'main:refs/remotes/origin/main','--quiet'])
    local=run(['git','rev-parse','HEAD']).stdout.strip(); origin=run(['git','rev-parse','origin/main']).stdout.strip(); base=run(['git','merge-base','HEAD','origin/main']).stdout.strip()
    print('REMOTE_BEFORE='+origin[:7]); print('LOCAL_HEAD='+local[:7])
    if origin != base:
        print('REMOTE_HAS_NEW_COMMITS=yes'); sys.exit(3)
    push=run(['git','push',auth_url,'main:main'], check=False)
    if push.returncode:
        print('PUSH=failed'); print((push.stderr or push.stdout).replace(token,'[REDACTED]')[-1000:]); sys.exit(push.returncode)
    run(['git','fetch',auth_url,'main:refs/remotes/origin/main','--quiet'])
    print('PUSH=success'); print('REMOTE_AFTER='+run(['git','rev-parse','--short','origin/main']).stdout.strip())
finally:
    token=None; auth_url=None
PY
PYTHONPATH=src python -m jarvis_finance.cli.main git-safety-scan .
git status --short --branch


Output:
REMOTE_BEFORE=4e808ed
LOCAL_HEAD=1c5c3a0
PUSH=success
REMOTE_AFTER=1c5c3a0
GIT_SAFETY_OK
## main...origin/main


Explanation:
**Collecting runtime counts**

I need to gather final runtime counts and see which modules have changed. It might be necessary to run a `git status` to check on everything. I think I should do this using the terminal, but I need to consider that there might not be any output values at this point. I'm trying to stay organized and ensure I have everything captured accurately before moving on!

set -euo pipefail
cd /home/agent/.hermes/repos/FinanceManager
PYTHONPATH=src python - <<'PY'
import json
from jarvis_finance.config.settings import load_settings
from jarvis_finance.storage.database import connect
from jarvis_finance.storage.migrations import apply_migrations
from jarvis_finance.imports.broker_mapping import get_ready_for_import_by_source, get_review_readiness_counts
conn=connect(load_settings().db_path); apply_migrations(conn)
status_counts={}
for row in conn.execute('SELECT source_platform, review_status, COUNT(*) AS n FROM broker_import_review_items GROUP BY source_platform, review_status ORDER BY source_platform, review_status').fetchall():
    status_counts.setdefault(row['source_platform'], {})[row['review_status']]=row['n']
productive={t: conn.execute(f'SELECT COUNT(*) AS n FROM {t}').fetchone()['n'] for t in ['positions_snapshot','transactions']}
print(json.dumps({'status_counts':status_counts,'ready_by_source':get_ready_for_import_by_source(conn),'readiness_counts':get_review_readiness_counts(conn),'productive_counts':productive}, sort_keys=True))
PY
git status --short --branch


Output:
{"productive_counts": {"positions_snapshot": 0, "transactions": 0}, "readiness_counts": [{"count": "4", "readiness_status": "blocked"}, {"count": "50", "readiness_status": "not_ready"}, {"count": "5", "readiness_status": "review_needed"}], "ready_by_source": [{"ready_for_import": "0", "source_platform": "PostFinance", "total": "34"}, {"ready_for_import": "0", "source_platform": "Raiffeisen", "total": "7"}, {"ready_for_import": "0", "source_platform": "True Wealth", "total": "18"}], "status_counts": {"PostFinance": {"open": 34}, "Raiffeisen": {"blocked": 4, "open": 3}, "True Wealth": {"open": 18}}}
## main...origin/main


cd /home/agent/.hermes/repos/FinanceManager && PYTHONPATH=src python - <<'PY'
import json
from jarvis_finance.config.settings import load_settings
from jarvis_finance.storage.database import connect
from jarvis_finance.storage.migrations import apply_migrations
conn=connect(load_settings().db_path); apply_migrations(conn)
counts={}
for t in ['positions_snapshot','transactions','cash_balances']:
    counts[t]=conn.execute(f'SELECT COUNT(*) AS n FROM {t}').fetchone()['n']
print(json.dumps(counts, sort_keys=True))
PY


Output: {"cash_balances": 0, "positions_snapshot": 0, "transactions": 0}


Erledigt, Sir. Phase B4 ist implementiert, getestet, committed und gepusht.

## Gesamtstatus

- Phase B4: **abgeschlossen**
- Commit: `1c5c3a0`
- Push Status: **erfolgreich**
- Remote HEAD: `1c5c3a0`
- Tests: `155 passed`
- Compile: **OK**
- Git-Safety: `GIT_SAFETY_OK`
- Git Status: **clean**, `main...origin/main`
- Echte Dateien ins Git: **nein**
- Echte Werte/Mengen im Chat: **nein**
- Produktive Positionen geschrieben: **nein**
- Cash-Snapshots geschrieben: **nein**
- Ledger-Transaktionen geschrieben: **nein**

## Neue/geänderte Module

Geändert:

- `src/jarvis_finance/imports/broker_mapping.py`
- `src/jarvis_finance/storage/migrations.py`
- `src/jarvis_finance/dashboard/data.py`
- `src/jarvis_finance/dashboard/pages/page_14_import_wizard.py`
- `src/jarvis_finance/dashboard/pages/page_15_manual_review_queue.py`
- `src/jarvis_finance/dashboard/pages/page_16_data_quality_center.py`
- `tests/unit/test_broker_parser_dry_run_pipeline.py`
- `tests/unit/test_schema.py`

Schema-Version:

- von `5` auf `6`
- Migration: `006_broker_review_queue_hardening`

## Implementiert: Review Queue Filter

Manual Review Queue erweitert um Filter für:

- Quelle:
  - PostFinance
  - True Wealth
  - Raiffeisen
- Dry Run Session
- Status:
  - open
  - blocked
  - mapped
  - ignored
  - resolved
- Quality Flag:
  - `missing_isin`
  - `missing_ticker`
  - `snapshot_only`
  - `cost_basis_uncertain`
  - `market_value_legacy`
  - `missing_fx`
  - `aggregate_only`
  - `missing_instrument_details`
  - `cash_snapshot_candidate`
  - `needs_manual_review`
- Assetklasse
- Währung
- Account Mapping Status
- Import Readiness Status

## Implementiert: Detailansicht pro Review Item

Die Runtime-UI kann nun pro Review Item anzeigen:

- Quelle
- Dry Run ID
- Source Row Reference
- Source Label
- Normalized Name
- erkannte Assetklasse
- erkannte Währung
- erkannte ISIN
- erkannter Ticker
- erkannte Exchange
- Account Mapping
- Quality Flags
- Review Status
- Import Readiness
- Reviewer Note
- Audit History

Hinweis: Details bleiben Runtime-UI/Runtime-DB. Keine echten Inhalte wurden in Tests, Fixtures, Docs oder Chat ausgegeben.

## Implementierte Review-Metadaten-Aktionen

Aktiviert als kontrollierte Backend-Funktionen mit Audit-Log:

### A. ISIN ergänzen

- grobe ISIN-Formatvalidierung
- entfernt `missing_isin`
- schreibt Audit-Log

### B. Ticker / Exchange ergänzen

- Ticker normalisiert
- Exchange normalisiert
- Ticker ohne Exchange bleibt review-pflichtig
- Flag `ticker_without_exchange`, falls nötig
- schreibt Audit-Log

### C. Exchange ergänzen

- über Ticker/Exchange-Aktion abgedeckt
- schreibt Audit-Log

### D. Instrument Mapping bestätigen

Regeln:

- ISIN erlaubt Mapping auf vorhandenes Instrument
- Ticker + Exchange nur mit expliziter Bestätigung
- Name-only darf **nicht** auf mapped gesetzt werden
- kein stilles Auto-Mapping
- schreibt Audit-Log

### E. Account Mapping bestätigen

Regeln:

- interne Account-ID wird bestätigt
- Cash darf nicht versehentlich auf Brokerage-only gemappt werden
- Brokerage/ETF/Equity darf nicht auf Cash-only gemappt werden
- schreibt Audit-Log

### F. Ignore with note

- Notiz Pflicht
- ohne Notiz abgelehnt
- Status: `ignored`
- Readiness: `ignored`
- schreibt Audit-Log

### G. Mark resolved

- nur möglich, wenn Pflichtprobleme gelöst sind
- sonst abgelehnt
- schreibt Audit-Log

## Implementiert: Import Readiness

Pro Review Item berechnet:

- `not_ready`
- `review_needed`
- `ready_for_import`
- `ignored`
- `blocked`

Regeln umgesetzt:

### Aktien/ETFs

`ready_for_import` nur wenn:

- Quelle vorhanden
- Account Mapping vorhanden
- Assetklasse `equity` oder `etf`
- ISIN vorhanden **oder** Ticker + Exchange explizit bestätigt
- Währung vorhanden
- Snapshot Date manuell bestätigt
- Menge vorhanden
- keine blockierenden Quality Flags
- Reviewer Confirmation vorhanden

### Cash

`ready_for_import` nur wenn:

- Account Mapping vorhanden
- Währung vorhanden
- Snapshot Date bestätigt
- Betrag/Market-Value-Presence vorhanden
- Reviewer Confirmation vorhanden

### Aggregate / Raiffeisen

- `aggregate_only` bleibt blockiert
- keine Instrumentposition daraus
- Cash-/Kontrollsnapshot nur reviewbar, wenn eindeutig

Sehr trocken, sehr sicher. Fast schon schweizerisch.

## Implementiert: Dry Run Session Management

Dry Run Sessions zeigen jetzt:

- Dry Run ID
- Quelle
- Dateityp
- Erstellungszeitpunkt
- Snapshot Date Status
- Anzahl Review Items
- Anzahl `ready_for_import`
- Anzahl `blocked`
- Anzahl `ignored`
- Anzahl `open`
- Warnings/Errors
- Session Status
- Current Marker

Vorbereitete Aktionen:

- Dry Run archivieren
- Dry Run verwerfen
- Dry Run als aktuell markieren

Regeln:

- keine Audit-Historie wird gelöscht
- kein produktiver Import
- Statusänderungen schreiben Audit-Log

## Data Quality Center erweitert

Neu sichtbar:

- Review Items je Quality Flag
- Readiness Counts:
  - `ready_for_import`
  - `not_ready`
  - `review_needed`
  - `blocked`
  - `ignored`
- Warnung, wenn eine Quelle `0 ready_for_import` hat
- Warnung bei fehlendem Snapshot Date
- Review Queue über Navigation/Filter nutzbar
- Buttons vorbereitet/deaktiviert:
  - Data Quality Recheck
  - Resolve Fixed Alerts
  - Review Items aktualisieren
  - Produktiven Import starten

Produktive Import-Buttons bleiben deaktiviert.

## Safe Mode / Write Enable

Weiter gestärkt:

- Runtime DB / echte Daten außerhalb Git bleibt sichtbar
- Write Mode bleibt default: `Read-only`
- Review-Metadaten-Aktionen sind fachlich getrennt von Portfolio-Writes
- produktive Portfolio-/Ledger-/Snapshot-Writes bleiben blockiert

Klare Trennung:

- Review-Metadaten schreiben: **erlaubt nach Bestätigung**
- Portfolio-/Ledger-/Snapshot schreiben: **weiterhin blockiert**

## Tests ergänzt

Neue/erweiterte Testabdeckung:

- Review Queue Filter nach Quelle
- Review Queue Filter nach Dry Run ID
- Review Queue Filter nach Quality Flag
- ISIN ergänzen erzeugt Audit
- Ticker/Exchange ergänzen erzeugt Audit
- Name allein bleibt nicht automatisch mapped
- Instrument Mapping mit ISIN kann `ready_for_import` werden
- Ticker ohne Exchange bleibt review-pflichtig
- Account Mapping Bestätigung erzeugt Audit
- Ignore with note funktioniert
- Ignore ohne Note wird abgelehnt
- Mark resolved nur bei gelösten Pflichtproblemen
- `ready_for_import` für Equity
- `ready_for_import` für ETF
- `ready_for_import` für Cash
- `aggregate_only` bleibt blockiert
- Dry Run archivieren / als aktuell markieren erzeugt Audit
- Data Quality Center zeigt Readiness Counts
- produktiver Import bleibt deaktiviert
- keine echten Dateien im Repo
- Git-Safety OK

Testergebnis:

- `155 passed`

## Runtime Review Item Counts nach Status

Aktueller Runtime-Stand nach B4-Readiness-Recheck:

### PostFinance

- open: `34`
- ready_for_import: `0`

### True Wealth

- open: `18`
- ready_for_import: `0`

### Raiffeisen

- open: `3`
- blocked: `4`
- ready_for_import: `0`

## Readiness Counts gesamt

- blocked: `4`
- not_ready: `50`
- review_needed: `5`
- ready_for_import: `0`

Das ist erwartbar: Es wurden noch keine echten manuellen ISIN-/Account-/Snapshot-Bestätigungen vorgenommen.

## Produktive Portfolio-Daten

Unverändert / keine produktiven Writes:

- `positions_snapshot`: `0`
- `transactions`: `0`
- `cash_balances`: `0`

## Commit / Push

- Commit: `1c5c3a0`
- Message: `feat: harden broker review queue mapping workflow`
- Push: **success**
- Git Status: **clean**

## Nächste Empfehlung

Phase B5 sollte sein:

**Guided Manual Review Pilot — kleine sichere Teilmenge freigeben, aber noch ohne produktiven Import.**

Vorschlag:

1. Eine Quelle auswählen, vermutlich PostFinance oder True Wealth.
2. 1–3 synthetisch harmlose echte Review Items im Dashboard manuell bearbeiten:
   - ISIN ergänzen
   - Ticker/Exchange ergänzen
   - Account Mapping bestätigen
   - Snapshot Date bestätigen
   - Instrument Mapping bestätigen
3. Prüfen, ob `ready_for_import` korrekt steigt.
4. Data Quality Center prüfen.
5. Audit History prüfen.
6. Weiterhin keinen produktiven Initial Snapshot committen.
7. Danach erst Phase B6: explizit freigegebener Mini-Import einer reviewed Safe Subset Position.

Kurz: Die Review Queue ist jetzt bedienbar und auditierbar. Noch schreibt sie brav nur Metadaten — kein Portfolio-Voodoo im Hintergrund.