from __future__ import annotations

from dataclasses import dataclass, field
from decimal import Decimal, InvalidOperation
from sqlite3 import Connection

from jarvis_finance.audit.log import record_audit_event
from jarvis_finance.crypto.assets import create_crypto_asset
from jarvis_finance.crypto.holdings import calculate_crypto_holdings, create_initial_holding_snapshot
from jarvis_finance.imports.common import stable_id, utc_now
from jarvis_finance.quality.alerts import create_alert

CRYPTO_MANUAL_REVIEW_COIN_LABELS = [
    "ADA Cardano",
    "Amp",
    "Avalanche AVAX",
    "Bitcoin BTC",
    "Chainlink",
    "DOGE",
    "Decentraland MANA",
    "Ethereum ETH",
    "HIGH Highstreet",
    "IOTA IOT",
    "LUNA Terra",
    "LUNC Terra Classic",
    "NETVR",
    "NuCypher NU",
    "Ox ZRX",
    "Polygon MATIC",
    "Ripple XRP",
    "SAND The Sandbox",
    "SAROS",
    "SKY",
    "SOL Solana",
    "Sundae",
    "USTC Terra Classic",
]

VALID_ADD_TYPES = {"initial_snapshot_addition", "manual_adjustment", "crypto_buy"}

@dataclass(frozen=True)
class CryptoManageResult:
    asset_id: str
    wallet_id: str
    holding_id: str | None = None
    transaction_id: str | None = None
    warnings: list[str] = field(default_factory=list)

def parse_decimal_text(value: str, *, field_name: str = "quantity") -> Decimal:
    text = str(value).strip()
    if not text:
        raise ValueError(f"{field_name} is required")
    try:
        dec = Decimal(text)
    except (InvalidOperation, ValueError) as exc:
        raise ValueError(f"{field_name} must be a Decimal string") from exc
    if not dec.is_finite():
        raise ValueError(f"{field_name} must be finite")
    return dec

def decimal_text(value: Decimal) -> str:
    return format(value, "f")

def _require_confirm(confirm: bool) -> None:
    if not confirm:
        raise ValueError("explicit confirm is required before saving")

def _require_date(effective_date: str) -> str:
    text = str(effective_date).strip()
    if not text:
        raise ValueError("date is required")
    # Keep validation intentionally simple and deterministic; Streamlit date_input supplies ISO dates.
    parts = text.split("-")
    if len(parts) != 3 or not all(part.isdigit() for part in parts):
        raise ValueError("date must be ISO YYYY-MM-DD")
    return text

def _require_wallet(conn: Connection, wallet_id: str) -> str:
    wallet_id = str(wallet_id).strip()
    if not wallet_id:
        raise ValueError("wallet is required")
    row = conn.execute("SELECT wallet_id FROM crypto_wallets WHERE wallet_id=?", (wallet_id,)).fetchone()
    if row is None:
        raise ValueError("wallet not found")
    return wallet_id

def _require_coin_fields(coin_name: str, symbol: str) -> tuple[str, str]:
    coin_name = str(coin_name).strip()
    symbol = str(symbol).strip().upper()
    if not coin_name:
        raise ValueError("coin name is required")
    if not symbol:
        raise ValueError("symbol is required")
    return coin_name, symbol

def _require_note(note: str, *, reason: str) -> str:
    note = str(note or "").strip()
    if not note:
        raise ValueError(f"note is required for {reason}")
    return note

def _find_asset(conn: Connection, *, coin_name: str, symbol: str, coingecko_id: str | None) -> str | None:
    if coingecko_id:
        row = conn.execute("SELECT asset_id FROM crypto_assets WHERE coingecko_id=?", (coingecko_id.strip(),)).fetchone()
        if row:
            return row["asset_id"]
    row = conn.execute(
        "SELECT asset_id FROM crypto_assets WHERE lower(coin_name)=lower(?) AND upper(symbol)=upper(?)",
        (coin_name, symbol),
    ).fetchone()
    return row["asset_id"] if row else None

def _ensure_asset(
    conn: Connection,
    *,
    coin_name: str,
    symbol: str,
    coingecko_id: str | None,
    coingecko_status: str,
    note: str | None,
) -> tuple[str, list[str]]:
    warnings: list[str] = []
    cg = coingecko_id.strip() if coingecko_id and coingecko_id.strip() else None
    existing = _find_asset(conn, coin_name=coin_name, symbol=symbol, coingecko_id=cg)
    if existing:
        asset_id = existing
    else:
        asset_id = create_crypto_asset(conn, coin_name=coin_name, symbol=symbol, coingecko_id=cg, notes=note)
    if not cg:
        warnings.append("missing_coingecko_id")
    if coingecko_status in {"missing_confirmed", "uncertain"}:
        rule = "crypto_coingecko_mapping_uncertain" if coingecko_status == "uncertain" else "missing_coingecko_id"
        create_alert(
            conn,
            priority="warnung",
            category="crypto",
            entity_type="crypto_asset",
            entity_id=asset_id,
            rule_id=rule,
            message="Crypto asset CoinGecko mapping needs manual confirmation.",
            evidence={"symbol": symbol, "coin_name": coin_name, "coingecko_status": coingecko_status},
            fingerprint=coingecko_status,
        )
        conn.commit()
        if rule not in warnings:
            warnings.append(rule)
    return asset_id, warnings

def current_wallet_asset_quantity(conn: Connection, *, wallet_id: str, asset_id: str) -> Decimal:
    result = calculate_crypto_holdings(conn)
    holding = result.wallet_holdings.get((wallet_id, asset_id))
    return holding.quantity if holding else Decimal("0")

