## Audit result Audited exact tree `b3c11a2a7ed3a5f1e8620fdfb94e355a20807038`. No files were modified by me. ### Exact frontend change map - **`frontend/src/pages/CryptoPage.vue`** - Current reload control: `LoadActionBar` at line 3 calls `loadPositions(true)`. - Current initial/read reload bundle: `loadPositions()` at lines 144 and 154: - `GET /api/health` - `GET /api/runtime/status` - `GET /api/crypto/positions` - `GET /api/wallets` - `GET /api/crypto/reconciliation` - `GET /api/crypto/quality-summary` - Make the current action explicitly **“Ansicht neu laden”** and keep it GET-only. - Add a distinct **“Kryptokurse aktualisieren”** action with independent `marketLoading`, result, and error state. It should call the crypto-specific POST, then `loadPositions(true)`. - Do **not** call the all-assets `AssetRefreshControl` from this page: it refreshes equity, crypto, and FX together and therefore does not satisfy separation. - Replace raw/environment-dependent timestamps at lines 17, 25, 30, 73, 80 and 83–89 with one Swiss formatter. - Fix `latestPriceUpdate` at line 138: it currently takes the first timestamp from rows sorted by **market value**, not the chronologically latest timestamp. - Replace the generic `PositionTable` at line 43 with a local rich table, patterned after `EquityPage.vue`, if sortable formatted cells and inline sparklines are required. - Mobile cards at lines 37–41 need the same essential market fields as desktop and keyboard semantics. - **`frontend/src/api/crypto.ts`** - Existing explicit provider/write path: - `updateAllCryptoLiveStats()` → `POST /api/market/crypto/update-live-stats` - `updateCryptoLiveStats()` → same endpoint with `asset_id`. - Change the batch helper’s return type from the current incomplete inline shape to the existing `MarketBatchUpdateResponse`. - Accept bounded request options rather than only provider/limit/currency, e.g. `interval`, `only_missing`, and a backend-defined freshness threshold. - `getCryptoChart()` is a GET of stored points; `{ refresh: true }` only bypasses browser cache. - **`frontend/src/api/types.ts`** - Existing `CryptoPosition` lacks both 24h change and row-level history. - Minimal summary-contract additions if the table must show those without N+1 requests: - `change_24h_pct: string | null` - `price_history: PricePoint[]` or a bounded `sparkline_points: PricePoint[]` - Reuse existing `PricePoint` and `MarketBatchUpdateResponse`. - `last_price_update` already provides the per-row freshness timestamp. - Do not fetch `CryptoPositionDetail` once per table row; that would create an avoidable N+1 GET pattern. - **`frontend/src/utils/formatters.ts`** - Reuse existing `formatMoney`, `formatPercent`, and `formatQuantity`. - Add/export a shared datetime formatter using: - locale `de-CH` - `timeZone: 'Europe/Zurich'` - explicit date/time styles - deterministic fallback for null/invalid values. - Existing local examples with the correct timezone are in: - `WealthCockpitPanel.vue:121` - `PortfolioPerformancePanel.vue` - `DataIngestionReconciliationPanel.vue` - **`frontend/src/components/SparklineChart.vue`** - Reusable for `PricePoint[]`, but currently: - requires at least three positive points; - silently drops zero/negative/invalid points; - has no visible trend value or timestamp; - uses a full-width bordered container unsuitable for a compact table cell. - Minimal enhancement: `compact` prop, fixed accessible label, no outer card in compact mode. Preserve the current detail-drawer default. - **`frontend/src/components/PositionTable.vue`** - Currently supports only raw string cells and row selection. - It has no sorting, custom cell slots, numeric alignment, `aria-sort`, or embedded component support. - Extending it is possible but affects `WalletsPage.vue` and `ReportsPage.vue`; the lower-risk change is a crypto-local table copied from the established `EquityPage.vue:59–79, 140–169` pattern. ### Refresh versus reload Recommended UI behavior: 1. **Ansicht neu laden** - Calls only the six existing GET endpoints with cache bypass. - Never invokes a provider or writes server-side data. - Label must not imply market-price refresh. 2. **Kryptokurse aktualisieren** - Calls `POST /api/market/crypto/update-live-stats` exactly once. - On success, reloads the six stored-data GET surfaces. - Shows `updated`, `skipped`, warnings/errors and completion timestamp. - Must not start `/api/market/asset-price-refresh`, because that includes equity and FX. `AssetRefreshControl.vue` should remain the wealth-wide control. Its hardcoded button, copy, sources, and `startAssetRefreshJob()` payload are intentionally all-assets. ### Important 15-minute contract gap The frontend cannot currently guarantee a “refresh only when older than 15 minutes” policy: - `QuoteRefreshRequest.interval = '15m'` only labels the stored chart point. - `refresh_crypto_quotes_batch()` currently ignores `only_missing` and `stale_before`. - `AssetRefreshRequest.stale_hours` accepts integer hours only, minimum `1`. Therefore: - If **15m means chart-point interval**, the frontend can send `interval: '15m'`, but it does not enforce freshness. - If **15m means refresh eligibility/throttling**, the backend must first enforce a 15-minute stale cutoff. Do not implement a client-clock-only gate; reloads, multiple tabs, and direct API calls would bypass it. ### Table/mobile recommendation Suggested desktop columns: 1. Coin / symbol 2. Kurs CHF 3. 24h 4. Menge 5. Marktwert CHF 6. Portfolioanteil 7. Wallets 8. Datenstand 9. Status 10. Trend Default sort: market value descending. Sorting should be numeric for price, change, quantity, value, share, and wallets; chronological for data timestamp; locale-aware for name/symbol. Null values should remain last in both directions rather than becoming `0`. Mobile cards should show at minimum: - coin and symbol; - market value; - CHF price; - 24h change; - quantity and portfolio share; - Swiss-local data timestamp/status; - compact sparkline where enough points exist. ### GET/render read-only findings - `apiGet(..., { refresh: true })` only bypasses `sessionCache`; it does not add a backend refresh query or trigger providers. - The existing crypto positions, quality, wallet, reconciliation, detail, chart, health, and runtime GET routes read stored data. - `GET /api/market/asset-price-refresh/{job_id}` is explicitly stored-status-only. - The explicit provider call is the POST update endpoint. - One strict-network caveat: the detail drawer renders `coingecko_info.image_url` as a remote ``. That causes a browser-side external image request when the detail renders. If “no external request during render” is literal, proxy/cache or omit the logo; otherwise clarify that the guarantee concerns backend provider API calls. ### Minimal test changes - **`CryptoPage.test.ts`** - Mock `updateAllCryptoLiveStats`. - Assert mount performs no POST. - Assert “Ansicht neu laden” calls stored-data GETs with `true` and no POST. - Assert explicit crypto refresh calls exactly one POST, then reloads GET data. - Assert the expected 15m request field once backend semantics are finalized. - Test POST partial/error state without hiding existing rows. - Freeze a UTC timestamp and assert deterministic Europe/Zurich rendering. - Verify chronological latest-price selection. - Verify default sort, header sort toggles, numeric/null behavior. - Verify formatted desktop cells, mobile equivalents, and compact sparkline/empty state. - **`AssetRefreshControl.test.ts`** - Preserve current no-auto-start, polling, source isolation, and double-click tests. - Add an integration-level assertion that CryptoPage’s crypto-only action does **not** call `startAssetRefreshJob`. - No component rewrite is needed unless a backend source-filtered job contract is introduced. - **`MobileUx.test.ts`** - Extend the crypto fixture with 24h/history fields. - Assert cards contain price, 24h, timestamp/status, and accessible activation. - Retain the desktop-table-hidden assertion. - **API tests** - Add a request test for exact POST method/body and response typing. - Retain `client.test.ts` cache-bypass proof for GET reloads. - Assert GET/render paths never call the POST helper. ### Responsive/accessibility pitfalls - `md:hidden` / `md:block` switches to the desktop table at 768px; tablet/iPad widths therefore need horizontal overflow and a deliberate table `min-width`. - Current mobile crypto cards are click-only `
` elements, unlike Equity cards; add `role="button"`, `tabindex="0"`, Enter and Space handling, or use a real button. - Inline sparklines need fixed cell dimensions or they will force column expansion. - Add `min-w-0`, truncation, `tabular-nums`, right alignment, and `whitespace-nowrap` selectively. - Sort headers need real buttons plus `aria-sort`; arrow glyphs alone are insufficient. - Keep refresh/reload controls wrapping at narrow widths, with independent disabled states so reloading does not falsely display “provider refresh running.” ### Verification/issues - Targeted frontend tests could not start because this worktree lacks installed frontend dependencies (`vite` and `@vitejs/plugin-vue` unresolved). - A concurrent modification appeared during the audit in `src/jarvis_finance/services/crypto_market_recovery.py`; I did not create or alter it. - No files were created or modified by this audit.