## 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.