def _insert_manual_adjustment_transaction(
    conn: Connection,
    *,
    asset_id: str,
    wallet_id: str,
    delta: Decimal,
    effective_date: str,
    note: str,
) -> str:
    if delta == 0:
        raise ValueError("manual adjustment delta must not be zero")
    now = utc_now()
    qty = abs(delta)
    tx_id = stable_id("cryptotx", "manual_adjustment", asset_id, wallet_id, decimal_text(delta), effective_date, now)
    from_wallet_id = wallet_id if delta < 0 else None
    to_wallet_id = wallet_id if delta > 0 else None
    conn.execute(
        """
        INSERT INTO crypto_transactions(
            crypto_transaction_id, transaction_type, asset_id, quantity,
            from_wallet_id, to_wallet_id, transaction_datetime, source,
            confirmation_status, parse_confidence, notes, created_at
        ) VALUES (?, 'manual_adjustment', ?, ?, ?, ?, ?, 'manual_dashboard', 'confirmed', 1, ?, ?)
        """,
        (tx_id, asset_id, decimal_text(qty), from_wallet_id, to_wallet_id, effective_date, note, now),
    )
    return tx_id

def add_crypto_position(
    conn: Connection,
    *,
    coin_name: str,
    symbol: str,
    wallet_id: str,
    quantity_text: str,
    effective_date: str,
    operation_type: str,
    note: str,
    confirm: bool,
    coingecko_id: str | None = None,
    coingecko_status: str = "selected",
) -> CryptoManageResult:
    _require_confirm(confirm)
    if operation_type not in VALID_ADD_TYPES:
        raise ValueError("unsupported crypto management operation type")
    if operation_type == "crypto_buy":
        raise ValueError("crypto_buy requires the dedicated ledger buy workflow; use manual_adjustment or initial_snapshot_addition in this MVP")
    effective_date = _require_date(effective_date)
    wallet_id = _require_wallet(conn, wallet_id)
    coin_name, symbol = _require_coin_fields(coin_name, symbol)
    quantity = parse_decimal_text(quantity_text)
    if quantity <= 0:
        raise ValueError("quantity must be > 0")
    if operation_type == "manual_adjustment":
        note = _require_note(note, reason="manual_adjustment")
    else:
        note = str(note or "").strip() or "manual initial snapshot addition"
    asset_id, warnings = _ensure_asset(conn, coin_name=coin_name, symbol=symbol, coingecko_id=coingecko_id, coingecko_status=coingecko_status, note=note)
    existing_holding = conn.execute(
        "SELECT crypto_holding_id FROM crypto_holdings WHERE asset_id=? AND wallet_id=?",
        (asset_id, wallet_id),
    ).fetchone()
    if operation_type == "initial_snapshot_addition":
        if existing_holding:
            raise ValueError("initial snapshot addition would overwrite an existing holding; use manual_adjustment")
        holding_id = create_initial_holding_snapshot(
            conn,
            asset_id=asset_id,
            wallet_id=wallet_id,
            quantity=quantity,
            verification_status="verified",
            last_verified_at=effective_date,
            legacy_snapshot_date=effective_date,
            note=note,
        )
        return CryptoManageResult(asset_id=asset_id, wallet_id=wallet_id, holding_id=holding_id, warnings=warnings)
    before = current_wallet_asset_quantity(conn, wallet_id=wallet_id, asset_id=asset_id)
    tx_id = _insert_manual_adjustment_transaction(conn, asset_id=asset_id, wallet_id=wallet_id, delta=quantity, effective_date=effective_date, note=note)
    after = before + quantity
    record_audit_event(
        conn,
        source="crypto_manage",
        action="crypto_manual_adjustment",
        entity_type="crypto_transaction",
        entity_id=tx_id,
        old_values={"asset_id": asset_id, "wallet_id": wallet_id, "quantity": decimal_text(before)},
        new_values={"asset_id": asset_id, "wallet_id": wallet_id, "quantity": decimal_text(after), "delta": decimal_text(quantity)},
        user_text_note=note,
        confirmed=True,
        created_by="dashboard",
    )
    conn.commit()
    return CryptoManageResult(asset_id=asset_id, wallet_id=wallet_id, transaction_id=tx_id, warnings=warnings)

def adjust_crypto_position(
    conn: Connection,
    *,
    asset_id: str,
    wallet_id: str,
    new_quantity_text: str,
    effective_date: str,
    note: str,
    confirm: bool,
) -> CryptoManageResult:
    _require_confirm(confirm)
    effective_date = _require_date(effective_date)
    wallet_id = _require_wallet(conn, wallet_id)
    note = _require_note(note, reason="manual_adjustment")
    new_quantity = parse_decimal_text(new_quantity_text, field_name="new quantity")
    if new_quantity < 0:
        raise ValueError("new quantity must be >= 0")
    asset = conn.execute("SELECT asset_id FROM crypto_assets WHERE asset_id=?", (asset_id,)).fetchone()
    if asset is None:
        raise ValueError("asset not found")
    before = current_wallet_asset_quantity(conn, wallet_id=wallet_id, asset_id=asset_id)
    delta = new_quantity - before
    if before + delta < 0:
        raise ValueError("negative holding blocked")
    tx_id = _insert_manual_adjustment_transaction(conn, asset_id=asset_id, wallet_id=wallet_id, delta=delta, effective_date=effective_date, note=note)
    record_audit_event(
        conn,
        source="crypto_manage",
        action="crypto_manual_adjustment",
        entity_type="crypto_transaction",
        entity_id=tx_id,
        old_values={"asset_id": asset_id, "wallet_id": wallet_id, "quantity": decimal_text(before)},
        new_values={"asset_id": asset_id, "wallet_id": wallet_id, "quantity": decimal_text(new_quantity), "delta": decimal_text(delta)},
        user_text_note=note,
        confirmed=True,
        created_by="dashboard",
    )
    if before != 0 and (new_quantity / before >= Decimal("10") or new_quantity == 0):
        create_alert(
            conn,
            priority="hinweis",
            category="crypto",
            entity_type="crypto_transaction",
            entity_id=tx_id,
            rule_id="crypto_manual_adjustment_review",
            message="Manual crypto adjustment should be reviewed because it materially changes the holding.",
            evidence={"asset_id": asset_id, "wallet_id": wallet_id},
            fingerprint="manual_adjustment_review",
        )
    conn.commit()
    return CryptoManageResult(asset_id=asset_id, wallet_id=wallet_id, transaction_id=tx_id)

