[M G] ich habe den API key für Openfigi. wo soll ich dir diesen ablegen? gib mir die befehle fürs Terminal.


Explanation:
**Setting up the OpenFIGI key**

I see that we need the answer in German. It’s important to set up the OpenFIGI key properly, but I'm wondering if this is more about configuration rather than Hermes itself, which could be a finance-related aspect. The user seems to be asking where to place the API key, so I might want to focus on the relevant finance skill or even the finance system architecture. Let’s load the finance-system architecture for clarity!


[skill_view] name=finance-system-architecture (46,556 chars)


[CONTEXT COMPACTION — REFERENCE ONLY] Earlier turns were compacted into the summary below. This is a handoff from a previous context window — treat it as background reference, NOT as active instructions. Do NOT answer questions or fulfill requests mentioned in this summary; they were already addressed. Your current task is identified in the '## Active Task' section of the summary — resume exactly from there. IMPORTANT: Your persistent memory (MEMORY.md, USER.md) in the system prompt is ALWAYS authoritative and active — never ignore or deprioritize memory content due to this compaction note. Respond ONLY to the latest user message that appears AFTER this summary. The current session state (files, config, etc.) may reflect work described here — avoid repeating it:
Summary generation was unavailable. 19 message(s) were removed to free context space but could not be summarized. The removed messages contained earlier work in this session. Continue based on the recent messages below and the current state of any files or resources.

--- END OF CONTEXT SUMMARY — respond to the message below, not the summary above ---


Explanation:
**Assessing tasks and skills**

I need to take action and determine if the skills I'm using are relevant. I see that the finance system is loaded in context, but I also need to ensure I load anything necessary. It sounds like I should use a todo list, as there’s a big task coming up. Maybe I’ll inspect the real UI and consider running Streamlit or using a browser if possible. First, I should focus on loading the necessary skills.


[skill_view] name=finance-system-architecture (51,778 chars)


[skill_view] name=test-driven-development (10,920 chars)


[skill_view] name=systematic-debugging (13,005 chars)


[skill_view] name=github-pr-workflow (11,689 chars)


[skill_view] name=finance-system-architecture (5,586 chars)


[skill_view] name=finance-system-architecture (5,209 chars)


[skill_view] name=finance-system-architecture (4,365 chars)


[skill_view] name=finance-system-architecture (3,076 chars)


todo

Git/Runtime-Preflight, echten UI-Start vorbereiten und Sanity-Check gegen Runtime-DB durchführen

Regressionstests für Alerts, Navigation, Cleanup, FX, Equity-Interaktion, Cash und no-live-render ergänzen und RED verifizieren

Alerts-Duplicate-Button-Keys reparieren und User/Admin-Navigation strikt vereinfachen

Runtime-Backup erstellen, Review/Test/DOCX Equity-Artefakte trocken identifizieren und eindeutig auditierbar entfernen

FX-UX und Cash-Workflow nutzerfreundlich/funktional machen

Aktien/ETF-Seite mit Summary Cards, Gruppierung, Sortierung, Auswahl, Details, Korrigieren, Verlauf umbauen

Command Center und minimalen CoinGecko-Link korrigieren

Dashboard erneut gegen Runtime-DB starten und User-Mode-Seiten aggregiert prüfen

Compile, pytest, Git-Safety, git diff --check, Status verifizieren

Commit, Push und remote Hash verifizieren


[todo] updated task list


terminal

git status --short --branch && git log -1 --oneline


Output:
## main...origin/main
725a493 fix: harden MVP finance dashboard acceptance flows


[search_files] files search for '*.py' in /home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard -> 30 matches


[read_file] read /home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/views/page_09_alerts.py from line 1 (1,382 chars)


[read_file] read /home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/main.py from line 1 (6,373 chars)


[read_file] read /home/agent/.hermes/repos/FinanceManager/tests/unit/test_dashboard_ux_sprint2.py from line 1 (10,689 chars)


[read_file] read /home/agent/.hermes/repos/FinanceManager/tests/unit/test_dashboard_mvp_regressions.py from line 1 (9,924 chars)


[read_file] read /home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/views/page_03_equities_etfs.py from line 1 (5,917 chars)


[read_file] read /home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/views/page_00_command_center.py from line 1 (5,885 chars)


Explanation:
**Planning UI Implementation**

I think I need to consider running Streamlit for a better user interface. Before diving into that, I should read up on data functions and the schema to get a clear understanding. Implementing a cleanup module will also be important, and I need to prioritize speed. Additionally, I should inspect the schema related to tests, especially since I don’t want to use sed for this Streamlit launch. Let's keep things organized!

python - <<'PY'
from pathlib import Path
p=Path('~/jarvis_runtime/finance-system/data/finance.sqlite3').expanduser()
print('runtime_db_exists', p.exists(), 'path', p)
PY


Output: runtime_db_exists True path /home/agent/jarvis_runtime/finance-system/data/finance.sqlite3


python -m streamlit run src/jarvis_finance/dashboard/main.py --server.headless true --server.port 8501 --browser.gatherUsageStats false


Output: Background process started


process

[process] poll session=proc_e8c2d7e19762


Explanation:
**Checking port usage**

It looks like I might need to check if the port is already in use. I think I can go ahead and use port 8502 instead as a backup option. It's essential to ensure that the dashboard is functioning correctly and that there are no conflicts. I'll want to remember to verify this before proceeding to avoid any hiccups in the process. Let's keep things smooth sailing!

terminal

python -m streamlit run src/jarvis_finance/dashboard/main.py --server.headless true --server.port 8502 --browser.gatherUsageStats false


Output: Background process started


process

[process] poll session=proc_aa99dc12edd3


[browser_navigate] http://localhost:8502 (511 chars)


[browser_snapshot] (8,425 chars)


[browser_snapshot] (7,315 chars)


[browser_snapshot] (6,547 chars)


[browser_snapshot] (4,466 chars)


Explanation:
**Inspecting server issue**

I've confirmed that there has been a crash. My next steps involve stopping the server later and inspecting the data functions. I want to ensure that everything is running smoothly before making any changes. It's crucial to gather all necessary information to diagnose the problem, so I'm careful about how I proceed. This investigation will help me understand what went wrong and how to fix it effectively.

def get_positions|def get_command_center_summary|def get_alert_cards|def get_crypto_coin_detail|def get_portfolio_positions_user


[read_file] read /home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/data.py from line 500 (37,702 chars)


Explanation:
**Implementing code execution**

I think I need to implement some patches, and using `execute_code` could make it easier for multiple files. But I must avoid using heredoc. It seems like `execute_code` is okay, so I need to set up tests as well. I need to determine how to display the table with `show_table` no matter what. Also, there's a need for cleanup in the module, particularly since the existing runtime cleanup in adhoc isn't code. I guess I'll add `jarvis_finance/runtime_cleanup` and search for any backups.


[read_file] read /home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/runtime/backup.py from line 1 (5,462 chars)


[Your active task list was preserved across context compression]
- [>] preflight. Git/Runtime-Preflight, echten UI-Start vorbereiten und Sanity-Check gegen Runtime-DB durchführen (in_progress)
- [ ] redtests. Regressionstests für Alerts, Navigation, Cleanup, FX, Equity-Interaktion, Cash und no-live-render ergänzen und RED verifizieren (pending)
- [ ] fixalertsnav. Alerts-Duplicate-Button-Keys reparieren und User/Admin-Navigation strikt vereinfachen (pending)
- [ ] cleanup. Runtime-Backup erstellen, Review/Test/DOCX Equity-Artefakte trocken identifizieren und eindeutig auditierbar entfernen (pending)
- [ ] fixfxcash. FX-UX und Cash-Workflow nutzerfreundlich/funktional machen (pending)
- [ ] fixequityui. Aktien/ETF-Seite mit Summary Cards, Gruppierung, Sortierung, Auswahl, Details, Korrigieren, Verlauf umbauen (pending)
- [ ] fixcommandcrypto. Command Center und minimalen CoinGecko-Link korrigieren (pending)
- [ ] uisanity. Dashboard erneut gegen Runtime-DB starten und User-Mode-Seiten aggregiert prüfen (pending)
- [ ] verify. Compile, pytest, Git-Safety, git diff --check, Status verifizieren (pending)
- [ ] commitpush. Commit, Push und remote Hash verifizieren (pending)


Explanation:
**Continuing the task**

I need to continue working on this. I should inspect the files carefully, making sure everything is in order. It’s important to use the right tools for the job here. I must execute the necessary steps, and I might need to implement a lot of different strategies. I’ll focus on executing code and reading the files to gather all the information I need. Let's get started!

todo

Git/Runtime-Preflight, echten UI-Start vorbereiten und Sanity-Check gegen Runtime-DB durchführen

Regressionstests für Alerts, Navigation, Cleanup, FX, Equity-Interaktion, Cash und no-live-render ergänzen und RED verifizieren


[todo] updated task list


[read_file] read /home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/views/page_17_position_add.py from line 1 (21,252 chars)


[read_file] read /home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/equity/manage.py from line 1 (42,290 chars)


[read_file] read /home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/views/page_04_crypto.py from line 1 (11,123 chars)


Explanation:
**Inspecting script cleanup**

I see I need to work on cleaning up the current script and inspecting its schema. This is probably about improving its efficiency or perhaps fixing any issues that might have arisen. I’ll take my time to ensure I understand how everything is supposed to function and identify what changes need to be made. Making sure everything is in good order is crucial for smooth operation. Let's get started!


[read_file] read /home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/views/page_09_alerts.py from line 1 (1,382 chars)


[read_file] read /home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/main.py from line 1 (6,567 chars)


Explanation:
**Planning testing infrastructure**

I have a lot to do. I need to set up the test infrastructure and maybe create some fake tests to start. It’s also important to inspect the style of those tests. I’m considering how to approach this clearly and effectively. I'll make sure everything is organized so that I can keep track of my progress. Alright, let’s see how I can get this done!


[read_file] read /home/agent/.hermes/repos/FinanceManager/tests/unit/test_dashboard_ux_sprint2.py from line 1 (10,689 chars)


terminal

python - <<'PY'
import sqlite3
from pathlib import Path
p=Path('~/jarvis_runtime/finance-system/data/finance.sqlite3').expanduser()
conn=sqlite3.connect(p)
for t in ['transactions','accounts','instruments','alerts','audit_log','market_prices','fx_rates']:
 print('\n--',t)
 for row in conn.execute(f'pragma table_info({t})'):
  print(row)
PY


[terminal] ran `python - <<'PY'
import sqlite3
from pathlib import Path
p=Path('~/jarvis_runt...` -> exit 0, 1 lines output


Explanation:
**Considering implementation details**

I need to ensure I've got enough functionality implemented. I might consider using a write method and also think about cleaning up the module. Adding functions in the equity/manage section could be beneficial. There's also the thought of executing a runtime cleanup later. I need to check on how the positions calculation uses the price date in FX to make sure everything aligns correctly. Let's keep everything tidy and functional!


[read_file] read /home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/ledger/positions.py from line 1 (18,051 chars)


Explanation:
**Planning FX rate updates**

I’m considering whether the latest foreign exchange rate function needs a date parameter. It seems like it should only return rates that are equal to or less than today’s date — that makes sense! I'll focus on writing a helper module to handle this cleanup. After the implementation, I can think about adding tests, but time is a factor, so I might tackle the coding first and then add tests later. Using a Python script for patching sounds like a good plan.


[read_file] read /home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/audit/log.py from line 1 (1,934 chars)


from __future__ import annotations