def reduce_crypto_position(
    conn: Connection,
    *,
    asset_id: str,
    wallet_id: str,
    reduction_quantity_text: str,
    effective_date: str,
    note: str,
    confirm: bool,
) -> CryptoManageResult:
    _require_confirm(confirm)
    reduction = parse_decimal_text(reduction_quantity_text, field_name="reduction quantity")
    if reduction <= 0:
        raise ValueError("reduction quantity must be > 0")
    current = current_wallet_asset_quantity(conn, wallet_id=wallet_id, asset_id=asset_id)
    new_quantity = current - reduction
    if new_quantity < 0:
        raise ValueError("negative holding blocked")
    return adjust_crypto_position(
        conn,
        asset_id=asset_id,
        wallet_id=wallet_id,
        new_quantity_text=decimal_text(new_quantity),
        effective_date=effective_date,
        note=note,
        confirm=confirm,
    )

def get_crypto_management_options(conn: Connection) -> dict[str, list[dict[str, str]] | list[str]]:
    wallets = [
        {"wallet_id": row["wallet_id"], "wallet_name": row["wallet_name"], "wallet_type": row["wallet_type"]}
        for row in conn.execute("SELECT wallet_id, wallet_name, wallet_type FROM crypto_wallets WHERE is_active=1 ORDER BY wallet_name").fetchall()
    ]
    assets = [
        {"asset_id": row["asset_id"], "coin_name": row["coin_name"], "symbol": row["symbol"], "coingecko_id": row["coingecko_id"] or ""}
        for row in conn.execute("SELECT asset_id, coin_name, symbol, coingecko_id FROM crypto_assets WHERE is_active=1 ORDER BY coin_name, symbol").fetchall()
    ]
    holdings = [
        {
            "asset_id": row["asset_id"],
            "wallet_id": row["wallet_id"],
            "coin_name": row["coin_name"],
            "symbol": row["symbol"],
            "wallet_name": row["wallet_name"],
            "quantity": row["quantity"],
        }
        for row in conn.execute(
            """
            SELECT h.asset_id, h.wallet_id, a.coin_name, a.symbol, w.wallet_name, h.quantity
            FROM crypto_holdings h
            JOIN crypto_assets a ON a.asset_id=h.asset_id
            JOIN crypto_wallets w ON w.wallet_id=h.wallet_id
            ORDER BY a.coin_name, w.wallet_name
            """
        ).fetchall()
    ]
    return {
        "wallets": wallets,
        "assets": assets,
        "holdings": holdings,
        "manual_review_coin_labels": list(CRYPTO_MANUAL_REVIEW_COIN_LABELS),
    }


patch

        elif t == "fee":
            _apply_delta(result, row["from_wallet_id"], asset_id, -fee_qty)
    for asset in conn.execute("SELECT asset_id, symbol, coingecko_id FROM crypto_assets").fetchall():

        elif t == "fee":
            _apply_delta(result, row["from_wallet_id"], asset_id, -fee_qty)
        elif t == "manual_adjustment":
            if row["to_wallet_id"]:
                _apply_delta(result, row["to_wallet_id"], asset_id, qty)
            elif row["from_wallet_id"]:
                _apply_delta(result, row["from_wallet_id"], asset_id, -qty)
    for asset in conn.execute("SELECT asset_id, symbol, coingecko_id FROM crypto_assets").fetchall():


--- a//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/crypto/holdings.py
+++ b//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/crypto/holdings.py
@@ -135,6 +135,11 @@
             _apply_delta(result, row["from_wallet_id"], asset_id, -(qty + fee_qty))
         elif t == "fee":
             _apply_delta(result, row["from_wallet_id"], asset_id, -fee_qty)
+        elif t == "manual_adjustment":
+            if row["to_wallet_id"]:
+                _apply_delta(result, row["to_wallet_id"], asset_id, qty)
+            elif row["from_wallet_id"]:
+                _apply_delta(result, row["from_wallet_id"], asset_id, -qty)
     for asset in conn.execute("SELECT asset_id, symbol, coingecko_id FROM crypto_assets").fetchall():
         if not asset["coingecko_id"]:
             msg = "Crypto asset has no CoinGecko ID; current valuation is unavailable."


from __future__ import annotations

from datetime import date

from jarvis_finance.crypto.manage import (
    add_crypto_position,
    adjust_crypto_position,
    get_crypto_management_options,
    reduce_crypto_position,
)
from jarvis_finance.dashboard.components.tables import show_table
from jarvis_finance.dashboard import data

PAGE_TITLE = "Crypto Manage"

def _option_label(row: dict[str, str]) -> str:
    if "wallet_name" in row and "coin_name" in row:
        return f"{row['coin_name']} ({row['symbol']}) / {row['wallet_name']}"
    if "wallet_name" in row:
        return f"{row['wallet_name']} [{row.get('wallet_type', '')}]"
    return f"{row['coin_name']} ({row['symbol']})"

def _selected_row(rows: list[dict[str, str]], label: str | None) -> dict[str, str] | None:
    if not label:
        return None
    for row in rows:
        if _option_label(row) == label:
            return row
    return None

def render(st, conn) -> None:
    st.title("Crypto Manage")
    st.caption("Auditierbare manuelle Crypto-Ergänzungen. Keine Live-API-Abfragen beim Rendern; CoinGecko-Mapping ist manuell zu bestätigen.")
    st.warning("Sicherheitsregel: Bestände werden nicht still gelöscht oder überschrieben. Änderungen laufen über Initial-Snapshot-Ergänzungen oder manual_adjustment mit Audit-Log.")

    options = get_crypto_management_options(conn)
    wallets = options["wallets"]
    holdings = options["holdings"]
    wallet_labels = [_option_label(row) for row in wallets]
    holding_labels = [_option_label(row) for row in holdings]

    tabs = st.tabs(["Hinzufügen", "Entfernen", "Korrigieren", "Nachtragsliste"])

    with tabs[0]:
        st.subheader("Coin/Bestand hinzufügen")
        st.info("Symbol allein wird nie automatisch eindeutig behandelt. Wähle/erfasse CoinGecko-ID bewusst oder speichere ohne ID mit Warnung.")
        coin_name = st.text_input("Coin-Name", key="crypto_manage_add_coin_name")
        symbol = st.text_input("Symbol", key="crypto_manage_add_symbol")
        coingecko_id = st.text_input("CoinGecko-ID (optional/manuell bestätigt)", key="crypto_manage_add_cg")
        coingecko_status = st.selectbox("CoinGecko-Mapping-Status", ["selected", "uncertain", "missing_confirmed"], key="crypto_manage_add_cg_status")
        wallet_label = st.selectbox("Wallet", wallet_labels, key="crypto_manage_add_wallet") if wallet_labels else None
        quantity = st.text_input("Menge als Decimal-Text", key="crypto_manage_add_qty")
        op_type = st.selectbox("Typ", ["initial_snapshot_addition", "manual_adjustment", "crypto_buy"], key="crypto_manage_add_type")
        default_day = date(2025, 12, 31) if op_type == "initial_snapshot_addition" else date.today()
        effective_date = st.date_input("Datum", value=default_day, key="crypto_manage_add_date")
        note = st.text_area("Notiz (Pflicht bei manual_adjustment; empfohlen bei unsicherem Mapping)", key="crypto_manage_add_note")
        confirm = st.checkbox("Ich bestätige diese einzelne Änderung", key="crypto_manage_add_confirm")
        st.write({
            "Aktion": op_type,
            "Coin": coin_name,
            "Symbol": symbol,
            "CoinGecko-ID": coingecko_id or "<fehlt/unsicher>",
            "Wallet": wallet_label,
            "Datum": str(effective_date),
            "Hinweis": "Keine Massenänderung; Speicherung erst nach Bestätigung.",
        })
        if st.button("Bestätigt speichern", key="crypto_manage_add_save", type="primary"):
            try:
                wallet = _selected_row(wallets, wallet_label)
                result = add_crypto_position(
                    conn,
                    coin_name=coin_name or "",
                    symbol=symbol or "",
                    coingecko_id=coingecko_id or None,
                    coingecko_status=coingecko_status or "selected",
                    wallet_id=wallet["wallet_id"] if wallet else "",
                    quantity_text=quantity or "",
                    effective_date=str(effective_date),
                    operation_type=op_type,
                    note=note or "",
                    confirm=bool(confirm),
                )
                st.success("Änderung gespeichert und auditierbar erfasst.")
                st.write({"asset_id": result.asset_id, "wallet_id": result.wallet_id, "warnings": result.warnings})
            except Exception as exc:
                st.error(f"Speichern abgelehnt: {exc}")

    with tabs[1]:
        st.subheader("Coin/Bestand entfernen — via manual_adjustment, nicht löschen")
        st.warning("Historie bleibt erhalten. Es wird nichts still gelöscht; die Reduktion wird als bestätigte Korrektur erfasst.")
        holding_label = st.selectbox("Bestand", holding_labels, key="crypto_manage_reduce_holding") if holding_labels else None
        reduction_mode = st.radio("Aktion", ["Bestand auf 0 setzen", "Teilmenge reduzieren"], key="crypto_manage_reduce_mode")
        reduction_quantity = st.text_input("Teilmenge als Decimal-Text", key="crypto_manage_reduce_qty")
        effective_date = st.date_input("Datum", value=date.today(), key="crypto_manage_reduce_date")
        note = st.text_area("Notiz / Grund (Pflicht)", key="crypto_manage_reduce_note")
        confirm = st.checkbox("Ich bestätige diese Reduktion", key="crypto_manage_reduce_confirm")
        selected = _selected_row(holdings, holding_label)
        st.write({"Aktion": reduction_mode, "Bestand": holding_label, "Datum": str(effective_date), "Historie": "bleibt erhalten"})
        if st.button("Reduktion bestätigt speichern", key="crypto_manage_reduce_save"):
            try:
                if not selected:
                    raise ValueError("holding selection is required")
                qty = selected["quantity"] if reduction_mode == "Bestand auf 0 setzen" else (reduction_quantity or "")
                result = reduce_crypto_position(
                    conn,
                    asset_id=selected["asset_id"],
                    wallet_id=selected["wallet_id"],
                    reduction_quantity_text=qty,
                    effective_date=str(effective_date),
                    note=note or "",
                    confirm=bool(confirm),
                )
                st.success("Reduktion als manual_adjustment gespeichert.")
                st.write({"asset_id": result.asset_id, "wallet_id": result.wallet_id})
            except Exception as exc:
                st.error(f"Speichern abgelehnt: {exc}")

    with tabs[2]:
        st.subheader("Coin/Bestand korrigieren")
        holding_label = st.selectbox("Bestehender Bestand", holding_labels, key="crypto_manage_adjust_holding") if holding_labels else None
        new_quantity = st.text_input("Neue Menge als Decimal-Text", key="crypto_manage_adjust_new_qty")
        effective_date = st.date_input("Datum", value=date.today(), key="crypto_manage_adjust_date")
        note = st.text_area("Notiz / Grund (Pflicht)", key="crypto_manage_adjust_note")
        confirm = st.checkbox("Ich bestätige diese Korrektur", key="crypto_manage_adjust_confirm")
        selected = _selected_row(holdings, holding_label)
        st.write({"Aktion": "manual_adjustment", "Bestand": holding_label, "Neue Menge": new_quantity, "Datum": str(effective_date)})
        if st.button("Korrektur bestätigt speichern", key="crypto_manage_adjust_save"):
            try:
                if not selected:
                    raise ValueError("holding selection is required")
                result = adjust_crypto_position(
                    conn,
                    asset_id=selected["asset_id"],
                    wallet_id=selected["wallet_id"],
                    new_quantity_text=new_quantity or "",
                    effective_date=str(effective_date),
                    note=note or "",
                    confirm=bool(confirm),
                )
                st.success("Korrektur als manual_adjustment gespeichert.")
                st.write({"asset_id": result.asset_id, "wallet_id": result.wallet_id})
            except Exception as exc:
                st.error(f"Speichern abgelehnt: {exc}")

    with tabs[3]:
        st.subheader("Ausgelassene/manuell zu prüfende Coins")
        st.caption("Reminder-Liste aus dem Initial-Snapshot-Dry-Run. Diese Coins werden nicht automatisch importiert.")
        show_table(st, [{"coin_label": label, "status": "manual_review"} for label in options["manual_review_coin_labels"]])

    st.subheader("Aktuelle Crypto-Bestände nach lokaler DB")
    show_table(st, data.get_crypto_by_wallet(conn))