from dataclasses import dataclass
from sqlite3 import Connection

from jarvis_finance.audit.log import record_audit_event
from jarvis_finance.imports.common import ...[truncated]


def get_command_center_summary|def equity_etf_user_label|def get_crypto_coin_detail


     1|from __future__ import annotations
     2|
     3|import json
     4|from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
     5|from pathlib import Path
     6|from sqlite3 import Connection
     7|
     8|from jarvis_finance.config.settings import Settings
     9|from jarvis_finance.crypto.holdings import calculate_crypto_holdings
    10|from jarvis_finance.ledger.cash import calculate_cash_balances
    11|from jarvis_finance.ledger.positions import calculate_positions
    12|from jarvis_finance.market.providers import is_price_stale
    13|
    14|MONEY = Decimal("0.01")
    15|
    16|
    17|def d(value: object, default: str = "0") -> Decimal:
    18|    if value is None or str(value).strip() == "":
    19|        return Decimal(default)
    20|    try:
    21|        return Decimal(str(value))
    22|    except InvalidOperation:
    23|        return Decimal(default)
    24|
    25|
    26|def dec_text(value: Decimal | int | str | None, places: str | None = None) -> str:
    27|    if value is None:
    28|        return ""
    29|    value = d(value)
    30|    if places:
    31|        value = value.quantize(Decimal(places), rounding=ROUND_HALF_UP)
    32|    return format(value, "f")
    33|
    34|
    35|def grouped_number(value: Decimal | int | str, places: str = "0.01") -> str:
    36|    text = dec_text(value, places)
    37|    whole, _, frac = text.partition(".")
    38|    sign = "-" if whole.startswith("-") else ""
    39|    whole = whole.lstrip("-")
    40|    groups: list[str] = []
    41|    while whole:
    42|        groups.append(whole[-3:])
    43|        whole = whole[:-3]
    44|    grouped = "'".join(reversed(groups)) if groups else "0"
    45|    return f"{sign}{grouped}{('.' + frac) if frac else ''}"
    46|
    47|
    48|def chf_text(value: Decimal | int | str | None) -> str:
    49|    if value is None:
    50|        return "nicht bewertet"
    51|    return f"CHF {grouped_number(value)}"
    52|
    53|
    54|def date_text(value: str | None) -> str:
    55|    if not value:
    56|        return ""
    57|    raw = str(value)
    58|    if "T" in raw:
    59|        return raw.split("T", 1)[0]
    60|    return raw[:10]
    61|
    62|
    63|def money_text(value: Decimal | None, currency: str = "CHF") -> str:
    64|    if currency.upper() == "CHF":
    65|        return chf_text(value)
    66|    if value is None:
    67|        return f"n/a {currency}"
    68|    return f"{dec_text(value, '0.01')} {currency}"
    69|
    70|
    71|STATUS_LABELS = {
    72|    "missing_market_price": "Preis fehlt",
    73|    "missing_provider_symbol": "Mapping fehlt",
    74|    "missing_price": "Preis fehlt",
    75|    "missing_fx": "FX-Kurs fehlt",
    76|    "cost_basis_uncertain": "Einstand unvollständig",
    77|    "incomplete": "Unvollständig",
    78|    "snapshot_only": "Snapshot-only",
    79|    "fresh": "Aktuell",
    80|    "stale": "Veraltet",
    81|    "missing": "Preis fehlt",
    82|    "unknown": "Ungeklärt",
    83|    "mapped": "Zugeordnet",
    84|    "confirmed": "Bestätigt",
    85|    "user_confirmed": "Bestätigt",
    86|    "ok": "Aktuell",
    87|    "warning": "Hinweis",
    88|    "critical": "Kritisch",
    89|    "kritisch": "Kritisch",
    90|    "warnung": "Wichtig",
    91|    "info": "Info",
    92|    "active": "Aktiv",
    93|    "inactive": "Inaktiv",
    94|    "verified": "Verifiziert",
    95|}
    96|
    97|
    98|def translate_status(status: str | None) -> str:
    99|    if not status:
   100|        return "Ungeklärt"
   101|    return STATUS_LABELS.get(str(status), str(status).replace("_", " ").capitalize())
   102|
   103|
   104|def is_technical_note(text: str | None) -> bool:
   105|    if not text:
   106|        return False
   107|    lowered = text.lower()
   108|    markers = ["productive initial crypto snapshot import", "no wallet address from xlsx", "initial crypto snapshot import"]
   109|    return any(marker in lowered for marker in markers)
   110|
   111|
   112|def user_note(text: str | None) -> str:
   113|    return "" if is_technical_note(text) else (text or "")
   114|
   115|
   116|def visible_user_columns(row: dict[str, str]) -> list[str]:
   117|    hidden = {"asset_id", "wallet_id", "instrument_id", "entity_id", "rule_id", "source_id", "provider_symbol_status", "hedge_status", "dedup_key", "fingerprint", "raw_json"}
   118|    return [k for k in row if not k.startswith("_") and k not in hidden]
   119|
   120|
   121|def percent_text(value: Decimal | None) -> str:
   122|    if value is None:
   123|        return ""
   124|    return f"{dec_text(value, '0.01')}%"
   125|
   126|
   127|def price_status(price: dict[str, str] | None) -> str:
   128|    if not price:
   129|        return "missing"
   130|    return price.get("quality_status") or "missing"
   131|
   132|
   133|def _latest_price_time(conn: Connection, *, currency: str = "CHF") -> str:
   134|    row = conn.execute(
   135|        """
   136|        SELECT COALESCE(MAX(provider_timestamp), MAX(fetched_at)) AS ts
   137|        FROM crypto_prices WHERE price_currency=?
   138|        """,
   139|        (currency.upper(),),
   140|    ).fetchone()
   141|    return row["ts"] if row and row["ts"] else ""
   142|
   143|
   144|def _crypto_value_rows(conn: Connection, *, price_max_age_seconds: int = 86_400) -> list[dict[str, object]]:
   145|    holdings = calculate_crypto_holdings(conn)
   146|    rows: list[dict[str, object]] = []
   147|    for asset_id, holding in sorted(holdings.total_by_asset.items()):
   148|        asset = conn.execute("SELECT * FROM crypto_assets WHERE asset_id=?", (asset_id,)).fetchone()
   149|        if not asset:
   150|            continue
   151|        price = latest_crypto_price(conn, asset_id, "CHF", max_age_seconds=price_max_age_seconds)
   152|        price_dec = d(price["price"]) if price and price["price"] else None
   153|        value = holding.quantity * price_dec if price_dec is not None else None
   154|        wallets = {wid for (wid, aid), wh in holdings.wallet_holdings.items() if aid == asset_id and wh.quantity != 0}
   155|        rows.append({
   156|            "asset_id": asset_id,
   157|            "coin": asset["coin_name"],
   158|            "symbol": asset["symbol"],
   159|            "coingecko_id": asset["coingecko_id"] or "",
   160|            "quantity": holding.quantity,
   161|            "price": price_dec,
   162|            "value": value,
   163|            "wallet_count": len(wallets),
   164|            "price_status": price_status(price),
   165|            "last_price_update": (price["provider_timestamp"] or price["fetched_at"]) if price else "",
   166|        })
   167|    return rows
   168|
   169|
   170|def rowdicts(rows) -> list[dict[str, str]]:
   171|    return [{k: ("" if row[k] is None else str(row[k])) for k in row.keys()} for row in rows]
   172|
   173|
   174|def db_exists(path: str | Path) -> bool:
   175|    return Path(path).expanduser().exists()
   176|
   177|
   178|def runtime_outside_repo(settings: Settings, repo_root: Path) -> bool:
   179|    try:
   180|        settings.runtime_paths.base_dir.resolve().relative_to(repo_root.resolve())
   181|        return False
   182|    except ValueError:
   183|        return True
   184|
   185|
   186|def latest_crypto_price(conn: Connection, asset_id: str, currency: str = "CHF", *, max_age_seconds: int = 86_400) -> dict[str, str] | None:
   187|    row = conn.execute(
   188|        """
   189|        SELECT price, price_currency, fetched_at, provider_timestamp, quality_status, provider
   190|        FROM crypto_prices
   191|        WHERE asset_id=? AND price_currency=?
   192|        ORDER BY COALESCE(provider_timestamp, fetched_at, '') DESC, fetched_at DESC LIMIT 1
   193|        """,
   194|        (asset_id, currency.upper()),
   195|    ).fetchone()
   196|    if not row:
   197|        return None
   198|    item = dict(row)
   199|    ts = item.get("provider_timestamp") or item.get("fetched_at")
   200|    if item.get("quality_status") == "fresh" and is_price_stale(ts, max_age_seconds=max_age_seconds):
   201|        item["quality_status"] = "stale"
   202|    return item
   203|
   204|
   205|def get_crypto_summary_cards(conn: Connection, *, price_max_age_seconds: int = 86_400) -> dict[str, str]:
   206|    rows = _crypto_value_rows(conn, price_max_age_seconds=price_max_age_seconds)
   207|    holdings = calculate_crypto_holdings(conn)
   208|    total_value = sum((r["value"] for r in rows if r["value"] is not None), Decimal("0"))
   209|    missing = sum(1 for r in rows if r["price_status"] == "missing")
   210|    stale = sum(1 for r in rows if r["price_status"] == "stale")
   211|    wallets_with_holdings = {wid for (wid, _aid), holding in holdings.wallet_holdings.items() if holding.quantity != 0}
   212|    status = "ok" if missing == 0 and stale == 0 else ("warning" if missing or stale else "ok")
   213|    return {
   214|        "total_value_chf": dec_text(total_value),
   215|        "coin_count": str(len(rows)),
   216|        "wallets_with_holdings": str(len(wallets_with_holdings)),
   217|        "holding_count": str(sum(1 for h in holdings.wallet_holdings.values() if h.quantity != 0)),
   218|        "coins_without_price": str(missing),
   219|        "stale_price_count": str(stale),
   220|        "last_price_update_at": _latest_price_time(conn),
   221|

Use offset=221 to continue reading (showing 1-220 of 1122 lines)


   300|        item = {
   301|            "wallet": wallet["wallet_name"] if wallet else wallet_id,
   302|            "typ": wallet["wallet_type"] if wallet else "",
   303|            "menge": dec_text(holding.quantity),
   304|            "wert_chf": chf_text(value) if value is not None else "nicht bewertet",
   305|            "anteil": percent_text(share),
   306|            "letzte_verifikation": date_text(holding.last_verified_at),
   307|            "status": translate_status(holding.verification_status),
   308|            "notiz": user_note(wallet["notes"] if wallet and wallet["notes"] else ""),
   309|            "_wallet_id": wallet_id,
   310|            "_asset_id": asset_id,
   311|        }
   312|        if admin_mode:
   313|            item.update({"wallet_id": wallet_id, "asset_id": asset_id})
   314|        rows.append(item)
   315|    return rows
   316|
   317|
   318|def get_wallet_overview_cards(conn: Connection, *, price_max_age_seconds: int = 86_400) -> dict[str, str]:
   319|    rows = get_wallet_user_overview(conn, price_max_age_seconds=price_max_age_seconds)
   320|    total_value = sum((d(r["_numeric_value_chf"]) for r in rows), Decimal("0"))
   321|    missing_verification = sum(1 for r in rows if not r.get("last_verified_at"))
   322|    return {"wallet_count": str(len(rows)), "total_value_chf": dec_text(total_value), "wallets_missing_verification": str(missing_verification)}
   323|
   324|
   325|def get_wallet_user_overview(conn: Connection, *, price_max_age_seconds: int = 86_400, admin_mode: bool = False) -> list[dict[str, str]]:
   326|    holdings = calculate_crypto_holdings(conn)
   327|    rows: list[dict[str, str]] = []
   328|    for wallet in conn.execute("SELECT * FROM crypto_wallets ORDER BY wallet_name").fetchall():
   329|        value = Decimal("0")
   330|        has_missing_price = False
   331|        coins: set[str] = set()
   332|        for (wallet_id, asset_id), holding in holdings.wallet_holdings.items():
   333|            if wallet_id != wallet["wallet_id"] or holding.quantity == 0:
   334|                continue
   335|            coins.add(asset_id)
   336|            price = latest_crypto_price(conn, asset_id, "CHF", max_age_seconds=price_max_age_seconds)
   337|            if price and price.get("price"):
   338|                value += holding.quantity * d(price["price"])
   339|            else:
   340|                has_missing_price = True
   341|        item = {
   342|            "wallet_name": wallet["wallet_name"],
   343|            "wallet_type": wallet["wallet_type"],
   344|            "provider": wallet["platform_provider"] or "",
   345|            "total_value_chf": chf_text(value) if not has_missing_price else f"{chf_text(value)} + unbewertet",
   346|            "coin_count": str(len(coins)),
   347|            "last_verified_at": date_text(wallet["last_verified_at"]),
   348|            "status": translate_status("active" if int(wallet["is_active"] or 0) else "inactive"),
   349|            "actions": "Öffnen · Coin hinzufügen · Korrigieren · Transfer",
   350|            "_numeric_value_chf": dec_text(value, "0.01"),
   351|            "_wallet_id": wallet["wallet_id"],
   352|        }
   353|        if admin_mode:
   354|            item["wallet_id"] = wallet["wallet_id"]
   355|        rows.append(item)
   356|    return rows
   357|
   358|
   359|def get_wallet_coin_details(conn: Connection, wallet_id: str, *, price_max_age_seconds: int = 86_400, admin_mode: bool = False) -> list[dict[str, str]]:
   360|    holdings = calculate_crypto_holdings(conn)
   361|    rows: list[dict[str, str]] = []
   362|    for (wid, asset_id), holding in sorted(holdings.wallet_holdings.items()):
   363|        if wid != wallet_id or holding.quantity == 0:
   364|            continue
   365|        asset = conn.execute("SELECT coin_name, symbol FROM crypto_assets WHERE asset_id=?", (asset_id,)).fetchone()
   366|        price = latest_crypto_price(conn, asset_id, "CHF", max_age_seconds=price_max_age_seconds)
   367|        price_dec = d(price["price"]) if price and price["price"] else None
   368|        value = holding.quantity * price_dec if price_dec is not None else None
   369|        item = {
   370|            "coin": asset["coin_name"] if asset else asset_id,
   371|            "symbol": asset["symbol"] if asset else "",
   372|            "menge": dec_text(holding.quantity),
   373|            "wert_chf": chf_text(value) if value is not None else "nicht bewertet",
   374|            "status": translate_status(price_status(price)),
   375|            "actions": "Bestand korrigieren · Transfer aus Wallet · Audit anzeigen",
   376|            "_asset_id": asset_id,
   377|            "_wallet_id": wallet_id,
   378|        }
   379|        if admin_mode:
   380|            item.update({"asset_id": asset_id, "wallet_id": wallet_id})
   381|        rows.append(item)
   382|    return rows
   383|
   384|
   385|def get_crypto_coin_allocation_chart(conn: Connection, *, price_max_age_seconds: int = 86_400) -> list[dict[str, str]]:
   386|    rows = _crypto_value_rows(conn, price_max_age_seconds=price_max_age_seconds)
   387|    out = []
   388|    for r in sorted(rows, key=lambda item: item["value"] or Decimal("-1"), reverse=True):
   389|        if r["value"] is not None:
   390|            out.append({"label": str(r["coin"]), "value_chf": dec_text(r["value"], "0.01")})
   391|    return out
   392|
   393|
   394|def get_crypto_wallet_allocation_chart(conn: Connection, *, price_max_age_seconds: int = 86_400) -> list[dict[str, str]]:
   395|    rows = get_wallet_user_overview(conn, price_max_age_seconds=price_max_age_seconds)
   396|    out = []
   397|    for row in rows:
   398|        value = row["_numeric_value_chf"]
   399|        if d(value) > 0:
   400|            out.append({"label": row["wallet_name"], "value_chf": dec_text(value, "0.01")})
   401|    return sorted(out, key=lambda item: d(item["value_chf"]), reverse=True)
   402|
   403|
   404|def get_crypto_coin_detail(conn: Connection, asset_id: str, *, price_max_age_seconds: int = 86_400) -> dict[str, object]:
   405|    row = next((r for r in _crypto_value_rows(conn, price_max_age_seconds=price_max_age_seconds) if r["asset_id"] == asset_id), None)
   406|    if not row:
   407|        return {}
   408|    all_value = sum((r["value"] for r in _crypto_value_rows(conn, price_max_age_seconds=price_max_age_seconds) if r["value"] is not None), Decimal("0"))
   409|    value = row["value"]
   410|    share = (value / all_value * Decimal("100")) if value is not None and all_value else None
   411|    latest_price = latest_crypto_price(conn, asset_id, "CHF", max_age_seconds=price_max_age_seconds)
   412|    return {
   413|        "coin": str(row["coin"]),
   414|        "symbol": str(row["symbol"]),
   415|        "gesamtmenge": dec_text(row["quantity"]),
   416|        "gesamtwert_chf": chf_text(value) if value is not None else "nicht bewertet",
   417|        "kurs_chf": chf_text(row["price"]) if row["price"] is not None else "nicht bewertet",
   418|        "anteil": percent_text(share),
   419|        "preisstatus": translate_status(str(row["price_status"])),
   420|        "letzte_preisaktualisierung": date_text(str(row["last_price_update"])),
   421|        "preisquelle": str(latest_price.get("provider") or "unbekannt") if latest_price else "unbekannt",
   422|        "cache_status": "lokaler Cache" if latest_price else "kein lokaler Preis",
   423|        "wallets": get_crypto_coin_wallet_details(conn, asset_id, price_max_age_seconds=price_max_age_seconds),
   424|        "actions": ["Bestand hinzufügen", "Bestand korrigieren", "Transfer", "Bestand auf 0 setzen", "Audit / Verlauf anzeigen"],
   425|    }
   426|
   427|
   428|def get_wallet_detail(conn: Connection, wallet_id: str, *, price_max_age_seconds: int = 86_400) -> dict[str, object]:
   429|    wallet = next((w for w in get_wallet_user_overview(conn, price_max_age_seconds=price_max_age_seconds, admin_mode=True) if w.get("wallet_id") == wallet_id), None)
   430|    if not wallet:
   431|        return {}
   432|    return {
   433|        "wallet_name": wallet["wallet_name"],
   434|        "typ": wallet["wallet_type"],
   435|        "provider": wallet["provider"],
   436|        "gesamtwert_chf": wallet["total_value_chf"],
   437|        "coins": get_wallet_coin_details(conn, wallet_id, price_max_age_seconds=price_max_age_seconds),
   438|        "letzte_verifikation": wallet["last_verified_at"],
   439|        "notizen": user_note(wallet.get("notes", "")),
   440|        "status": wallet["status"],
   441|        "actions": ["Coin in Wallet hinzufügen", "Bestand korrigieren", "Transfer aus Wallet", "Wallet verifizieren", "Wallet bearbeiten", "Wallet deaktivieren"],
   442|    }
   443|
   444|
   445|def get_crypto_overview(conn: Connection, *, price_max_age_seconds: int = 86_400) -> list[dict[str, str]]:
   446|    holdings = calculate_crypto_holdings(conn)
   447|    rows: list[dict[str, str]] = []
   448|    for asset_id, holding in sorted(holdings.total_by_asset.items()):
   449|        qty = holding.quantity
   450|        asset = conn.execute("SELECT * FROM crypto_assets WHERE asset_id=?", (asset_id,)).fetchone()
   451|        if not asset:
   452|            continue
   453|        price = latest_crypto_price(conn, asset_id, "CHF", max_age_seconds=price_max_age_seconds)
   454|        price_dec = d(price["price"]) if price and price["price"] else None
   455|        value = qty * price_dec if price_dec is not None else None
   456|        rows.append({
   457|            "asset_id": asset_id,
   458|            "coin_name": asset["coin_name"],
   459|            "symbol": asset["symbol"],
   460|            "coingecko_id": asset["coingecko_id"] or "",
   461|            "quantity": dec_text(qty),
   462|            "latest_price_chf": dec_text(price_dec, "0.01") if price_dec is not None else "",
   463|            "market_value_chf": dec_text(value, "0.01") if value is not None else "",
   464|            "price_quality": price["quality_status"] if price else "missing",
   465|            "fetched_at": price["fetched_at"] if price else "",
   466|        })
   467|    return rows
   468|
   469|
   470|def get_crypto_by_wallet(conn: Connection, *, price_max_age_seconds: int = 86_400) -> list[dict[str, str]]:
   471|    holdings = calculate_crypto_holdings(conn)
   472|    rows: list[dict[str, str]] = []
   473|    for (wallet_id, asset_id), holding in sorted(holdings.wallet_holdings.items()):
   474|        wallet = conn.execute("SELECT wallet_name, wallet_type, platform_provider FROM crypto_wallets WHERE wallet_id=?", (wallet_id,)).fetchone()
   475|        asset = conn.execute("SELECT coin_name, symbol, coingecko_id FROM crypto_assets WHERE asset_id=?", (asset_id,)).fetchone()
   476|        price = latest_crypto_price(conn, asset_id, "CHF", max_age_seconds=price_max_age_seconds)
   477|        price_dec = d(price["price"]) if price and price["price"] else None
   478|        value = holding.quantity * price_dec if price_dec is not None else None
   479|        rows.append({
   480|            "wallet_id": wallet_id,
   481|            "wallet_name": wallet["wallet_name"] if wallet else wallet_id,
   482|            "wallet_type": wallet["wallet_type"] if wallet else "",
   483|            "provider": wallet["platform_provider"] if wallet else "",
   484|            "coin_name": asset["coin_name"] if asset else asset_id,
   485|            "symbol": asset["symbol"] if asset else "",
   486|            "coingecko_id": asset["coingecko_id"] if asset else "",
   487|            "quantity": dec_text(holding.quantity),
   488|            "market_value_chf": dec_text(value, "0.01") if value is not None else "",
   489|            "wallet_value_chf": dec_text(value, "0.01") if value is not None else "",
   490|            "price_quality": price["quality_status"] if price else "missing",
   491|            "price_fetched_at": price["fetched_at"] if price else "",
   492|            "verification_status": holding.verification_status,
   493|            "last_verified_at": holding.last_verified_at or "",
   494|        })
   495|    return rows
   496|
   497|
   498|def get_wallets(conn: Connection) -> list[dict[str, str]]:
   499|    return rowdicts(conn.execute(
   500|        """
   501|        SELECT wallet_id, wallet_name, wallet_type, platform_provider AS provider,
   502|               network_chain, last_verified_at, is_active, notes
   503|        FROM crypto_wallets ORDER BY wallet_name
   504|        """
   505|    ).fetchall())
   506|
   507|
   508|def get_latest_crypto_transactions(conn: Connection, limit: int = 20) -> list[dict[str, str]]:
   509|    return rowdicts(conn.execute(
   510|        """
   511|        SELECT ct.transaction_datetime, ct.transaction_type, ca.symbol, ct.quantity, ct.fee_quantity,
   512|               fw.wallet_name AS from_wallet, tw.wallet_name AS to_wallet, ct.currency_original,
   513|               ct.amount_chf, ct.confirmation_status, ct.source
   514|        FROM crypto_transactions ct
   515|        LEFT JOIN crypto_assets ca ON ca.asset_id=ct.asset_id
   516|        LEFT JOIN crypto_wallets fw ON fw.wallet_id=ct.from_wallet_id
   517|        LEFT JOIN crypto_wallets tw ON tw.wallet_id=ct.to_wallet_id
   518|        ORDER BY ct.transaction_datetime DESC, ct.created_at DESC LIMIT ?
   519|        """, (limit,)
   520|    ).fetchall())
   521|
   522|
   523|def get_platform_overview(conn: Connection) -> list[dict[str, str]]:
   524|    return rowdicts(conn.execute(
   525|        """
   526|        SELECT p.name AS platform, p.platform_type, a.account_name, a.account_type, a.currency,
   527|               a.performance_included, a.is_active
   528|        FROM accounts a JOIN platforms p ON p.platform_id=a.platform_id
   529|        ORDER BY p.name, a.account_name
   530|

Use offset=530 to continue reading (showing 300-529 of 1122 lines)


   220|        "last_price_update_at": _latest_price_time(conn),
   221|        "data_quality_status": status,
   222|    }
   223|
   224|
   225|def _wallet_names_for_asset(conn: Connection, asset_id: str) -> list[str]:
   226|    holdings = calculate_crypto_holdings(conn)
   227|    names: list[str] = []
   228|    for (wallet_id, aid), holding in holdings.wallet_holdings.items():
   229|        if aid == asset_id and holding.quantity != 0:
   230|            row = conn.execute("SELECT wallet_name FROM crypto_wallets WHERE wallet_id=?", (wallet_id,)).fetchone()
   231|            if row:
   232|                names.append(row["wallet_name"])
   233|    return sorted(names)
   234|
   235|
   236|def get_crypto_coin_summary(
   237|    conn: Connection,
   238|    *,
   239|    admin_mode: bool = False,
   240|    price_max_age_seconds: int = 86_400,
   241|    sort_by: str = "Wert absteigend",
   242|    search: str | None = None,
   243|    status_filter: str | None = None,
   244|    wallet_filter: str | None = None,
   245|) -> list[dict[str, str]]:
   246|    raw = _crypto_value_rows(conn, price_max_age_seconds=price_max_age_seconds)
   247|    total = sum((r["value"] for r in raw if r["value"] is not None), Decimal("0"))
   248|    out: list[dict[str, str]] = []
   249|    for r in sorted(raw, key=lambda item: item["value"] or Decimal("-1"), reverse=True):
   250|        value = r["value"]
   251|        share = ((value / total) * Decimal("100")) if value is not None and total else None
   252|        item = {
   253|            "coin": str(r["coin"]),
   254|            "symbol": str(r["symbol"]),
   255|            "gesamtmenge": dec_text(r["quantity"]),
   256|            "kurs_chf": chf_text(r["price"]) if r["price"] is not None else "nicht bewertet",
   257|            "gesamtwert_chf": chf_text(value) if value is not None else "nicht bewertet",
   258|            "anteil": percent_text(share),
   259|            "wallets": str(r["wallet_count"]),
   260|            "status": translate_status(str(r["price_status"])),
   261|            "_sort_value_chf": dec_text(value or Decimal("0"), "0.01"),
   262|            "_last_price_update": date_text(str(r["last_price_update"])),
   263|            "_asset_id": str(r["asset_id"]),
   264|            "_wallet_names": " | ".join(_wallet_names_for_asset(conn, str(r["asset_id"]))),
   265|        }
   266|        if admin_mode:
   267|            item.update({"asset_id": str(r["asset_id"]), "coingecko_id": str(r["coingecko_id"]), "price_status": str(r["price_status"]), "last_price_update": str(r["last_price_update"])})
   268|        out.append(item)
   269|    if search:
   270|        needle = search.lower()
   271|        out = [row for row in out if needle in row["coin"].lower() or needle in row["symbol"].lower()]
   272|    if status_filter and status_filter != "Alle":
   273|        out = [row for row in out if row["status"] == status_filter]
   274|    if wallet_filter and wallet_filter != "Alle":
   275|        out = [row for row in out if wallet_filter in row.get("_wallet_names", "")]
   276|    sorters = {
   277|        "Wert absteigend": lambda row: (d(row["_sort_value_chf"]), row["coin"]),
   278|        "Wert aufsteigend": lambda row: (d(row["_sort_value_chf"]), row["coin"]),
   279|        "Name A–Z": lambda row: row["coin"].lower(),
   280|        "Anteil": lambda row: (d(row["anteil"].replace("%", "")), row["coin"]),
   281|        "Wallet-Anzahl": lambda row: (int(row["wallets"]), row["coin"]),
   282|        "Status": lambda row: (row["status"], row["coin"]),
   283|    }
   284|    reverse = sort_by in {"Wert absteigend", "Anteil", "Wallet-Anzahl"}
   285|    return sorted(out, key=sorters.get(sort_by, sorters["Wert absteigend"]), reverse=reverse)
   286|
   287|
   288|def get_crypto_coin_wallet_details(conn: Connection, asset_id: str, *, price_max_age_seconds: int = 86_400, admin_mode: bool = False) -> list[dict[str, str]]:
   289|    holdings = calculate_crypto_holdings(conn)
   290|    price = latest_crypto_price(conn, asset_id, "CHF", max_age_seconds=price_max_age_seconds)
   291|    price_dec = d(price["price"]) if price and price["price"] else None
   292|    total_qty = sum((h.quantity for (wid, aid), h in holdings.wallet_holdings.items() if aid == asset_id), Decimal("0"))
   293|    rows: list[dict[str, str]] = []
   294|    for (wallet_id, aid), holding in sorted(holdings.wallet_holdings.items()):
   295|        if aid != asset_id or holding.quantity == 0:
   296|            continue
   297|        wallet = conn.execute("SELECT wallet_name, wallet_type, platform_provider, notes FROM crypto_wallets WHERE wallet_id=?", (wallet_id,)).fetchone()
   298|        value = holding.quantity * price_dec if price_dec is not None else None
   299|        share = (holding.quantity / total_qty * Decimal("100")) if total_qty else None
   300|        item = {
   301|            "wallet": wallet["wallet_name"] if wallet else wallet_id,
   302|            "typ": wallet["wallet_type"] if wallet else "",
   303|            "menge": dec_text(holding.quantity),
   304|            "wert_chf": chf_text(value) if value is not None else "nicht bewertet",
   305|            "anteil": percent_text(share),
   306|            "letzte_verifikation": date_text(holding.last_verified_at),
   307|            "status": translate_status(holding.verification_status),
   308|            "notiz": user_note(wallet["notes"] if wallet and wallet["notes"] else ""),
   309|            "_wallet_id": wallet_id,
   310|            "_asset_id": asset_id,
   311|        }
   312|        if admin_mode:
   313|            item.update({"wallet_id": wallet_id, "asset_id": asset_id})
   314|        rows.append(item)
   315|    return rows
   316|
   317|
   318|def get_wallet_overview_cards(conn: Connection, *, price_max_age_seconds: int = 86_400) -> dict[str, str]:
   319|    rows = get_wallet_user_overview(conn, price_max_age_seconds=price_max_age_seconds)
   320|    total_value = sum((d(r["_numeric_value_chf"]) for r in rows), Decimal("0"))
   321|    missing_verification = sum(1 for r in rows if not r.get("last_verified_at"))
   322|    return {"wallet_count": str(len(rows)), "total_value_chf": dec_text(total_value), "wallets_missing_verification": str(missing_verification)}
   323|
   324|
   325|def get_wallet_user_overview(conn: Connection, *, price_max_age_seconds: int = 86_400, admin_mode: bool = False) -> list[dict[str, str]]:
   326|    holdings = calculate_crypto_holdings(conn)
   327|    rows: list[dict[str, str]] = []
   328|    for wallet in conn.execute("SELECT * FROM crypto_wallets ORDER BY wallet_name").fetchall():
   329|        value = Decimal("0")
   330|        has_missing_price = False
   331|        coins: set[str] = set()
   332|        for (wallet_id, asset_id), holding in holdings.wallet_holdings.items():
   333|            if wallet_id != wallet["wallet_id"] or holding.quantity == 0:
   334|                continue
   335|            coins.add(asset_id)
   336|            price = latest_crypto_price(conn, asset_id, "CHF", max_age_seconds=price_max_age_seconds)
   337|            if price and price.get("price"):
   338|                value += holding.quantity * d(price["price"])
   339|            else:
   340|                has_missing_price = True
   341|        item = {
   342|            "wallet_name": wallet["wallet_name"],
   343|            "wallet_type": wallet["wallet_type"],
   344|            "provider": wallet["platform_provider"] or "",
   345|            "total_value_chf": chf_text(value) if not has_missing_price else f"{chf_text(value)} + unbewertet",
   346|            "coin_count": str(len(coins)),
   347|            "last_verified_at": date_text(wallet["last_verified_at"]),
   348|            "status": translate_status("active" if int(wallet["is_active"] or 0) else "inactive"),
   349|            "actions": "Öffnen · Coin hinzufügen · Korrigieren · Transfer",
   350|            "_numeric_value_chf": dec_text(value, "0.01"),
   351|            "_wallet_id": wallet["wallet_id"],
   352|        }
   353|        if admin_mode:
   354|            item["wallet_id"] = wallet["wallet_id"]
   355|        rows.append(item)
   356|    return rows
   357|
   358|
   359|def get_wallet_coin_details(conn: Connection, wallet_id: str, *, price_max_age_seconds: int = 86_400, admin_mode: bool = False) -> list[dict[str, str]]:
   360|    holdings = calculate_crypto_holdings(conn)
   361|    rows: list[dict[str, str]] = []
   362|    for (wid, asset_id), holding in sorted(holdings.wallet_holdings.items()):
   363|        if wid != wallet_id or holding.quantity == 0:
   364|            continue
   365|        asset = conn.execute("SELECT coin_name, symbol FROM crypto_assets WHERE asset_id=?", (asset_id,)).fetchone()
   366|        price = latest_crypto_price(conn, asset_id, "CHF", max_age_seconds=price_max_age_seconds)
   367|        price_dec = d(price["price"]) if price and price["price"] else None
   368|        value = holding.quantity * price_dec if price_dec is not None else None
   369|        item = {
   370|            "coin": asset["coin_name"] if asset else asset_id,
   371|            "symbol": asset["symbol"] if asset else "",
   372|            "menge": dec_text(holding.quantity),
   373|            "wert_chf": chf_text(value) if value is not None else "nicht bewertet",
   374|            "status": translate_status(price_status(price)),
   375|            "actions": "Bestand korrigieren · Transfer aus Wallet · Audit anzeigen",
   376|            "_asset_id": asset_id,
   377|            "_wallet_id": wallet_id,
   378|        }
   379|        if admin_mode:
   380|            item.update({"asset_id": asset_id, "wallet_id": wallet_id})
   381|        rows.append(item)
   382|    return rows
   383|
   384|
   385|def get_crypto_coin_allocation_chart(conn: Connection, *, price_max_age_seconds: int = 86_400) -> list[dict[str, str]]:
   386|    rows = _crypto_value_rows(conn, price_max_age_seconds=price_max_age_seconds)
   387|    out = []
   388|    for r in sorted(rows, key=lambda item: item["value"] or Decimal("-1"), reverse=True):
   389|        if r["value"] is not None:
   390|            out.append({"label": str(r["coin"]), "value_chf": dec_text(r["value"], "0.01")})
   391|    return out
   392|
   393|
   394|def get_crypto_wallet_allocation_chart(conn: Connection, *, price_max_age_seconds: int = 86_400) -> list[dict[str, str]]:
   395|    rows = get_wallet_user_overview(conn, price_max_age_seconds=price_max_age_seconds)
   396|    out = []
   397|    for row in rows:
   398|        value = row["_numeric_value_chf"]
   399|        if d(value) > 0:
   400|            out.append({"label": row["wallet_name"], "value_chf": dec_text(value, "0.01")})
   401|    return sorted(out, key=lambda item: d(item["value_chf"]), reverse=True)
   402|
   403|
   404|def get_crypto_coin_detail(conn: Connection, asset_id: str, *, price_max_age_seconds: int = 86_400) -> dict[str, object]:
   405|    row = next((r for r in _crypto_value_rows(conn, price_max_age_seconds=price_max_age_seconds) if r["asset_id"] == asset_id), None)
   406|    if not row:
   407|        return {}
   408|    all_value = sum((r["value"] for r in _crypto_value_rows(conn, price_max_age_seconds=price_max_age_seconds) if r["value"] is not None), Decimal("0"))
   409|    value = row["value"]
   410|    share = (value / all_value * Decimal("100")) if value is not None and all_value else None
   411|    latest_price = latest_crypto_price(conn, asset_id, "CHF", max_age_seconds=price_max_age_seconds)
   412|    return {
   413|        "coin": str(row["coin"]),
   414|        "symbol": str(row["symbol"]),
   415|        "gesamtmenge": dec_text(row["quantity"]),
   416|        "gesamtwert_chf": chf_text(value) if value is not None else "nicht bewertet",
   417|        "kurs_chf": chf_text(row["price"]) if row["price"] is not None else "nicht bewertet",
   418|        "anteil": percent_text(share),
   419|        "preisstatus": translate_status(str(row["price_status"])),
   420|        "letzte_preisaktualisierung": date_text(str(row["last_price_update"])),
   421|        "preisquelle": str(latest_price.get("provider") or "unbekannt") if latest_price else "unbekannt",
   422|        "cache_status": "lokaler Cache" if latest_price else "kein lokaler Preis",
   423|        "wallets": get_crypto_coin_wallet_details(conn, asset_id, price_max_age_seconds=price_max_age_seconds),
   424|        "actions": ["Bestand hinzufügen", "Bestand korrigieren", "Transfer", "Bestand auf 0 setzen", "Audit / Verlauf anzeigen"],
   425|    }
   426|
   427|
   428|def get_wallet_detail(conn: Connection, wallet_id: str, *, price_max_age_seconds: int = 86_400) -> dict[str, object]:
   429|    wallet = next((w for w in get_wallet_user_overview(conn, price_max_age_seconds=price_max_age_seconds, admin_mode=True) if w.get("wallet_id") == wallet_id), None)
   430|    if not wallet:
   431|        return {}
   432|    return {
   433|        "wallet_name": wallet["wallet_name"],
   434|        "typ": wallet["wallet_type"],
   435|        "provider": wallet["provider"],
   436|        "gesamtwert_chf": wallet["total_value_chf"],
   437|        "coins": get_wallet_coin_details(conn, wallet_id, price_max_age_seconds=price_max_age_seconds),
   438|        "letzte_verifikation": wallet["last_verified_at"],
   439|        "notizen": user_note(wallet.get("notes", "")),
   440|        "status": wallet["status"],
   441|        "actions": ["Coin in Wallet hinzufügen", "Bestand korrigieren", "Transfer aus Wallet", "Wallet verifizieren", "Wallet bearbeiten", "Wallet deaktivieren"],
   442|    }
   443|
   444|
   445|def get_crypto_overview(conn: Connection, *, price_max_age_seconds: int = 86_400) -> list[dict[str, str]]:
   446|    holdings = calculate_crypto_holdings(conn)
   447|    rows: list[dict[str, str]] = []
   448|    for asset_id, holding in sorted(holdings.total_by_asset.items()):
   449|        qty = holding.quantity
   450|

Use offset=450 to continue reading (showing 220-449 of 1122 lines)


  1030|        "data_location": "Echte Daten liegen außerhalb Git",
  1031|    }
  1032|
  1033|
  1034|def get_broker_review_items(conn: Connection, limit: int = 100, **filters) -> list[dict[str, str]]:
  1035|    from jarvis_finance.imports.broker_mapping import get_broker_review_items as _items
  1036|
  1037|    return _items(conn, limit=limit, **filters)
  1038|
  1039|
  1040|def get_review_readiness_counts(conn: Connection) -> list[dict[str, str]]:
  1041|    from jarvis_finance.imports.broker_mapping import get_review_readiness_counts as _counts
  1042|
  1043|    return _counts(conn)
  1044|
  1045|
  1046|def get_ready_for_import_by_source(conn: Connection) -> list[dict[str, str]]:
  1047|    from jarvis_finance.imports.broker_mapping import get_ready_for_import_by_source as _counts
  1048|
  1049|    return _counts(conn)
  1050|
  1051|
  1052|def get_review_quality_flag_counts(conn: Connection) -> list[dict[str, str]]:
  1053|    from jarvis_finance.imports.broker_mapping import aggregate_review_quality_flags
  1054|
  1055|    return [{"quality_flag": k, "open_items": str(v)} for k, v in sorted(aggregate_review_quality_flags(conn).items())]
  1056|
  1057|
  1058|def equity_etf_user_label(summary: dict[str, str]) -> str:
  1059|    value = d(summary.get("equity_etf_total_chf"))
  1060|    return chf_text(value) if value > 0 else "noch nicht bewertet"
  1061|
  1062|
  1063|def equity_etf_is_user_ready(summary: dict[str, str]) -> bool:
  1064|    return d(summary.get("equity_etf_total_chf")) > 0 and summary.get("data_quality_status") == "ok"
  1065|
  1066|
  1067|def get_top_user_hints(conn: Connection, limit: int = 3) -> list[str]:
  1068|    hints: list[str] = []
  1069|    summary = get_command_center_summary(conn)
  1070|    if equity_etf_user_label(summary) == "noch nicht bewertet":
  1071|        hints.append("Aktien/ETF: Bewertung unvollständig")
  1072|    crypto = get_crypto_summary_cards(conn)
  1073|    if d(crypto.get("coins_without_price")) > 0:
  1074|        hints.append("Crypto-Preise fehlen für einzelne Coins")
  1075|    elif d(crypto.get("stale_price_count")) > 0:
  1076|        hints.append("Crypto-Preise teilweise veraltet")
  1077|    else:
  1078|        hints.append("Crypto-Preise aktuell")
  1079|    for alert in get_alert_cards(conn, limit=limit):
  1080|        text = alert["title"]
  1081|        if not any(marker in text for marker in ("asset_id", "wallet_id", "instrument-", "rule_id", "entity_id")):
  1082|            hints.append(text)
  1083|        if len(hints) >= limit:
  1084|            break
  1085|    return hints[:limit]
  1086|
  1087|
  1088|def compact_chart_rows(rows: list[dict[str, str]], *, limit: int = 10) -> list[dict[str, str]]:
  1089|    ordered = sorted(rows, key=lambda row: d(row.get("value_chf")), reverse=True)
  1090|    if len(ordered) <= limit:
  1091|        return ordered
  1092|    head = ordered[:limit]
  1093|    other = sum((d(row.get("value_chf")) for row in ordered[limit:]), Decimal("0"))
  1094|    if other > 0:
  1095|        head.append({"label": "Andere", "value_chf": dec_text(other, "0.01")})
  1096|    return head
  1097|
  1098|
  1099|def get_command_center_summary(conn: Connection) -> dict[str, str]:
  1100|    cash_rows = get_cash_overview(conn)
  1101|    crypto_summary = get_crypto_summary_cards(conn)
  1102|    positions = get_positions(conn)
  1103|    cash_total = sum(d(r["amount_chf"]) for r in cash_rows if r["amount_chf"])
  1104|    crypto_total = d(crypto_summary["total_value_chf"])
  1105|    equity_total = sum(d(r["market_value_chf"]) for r in positions if r.get("market_value_chf"))
  1106|    alert_counts = conn.execute("SELECT priority, COUNT(*) AS c FROM alerts WHERE status='active' GROUP BY priority").fetchall()
  1107|    critical = sum(int(r["c"] or 0) for r in alert_counts if r["priority"] == "kritisch")
  1108|    quality = get_data_quality(conn)
  1109|    dq = "kritisch" if any(i["severity"] == "kritisch" and i["count"] != "0" for i in quality) else ("warnung" if any(i["severity"] == "warnung" and i["count"] != "0" for i in quality) else "ok")
  1110|    return {
  1111|        "base_currency": "CHF",
  1112|        "cash_total_chf": dec_text(cash_total, "0.01"),
  1113|        "crypto_total_chf": dec_text(crypto_total, "0.01"),
  1114|        "equity_etf_total_chf": dec_text(equity_total, "0.01"),
  1115|        "total_portfolio_chf": dec_text(cash_total + crypto_total + equity_total, "0.01"),
  1116|        "total_value_chf": dec_text(cash_total + crypto_total + equity_total, "0.01"),
  1117|        "critical_alert_count": str(critical),
  1118|        "active_alerts": ", ".join(f"{r['priority']}: {r['c']}" for r in alert_counts) or "0",
  1119|        "data_quality_status": dq,
  1120|        "last_price_update_at": crypto_summary.get("last_price_update_at", ""),
  1121|        "crypto_price_data_as_of": crypto_summary.get("last_price_update_at", ""),
  1122|    }
  1123|


     1|from __future__ import annotations
     2|
     3|
     4|def edit_mode_enabled(st) -> bool:
     5|    try:
     6|        return bool(st.session_state.get("jarvis_edit_mode", False))
     7|    except Exception:
     8|        return False
     9|
    10|
    11|def read_only_hint(st) -> None:
    12|    st.info("Bearbeiten aktivieren, um diese Aktion auszuführen.")
    13|
    14|
    15|def action_button(st, label: str, *, key: str | None = None, edit_required: bool = True) -> bool:
    16|    disabled = edit_required and not edit_mode_enabled(st)
    17|    clicked = st.button(label, key=key, disabled=disabled)
    18|    if disabled:
    19|        return False
    20|    return bool(clicked)
    21|
    22|
    23|def open_crypto_manage(st, action: str, *, asset_id: str | None = None, wallet_id: str | None = None) -> None:
    24|    st.session_state["crypto_manage_action"] = action
    25|    if asset_id:
    26|        st.session_state["crypto_manage_asset_id"] = asset_id
    27|        st.session_state["selected_crypto_asset_id"] = asset_id
    28|    if wallet_id:
    29|        st.session_state["crypto_manage_wallet_id"] = wallet_id
    30|        st.session_state["selected_crypto_wallet_id"] = wallet_id
    31|


from pathlib import Path
p=Path('/home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/data.py')
s=p.read_text()
s=s.replace('''            "platform": row["platform"] if row else "",\n            "account_name": row["account_name"] if row else account_id,\n            "asset_class": row["asset_class"] if row else "",''','''            "account_id": account_id,\n            "instrument_id": instrument_id,\n            "platform": row["platform"] if row else "",\n            "account_name": row["account_name"] if row else account_id,\n            "asset_class": row["asset_class"] if row else "",''')
s=s.replace('''        SELECT priority, category, entity_type, entity_id, rule_id, message, occurrence_count, last_seen_at\n        FROM alerts WHERE status='active' ''','''        SELECT alert_id, priority, category, entity_type, entity_id, rule_id, message, occurrence_count, last_seen_at\n        FROM alerts WHERE status='active' ''')
s=s.replace('''        cards.append({\n            "title": alert_title(row["rule_id"], row["message"], affected),''','''        cards.append({\n            "_alert_id": row["alert_id"],\n            "title": alert_title(row["rule_id"], row["message"], affected),''')
s=s.replace('''        "cache_status": "lokaler Cache" if latest_price else "kein lokaler Preis",\n        "wallets": get_crypto_coin_wallet_details(conn, asset_id, price_max_age_seconds=price_max_age_seconds),''','''        "cache_status": "lokaler Cache" if latest_price else "kein lokaler Preis",\n        "coingecko_link": f"https://www.coingecko.com/en/coins/{row['coingecko_id']}" if row.get("coingecko_id") else "",\n        "wallets": get_crypto_coin_wallet_details(conn, asset_id, price_max_age_seconds=price_max_age_seconds),''')
p.write_text(s)
print('done')


Output: done


patch

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

PAGE_TITLE = "Alerts"

def render(st, conn) -> None:
    st.title("Alerts")
    st.caption("Lesbare Warnkarten im User Mode. Raw rule IDs und Dedup-Details bleiben unter Admin.")
    summary = data.get_alert_summary_cards(conn)
    c1, c2, c3 = st.columns(3)
    c1.metric("Kritisch", summary["kritisch"])
    c2.metric("Wichtig", summary["wichtig"])
    c3.metric("Info", summary["info"])
    for card in data.get_alert_cards(conn, limit=20):
        st.markdown(f"### {card['severity']}: {card['title']}")
        st.write(card["description"])
        st.caption(f"Betroffen: {card['affected']} · zuletzt gesehen: {card['last_seen']}")
        cols = st.columns(3)
        for col, action in zip(cols, ["Prüfen", "Ignorieren", "Details"]):
            col.button(action)
    with st.expander("Admin: technische Alert-Tabelle anzeigen"):
        show_table(st, data.get_alerts(conn))

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

PAGE_TITLE = "Alerts"

def _alert_button_key(card: dict[str, str], index: int, action: str) -> str:
    alert_id = card.get("_alert_id") or f"idx-{index}"
    return f"alert_{alert_id}_{action.lower()}".replace(" ", "_").replace("/", "_")

def render(st, conn) -> None:
    st.title("Alerts")
    admin_mode = bool(getattr(st, "session_state", {}).get("jarvis_admin_mode", False))
    st.caption("Lesbare Warnkarten im User Mode. Raw rule IDs und Dedup-Details bleiben unter Admin.")
    summary = data.get_alert_summary_cards(conn)
    c1, c2, c3 = st.columns(3)
    c1.metric("Kritisch", summary["kritisch"])
    c2.metric("Wichtig", summary["wichtig"])
    c3.metric("Info", summary["info"])
    for index, card in enumerate(data.get_alert_cards(conn, limit=20)):
        st.markdown(f"### {card['severity']}: {card['title']}")
        st.write(card["description"])
        st.caption(f"Betroffen: {card['affected']} · zuletzt gesehen: {card['last_seen']}")
        cols = st.columns(3)
        for col, action in zip(cols, ["Prüfen", "Ignorieren", "Details"]):
            col.button(action, key=_alert_button_key(card, index, action))
    if admin_mode:
        with st.expander("Admin: technische Alert-Tabelle anzeigen"):
            show_table(st, data.get_alerts(conn))


--- a//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/views/page_09_alerts.py
+++ b//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/views/page_09_alerts.py
@@ -6,20 +6,27 @@
 PAGE_TITLE = "Alerts"

+def _alert_button_key(card: dict[str, str], index: int, action: str) -> str:
+    alert_id = card.get("_alert_id") or f"idx-{index}"
+    return f"alert_{alert_id}_{action.lower()}".replace(" ", "_").replace("/", "_")
+
+
 def render(st, conn) -> None:
     st.title("Alerts")
+    admin_mode = bool(getattr(st, "session_state", {}).get("jarvis_admin_mode", False))
     st.caption("Lesbare Warnkarten im User Mode. Raw rule IDs und Dedup-Details bleiben unter Admin.")
     summary = data.get_alert_summary_cards(conn)
     c1, c2, c3 = st.columns(3)
     c1.metric("Kritisch", summary["kritisch"])
     c2.metric("Wichtig", summary["wichtig"])
     c3.metric("Info", summary["info"])
-    for card in data.get_alert_cards(conn, limit=20):
+    for index, card in enumerate(data.get_alert_cards(conn, limit=20)):
         st.markdown(f"### {card['severity']}: {card['title']}")
         st.write(card["description"])
         st.caption(f"Betroffen: {card['affected']} · zuletzt gesehen: {card['last_seen']}")
         cols = st.columns(3)
         for col, action in zip(cols, ["Prüfen", "Ignorieren", "Details"]):
-            col.button(action)
-    with st.expander("Admin: technische Alert-Tabelle anzeigen"):
-        show_table(st, data.get_alerts(conn))
+            col.button(action, key=_alert_button_key(card, index, action))
+    if admin_mode:
+        with st.expander("Admin: technische Alert-Tabelle anzeigen"):
+            show_table(st, data.get_alerts(conn))


from __future__ import annotations

from decimal import Decimal

from jarvis_finance.dashboard.actions import action_button, edit_mode_enabled, read_only_hint
from jarvis_finance.dashboard.components.tables import show_table
from jarvis_finance.dashboard import data
from jarvis_finance.equity.manage import add_equity_transaction
from jarvis_finance.market_data.prices import equity_price_provider_by_name, refresh_market_prices

PAGE_TITLE = "Equities/ETFs"

def _go_to_add_position(st) -> None:
    if hasattr(st, "session_state"):
        st.session_state["jarvis_requested_page"] = "Position hinzufügen"

def _equity_rows(conn) -> list[dict]:
    return [r for r in data.get_positions(conn) if str(r.get("asset_class", "")).lower() in {"stock", "equity", "etf"} and data.d(r.get("quantity")) != 0]

def _asset_type(row: dict) -> str:
    return "ETF" if str(row.get("asset_class", "")).lower() == "etf" else "Aktie"

def _status(row: dict) -> str:
    warnings = set(str(row.get("quality_warnings") or "").split(","))
    if not row.get("market_price_original"):
        return "Preis fehlt"
    if "missing_fx" in warnings or row.get("fx_status") == "missing_fx" or (row.get("price_currency") not in {"", "CHF"} and not row.get("market_value_chf")):
        return "FX fehlt"
    if "cost_basis_uncertain" in warnings or not row.get("cost_basis_chf"):
        return "Einstand unvollständig"
    if row.get("market_value_chf"):
        return "Bewertet"
    return "Unvollständig"

def _market_value(row: dict) -> str:
    return data.chf_text(row.get("market_value_chf")) if row.get("market_value_chf") else "nicht bewertet"

def _price(row: dict) -> str:
    return row.get("market_price_original") or "Preis fehlt"

def _table_row(row: dict) -> dict[str, str]:
    return {
        "Name": row.get("name", ""),
        "Ticker": row.get("ticker", ""),
        "ISIN": row.get("isin", ""),
        "Depot": row.get("account_name", ""),
        "Assettyp": _asset_type(row),
        "Menge": row.get("quantity", ""),
        "Kurs": _price(row),
        "Kurswährung": row.get("price_currency") or row.get("currency") or "",
        "Marktwert CHF": _market_value(row),
        "Status": _status(row),
    }

def _sort_key(label: str):
    if label == "Marktwert CHF absteigend":
        return lambda r: (data.d(r.get("market_value_chf")), r.get("name", "").lower())
    if label == "Name A–Z":
        return lambda r: r.get("name", "").lower()
    if label == "Depot":
        return lambda r: (r.get("account_name", ""), r.get("name", ""))
    if label == "Assettyp":
        return lambda r: (_asset_type(r), r.get("name", ""))
    if label == "Status":
        return lambda r: (_status(r), r.get("name", ""))
    if label == "Währung":
        return lambda r: (r.get("price_currency") or r.get("currency") or "", r.get("name", ""))
    return lambda r: r.get("name", "").lower()

def _summary_cards(st, rows: list[dict]) -> None:
    missing_price = sum(1 for r in rows if not r.get("market_price_original"))
    missing_fx = sum(1 for r in rows if _status(r) == "FX fehlt")
    incomplete_cost = sum(1 for r in rows if "cost_basis_uncertain" in str(r.get("quality_warnings") or "") or not r.get("cost_basis_chf"))
    values = [data.d(r.get("market_value_chf")) for r in rows if r.get("market_value_chf")]
    latest_price_update = max((str(r.get("price_date") or "") for r in rows), default="") or "kein Preisupdate"
    c1, c2, c3, c4, c5, c6 = st.columns(6)
    c1.metric("Gesamtwert Aktien/ETFs CHF", data.chf_text(sum(values, Decimal("0"))) if values and not missing_price and not missing_fx else "Bewertung unvollständig")
    c2.metric("Anzahl Positionen", str(len(rows)))
    c3.metric("Positionen ohne Preis", str(missing_price))
    c4.metric("Positionen ohne FX", str(missing_fx))
    c5.metric("Einstand unvollständig", str(incomplete_cost))
    c6.metric("Letztes Preisupdate", latest_price_update)

def _render_detail(st, conn, row: dict) -> None:
    st.subheader("Detailpanel")
    detail = {
        "Name": row.get("name", ""),
        "ISIN": row.get("isin", ""),
        "Ticker": row.get("ticker", ""),
        "Depot": row.get("account_name", ""),
        "Assettyp": _asset_type(row),
        "Menge": row.get("quantity", ""),
        "Währung": row.get("currency", ""),
        "Kurs": _price(row),
        "Marktwert CHF": _market_value(row),
        "Preisstatus": "Bewertet" if row.get("market_price_original") else "Preis fehlt",
        "FX-Status": "OK" if _status(row) != "FX fehlt" else "FX fehlt",
        "Einstandsstatus": "Einstand unvollständig" if "cost_basis_uncertain" in str(row.get("quality_warnings") or "") or not row.get("cost_basis_chf") else "OK",
    }
    st.write(detail)
    a, b, c, d, e, f = st.columns(6)
    if a.button("Details", key="equity_action_details"):
        st.session_state["equity_active_action"] = "Details"
    if action_button(b, "Korrigieren", key="equity_action_correct", edit_required=True):
        st.session_state["equity_active_action"] = "Korrigieren"
    if c.button("Verlauf", key="equity_action_history"):
        st.session_state["equity_active_action"] = "Verlauf"
    d.button("Dividende/Ausschüttung", key="equity_action_dividend", disabled=True, help="kommt später")
    e.button("Kaufen", key="equity_action_buy", disabled=True, help="kommt später")
    f.button("Verkaufen", key="equity_action_sell", disabled=True, help="kommt später")
    active = st.session_state.get("equity_active_action", "Details") if hasattr(st, "session_state") else "Details"
    if active == "Korrigieren":
        if not edit_mode_enabled(st):
            read_only_hint(st)
            return
        st.markdown("### Korrigieren — Inline-Formular")
        st.caption("Auditierbare manuelle Korrektur; keine Löschung, keine Admin-Suche nötig.")
        qty = st.text_input("Korrektur-Menge als Decimal", key="equity_correct_qty")
        gross = st.text_input("Korrektur-Betrag original", key="equity_correct_gross")
        note = st.text_area("Korrektur-Notiz Pflicht", key="equity_correct_note")
        confirm = st.checkbox("Ich bestätige die Korrektur.", key="equity_correct_confirm")
        if st.button("Korrektur speichern", key="equity_correct_save"):
            try:
                result = add_equity_transaction(conn, account_id=row["account_id"], instrument_id=row["instrument_id"], transaction_type="manual_adjustment", trade_date=str(row.get("price_date") or "2026-01-01"), quantity_text=qty, gross_amount_original_text=gross or "0", currency=row.get("currency") or "CHF", note=note, confirm=confirm, fx_status="not_needed" if (row.get("currency") or "CHF") == "CHF" else "missing")
                st.success(f"Korrektur auditiert: {result.transaction_id}")
            except Exception as exc:
                st.error(f"Korrektur abgelehnt: {exc}")
    elif active == "Verlauf":
        st.markdown("### Verlauf")
        tx = data.rowdicts(conn.execute("SELECT trade_date, transaction_type, quantity, currency_original, fx_status, quality_status, notes FROM transactions WHERE account_id=? AND instrument_id=? ORDER BY trade_date DESC, created_at DESC", (row["account_id"], row["instrument_id"])).fetchall())
        audit = data.rowdicts(conn.execute("SELECT timestamp, source, action, user_text_note, created_by FROM audit_log WHERE entity_id IN (SELECT transaction_id FROM transactions WHERE account_id=? AND instrument_id=?) ORDER BY timestamp DESC LIMIT 50", (row["account_id"], row["instrument_id"])).fetchall())
        show_table(st, tx, empty_message="Kein Transaktionsverlauf vorhanden.")
        show_table(st, audit, empty_message="Kein Audit-Verlauf vorhanden.")
    else:
        st.info("Details werden oben im Detailpanel angezeigt.")

def render(st, conn) -> None:
    st.title("Aktien & ETFs")
    st.caption("Bedienbare User-Ansicht für manuell erfasste Aktien-/ETF- und Cash-nahe Portfolio-Positionen. Keine DOCX-Review-Rohdaten im Alltag.")
    rows = _equity_rows(conn)
    _summary_cards(st, rows)

    if st.button("Bewertung aktualisieren", key="equity_refresh_valuation"):
        stock = refresh_market_prices(conn, provider=equity_price_provider_by_name("auto"), asset_class="stock", only_missing=True)
        etf = refresh_market_prices(conn, provider=equity_price_provider_by_name("auto"), asset_class="etf", only_missing=True)
        st.success(f"Bewertung aktualisiert: Aktien/ETF aktualisiert={stock.updated_count + etf.updated_count}, cached={stock.cached_count + etf.cached_count}, warnings={stock.warning_count + etf.warning_count}, errors={stock.error_count + etf.error_count}")
        rows = _equity_rows(conn)

    if not rows:
        st.info("Noch keine Aktien oder ETFs erfasst.")
        if st.button("Aktie/ETF hinzufügen", key="equity_empty_add_position", on_click=_go_to_add_position, args=(st,)):
            st.success("Öffne im User Mode die Seite 'Position hinzufügen'.")
        return

    sort_by = st.selectbox("Sortierung", ["Marktwert CHF absteigend", "Name A–Z", "Depot", "Assettyp", "Status", "Währung"], key="equity_sort_by")
    rows = sorted(rows, key=_sort_key(sort_by), reverse=sort_by == "Marktwert CHF absteigend")
    view = st.radio("Ansicht", ["Nach Anbieter / Depot", "Nach Assettyp", "Alle Positionen"], key="equity_view")

    if view == "Nach Anbieter / Depot":
        groups = ["PostFinance", "Raiffeisen", "True Wealth", "Anderes Konto"]
        for group in groups:
            group_rows = [r for r in rows if group.lower() in (r.get("platform", "") + " " + r.get("account_name", "")).lower()]
            if group_rows:
                st.markdown(f"### {group}")
                show_table(st, [_table_row(r) for r in group_rows])
    elif view == "Nach Assettyp":
        cash_rows = data.get_cash_overview(conn)
        if cash_rows:
            st.markdown("### Cash")
            show_table(st, [{"Name": r["account_name"], "Depot": r["platform"], "Assettyp": "Cash", "Menge": r["amount_original"], "Kurswährung": r["currency"], "Marktwert CHF": data.chf_text(r["amount_chf"]) if r.get("amount_chf") else "Bewertung unvollständig", "Status": data.translate_status(r.get("quality_status"))} for r in cash_rows])
        for label in ["Aktie", "ETF"]:
            group_rows = [r for r in rows if _asset_type(r) == label]
            if group_rows:
                st.markdown(f"### {label if label == 'Aktie' else 'ETFs'}")
                show_table(st, [_table_row(r) for r in group_rows])
    else:
        st.markdown("### Alle Positionen")
        show_table(st, [_table_row(r) for r in rows])

    st.subheader("Position auswählen")
    labels = [f"{r.get('name')} | {r.get('ticker') or 'Ticker fehlt'} | {r.get('account_name')}" for r in rows]
    selected_label = st.selectbox("Position auswählen", labels, key="equity_selected_position")
    selected = rows[labels.index(selected_label)] if selected_label in labels else rows[0]
    _render_detail(st, conn, selected)

    st.subheader("Weitere Aktion")
    if st.button("Aktie/ETF hinzufügen", key="equity_add_position", on_click=_go_to_add_position, args=(st,)):
        st.success("Öffne im User Mode die Seite 'Position hinzufügen'.")


Explanation:
**Evaluating cash metrics**

I need to address the Command Center cash metric and focus on top equity. It seems like I should also consider adding in the cash type for positions and the FX messaging. Additionally, I’ll need to review the ledger for cash details. This all feels important for keeping things organized and ensuring that I’m looking at the right financial data clearly!


     1|from __future__ import annotations
     2|
     3|from dataclasses import dataclass, field
     4|from decimal import Decimal
     5|from sqlite3 import Connection
     6|
     7|from jarvis_finance.audit.log import record_audit_event
     8|from jarvis_finance.imports.common import stable_id, utc_now
     9|from jarvis_finance.quality.alerts import create_alert
    10|
    11|
    12|@dataclass
    13|class CashBalance:
    14|    account_id: str
    15|    currency: str
    16|    amount_original: Decimal = Decimal("0")
    17|    amount_chf: Decimal | None = Decimal("0")
    18|    quality_status: str = "ok"
    19|
    20|
    21|@dataclass
    22|class CashCalculationResult:
    23|    balances: dict[tuple[str, str], CashBalance] = field(default_factory=dict)
    24|    negative_cash_warnings: list[str] = field(default_factory=list)
    25|
    26|
    27|def _d(value: object, default: str = "0") -> Decimal:
    28|    if value is None or str(value).strip() == "":
    29|        return Decimal(default)
    30|    return Decimal(str(value))
    31|
    32|
    33|def _signed_cash_delta(row) -> tuple[Decimal, Decimal | None]:
    34|    t = row["transaction_type"]
    35|    gross = _d(row["gross_amount_original"])
    36|    net = _d(row["net_amount_original"]) if row["net_amount_original"] is not None else None
    37|    fee = _d(row["fee_original"])
    38|    tax = _d(row["tax_original"])
    39|    fx = _d(row["fx_rate_to_chf"], "1") if row["fx_rate_to_chf"] is not None else None
    40|
    41|    if t == "initial_cash_snapshot":
    42|        original = net if net is not None else gross
    43|    elif t == "cash_deposit":
    44|        original = net if net is not None else gross
    45|    elif t == "cash_withdrawal":
    46|        original = -(net if net is not None else gross)
    47|    elif t == "buy":
    48|        original = -(net if net is not None else gross + fee + tax)
    49|    elif t in {"partial_sell", "full_sell"}:
    50|        original = net if net is not None else gross - fee - tax
    51|    elif t in {"dividend", "etf_distribution"}:
    52|        original = net if net is not None else gross - tax - fee
    53|    elif t in {"fee", "tax"}:
    54|        original = -(net if net is not None else gross or fee or tax)
    55|    elif t == "fx_conversion":
    56|        original = net if net is not None else gross
    57|    elif t == "manual_cash_correction":
    58|        original = net if net is not None else gross
    59|    else:
    60|        original = Decimal("0")
    61|    chf = original * fx if fx is not None else None
    62|    return original, chf
    63|
    64|
    65|def calculate_cash_balances(conn: Connection, *, as_of_date: str | None = None) -> CashCalculationResult:
    66|    result = CashCalculationResult()
    67|    params: tuple[object, ...] = ()
    68|    where = "WHERE COALESCE(is_voided, 0) = 0"
    69|    if as_of_date:
    70|        where += " AND trade_date <= ?"
    71|        params = (as_of_date,)
    72|    rows = conn.execute(
    73|        f"""
    74|        SELECT * FROM transactions
    75|        {where}
    76|        ORDER BY account_id, currency_original, trade_date, created_at, transaction_id
    77|        """,
    78|        params,
    79|    ).fetchall()
    80|    for row in rows:
    81|        account_id = row["account_id"]
    82|        currency = row["currency_original"]
    83|        key = (account_id, currency)
    84|        balance = result.balances.setdefault(key, CashBalance(account_id, currency))
    85|        original, chf = _signed_cash_delta(row)
    86|        balance.amount_original += original
    87|        if chf is None:
    88|            balance.amount_chf = None
    89|            balance.quality_status = "incomplete"
    90|        elif balance.amount_chf is not None:
    91|            balance.amount_chf += chf
    92|    for key, balance in result.balances.items():
    93|        if balance.amount_original < 0:
    94|            msg = f"Negative cash balance for account={balance.account_id} currency={balance.currency}: {balance.amount_original}"
    95|            result.negative_cash_warnings.append(msg)
    96|            create_alert(
    97|                conn,
    98|                priority="warnung",
    99|                category="cash",
   100|                entity_type="account",
   101|                entity_id=balance.account_id,
   102|                rule_id="negative_cash",
   103|                message=msg,
   104|                evidence={"currency": balance.currency, "amount_original": str(balance.amount_original)},
   105|            )
   106|    conn.commit()
   107|    return result
   108|
   109|
   110|def apply_manual_cash_correction(
   111|    conn: Connection,
   112|    *,
   113|    account_id: str,
   114|    currency: str,
   115|    amount: Decimal,
   116|    note: str,
   117|    trade_date: str | None = None,
   118|) -> str:
   119|    if not note.strip():
   120|        raise ValueError("manual cash correction requires a note")
   121|    now = utc_now()
   122|    trade_date = trade_date or now[:10]
   123|    transaction_id = stable_id("cashcorr", account_id, currency, trade_date, amount, note)
   124|    fx_rate = Decimal("1") if currency.upper() == "CHF" else None
   125|    conn.execute(
   126|        """
   127|        INSERT INTO transactions(
   128|            transaction_id, transaction_type, account_id, trade_date, gross_amount_original,
   129|            net_amount_original, currency_original, fx_rate_to_chf, fx_status,
   130|            gross_amount_chf, net_amount_chf, source_type, is_confirmed,
   131|            quality_status, notes, created_at
   132|        ) VALUES (?, 'manual_cash_correction', ?, ?, ?, ?, ?, ?, ?, ?, ?, 'manual', 1, ?, ?, ?)
   133|        """,
   134|        (
   135|            transaction_id, account_id, trade_date, str(amount), str(amount), currency.upper(),
   136|            str(fx_rate) if fx_rate is not None else None,
   137|            "ok" if fx_rate is not None else "missing",
   138|            str(amount * fx_rate) if fx_rate is not None else None,
   139|            str(amount * fx_rate) if fx_rate is not None else None,
   140|            "ok" if fx_rate is not None else "incomplete",
   141|            note,
   142|            now,
   143|        ),
   144|    )
   145|    audit_id = record_audit_event(
   146|        conn,
   147|        source="manual_cash_correction",
   148|        action="manual_cash_correction",
   149|        entity_type="transaction",
   150|        entity_id=transaction_id,
   151|        new_values={"account_id": account_id, "currency": currency.upper(), "amount": str(amount)},
   152|        user_text_note=note,
   153|        confirmed=True,
   154|        created_by="system",
   155|    )
   156|    conn.commit()
   157|    return audit_id
   158|


[Your active task list was preserved across context compression]
- [>] redtests. Regressionstests für Alerts, Navigation, Cleanup, FX, Equity-Interaktion, Cash und no-live-render ergänzen und RED verifizieren (in_progress)
- [ ] fixalertsnav. Alerts-Duplicate-Button-Keys reparieren und User/Admin-Navigation strikt vereinfachen (pending)
- [ ] cleanup. Runtime-Backup erstellen, Review/Test/DOCX Equity-Artefakte trocken identifizieren und eindeutig auditierbar entfernen (pending)
- [ ] fixfxcash. FX-UX und Cash-Workflow nutzerfreundlich/funktional machen (pending)
- [ ] fixequityui. Aktien/ETF-Seite mit Summary Cards, Gruppierung, Sortierung, Auswahl, Details, Korrigieren, Verlauf umbauen (pending)
- [ ] fixcommandcrypto. Command Center und minimalen CoinGecko-Link korrigieren (pending)
- [ ] uisanity. Dashboard erneut gegen Runtime-DB starten und User-Mode-Seiten aggregiert prüfen (pending)
- [ ] verify. Compile, pytest, Git-Safety, git diff --check, Status verifizieren (pending)
- [ ] commitpush. Commit, Push und remote Hash verifizieren (pending)


     1|from __future__ import annotations
     2|
     3|from dataclasses import dataclass
     4|from sqlite3 import Connection
     5|
     6|from jarvis_finance.audit.log import record_audit_event
     7|from jarvis_finance.imports.common import utc_now
     8|
     9|REVIEW_TOKENS = ("review", "docx", "test", "demo", "synthetic", "dryrun", "dry run", "wizard-test", "wizard test")
    10|REVIEW_SOURCE_TYPES = ("broker_import_reviewed_snapshot", "broker_import_dry_run", "docx_import", "wizard_test", "test", "demo", "synthetic", "dryrun")
    11|
    12|
    13|@dataclass(frozen=True)
    14|class CleanupPlan:
    15|    account_ids: list[str]
    16|    instrument_ids: list[str]
    17|    transaction_ids: list[str]
    18|    alert_ids: list[str]
    19|    manual_position_count: int
    20|
    21|    @property
    22|    def artifact_count(self) -> int:
    23|        return len(self.account_ids) + len(self.instrument_ids) + len(self.transaction_ids)
    24|
    25|
    26|def _token_sql(alias: str, columns: list[str]) -> str:
    27|    parts = []
    28|    for col in columns:
    29|        for token in REVIEW_TOKENS:
    30|            parts.append(f"lower(coalesce({alias}.{col},'')) LIKE '%{token}%'")
    31|    return " OR ".join(parts) or "0"
    32|
    33|
    34|def identify_review_test_equity_artifacts(conn: Connection) -> CleanupPlan:
    35|    """Find clearly non-manual review/test/DOCX equity artifacts without exposing values.
    36|
    37|    Conservative by design: manual_dashboard/manual_position_add data is excluded unless the
    38|    account itself is clearly a review/test account. Crypto tables are never touched.
    39|    """
    40|    account_where = _token_sql("a", ["account_name", "notes"])
    41|    platform_where = _token_sql("p", ["name", "notes"])
    42|    account_rows = conn.execute(
    43|        f"""
    44|        SELECT DISTINCT a.account_id
    45|        FROM accounts a JOIN platforms p ON p.platform_id=a.platform_id
    46|        WHERE ({account_where} OR {platform_where})
    47|          AND EXISTS (
    48|              SELECT 1 FROM transactions t JOIN instruments i ON i.instrument_id=t.instrument_id
    49|              WHERE t.account_id=a.account_id AND lower(i.asset_class) IN ('stock','equity','etf')
    50|          )
    51|        """
    52|    ).fetchall()
    53|    account_ids = sorted({row["account_id"] for row in account_rows})
    54|
    55|    source_placeholders = ",".join("?" for _ in REVIEW_SOURCE_TYPES)
    56|    tx_rows = conn.execute(
    57|        f"""
    58|        SELECT DISTINCT t.transaction_id
    59|        FROM transactions t
    60|        JOIN instruments i ON i.instrument_id=t.instrument_id
    61|        LEFT JOIN accounts a ON a.account_id=t.account_id
    62|        WHERE lower(i.asset_class) IN ('stock','equity','etf')
    63|          AND coalesce(t.is_voided,0)=0
    64|          AND (
    65|              t.source_type IN ({source_placeholders})
    66|              OR lower(coalesce(t.source_type,'')) LIKE '%review%'
    67|              OR lower(coalesce(t.source_id,'')) LIKE '%review%'
    68|              OR lower(coalesce(t.notes,'')) LIKE '%review%'
    69|              OR lower(coalesce(t.notes,'')) LIKE '%docx%'
    70|              OR lower(coalesce(t.notes,'')) LIKE '%synthetic%'
    71|              OR lower(coalesce(t.notes,'')) LIKE '%demo%'
    72|              OR lower(coalesce(t.notes,'')) LIKE '%test%'
    73|              OR a.account_id IN ({','.join('?' for _ in account_ids) if account_ids else "''"})
    74|          )
    75|          AND t.source_type NOT IN ('manual_dashboard')
    76|        """,
    77|        (*REVIEW_SOURCE_TYPES, *account_ids),
    78|    ).fetchall()
    79|    transaction_ids = sorted({row["transaction_id"] for row in tx_rows})
    80|
    81|    # Instruments are removable only when all their open equity transactions are in the cleanup set.
    82|    if transaction_ids:
    83|        tx_ph = ",".join("?" for _ in transaction_ids)
    84|        instrument_rows = conn.execute(
    85|            f"""
    86|            SELECT i.instrument_id
    87|            FROM instruments i
    88|            WHERE lower(i.asset_class) IN ('stock','equity','etf')
    89|              AND EXISTS (SELECT 1 FROM transactions t WHERE t.instrument_id=i.instrument_id AND t.transaction_id IN ({tx_ph}))
    90|              AND NOT EXISTS (
    91|                  SELECT 1 FROM transactions t
    92|                  WHERE t.instrument_id=i.instrument_id
    93|                    AND coalesce(t.is_voided,0)=0
    94|                    AND t.transaction_id NOT IN ({tx_ph})
    95|              )
    96|            """,
    97|            (*transaction_ids, *transaction_ids),
    98|        ).fetchall()
    99|        instrument_ids = sorted({row["instrument_id"] for row in instrument_rows})
   100|    else:
   101|        instrument_ids = []
   102|
   103|    # Deactivate cleanup accounts only when no non-cleanup active transactions remain.
   104|    removable_accounts: list[str] = []
   105|    for account_id in account_ids:
   106|        remaining = conn.execute(
   107|            f"""
   108|            SELECT COUNT(*) AS c FROM transactions
   109|            WHERE account_id=? AND coalesce(is_voided,0)=0
   110|              {'AND transaction_id NOT IN (' + ','.join('?' for _ in transaction_ids) + ')' if transaction_ids else ''}
   111|            """,
   112|            (account_id, *transaction_ids),
   113|        ).fetchone()["c"]
   114|        if int(remaining or 0) == 0:
   115|            removable_accounts.append(account_id)
   116|
   117|    if instrument_ids:
   118|        inst_ph = ",".join("?" for _ in instrument_ids)
   119|        alert_rows = conn.execute(f"SELECT alert_id FROM alerts WHERE entity_id IN ({inst_ph})", tuple(instrument_ids)).fetchall()
   120|        alert_ids = sorted({row["alert_id"] for row in alert_rows})
   121|    else:
   122|        alert_ids = []
   123|
   124|    manual_position_count = int(conn.execute(
   125|        """
   126|        SELECT COUNT(*) AS c
   127|        FROM transactions t JOIN instruments i ON i.instrument_id=t.instrument_id
   128|        WHERE lower(i.asset_class) IN ('stock','equity','etf')
   129|          AND coalesce(t.is_voided,0)=0
   130|          AND t.source_type='manual_dashboard'
   131|        """
   132|    ).fetchone()["c"] or 0)
   133|    return CleanupPlan(removable_accounts, instrument_ids, transaction_ids, alert_ids, manual_position_count)
   134|
   135|
   136|def apply_review_test_equity_cleanup(conn: Connection, *, plan: CleanupPlan | None = None, note: str = "MVP Real UI Acceptance cleanup") -> str:
   137|    plan = plan or identify_review_test_equity_artifacts(conn)
   138|    now = utc_now()
   139|    if plan.transaction_ids:
   140|        ph = ",".join("?" for _ in plan.transaction_ids)
   141|        conn.execute(
   142|            f"UPDATE transactions SET is_voided=1, voided_at=?, void_reason=?, voided_by='dashboard_cleanup', updated_at=? WHERE transaction_id IN ({ph})",
   143|            (now, note, now, *plan.transaction_ids),
   144|        )
   145|    if plan.alert_ids:
   146|        ph = ",".join("?" for _ in plan.alert_ids)
   147|        conn.execute(
   148|            f"UPDATE alerts SET status='resolved', resolved_at=? WHERE alert_id IN ({ph})",
   149|            (now, *plan.alert_ids),
   150|        )
   151|    if plan.instrument_ids:
   152|        ph = ",".join("?" for _ in plan.instrument_ids)
   153|        conn.execute(f"UPDATE instruments SET is_active=0, updated_at=? WHERE instrument_id IN ({ph})", (now, *plan.instrument_ids))
   154|    if plan.account_ids:
   155|        ph = ",".join("?" for _ in plan.account_ids)
   156|        conn.execute(f"UPDATE accounts SET is_active=0, updated_at=? WHERE account_id IN ({ph})", (now, *plan.account_ids))
   157|    audit_id = record_audit_event(
   158|        conn,
   159|        source="runtime_cleanup",
   160|        action="archive_review_test_equity_artifacts",
   161|        entity_type="runtime_db",
   162|        entity_id="equity_review_test_artifacts",
   163|        old_values={},
   164|        new_values={
   165|            "accounts_archived": len(plan.account_ids),
   166|            "instruments_archived": len(plan.instrument_ids),
   167|            "transactions_voided": len(plan.transaction_ids),
   168|            "alerts_resolved": len(plan.alert_ids),
   169|            "manual_positions_preserved": plan.manual_position_count,
   170|        },
   171|        user_text_note=note,
   172|        confirmed=True,
   173|        created_by="dashboard_cleanup",
   174|    )
   175|    conn.commit()
   176|    return audit_id
   177|