from . import page_11_settings_data_quality

PAGES = [

from . import page_11_settings_data_quality
from . import page_12_crypto_manage

PAGES = [


--- a//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/pages/__init__.py
+++ b//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/pages/__init__.py
@@ -12,6 +12,7 @@
 from . import page_09_alerts
 from . import page_10_audit
 from . import page_11_settings_data_quality
+from . import page_12_crypto_manage

 PAGES = [
     page_00_command_center,


    page_10_audit,
    page_11_settings_data_quality,
    page_12_crypto_manage,
]

    page_10_audit,
    page_11_settings_data_quality,
]


--- a//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/pages/__init__.py
+++ b//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/pages/__init__.py
@@ -27,4 +27,5 @@
     page_09_alerts,
     page_10_audit,
     page_11_settings_data_quality,
+    page_12_crypto_manage,
 ]


    page_11_settings_data_quality,
    page_12_crypto_manage,
)


--- a//home/agent/.hermes/repos/FinanceManager/tests/unit/test_dashboard_mvp.py
+++ b//home/agent/.hermes/repos/FinanceManager/tests/unit/test_dashboard_mvp.py
@@ -17,6 +17,7 @@
     page_09_alerts,
     page_10_audit,
     page_11_settings_data_quality,
+    page_12_crypto_manage,
 )
 from jarvis_finance.storage.database import connect_memory
 from jarvis_finance.storage.migrations import apply_migrations


        page_10_audit,
        page_11_settings_data_quality,
        page_12_crypto_manage,
    ]:

        page_10_audit,
        page_11_settings_data_quality,
    ]:


--- a//home/agent/.hermes/repos/FinanceManager/tests/unit/test_dashboard_mvp.py
+++ b//home/agent/.hermes/repos/FinanceManager/tests/unit/test_dashboard_mvp.py
@@ -72,6 +72,7 @@
         page_09_alerts,
         page_10_audit,
         page_11_settings_data_quality,
+        page_12_crypto_manage,
     ]:
         module.render(st, conn)
     assert any(call[0] in {"title", "header", "subheader"} for call in st.calls)


terminal

PYTHONPATH=src pytest tests/unit/test_crypto_manage.py tests/unit/test_dashboard_mvp.py -q


Output:
.............                                                            [100%]
13 passed in 0.13s


Explanation:
**Evaluating test requirements**

I think I need to ensure that full tests are likely to pass. I should consider directly importing from 12 instead of using a file. Do I need a numbered page for Streamlit? It seems like the main module uses pages. I might need to run everything fully, compile it, and check for safety. Also, I should inspect the differences beforehand. There are a lot of details to think through!

PYTHONPATH=src python -m compileall src tests && PYTHONPATH=src pytest tests -q


Output:
Listing 'src'...
Listing 'src/jarvis_finance'...
Compiling 'src/jarvis_finance/app.py'...
Listing 'src/jarvis_finance/audit'...
Compiling 'src/jarvis_finance/audit/decorators.py'...
Compiling 'src/jarvis_finance/audit/validators.py'...
Listing 'src/jarvis_finance/cli'...
Listing 'src/jarvis_finance/config'...
Compiling 'src/jarvis_finance/config/logging.py'...
Listing 'src/jarvis_finance/crypto'...
Compiling 'src/jarvis_finance/crypto/validators.py'...
Compiling 'src/jarvis_finance/crypto/valuation.py'...
Listing 'src/jarvis_finance/dashboard'...
Listing 'src/jarvis_finance/dashboard/components'...
Compiling 'src/jarvis_finance/dashboard/components/charts.py'...
Compiling 'src/jarvis_finance/dashboard/components/forms.py'...
Compiling 'src/jarvis_finance/dashboard/components/warnings.py'...
Compiling 'src/jarvis_finance/dashboard/main.py'...
Listing 'src/jarvis_finance/dashboard/pages'...
Compiling 'src/jarvis_finance/dashboard/pages/00_command_center.py'...
Compiling 'src/jarvis_finance/dashboard/pages/01_portfolio.py'...
Compiling 'src/jarvis_finance/dashboard/pages/02_platforms.py'...
Compiling 'src/jarvis_finance/dashboard/pages/03_equities_etfs.py'...
Compiling 'src/jarvis_finance/dashboard/pages/04_crypto.py'...
Compiling 'src/jarvis_finance/dashboard/pages/05_wallets.py'...
Compiling 'src/jarvis_finance/dashboard/pages/06_ledger.py'...
Compiling 'src/jarvis_finance/dashboard/pages/07_watchlist.py'...
Compiling 'src/jarvis_finance/dashboard/pages/08_reports.py'...
Compiling 'src/jarvis_finance/dashboard/pages/09_alerts.py'...
Compiling 'src/jarvis_finance/dashboard/pages/10_audit.py'...
Compiling 'src/jarvis_finance/dashboard/pages/11_settings_data_quality.py'...
Listing 'src/jarvis_finance/fx'...
Compiling 'src/jarvis_finance/fx/__init__.py'...
Compiling 'src/jarvis_finance/fx/conversion.py'...
Compiling 'src/jarvis_finance/fx/overrides.py'...
Compiling 'src/jarvis_finance/fx/providers.py'...
Compiling 'src/jarvis_finance/fx/rates.py'...
Listing 'src/jarvis_finance/imports'...
Compiling 'src/jarvis_finance/imports/validators.py'...
Listing 'src/jarvis_finance/ledger'...
Compiling 'src/jarvis_finance/ledger/performance.py'...
Compiling 'src/jarvis_finance/ledger/transactions.py'...
Compiling 'src/jarvis_finance/ledger/validators.py'...
Listing 'src/jarvis_finance/market'...
Listing 'src/jarvis_finance/market_data'...
Compiling 'src/jarvis_finance/market_data/__init__.py'...
Compiling 'src/jarvis_finance/market_data/cache.py'...
Compiling 'src/jarvis_finance/market_data/coingecko.py'...
Compiling 'src/jarvis_finance/market_data/providers.py'...
Compiling 'src/jarvis_finance/market_data/staleness.py'...
Listing 'src/jarvis_finance/models'...
Compiling 'src/jarvis_finance/models/__init__.py'...
Compiling 'src/jarvis_finance/models/datatypes.py'...
Compiling 'src/jarvis_finance/models/dto.py'...
Compiling 'src/jarvis_finance/models/enums.py'...
Listing 'src/jarvis_finance/quality'...
Compiling 'src/jarvis_finance/quality/checks.py'...
Compiling 'src/jarvis_finance/quality/rules.py'...
Listing 'src/jarvis_finance/reports'...
Compiling 'src/jarvis_finance/reports/__init__.py'...
Compiling 'src/jarvis_finance/reports/crypto_inventory.py'...
Compiling 'src/jarvis_finance/reports/crypto_pdf.py'...
Compiling 'src/jarvis_finance/reports/metadata.py'...
Compiling 'src/jarvis_finance/reports/render.py'...
Listing 'src/jarvis_finance/reports/templates'...
Compiling 'src/jarvis_finance/reports/templates/__init__.py'...
Listing 'src/jarvis_finance/storage'...
Compiling 'src/jarvis_finance/storage/repositories.py'...
Compiling 'src/jarvis_finance/storage/units_of_work.py'...
Listing 'tests'...
Compiling 'tests/conftest.py'...
Listing 'tests/fixtures'...
Listing 'tests/integration'...
Compiling 'tests/integration/test_dummy_data_load.py'...
Compiling 'tests/integration/test_end_to_end_dummy_portfolio.py'...
Listing 'tests/unit'...
Compiling 'tests/unit/test_audit.py'...
Compiling 'tests/unit/test_coingecko_provider.py'...
Compiling 'tests/unit/test_crypto.py'...
Compiling 'tests/unit/test_crypto_core.py'...
Compiling 'tests/unit/test_crypto_manage.py'...
Compiling 'tests/unit/test_crypto_report.py'...
Compiling 'tests/unit/test_csv_importers_extended.py'...
Compiling 'tests/unit/test_dashboard_mvp.py'...
Compiling 'tests/unit/test_fx.py'...
Compiling 'tests/unit/test_fx_logic.py'...
Compiling 'tests/unit/test_git_safety.py'...
Compiling 'tests/unit/test_importers_instruments.py'...
Compiling 'tests/unit/test_importers_platforms_accounts.py'...
Compiling 'tests/unit/test_imports.py'...
Compiling 'tests/unit/test_ledger_cash.py'...
Compiling 'tests/unit/test_ledger_cash_wac_positions.py'...
Compiling 'tests/unit/test_ledger_equities.py'...
Compiling 'tests/unit/test_market_alerts.py'...
Compiling 'tests/unit/test_quality.py'...
Compiling 'tests/unit/test_row_hash_dedup.py'...
Compiling 'tests/unit/test_schema.py'...
Compiling 'tests/unit/test_settings.py'...
Compiling 'tests/unit/test_transactions_importer.py'...
........................................................................ [ 81%]
................                                                         [100%]
88 passed in 0.37s


Explanation:
**Considering git safety and cache**

I need to clean the caches to ensure everything is tidy. Before that, it's probably a good idea to commit and check git status first; I want to stay on top of things! Also, I should look for any __pycache__ that might have been created and remove that. I think I’ll also check the diff to see if there are any changes that need my attention. Just being thorough!

set -euo pipefail
find . -type d \( -name __pycache__ -o -name .pytest_cache \) -prune -exec rm -rf {} +
PYTHONPATH=src python -m jarvis_finance.cli.main git-safety-scan .
git status --short


Output:
GIT_SAFETY_OK
 M src/jarvis_finance/crypto/holdings.py
 M src/jarvis_finance/dashboard/pages/__init__.py
 M tests/unit/test_dashboard_mvp.py
?? src/jarvis_finance/crypto/manage.py
?? src/jarvis_finance/dashboard/pages/page_12_crypto_manage.py
?? tests/unit/test_crypto_manage.py


Explanation:
**Validating report status**

I think I need to check if there's no report available. It seems I might want to inspect the differences in a summary to ensure everything is accurate and clear. I'll take a moment to verify this to prevent any misunderstandings or inaccuracies. It’s important that I do this step so that I can provide the best information possible. Let’s see what I find when I take a closer look!

git diff --stat && git diff -- src/jarvis_finance/crypto/manage.py src/jarvis_finance/crypto/holdings.py src/jarvis_finance/dashboard/pages/page_12_crypto_manage.py tests/unit/test_crypto_manage.py | sed -n '1,260p'


Output:
src/jarvis_finance/crypto/holdings.py          | 5 +++++
 src/jarvis_finance/dashboard/pages/__init__.py | 2 ++
 tests/unit/test_dashboard_mvp.py               | 2 ++
 3 files changed, 9 insertions(+)
diff --git a/src/jarvis_finance/crypto/holdings.py b/src/jarvis_finance/crypto/holdings.py
index db24c08..1538037 100644
--- a/src/jarvis_finance/crypto/holdings.py
+++ b/src/jarvis_finance/crypto/holdings.py
@@ -135,6 +135,11 @@ def calculate_crypto_holdings(conn: Connection) -> CryptoHoldingsResult:
             _apply_delta(result, row["from_wallet_id"], asset_id, -(qty + fee_qty))
         elif t == "fee":
             _apply_delta(result, row["from_wallet_id"], asset_id, -fee_qty)
+        elif t == "manual_adjustment":
+            if row["to_wallet_id"]:
+                _apply_delta(result, row["to_wallet_id"], asset_id, qty)
+            elif row["from_wallet_id"]:
+                _apply_delta(result, row["from_wallet_id"], asset_id, -qty)
     for asset in conn.execute("SELECT asset_id, symbol, coingecko_id FROM crypto_assets").fetchall():
         if not asset["coingecko_id"]:
             msg = "Crypto asset has no CoinGecko ID; current valuation is unavailable."


set -euo pipefail
git add src/jarvis_finance/crypto/holdings.py src/jarvis_finance/crypto/manage.py src/jarvis_finance/dashboard/pages/__init__.py src/jarvis_finance/dashboard/pages/page_12_crypto_manage.py tests/unit/test_dashboard_mvp.py tests/unit/test_crypto_manage.py
git commit -m "feat: add audited crypto management dashboard"


Output:
[main fc67bb3] feat: add audited crypto management dashboard
 6 files changed, 750 insertions(+)
 create mode 100644 src/jarvis_finance/crypto/manage.py
 create mode 100644 src/jarvis_finance/dashboard/pages/page_12_crypto_manage.py
 create mode 100644 tests/unit/test_crypto_manage.py


Output: fatal: could not read Username for 'https://github.com': Kein passendes Gerät bzw. keine passende Adresse gefunden


set -euo pipefail
git remote -v
if command -v gh >/dev/null 2>&1; then gh auth status || true; fi
if [ -f ~/.git-credentials ]; then echo GIT_CREDENTIALS_PRESENT; else echo GIT_CREDENTIALS_MISSING; fi
git status --short --branch


Output:
origin	https://github.com/Gamexgit/FinanceManager.git (fetch)
origin	https://github.com/Gamexgit/FinanceManager.git (push)
GIT_CREDENTIALS_MISSING
## main...origin/main [voraus 1]


Explanation:
**Considering credentials management**

I might need the GitHub authentication skill for this task. I loaded the workflow, but I’m not sure if I should use push credentials from the environment. I’ll check the `.env` file located in `/.hermes/`, but I won't output any secrets. Using the terminal sounds like the right approach to manage this securely. I want to ensure I handle everything carefully—protecting sensitive information is really important!

set -euo pipefail
for f in ~/.hermes/.env ~/.config/gh/hosts.yml; do
  if [ -f "$f" ]; then echo "FOUND:$f"; else echo "MISSING:$f"; fi
done
python - <<'PY'
import os
print('GITHUB_TOKEN_ENV=' + ('present' if os.getenv('GITHUB_TOKEN') else 'missing'))
print('GH_TOKEN_ENV=' + ('present' if os.getenv('GH_TOKEN') else 'missing'))
PY


Output:
FOUND:/home/agent/.hermes/.env
MISSING:/home/agent/.config/gh/hosts.yml
GITHUB_TOKEN_ENV=***
GH_TOKEN_ENV=***


set -euo pipefail
set -a
. ~/.hermes/.env
set +a
TOKEN="${GITHUB_TOKEN:-${GH_TOKEN:-}}"
if [ -z "$TOKEN" ]; then echo "NO_TOKEN"; exit 1; fi
git -c http.https://github.com/.extraheader="AUTHORIZATION: bearer $TOKEN" push origin main


Explanation:
**Inspecting environment variables**

I need to focus on inspecting environment variable names, not their values. It seems like a straightforward task, but I want to ensure I’m thorough in checking each one. I might want to make a list or something to keep track of them as I go. Hmm, I just want to make sure I’m not overlooking anything important! Let’s take a look at everything carefully to ensure I’m getting it right.

python - <<'PY'
from pathlib import Path
p=Path.home()/'.hermes/.env'
for line in p.read_text().splitlines():
    if '=' in line and not line.strip().startswith('#'):
        k=line.split('=',1)[0]
        if any(tok in k.upper() for tok in ['GITHUB','GH','TOKEN','PAT']):
            print(k)
PY


Output:
TELEGRAM_BOT_TOKEN
GITHUB_PAT
GITHUB_REPO


set -euo pipefail
set -a
. ~/.hermes/.env
set +a
TOKEN="${GITHUB_PAT:-}"
if [ -z "$TOKEN" ]; then echo "NO_PAT"; exit 1; fi
git -c http.https://github.com/.extraheader="AUTHORIZATION: bearer $TOKEN" push origin main


Output: fatal: could not read Username for 'https://github.com': Kein passendes Gerät bzw. keine passende Adresse gefunden


set -euo pipefail
set -a
. ~/.hermes/.env
set +a
TOKEN="${GITHUB_PAT:-}"
if [ -z "$TOKEN" ]; then echo "NO_PAT"; exit 1; fi
git push "https://x-access-token:${TOKEN}@github.com/Gamexgit/FinanceManager.git" main


Output:
remote: Write access to repository not granted.
fatal: konnte nicht auf 'https://github.com/Gamexgit/FinanceManager.git/' zugreifen: The requested URL returned error: 403


Explanation:
**Checking push status**

I’m noticing that the push might have failed due to no write access. It seems I need to confirm if everything is clean for the commit and push. I could check the remotes as well as the SSH settings. After committing, I should also verify using git status to ensure there are no changes left. It’s important to finalize validation after the commit to understand what went wrong with the push.

set -euo pipefail
find . -type d \( -name __pycache__ -o -name .pytest_cache \) -prune -exec rm -rf {} +
PYTHONPATH=src python -m jarvis_finance.cli.main git-safety-scan .
git status --short --branch
git rev-parse --short HEAD


Output:
GIT_SAFETY_OK
## main...origin/main [voraus 1]
fc67bb3


todo

Service- und Dashboard-Code für auditierte Crypto-Änderungen implementieren

Compile, pytest, Git-Safety, git status ausführen

Commit durchgeführt; Push auf main versucht, aber Remote verweigert Schreibzugriff

Kurzbericht mit Ergebnissen, Commit, Push-Status liefern


Repo-/Dashboard-/Crypto-Modelle inspizieren und TDD-Teststellen bestimmen

Failing Tests für Crypto Manage Use-Cases schreiben und RED verifizieren


Explanation:
**Resolving git push issues**

I'm noting that the final report needs to address changes in files, not real values. The terminal indicates six files changed, but the stat excludes new files due earlier. I think I need to mention the commit hash fc67bb3 and note that the push failed due to lack of write access. It might be good to inform the user about needing a personal access token or SSH. I want to keep everything concise before marking this complete!


Erledigt, Sir — der MVP-Block **„Manuelle Crypto-Ergänzungs-UI“** ist lokal implementiert, getestet und committed. Push wurde versucht, aber GitHub verweigert aktuell den Schreibzugriff für das vorhandene Token. Natürlich genau der Moment, in dem GitHub beschließt, britische Zurückhaltung zu üben.

## Implementierte UI-Funktionen

Neue Dashboard-Seite: **Crypto Manage**

Funktionen:

- **Coin/Bestand hinzufügen**
  - Coin-Name, Symbol, CoinGecko-ID manuell erfassbar
  - CoinGecko-Mapping-Status:
    - `selected`
    - `uncertain`
    - `missing_confirmed`
  - Wallet-Auswahl
  - Decimal-Menge als Text
  - Datumsauswahl
  - Typ:
    - `initial_snapshot_addition`
    - `manual_adjustment`
    - `crypto_buy` sichtbar, aber im MVP bewusst blockiert zugunsten dediziertem Ledger-Buy-Workflow
  - explizite Bestätigung vor Speicherung
  - Audit-Log
  - Data-Quality-Warnung bei fehlender/unsicherer CoinGecko-ID

- **Coin/Bestand entfernen**
  - kein Löschen
  - Reduktion läuft über `manual_adjustment`
  - Bestand auf 0 setzen möglich
  - Teilmenge reduzieren möglich
  - Notiz Pflicht
  - Audit-Log mit nachvollziehbarer Änderung

- **Coin/Bestand korrigieren**
  - kein direktes Überschreiben
  - neue Menge wird in Differenz umgerechnet
  - Speicherung als `manual_adjustment`
  - Notiz Pflicht
  - Audit-Log mit `old_values` / `new_values`

- **Nachtragsliste**
  - ausgelassene/manuell zu prüfende Coins sichtbar im Dashboard
  - keine automatische Importlogik
  - keine Mengen/Werte enthalten

- **No-live-API**
  - Dashboard-Render macht keine CoinGecko/API-Abfrage
  - CoinGecko-Auswahl ist manuell vorbereitet, spätere Cache-/Suchintegration bleibt möglich

## Wichtige Designentscheidungen

- `crypto_holdings` wird nicht still überschrieben.
- Initial-Snapshot-Ergänzung wird nur erlaubt, wenn für `asset_id + wallet_id` noch kein Holding existiert.
- Korrekturen laufen über `crypto_transactions.transaction_type = manual_adjustment`.
- Holding-Berechnung berücksichtigt jetzt `manual_adjustment`.
- Mengen bleiben Decimal/Text; keine Float-Konvertierung.
- Kleine Mengen wie `0.00000001` bleiben exakt erhalten.
- `crypto_buy` ist in der UI vorbereitet, aber noch nicht als vereinfachtes Formular produktiv freigegeben, weil echte Buys sauber in den Cash-/Ledger-Kontext gehören.

## Neue/geänderte Dateien

Neu:

- `src/jarvis_finance/crypto/manage.py`
- `src/jarvis_finance/dashboard/pages/page_12_crypto_manage.py`
- `tests/unit/test_crypto_manage.py`

Geändert:

- `src/jarvis_finance/crypto/holdings.py`
- `src/jarvis_finance/dashboard/pages/__init__.py`
- `tests/unit/test_dashboard_mvp.py`

## Neue Tests

Abgedeckt:

- Add Coin / Initial Snapshot Addition
- Add Coin mit CoinGecko-ID
- Add Coin ohne CoinGecko-ID erzeugt Warning
- Decimal-Menge bleibt exakt
- Manual Adjustment mit Notiz
- Manual Adjustment ohne Notiz wird abgelehnt
- Bestand auf 0 setzen ohne Löschen
- negative Menge wird abgelehnt
- Audit-Log bei bestätigter Änderung
- `old_values` / `new_values` bei Korrektur
- Dashboard-Module importieren sauber
- Crypto Manage Page rendert im Dashboard-Smoke-Test
- keine Live-API beim Crypto-Dashboard-Render bleibt abgedeckt

## Validierung

Ausgeführt:

```bash
PYTHONPATH=src python -m compileall src tests
PYTHONPATH=src pytest tests -q
PYTHONPATH=src python -m jarvis_finance.cli.main git-safety-scan .
git status
```

Ergebnis:

- Compile: **ok**
- Tests: **88 passed**
- Git-Safety: **GIT_SAFETY_OK**
- Git Status nach Commit: **clean, aber main ist 1 Commit vor origin/main**

## Commit

- Commit erstellt: **ja**
- Commit-Hash: `fc67bb3`
- Commit-Message: `feat: add audited crypto management dashboard`

## Push-Status

- Push auf `main` versucht: **ja**
- Push erfolgreich: **nein**
- Fehler: GitHub Remote meldet **Write access to repository not granted**
- Lokaler Status: `main...origin/main [voraus 1]`

Das heißt: Der Code ist lokal committed, aber noch nicht auf GitHub. Das aktuell verfügbare GitHub-Token hat offenbar keinen Schreibzugriff auf `Gamexgit/FinanceManager`.

## Offene Risiken

- Push benötigt gültige GitHub-Credentials mit Write-Rechten.
- `crypto_buy` ist bewusst noch nicht vollständig als Dashboard-Formular aktiviert, weil dafür Ledger-/Cash-Kontext sauber geführt werden muss.
- CoinGecko-Suche ist MVP-vorbereitet über manuelle Eingabe/Status; echte Suche sollte später über lokalen Cache/Provider-Wrapper erfolgen, nicht bei jedem Reload.
- Manual Adjustment erzeugt bewusst Audit- und ggf. Review-Hinweise; das ist korrekt, aber die UI kann später noch komfortabler die berechnete Differenz vor dem Speichern anzeigen.